- Modelo Usuario + seed desde lista de vendedores (18 usuarios, bcrypt cost 12) - POST /api/auth/login + /api/auth/me; middleware Bearer en todo /api excepto health, login y webhook Monday; flag AUTH_ENABLED - Username = nombre completo normalizado (case/acentos insensible), password inicial Frasa2026, admin vía ADMIN_PASSWORD - Apps: LoginScreen + AuthGate (StudioBlank), token en localStorage, header Authorization en todas las llamadas, logout en 401 - Tests: api 136, cotizador 71, alta 65, cobranza 71, agente-ia 13, landing 4
78 lines
2.3 KiB
TypeScript
78 lines
2.3 KiB
TypeScript
// Sesión de autenticación contra el API Gateway (JWT guardado en localStorage)
|
|
const API_URL = import.meta.env.VITE_API_URL ?? 'http://192.168.10.123:3000/api'
|
|
const STORAGE_KEY = 'frasa_auth'
|
|
|
|
export interface UsuarioSesion {
|
|
nombre: string
|
|
rol: string
|
|
asignacion: string | null
|
|
}
|
|
|
|
export interface Sesion {
|
|
token: string
|
|
usuario: UsuarioSesion
|
|
}
|
|
|
|
/** Error de login con mensaje genérico (no filtra si el usuario existe) */
|
|
export class ErrorLogin extends Error {
|
|
constructor(message = 'Usuario o contraseña incorrectos') {
|
|
super(message)
|
|
this.name = 'ErrorLogin'
|
|
}
|
|
}
|
|
|
|
export function getSesion(): Sesion | null {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY)
|
|
if (!raw) return null
|
|
const sesion = JSON.parse(raw) as Sesion
|
|
if (typeof sesion?.token !== 'string' || !sesion.usuario) return null
|
|
return sesion
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function getToken(): string | null {
|
|
return getSesion()?.token ?? null
|
|
}
|
|
|
|
export function logout(): void {
|
|
localStorage.removeItem(STORAGE_KEY)
|
|
}
|
|
|
|
/** POST /auth/login; guarda token + usuario en localStorage */
|
|
export async function login(username: string, password: string): Promise<UsuarioSesion> {
|
|
let res: Response
|
|
try {
|
|
res = await fetch(`${API_URL}/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password }),
|
|
})
|
|
} catch {
|
|
throw new ErrorLogin('No se pudo conectar con el servidor')
|
|
}
|
|
if (!res.ok) throw new ErrorLogin()
|
|
const data = (await res.json()) as Sesion
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({ token: data.token, usuario: data.usuario }))
|
|
return data.usuario
|
|
}
|
|
|
|
/**
|
|
* Agrega Authorization: Bearer a un RequestInit si hay sesión.
|
|
* Sin token devuelve el init tal cual (las llamadas siguen iguales que antes).
|
|
*/
|
|
export function conAuth(init: RequestInit = {}): RequestInit {
|
|
const token = getToken()
|
|
if (!token) return init
|
|
return { ...init, headers: { ...(init.headers as Record<string, string>), Authorization: `Bearer ${token}` } }
|
|
}
|
|
|
|
/** Si el API responde 401: cierra sesión y recarga para volver al login */
|
|
export function manejar401(res: Response): void {
|
|
if (res.status !== 401) return
|
|
logout()
|
|
if (typeof window !== 'undefined') window.location.reload()
|
|
}
|