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
| Action | OWNER | ADMIN | MEMBER |
|---|---|---|---|
| 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"](pasOWNER) - 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.nameestescapeHtml-é 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 / quitteToutes 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.