Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e542b09807 | |||
| 86756c41da |
+1
-1
@@ -1,7 +1,7 @@
|
||||
# Le Tribunal — Docker deployment
|
||||
|
||||
# --- Domain ---
|
||||
APP_DOMAIN=acropole.alxczl.fr
|
||||
APP_DOMAIN=agora.alxczl.fr
|
||||
|
||||
# --- Traefik ---
|
||||
TRAEFIK_NETWORK=proxy
|
||||
|
||||
@@ -28,7 +28,7 @@ Hors périmètre pour l'instant (voir roadmap en fin de doc) : éditions/saisons
|
||||
- **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
|
||||
- **Déploiement** : self-hosted, Docker Compose + Traefik (reverse proxy), Supabase (managé) — CI via Gitea Actions (`.gitea/workflows/deploy.yml`) qui build le standalone Next.js et le POST à un webhook (`docker/webhook/receive.py`) déclenchant un hot-swap sans downtime (`docker/entrypoint.sh`) ; domaine injecté par `APP_DOMAIN` (`.env.docker`, gitignored — seul `.env.docker.example` est versionné) dans le label Traefik `Host()`. Domaine actuel : `agora.alxczl.fr`.
|
||||
|
||||
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.
|
||||
|
||||
@@ -125,7 +125,7 @@ Ce pattern (RLS pour l'accès à la ligne + trigger `BEFORE UPDATE` pour l'accè
|
||||
|
||||
## 4. Authentification
|
||||
|
||||
**Sur invitation uniquement**, plus d'auto-inscription publique : "Allow new users to sign up" est désactivé côté Supabase. Un Archonte invite via Authentication → Users → Invite user ; le lien reçu établit une session directement sur `/signup`, qui sert alors à finaliser le compte (mot de passe, pseudo, photo) — sans session valide, la page affiche un message « invitation requise ». Le profil est créé dès l'invitation par le trigger `handle_new_user` (pseudo temporaire `"Nouveau membre"` jusqu'à finalisation). Nécessite un SMTP custom 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 Citoyens — seuls les Archontes le voient, sur `/admin` (via `admin_list_members()`, voir §3).
|
||||
**Sur invitation uniquement**, plus d'auto-inscription publique : "Allow new users to sign up" est désactivé côté Supabase. Un Archonte invite via Authentication → Users → Invite user ; le lien reçu établit une session directement sur `/signup`, qui sert alors à finaliser le compte (mot de passe, pseudo, photo) — sans session valide, la page affiche un message « invitation requise ». Le profil est créé dès l'invitation par le trigger `handle_new_user` (pseudo temporaire `"Nouveau membre " + 8 premiers caractères de l'uuid` jusqu'à finalisation — le suffixe garantit l'unicité quand plusieurs personnes sont invitées avant d'avoir fini leur inscription ; `/signup` détecte l'état « pas encore finalisé » par préfixe, jamais par égalité stricte). Nécessite un SMTP custom 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 Citoyens — seuls les Archontes le voient, sur `/admin` (via `admin_list_members()`, voir §3).
|
||||
|
||||
---
|
||||
|
||||
@@ -197,4 +197,3 @@ Vocabulaire diégétique (toujours accompagné du terme fonctionnel dans le code
|
||||
|
||||
- **Éditions** : table `editions` (année, mood board) ; les points deviendront rattachables à une édition donnée. Le thème visuel (Grèce antique) reste fixe, ce n'est pas un système de re-thématisation par édition.
|
||||
- **Mode grand écran** : vue leaderboard optimisée pour vidéoprojecteur.
|
||||
- **Déploiement** : pas encore fait (Vercel + Supabase managé prévus, voir README).
|
||||
|
||||
@@ -28,7 +28,7 @@ Application web pour animer et scorer le jeu **« Le Tribunal »** entre amis pe
|
||||
| Auth · Base · Stockage · Temps réel | Supabase (Auth, Postgres, Storage, Realtime) |
|
||||
| Recadrage photo | react-easy-crop + canvas (resize/compression client) |
|
||||
| Email transactionnel | SMTP custom (ex. Resend) branché sur Supabase Auth |
|
||||
| Déploiement | Vercel + Supabase (managé) |
|
||||
| Déploiement | Docker Compose + Traefik (self-hosted) + Supabase (managé) |
|
||||
|
||||
---
|
||||
|
||||
@@ -37,7 +37,7 @@ Application web pour animer et scorer le jeu **« Le Tribunal »** entre amis pe
|
||||
- [Node.js](https://nodejs.org/) 20.19+ (Next.js 16 / React 19)
|
||||
- Un compte [Supabase](https://supabase.com/) (offre gratuite suffisante)
|
||||
- Un fournisseur SMTP gratuit (ex. [Resend](https://resend.com/), 100 emails/jour) — le service email intégré de Supabase a un rate limit trop bas pour un usage réel
|
||||
- Un compte [Vercel](https://vercel.com/) pour le déploiement (optionnel en local)
|
||||
- Pour le déploiement (optionnel en local) : un serveur avec Docker + Traefik déjà en place, voir [Déploiement](#déploiement)
|
||||
|
||||
---
|
||||
|
||||
@@ -95,11 +95,17 @@ L'application tourne sur [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
---
|
||||
|
||||
## Déploiement (Vercel)
|
||||
## Déploiement (Docker + Traefik)
|
||||
|
||||
1. Importez le dépôt dans Vercel.
|
||||
2. Renseignez les variables d'environnement (`NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY`).
|
||||
3. Déployez. Vercel rebuild automatiquement à chaque push sur la branche principale.
|
||||
Self-hosted, pas de Vercel. Un serveur avec Docker Compose et un reverse proxy [Traefik](https://traefik.io/) déjà en place (réseau externe partagé, voir `TRAEFIK_NETWORK`), plus un pipeline Gitea Actions qui build et déploie automatiquement à chaque push sur la branche principale.
|
||||
|
||||
1. Sur le serveur, copiez `.env.docker.example` en `.env.docker` et renseignez :
|
||||
- `APP_DOMAIN` : le domaine sur lequel l'app doit répondre (utilisé dans le label Traefik `Host(\`${APP_DOMAIN}\`)`).
|
||||
- `TRAEFIK_NETWORK` : le nom du réseau Docker externe sur lequel Traefik écoute.
|
||||
- `WEBHOOK_SECRET` : secret partagé avec le workflow Gitea Actions (`openssl rand -hex 16`).
|
||||
2. `docker compose up -d` démarre le conteneur `app` (Next.js standalone) et le petit serveur webhook (`docker/webhook/receive.py`) qui reçoit les déploiements.
|
||||
3. Le workflow `.gitea/workflows/deploy.yml` build l'app à chaque push sur `main` et POST le build vers le webhook, qui déclenche un hot-swap (voir `docker/entrypoint.sh`) sans downtime — pas de rebuild manuel nécessaire une fois configuré.
|
||||
4. **Changer de domaine** : mettez à jour `APP_DOMAIN` dans `.env.docker` sur le serveur, puis recréez le conteneur (`docker compose up -d --force-recreate app`) pour que Traefik prenne en compte le nouveau label `Host()`. Pensez aussi à mettre à jour, côté Supabase Dashboard, **Authentication → URL Configuration** (Site URL + Redirect URLs) et le template d'email d'invitation (logo en dur), sans quoi les liens d'invitation et le logo pointeront encore vers l'ancien domaine.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function SignupPage() {
|
||||
.eq("id", user.id)
|
||||
.single();
|
||||
|
||||
if (profile && profile.pseudo !== "Nouveau membre") {
|
||||
if (profile && !profile.pseudo.startsWith("Nouveau membre")) {
|
||||
window.location.href = "/leaderboard";
|
||||
return;
|
||||
}
|
||||
@@ -147,7 +147,14 @@ export default function SignupPage() {
|
||||
}
|
||||
|
||||
if (status === "checking") {
|
||||
return <div className="flex flex-1 items-center justify-center px-4 py-12" />;
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center px-4 py-12">
|
||||
<div className="marble-surface w-full max-w-sm rounded-2xl border border-gold/40 p-6 text-center shadow-xl sm:p-8">
|
||||
<AgoraLogo className="mx-auto mb-3 h-16 w-16 animate-pulse" />
|
||||
<p className="text-sm text-text-marble/80">Connexion à ton invitation…</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === "no-invite") {
|
||||
|
||||
@@ -47,6 +47,7 @@ export async function updateSession(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
const isProtected = PROTECTED_PATHS.some((path) => pathname.startsWith(path));
|
||||
const isAuthPath = AUTH_PATHS.some((path) => pathname.startsWith(path));
|
||||
const isSignup = pathname.startsWith("/signup");
|
||||
|
||||
if (!user && isProtected) {
|
||||
const url = request.nextUrl.clone();
|
||||
@@ -54,6 +55,26 @@ export async function updateSession(request: NextRequest) {
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
|
||||
// Le token d'un lien d'invitation Supabase s'établit dès qu'il touche le
|
||||
// navigateur (detectSessionInUrl du SDK), sur N'IMPORTE QUELLE page, pas
|
||||
// seulement /signup. Si le compte n'a pas encore fini son inscription
|
||||
// (pseudo toujours au placeholder du trigger handle_new_user), on le
|
||||
// renvoie systématiquement vers /signup pour ne jamais le laisser bloqué
|
||||
// connecté mais sans mot de passe choisi.
|
||||
if (user && !isSignup) {
|
||||
const { data: profile } = await supabase
|
||||
.from("profiles")
|
||||
.select("pseudo")
|
||||
.eq("id", user.id)
|
||||
.single();
|
||||
|
||||
if (profile?.pseudo.startsWith("Nouveau membre")) {
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = "/signup";
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
}
|
||||
|
||||
if (user && isAuthPath) {
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = "/leaderboard";
|
||||
|
||||
+9
-1
@@ -147,6 +147,11 @@ create trigger profiles_before_update
|
||||
-- Création automatique du profil à l'inscription (auth.users → profiles),
|
||||
-- indépendamment de l'état de la session (fonctionne même si l'email
|
||||
-- n'est pas encore confirmé, contrairement à un insert fait depuis le client).
|
||||
-- Le pseudo temporaire inclut les 8 premiers caractères de l'uuid : un
|
||||
-- littéral fixe "Nouveau membre" pour tout le monde entrait en collision
|
||||
-- avec profiles_pseudo_lower_key (unique) dès qu'une 2e personne était
|
||||
-- invitée avant que la 1re ait fini son inscription — ce qui n'était
|
||||
-- jamais arrivé avant d'inviter plusieurs personnes d'affilée.
|
||||
create or replace function public.handle_new_user()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
@@ -154,7 +159,10 @@ security definer set search_path = public
|
||||
as $$
|
||||
begin
|
||||
insert into public.profiles (id, pseudo)
|
||||
values (new.id, coalesce(new.raw_user_meta_data ->> 'pseudo', 'Nouveau membre'));
|
||||
values (
|
||||
new.id,
|
||||
coalesce(new.raw_user_meta_data ->> 'pseudo', 'Nouveau membre ' || substr(new.id::text, 1, 8))
|
||||
);
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
Reference in New Issue
Block a user