API de Preferencias de usuario (/preferencias/mia)¶
Para quién es: el equipo de frontend que construye el Sistema de Experiencias. Todo lo que necesita para consumir la API está aquí y en el contrato OpenAPI.
Qué hace: guarda en el servidor la experiencia visual que eligió cada usuario y le dice cuáles puede usar. La decisión de qué experiencias existe y quién las ve la toma el servidor, nunca el front.
| Entorno | URL base | Estado |
|---|---|---|
| PreProd | https://8u5isw5u30.execute-api.us-east-1.amazonaws.com/PreProd |
Disponible cuando el backend se despliega y valida en PreProd (ver estado) |
| Producción | https://7elgyten88.execute-api.us-east-1.amazonaws.com/Produccion |
Todavía no. Se monta cuando el front esté listo y Iker lo ordene |
Una sola ruta: /preferencias/mia (la "mia" es el propio usuario: no existen identificadores en la ruta ni en el cuerpo).
No escribir estas colecciones por otra vía
PreferenciasUsuario y Experiencias se escriben solo con esta API y la de administración. No usen /metodoGenerico, insertarGenerico ni scripts propios: se saltan las reglas de negocio.
Autenticación¶
- Header
Token: <id_token de Cognito>(el mismo que usan las demás APIs de la plataforma). Use siempreToken: el authorizer del gateway solo lee ese header (conAuthorizationel gateway responde 401 aunque la Lambda lo acepte). - El gateway valida el token con el authorizer de Cognito y la Lambda vuelve a verificar la firma (defensa en profundidad).
- La identidad es siempre el
subdel token. Enviar otro usuario en la query, un header o el cuerpo no tiene ningún efecto. - No envíe nunca roles ni permisos: el servidor los lee de
Users.
GET /preferencias/mia¶
Devuelve la preferencia del usuario y las experiencias que puede elegir. No escribe nada (un usuario nuevo no tiene documento hasta su primer PUT).
200 OK
{
"experiencia": "clasica",
"tema": "claro",
"version": 3,
"disponibles": [
{ "id": "clasica", "nombre": "Clásica", "estado": "general", "orden": 10, "temas": ["claro"], "temaPorDefecto": "claro" },
{ "id": "prueba-interna", "nombre": "Prueba interna", "estado": "borrador", "orden": 100, "temas": ["claro"], "temaPorDefecto": "claro" }
]
}
| Campo | Tipo | Significado |
|---|---|---|
experiencia |
string |
Experiencia activa del usuario. Si la guardada ya no está disponible (retirada, dejó de ser beta, perdió el rol) llega clasica |
tema |
string |
Tema activo. Hoy solo claro |
version |
number |
Versión del documento. Se devuelve tal cual en el siguiente PUT. 0 = el usuario nunca ha guardado |
disponibles |
{ id, nombre, estado, orden, temas, temaPorDefecto }[] |
Lo que este usuario puede elegir, ordenado por orden (menor primero). Siempre incluye clasica. estado: general, beta o borrador. temas son los temas que soporta esa experiencia |
Usuario sin documento: {"experiencia":"clasica","tema":"claro","version":0,"disponibles":[...]}.
PUT /preferencias/mia¶
PUT /PreProd/preferencias/mia HTTP/1.1
Token: eyJraWQiOi...
Content-Type: application/json
{ "experiencia": "prueba-interna", "tema": "claro", "version": 3 }
Reglas del cuerpo (máximo 2048 bytes): exactamente los tres campos, sin extras ni repetidos. experiencia y tema cumplen
^[a-z][a-z0-9-]{1,39}$, y el tema debe ser uno de los temas de esa experiencia (si no, 422 TEMA_NO_DISPONIBLE). version es un entero entre 0 y 1.000.000.
200 OK: mismo formato que el GET, con version incrementada en 1 (el cuerpo trae el estado nuevo; no hace falta otro GET).
Errores¶
Todo error trae { "codigo": "...", "traceId": "<12 hex>" }. El traceId sirve para pedir ayuda al backend; no se muestra al usuario final.
| HTTP | codigo |
Cuándo | Qué hace el front |
|---|---|---|---|
| 400 | VALIDACION |
JSON inválido, campo faltante/extra/repetido, tipo o formato incorrecto | Es un bug del front: registrar y seguir con la experiencia actual |
| 401 | SIN_TOKEN · TOKEN_INVALIDO · USUARIO_NO_ENCONTRADO |
Sin sesión válida (el gateway también responde 401) | Flujo normal de sesión vencida; mientras tanto usar clasica |
| 404 | RUTA_NO_ENCONTRADA |
Ruta distinta de /preferencias/mia |
Bug del front |
| 405 | METODO_NO_PERMITIDO |
Método distinto de GET/PUT/OPTIONS | Bug del front |
| 409 | CONFLICTO_VERSION |
Otro dispositivo guardó antes (la version enviada no es la actual) |
Hacer GET, aplicar lo que llegó y avisar al usuario; no reintentar en bucle |
| 413 | CUERPO_DEMASIADO_GRANDE |
Más de 2048 bytes | Bug del front |
| 422 | EXPERIENCIA_NO_DISPONIBLE |
Id desconocido o no disponible para este usuario | Hacer GET, refrescar el selector y volver a clasica si hace falta |
| 422 | TEMA_NO_DISPONIBLE |
Tema desconocido | Igual que el anterior |
| 500 | ERROR_INTERNO |
Fallo del servidor | Usar clasica; reintentar más tarde |
Un
401generado por el gateway (token vencido o ausente) llega como{"message":"Unauthorized"}y, como todo error del gateway en esta plataforma, puede no traer cabeceras CORS: el navegador lo mostrará como error de red. El front debe tratar cualquier fallo como "usarclasica" y dejar que el interceptor de sesión haga lo suyo.
Reglas de disponibilidad¶
| Estado de la experiencia | La ve |
|---|---|
general |
Todos los usuarios |
beta |
Usuarios con el rol Experiencia, los Desarrollador y los roles extra de la experiencia (rolesAcceso) |
borrador |
Los Desarrollador y los roles extra de la experiencia (rolesAcceso) |
retirada |
Nadie (no aparece en disponibles) |
Los estados y la configuración de cada experiencia los administra el equipo de desarrollo con la API de administración (sin tocar la base de datos). Cuando negocio apruebe una experiencia, se cambia su estado de beta a general y todos los usuarios la ven en
disponibles; el front sigue mostrando a cada usuario solo los módulos que ya tiene asignados (esa parte no cambia).
clasica siempre existe y es general.
Cómo consumirla (receta para el front)¶
- Arranque sin esperar a la red: leer el id guardado en
localStorage(solo el id, nada más), aplicar esa experiencia si está en el registro del front y, si no,clasica. Aplicar tokens de inmediato (sin parpadeo). - Después del login:
GET /preferencias/miacon timeout corto (5 s). Si falla por cualquier motivo, seguir con lo que ya está aplicado. - Validar lo que llega contra el registro del front: un
experienciaoidque el front no conoce se ignora (se usaclasica). El servidor es la fuente de verdad de disponibilidad; el registro del front lo es de qué sabe pintar. - Selector: mostrar solo los
disponiblesque el front conoce. Si solo hay una, ocultar el selector. - Al cambiar: aplicar la experiencia de inmediato (optimista), guardar el id en
localStoragey hacerPUTcon laversiondel últimoGET/PUT. Con la respuesta200, guardar la nuevaversion. - Concurrencia:
409→GET, aplicar y avisar.422→GETy refrescar el selector. Nunca reintentar409en bucle. - Sin polling: un
GETpor sesión es suficiente.
// Tipos (espejo del contrato)
export interface ExperienciaDisponible {
id: string; nombre: string; estado: 'general' | 'beta' | 'borrador';
orden: number; temas: string[]; temaPorDefecto: string;
}
export interface PreferenciasUsuario {
experiencia: string;
tema: string;
version: number;
disponibles: ExperienciaDisponible[];
}
export interface ErrorPreferencias { codigo: string; traceId: string; }
// Adaptador mínimo (el puerto real lo define core/experiencias)
async function obtener(base: string, idToken: string): Promise<PreferenciasUsuario> {
const r = await fetch(`${base}/preferencias/mia`, {
headers: { Token: idToken },
signal: AbortSignal.timeout(5000),
});
if (!r.ok) throw new Error(String(r.status)); // el llamador cae a 'clasica'
return r.json();
}
async function guardar(base: string, idToken: string, cuerpo: { experiencia: string; tema: string; version: number }) {
const r = await fetch(`${base}/preferencias/mia`, {
method: 'PUT',
headers: { Token: idToken, 'Content-Type': 'application/json' },
body: JSON.stringify(cuerpo),
signal: AbortSignal.timeout(5000),
});
if (r.status === 200) return (await r.json()) as PreferenciasUsuario;
throw await r.json().catch(() => ({ codigo: `HTTP_${r.status}` })); // ErrorPreferencias
}
Para construir sin backend (repositorio falso)¶
Respuestas de ejemplo para el repositorio falso de las pruebas:
// GET, usuario sin rol y sin documento
{ "experiencia": "clasica", "tema": "claro", "version": 0, "disponibles": [ { "id": "clasica", "nombre": "Clásica", "estado": "general", "orden": 10, "temas": ["claro"], "temaPorDefecto": "claro" } ] }
// GET, usuario Desarrollador con una experiencia guardada
{ "experiencia": "prueba-interna", "tema": "claro", "version": 2,
"disponibles": [ { "id": "clasica", "nombre": "Clásica", "estado": "general", "orden": 10, "temas": ["claro"], "temaPorDefecto": "claro" }, { "id": "prueba-interna", "nombre": "Prueba interna", "estado": "borrador", "orden": 100, "temas": ["claro"], "temaPorDefecto": "claro" } ] }
// PUT 409
{ "codigo": "CONFLICTO_VERSION", "traceId": "3fa85f6457a1" }
// PUT 422
{ "codigo": "EXPERIENCIA_NO_DISPONIBLE", "traceId": "9c1d2e3f4a5b" }
CORS¶
El servidor concede CORS solo a los orígenes del front de cada entorno (lista cerrada; nunca *):
| Entorno | Orígenes |
|---|---|
| PreProd | https://preprod-logigho.vercel.app y http://localhost:4200 |
| Producción | https://logigho.com (se confirma al montar producción) |
El preflight (OPTIONS) lo responde la propia Lambda (204, Access-Control-Max-Age: 600). Cabeceras permitidas: Content-Type, Token, Authorization.
Límites y comportamiento operativo¶
- Throttling por método:
GETyOPTIONS50 peticiones/s (ráfaga 100);PUT20/s (ráfaga 40). Pasado el límite el gateway responde 429 y el front usaclasica. Cada usuario necesita muy pocas llamadas (unGETpor sesión y unPUTpor cambio). - Respuestas con
Cache-Control: no-store. - Metas de rendimiento (a medir en PreProd): Lambda en caliente p95 < 100 ms (
Duration), arranque en frío < 500 ms (Init Duration, Native AOT); extremo a extremo (con el gateway) p95 < 300 ms.
Estado por entorno¶
| Entorno | Backend | Colección | Última validación |
|---|---|---|---|
| PreProd | Código y documentación listos; despliegue y validación en curso (se actualiza esta tabla al terminar) | PreferenciasUsuario con índice único ux_sub (lo crea Iker) |
Pendiente |
| Producción | No montado | No creada | — |
En PreProd se crea la experiencia de prueba prueba-interna (estado borrador, la ve solo el rol Desarrollador) con POST /experiencias para probar el ciclo completo. El catálogo está en la colección Experiencias y los cambios tardan hasta ~60 s en verse para todos.
Usuarios de prueba: los define Iker (uno sin rol, uno Desarrollador, opcionalmente uno con rol Experiencia).