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;