Documentation

Organisations / équipes

Modèles Organization + OrganizationMember, invitations email, rôles OWNER/ADMIN/MEMBER, transfert de propriété.

Permet à un user de créer une organisation et d'inviter d'autres users avec différents rôles. Modèle classique : un OWNER par org, des ADMINs (gestion sans pouvoir supprimer l'org), des MEMBERs (accès lecture).

Page : /account/organizations. Click sur une org → fiche détail.

Modèle de données

model Organization {
  id          String   @id @default(cuid())
  name        String
  logoUrl     String?
  createdAt   DateTime @default(now())
  members     OrganizationMember[]
  invitations OrganizationInvitation[]
}

enum OrgRole {
  OWNER
  ADMIN
  MEMBER
}

model OrganizationMember {
  id             String   @id @default(cuid())
  organizationId String
  userId         String
  role           OrgRole  @default(MEMBER)
  joinedAt       DateTime @default(now())

  @@unique([organizationId, userId])
}

model OrganizationInvitation {
  id             String   @id @default(cuid())
  organizationId String
  email          String
  role           OrgRole  @default(MEMBER)
  token          String   @unique
  expiresAt      DateTime
  acceptedAt     DateTime?
  createdAt      DateTime @default(now())
}

Permissions

ActionOWNERADMINMEMBER
Voir l'org
Modifier name / logo
Inviter
Changer rôle (sauf OWNER)
Changer rôle d'un OWNER
Transférer la propriété
Retirer un membre✅ (sauf OWNER)
Quitter l'org✅ (si autre OWNER)
Supprimer l'org

Garde-fous :

  • Un ADMIN ne peut pas modifier le rôle d'un OWNER ni le retirer
  • Le dernier OWNER ne peut pas quitter l'org tant qu'il n'a pas transféré la propriété (sinon org orpheline)
  • Le transfert de propriété démote l'ancien OWNER à ADMIN dans la même transaction (jamais 2 OWNER)

Invitations

POST /api/organizations/[id]/invite — envoie un mail d'invitation.

  • Validation : email valide, role ∈ ["ADMIN", "MEMBER"] (pas OWNER)
  • Dédup : refuse si l'email est déjà membre OU a une invitation pending non-expirée (évite le spam d'invits)
  • Token 7j stocké en DB (OrganizationInvitation.token)
  • Le organization.name est escapeHtml-é avant l'interpolation dans le HTML email (sanitization)

Le destinataire clique le lien → /invite/[token] valide + crée le membership.

Routes API

GET    /api/organizations/[id]                        → fiche + membres + invits
PATCH  /api/organizations/[id]                        → name / logoUrl (OWNER/ADMIN)
DELETE /api/organizations/[id]                        → supprime (OWNER only)
POST   /api/organizations/[id]/invite                 → invite par email
GET    /api/organizations/[id]/members                → liste des membres
PATCH  /api/organizations/[id]/members/[userId]       → change le rôle
DELETE /api/organizations/[id]/members/[userId]       → retire / quitte

Toutes guarded par requireApiAuth() + checks de membership in-route.

Audit

Actions tracées : org.create, org.invite, org.member_added, org.member_removed, org.member_leave, org.transfer_ownership, org.delete, etc. Visibles dans le user activity log si l'user est concerné.

UI

/account/organizations :

  • Liste DataList default-card des orgs où l'user est membre
  • Bouton "Créer une org" → modal
  • Click sur une org → /account/organizations/[id] :
    • Header avec name + logo + rôle de l'user courant
    • Tab Membres (liste + invites pending)
    • Tab Paramètres (édit name/logo si autorisé, danger zone)

Voir Compte utilisateur pour la nav globale.

On this page