/* Предрейсовые осмотры - мобильное приложение Хук для управления авторизацией */ //--------------------- //Подключение библиотек //--------------------- 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;