diff --git a/.env.example b/.env.example index a2ca997..6846b82 100644 --- a/.env.example +++ b/.env.example @@ -19,9 +19,17 @@ EXPO_PUBLIC_POSTHOG_API_KEY=phc_XXXXXXXXXXXXXXXXXXXX ### Invite HTTPS host (Firebase Hosting default domain, no protocol) # Used for WhatsApp share links: https://HOST/m/{id} and https://HOST/t/{id} -# eas env:create --name EXPO_PUBLIC_INVITE_HOST --value "musapp-731e1.web.app" --environment production --visibility plain +# eas env:create --name EXPO_PUBLIC_INVITE_HOST --value "musapp-731e1.web.app" --environment production --visibility plaintext EXPO_PUBLIC_INVITE_HOST=musapp-731e1.web.app +### Cloudflare Turnstile (Dashboard → Turnstile → widget → Site Key) +# Public site key only. The Secret Key goes in Supabase Auth → Bot and Abuse Protection, never here. +# Hostnames on the widget (no https://): localhost (Expo Go / web) and musapp-731e1.web.app (release). +# eas env:create production --name EXPO_PUBLIC_TURNSTILE_SITE_KEY --value "0x4AAAAA..." --type string --visibility plaintext +EXPO_PUBLIC_TURNSTILE_SITE_KEY=0x4AAAAA_YOUR_SITE_KEY +# Optional override of the production native page host (default: EXPO_PUBLIC_INVITE_HOST) +# EXPO_PUBLIC_TURNSTILE_HOSTNAME=musapp-731e1.web.app + ## CI/CD (secrets: NO van al bundle) ### GitHub Actions — repo secrets @@ -39,7 +47,8 @@ EXPO_PUBLIC_INVITE_HOST=musapp-731e1.web.app ### EAS Build (cloud/CI) # Crea las mismas variables en EAS (Environment = production/preview/...) para que el build release arranque: -# eas env:create production --name EXPO_PUBLIC_SUPABASE_URL --value "https://xxx.supabase.co" --type string --visibility plain +# eas env:create production --name EXPO_PUBLIC_SUPABASE_URL --value "https://xxx.supabase.co" --type string --visibility plaintext # eas env:create production --name EXPO_PUBLIC_SUPABASE_ANON_KEY --value "..." --type string --visibility secret -# eas env:create production --name EXPO_PUBLIC_SENTRY_DSN --value "https://..." --type string --visibility plain -# eas env:create production --name EXPO_PUBLIC_POSTHOG_API_KEY --value "phc_..." --type string --visibility secret \ No newline at end of file +# eas env:create production --name EXPO_PUBLIC_SENTRY_DSN --value "https://..." --type string --visibility plaintext +# eas env:create production --name EXPO_PUBLIC_POSTHOG_API_KEY --value "phc_..." --type string --visibility secret +# eas env:create production --name EXPO_PUBLIC_TURNSTILE_SITE_KEY --value "0x4AAAAA..." --type string --visibility plaintext \ No newline at end of file diff --git a/README.md b/README.md index 4dd5da8..788b867 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ La aplicación sigue una **arquitectura cliente-servidor** con separación clara │ ┌────────────────────────────▼────────────────────────────────┐ │ SERVICIOS EXTERNOS E INFRAESTRUCTURA │ -│ Sentry · PostHog · Expo EAS · GitHub Actions · Push (FCM/APNs) │ +│ Sentry · PostHog · Expo EAS · GitHub Actions · Push · Turnstile │ └─────────────────────────────────────────────────────────────┘ ``` @@ -149,6 +149,7 @@ La aplicación sigue una **arquitectura cliente-servidor** con separación clara | 🛡️ **Sentry** | Monitorización de errores y rendimiento | | 📊 **PostHog** | Analítica de producto | | 🔔 **Expo Push Notifications** | Notificaciones en Android (FCM) e iOS (APNs) | +| 🧩 **Cloudflare Turnstile** | CAPTCHA en login, registro y recuperación de contraseña | | 🌿 **GitFlow** | Estrategia de ramas: `main`, `develop`, `feature/*`, `release/*` | --- @@ -160,12 +161,15 @@ La aplicación sigue una **arquitectura cliente-servidor** con separación clara - Inicio de sesión con **Google**, **Apple ID** y **correo electrónico** - Registro con aceptación de términos legales y política de privacidad - Recuperación de contraseña y persistencia de sesión entre reinicios +- **Cloudflare Turnstile** al enviar login, registro o recuperación (no en Google/Apple) +- Contraseña con mayúscula, minúscula, dígito y símbolo; cambio desde el perfil con la contraseña actual - **Eliminación de cuenta** conforme al RGPD, con anonimización del historial compartido (partidas, ligas y torneos) - Cierre de sesión con confirmación explícita ### 👤 Perfil de usuario - Datos personales: nombre, teléfono con validación internacional, localidad y foto de perfil +- Cambio de contraseña en edición de perfil (exige la actual) - Preferencias granulares de **notificaciones push** por tipo de evento - Historial personal de partidas con indicadores de victoria y derrota - Resumen de **estadísticas / ELO**, logros destacados y pantalla de stats detallada @@ -236,6 +240,7 @@ La aplicación sigue una **arquitectura cliente-servidor** con separación clara ### 🔒 Seguridad y cumplimiento - Autenticación OAuth2 con flujo PKCE y tokens de corta duración +- **Turnstile** (Cloudflare) como desafío anti-bots en los formularios de email - Políticas de acceso a datos por usuario y por rol en base de datos - Protección de datos personales: teléfono visible solo entre participantes de la misma partida - Cumplimiento RGPD: consentimiento, minimización de datos y derecho de supresión diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index e417080..6874504 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -11,12 +11,14 @@ App móvil para jugadores de mus en España que permite encontrar contrincantes ### Decisiones clave - **Marca y legal (may. 2026):** nombre comercial **jugaMUS** (`APP_DISPLAY_NAME`; nombre en launcher vía `app.json`). Deep link scheme `jugamus`. Términos y privacidad con texto estático y disclaimer «Texto legal definitivo pendiente de revisión jurídica.» hasta revisión legal. -- **CI/CD (jun.–ago. 2026):** workflow reutilizable `quality.yml` (job `Quality`: Gitleaks, `expo-doctor`, lint, tests, cobertura ≥60% líneas en `src/{utils,lib,services,hooks}`). `eas.yml` en push a `main`: quality → tag `v{version}-{YYYYMMDD.HHmm}` → build Android/iOS → submit Play + TestFlight. `GITHUB_TOKEN` con mínimo privilegio: `contents: read` por defecto; `contents: write` solo en el job de etiquetado de release (`eas.yml`). Dependabot (npm + github-actions): version updates a **`develop`**, sin majors ni patch/minor del stack Expo/RN (upgrade de SDK con `npx expo upgrade`), grupos prod/dev + security, cooldown. Política de vulnerabilidades en `SECURITY.md`. Secrets GitHub: `EXPO_TOKEN`, `GOOGLE_PLAY_SERVICE_KEY_JSON`. EAS `production`: `GOOGLE_SERVICES_JSON`, `SENTRY_AUTH_TOKEN`, **`EXPO_PUBLIC_SUPABASE_URL`** / **`EXPO_PUBLIC_SUPABASE_ANON_KEY`**, **`EXPO_PUBLIC_INVITE_HOST`**. iOS: `ascAppId` `6775626292`. +- **CI/CD (jun.–ago. 2026):** workflow reutilizable `quality.yml` (job `Quality`: Gitleaks, `expo-doctor`, lint, tests, cobertura ≥60% líneas en `src/{utils,lib,services,hooks}`). `eas.yml` en push a `main`: quality → tag `v{version}-{YYYYMMDD.HHmm}` → build Android/iOS → submit Play + TestFlight. `GITHUB_TOKEN` con mínimo privilegio: `contents: read` por defecto; `contents: write` solo en el job de etiquetado de release (`eas.yml`). Dependabot (npm + github-actions): version updates a **`develop`**, sin majors ni patch/minor del stack Expo/RN (upgrade de SDK con `npx expo upgrade`), grupos prod/dev + security, cooldown. Política de vulnerabilidades en `SECURITY.md`. Secrets GitHub: `EXPO_TOKEN`, `GOOGLE_PLAY_SERVICE_KEY_JSON`. EAS `production`: `GOOGLE_SERVICES_JSON`, `SENTRY_AUTH_TOKEN`, **`EXPO_PUBLIC_SUPABASE_URL`** / **`EXPO_PUBLIC_SUPABASE_ANON_KEY`**, **`EXPO_PUBLIC_INVITE_HOST`**, **`EXPO_PUBLIC_TURNSTILE_SITE_KEY`**. iOS: `ascAppId` `6775626292`. - **Legal / Play (may. 2026):** URLs públicas de privacidad y eliminación de cuenta vía **GitHub Pages** (`docs/` en `main`, carpeta `/docs`). Contacto de soporte / seguridad: `japenago@gmail.com`. Package Android: `com.javiwacho.musapp`; slug EAS `musapp`. - **Partidas (may. 2026):** el creador puede cancelar partidas en `planned` e `in_progress` desde la ficha (no hace falta ser participante). En web, las confirmaciones destructivas (cancelar, abandonar, aprobar resultado) usan **modales** en lugar de `Alert.alert`, que no es fiable en Expo Web. **Empezar partida (jul. 2026):** el creador puede pasar una partida `planned` a `in_progress` manualmente (sin modal de confirmación; sí aviso si la plantilla está incompleta); se fija `start_at` al instante actual. - **Plantilla mixta (may. 2026):** en crear/editar se pueden añadir compañeros/rivales **por nombre** además de cuentas registradas; las plazas (UI, explore y cron) cuentan texto + confirmados (máx. 2 por equipo). El creador puede registrar marcador **sin validación rival** solo si no hay otros participantes con cuenta y la partida está **`in_progress`** (`record_match_result_direct`). Tras aprobar un resultado rival, un trigger en BD confirma el resultado y finaliza la partida (`018`). - **Eliminación de cuenta (may.–ago. 2026):** derecho de supresión RGPD vía Edge Function `delete-account`. Se borran auth, perfil, avatar y datos personales (reportes, cola de notificaciones). El **historial compartido se anonimiza**, no se elimina: referencias de partidas, **ligas y torneos** (creador, parejas, retos, grants) pasan al perfil interno **Usuario eliminado** (sentinel) o a texto «Usuario eliminado» (mig. `023`–`025`, `105`). CORS de la función: orígenes de producción + loopback local (`localhost` / `127.0.0.1`) para desarrollo web. -- **Recuperación de contraseña (jul. 2026):** el email de reset redirige a `jugamus://auth/update-password` (debe estar en Redirect URLs de Supabase). Pantalla dedicada para nueva contraseña; errores visibles en web (sin depender de `Alert.alert`); tras éxito, cierre de sesión y CTA a login. No reutilizar la misma contraseña (`same_password` → 422). +- **Recuperación de contraseña (jul. 2026):** el email de reset redirige a `jugamus://auth/update-password` (debe estar en Redirect URLs de Supabase). Pantalla dedicada para nueva contraseña; errores visibles en web (sin depender de `Alert.alert`); tras éxito, cierre de sesión y CTA a login. No reutilizar la misma contraseña (`same_password` → 422). La sesión de recovery **no** exige contraseña actual. +- **Política de contraseña (ago. 2026):** mínimo 8 caracteres con mayúscula, minúscula, dígito y símbolo (Auth dashboard + `authPasswordSchema` / `AUTH_PASSWORD_HINT`). Cambio de contraseña en Editar perfil exige la actual (`current_password`) y mantiene la sesión. +- **CAPTCHA Turnstile (ago. 2026):** desafío al pulsar enviar en login, registro y recuperación (no OAuth). Site key `EXPO_PUBLIC_TURNSTILE_SITE_KEY` (EAS `production`); secret y CAPTCHA activos en Supabase Auth. Expo Go: hostname `localhost`. Release nativo: `https://musapp-731e1.web.app/turnstile.html` (Firebase Hosting + rebuild con `react-native-webview`). - **Notificaciones en perfil (may./jul./ago. 2026):** preferencias **push** («Todas») y por **evento** en perfil; sin notificaciones por correo; enlaces legales (términos, privacidad) en la misma pantalla. Eventos: unión; partida/torneo inicio, edición y cancelación (separados); resultado pendiente de validar; resultado pendiente de enviar (aviso ~5 h en curso); recordatorios 24 h y/o 2 h antes (chips multi-selección); **solicitudes de amistad** e **invitaciones a partidas** (`notify_on_friend_request` / `notify_on_match_invitation`, mig. `110`). `enqueue_notification` y `process-notifications` respetan `notify_push` + `notify_on_*` (migraciones `077`–`079`, `110`). - **Branding (may. 2026):** icono y splash con diseño minimalista de baraja española (basto); color de fondo `#1a5f4a` en splash e icono adaptativo Android. - **UI Ultra Limpio (may. 2026):** rediseño visual con tokens en `src/theme/` (fondo blanco, verde `#1A5F4A`, tipografía DM Sans). Listas principales (Mis partidas, Descubrir) con filas y punto de estado; previews con `ciudad · lugar`; cabecera Mis partidas sin contador de activas; FAB speed-dial encima de la tab bar; tab bar activa en verde brand. @@ -40,7 +42,7 @@ App móvil para jugadores de mus en España que permite encontrar contrincantes - **PostHog producto (jul./ago. 2026):** eventos `user_signed_up`, `match_created`, `match_joined`, `match_completed` (este último solo al pasar a `finished`, idempotente por `match_id`); `friend_request_sent`, `friend_request_accepted` (auto-aceptación), `match_invite_sent`, `match_invite_accepted` (v1.8). KPIs de panel deben usar estos eventos / `Application Opened`, no `$pageview`. - **Torneos — cuadro interactivo (jul. 2026):** cada pareja en tarjeta propia; tap en pareja para registrar resultado y avanzar (modal rápido o ficha de partido). Etiquetas de ronda (cuartos, semifinal, final). Confirmación destructiva al organizar cuadro. Al cancelar torneo se cancelan **todos** los partidos del cuadro (`082`). Fix auto-cancel al poblar la final (`081`). Notificación «Validar resultado» solo si el resultado queda `pending_validation` (`083`). - **Amigos e invitaciones a partidas (ago. 2026, v1.8):** solicitudes de amistad con mensaje opcional (máx. 200) desde perfil ajeno o búsqueda por nombre; cooldown tras rechazo reciente; lista de amigos/solicitudes en perfil propio. Invitar amigos a pareja/rival al crear o editar equipo (solo partida standalone `planned`/`in_progress` del creador); pendientes ocupan plaza; compartir vía modal en ficha tras crear. Mis Partidas: Invitaciones; ficha: banner Aceptar/Rechazar. Rechazar en partida iniciada/finalizada (o con resultado confirmado) cancela y voidea resultado; rivales invitados fuerzan `pending_validation`. Migraciones `108`–`116`. -- **Versión app (ago. 2026):** **1.8.0** (`app.json`, `package.json`). Incluye hotfix de calidad v1.7.1 (characterization tests, umbrales de cobertura ≥60%, proceso TDD en `docs/testing.md`). +- **Versión app (ago. 2026):** **1.8.1** (`app.json`, `package.json`). Incluye v1.8.0 (amigos/invites) y este hotfix: Turnstile al enviar login/registro/recuperación, política de contraseña (mayúscula, minúscula, dígito y símbolo), cambio de contraseña en perfil, mig. `118`/`119`. - **Security hardening ligas/stats (ago. 2026, mig. `106`):** `process_league_lifecycle` solo vía pg_cron (REVOKE a PUBLIC/authenticated). `enqueue_player_stats_recompute` no ejecutable por clientes. `get_player_stats` recalcula ELO/agregados solo para el propio usuario o admin; el resto lee stats cacheadas (mitiga amplificación cross-user). - **UI responsive (jul. 2026):** helpers `useResponsiveLayout` / `ScrollableModalBody` para escalado tipográfico y modales con scroll seguro en pantallas pequeñas. - **Crear partida — UX (jul.–ago. 2026):** título, ciudad y lugar **opcionales** al crear (defaults: «Partida», «Ciudad por definir», «Lugar por definir» si vacíos). Sin toggle «Lugar por definir» en creación. Fecha/hora por defecto **+10 min** respecto a ahora. Botón ✕ cierra a **Mis partidas** (no `back` a Descubrir). Sin autofocus/teclado al abrir el formulario. Etiquetas de campo en negrita; sin asteriscos ni «(opcional)» en labels de crear/editar. **Empezar partida:** sin modal de confirmación (sí aviso si plantilla incompleta). Edición de partida conserva validación anterior. Aviso si plantilla incompleta (auto-cancel al llegar `start_at`). **Partida ya jugada (ago. 2026):** si `start_at` es anterior a ahora, el formulario muestra el marcador al final (casillas vacías; nombres de equipo derivados como en ficha); exige plantilla completa (nombres o amigos invitados); al crear registra resultado (directo o `pending_validation` si hay rivales invitados) y abre la partida. Orden de pareja/equipo en formularios y tarjetas: **integrantes primero, nombre después**. **Invitar amigos (v1.8):** selector de amigos como compañero/rival al crear; pestaña «Añadir amigo» en editar pareja. @@ -99,9 +101,9 @@ Solo dos roles en el MVP: - Login con Google - Login con Apple ID (obligatorio por requisitos de App Store) -- Login con email y contraseña +- Login con email y contraseña (Turnstile al enviar; complejidad: mayúscula, minúscula, dígito y símbolo) - Registro con aceptación de términos y política de privacidad -- Recuperación de contraseña para usuarios de email (pantalla `auth/update-password` tras el enlace; login con la nueva contraseña) +- Recuperación de contraseña para usuarios de email (pantalla `auth/update-password` tras el enlace; login con la nueva contraseña; Turnstile al enviar el email) - Gestión de sesiones con JWT y refresh tokens (gestionado por Supabase Auth) - Estado de sesión persistente entre cierres de la app - Eliminación de cuenta (RGPD): borrado de identidad + anonimización del historial en partidas, ligas y torneos @@ -109,6 +111,7 @@ Solo dos roles en el MVP: ### F2 - Perfil de usuario (Fase 1) - Nombre a mostrar (obligatorio) +- Cambio de contraseña en edición de perfil (contraseña actual + nueva que cumple la política) - Teléfono (obligatorio, validación formato E.164; en la app: selector de prefijo por país + validación genérica ITU-T, ej. `+34612345678` u otros países del listado) - Localidad/pueblo (opcional, informativo) - Foto de perfil (opcional, comprimida automáticamente a ≤ 500KB) @@ -569,6 +572,7 @@ Supabase (PostgreSQL + Auth + Storage) - TLS extremo a extremo - JWT con expiración corta + refresh tokens - Rate limiting por IP y usuario (Supabase + Edge Functions) +- **Cloudflare Turnstile** en login, registro y recuperación de contraseña (site key en el cliente; secret en Auth) - Protección CSRF/XSS/SSRF, validación exhaustiva de inputs - Row Level Security (RLS) de PostgreSQL para aislar datos por usuario - **Hardening (may. 2026, migraciones 038–047):** @@ -633,6 +637,9 @@ Supabase (PostgreSQL + Auth + Storage) - CA_AUTH2: Un usuario puede autenticarse con Google - CA_AUTH3: Un usuario puede autenticarse con Apple ID - CA_AUTH4: Un usuario puede recuperar su contraseña por email, fijar una nueva en `auth/update-password` e iniciar sesión después +- CA_AUTH4b: Login, registro y «olvidé contraseña» piden Turnstile al pulsar enviar (no al abrir la pantalla; no aplica a Google/Apple) +- CA_AUTH4c: El registro y el cambio de contraseña rechazan secretos sin mayúscula, minúscula, dígito y símbolo +- CA_AUTH4d: En Editar perfil, cambiar la contraseña exige la actual y no cierra la sesión - CA_AUTH5: La sesión persiste entre cierres de la app (refresh token); tras ≥6 h en segundo plano, al volver a primer plano se cierra la sesión con aviso - CA_AUTH6: Un usuario con `status: suspended` no puede iniciar sesión - CA_AUTH7: Un usuario puede eliminar su cuenta; se borra la identidad y se anonimiza su huella en partidas, ligas y torneos (perfil sentinel «Usuario eliminado») diff --git a/SECURITY.md b/SECURITY.md index cbefb90..f79f9ed 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -30,7 +30,7 @@ Include as much detail as you can: In scope: -- Authentication / session issues (including OAuth) +- Authentication / session issues (including OAuth and CAPTCHA/Turnstile bypass) - Unauthorized data access (RLS / API / Edge Functions) - Injection, XSS, or similar client/server flaws - Secrets exposure in the repo or client builds diff --git a/TASKS.md b/TASKS.md index 112cdce..f66a28a 100644 --- a/TASKS.md +++ b/TASKS.md @@ -1,6 +1,6 @@ # Tareas - jugaMUS -> Actualizado: 12/08/2026 (merge main hotfix v1.7.1; timeout sesión 6 h; invites `117`) +> Actualizado: 13/08/2026 (hotfix v1.8.1 cerrado: operación Turnstile lista) > Metodología: Kanban personal. Actualizar al inicio y al final de cada sesión de trabajo. --- @@ -27,6 +27,7 @@ | Release 1.7 | Completada | Contactos, stats/ELO/badges, ligas, CI permissions, deps; v1.7.0 | | Hotfix tests TDD | Completada | Characterization tests + coverage gates + docs; v1.7.1 | | Release 1.8 | Completada | Amigos, invitaciones a partidas, prefs notif. friend/invite; v1.8.0 | +| Hotfix auth/Turnstile | Completada | CAPTCHA Turnstile, política de contraseña, cambio en perfil; ops lista; v1.8.1 | --- @@ -65,14 +66,19 @@ - Migraciones `006`/`007` en repo: RLS sin recursión en `match_participants` + backfill `profiles` desde `auth.users` (evitar 500 tras login). - [x] Pantalla de login con email/contraseña - Mensajes claros si Supabase devuelve 429 (rate limit por IP en plan gratuito). + - Cloudflare Turnstile al pulsar **Entrar** (no al abrir la pantalla). Site key `EXPO_PUBLIC_TURNSTILE_SITE_KEY`; secret solo en Supabase Auth → Bot and Abuse Protection. - [x] Pantalla de registro con aceptación de términos y política de privacidad - Textos estáticos en `src/app/(auth)/terms.tsx` y `privacy.tsx` con disclaimer jurídico; **revisión legal pendiente** antes de release. - Confirmación por email configurada según entorno (desactivada en dev; producción según política del proyecto). + - Contraseña: mínimo 8 + mayúscula + minúscula + dígito + símbolo (`AUTH_PASSWORD_HINT`; alineado con Auth dashboard). + - Turnstile al pulsar **Registrarme**. - [x] Pantalla de recuperación de contraseña - Email → redirect `jugamus://auth/update-password` (`getPasswordResetRedirectUrl`). - Pantalla `src/app/auth/update-password.tsx`: nueva contraseña, errores inline (web; `Alert` no fiable), ✕ → login, éxito → CTA «Ir al login». - Tras guardar: `updateUser` + `signOut`; gate `passwordRecoveryPending` se limpia en login normal (evita redirigir otra vez a update-password). + - Recovery **no** pide contraseña actual (sesión de recovery en GoTrue). - Supabase Redirect URLs: añadir `jugamus://auth/update-password` (además de `auth/callback`). + - Turnstile al pulsar **Enviar enlace**. - [x] Login con Google (OAuth via Supabase) - Requiere MANUAL-1 y MANUAL-2 del plan (Google Cloud + Supabase provider). - Redirects típicos: `exp://**` (Expo Go), `jugamus://auth/callback`, y en web el `http://localhost:PUERTO/` del `expo start --web`. @@ -99,6 +105,7 @@ - [x] Pantalla de perfil (vista propia) - [x] Pantalla de edición de perfil + - Cambio de contraseña con contraseña actual (`updateUser` + `current_password`; la sesión se mantiene). - [x] Campo de teléfono con validación E.164 (selector de país + número; validación genérica `+` y 7–15 dígitos) - [x] Subida de foto de perfil a Supabase Storage (compresión ≤ 500 KB; bucket `avatars` migración `008`; subida sin `Blob.arrayBuffer` en iOS/Hermes) - [x] Preferencias de notificación (push; sin email, migración `057`) @@ -255,7 +262,7 @@ Las notificaciones push **no** funcionan en Expo Go; hace falta un build con cre - [x] Configurar EAS Submit para publicación automática en Google Play - Workflow `.github/workflows/eas.yml`: push a `main` → `eas build` → `eas submit` Android. Secrets GitHub: `EXPO_TOKEN`, `GOOGLE_PLAY_SERVICE_KEY_JSON` (clave JSON de **cuenta de servicio** en Google Cloud: `type`, `private_key`, `client_email` — **no** `google-services.json` de Firebase). Variables EAS `production`: `GOOGLE_SERVICES_JSON`, `SENTRY_AUTH_TOKEN`. - PRs mergeados en `develop`: slug EAS `musapp`, `appVersionSource: remote`, `app.config.js` + `GOOGLE_SERVICES_JSON`, validación JSON Play submit, `npm ci` antes de `eas submit`. -- [x] **Variables EAS `production` obligatorias para el bundle:** `EXPO_PUBLIC_SUPABASE_URL`, `EXPO_PUBLIC_SUPABASE_ANON_KEY` (sin ellas la app en release queda en pantalla en blanco). Opcional: `EXPO_PUBLIC_SENTRY_DSN`, `EXPO_PUBLIC_POSTHOG_API_KEY`. +- [x] **Variables EAS `production` obligatorias para el bundle:** `EXPO_PUBLIC_SUPABASE_URL`, `EXPO_PUBLIC_SUPABASE_ANON_KEY` (sin ellas la app en release queda en pantalla en blanco), **`EXPO_PUBLIC_TURNSTILE_SITE_KEY`** (CAPTCHA Auth activo; secret en Supabase). Opcional: `EXPO_PUBLIC_SENTRY_DSN`, `EXPO_PUBLIC_POSTHOG_API_KEY`. - Confirmar con `eas env:list --environment production`; **nuevo build** tras añadirlas. - [x] Configurar EAS Submit para publicación automática en App Store - PR #59 mergeado en `develop`: jobs `build-ios` + `submit-ios`; submit iOS vía ASC API key en EAS (`EXPO_TOKEN`). @@ -567,6 +574,18 @@ Las notificaciones push **no** funcionan en Expo Go; hace falta un build con cre - [x] Documentación alineada (`REQUIREMENTS.md`, `TASKS.md`, `README.md`) - [x] Merge `main` (hotfix v1.7.1) en `develop` para desbloquear PR #158 +### Hotfix v1.8.1 — Turnstile y contraseñas (ago. 2026) + +- [x] Cloudflare Turnstile en login/registro/recuperación (modal al enviar; no OAuth) + - Site key `EXPO_PUBLIC_TURNSTILE_SITE_KEY`; secret solo en Supabase Auth → Bot and Abuse Protection. + - Expo Go: hostname del widget **`localhost`**. Release: **`musapp-731e1.web.app`** + página `invite-hosting/public/turnstile.html`. +- [x] Operación Turnstile: site key en EAS `production`, hostnames Cloudflare (`localhost`, `musapp-731e1.web.app`), `turnstile.html` en Firebase Hosting, CAPTCHA activo en Supabase Auth, rebuild nativo con `react-native-webview` +- [x] Política de contraseña: min 8 + mayúscula + minúscula + dígito + símbolo +- [x] Cambio de contraseña en Editar perfil (exige actual; recovery no) +- [x] Security RLS/RPC lockdown mig. `118`; `search_path` helpers de torneo mig. `119` +- [x] Versión app → **1.8.1** (`app.json`, `package.json`) +- [x] README: Turnstile en autenticación, stack y seguridad + --- ## UI — Rediseño Ultra Limpio (may. 2026) @@ -682,3 +701,4 @@ Las notificaciones push **no** funcionan en Expo Go; hace falta un build con cre - `process_league_lifecycle`: REVOKE PUBLIC/authenticated + cron `match-state-transitions` (igual que torneos). - Fix typo REVOKE en migración `104` (`enqueue_player_stats_recompute`) + REVOKE defensivo en `106`. - `get_player_stats`: refresh-on-read (ELO + agregados) solo para `auth.uid()` o admin; resto lee cache. +- [x] **Security RLS/RPC (ago. 2026, mig. `118`/`119`):** RLS en cola de stats; SELECT de `match_invitations` y `player_stats` acotado; REVOKE de RPCs de lifecycle/`enqueue_notification`; `search_path` en helpers de torneo. HaveIBeenPwned no disponible en plan Free. diff --git a/app.json b/app.json index ce6607d..2b0fb77 100644 --- a/app.json +++ b/app.json @@ -3,7 +3,7 @@ "name": "jugaMUS", "slug": "musapp", "scheme": "jugamus", - "version": "1.8.0", + "version": "1.8.1", "orientation": "default", "icon": "./assets/icon.png", "userInterfaceStyle": "light", diff --git a/invite-hosting/README.md b/invite-hosting/README.md index ce0c234..184f85b 100644 --- a/invite-hosting/README.md +++ b/invite-hosting/README.md @@ -1,9 +1,10 @@ # Invite Hosting (Firebase) -Static HTTPS endpoints for WhatsApp invite links: +Static HTTPS endpoints for WhatsApp invite links and the Turnstile challenge page: - `https://musapp-731e1.web.app/m/{matchId}` - `https://musapp-731e1.web.app/t/{tournamentId}` +- `https://musapp-731e1.web.app/turnstile.html` (native captcha WebView in release builds) With the app installed and App/Universal Links verified, the OS opens jugaMUS. Otherwise `redirect.html` sends the browser to Play Store / App Store. @@ -14,7 +15,7 @@ With the app installed and App/Universal Links verified, the OS opens jugaMUS. O 3. From this folder: `firebase deploy --only hosting`. 4. Set app env: - Local `.env.local`: `EXPO_PUBLIC_INVITE_HOST=musapp-731e1.web.app` - - EAS: `eas env:create --name EXPO_PUBLIC_INVITE_HOST --value "musapp-731e1.web.app" --environment production --visibility plain` + - EAS: `eas env:create --name EXPO_PUBLIC_INVITE_HOST --value "musapp-731e1.web.app" --environment production --visibility plaintext` 5. Ship a new native build (EAS) so associated domains / intent filters are in the binary. ## Verify diff --git a/invite-hosting/public/turnstile.html b/invite-hosting/public/turnstile.html new file mode 100644 index 0000000..0057795 --- /dev/null +++ b/invite-hosting/public/turnstile.html @@ -0,0 +1,63 @@ + + + + + + jugaMUS + + + + + +
+ + diff --git a/package-lock.json b/package-lock.json index 74823cc..264e394 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "jugamus", - "version": "1.8.0", + "version": "1.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jugamus", - "version": "1.8.0", + "version": "1.8.1", "dependencies": { "@expo-google-fonts/dm-sans": "^0.4.2", "@expo/vector-icons": "^15.1.1", "@hookform/resolvers": "^5.7.1", + "@marsidev/react-turnstile": "^1.6.0", "@react-native-async-storage/async-storage": "2.2.0", "@react-native-community/datetimepicker": "8.4.4", "@react-native-community/slider": "5.0.1", @@ -50,6 +51,7 @@ "react-native-screens": "~4.16.0", "react-native-svg": "15.12.1", "react-native-web": "^0.21.0", + "react-native-webview": "13.15.0", "zod": "^4.4.3", "zustand": "^5.0.14" }, @@ -4161,6 +4163,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@marsidev/react-turnstile": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@marsidev/react-turnstile/-/react-turnstile-1.6.0.tgz", + "integrity": "sha512-T2Um71ZdBgQBiyS01xgRMvDWk+mEsrLtXLFVhOC917OVEO584wbPOWj1sw35RaM17+q9QoADgLVcH60xr9NRqQ==", + "license": "MIT", + "peerDependencies": { + "react": "^17.0.2 || ^18.0.0 || ^19.0", + "react-dom": "^17.0.2 || ^18.0.0 || ^19.0" + } + }, "node_modules/@posthog/core": { "version": "1.47.0", "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.47.0.tgz", @@ -16638,6 +16650,32 @@ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", "license": "MIT" }, + "node_modules/react-native-webview": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.15.0.tgz", + "integrity": "sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "invariant": "2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-webview/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/react-native/node_modules/@react-native/virtualized-lists": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz", diff --git a/package.json b/package.json index 7ce3316..727d023 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jugamus", - "version": "1.8.0", + "version": "1.8.1", "main": "expo-router/entry", "scripts": { "start": "expo start", @@ -20,6 +20,7 @@ "@expo-google-fonts/dm-sans": "^0.4.2", "@expo/vector-icons": "^15.1.1", "@hookform/resolvers": "^5.7.1", + "@marsidev/react-turnstile": "^1.6.0", "@react-native-async-storage/async-storage": "2.2.0", "@react-native-community/datetimepicker": "8.4.4", "@react-native-community/slider": "5.0.1", @@ -59,6 +60,7 @@ "react-native-screens": "~4.16.0", "react-native-svg": "15.12.1", "react-native-web": "^0.21.0", + "react-native-webview": "13.15.0", "zod": "^4.4.3", "zustand": "^5.0.14" }, diff --git a/src/app/(auth)/forgot-password.tsx b/src/app/(auth)/forgot-password.tsx index 84b00dd..569f9b7 100644 --- a/src/app/(auth)/forgot-password.tsx +++ b/src/app/(auth)/forgot-password.tsx @@ -12,9 +12,11 @@ import { zodResolver } from '@hookform/resolvers/zod' import { Controller, useForm } from 'react-hook-form' import { Link } from 'expo-router' +import { TurnstileChallengeModal } from '@/components/auth/TurnstileChallengeModal' import { Button } from '@/components/ui/Button' import { Input } from '@/components/ui/Input' import { useAuthStore } from '@/hooks/useAuth' +import { useTurnstileCaptcha } from '@/hooks/useTurnstileCaptcha' import { forgotPasswordSchema, type ForgotPasswordFormValues } from '@/utils/authSchemas' import { Colors } from '@/theme/colors' import { useResponsiveLayout } from '@/theme/responsive' @@ -23,6 +25,7 @@ import { Fonts } from '@/theme/typography' export default function ForgotPasswordScreen() { const { authTopPadding, font } = useResponsiveLayout() const resetPassword = useAuthStore((s) => s.resetPassword) + const captcha = useTurnstileCaptcha() const [sent, setSent] = useState(false) const [submitting, setSubmitting] = useState(false) @@ -36,9 +39,15 @@ export default function ForgotPasswordScreen() { }) const onSubmit = handleSubmit(async (values) => { + const captchaResult = await captcha.solve() + if (captchaResult.cancelled) return + if (captchaResult.error) { + Alert.alert('Recuperación', captchaResult.error) + return + } setSubmitting(true) try { - const { error } = await resetPassword(values.email) + const { error } = await resetPassword(values.email, captchaResult.token) if (error) { Alert.alert('Recuperación', error.message) return @@ -95,7 +104,7 @@ export default function ForgotPasswordScreen() {