Set-password / primer acceso
/auth/set-password es la pantalla final de cualquier invite —
staff o jugador, masivo o individual. Pide contraseña, la valida
en el server (≥8 caracteres, no en lista de triviales), y si el
invite era de un jugador, ejecuta linkPlayerAfterPasswordSet
para escribir players.auth_user_id y onboarding_consumed_at
de forma race-safe.
Para comercial
Sección titulada «Para comercial»- Problema que resuelve: activación de cualquier usuario con la misma URL y mismo flow, sin diferenciar tipos de invite.
- Diferenciador: validación server-side (no client-side) — un usuario con DevTools no puede bypassear el length check ni la lista de passwords triviales.
Cómo lo usa el staff
Sección titulada «Cómo lo usa el staff»Acceso y permisos
Sección titulada «Acceso y permisos»Pública, pero requiere sesión válida (cookie creada por el invite link de Supabase Auth). Sin sesión activa, la action devuelve “Sesión expirada. Pedí un nuevo link de invitación.”
Flujos paso a paso
Sección titulada «Flujos paso a paso»- Usuario abre el link del mail de invitación.
- El client
_client.tsxescuchaonAuthStateChange. Si llega por implicit flow (tokens enwindow.location.hash), llamasetSession({ access_token, refresh_token })manualmente porque@supabase/ssrno procesa hash tokens. Si llega por PKCE (?code=…), el client de Supabase auto-intercambia. - La página
/auth/set-passwordrenderiza un formulario con dos inputs (contraseña + confirmar contraseña) yminLength=8en el input. - Submit hace primero un check client-side (
length >= 8ypassword === confirm) y luego disparasetPasswordAction(password):- Valida
length >= 8en server (defense in depth: bypaseable en client con DevTools). - Rechaza passwords triviales contra
COMMON_PASSWORDS— lista corta (12345678,password,qwerty12,abc12345, etc.). - Exige sesión activa (
auth.getUser()no null). - Llama
supabase.auth.updateUser({ password }). - Si el invite era de jugador (metadata
player_id), llamalinkPlayerAfterPasswordSet():- Lee
user_metadata.player_id(y opcionalmenteorganization_id) server-trusted desde la sesión, no de la URL. - Verifica que el
playerexista, que suorganization_idcoincida con la del metadata (si vino), questatusno seainactive, y que el slot esté libre. - Hace
UPDATE players SET auth_user_id = user.id, onboarding_consumed_at = now()conWHERE auth_user_id IS NULL AND onboarding_consumed_at IS NULL— race-safe contra dos sesiones concurrentes con el mismo metadata. - Si el linkeo falla, no rompe la activación de la contraseña: queda fijada igual, Sentry registra warning.
- Lee
- Valida
- El client hace
router.replace(next)connextsanitizado porsanitizeNextPath(bloquea//evil.com, esquemas no relativos, etc.). Default por invite link:/playerpara jugadores,/dashboardpara staff.
Configuración relacionada
Sección titulada «Configuración relacionada»- Lista de passwords triviales en
app/auth/set-password/actions.ts(COMMON_PASSWORDS). Es una lista corta — no zxcvbn — pero corta los abuses más obvios. NEXT_PUBLIC_SITE_URLdefine el dominio absoluto del redirect.
FAQ / casos límite
Sección titulada «FAQ / casos límite»- Contraseña con menos de 8 caracteres: rechazada en server.
- Contraseña en lista trivial: rechazada con “Elegí una contraseña menos común.”
- Link expirado: Supabase Auth rebota la sesión. La action devuelve “Sesión expirada. Pedí un nuevo link de invitación.” El usuario tiene que pedir un invite nuevo al staff.
- Linkeo de jugador falla: la contraseña queda fijada (no
rompemos la activación), pero el
auth_user_idno se asocia. Sentry registra un warning con eluser.idy elmetaPlayerIdpara que el cuerpo técnico pueda intervenir. La action devuelve igual{ ok: true, linkedPlayerId: null }para que la UI no bloquee al usuario.
Cómo lo ve el jugador
Sección titulada «Cómo lo ve el jugador»- Ve dos inputs (contraseña + confirmar) + botón de submit.
- Tras submit exitoso, queda logueado y redirigido a
/player(default del invite del jugador). Si todavía no aceptó los consents obligatorios,/player/layout.tsxlo manda a/auth/consentsantes de mostrarle el home. - Sin texto técnico: errores se muestran en castellano simple.
- Labels e instrucciones vienen de
messages/es.jsonnamespaceauth.setPassword.
Integraciones
Sección titulada «Integraciones»- Supabase Auth procesa el JWT del invite y persiste la password.
- Tabla
playersse actualiza si el invite teníaplayer_iden metadata. - Sentry captura el linkeo fallido y los errores de
auth.updateUser.
Limitaciones / roadmap
Sección titulada «Limitaciones / roadmap»- Bug anecdótico #10: algunos reportes de jugadores que
llegan a
/playersin quedar linkeados conauth_user_id. Sin repro reproducible — investigar cuando aparezca un caso concreto. Mientras tanto, el cuerpo técnico puede invitar de nuevo desde Plantel. - Sin “olvidé mi contraseña” en este path. Esa pantalla vive en
/auth/reset-password. - Lista de passwords triviales es corta — para clientes con requisitos de compliance mayores, evaluar integrar zxcvbn o similar.