727 lines
26 KiB
JavaScript
727 lines
26 KiB
JavaScript
/**
|
|
* Shared Auth Store Factory
|
|
*
|
|
* Creates a Pinia auth store that can be used by any ROA2WEB application.
|
|
* Each app passes its own apiService instance configured with the correct baseURL.
|
|
*
|
|
* Usage:
|
|
* import { createAuthStore } from '@shared/frontend/stores/auth';
|
|
* import { apiService } from '../services/api';
|
|
* export const useAuthStore = createAuthStore(apiService);
|
|
*
|
|
* Multi-Server Login Flow (US-010):
|
|
* 1. Call checkEmail(email) to verify email exists and get available servers
|
|
* 2. If multiple servers, user selects one; if single server, auto-select
|
|
* 3. Call login({username, password, server_id}) to authenticate
|
|
* 4. Server ID is saved to localStorage for next login pre-selection
|
|
*/
|
|
|
|
import { defineStore } from "pinia";
|
|
import { ref, computed } from "vue";
|
|
|
|
// localStorage keys
|
|
const STORAGE_KEYS = {
|
|
ACCESS_TOKEN: "access_token",
|
|
REFRESH_TOKEN: "refresh_token",
|
|
USER: "user",
|
|
LAST_SERVER_ID: "last_server_id",
|
|
AUTH_MODE: "auth_mode",
|
|
};
|
|
|
|
/**
|
|
* Returnează cheia localStorage pentru tokenul de trusted device.
|
|
* Cheia e per-user și per-server pentru izolare corectă.
|
|
* Exemplu: "trusted_device_MARIUS M_romfast"
|
|
*/
|
|
const _getTrustedDeviceKey = (username, serverId) => {
|
|
const base = `trusted_device_${(username || "unknown").toUpperCase()}`;
|
|
return serverId ? `${base}_${serverId}` : base;
|
|
};
|
|
|
|
/**
|
|
* Caută orice token de trusted device, indiferent de username sau server.
|
|
* Backend validează oricum username-ul corect.
|
|
*/
|
|
const _findAnyTrustedDeviceToken = () => {
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
const key = localStorage.key(i);
|
|
if (key && key.startsWith("trusted_device_")) {
|
|
const val = localStorage.getItem(key);
|
|
if (val) return val;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Factory function to create an auth store with the provided API service
|
|
* @param {Object} apiService - Axios instance configured for the app's API
|
|
* @param {Object} options - Optional configuration
|
|
* @param {Function} options.onLogout - Callback to reset other stores on logout (US-028)
|
|
* @returns {Function} Pinia store definition
|
|
*/
|
|
export function createAuthStore(apiService, options = {}) {
|
|
return defineStore("auth", () => {
|
|
// State - Core auth
|
|
const accessToken = ref(localStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN));
|
|
const refreshToken = ref(localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN));
|
|
const user = ref(JSON.parse(localStorage.getItem(STORAGE_KEYS.USER) || "null"));
|
|
const isLoading = ref(false);
|
|
const error = ref(null);
|
|
|
|
// State - Auth mode detection (US-011 - Backward Compatibility)
|
|
// "single-server": legacy username/password login
|
|
// "multi-server": email-based login with server selection
|
|
const authMode = ref(localStorage.getItem(STORAGE_KEYS.AUTH_MODE) || null);
|
|
const isLoadingAuthMode = ref(false);
|
|
|
|
// State - Multi-step login (US-010)
|
|
// Login steps: 'email' -> 'server' (if multiple) -> 'password' -> 'complete'
|
|
// In single-server mode: 'username' -> 'password' -> 'complete'
|
|
const loginStep = ref("loading"); // Start with loading until auth mode is determined
|
|
const loginEmail = ref("");
|
|
const loginUsername = ref(""); // For single-server mode (US-011)
|
|
const availableServers = ref([]); // [{id: 'romfast', name: 'Romfast - Producție'}, ...]
|
|
const selectedServerId = ref(localStorage.getItem(STORAGE_KEYS.LAST_SERVER_ID) || null);
|
|
const isCheckingEmail = ref(false);
|
|
|
|
// Flag pentru a preveni 401 interceptor în timpul autentificării
|
|
// Când este true, interceptorul 401 din App.vue nu va face redirect la /login
|
|
// Acest lucru permite modalului de server switch să gestioneze erorile de parolă
|
|
const isAuthenticating = ref(false);
|
|
|
|
// State - URL pre-selection (US-004)
|
|
// Allows URL bookmark to pre-select a server (e.g., /login?server=romfast)
|
|
const preselectedServerId = ref(null);
|
|
|
|
// State - 2FA (pasul 2 din fluxul de login cu email)
|
|
const otpEmail = ref(""); // Email complet — trimis la /auth/verify-2fa-code
|
|
const otpMaskedEmail = ref(""); // Email mascat pentru display ("m***@romfast.ro")
|
|
const is2FALoading = ref(false); // Loading state pt butonul "Verifică"
|
|
const resendCountdown = ref(0); // Countdown 60s pt butonul "Retrimite codul"
|
|
let _resendTimer = null; // Interval timer intern (nu ref — nu e nevoie de reactivitate)
|
|
|
|
// State - pending server ID pentru 2FA în contextul server switch
|
|
// Login() returnează requires_2fa înainte de a actualiza selectedServerId,
|
|
// deci verify2FA() trebuie să folosească pendingServerId pentru server-ul corect
|
|
const pendingServerId = ref(null);
|
|
|
|
// Getters
|
|
const isAuthenticated = computed(() => !!accessToken.value);
|
|
const currentUser = computed(() => user.value);
|
|
|
|
// Getters - Auth mode (US-011)
|
|
const isSingleServerMode = computed(() => authMode.value === "single-server");
|
|
const isMultiServerMode = computed(() => authMode.value === "multi-server");
|
|
|
|
// Getters - Multi-step login (US-010)
|
|
const hasMultipleServers = computed(() => availableServers.value.length > 1);
|
|
const lastServerId = computed(() => localStorage.getItem(STORAGE_KEYS.LAST_SERVER_ID));
|
|
|
|
// Getter - Server name for display (US-026)
|
|
const serverName = computed(() => {
|
|
// If authenticated, get from JWT payload (user.server_name)
|
|
if (user.value?.server_name) {
|
|
return user.value.server_name;
|
|
}
|
|
// Fallback: search in availableServers
|
|
const server = availableServers.value.find(s => s.id === selectedServerId.value);
|
|
return server?.name || selectedServerId.value?.toUpperCase() || null;
|
|
});
|
|
|
|
// Actions
|
|
|
|
/**
|
|
* Get authentication mode from server (US-011 - Backward Compatibility)
|
|
* Determines whether to use single-server (username/password) or
|
|
* multi-server (email-based) login flow.
|
|
*
|
|
* @returns {Promise<{mode: string, supports_email_login: boolean}>}
|
|
*/
|
|
const getAuthMode = async () => {
|
|
isLoadingAuthMode.value = true;
|
|
|
|
try {
|
|
const response = await apiService.get("/system/auth-mode");
|
|
const { mode, supports_email_login } = response.data;
|
|
|
|
authMode.value = mode;
|
|
localStorage.setItem(STORAGE_KEYS.AUTH_MODE, mode);
|
|
|
|
// Set initial login step based on auth mode
|
|
if (mode === "single-server") {
|
|
loginStep.value = "username";
|
|
} else {
|
|
loginStep.value = "email";
|
|
}
|
|
|
|
return { mode, supports_email_login };
|
|
} catch (err) {
|
|
console.error("Failed to get auth mode:", err);
|
|
// Default to single-server mode on error (backward compatible)
|
|
authMode.value = "single-server";
|
|
loginStep.value = "username";
|
|
return { mode: "single-server", supports_email_login: false };
|
|
} finally {
|
|
isLoadingAuthMode.value = false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Check if identity (email or username) exists in the system and get available servers (US-010, US-013)
|
|
* @param {string} identity - Email address or username to check
|
|
* @returns {Promise<{exists: boolean, servers: Array<{id: string, name: string}>, identity_type: string}>}
|
|
*/
|
|
const checkIdentity = async (identity) => {
|
|
isCheckingEmail.value = true;
|
|
error.value = null;
|
|
|
|
try {
|
|
// Use new check-identity endpoint (US-013)
|
|
const response = await apiService.post("/auth/check-identity", { identity });
|
|
const { exists, servers, identity_type } = response.data;
|
|
|
|
loginEmail.value = identity; // Store identity (could be email or username)
|
|
availableServers.value = servers;
|
|
|
|
if (exists && servers.length > 0) {
|
|
// Server selection priority (US-004):
|
|
// 1. preselectedServerId (from URL ?server=xyz)
|
|
// 2. lastServer (from localStorage)
|
|
// 3. First server in list
|
|
const lastServer = localStorage.getItem(STORAGE_KEYS.LAST_SERVER_ID);
|
|
|
|
if (preselectedServerId.value && servers.some((s) => s.id === preselectedServerId.value)) {
|
|
// URL pre-selection takes highest priority
|
|
selectedServerId.value = preselectedServerId.value;
|
|
} else if (lastServer && servers.some((s) => s.id === lastServer)) {
|
|
// Fall back to last used server
|
|
selectedServerId.value = lastServer;
|
|
} else {
|
|
// Default to first server
|
|
selectedServerId.value = servers[0].id;
|
|
}
|
|
|
|
// Skip server selection step if only one server available
|
|
if (servers.length === 1) {
|
|
loginStep.value = "password";
|
|
} else {
|
|
loginStep.value = "server";
|
|
}
|
|
}
|
|
|
|
return { exists, servers, identity_type };
|
|
} catch (err) {
|
|
const errorMessage = err.response?.data?.detail || "Eroare la verificare";
|
|
error.value = errorMessage;
|
|
return { exists: false, servers: [], identity_type: "unknown", error: errorMessage };
|
|
} finally {
|
|
isCheckingEmail.value = false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Check if email exists in the system and get available servers (US-010)
|
|
* DEPRECATED: Use checkIdentity for dual email/username support (US-013)
|
|
* @param {string} email - Email address to check
|
|
* @returns {Promise<{exists: boolean, servers: Array<{id: string, name: string}>}>}
|
|
*/
|
|
const checkEmail = async (email) => {
|
|
// Delegate to checkIdentity for backward compatibility
|
|
const result = await checkIdentity(email);
|
|
return { exists: result.exists, servers: result.servers };
|
|
};
|
|
|
|
/**
|
|
* Login with credentials and optional server_id (US-010)
|
|
* @param {Object} credentials - {username, password, server_id?}
|
|
* @returns {Promise<{success: boolean, error?: string}>}
|
|
*/
|
|
const login = async (credentials) => {
|
|
isAuthenticating.value = true; // Previne 401 interceptor redirect
|
|
isLoading.value = true;
|
|
error.value = null;
|
|
|
|
try {
|
|
// Build request payload
|
|
const payload = {
|
|
username: credentials.username,
|
|
password: credentials.password,
|
|
};
|
|
|
|
// Add server_id if provided (multi-server mode)
|
|
if (credentials.server_id) {
|
|
payload.server_id = credentials.server_id;
|
|
}
|
|
|
|
// Trusted device: trimitem tokenul stocat local pentru posibil skip 2FA
|
|
// Fallback cross-server: dacă nu există token pentru serverul curent, căutăm orice token al utilizatorului
|
|
const tdKey = _getTrustedDeviceKey(credentials.username, credentials.server_id);
|
|
const storedTrustedToken = localStorage.getItem(tdKey) || _findAnyTrustedDeviceToken();
|
|
if (storedTrustedToken) {
|
|
payload.trusted_device_token = storedTrustedToken;
|
|
}
|
|
|
|
const response = await apiService.post("/auth/login", payload);
|
|
const responseData = response.data;
|
|
|
|
// 2FA: backend cere verificare cod pe email
|
|
if (responseData.requires_2fa === true) {
|
|
otpEmail.value = responseData.email;
|
|
otpMaskedEmail.value = responseData.masked_email;
|
|
// Salvăm server_id pending — verify2FA() îl va folosi în loc de selectedServerId
|
|
pendingServerId.value = credentials.server_id || null;
|
|
loginStep.value = "2fa";
|
|
_startResendCountdown();
|
|
return { success: true, requires_2fa: true, masked_email: responseData.masked_email };
|
|
}
|
|
|
|
// Flow normal — extragem tokens
|
|
const { access_token, refresh_token, user: userData } = responseData;
|
|
|
|
// IMPORTANT: Update selectedServerId BEFORE user.value to ensure
|
|
// the companies store watch uses the correct server ID for localStorage key
|
|
// (US-027: Multi-server company selection persistence)
|
|
if (credentials.server_id) {
|
|
selectedServerId.value = credentials.server_id;
|
|
localStorage.setItem(STORAGE_KEYS.LAST_SERVER_ID, credentials.server_id);
|
|
} else {
|
|
// Single-server mode: clear any stale server_id from previous multi-server session
|
|
selectedServerId.value = null;
|
|
}
|
|
|
|
accessToken.value = access_token;
|
|
refreshToken.value = refresh_token;
|
|
user.value = userData;
|
|
|
|
localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, access_token);
|
|
localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refresh_token);
|
|
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(userData));
|
|
|
|
apiService.defaults.headers.common["Authorization"] = `Bearer ${access_token}`;
|
|
|
|
// Reset login step state
|
|
loginStep.value = "complete";
|
|
|
|
return { success: true };
|
|
} catch (err) {
|
|
error.value = err.response?.data?.detail || "Login failed";
|
|
return { success: false, error: error.value };
|
|
} finally {
|
|
isLoading.value = false;
|
|
isAuthenticating.value = false; // Reset flag în finally pentru a garanta cleanup
|
|
}
|
|
};
|
|
|
|
const logout = () => {
|
|
// US-031 FIX: Save username and serverId BEFORE clearing them
|
|
// This allows resetAllStores to properly clear localStorage keys
|
|
const logoutContext = {
|
|
username: user.value?.username,
|
|
serverId: selectedServerId.value
|
|
};
|
|
|
|
accessToken.value = null;
|
|
refreshToken.value = null;
|
|
user.value = null;
|
|
error.value = null;
|
|
|
|
// Reset multi-step login state
|
|
loginStep.value = "email";
|
|
loginEmail.value = "";
|
|
availableServers.value = [];
|
|
// Note: Don't clear selectedServerId - keep it for next login pre-selection
|
|
|
|
// Reset 2FA state
|
|
otpEmail.value = "";
|
|
otpMaskedEmail.value = "";
|
|
is2FALoading.value = false;
|
|
pendingServerId.value = null;
|
|
_stopResendCountdown();
|
|
|
|
localStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
|
|
localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
localStorage.removeItem(STORAGE_KEYS.USER);
|
|
// Note: Don't remove LAST_SERVER_ID - keep it for next login
|
|
|
|
delete apiService.defaults.headers.common["Authorization"];
|
|
|
|
// US-028/US-031: Call onLogout callback with context to reset other stores
|
|
// Context includes username and serverId saved BEFORE clearing user state
|
|
// This allows proper cleanup of localStorage keys
|
|
if (options.onLogout) {
|
|
try {
|
|
options.onLogout(logoutContext);
|
|
} catch (err) {
|
|
console.error("[Auth] Error in onLogout callback:", err);
|
|
}
|
|
}
|
|
};
|
|
|
|
const refreshAccessToken = async () => {
|
|
if (!refreshToken.value) {
|
|
logout();
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const response = await apiService.post("/auth/refresh", {
|
|
refresh_token: refreshToken.value,
|
|
});
|
|
|
|
const { access_token } = response.data;
|
|
accessToken.value = access_token;
|
|
localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, access_token);
|
|
apiService.defaults.headers.common["Authorization"] = `Bearer ${access_token}`;
|
|
|
|
return true;
|
|
} catch (err) {
|
|
console.error("Token refresh failed:", err);
|
|
logout();
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const initializeAuth = () => {
|
|
if (accessToken.value) {
|
|
apiService.defaults.headers.common["Authorization"] = `Bearer ${accessToken.value}`;
|
|
}
|
|
};
|
|
|
|
const clearError = () => {
|
|
error.value = null;
|
|
};
|
|
|
|
/**
|
|
* Reset login flow to initial state (US-010, US-011)
|
|
* Called when user wants to change email/username
|
|
*/
|
|
const resetLoginFlow = () => {
|
|
// Reset step based on auth mode
|
|
if (authMode.value === "single-server") {
|
|
loginStep.value = "username";
|
|
} else {
|
|
loginStep.value = "email";
|
|
}
|
|
loginEmail.value = "";
|
|
loginUsername.value = "";
|
|
availableServers.value = [];
|
|
error.value = null;
|
|
// Keep selectedServerId from localStorage for pre-selection
|
|
|
|
// Reset 2FA state
|
|
otpEmail.value = "";
|
|
otpMaskedEmail.value = "";
|
|
is2FALoading.value = false;
|
|
pendingServerId.value = null;
|
|
_stopResendCountdown();
|
|
};
|
|
|
|
/**
|
|
* Go to password step (US-010)
|
|
* Called after server selection
|
|
*/
|
|
const goToPasswordStep = () => {
|
|
loginStep.value = "password";
|
|
};
|
|
|
|
/**
|
|
* Set selected server ID (US-010)
|
|
* @param {string} serverId - Server ID to select
|
|
*/
|
|
const setSelectedServer = (serverId) => {
|
|
selectedServerId.value = serverId;
|
|
};
|
|
|
|
/**
|
|
* Set pre-selected server ID from URL bookmark (US-004)
|
|
* Called from LoginView when ?server=xyz query param is present.
|
|
* The server will be validated against available servers in checkIdentity().
|
|
* @param {string} serverId - Server ID to pre-select
|
|
*/
|
|
const setPreselectedServer = (serverId) => {
|
|
preselectedServerId.value = serverId;
|
|
};
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 2FA HELPERS — countdown timer
|
|
// -------------------------------------------------------------------------
|
|
|
|
const _startResendCountdown = () => {
|
|
resendCountdown.value = 60;
|
|
_stopResendCountdown(); // Curăță orice timer anterior
|
|
_resendTimer = setInterval(() => {
|
|
if (resendCountdown.value > 0) {
|
|
resendCountdown.value--;
|
|
} else {
|
|
_stopResendCountdown();
|
|
}
|
|
}, 1000);
|
|
};
|
|
|
|
const _stopResendCountdown = () => {
|
|
if (_resendTimer) {
|
|
clearInterval(_resendTimer);
|
|
_resendTimer = null;
|
|
}
|
|
resendCountdown.value = 0;
|
|
};
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 2FA ACTIONS
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Verifică codul OTP introdus de utilizator (pasul 2 al 2FA).
|
|
* Dacă codul este valid, stochează JWT tokens și completează autentificarea.
|
|
*
|
|
* @param {Object} params - {code: "483921", trustDevice?: false}
|
|
* @returns {Promise<{success: boolean, error?: string}>}
|
|
*/
|
|
const verify2FA = async ({ code, trustDevice = false }) => {
|
|
is2FALoading.value = true;
|
|
error.value = null;
|
|
|
|
try {
|
|
const payload = {
|
|
code: code.trim(),
|
|
email: otpEmail.value,
|
|
trust_device: trustDevice,
|
|
};
|
|
|
|
// Adăugăm server_id — folosim pendingServerId dacă există (server switch context)
|
|
const effectiveServerId = pendingServerId.value || selectedServerId.value;
|
|
if (effectiveServerId) {
|
|
payload.server_id = effectiveServerId;
|
|
}
|
|
|
|
const response = await apiService.post("/auth/verify-2fa-code", payload);
|
|
const { access_token, refresh_token, user: userData, trusted_device_token } = response.data;
|
|
|
|
// IMPORTANT: Commit pendingServerId -> selectedServerId BEFORE user (pattern existent)
|
|
if (effectiveServerId) {
|
|
selectedServerId.value = effectiveServerId;
|
|
localStorage.setItem(STORAGE_KEYS.LAST_SERVER_ID, effectiveServerId);
|
|
}
|
|
|
|
accessToken.value = access_token;
|
|
refreshToken.value = refresh_token;
|
|
user.value = userData;
|
|
|
|
localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, access_token);
|
|
localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refresh_token);
|
|
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(userData));
|
|
|
|
apiService.defaults.headers.common["Authorization"] = `Bearer ${access_token}`;
|
|
|
|
// Salvează tokenul de trusted device dacă utilizatorul a bifat "Ține minte"
|
|
if (trusted_device_token && userData?.username) {
|
|
const tdKey = _getTrustedDeviceKey(userData.username, effectiveServerId);
|
|
localStorage.setItem(tdKey, trusted_device_token);
|
|
}
|
|
|
|
// Curăță starea 2FA
|
|
otpEmail.value = "";
|
|
otpMaskedEmail.value = "";
|
|
pendingServerId.value = null;
|
|
_stopResendCountdown();
|
|
loginStep.value = "complete";
|
|
|
|
const backupCodes = response.data.backup_codes;
|
|
return { success: true, backup_codes: backupCodes };
|
|
} catch (err) {
|
|
error.value = err.response?.data?.detail || "Verificare eșuată. Încercați din nou.";
|
|
return { success: false, error: error.value };
|
|
} finally {
|
|
is2FALoading.value = false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Retrimite codul OTP pe email (butonul "Retrimite codul").
|
|
* Resetează countdown-ul de 60 secunde.
|
|
*
|
|
* @returns {Promise<{success: boolean, error?: string}>}
|
|
*/
|
|
const resendOTP = async () => {
|
|
// Nu permite retrimis dacă countdown-ul nu a ajuns la 0
|
|
if (resendCountdown.value > 0) {
|
|
return { success: false, error: `Așteptați ${resendCountdown.value} secunde` };
|
|
}
|
|
|
|
error.value = null;
|
|
|
|
try {
|
|
const payload = { email: otpEmail.value };
|
|
const effectiveServerId = pendingServerId.value || selectedServerId.value;
|
|
if (effectiveServerId) {
|
|
payload.server_id = effectiveServerId;
|
|
}
|
|
|
|
await apiService.post("/auth/resend-2fa-code", payload);
|
|
|
|
// Resetăm countdown-ul
|
|
_startResendCountdown();
|
|
return { success: true };
|
|
} catch (err) {
|
|
error.value = err.response?.data?.detail || "Nu s-a putut retrimite codul.";
|
|
return { success: false, error: error.value };
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Verifică un cod de backup (fallback când emailul nu sosește).
|
|
* @param {Object} params - {code: "AB3K9PQR", serverId?: "romfast"}
|
|
* @returns {Promise<{success: boolean, error?: string}>}
|
|
*/
|
|
const verifyBackupCode = async ({ code, serverId, trustDevice = false }) => {
|
|
is2FALoading.value = true;
|
|
error.value = null;
|
|
|
|
try {
|
|
const payload = {
|
|
code: code.trim().toUpperCase(),
|
|
email: otpEmail.value,
|
|
trust_device: trustDevice,
|
|
};
|
|
const effectiveServerId = serverId || pendingServerId.value || selectedServerId.value;
|
|
if (effectiveServerId) payload.server_id = effectiveServerId;
|
|
|
|
const response = await apiService.post("/auth/verify-backup-code", payload);
|
|
const { access_token, refresh_token, user: userData, trusted_device_token } = response.data;
|
|
|
|
if (effectiveServerId) {
|
|
selectedServerId.value = effectiveServerId;
|
|
localStorage.setItem(STORAGE_KEYS.LAST_SERVER_ID, effectiveServerId);
|
|
}
|
|
|
|
accessToken.value = access_token;
|
|
refreshToken.value = refresh_token;
|
|
user.value = userData;
|
|
|
|
localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, access_token);
|
|
localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refresh_token);
|
|
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(userData));
|
|
|
|
apiService.defaults.headers.common["Authorization"] = `Bearer ${access_token}`;
|
|
|
|
if (trusted_device_token && userData?.username) {
|
|
const tdKey = _getTrustedDeviceKey(userData.username, effectiveServerId);
|
|
localStorage.setItem(tdKey, trusted_device_token);
|
|
}
|
|
|
|
otpEmail.value = "";
|
|
otpMaskedEmail.value = "";
|
|
pendingServerId.value = null;
|
|
_stopResendCountdown();
|
|
loginStep.value = "complete";
|
|
|
|
return { success: true };
|
|
} catch (err) {
|
|
error.value = err.response?.data?.detail || "Cod de recuperare invalid.";
|
|
return { success: false, error: error.value };
|
|
} finally {
|
|
is2FALoading.value = false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Switch to a different server without full logout (US-007)
|
|
* Re-authenticates the current user on the new server.
|
|
*
|
|
* @param {string} newServerId - Server ID to switch to
|
|
* @param {string} password - User's password for re-authentication
|
|
* @returns {Promise<{success: boolean, error?: string}>}
|
|
*/
|
|
const switchServer = async (newServerId, password) => {
|
|
// Save current username BEFORE login() potentially modifies user state
|
|
const currentUsername = user.value?.username;
|
|
|
|
if (!currentUsername) {
|
|
return { success: false, error: "Nu există utilizator autentificat" };
|
|
}
|
|
|
|
if (!newServerId) {
|
|
return { success: false, error: "Server ID lipsește" };
|
|
}
|
|
|
|
// Re-authenticate on the new server using existing login() method
|
|
const result = await login({
|
|
username: currentUsername,
|
|
password: password,
|
|
server_id: newServerId
|
|
});
|
|
|
|
return result;
|
|
};
|
|
|
|
// Initialize on store creation
|
|
initializeAuth();
|
|
|
|
return {
|
|
// State - Core auth
|
|
accessToken,
|
|
refreshToken,
|
|
user,
|
|
isLoading,
|
|
error,
|
|
|
|
// State - Auth mode (US-011)
|
|
authMode,
|
|
isLoadingAuthMode,
|
|
|
|
// State - Multi-step login (US-010)
|
|
loginStep,
|
|
loginEmail,
|
|
loginUsername,
|
|
availableServers,
|
|
selectedServerId,
|
|
isCheckingEmail,
|
|
isAuthenticating, // Flag pentru a preveni 401 redirect în timpul login/server-switch
|
|
preselectedServerId, // US-004: URL bookmark pre-selection
|
|
|
|
// State - 2FA
|
|
otpEmail,
|
|
otpMaskedEmail,
|
|
is2FALoading,
|
|
resendCountdown,
|
|
pendingServerId,
|
|
|
|
// Getters
|
|
isAuthenticated,
|
|
currentUser,
|
|
isSingleServerMode,
|
|
isMultiServerMode,
|
|
hasMultipleServers,
|
|
lastServerId,
|
|
serverName,
|
|
|
|
// Actions - Core auth
|
|
login,
|
|
logout,
|
|
refreshAccessToken,
|
|
initializeAuth,
|
|
clearError,
|
|
|
|
// Actions - Auth mode (US-011)
|
|
getAuthMode,
|
|
|
|
// Actions - Multi-step login (US-010, US-013)
|
|
checkIdentity,
|
|
checkEmail, // Deprecated, delegates to checkIdentity
|
|
resetLoginFlow,
|
|
goToPasswordStep,
|
|
setSelectedServer,
|
|
setPreselectedServer, // US-004: URL bookmark pre-selection
|
|
|
|
// Actions - Server switch (US-007)
|
|
switchServer,
|
|
|
|
// Actions - 2FA
|
|
verify2FA,
|
|
resendOTP,
|
|
verifyBackupCode,
|
|
};
|
|
});
|
|
}
|