feat: vínculo con usuario Monday en landing + alta prellenada desde leads
API: - GET /api/monday/usuarios: usuarios activos no-guest del workspace - PATCH /api/usuarios/me/monday: vincula/desvincula el mondayUserId del usuario autenticado (valida contra el workspace) - GET /api/leads/buscar?q=: búsqueda de leads por nombre/teléfono/email (sin acentos, orden updatedAt desc, límite 15) - aPublico/me ahora incluyen mondayUserId Landing (/): - Nueva sección 'Configuración de cuenta': login inline, selector del usuario de Monday, guardar/desvincular, cerrar sesión Alta de clientes (/client): - Búsqueda de leads de Monday con debounce en la pantalla de entrada; al elegir uno prellena nombre/email/celular y guarda leadMondayId - El picker (borradores + alta nueva + búsqueda de lead) es ahora siempre la pantalla de entrada Tests: api 294, landing 8, alta-clientes 96 — todos verdes
This commit is contained in:
38
apps/landing/src/services/api.ts
Normal file
38
apps/landing/src/services/api.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { API_URL, conAuth, manejar401, type UsuarioSesion } from './auth'
|
||||
|
||||
export interface UsuarioMonday {
|
||||
id: number
|
||||
nombre: string
|
||||
email: string
|
||||
}
|
||||
|
||||
/** GET /auth/me — perfil del usuario autenticado (incluye mondayUserId) */
|
||||
export async function getMe(): Promise<UsuarioSesion> {
|
||||
const resp = await fetch(`${API_URL}/auth/me`, conAuth())
|
||||
manejar401(resp)
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
return (await resp.json()) as UsuarioSesion
|
||||
}
|
||||
|
||||
/** GET /monday/usuarios — usuarios activos del workspace para el dropdown */
|
||||
export async function getUsuariosMonday(): Promise<UsuarioMonday[]> {
|
||||
const resp = await fetch(`${API_URL}/monday/usuarios`, conAuth())
|
||||
manejar401(resp)
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
return (await resp.json()) as UsuarioMonday[]
|
||||
}
|
||||
|
||||
/** PATCH /usuarios/me/monday — vincula (number) o desvincula (null) la cuenta Monday */
|
||||
export async function guardarVinculoMonday(mondayUserId: number | null): Promise<UsuarioSesion> {
|
||||
const resp = await fetch(
|
||||
`${API_URL}/usuarios/me/monday`,
|
||||
conAuth({
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mondayUserId }),
|
||||
}),
|
||||
)
|
||||
manejar401(resp)
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
return (await resp.json()) as UsuarioSesion
|
||||
}
|
||||
78
apps/landing/src/services/auth.ts
Normal file
78
apps/landing/src/services/auth.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
// Sesión de autenticación contra el API Gateway (JWT guardado en localStorage)
|
||||
export const API_URL = import.meta.env.VITE_API_URL ?? '/api'
|
||||
const STORAGE_KEY = 'frasa_auth'
|
||||
|
||||
export interface UsuarioSesion {
|
||||
nombre: string
|
||||
rol: string
|
||||
asignacion: string | null
|
||||
mondayUserId: number | 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()
|
||||
}
|
||||
Reference in New Issue
Block a user