OpenTrailPaper
LilyGO T5S3 4.7" E-Paper PRO용 오픈소스 자전거 컴퓨터 펌웨어, 오프라인 지도·GPX 경로·FIT 기록·블루투스 센서 지원
저장소 정보는 아직 수집하지 않았습니다.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Application web du foot en salle du lundi (5 contre 5). Chaque semaine, les joueurs élisent anonymement leurs 5 meilleurs joueurs (classés du 1er au 5e) et le joueur le plus nul. Le mardi matin, l’application publie la Team of the Week sous forme de cartes façon FIFA Ultimate Team, avec le « Boulet de la semaine ». Chaque mois, le joueur le plus souvent 1er est élu « Joueur du mois », et le plus souvent dernier porte un dossard « Joueur le plus nul du mois » pendant tout le mois suivant.
| Domaine | Choix |
|---|---|
| Framework | Next.js 15 (App Router, Server Components, Server Actions), TypeScript strict |
| Style | Tailwind CSS 3, polices Bebas Neue + Inter |
| Base de données | PostgreSQL (Neon ou Supabase) via Prisma ORM 6 |
| Authentification | Auth.js v5 (NextAuth) : e-mail + mot de passe et Google OAuth, sessions JWT |
| Mots de passe | bcrypt (12 tours) |
| Validation | Zod sur toutes les entrées |
| Photos de profil | Vercel Blob |
| Tâches planifiées | Vercel Cron |
| Tests | Vitest (algorithmes de classement, récompenses, fenêtres de vote) |
/admin et
par session.├── prisma/
│ ├── schema.prisma # schéma complet (commenté)
│ ├── migrations/ # migrations SQL versionnées
│ └── seed.ts # création de l'ADMIN initial (+ joueurs de démo en local)
├── src/
│ ├── middleware.ts # 1re barrière : connexion requise, /admin réservé à l'ADMIN
│ ├── auth.config.ts # config Auth.js compatible Edge (middleware)
│ ├── auth.ts # Auth.js complet : Google, Credentials, adaptateur Prisma, jeton resynchronisé
│ ├── actions/ # Server Actions (auth, compte, vote, admin)
│ ├── app/
│ │ ├── page.tsx # / accueil
│ │ ├── login/ register/ # /login /register
│ │ ├── onboarding/ # choix du surnom après une 1re connexion Google
│ │ ├── account/ # /account
│ │ ├── sessions/ [id]/ # /sessions /sessions/[id]
│ │ ├── vote/[sessionId]/ # /vote/[sessionId]
│ │ ├── team-of-the-week/ # /team-of-the-week (public, après publication)
│ │ ├── monthly/ # /monthly
│ │ ├── admin/setup/ # /admin/setup (initialisation unique de l'ADMIN)
│ │ ├── admin/(panel)/ # /admin, /admin/sessions(/[id]), /admin/players, /admin/results, /admin/rewards
│ │ └── api/
│ │ ├── auth/[...nextauth] # routes Auth.js
│ │ ├── avatar/ # envoi / suppression de la photo (Vercel Blob)
│ │ ├── security/capture/ # journal des tentatives de capture
│ │ └── cron/ # publish-totw (mardi) et monthly-rewards (1er du mois)
│ ├── components/
│ │ ├── ui/ # Button, Field, Alert, Badge, PageHeader, StatTile…
│ │ ├── fut/ # FutCard, FutsalPitch, PlayerAvatar, ShareButton
│ │ ├── security/ # AntiCapture, Watermark
│ │ ├── vote/ # VoteForm
│ │ ├── admin/ account/ auth/ layout/
│ ├── lib/
│ │ ├── results/ # compute.ts (classement, fonction pure), draw.ts (tirage traçable), rating.ts, publish.ts
│ │ ├── rewards/ # monthly.ts (règles, fonction pure), compute-month.ts
│ │ ├── voting/ # window.ts (fenêtre de vote), submit.ts (enregistrement anonyme)
│ │ ├── validations/ # schémas Zod
│ │ ├── guards.ts # requireUser / requireAdmin / contrôle d'origine
│ │ ├── dates.ts settings.ts queries.ts prisma.ts blob.ts token-resync.ts
│ └── types/next-auth.d.ts
├── tests/ # tests Vitest
├── vercel.json # crons
├── next.config.mjs # en-têtes de sécurité (CSP, HSTS…)
└── .env.example
Tables principales : User, Account, GameSession, SessionParticipant, VoteReceipt, VoteTally,
SessionResult, TieBreakDraw, MonthlyReward, AppSetting, ScreenshotEvent, AdminAuditLog.
Aucun bulletin de vote n’est stocké. Quand un joueur vote, une transaction unique :
VoteReceipt (sessionId, userId) : il indique seulement qui a voté, sans contenu ni heure, et sa clé
primaire empêche le double vote au niveau de la base ;VoteTally (sessionId, playerId, top1…top5, worst).Le lien « A a voté pour B » n’existe donc nulle part : ni l’ADMIN, ni personne lisant la base ne peut le reconstituer. L’ADMIN ne voit que le nombre de votants pendant le vote, puis les totaux par joueur après la fermeture.
Contrepartie : un recomptage bulletin par bulletin est impossible. Pour garder un anonymat parfait, ne journalisez pas les requêtes SQL en production (Neon et Supabase ne le font pas par défaut).
Dans les formulaires, les joueurs sont identifiés par l’id de leur participation, jamais par leur id utilisateur : aucun identifiant de compte (ADMIN compris) n’est exposé dans l’interface joueur.
src/lib/results/compute.ts)TieBreakDraw ; l’ordre est obtenu en triant
sha256(graine + ":" + idJoueur). Avec la graine, n’importe qui peut rejouer et vérifier le tirage (visible dans
/admin/results).60 + 30 × (part des votants l’ayant mis dans son Top 5) + 9 × (part de 1res places), plafonnée à
99. Pour le Boulet : de 59 à 30 selon la part de votes « plus nul ».src/lib/rewards/monthly.ts, src/lib/honors.ts)/admin/rewards (ex. après une session publiée en retard).Prérequis : Node.js 22 et PostgreSQL (local, Docker, ou une base Neon / Supabase gratuite).
git clone https://github.com/sadaissa/Futsal-Beausoleil-.git
cd Futsal-Beausoleil-
npm install
cp .env.example .env
# Renseigne au minimum DATABASE_URL, DIRECT_URL, AUTH_SECRET, ADMIN_SETUP_SECRET, CRON_SECRET
# AUTH_SECRET : openssl rand -base64 32
npx prisma migrate dev # crée les tables
SEED_DEMO_PLAYERS=10 npm run db:seed # facultatif : 10 joueurs de démo (mot de passe "demo-password")
npm run dev # http://localhost:3000
PostgreSQL rapide avec Docker :
docker run --name bu-postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=beausoleil -p 5432:5432 -d postgres:16
# DATABASE_URL="postgresql://postgres:postgres@localhost:5432/beausoleil"
# DIRECT_URL=même valeur
Sans BLOB_READ_WRITE_TOKEN, tout fonctionne sauf l’envoi de photos (les cartes affichent les initiales).
Sans AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET, le bouton Google est masqué.
| Variable | Obligatoire | Description |
|---|---|---|
DATABASE_URL | ✅ | URL PostgreSQL utilisée par l’application (URL poolée sur Neon / Supabase) |
DIRECT_URL | ✅ | URL directe pour les migrations (identique à DATABASE_URL en local) |
AUTH_SECRET | ✅ | Secret de chiffrement des sessions (openssl rand -base64 32) |
AUTH_URL | ➖ | URL publique (inutile sur Vercel) |
AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET | ➖ | Identifiants OAuth Google |
ADMIN_SETUP_SECRET | ✅ | Clé (12 caractères minimum) pour devenir ADMIN via /admin/setup, utilisable une seule fois |
ADMIN_EMAIL / ADMIN_NICKNAME / ADMIN_PASSWORD | ➖ | Alternative : création de l’ADMIN par npm run db:seed |
BLOB_READ_WRITE_TOKEN | ➖ | Jeton Vercel Blob (photos de profil) |
CRON_SECRET | ✅ en prod | Protège /api/cron/* (Vercel l’envoie automatiquement) |
APP_TIMEZONE | ➖ | Fuseau horaire (défaut Europe/Paris) |
VOTE_START_TIME / VOTE_END_TIME | ➖ | Horaires par défaut du vote (défaut 20:00 → 08:00, soit le lendemain) |
Méthode recommandée : crée-le toi-même, dès le déploiement.
ADMIN_SETUP_SECRET (longue et aléatoire, par exemple openssl rand -hex 24)./admin/setup et saisis la clé. Tu deviens ADMIN./admin/setup est définitivement désactivée : personne d’autre ne peut prendre le
contrôle. Aucune interface ne permet de promouvoir un autre ADMIN ; seule une modification directe en base le
permet (UPDATE "User" SET role='ADMIN' WHERE email='…').Alternative (seed) : [email protected] ADMIN_PASSWORD=… npm run db:seed. Le seed refuse de créer un
second ADMIN.
Fais-le immédiatement après la mise en ligne : tant qu’aucun ADMIN n’existe, toute personne connaissant la clé pourrait l’utiliser. Après la création, tu peux supprimer
ADMIN_SETUP_SECRET.
Pour compléter une équipe, l’ADMIN peut ajouter des joueurs invités ponctuels, sans compte :
/admin/sessions/[id], « Ajouter un invité ») : l’invité est créé et coché comme
participant ;/admin/players (section « Invités »), puis en le cochant dans une session.Règles :
.invalid, sans mot de passe) ;L’application n’envoie pas d’e-mails (pas de service d’envoi configuré). Un joueur qui a oublié son mot de passe a deux solutions :
/account./admin/players, le bouton « Réinitialiser le mot de passe » génère un mot de
passe temporaire, affiché une seule fois, que l’ADMIN transmet au joueur (WhatsApp, SMS…). Le joueur est
déconnecté de tous ses appareils et doit choisir un nouveau mot de passe à sa prochaine connexion
(/change-password). Le mot de passe temporaire n’est jamais stocké en clair ni journalisé ; seule l’action
apparaît dans le journal ADMIN. Le compte ADMIN ne peut pas être réinitialisé depuis l’interface.http://localhost:3000 et https://ton-domaine.vercel.app.http://localhost:3000/api/auth/callback/googlehttps://ton-domaine.vercel.app/api/auth/callback/googleAUTH_GOOGLE_ID et AUTH_GOOGLE_SECRET.Lors d’une première connexion Google, le joueur choisit son surnom sur /onboarding.
Si un compte e-mail / mot de passe non vérifié existait avec la même adresse, la connexion Google prouve la propriété
de l’adresse : le mot de passe est alors supprimé et les anciennes sessions sont révoquées (protection contre la
pré-création malveillante d’un compte). Le joueur peut redéfinir un mot de passe depuis /account.
DATABASE_URL = chaîne pooled (hôte -pooler), DIRECT_URL = chaîne directe.DATABASE_URL = port 6543 avec ?pgbouncer=true, DIRECT_URL = port 5432.npm run vercel-build (génère Prisma, applique les migrations puis compile).BLOB_READ_WRITE_TOKEN est
ajouté automatiquement).vercel.json, activés automatiquement au déploiement en production :
/api/cron/publish-totw : mardi à 07:10 UTC (08:10 en hiver, 09:10 en été à Paris), après la fermeture du vote
à 08h00 ;/api/cron/monthly-rewards : le 1er du mois à 07:20 UTC.
Définis CRON_SECRET : Vercel l’envoie dans l’en-tête Authorization.
En filet de sécurité, la page /team-of-the-week publie aussi les sessions dont le vote est terminé./admin/setup (voir plus haut).src/middleware.ts) : pages privées réservées aux utilisateurs connectés, /admin/* au rôle
ADMIN. Chaque page et chaque Server Action ADMIN revérifie le rôle en base (requireAdmin / assertAdmin).Origin ;
Auth.js utilise son propre jeton CSRF.dangerouslySetInnerHTML, CSP stricte (next.config.mjs),
X-Frame-Options: DENY, HSTS.AdminAuditLog, sans aucune donnée de vote.⚠️ Aucune application web ne peut garantir à 100 % l’impossibilité de faire une capture d’écran. Un navigateur n’a pas accès aux fonctions du système d’exploitation : une capture via le système, un autre appareil qui photographie l’écran, ou des outils de développement contournent toujours ces protections.
Les mesures mises en place réduisent le risque et découragent le partage sur les pages de vote et de résultats
(/vote/*, /sessions/*, /admin/results) :
PrintScreen, Cmd+Shift+3/4/5, Win+Shift+S (si le navigateur transmet la touche) et Ctrl+P :
contenu flouté, avertissement « Les captures d’écran des votes et résultats sont interdites. » et événement
journalisé (ScreenshotEvent, visible dans /admin) ;Permissions-Policy: display-capture=() : la page ne peut pas être enregistrée par l’API de capture d’écran du
navigateur.Sur mobile, les captures système (boutons physiques) ne sont pas détectables par une page web : seul le filigrane s’applique alors.
La page Team of the Week est volontairement partageable : elle n’est visible qu’une fois le vote clos et ne contient que des résultats agrégés.
| Commande | Rôle |
|---|---|
npm run dev | Serveur de développement |
npm run build / npm start | Build et serveur de production |
npm run vercel-build | Build Vercel (migrations incluses) |
npm run lint / npm run typecheck | ESLint / TypeScript |
npm test | Tests Vitest |
npm run db:migrate | Nouvelle migration en développement |
npm run db:deploy | Applique les migrations (production) |
npm run db:seed | ADMIN initial (+ joueurs de démo avec SEED_DEMO_PLAYERS) |
npm run db:studio | Explorateur Prisma |