🔐 Admin : comptes et authentification
LocalTown gère de vrais comptes : Google ou e-mail + mot de passe. Un compte suit la personne sur tous ses appareils, et son identifiant devient son identité dans les espaces (bureau, messages vocaux, rôle).
Tant que la base D1 et le secret AUTH_SECRET ne sont pas configurés, LocalTown reste dans l'ancien mode (sans compte, identité par navigateur). Tu peux donc déployer le code avant de tout configurer.
Dès que les comptes sont actifs, la connexion est obligatoire dans tous les espaces. Seule exception : les liens invités à durée limitée créés par un admin. Les personnes d'un domaine autorisé (dotworld.ch pour les espaces Dotworld) entrent automatiquement ; les autres demandent l'accès et un admin accepte ou refuse.
Mise en route (dans l'ordre)#
# 1. Base D1 (note l'id affiché, ajoute le bloc "d1_databases" dans wrangler.jsonc, voir §1)
npx wrangler d1 create localtown
# 2. Tables : 0001 (comptes) puis 0002 (demandes d'accès)
npx wrangler d1 migrations apply localtown --remote
# 3. Secret de signature
openssl rand -base64 48 | npx wrangler secret put AUTH_SECRET
# 4. Google (voir §4)
npx wrangler secret put GOOGLE_CLIENT_ID
npx wrangler secret put GOOGLE_CLIENT_SECRET
# 5. E-mails : domaine localtown.io onboardé (§3), puis bloc "send_email" dans wrangler.jsonc
# 6. Déployer
npm run deploy
Après le déploiement : ouvre un espace existant avec ton ancien lien administrateur (…/s/<espace>#admin=<clé>) pour en devenir propriétaire (voir « Anciens espaces »).
Ce qu'il faut configurer#
| Élément | Type | Obligatoire | Rôle |
|---|---|---|---|
Base D1 localtown (binding DB) | binding | ✅ | Comptes, sessions, membres, invitations, demandes d'accès, journal de sécurité |
AUTH_SECRET | secret | ✅ | Signe l'identité transmise aux espaces, protège les adresses IP dans les journaux |
Binding EMAIL | binding | ✅ pour e-mail + mot de passe et les demandes d'accès | E-mails de confirmation, réinitialisation, invitation, demandes d'accès (aux admins), acceptation / refus |
AUTH_EMAIL_FROM | variable | non | Expéditeur. Par défaut LocalTown <connexion@localtown.io> |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET | secrets | pour Google | Bouton « Continuer avec Google » |
AUTH_ORIGIN | variable | ✅ (déjà dans wrangler.jsonc) | Adresse publique https://app.localtown.io, utilisée dans les liens des e-mails. L'ancienne adresse localtown.gregoire-ohanessian.workers.dev continue de marcher (connexion Google comprise) |
AUTH_DEFAULT_DOMAINS | variable | non | Domaines autorisés donnés aux espaces créés avant les comptes (défaut dotworld.ch, séparés par des virgules) |
AUTH_GOOGLE_HD | variable | non | N'accepter que les comptes Google d'un domaine Workspace (ex : dotworld.ch). À ne pas mettre si des externes doivent pouvoir demander l'accès avec Google |
AUTH_HIBP | variable | non | 1 : refuser aussi les mots de passe présents dans des fuites connues (Have I Been Pwned, seuls 5 caractères d'empreinte sortent) |
AUTH_PBKDF2_ITER | variable | non | Coût du hachage des mots de passe (défaut 600 000) |
AUTH_ALLOWED_ORIGINS | variable | non | Autres adresses autorisées à appeler l'API (séparées par des virgules), ex : une préproduction |
AUTH_DEV_MAIL_LOG | variable | développement seulement | 1 : les e-mails sont écrits dans la console de wrangler dev au lieu d'être envoyés. Jamais en production |
AUTH_REQUIRED n'existe plus : la connexion est toujours obligatoire une fois les comptes actifs.
1. Créer la base D1#
npx wrangler d1 create localtown
Ajoute ce bloc dans wrangler.jsonc avec l'identifiant affiché. Il n'y est pas par défaut : un database_id factice fait échouer wrangler deploy.
"d1_databases": [
{ "binding": "DB", "database_name": "localtown", "database_id": "<id affiché>", "migrations_dir": "migrations" }
]
Puis crée les tables (à refaire après chaque nouvelle migration dans migrations/) :
npx wrangler d1 migrations apply localtown --remote # production
npx wrangler d1 migrations apply localtown --local # pour wrangler dev
La commande n'applique que les migrations pas encore passées (0001_auth.sql, puis 0002_access_requests.sql) : tu peux la relancer sans risque. Pour vérifier : npx wrangler d1 migrations list localtown --remote.
2. Le secret de signature#
openssl rand -base64 48 | npx wrangler secret put AUTH_SECRET
En local, mets une valeur quelconque dans .dev.vars (jamais commité) : AUTH_SECRET=dev-.... Ajoute aussi AUTH_ORIGIN=http://localhost:8787 (sinon les liens des e-mails pointent vers app.localtown.io) et AUTH_DEV_MAIL_LOG=1 pour lire les e-mails (liens de confirmation, demandes d'accès) dans le terminal de wrangler dev.
⚠️ Changer AUTH_SECRET ne déconnecte personne, mais invalide les connexions aux espaces en cours pendant quelques secondes (elles se reconnectent seules).
3. Envoi des e-mails (Cloudflare Email Service)#
LocalTown envoie ses e-mails avec le binding Workers send_email de Cloudflare Email Service (en bêta). Pour écrire à n'importe quelle adresse (et pas seulement aux adresses de destination vérifiées du compte), il faut deux choses : un domaine d'envoi onboardé et l'offre Workers Paid.
- Tableau de bord Cloudflare (compte Dotworld) : Compute → Email Service → Email Sending → Onboard Domain.
- Choisis
localtown.io(déjà sur Cloudflare DNS). Cloudflare ajoute lui-même les enregistrements MX (retours), SPF, DKIM et DMARC (_dmarc.localtown.io). Compte 5 à 15 minutes (jusqu'à 24 h) avant que le domaine passe en Verified. - Une fois vérifié, ajoute le binding dans
wrangler.jsonc(il n'y est pas par défaut) :"send_email": [{ "name": "EMAIL" }] - L'expéditeur par défaut est
LocalTown <connexion@localtown.io>. Pour en changer (une adresse du domaine vérifié) :"vars": { "AUTH_ORIGIN": "https://app.localtown.io", "AUTH_EMAIL_FROM": "LocalTown <bonjour@localtown.io>" } - Déploie, puis teste avec Mot de passe oublié sur ton adresse.
Sans binding, les comptes Google marchent, mais l'inscription par e-mail ne peut pas envoyer le lien de confirmation, et les admins ne reçoivent pas les demandes d'accès par e-mail (elles restent visibles dans l'app). Le journal du Worker affiche email not configured.
En local : AUTH_DEV_MAIL_LOG=1 dans .dev.vars écrit les e-mails dans la console. Pour tester le vrai service depuis ton poste, ajoute "remote": true au binding send_email (il appelle alors Cloudflare).
| Envoyé à | Quand | |
|---|---|---|
| Confirmez votre adresse | la personne | inscription par e-mail |
| Réinitialiser votre mot de passe | la personne | « Mot de passe oublié » |
| … vous invite dans « … » | l'invité | invitation par e-mail |
| … demande à rejoindre « … » | propriétaires et admins de l'espace | nouvelle demande d'accès (10 e-mails max par heure et par espace) |
| Bienvenue dans « … » | le demandeur | demande acceptée |
| Votre demande pour « … » | le demandeur | demande refusée |
4. Google (« Continuer avec Google »)#
Cette connexion ne demande que openid email profile. Le connecteur Google Agenda (voir Intégrations) a son propre accès, demandé plus tard.
- Ouvre console.cloud.google.com et crée (ou choisis) un projet, par exemple « LocalTown ».
- API et services → Écran de consentement OAuth (ou Google Auth Platform → Branding) :
- type Interne si seuls les comptes de ton Google Workspace doivent se connecter, sinon Externe ;
- nom de l'application :
LocalTown, e-mail d'assistance, logo (facultatif) ; - type Externe pour les espaces Dotworld : les personnes hors
dotworld.chdoivent pouvoir se connecter pour demander l'accès ; - domaines autorisés :
localtown.io(workers.devn'est pas accepté comme domaine vérifié) ; - Accès aux données : ajoute seulement
openid,.../auth/userinfo.email,.../auth/userinfo.profile.
- API et services → Identifiants → Créer des identifiants → ID client OAuth :
- type d'application : Application Web ;
- URI de redirection autorisés (exactement, sans barre finale) :
Pour URI Production (web, application desktop et mobile) https://app.localtown.io/api/auth/google/callbackAncienne adresse, toujours en service https://localtown.gregoire-ohanessian.workers.dev/api/auth/google/callbackDéveloppement avec wrangler devhttp://localhost:8787/api/auth/google/callback(ethttp://localhost:8851/api/auth/google/callbacksi tu utilises ce port)Développement avec npm run dev(Vite)http://localhost:5173/api/auth/google/callbackLa connexion Google revient sur l'adresse d'où elle est partie (en local :
AUTH_ORIGINde.dev.vars) : chaque adresse utilisée doit être déclarée.Les applications desktop et mobile passent par le navigateur du système puis reviennent avec le lien
localtown://auth/callback: aucune autre adresse n'est à déclarer chez Google. - Origines JavaScript autorisées : inutile (tout se passe côté serveur).
- Copie l'ID client et le code secret :
npx wrangler secret put GOOGLE_CLIENT_ID npx wrangler secret put GOOGLE_CLIENT_SECRET - Pour une application Externe, clique sur Publier l'application (sinon seuls les « utilisateurs test » peuvent se connecter).
Pour réserver Google aux comptes de l'entreprise : "AUTH_GOOGLE_HD": "dotworld.ch". Les autres comptes Google voient « Ce compte Google n'appartient pas au domaine de l'entreprise ».
Comptes existants#
- Une personne déjà inscrite par e-mail (adresse confirmée) qui clique sur Continuer avec Google avec la même adresse : Google est simplement ajouté à son compte.
- Si l'adresse n'avait jamais été confirmée, Google prouve que c'est bien sa boîte : le mot de passe non vérifié est retiré (protection contre quelqu'un qui aurait réservé l'adresse d'un autre). La personne peut en redéfinir un avec Mot de passe oublié.
5. Applications desktop et mobile#
Google bloque la connexion dans les vues web intégrées (disallowed_useragent). Les applications ouvrent donc le navigateur du système, puis reviennent via localtown://auth/callback?code=… : l'application échange ce code à usage unique (2 minutes, lié à un secret gardé par l'application) contre sa session.
Ce qu'il faut côté applications (fait par l'équipe desktop / mobile, rappelé ici pour la mise en production) :
- Desktop (Tauri) : autoriser l'application hébergée à ouvrir le navigateur (
opener:allow-open-url) et transformerlocaltown://auth/callback?…en<adresse>/auth/native?…dans la fenêtre. - Mobile (Capacitor) : le plugin
@capacitor/browser(onglet de navigateur système) et le schémalocaltown://(déjà déclaré parmobile/scripts/configure-native.mjs).
Règles d'accès des espaces#
Il faut toujours un compte (Google ou e-mail + mot de passe) pour entrer dans un espace. Seul un lien invité permet d'entrer sans compte.
Chaque espace a ses règles d'accès, réglables par ses propriétaires et administrateurs dans 🛡️ Administration → Membres & accès → Qui peut entrer ? :
| Réglage | Effet |
|---|---|
Domaines e-mail autorisés (ex : dotworld.ch) | Toute personne avec une adresse confirmée de ce domaine, ou un compte Google Workspace de ce domaine (claim hd), entre automatiquement comme membre. *.groupe.com accepte aussi les sous-domaines (eu.groupe.com). Une adresse non confirmée ne compte jamais. |
| Les autres personnes… | demandent l'accès (défaut : un admin valide), n'entrent que sur invitation, ou entrent directement (tout compte connecté). |
| Autoriser les liens invités | Les admins peuvent créer des liens d'accès sans compte, valables 2 h, 24 h, 7 ou 30 jours. |
Un nouvel espace prend le domaine de son créateur : greg@dotworld.ch crée un espace → domaine autorisé dotworld.ch, les autres demandent l'accès. Les messageries publiques (gmail.com, outlook.com, icloud.com…) ne deviennent jamais un domaine autorisé.
Demandes d'accès#
Une personne connectée hors des domaines autorisés voit Demander l'accès, avec un message facultatif. Ensuite :
- Les propriétaires et admins présents dans l'espace reçoivent une notification « … demande à entrer » avec un bouton Voir. Tous reçoivent aussi un e-mail.
- Dans 🛡️ Administration → Membres & accès → Demandes en attente (le nombre de demandes s'affiche à côté de la section) : avatar, nom, e-mail, domaine et message de chaque demande.
- Accepter : choisis Membre ou Invité. La personne entre toute seule si elle attend sur la page, et reçoit un e-mail.
- Toujours accepter @domaine (case à cocher avant Accepter) : ajoute le domaine aux domaines autorisés. Les autres demandes en attente de ce domaine sont acceptées en même temps. Impossible pour une messagerie publique.
- Refuser : la personne reçoit un e-mail et peut redemander 24 heures après.
- Limites : une seule demande en attente par personne et par espace, 10 demandes par personne et par jour (tous espaces), 30 demandes par espace et par heure.
Une invitation par e-mail passe avant tout : la personne invitée entre sans demander. Un admin peut donc aussi répondre à une demande en invitant la personne.
Inviter#
Membres → Inviter : colle une ou plusieurs adresses, choisis le rôle (Membre, Invité, ou Administrateur pour un propriétaire) puis Inviter. Chaque personne reçoit un e-mail « … vous invite dans « … » ». L'invitation vaut 7 jours et s'applique dès qu'elle se connecte avec cette adresse (confirmée), même hors des domaines autorisés. Créer un lien invité copie un lien à partager ; Annuler le désactive tout de suite.
Rôles#
| Rôle | Peut |
|---|---|
| Propriétaire | Tout, y compris nommer des administrateurs et d'autres propriétaires. Un espace garde toujours au moins un propriétaire. |
| Administrateur | Régler l'espace, les accès et les invitations, traiter les demandes, changer le rôle des membres et invités, retirer des membres et invités |
| Membre | Utiliser l'espace, voir la liste des membres, quitter l'espace |
| Invité | Entrer dans l'espace (droits réduits dans le monde) |
Créer un espace fait de toi son propriétaire.
Anciens espaces (créés avant les comptes)#
Les espaces vivent dans des Durable Objects : la base D1 ne les connaît pas. Leur règle d'accès est donc créée à la première visite après l'activation des comptes, une seule fois (INSERT OR IGNORE, sans jamais écraser un réglage existant) :
- connexion obligatoire ;
- domaine autorisé
dotworld.ch(ouAUTH_DEFAULT_DOMAINS) ; - demande d'accès pour tous les autres, liens invités autorisés.
La migration 0002_access_requests.sql ajoute les colonnes require_approval (défaut 1) et legacy, la table access_requests, et coupe l'ancien réglage « entrer sans compte » sur les espaces déjà réglés. Dans Membres, ces espaces affichent « Espace créé avant les comptes » tant qu'un admin n'a pas enregistré ses règles.
Prendre la propriété : connecte-toi, puis ouvre l'espace avec ton ancien lien administrateur (…/s/<espace>#admin=<clé>). La clé est échangée contre le rôle de propriétaire (ou d'administrateur s'il y a déjà un propriétaire), même si ton adresse n'est pas dans les domaines autorisés, et ta demande éventuelle est close. Règle ensuite les accès dans Membres.
Sécurité#
- Mots de passe : PBKDF2-SHA256, 600 000 itérations, sel unique par compte. Cloudflare limite un calcul à 100 000 itérations : LocalTown enchaîne 6 passes de 100 000 (même coût pour un attaquant). Mesuré à environ 60 à 80 ms de calcul par connexion : il faut l'offre Workers Paid (le plan gratuit limite à 10 ms de calcul par requête). Le nombre d'itérations est stocké avec chaque mot de passe : l'augmenter (
AUTH_PBKDF2_ITER) met à jour chaque compte à sa prochaine connexion. - Règles : 10 caractères minimum, liste de mots de passe courants refusés, option Have I Been Pwned.
- Sessions : cookie
lt_session(HttpOnly,Secure,SameSite=Lax), 30 jours glissants, renouvelé à chaque connexion. Seule l'empreinte SHA-256 est stockée, comme pour les liens de confirmation, de réinitialisation, les invitations et les jetons d'accès. - Protection CSRF : vérification de l'origine + jeton double (
lt_csrf/ en-têtex-lt-csrf). - Limitation : par adresse IP et par e-mail, avec blocage progressif du compte après 5 échecs (1 min, 2 min, 4 min… jusqu'à 1 h). Les messages ne révèlent jamais si un compte existe.
- Journal de sécurité (table
audit_log, gardé 400 jours) : connexions, échecs, réinitialisations, changements de rôle, invitations, jetons, demandes d'accès (access.request,access.accept,access.refuse). Jamais de mot de passe ni de jeton. Pour le consulter :npx wrangler d1 execute localtown --remote --command "SELECT datetime(at/1000,'unixepoch') AS quand, event, user_id, space_id, detail FROM audit_log ORDER BY at DESC LIMIT 50" - Désactiver un compte :
npx wrangler d1 execute localtown --remote --command "UPDATE users SET disabled = 1 WHERE email = 'x@y.z'"(ses sessions et jetons cessent de marcher immédiatement).
Dépannage#
| Symptôme | Cause probable |
|---|---|
| « Les comptes ne sont pas encore configurés sur ce serveur » | Binding DB ou secret AUTH_SECRET manquant |
Google : redirect_uri_mismatch | L'adresse utilisée (ex : https://app.localtown.io ou l'adresse workers.dev) n'est pas déclarée chez Google avec /api/auth/google/callback |
Un collègue @dotworld.ch doit demander l'accès | Son adresse n'est pas confirmée (lien reçu par e-mail), ou le domaine n'est pas dans Qui peut entrer ? |
| Les admins ne reçoivent pas les demandes par e-mail | Binding EMAIL absent ou domaine non vérifié. Les demandes restent dans Membres → Demandes en attente |
| Google : « Accès bloqué : cette appli n'a pas été validée » | Écran de consentement Externe non publié : publie-le ou ajoute des utilisateurs test |
| Aucun e-mail reçu | Domaine localtown.io non vérifié dans Email Service (E_SENDER_NOT_VERIFIED dans les journaux), binding send_email absent, ou plan gratuit |
| « Requête refusée. Rechargez la page » | Appel depuis une autre adresse que AUTH_ORIGIN : ajoute-la à AUTH_ALLOWED_ORIGINS |
| Erreur 500 à la connexion en production mais pas en local | Ancienne version du code qui demande plus de 100 000 itérations en un seul calcul : mets à jour |