629 lines
25 KiB
JavaScript
629 lines
25 KiB
JavaScript
/*
|
|
Предрейсовые осмотры - мобильное приложение
|
|
Хук для управления авторизацией
|
|
*/
|
|
|
|
//---------------------
|
|
//Подключение библиотек
|
|
//---------------------
|
|
|
|
const React = require('react');
|
|
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider');
|
|
const { AUTH_SETTINGS_KEYS, DEFAULT_IDLE_TIMEOUT } = require('../config/authConfig');
|
|
const { ACTION_CODES, RESPONSE_STATES, ERROR_MESSAGES } = require('../config/authApi');
|
|
const { generateSecretKey, encryptData, decryptData } = require('../utils/secureStorage');
|
|
const { getPersistentDeviceId, isPersistentIdAvailable } = require('../utils/deviceId');
|
|
|
|
//-----------
|
|
//Тело модуля
|
|
//-----------
|
|
|
|
//Хук для управления авторизацией
|
|
function useAuth() {
|
|
const { getSetting, setSetting, getAuthSession, setAuthSession, clearAuthSession, isDbReady } = useAppLocalDbContext();
|
|
|
|
//Состояние авторизации
|
|
const [session, setSession] = React.useState(null);
|
|
const [isAuthenticated, setIsAuthenticated] = React.useState(false);
|
|
const [isLoading, setIsLoading] = React.useState(false);
|
|
const [isInitialized, setIsInitialized] = React.useState(false);
|
|
const [error, setError] = React.useState(null);
|
|
|
|
//Ссылка для контроллера отмены запросов
|
|
const abortControllerRef = React.useRef(null);
|
|
|
|
//Флаг: сессия восстановлена из хранилища при открытии приложения
|
|
const sessionRestoredFromStorageRef = React.useRef(false);
|
|
|
|
//Загрузка или получение постоянного идентификатора устройства
|
|
const getDeviceId = React.useCallback(async () => {
|
|
//Сначала проверяем сохранённый идентификатор
|
|
let deviceId = await getSetting(AUTH_SETTINGS_KEYS.DEVICE_ID);
|
|
|
|
if (deviceId) {
|
|
//Проверяем доступность постоянного идентификатора
|
|
const persistentAvailable = await isPersistentIdAvailable();
|
|
|
|
if (persistentAvailable) {
|
|
//Получаем постоянный идентификатор для сравнения
|
|
const persistentId = await getPersistentDeviceId();
|
|
|
|
//Если постоянный ID отличается - обновляем на постоянный
|
|
if (persistentId && !deviceId.includes('-') && deviceId !== persistentId) {
|
|
deviceId = persistentId;
|
|
await setSetting(AUTH_SETTINGS_KEYS.DEVICE_ID, deviceId);
|
|
}
|
|
}
|
|
|
|
return deviceId;
|
|
}
|
|
|
|
//Генерируем новый идентификатор
|
|
deviceId = await getPersistentDeviceId();
|
|
await setSetting(AUTH_SETTINGS_KEYS.DEVICE_ID, deviceId);
|
|
|
|
return deviceId;
|
|
}, [getSetting, setSetting]);
|
|
|
|
//Загрузка или генерация уникального секретного ключа устройства
|
|
const getSecretKey = React.useCallback(async () => {
|
|
let secretKey = await getSetting(AUTH_SETTINGS_KEYS.DEVICE_SECRET_KEY);
|
|
|
|
if (!secretKey) {
|
|
secretKey = generateSecretKey();
|
|
await setSetting(AUTH_SETTINGS_KEYS.DEVICE_SECRET_KEY, secretKey);
|
|
}
|
|
|
|
return secretKey;
|
|
}, [getSetting, setSetting]);
|
|
|
|
//Выполнение запроса к серверу
|
|
const executeRequest = React.useCallback(async (serverUrl, payload) => {
|
|
//Отменяем предыдущий запрос если есть
|
|
if (abortControllerRef.current) {
|
|
abortControllerRef.current.abort();
|
|
}
|
|
|
|
const abortController = new AbortController();
|
|
abortControllerRef.current = abortController;
|
|
|
|
try {
|
|
const response = await fetch(serverUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(payload),
|
|
signal: abortController.signal
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`${ERROR_MESSAGES.SERVER_ERROR}: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (fetchError) {
|
|
if (fetchError.name === 'AbortError') {
|
|
return null;
|
|
}
|
|
|
|
throw new Error(`${ERROR_MESSAGES.NETWORK_ERROR}: ${fetchError.message}`);
|
|
} finally {
|
|
abortControllerRef.current = null;
|
|
}
|
|
}, []);
|
|
|
|
//Сохранение credentials после успешной аутентификации
|
|
const saveCredentials = React.useCallback(
|
|
async (login, password, deviceId) => {
|
|
try {
|
|
//Получаем секретный ключ устройства
|
|
const secretKey = await getSecretKey();
|
|
|
|
//Шифруем пароль используя секретный ключ и deviceId как соль
|
|
const encryptedPassword = encryptData(password, secretKey, deviceId || '');
|
|
|
|
await setSetting(AUTH_SETTINGS_KEYS.SAVED_LOGIN, login);
|
|
await setSetting(AUTH_SETTINGS_KEYS.SAVED_PASSWORD, encryptedPassword);
|
|
await setSetting(AUTH_SETTINGS_KEYS.SAVE_PASSWORD_ENABLED, 'true');
|
|
|
|
return true;
|
|
} catch (saveError) {
|
|
console.error('Ошибка сохранения credentials:', saveError);
|
|
return false;
|
|
}
|
|
},
|
|
[setSetting, getSecretKey]
|
|
);
|
|
|
|
//Загрузка сохранённых credentials
|
|
const getSavedCredentials = React.useCallback(async () => {
|
|
try {
|
|
const savedLogin = await getSetting(AUTH_SETTINGS_KEYS.SAVED_LOGIN);
|
|
|
|
//Если нет сохранённого логина - возвращаем null
|
|
if (!savedLogin) {
|
|
return null;
|
|
}
|
|
|
|
const savePasswordEnabled = (await getSetting(AUTH_SETTINGS_KEYS.SAVE_PASSWORD_ENABLED)) === 'true';
|
|
|
|
//Если пароль не сохранялся - возвращаем только логин
|
|
if (!savePasswordEnabled) {
|
|
return {
|
|
login: savedLogin,
|
|
password: null,
|
|
savePasswordEnabled: false
|
|
};
|
|
}
|
|
|
|
const savedPassword = await getSetting(AUTH_SETTINGS_KEYS.SAVED_PASSWORD);
|
|
|
|
if (!savedPassword) {
|
|
return {
|
|
login: savedLogin,
|
|
password: null,
|
|
savePasswordEnabled: false
|
|
};
|
|
}
|
|
|
|
//Получаем deviceId и секретный ключ для расшифровки
|
|
const deviceId = await getDeviceId();
|
|
const secretKey = await getSecretKey();
|
|
|
|
//Расшифровываем пароль
|
|
const decryptedPassword = decryptData(savedPassword, secretKey, deviceId || '');
|
|
|
|
return {
|
|
login: savedLogin,
|
|
password: decryptedPassword,
|
|
savePasswordEnabled: true
|
|
};
|
|
} catch (loadError) {
|
|
console.error('Ошибка загрузки credentials:', loadError);
|
|
return null;
|
|
}
|
|
}, [getSetting, getDeviceId, getSecretKey]);
|
|
|
|
//Очистка сохранённых credentials
|
|
const clearSavedCredentials = React.useCallback(async () => {
|
|
try {
|
|
await setSetting(AUTH_SETTINGS_KEYS.SAVED_LOGIN, '');
|
|
await setSetting(AUTH_SETTINGS_KEYS.SAVED_PASSWORD, '');
|
|
await setSetting(AUTH_SETTINGS_KEYS.SAVE_PASSWORD_ENABLED, 'false');
|
|
return true;
|
|
} catch (clearError) {
|
|
console.error('Ошибка очистки credentials:', clearError);
|
|
return false;
|
|
}
|
|
}, [setSetting]);
|
|
|
|
//Вход в систему
|
|
const login = React.useCallback(
|
|
async ({ serverUrl, company, user, password, timeout, savePassword = false }) => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
//Получаем идентификатор устройства
|
|
const deviceId = await getDeviceId();
|
|
|
|
//Формируем запрос
|
|
const requestPayload = {
|
|
XREQUEST: {
|
|
XACTION: {
|
|
SCODE: ACTION_CODES.LOG_IN
|
|
},
|
|
XPAYLOAD: {
|
|
SUSER: user,
|
|
SPASSWORD: password,
|
|
STERMINAL: deviceId
|
|
}
|
|
}
|
|
};
|
|
|
|
//Добавляем организацию если указана
|
|
if (company) {
|
|
requestPayload.XREQUEST.XPAYLOAD.SCOMPANY = company;
|
|
}
|
|
|
|
//Добавляем таймаут (используем значение по умолчанию если не указан)
|
|
const timeoutValue = timeout && timeout > 0 ? timeout : DEFAULT_IDLE_TIMEOUT;
|
|
requestPayload.XREQUEST.XPAYLOAD.NTIMEOUT = timeoutValue;
|
|
|
|
//Выполняем запрос
|
|
const response = await executeRequest(serverUrl, requestPayload);
|
|
|
|
if (!response) {
|
|
return { success: false, error: ERROR_MESSAGES.NETWORK_ERROR };
|
|
}
|
|
|
|
//Проверяем ответ
|
|
if (!response.XRESPONSE) {
|
|
return { success: false, error: ERROR_MESSAGES.INVALID_RESPONSE };
|
|
}
|
|
|
|
const { SSTATE, XPAYLOAD } = response.XRESPONSE;
|
|
|
|
if (SSTATE !== RESPONSE_STATES.OK) {
|
|
return { success: false, error: XPAYLOAD?.SERROR || ERROR_MESSAGES.LOGIN_FAILED };
|
|
}
|
|
|
|
//Проверяем наличие списка организаций (требуется выбор)
|
|
if (XPAYLOAD.XCOMPANIES && Array.isArray(XPAYLOAD.XCOMPANIES) && XPAYLOAD.XCOMPANIES.length > 0 && !XPAYLOAD.XCOMPANY) {
|
|
//Преобразуем массив организаций
|
|
const organizations = XPAYLOAD.XCOMPANIES.map(item => item.XCOMPANY || item);
|
|
|
|
return {
|
|
success: true,
|
|
needSelectCompany: true,
|
|
sessionId: XPAYLOAD.SSESSION,
|
|
user: XPAYLOAD.XUSER,
|
|
organizations,
|
|
serverUrl,
|
|
savePassword,
|
|
//Передаём данные для сохранения credentials после выбора организации
|
|
loginCredentials: {
|
|
login: user,
|
|
password,
|
|
deviceId
|
|
}
|
|
};
|
|
}
|
|
|
|
//Сохраняем сессию
|
|
const sessionData = {
|
|
sessionId: XPAYLOAD.SSESSION,
|
|
serverUrl,
|
|
userRn: XPAYLOAD.XUSER?.NRN,
|
|
userCode: XPAYLOAD.XUSER?.SCODE,
|
|
userName: XPAYLOAD.XUSER?.SNAME,
|
|
companyRn: XPAYLOAD.XCOMPANY?.NRN,
|
|
companyName: XPAYLOAD.XCOMPANY?.SNAME,
|
|
savePassword
|
|
};
|
|
|
|
await setAuthSession(sessionData);
|
|
await setSetting(AUTH_SETTINGS_KEYS.SERVER_URL, serverUrl);
|
|
await setSetting(AUTH_SETTINGS_KEYS.SAVED_LOGIN, user);
|
|
|
|
if (savePassword) {
|
|
await saveCredentials(user, password, deviceId);
|
|
}
|
|
|
|
setSession(sessionData);
|
|
setIsAuthenticated(true);
|
|
sessionRestoredFromStorageRef.current = false;
|
|
|
|
return {
|
|
success: true,
|
|
needSelectCompany: false,
|
|
session: sessionData
|
|
};
|
|
} catch (loginError) {
|
|
const errorMessage = loginError.message || ERROR_MESSAGES.LOGIN_FAILED;
|
|
setError(errorMessage);
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
},
|
|
[executeRequest, getDeviceId, setAuthSession, setSetting, saveCredentials]
|
|
);
|
|
|
|
//Выбор организации после входа (SET_COMPANY)
|
|
const selectCompany = React.useCallback(
|
|
async ({ serverUrl, sessionId, user, company, savePassword, loginCredentials }) => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
//Формируем запрос SET_COMPANY
|
|
const requestPayload = {
|
|
XREQUEST: {
|
|
XACTION: {
|
|
SCODE: ACTION_CODES.SET_COMPANY
|
|
},
|
|
XPAYLOAD: {
|
|
SSESSION: sessionId,
|
|
NCOMPANY: company.NRN,
|
|
SCOMPANY: company.SNAME
|
|
}
|
|
}
|
|
};
|
|
|
|
//Выполняем запрос
|
|
const response = await executeRequest(serverUrl, requestPayload);
|
|
|
|
if (!response) {
|
|
return { success: false, error: ERROR_MESSAGES.NETWORK_ERROR };
|
|
}
|
|
|
|
//Проверяем ответ
|
|
if (!response.XRESPONSE) {
|
|
return { success: false, error: ERROR_MESSAGES.INVALID_RESPONSE };
|
|
}
|
|
|
|
const { SSTATE, XPAYLOAD } = response.XRESPONSE;
|
|
|
|
if (SSTATE !== RESPONSE_STATES.OK) {
|
|
return { success: false, error: XPAYLOAD?.SERROR || 'Ошибка установки организации' };
|
|
}
|
|
|
|
//Сохраняем сессию
|
|
const sessionData = {
|
|
sessionId,
|
|
serverUrl,
|
|
userRn: user?.NRN,
|
|
userCode: user?.SCODE,
|
|
userName: user?.SNAME,
|
|
companyRn: XPAYLOAD.XCOMPANY?.NRN || company.NRN,
|
|
companyName: XPAYLOAD.XCOMPANY?.SNAME || company.SNAME,
|
|
savePassword
|
|
};
|
|
|
|
await setAuthSession(sessionData);
|
|
await setSetting(AUTH_SETTINGS_KEYS.SERVER_URL, serverUrl);
|
|
const loginToSave = loginCredentials?.login || user?.SCODE || user?.SNAME || '';
|
|
if (loginToSave) {
|
|
await setSetting(AUTH_SETTINGS_KEYS.SAVED_LOGIN, loginToSave);
|
|
}
|
|
|
|
if (savePassword && loginCredentials) {
|
|
await saveCredentials(loginCredentials.login, loginCredentials.password, loginCredentials.deviceId);
|
|
}
|
|
|
|
setSession(sessionData);
|
|
setIsAuthenticated(true);
|
|
sessionRestoredFromStorageRef.current = false;
|
|
|
|
return {
|
|
success: true,
|
|
session: sessionData
|
|
};
|
|
} catch (selectError) {
|
|
const errorMessage = selectError.message || 'Ошибка установки организации';
|
|
setError(errorMessage);
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
},
|
|
[executeRequest, setAuthSession, setSetting, saveCredentials]
|
|
);
|
|
|
|
//Выход из системы
|
|
const logout = React.useCallback(
|
|
async (options = {}) => {
|
|
const { skipServerRequest = false } = options;
|
|
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const currentSession = session || (await getAuthSession());
|
|
|
|
//Запрос на сервер только при онлайн и если не отключено опцией
|
|
if (!skipServerRequest && currentSession?.sessionId && currentSession?.serverUrl) {
|
|
const requestPayload = {
|
|
XREQUEST: {
|
|
XACTION: {
|
|
SCODE: ACTION_CODES.LOG_OUT
|
|
},
|
|
XPAYLOAD: {
|
|
SSESSION: currentSession.sessionId
|
|
}
|
|
}
|
|
};
|
|
|
|
try {
|
|
await executeRequest(currentSession.serverUrl, requestPayload);
|
|
} catch (logoutError) {
|
|
console.warn('Ошибка при выходе из системы:', logoutError);
|
|
}
|
|
}
|
|
|
|
await clearAuthSession();
|
|
setSession(null);
|
|
setIsAuthenticated(false);
|
|
|
|
return { success: true };
|
|
} catch (logoutError) {
|
|
const errorMessage = logoutError.message || ERROR_MESSAGES.LOGOUT_FAILED;
|
|
setError(errorMessage);
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
},
|
|
[session, getAuthSession, executeRequest, clearAuthSession]
|
|
);
|
|
|
|
//Проверка сессии
|
|
const checkSession = React.useCallback(async () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
//Получаем сохраненную сессию
|
|
const savedSession = await getAuthSession();
|
|
|
|
if (!savedSession?.sessionId || !savedSession?.serverUrl) {
|
|
setSession(null);
|
|
setIsAuthenticated(false);
|
|
return { success: false, isOffline: false, error: null };
|
|
}
|
|
|
|
//Формируем запрос
|
|
const requestPayload = {
|
|
XREQUEST: {
|
|
XACTION: {
|
|
SCODE: ACTION_CODES.CHECK_SESSION
|
|
},
|
|
XPAYLOAD: {
|
|
SSESSION: savedSession.sessionId
|
|
}
|
|
}
|
|
};
|
|
|
|
//Выполняем запрос
|
|
let response = null;
|
|
try {
|
|
response = await executeRequest(savedSession.serverUrl, requestPayload);
|
|
} catch (requestError) {
|
|
const isNetworkError = requestError.message && String(requestError.message).indexOf(ERROR_MESSAGES.NETWORK_ERROR) !== -1;
|
|
if (isNetworkError) {
|
|
setSession(savedSession);
|
|
setIsAuthenticated(true);
|
|
return { success: true, isOffline: true, session: savedSession };
|
|
}
|
|
throw requestError;
|
|
}
|
|
|
|
if (!response) {
|
|
setSession(savedSession);
|
|
setIsAuthenticated(true);
|
|
return { success: true, isOffline: true, session: savedSession };
|
|
}
|
|
|
|
//Проверяем ответ: если сервер ответил, но структура некорректна — не считаем оффлайн
|
|
if (!response.XRESPONSE) {
|
|
return { success: false, isOffline: false, error: ERROR_MESSAGES.INVALID_RESPONSE };
|
|
}
|
|
|
|
const { SSTATE, XPAYLOAD } = response.XRESPONSE;
|
|
|
|
if (SSTATE !== RESPONSE_STATES.OK) {
|
|
//Сессия недействительна - очищаем данные
|
|
await clearAuthSession();
|
|
setSession(null);
|
|
setIsAuthenticated(false);
|
|
return { success: false, isOffline: false, error: ERROR_MESSAGES.SESSION_EXPIRED };
|
|
}
|
|
|
|
//Проверяем необходимость выбора организации
|
|
if (XPAYLOAD.XCOMPANIES && !XPAYLOAD.XCOMPANY) {
|
|
const organizations = XPAYLOAD.XCOMPANIES.map(item => item.XCOMPANY || item);
|
|
|
|
return {
|
|
success: true,
|
|
isOffline: false,
|
|
needSelectCompany: true,
|
|
sessionId: XPAYLOAD.SSESSION || savedSession.sessionId,
|
|
user: XPAYLOAD.XUSER,
|
|
organizations,
|
|
serverUrl: savedSession.serverUrl,
|
|
savePassword: savedSession.savePassword
|
|
};
|
|
}
|
|
|
|
//Обновляем данные сессии
|
|
const updatedSession = {
|
|
sessionId: XPAYLOAD.SSESSION || savedSession.sessionId,
|
|
serverUrl: savedSession.serverUrl,
|
|
userRn: XPAYLOAD.XUSER?.NRN || savedSession.userRn,
|
|
userCode: XPAYLOAD.XUSER?.SCODE || savedSession.userCode,
|
|
userName: XPAYLOAD.XUSER?.SNAME || savedSession.userName,
|
|
companyRn: XPAYLOAD.XCOMPANY?.NRN || savedSession.companyRn,
|
|
companyName: XPAYLOAD.XCOMPANY?.SNAME || savedSession.companyName,
|
|
savePassword: savedSession.savePassword
|
|
};
|
|
|
|
await setAuthSession(updatedSession);
|
|
setSession(updatedSession);
|
|
setIsAuthenticated(true);
|
|
|
|
return { success: true, isOffline: false, session: updatedSession };
|
|
} catch (checkError) {
|
|
//Оффлайн только при ошибке сети; при прочих ошибках — не переключаем в оффлайн
|
|
const isNetworkError = checkError.message && String(checkError.message).indexOf(ERROR_MESSAGES.NETWORK_ERROR) !== -1;
|
|
const savedSession = await getAuthSession();
|
|
|
|
if (isNetworkError && savedSession?.sessionId) {
|
|
setSession(savedSession);
|
|
setIsAuthenticated(true);
|
|
return { success: true, isOffline: true, session: savedSession };
|
|
}
|
|
|
|
setSession(null);
|
|
setIsAuthenticated(false);
|
|
return { success: false, isOffline: false, error: checkError.message };
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [getAuthSession, executeRequest, clearAuthSession, setAuthSession]);
|
|
|
|
//Инициализация при готовности БД
|
|
React.useEffect(() => {
|
|
if (!isDbReady || isInitialized) {
|
|
return;
|
|
}
|
|
|
|
const initAuth = async () => {
|
|
try {
|
|
const savedSession = await getAuthSession();
|
|
|
|
if (savedSession?.sessionId) {
|
|
setSession(savedSession);
|
|
setIsAuthenticated(true);
|
|
sessionRestoredFromStorageRef.current = true;
|
|
}
|
|
} catch (initError) {
|
|
console.error('Ошибка инициализации авторизации:', initError);
|
|
} finally {
|
|
setIsInitialized(true);
|
|
}
|
|
};
|
|
|
|
initAuth();
|
|
}, [isDbReady, isInitialized, getAuthSession]);
|
|
|
|
//Получить и сбросить флаг «сессия восстановлена при открытии» (для проверки соединения только при холодном старте)
|
|
const getAndClearSessionRestoredFromStorage = React.useCallback(() => {
|
|
const value = sessionRestoredFromStorageRef.current;
|
|
sessionRestoredFromStorageRef.current = false;
|
|
return value;
|
|
}, []);
|
|
|
|
//Отмена запросов при размонтировании
|
|
React.useEffect(
|
|
() => () => {
|
|
if (abortControllerRef.current) {
|
|
abortControllerRef.current.abort();
|
|
}
|
|
},
|
|
[]
|
|
);
|
|
|
|
return {
|
|
//Состояние
|
|
session,
|
|
isAuthenticated,
|
|
isLoading,
|
|
isInitialized,
|
|
error,
|
|
|
|
//Методы
|
|
login,
|
|
logout,
|
|
selectCompany,
|
|
checkSession,
|
|
getDeviceId,
|
|
getSavedCredentials,
|
|
clearSavedCredentials,
|
|
getAuthSession,
|
|
clearAuthSession,
|
|
getAndClearSessionRestoredFromStorage,
|
|
|
|
//Константы
|
|
AUTH_SETTINGS_KEYS
|
|
};
|
|
}
|
|
|
|
//----------------
|
|
//Интерфейс модуля
|
|
//----------------
|
|
|
|
module.exports = useAuth;
|