Sistema limpio
This commit is contained in:
@@ -1,23 +1,22 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
// Types
|
||||
export interface InternalUser {
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
role: 'ADMIN' | 'SUPPORT_MANAGER' | 'AGENT' | 'AUDITOR';
|
||||
tenant_id: string;
|
||||
tenant_slug: string;
|
||||
role: 'CLIENT_ADMIN' | 'CLIENT_USER';
|
||||
is_active: boolean;
|
||||
is_two_factor_enabled: boolean;
|
||||
created_at: string;
|
||||
tenant_id: string;
|
||||
}
|
||||
|
||||
export interface AuthState {
|
||||
user: InternalUser | null;
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
refreshToken: string | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
@@ -25,181 +24,90 @@ export interface AuthState {
|
||||
export interface LoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
tenant_slug: string;
|
||||
tenant_slug?: string;
|
||||
totp_code?: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
user: InternalUser;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export interface RefreshTokenRequest {
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
export interface TokenResponse {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
}
|
||||
|
||||
// Initial state
|
||||
const initialState: AuthState = {
|
||||
user: null,
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false
|
||||
};
|
||||
|
||||
// Create auth store
|
||||
function createAuthStore() {
|
||||
const { subscribe, set, update } = writable<AuthState>(initialState);
|
||||
|
||||
// Track current state for uso interno (evita dependencias circulares)
|
||||
const { subscribe, set, update }: Writable<AuthState> = writable(initialState);
|
||||
let _state = initialState;
|
||||
subscribe(s => { _state = s; });
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
// Rehidrata sesión desde cookie HttpOnly (no toca localStorage)
|
||||
init: async () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const response = await fetch('/api/v1/auth/me', {
|
||||
credentials: 'include',
|
||||
headers: { 'X-App': 'internal' }
|
||||
headers: { 'X-App': 'client' }
|
||||
});
|
||||
if (response.ok) {
|
||||
const user = await response.json();
|
||||
set({
|
||||
user,
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: true,
|
||||
isLoading: false
|
||||
});
|
||||
set({ user, token: null, isAuthenticated: true, isLoading: false });
|
||||
}
|
||||
// 401/400 es esperado cuando no hay sesión activa — no es un error
|
||||
} catch (error) {
|
||||
// Ignorar errores de red en init
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
},
|
||||
|
||||
// Login
|
||||
login: async (credentials: LoginRequest): Promise<void> => {
|
||||
update(state => ({ ...state, isLoading: true }));
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-Slug': credentials.tenant_slug,
|
||||
'X-App': 'client',
|
||||
},
|
||||
body: JSON.stringify(credentials)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Login failed');
|
||||
}
|
||||
|
||||
const data: LoginResponse = await response.json();
|
||||
|
||||
set({
|
||||
user: data.user,
|
||||
token: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
isAuthenticated: true,
|
||||
isLoading: false
|
||||
});
|
||||
set({ user: data.user, token: data.access_token, isAuthenticated: true, isLoading: false });
|
||||
} catch (error) {
|
||||
update(state => ({ ...state, isLoading: false }));
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Refresh Session
|
||||
refreshSession: async (): Promise<void> => {
|
||||
const currentRefreshToken = _state.refreshToken;
|
||||
|
||||
if (!currentRefreshToken) {
|
||||
throw new Error("No refresh token available");
|
||||
}
|
||||
|
||||
update(state => ({ ...state, isLoading: true }));
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/auth/refresh', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ refresh_token: currentRefreshToken })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// If refresh fails, logout
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
auth.logout();
|
||||
}
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Refresh failed');
|
||||
}
|
||||
|
||||
const data: TokenResponse = await response.json();
|
||||
|
||||
update(state => ({
|
||||
...state,
|
||||
token: data.access_token,
|
||||
isLoading: false
|
||||
}));
|
||||
|
||||
} catch (error) {
|
||||
update(state => ({ ...state, isLoading: false }));
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Logout
|
||||
logout: async () => {
|
||||
// Llamar al backend para que borre la cookie HttpOnly
|
||||
try {
|
||||
const token = _state.token;
|
||||
const slug = _state.user?.tenant_slug || _state.user?.tenant_id || '';
|
||||
await fetch('/api/v1/auth/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'X-App': 'internal' }
|
||||
headers: {
|
||||
'X-App': 'client',
|
||||
'X-Tenant-Slug': slug,
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
}
|
||||
});
|
||||
} catch { /* ignorar errores de red */ }
|
||||
} catch {}
|
||||
set(initialState);
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
},
|
||||
|
||||
// Update user data
|
||||
updateUser: (user: InternalUser) => {
|
||||
update(state => ({ ...state, user }));
|
||||
},
|
||||
|
||||
// Set user from SSR pre-load (no fetch required)
|
||||
setUser: (user: InternalUser) => {
|
||||
set({ user, token: null, refreshToken: null, isAuthenticated: true, isLoading: false });
|
||||
},
|
||||
|
||||
// Set loading state
|
||||
setLoading: (isLoading: boolean) => {
|
||||
update(state => ({ ...state, isLoading }));
|
||||
}
|
||||
updateUser: (user: User) => { update(state => ({ ...state, user })); },
|
||||
setUser: (user: User) => { set({ user, token: null, isAuthenticated: true, isLoading: false }); },
|
||||
setLoading: (isLoading: boolean) => { update(state => ({ ...state, isLoading })); }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user