V2 + V3 : rôles, points, journal, podium et recadrage photo
V2 — authentification et administration :
- Authentification par email + mot de passe (email confirmé, un compte
par email), abandon de l'email interne dérivé du pseudo.
- Rôles public/judge sur profiles, section Administration (juges
uniquement, vérifiée côté serveur) pour gérer les membres.
- Verrou de pseudo : modifiable une fois par son propriétaire puis figé,
contournable par un juge.
- RLS étendue par un trigger BEFORE UPDATE (enforce_profile_update) pour
verrouiller les colonnes sensibles (role, points, pseudo_locked, pseudo
figé) — la RLS seule ne peut pas exprimer une règle par colonne.
V3 — points, journal, podium, progression, photo :
- RPC award_points() (SECURITY DEFINER) : seul point d'écriture de la
colonne points, vérifie le rôle juge côté serveur, delta signé sans
plancher à 0, trace chaque opération dans points_log.
- Leaderboard temps réel (Supabase Realtime) avec podium top 3
(égalités gérées), flèches de progression (previous_rank), et
contrôles de points juges avec confirmation explicite (plus de
debounce auto) avant envoi.
- Page /journal ("le crieur") : fil live et public des attributions de
points.
- Recadrage photo carré + compression client (react-easy-crop + canvas)
au signup et sur le profil.
- Passe de polish visuel : cartes/boutons cohérents, lien actif dans la
nav, podium retravaillé.
schema.sql, README.md et CLAUDE.md mis à jour en conséquence (schéma
idempotent, instructions SMTP/rôles/migration, conventions RLS+trigger
documentées pour les futures colonnes sensibles).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,178 +1,113 @@
|
||||
@AGENTS.md
|
||||
|
||||
# Le Tribunal — Brief de démarrage V1
|
||||
# Le Tribunal
|
||||
|
||||
> **À l'attention de Claude Code.** Ce document décrit **uniquement la V1**. Reste simple, ne construis rien qui n'est pas listé dans « Périmètre V1 ». Les fonctionnalités futures sont listées en fin de doc **seulement** pour que l'architecture reste extensible — ne les implémente pas maintenant.
|
||||
Application web pour un groupe d'amis (~12 personnes) qui anime un jeu et des olympiades pendant une semaine de vacances : comptes, rôles, leaderboard en direct, attribution de points par des juges, journal public des événements. Ré-thématisable par édition (un thème différent chaque année).
|
||||
|
||||
## 1. État actuel
|
||||
|
||||
V1, V2 et V3 sont livrées :
|
||||
- **V1** : comptes, profils, leaderboard statique.
|
||||
- **V2** : authentification par email réel (plus de pseudo+email interne), rôles `public`/`judge`, verrou de pseudo (modifiable une fois), section Administration.
|
||||
- **V3** : attribution de points par les juges (RPC sécurisée, +/- avec confirmation explicite), journal public « le crieur », podium top 3, flèches de progression, recadrage/compression photo côté client.
|
||||
|
||||
Hors périmètre pour l'instant (voir roadmap en fin de doc) : éditions/thèmes, le jeu du Tribunal lui-même (Char, roulette, ostracisme, timer du Gardien), mode grand écran.
|
||||
|
||||
---
|
||||
|
||||
## 1. But du projet
|
||||
## 2. Stack technique
|
||||
|
||||
Application web pour un groupe d'amis (~12 personnes) qui animera un jeu et des olympiades pendant une semaine de vacances. On démarre par le socle : **comptes utilisateurs + leaderboard**. Tout le reste (jeu du « Tribunal », roulette, ostracisme, etc.) viendra ensuite.
|
||||
- **Framework** : Next.js 16 (App Router) + TypeScript
|
||||
- **Style** : Tailwind CSS v4 (design tokens en variables CSS, voir `globals.css`)
|
||||
- **Auth + Base + Stockage + Temps réel** : Supabase (Auth, Postgres, Storage, Realtime)
|
||||
- **Recadrage photo** : `react-easy-crop` + canvas natif (resize/compression client, jamais d'upload de l'image brute)
|
||||
- **Déploiement** : Vercel (front) + Supabase (managé) — pas encore déployé, tourne en local pour l'instant
|
||||
|
||||
## 2. Périmètre V1 (et seulement ça)
|
||||
|
||||
1. **Site responsive** (mobile-first, propre aussi sur ordinateur).
|
||||
2. **Création de compte** : pseudo + mot de passe + photo de profil.
|
||||
3. **Connexion / déconnexion**.
|
||||
4. **Page profil** : voir et modifier son pseudo et sa photo.
|
||||
5. **Leaderboard** : liste de toutes les personnes inscrites, triée par points décroissants, affichant **rang, photo, pseudo, points**.
|
||||
|
||||
### Hors périmètre V1 (ne pas coder maintenant)
|
||||
Attribution de points depuis l'app, rôles admin/juges, jeu du Char, roulette, ostracisme, timer du Gardien, thèmes/éditions, temps réel. En V1, les points existent en base (défaut `0`) mais **il n'y a pas d'interface pour les modifier** — on les ajustera plus tard (ou manuellement via la console Supabase pour tester).
|
||||
Next.js 16 a renommé `middleware.ts` en `proxy.ts` (fichier `src/proxy.ts`, fonction `proxy`). `src/lib/supabase/middleware.ts` est un nom de fichier utilitaire interne, sans rapport avec cette convention.
|
||||
|
||||
---
|
||||
|
||||
## 3. Stack technique
|
||||
## 3. Modèle de données (`supabase/schema.sql`, source de vérité)
|
||||
|
||||
Choisie pour être simple à construire, sécurisée et gratuite à héberger pour 12 utilisateurs :
|
||||
Le script est **idempotent** : toujours le ré-exécuter en entier après une modification, jamais de migration incrémentale séparée.
|
||||
|
||||
- **Framework** : Next.js (App Router) + TypeScript
|
||||
- **Style** : Tailwind CSS (mobile-first)
|
||||
- **Auth + Base de données + Stockage photos** : **Supabase**
|
||||
- Supabase Auth pour la connexion
|
||||
- Postgres pour les profils
|
||||
- Supabase Storage (bucket `avatars`) pour les photos
|
||||
- **Déploiement** : Vercel (front) + Supabase (managé)
|
||||
### `public.profiles` (liée à `auth.users`, créée automatiquement par un trigger sur `auth.users`)
|
||||
|
||||
Pas d'autre dépendance lourde. Composants UI faits main avec Tailwind (pas de librairie de composants pour l'instant).
|
||||
| Colonne | Notes |
|
||||
|-----------------|-------------------------------------------------------------------|
|
||||
| `id` | = `auth.users.id` |
|
||||
| `pseudo` | affiché partout, unique (insensible à la casse) |
|
||||
| `avatar_url` | URL publique Storage (`{user_id}.webp`) |
|
||||
| `points` | jamais modifiable directement — uniquement via RPC `award_points` |
|
||||
| `role` | `public` (défaut) ou `judge` |
|
||||
| `pseudo_locked` | passe à `true` automatiquement au 1er changement de pseudo |
|
||||
| `previous_rank` | repère de classement, mis à jour par `reset_rank_reference()` |
|
||||
|
||||
### `public.points_log` (journal, lecture seule côté client)
|
||||
|
||||
`id, target_id, judge_id, delta, reason, created_at` — écrit uniquement par la RPC `award_points`.
|
||||
|
||||
### Colonnes sensibles : RLS + trigger, jamais confiance au client
|
||||
|
||||
La RLS est **au niveau ligne** : elle ne peut pas exprimer « cette colonne seulement si tel rôle ». Le verrouillage fin des colonnes (`role`, `points`, `pseudo_locked`, pseudo figé, `previous_rank`) passe par le trigger `enforce_profile_update()` :
|
||||
- `points` : bloqué pour **tout le monde**, y compris les juges, en écriture directe. Seule la RPC `award_points()` (SECURITY DEFINER) peut la modifier, via un flag de session (`app.bypass_points_lock`) qui autorise explicitement CETTE écriture précise.
|
||||
- `role`, `previous_rank` : modifiables uniquement par un `judge`.
|
||||
- `pseudo` : modifiable une fois par le propriétaire (auto-lock ensuite), ou à tout moment par un `judge`.
|
||||
- `auth.uid() is null` (SQL Editor, migrations) bypass le trigger : ces accès sont déjà fiables par construction.
|
||||
|
||||
Ce pattern (RLS pour l'accès à la ligne + trigger `BEFORE UPDATE` pour l'accès aux colonnes) est la convention du projet — le reproduire pour toute nouvelle colonne sensible plutôt que d'inventer autre chose.
|
||||
|
||||
### RPC exposées
|
||||
|
||||
- `is_pseudo_taken(p_pseudo)` — anon + authenticated, ne renvoie qu'un booléen (l'anon ne peut pas lire `profiles`).
|
||||
- `award_points(p_target_id, p_delta, p_reason)` — authenticated, vérifie `role = 'judge'` côté serveur, jamais côté client.
|
||||
- `reset_rank_reference()` — authenticated, vérifie `role = 'judge'`, fige le classement courant dans `previous_rank`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Authentification (pseudo + mot de passe)
|
||||
## 4. Authentification
|
||||
|
||||
L'utilisateur se connecte avec **un pseudo et un mot de passe** — pas d'email visible.
|
||||
|
||||
**Implémentation :** on utilise Supabase Auth (email/password) en **dérivant un email interne** à partir du pseudo, invisible pour l'utilisateur :
|
||||
|
||||
```ts
|
||||
const internalEmail = `${slugify(pseudo)}@letribunal.test`;
|
||||
// signup: supabase.auth.signUp({ email: internalEmail, password })
|
||||
// login : supabase.auth.signInWithPassword({ email: internalEmail, password })
|
||||
```
|
||||
|
||||
- `slugify` : minuscules, sans accents, espaces → `-`, caractères non alphanumériques retirés.
|
||||
- Le pseudo affiché (avec accents/casse) est stocké dans la table `profiles`.
|
||||
- Vérifier l'**unicité du pseudo** au signup (le slug sert de clé technique).
|
||||
- Mots de passe : gérés/hashés par Supabase Auth (ne jamais stocker de mot de passe en clair).
|
||||
|
||||
> Règle de sécurité produit : on ne collecte pas d'email réel, c'est un cercle d'amis. Pas de reset par email en V1 (un admin pourra réinitialiser via Supabase si besoin).
|
||||
Email + mot de passe (Supabase Auth impose l'unicité de l'email). Confirmation d'email **activée** — nécessite un SMTP custom configuré côté Supabase (le service intégré est trop limité en volume, voir README). Le pseudo est un simple nom d'affichage stocké dans `profiles`, jamais l'email n'est montré aux autres membres.
|
||||
|
||||
---
|
||||
|
||||
## 5. Modèle de données
|
||||
## 5. Pages & navigation
|
||||
|
||||
Table `profiles` (liée à `auth.users`) :
|
||||
| Route | Contenu |
|
||||
|----------------|---------------------------------------------------------------------------|
|
||||
| `/` | Redirige vers `/leaderboard` si connecté, sinon `/login` |
|
||||
| `/login` | Email + mot de passe |
|
||||
| `/signup` | Email + mot de passe + pseudo + photo (recadrée) ; gère l'attente de confirmation email |
|
||||
| `/leaderboard` | Podium top 3 + liste, Realtime, contrôles de points pour les juges |
|
||||
| `/journal` | « Le crieur » — fil live des attributions de points, lecture pour tous |
|
||||
| `/profile` | Pseudo (verrouillable), photo, déconnexion |
|
||||
| `/admin` | Juges uniquement, **vérifié côté serveur** (Server Component) — gestion des membres, reset du repère de classement |
|
||||
|
||||
| Colonne | Type | Notes |
|
||||
|--------------|-------------|----------------------------------------------|
|
||||
| `id` | uuid (PK) | = `auth.users.id` |
|
||||
| `pseudo` | text | affiché, unique (insensible à la casse) |
|
||||
| `slug` | text | unique, dérivé du pseudo (clé technique) |
|
||||
| `avatar_url` | text (null) | URL publique de la photo dans Storage |
|
||||
| `points` | int | défaut `0` |
|
||||
| `created_at` | timestamptz | défaut `now()` |
|
||||
|
||||
**Storage :** bucket public `avatars`, un fichier par utilisateur (`{user_id}.<ext>`).
|
||||
|
||||
**Row Level Security (RLS) — à activer :**
|
||||
- `SELECT` sur `profiles` : autorisé à tout utilisateur **authentifié** (nécessaire pour afficher le leaderboard de tout le monde).
|
||||
- `INSERT` : un utilisateur ne peut créer que **sa propre** ligne (`auth.uid() = id`).
|
||||
- `UPDATE` : un utilisateur ne peut modifier que **sa propre** ligne, et **uniquement** `pseudo`, `slug`, `avatar_url` — **pas `points`** (les points seront gérés plus tard côté admin).
|
||||
- `DELETE` : interdit en V1.
|
||||
|
||||
Fournis le SQL des tables + policies dans un fichier `supabase/schema.sql` versionné.
|
||||
`/leaderboard`, `/profile`, `/admin`, `/journal` sont protégées par `src/proxy.ts` (redirection `/login` si non connecté). L'accès juge-only de `/admin` n'est **pas** géré par le proxy (il n'a pas facilement le rôle) — c'est la page elle-même qui vérifie et redirige.
|
||||
|
||||
---
|
||||
|
||||
## 6. Pages & navigation
|
||||
## 6. Look & feel
|
||||
|
||||
| Route | Contenu |
|
||||
|---------------|-------------------------------------------------------------------------|
|
||||
| `/` | Redirige vers `/leaderboard` si connecté, sinon vers `/login` |
|
||||
| `/login` | Formulaire pseudo + mot de passe ; lien vers `/signup` |
|
||||
| `/signup` | Pseudo + mot de passe + upload photo ; crée le compte et le profil |
|
||||
| `/leaderboard`| Liste triée par points ↓ : rang, photo, pseudo, points (protégée) |
|
||||
| `/profile` | Voir/modifier son pseudo et sa photo ; bouton déconnexion (protégée) |
|
||||
Design tokens en variables CSS (`globals.css`) : `--navy #0F2748`, `--gold #C9A227`, `--ivory #F4ECD8`, `--ink #2A2116`. Ne jamais coder une couleur en dur dans un composant — passer par ces tokens pour que le re-thème par édition reste possible.
|
||||
|
||||
- Les routes `/leaderboard` et `/profile` sont **protégées** : rediriger vers `/login` si non connecté (via middleware Next.js + session Supabase).
|
||||
- Une barre de navigation simple (Leaderboard · Profil · Déconnexion) visible une fois connecté.
|
||||
Convention de carte : `rounded-xl border border-navy/10 bg-white shadow-sm` (formulaires, podium) ou `rounded-lg` pour les lignes de liste. Bouton primaire : `bg-navy text-ivory hover:bg-navy/90`. Bouton secondaire : `border border-navy/20 hover:bg-navy/5`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Look & feel
|
||||
## 7. Conventions
|
||||
|
||||
Sobre, lisible, **mobile-first**, agréable en vidéoprojection plus tard. Définir les couleurs comme **variables CSS / tokens Tailwind** pour pouvoir re-thématiser facilement par la suite (un thème par édition est prévu plus tard — ne pas coder de thème « en dur » dans les composants).
|
||||
|
||||
Palette par défaut (tokens) :
|
||||
- `--navy: #0F2748` (fond / barres)
|
||||
- `--gold: #C9A227` (accent, rang 1)
|
||||
- `--ivory: #F4ECD8` (surfaces claires)
|
||||
- `--ink: #2A2116` (texte sur clair)
|
||||
|
||||
Leaderboard : le **rang 1** est mis en valeur (accent doré). Photos en avatars ronds. Fallback si pas de photo : initiales du pseudo sur fond de couleur.
|
||||
|
||||
Qualité minimale : responsive jusqu'au mobile, focus clavier visible, images optimisées, états de chargement et messages d'erreur clairs (« Ce pseudo est déjà pris », « Mot de passe incorrect », etc. — pas de message technique brut).
|
||||
|
||||
---
|
||||
|
||||
## 8. Plan de construction (ordre conseillé)
|
||||
|
||||
1. Scaffolder Next.js + TypeScript + Tailwind.
|
||||
2. Configurer le client Supabase (`.env.local`) et le middleware de session.
|
||||
3. Écrire `supabase/schema.sql` (table `profiles` + RLS + bucket `avatars`).
|
||||
4. Auth : pages `/signup` et `/login` avec la logique pseudo → email interne.
|
||||
5. Upload de la photo au signup + stockage dans le bucket + `avatar_url` en base.
|
||||
6. Page `/leaderboard` (lecture de tous les profils, tri par points).
|
||||
7. Page `/profile` (édition pseudo/photo, déconnexion).
|
||||
8. Middleware de protection des routes + redirections.
|
||||
9. Polissage responsive + états vides/chargement/erreurs.
|
||||
10. `README.md` avec instructions de setup et de déploiement.
|
||||
|
||||
---
|
||||
|
||||
## 9. Configuration & commandes
|
||||
|
||||
Variables d'environnement (`.env.local`) :
|
||||
```
|
||||
NEXT_PUBLIC_SUPABASE_URL=...
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
|
||||
```
|
||||
|
||||
Documenter dans le `README.md` : création du projet Supabase, exécution de `schema.sql`, création du bucket `avatars`, lancement (`npm run dev`) et déploiement Vercel.
|
||||
|
||||
---
|
||||
|
||||
## 10. Definition of Done (V1)
|
||||
|
||||
- [ ] On peut créer un compte (pseudo + mot de passe + photo) et être connecté ensuite.
|
||||
- [ ] On peut se déconnecter puis se reconnecter avec pseudo + mot de passe.
|
||||
- [ ] Un pseudo déjà pris est refusé avec un message clair.
|
||||
- [ ] Le leaderboard affiche **tous** les inscrits, triés par points, avec rang/photo/pseudo/points.
|
||||
- [ ] On peut changer sa photo et son pseudo depuis `/profile`.
|
||||
- [ ] Les routes protégées redirigent vers `/login` si non connecté.
|
||||
- [ ] Le site est propre et utilisable sur téléphone **et** ordinateur.
|
||||
- [ ] RLS activée : personne ne peut modifier les points ni le profil d'autrui.
|
||||
|
||||
---
|
||||
|
||||
## 11. Roadmap (ne pas coder maintenant — juste pour garder l'archi extensible)
|
||||
|
||||
Prévoir que ces éléments s'ajouteront ensuite, sans refonte :
|
||||
- **Rôles** : juge/admin (`role` sur `profiles`) → attribution de points, modération.
|
||||
- **Notion d'édition/thème** : une table `editions` (année, nom du thème, mood board, vocabulaire) ; les points deviendront rattachables à une édition.
|
||||
- **Jeu du Tribunal** : le Char (zone de pénalité), batailles de cul sec, ostracisme, roulette, timer du Gardien.
|
||||
- **Temps réel** : leaderboard et fil d'événements live via Supabase Realtime, mode « grand écran » pour vidéoprojecteur.
|
||||
|
||||
Garde le code modulaire (composants réutilisables, accès aux données isolé) pour que ces ajouts restent simples.
|
||||
|
||||
---
|
||||
|
||||
## 12. À reporter dans `CLAUDE.md` (conventions durables du repo)
|
||||
|
||||
- Stack : Next.js (App Router) + TS + Tailwind + Supabase.
|
||||
- Auth = pseudo + mot de passe via email interne dérivé (`slug@letribunal.test`).
|
||||
- Toujours passer par les policies RLS ; ne jamais exposer la `service_role key` côté client.
|
||||
- Design tokens = variables CSS (thème swappable) ; pas de couleur codée en dur dans les composants.
|
||||
- Toujours passer par les policies RLS + trigger ; ne jamais exposer la `service_role key` côté client.
|
||||
- Toute nouvelle colonne sensible sur `profiles` (ou une future table) suit le pattern RLS+trigger de la section 3, pas un `grant`/`revoke` par colonne (qui ne peut pas distinguer les rôles applicatifs).
|
||||
- Aperçus locaux d'image avant upload (crop, fichier choisi) = `blob:`/`data:` URL → toujours `unoptimized` sur `next/image` ou une balise `<img>` classique (`next/image` ne sait pas résoudre ces schémas côté serveur).
|
||||
- Commandes : `npm run dev`, `npm run build`, `npm run lint`.
|
||||
- Garder chaque fonctionnalité future isolée et le schéma SQL versionné dans `supabase/schema.sql`.
|
||||
- Le schéma SQL (`supabase/schema.sql`) est la seule source de vérité pour la base — le tenir à jour à chaque changement de données/sécurité, et le garder idempotent.
|
||||
|
||||
---
|
||||
|
||||
## 8. Roadmap (pas encore codé)
|
||||
|
||||
- **Éditions & thèmes** : table `editions` (année, thème, mood board, vocabulaire) ; les points deviendront rattachables à une édition.
|
||||
- **Le jeu du Tribunal** : le Char (zone de pénalité), batailles de cul sec, ostracisme, roulette, timer du Gardien.
|
||||
- **Mode grand écran** : vue leaderboard optimisée pour vidéoprojecteur.
|
||||
- **Déploiement** : pas encore fait (Vercel + Supabase managé prévus, voir README).
|
||||
|
||||
Reference in New Issue
Block a user