feat: Implémentation complète du système de permissions
- Créer lib/permissions.ts avec toutes les fonctions de vérification - Protéger routes API: facturation, cotisations, virements (bloquer AGENT) - Protéger routes API: contrats (bloquer COMPTA) - Protéger routes API: gestion utilisateurs (bloquer AGENT/COMPTA) - Empêcher ADMIN de modifier/révoquer/créer SUPER_ADMIN - Ajouter documentation complète dans PERMISSIONS_MATRIX.md Système à 5 niveaux: - STAFF (équipe Odentas) - SUPER_ADMIN (admin principal, 1 par org, protégé) - ADMIN (admins secondaires) - AGENT (opérationnel: contrats/paies/salariés) - COMPTA (financier lecture seule: cotisations/virements/factures)
This commit is contained in:
parent
65d367cb5f
commit
78c43f0bfa
11 changed files with 926 additions and 3 deletions
354
PERMISSIONS_MATRIX.md
Normal file
354
PERMISSIONS_MATRIX.md
Normal file
|
|
@ -0,0 +1,354 @@
|
||||||
|
# Matrice des Permissions - Espace Paie Odentas
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
Le système de permissions de l'Espace Paie Odentas repose sur **5 rôles hiérarchiques** :
|
||||||
|
|
||||||
|
1. **STAFF** - Équipe Odentas (super-administrateurs globaux)
|
||||||
|
2. **SUPER_ADMIN** - Administrateur principal de l'organisation (1 seul par organisation)
|
||||||
|
3. **ADMIN** - Administrateurs secondaires (plusieurs possibles)
|
||||||
|
4. **AGENT** - Opérateurs RH (gestion contrats/paies/salariés uniquement)
|
||||||
|
5. **COMPTA** - Comptables (accès financier en lecture seule)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tableau Récapitulatif des Permissions
|
||||||
|
|
||||||
|
| Ressource/Action | STAFF | SUPER_ADMIN | ADMIN | AGENT | COMPTA |
|
||||||
|
|------------------|-------|-------------|-------|-------|--------|
|
||||||
|
| **CONTRATS** |
|
||||||
|
| Voir liste contrats | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| Créer contrat | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| Modifier contrat | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| Supprimer contrat | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| **PAIES** |
|
||||||
|
| Voir fiches de paie | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| Générer fiches de paie | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| Modifier fiches de paie | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| **SALARIÉS** |
|
||||||
|
| Voir liste salariés | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| Créer salarié | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| Modifier salarié | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| Supprimer salarié | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| **COTISATIONS** |
|
||||||
|
| Voir cotisations mensuelles | ✅ | ✅ | ✅ | ❌ | ✅ (lecture seule) |
|
||||||
|
| Modifier cotisations | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||||
|
| **VIREMENTS SALAIRES** |
|
||||||
|
| Voir virements | ✅ | ✅ | ✅ | ❌ | ✅ (lecture seule) |
|
||||||
|
| Créer/modifier virements | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||||
|
| **FACTURATION** |
|
||||||
|
| Voir factures | ✅ | ✅ | ✅ | ❌ | ✅ (lecture seule) |
|
||||||
|
| Créer/modifier factures | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||||
|
| **DOCUMENTS** |
|
||||||
|
| Upload documents | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| Voir tous documents | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| Voir documents comptables* | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||||
|
| **GESTION UTILISATEURS** |
|
||||||
|
| Voir liste utilisateurs | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||||
|
| Créer SUPER_ADMIN | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| Créer ADMIN/AGENT/COMPTA | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||||
|
| Modifier rôle SUPER_ADMIN | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| Modifier rôle autres | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||||
|
| Révoquer SUPER_ADMIN | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| Révoquer autres | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||||
|
| **INFORMATIONS STRUCTURE** |
|
||||||
|
| Voir informations | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Modifier SIRET/SEPA | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||||
|
| Modifier autres infos | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||||
|
|
||||||
|
**Documents comptables inclus** : factures, devis, relevés cotisations, virements salaires, justificatifs bancaires, bilans, documents comptables généraux
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Détails par Rôle
|
||||||
|
|
||||||
|
### 🔴 STAFF (Équipe Odentas)
|
||||||
|
**Accès complet** à toutes les organisations et toutes les fonctionnalités. Ce rôle est réservé à l'équipe technique Odentas.
|
||||||
|
|
||||||
|
**Permissions spécifiques :**
|
||||||
|
- Peut créer et supprimer des SUPER_ADMIN
|
||||||
|
- Accès multi-organisations
|
||||||
|
- Aucune restriction
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🟣 SUPER_ADMIN (Administrateur Principal)
|
||||||
|
|
||||||
|
**Responsable principal de l'organisation**. Il existe **un seul SUPER_ADMIN par organisation**.
|
||||||
|
|
||||||
|
**Permissions :**
|
||||||
|
- ✅ Accès complet à toutes les données de l'organisation
|
||||||
|
- ✅ Peut gérer tous les utilisateurs (sauf autres SUPER_ADMIN)
|
||||||
|
- ✅ Peut créer ADMIN, AGENT, COMPTA
|
||||||
|
- ✅ Peut modifier les informations structurelles critiques (SIRET, SEPA)
|
||||||
|
- ✅ Accès en lecture/écriture à facturation, cotisations, virements
|
||||||
|
- ✅ Gestion complète des contrats, paies, salariés
|
||||||
|
|
||||||
|
**Restrictions :**
|
||||||
|
- ❌ Ne peut pas être modifié ou révoqué par un ADMIN
|
||||||
|
- ❌ Seul STAFF peut créer un nouveau SUPER_ADMIN
|
||||||
|
|
||||||
|
**Protection :**
|
||||||
|
- Les ADMIN ne peuvent pas modifier ou supprimer le SUPER_ADMIN
|
||||||
|
- C'est le "propriétaire" de l'organisation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔵 ADMIN (Administrateur Secondaire)
|
||||||
|
|
||||||
|
**Administrateurs opérationnels**. Plusieurs ADMIN peuvent coexister.
|
||||||
|
|
||||||
|
**Permissions :**
|
||||||
|
- ✅ Accès complet aux données opérationnelles
|
||||||
|
- ✅ Peut gérer les utilisateurs (sauf SUPER_ADMIN)
|
||||||
|
- ✅ Peut créer AGENT et COMPTA
|
||||||
|
- ✅ Accès en lecture/écriture à facturation, cotisations, virements
|
||||||
|
- ✅ Gestion complète des contrats, paies, salariés
|
||||||
|
- ✅ Upload et accès à tous les documents
|
||||||
|
|
||||||
|
**Restrictions :**
|
||||||
|
- ❌ Ne peut pas modifier les informations structurelles critiques (SIRET/SEPA)
|
||||||
|
- ❌ Ne peut pas modifier/révoquer un SUPER_ADMIN
|
||||||
|
- ❌ Ne peut pas créer de SUPER_ADMIN
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🟢 AGENT (Opérateur RH)
|
||||||
|
|
||||||
|
**Rôle opérationnel** pour la gestion quotidienne des contrats, paies et salariés.
|
||||||
|
|
||||||
|
**Permissions :**
|
||||||
|
- ✅ Gestion complète des contrats (CDDU, RG)
|
||||||
|
- ✅ Gestion des fiches de paie
|
||||||
|
- ✅ Gestion des salariés
|
||||||
|
- ✅ Upload de documents généraux
|
||||||
|
- ✅ Consultation des informations de structure
|
||||||
|
|
||||||
|
**Restrictions :**
|
||||||
|
- ❌ Pas d'accès aux données financières (facturation, cotisations, virements)
|
||||||
|
- ❌ Pas d'accès à la gestion des utilisateurs
|
||||||
|
- ❌ Pas d'accès aux documents comptables
|
||||||
|
- ❌ Pas de modification des informations de structure
|
||||||
|
|
||||||
|
**Cas d'usage :** Assistant RH, chargé de production, gestionnaire de paie junior
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🟡 COMPTA (Comptable)
|
||||||
|
|
||||||
|
**Accès financier en lecture seule**. Le COMPTA ne peut ni créer, ni modifier, ni supprimer de données.
|
||||||
|
|
||||||
|
**Permissions :**
|
||||||
|
- ✅ Consultation des cotisations mensuelles
|
||||||
|
- ✅ Consultation des virements salaires
|
||||||
|
- ✅ Consultation des factures
|
||||||
|
- ✅ Consultation des documents comptables uniquement :
|
||||||
|
- Factures
|
||||||
|
- Devis
|
||||||
|
- Relevés de cotisations
|
||||||
|
- Virements salaires
|
||||||
|
- Justificatifs bancaires
|
||||||
|
- Bilans
|
||||||
|
- Documents comptables généraux
|
||||||
|
- ✅ Consultation des informations de structure
|
||||||
|
|
||||||
|
**Restrictions :**
|
||||||
|
- ❌ Pas d'accès aux contrats
|
||||||
|
- ❌ Pas d'accès aux fiches de paie
|
||||||
|
- ❌ Pas d'accès aux salariés
|
||||||
|
- ❌ Pas d'upload de documents
|
||||||
|
- ❌ Pas d'accès à la gestion des utilisateurs
|
||||||
|
- ❌ Pas de modification possible (lecture seule uniquement)
|
||||||
|
|
||||||
|
**Cas d'usage :** Expert-comptable externe, comptable ne nécessitant que les données financières
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hiérarchie et Protection
|
||||||
|
|
||||||
|
### Règle de Hiérarchie
|
||||||
|
|
||||||
|
```
|
||||||
|
STAFF
|
||||||
|
└── SUPER_ADMIN (protégé)
|
||||||
|
└── ADMIN
|
||||||
|
├── AGENT
|
||||||
|
└── COMPTA
|
||||||
|
```
|
||||||
|
|
||||||
|
**Règles de modification :**
|
||||||
|
1. **STAFF** peut tout modifier
|
||||||
|
2. **SUPER_ADMIN** peut modifier ADMIN, AGENT, COMPTA (pas d'autres SUPER_ADMIN)
|
||||||
|
3. **ADMIN** peut modifier AGENT, COMPTA (pas SUPER_ADMIN ni autres ADMIN)
|
||||||
|
4. **AGENT** et **COMPTA** ne peuvent rien modifier
|
||||||
|
|
||||||
|
### Protection du SUPER_ADMIN
|
||||||
|
|
||||||
|
Le SUPER_ADMIN est **protégé** :
|
||||||
|
- Un ADMIN ne peut pas changer son rôle
|
||||||
|
- Un ADMIN ne peut pas le révoquer
|
||||||
|
- Un ADMIN ne peut pas créer un nouveau SUPER_ADMIN
|
||||||
|
|
||||||
|
Seul le **STAFF** d'Odentas peut modifier un SUPER_ADMIN.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implémentation Technique
|
||||||
|
|
||||||
|
### Fichier central : `lib/permissions.ts`
|
||||||
|
|
||||||
|
Toutes les vérifications de permissions sont centralisées dans `lib/permissions.ts`.
|
||||||
|
|
||||||
|
**Fonctions principales :**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Récupérer les permissions d'un utilisateur
|
||||||
|
getUserPermissions(supabaseClient): Promise<UserPermissions | null>
|
||||||
|
|
||||||
|
// Ressources opérationnelles
|
||||||
|
canAccessContrats(role: UserRole): boolean
|
||||||
|
canAccessPaies(role: UserRole): boolean
|
||||||
|
canAccessSalaries(role: UserRole): boolean
|
||||||
|
|
||||||
|
// Ressources financières
|
||||||
|
canAccessFacturation(role: UserRole): boolean
|
||||||
|
canAccessCotisations(role: UserRole): boolean
|
||||||
|
canAccessVirements(role: UserRole): boolean
|
||||||
|
|
||||||
|
// Gestion utilisateurs
|
||||||
|
canManageUsers(role: UserRole): boolean
|
||||||
|
canModifyUser(permissions: UserPermissions, targetRole: UserRole): boolean
|
||||||
|
canCreateUserWithRole(permissions: UserPermissions, newRole: UserRole): boolean
|
||||||
|
|
||||||
|
// Documents
|
||||||
|
canUploadDocuments(role: UserRole): boolean
|
||||||
|
canAccessDocumentType(role: UserRole, docType: string): boolean
|
||||||
|
|
||||||
|
// Informations structure
|
||||||
|
canModifyStructureInfo(role: UserRole): boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
### Routes protégées
|
||||||
|
|
||||||
|
**Backend (API Routes) :**
|
||||||
|
- `/api/contrats/*` - Bloque COMPTA
|
||||||
|
- `/api/facturation/*` - Bloque AGENT
|
||||||
|
- `/api/cotisations/*` - Bloque AGENT
|
||||||
|
- `/api/virements-salaires/*` - Bloque AGENT
|
||||||
|
- `/api/access/*` - Bloque AGENT et COMPTA
|
||||||
|
- `/api/access/[memberId]/role/*` - Empêche ADMIN de modifier SUPER_ADMIN
|
||||||
|
- `/api/access/[memberId]/revoke/*` - Empêche ADMIN de révoquer SUPER_ADMIN
|
||||||
|
- `/api/access/nouveau/*` - Empêche ADMIN de créer SUPER_ADMIN
|
||||||
|
|
||||||
|
**Frontend (Composants React) :**
|
||||||
|
- Masquage/désactivation des boutons selon le rôle
|
||||||
|
- Navigation conditionnelle
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exemples de Cas d'Usage
|
||||||
|
|
||||||
|
### Scénario 1 : Cabinet de Production
|
||||||
|
**Organisation :** Compagnie de théâtre de 20 salariés
|
||||||
|
|
||||||
|
**Utilisateurs :**
|
||||||
|
- **1 SUPER_ADMIN** : Directeur administratif (gestion complète + informations critiques)
|
||||||
|
- **2 ADMIN** : Responsable RH + Responsable production (gestion complète opérationnelle)
|
||||||
|
- **3 AGENT** : Assistants RH (gestion contrats/paies uniquement)
|
||||||
|
- **1 COMPTA** : Expert-comptable externe (consultation financière uniquement)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scénario 2 : Petite Structure Associative
|
||||||
|
**Organisation :** Association culturelle de 5 salariés
|
||||||
|
|
||||||
|
**Utilisateurs :**
|
||||||
|
- **1 SUPER_ADMIN** : Président de l'association (tout)
|
||||||
|
- **1 AGENT** : Secrétaire générale (contrats/paies)
|
||||||
|
- **1 COMPTA** : Trésorier (consultation financière)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scénario 3 : Grande Production
|
||||||
|
**Organisation :** Festival avec 100+ salariés
|
||||||
|
|
||||||
|
**Utilisateurs :**
|
||||||
|
- **1 SUPER_ADMIN** : Directeur général
|
||||||
|
- **5 ADMIN** : Équipe de direction
|
||||||
|
- **10 AGENT** : Équipe RH/production
|
||||||
|
- **2 COMPTA** : Cabinet comptable externe
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration et Changement de Rôle
|
||||||
|
|
||||||
|
### Promotion d'utilisateur
|
||||||
|
|
||||||
|
**AGENT → ADMIN**
|
||||||
|
- Action possible par SUPER_ADMIN ou ADMIN
|
||||||
|
- Débloque l'accès financier et la gestion utilisateurs
|
||||||
|
|
||||||
|
**ADMIN → SUPER_ADMIN**
|
||||||
|
- ⚠️ **Action STAFF uniquement**
|
||||||
|
- L'ancien SUPER_ADMIN doit être rétrogradé en ADMIN d'abord
|
||||||
|
- Il ne peut y avoir qu'un seul SUPER_ADMIN
|
||||||
|
|
||||||
|
### Rétrogradation d'utilisateur
|
||||||
|
|
||||||
|
**ADMIN → AGENT**
|
||||||
|
- Action possible par SUPER_ADMIN
|
||||||
|
- Bloque l'accès financier et gestion utilisateurs
|
||||||
|
|
||||||
|
**SUPER_ADMIN → ADMIN**
|
||||||
|
- ⚠️ **Action STAFF uniquement**
|
||||||
|
- Permet de désigner un nouveau SUPER_ADMIN
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Pourquoi un seul SUPER_ADMIN ?
|
||||||
|
|
||||||
|
Pour éviter les conflits de gestion et garantir une responsabilité claire. Le SUPER_ADMIN est le "propriétaire" de l'organisation.
|
||||||
|
|
||||||
|
### Un ADMIN peut-il modifier un autre ADMIN ?
|
||||||
|
|
||||||
|
Non. Les ADMIN peuvent uniquement gérer les AGENT et COMPTA. Seul le SUPER_ADMIN peut modifier un ADMIN.
|
||||||
|
|
||||||
|
### Pourquoi le COMPTA ne peut pas uploader de documents ?
|
||||||
|
|
||||||
|
Le rôle COMPTA est en **lecture seule** pour éviter toute modification des données opérationnelles ou financières. Il peut uniquement consulter.
|
||||||
|
|
||||||
|
### Comment ajouter un nouveau SUPER_ADMIN ?
|
||||||
|
|
||||||
|
Seul le **STAFF Odentas** peut créer un SUPER_ADMIN. Contactez support@odentas.com.
|
||||||
|
|
||||||
|
### Le COMPTA peut-il voir les fiches de paie ?
|
||||||
|
|
||||||
|
Non. Le COMPTA ne voit que les **données financières agrégées** :
|
||||||
|
- Cotisations mensuelles (montants globaux)
|
||||||
|
- Virements salaires (montants globaux)
|
||||||
|
- Factures
|
||||||
|
- Documents comptables
|
||||||
|
|
||||||
|
Les fiches de paie détaillées sont réservées aux rôles opérationnels (SUPER_ADMIN, ADMIN, AGENT).
|
||||||
|
|
||||||
|
### Un AGENT peut-il créer des factures ?
|
||||||
|
|
||||||
|
Non. Les factures sont considérées comme des **ressources financières**. Seuls SUPER_ADMIN et ADMIN ont accès.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support Technique
|
||||||
|
|
||||||
|
Pour toute question sur les permissions ou demande de modification de rôle, contactez :
|
||||||
|
|
||||||
|
**Support Odentas**
|
||||||
|
- Email : support@odentas.com
|
||||||
|
- Interface : [Espace Paie Odentas](https://espace-paie.odentas.com)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Version :** 1.0
|
||||||
|
**Dernière mise à jour :** Janvier 2025
|
||||||
|
**Auteur :** Équipe Technique Odentas
|
||||||
|
|
@ -5,6 +5,7 @@ import { cookies, headers } from "next/headers";
|
||||||
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
||||||
import { createClient } from "@supabase/supabase-js";
|
import { createClient } from "@supabase/supabase-js";
|
||||||
import { sendAccessRevokedEmail } from "@/lib/emailMigrationHelpers";
|
import { sendAccessRevokedEmail } from "@/lib/emailMigrationHelpers";
|
||||||
|
import { getUserPermissions, canModifyUser } from "@/lib/permissions";
|
||||||
|
|
||||||
type OrgRecord = { id: string };
|
type OrgRecord = { id: string };
|
||||||
type MemberRecord = { role: string; revoked: boolean };
|
type MemberRecord = { role: string; revoked: boolean };
|
||||||
|
|
@ -136,6 +137,29 @@ export async function POST(_req: Request, { params }: { params: { memberId: stri
|
||||||
{ auth: { autoRefreshToken: false, persistSession: false }}
|
{ auth: { autoRefreshToken: false, persistSession: false }}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Vérifier le rôle de l'utilisateur ciblé pour empêcher ADMIN de révoquer SUPER_ADMIN
|
||||||
|
let targetRole: string | null = null;
|
||||||
|
try {
|
||||||
|
const { data: targetMember } = await admin
|
||||||
|
.from('organization_members')
|
||||||
|
.select('role')
|
||||||
|
.eq('org_id', org_id)
|
||||||
|
.eq('user_id', params.memberId)
|
||||||
|
.is('revoked', false)
|
||||||
|
.maybeSingle();
|
||||||
|
targetRole = (targetMember as any)?.role || null;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const permissions = await getUserPermissions(sb);
|
||||||
|
if (!permissions) {
|
||||||
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetRole && !canModifyUser(permissions, targetRole as any)) {
|
||||||
|
console.log("❌ Permission refusée: impossible de révoquer un utilisateur", targetRole);
|
||||||
|
return new NextResponse("Forbidden - Vous ne pouvez pas révoquer cet utilisateur", { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const { error } = await admin.from("organization_members")
|
const { error } = await admin.from("organization_members")
|
||||||
.update({ revoked: true, revoked_at: new Date().toISOString() })
|
.update({ revoked: true, revoked_at: new Date().toISOString() })
|
||||||
.eq("org_id", org_id).eq("user_id", params.memberId);
|
.eq("org_id", org_id).eq("user_id", params.memberId);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { cookies, headers } from "next/headers";
|
||||||
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
||||||
import { createClient } from "@supabase/supabase-js";
|
import { createClient } from "@supabase/supabase-js";
|
||||||
import { sendAccessUpdatedEmail } from "@/lib/emailMigrationHelpers";
|
import { sendAccessUpdatedEmail } from "@/lib/emailMigrationHelpers";
|
||||||
|
import { getUserPermissions, canModifyUser } from "@/lib/permissions";
|
||||||
|
|
||||||
type MemberRecord = { role: string; revoked: boolean };
|
type MemberRecord = { role: string; revoked: boolean };
|
||||||
|
|
||||||
|
|
@ -134,6 +135,17 @@ export async function PATCH(req: Request, { params }: { params: { memberId: stri
|
||||||
oldRole = (existingMember as any)?.role || null;
|
oldRole = (existingMember as any)?.role || null;
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
// Vérification des permissions hiérarchiques: empêcher ADMIN de modifier SUPER_ADMIN
|
||||||
|
const permissions = await getUserPermissions(sb);
|
||||||
|
if (!permissions) {
|
||||||
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldRole && !canModifyUser(permissions, oldRole as any)) {
|
||||||
|
console.log("❌ Permission refusée: impossible de modifier un utilisateur", oldRole, "avec le rôle", permissions.role);
|
||||||
|
return new NextResponse("Forbidden - Vous ne pouvez pas modifier cet utilisateur", { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const { error } = await admin.from("organization_members")
|
const { error } = await admin.from("organization_members")
|
||||||
.update({ role })
|
.update({ role })
|
||||||
.eq("org_id", org_id)
|
.eq("org_id", org_id)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { createClient } from "@supabase/supabase-js";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
||||||
import { sendInvitationWithActivationEmail } from "@/lib/emailMigrationHelpers";
|
import { sendInvitationWithActivationEmail } from "@/lib/emailMigrationHelpers";
|
||||||
|
import { getUserPermissions, canCreateUserWithRole } from "@/lib/permissions";
|
||||||
|
|
||||||
// Types de rôles autorisés (alignés avec public.role)
|
// Types de rôles autorisés (alignés avec public.role)
|
||||||
const ROLES = ["SUPER_ADMIN","ADMIN","AGENT","COMPTA"] as const;
|
const ROLES = ["SUPER_ADMIN","ADMIN","AGENT","COMPTA"] as const;
|
||||||
|
|
@ -93,6 +94,18 @@ export async function POST(req: Request) {
|
||||||
|
|
||||||
console.log("🏛️ [API] Organisation trouvée:", org.name);
|
console.log("🏛️ [API] Organisation trouvée:", org.name);
|
||||||
|
|
||||||
|
// Vérification des permissions: empêcher AGENT/COMPTA de créer des utilisateurs
|
||||||
|
// et empêcher ADMIN de créer un SUPER_ADMIN
|
||||||
|
const permissions = await getUserPermissions(sb);
|
||||||
|
if (!permissions) {
|
||||||
|
return NextResponse.json({ error: "unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canCreateUserWithRole(permissions, requestedRole as any)) {
|
||||||
|
console.log("❌ [API] Permission refusée: impossible de créer un utilisateur", requestedRole);
|
||||||
|
return NextResponse.json({ error: "forbidden", message: "Vous n'avez pas la permission de créer cet utilisateur" }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// Client admin Supabase (service role)
|
// Client admin Supabase (service role)
|
||||||
const admin = createClient(
|
const admin = createClient(
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { cookies, headers } from "next/headers";
|
||||||
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
||||||
import { createClient } from "@supabase/supabase-js";
|
import { createClient } from "@supabase/supabase-js";
|
||||||
import { sendAccessUpdatedEmail, sendInvitationWithActivationEmail } from "@/lib/emailMigrationHelpers";
|
import { sendAccessUpdatedEmail, sendInvitationWithActivationEmail } from "@/lib/emailMigrationHelpers";
|
||||||
|
import { getUserPermissions, canManageUsers } from "@/lib/permissions";
|
||||||
// import type { Database } from "@/types/supabase"; // Temporairement commenté
|
// import type { Database } from "@/types/supabase"; // Temporairement commenté
|
||||||
|
|
||||||
type OrgRow = { id: string; name?: string; structure_api?: string | null };
|
type OrgRow = { id: string; name?: string; structure_api?: string | null };
|
||||||
|
|
@ -227,6 +228,18 @@ export async function GET() {
|
||||||
console.log("❌ Accès refusé");
|
console.log("❌ Accès refusé");
|
||||||
return new NextResponse("Forbidden", { status: g.status });
|
return new NextResponse("Forbidden", { status: g.status });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification des permissions: AGENT et COMPTA ne peuvent pas gérer les utilisateurs
|
||||||
|
const permissions = await getUserPermissions(sb);
|
||||||
|
if (!permissions) {
|
||||||
|
console.log("❌ Permission refusée: utilisateur non authentifié");
|
||||||
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canManageUsers(permissions)) {
|
||||||
|
console.log("❌ Permission refusée: gestion des utilisateurs non autorisée pour", permissions.role);
|
||||||
|
return new NextResponse("Forbidden - Accès réservé aux administrateurs", { status: 403 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("✅ Accès autorisé, récupération des membres...");
|
console.log("✅ Accès autorisé, récupération des membres...");
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { resolveActiveOrg } from "@/lib/resolveActiveOrg";
|
||||||
import { createClient } from '@supabase/supabase-js';
|
import { createClient } from '@supabase/supabase-js';
|
||||||
import { detectDemoModeFromHeaders } from "@/lib/demo-detector";
|
import { detectDemoModeFromHeaders } from "@/lib/demo-detector";
|
||||||
import { DEMO_CONTRACTS, DEMO_ORGANIZATION } from "@/lib/demo-data";
|
import { DEMO_CONTRACTS, DEMO_ORGANIZATION } from "@/lib/demo-data";
|
||||||
|
import { getUserPermissions, canAccessContrats, checkPermissionOrRespond } from "@/lib/permissions";
|
||||||
|
|
||||||
// Force dynamic rendering and disable revalidation cache for this proxy
|
// Force dynamic rendering and disable revalidation cache for this proxy
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
@ -76,6 +77,14 @@ export async function GET(req: Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const sb = createRouteHandlerClient({ cookies });
|
||||||
|
|
||||||
|
// 🔒 VÉRIFICATION DES PERMISSIONS (bloquer COMPTA)
|
||||||
|
const permissions = await getUserPermissions(sb);
|
||||||
|
const permissionCheck = canAccessContrats(permissions);
|
||||||
|
const errorResponse = checkPermissionOrRespond(permissionCheck);
|
||||||
|
if (errorResponse) return errorResponse;
|
||||||
|
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
const regime = url.searchParams.get("regime");
|
const regime = url.searchParams.get("regime");
|
||||||
// Si CDDU, RG, ou absence de regime (= tous), lire depuis Supabase (table cddu_contracts)
|
// Si CDDU, RG, ou absence de regime (= tous), lire depuis Supabase (table cddu_contracts)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
|
import { getUserPermissions, canAccessCotisations, checkPermissionOrRespond } from "@/lib/permissions";
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
export const revalidate = 0;
|
export const revalidate = 0;
|
||||||
|
|
@ -118,13 +119,20 @@ async function getClientInfoFromSession(session: any, supabase: any) {
|
||||||
|
|
||||||
export async function GET(req: Request) {
|
export async function GET(req: Request) {
|
||||||
try {
|
try {
|
||||||
|
const supabase = createRouteHandlerClient({ cookies });
|
||||||
|
|
||||||
|
// 🔒 VÉRIFICATION DES PERMISSIONS
|
||||||
|
const permissions = await getUserPermissions(supabase);
|
||||||
|
const permissionCheck = canAccessCotisations(permissions);
|
||||||
|
const errorResponse = checkPermissionOrRespond(permissionCheck);
|
||||||
|
if (errorResponse) return errorResponse;
|
||||||
|
|
||||||
const { searchParams } = new URL(req.url);
|
const { searchParams } = new URL(req.url);
|
||||||
const year = parseInt(searchParams.get('year') || `${new Date().getFullYear()}`, 10);
|
const year = parseInt(searchParams.get('year') || `${new Date().getFullYear()}`, 10);
|
||||||
const period = (searchParams.get('period') || 'toute_annee').toString();
|
const period = (searchParams.get('period') || 'toute_annee').toString();
|
||||||
const fromParam = searchParams.get('from') || undefined; // YYYY-MM-DD
|
const fromParam = searchParams.get('from') || undefined; // YYYY-MM-DD
|
||||||
const toParam = searchParams.get('to') || undefined; // YYYY-MM-DD
|
const toParam = searchParams.get('to') || undefined; // YYYY-MM-DD
|
||||||
|
|
||||||
const supabase = createRouteHandlerClient({ cookies });
|
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
if (!session) return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
|
if (!session) return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||||
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
||||||
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
|
import { getUserPermissions, canUploadDocuments, canAccessDocumentType } from "@/lib/permissions";
|
||||||
|
|
||||||
const s3Client = new S3Client({
|
const s3Client = new S3Client({
|
||||||
region: process.env.AWS_REGION || "eu-west-3",
|
region: process.env.AWS_REGION || "eu-west-3",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
|
import { getUserPermissions, canAccessFacturation, checkPermissionOrRespond } from "@/lib/permissions";
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
export const revalidate = 0;
|
export const revalidate = 0;
|
||||||
|
|
@ -87,13 +88,20 @@ async function getClientInfoFromSession(session: any, supabase: any) {
|
||||||
|
|
||||||
export async function GET(req: Request) {
|
export async function GET(req: Request) {
|
||||||
try {
|
try {
|
||||||
|
const supabase = createRouteHandlerClient({ cookies });
|
||||||
|
|
||||||
|
// 🔒 VÉRIFICATION DES PERMISSIONS
|
||||||
|
const permissions = await getUserPermissions(supabase);
|
||||||
|
const permissionCheck = canAccessFacturation(permissions);
|
||||||
|
const errorResponse = checkPermissionOrRespond(permissionCheck);
|
||||||
|
if (errorResponse) return errorResponse;
|
||||||
|
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
const page = Math.max(1, parseInt(url.searchParams.get('page') || '1', 10));
|
const page = Math.max(1, parseInt(url.searchParams.get('page') || '1', 10));
|
||||||
const limit = Math.max(1, Math.min(50, parseInt(url.searchParams.get('limit') || '25', 10)));
|
const limit = Math.max(1, Math.min(50, parseInt(url.searchParams.get('limit') || '25', 10)));
|
||||||
const from = (page - 1) * limit;
|
const from = (page - 1) * limit;
|
||||||
const to = from + limit - 1;
|
const to = from + limit - 1;
|
||||||
|
|
||||||
const supabase = createRouteHandlerClient({ cookies });
|
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
if (!session) return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
|
if (!session) return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||||
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { detectDemoModeFromHeaders } from "@/lib/demo-detector";
|
import { detectDemoModeFromHeaders } from "@/lib/demo-detector";
|
||||||
|
import { getUserPermissions, canAccessVirements, checkPermissionOrRespond } from "@/lib/permissions";
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
// 🎭 En mode démo, retourner des données vides (les données fictives sont gérées côté client)
|
// 🎭 En mode démo, retourner des données vides (les données fictives sont gérées côté client)
|
||||||
|
|
@ -22,6 +23,14 @@ export async function GET(req: NextRequest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const sb = createRouteHandlerClient({ cookies });
|
||||||
|
|
||||||
|
// 🔒 VÉRIFICATION DES PERMISSIONS
|
||||||
|
const permissions = await getUserPermissions(sb);
|
||||||
|
const permissionCheck = canAccessVirements(permissions);
|
||||||
|
const errorResponse = checkPermissionOrRespond(permissionCheck);
|
||||||
|
if (errorResponse) return errorResponse;
|
||||||
|
|
||||||
const { searchParams } = new URL(req.url);
|
const { searchParams } = new URL(req.url);
|
||||||
const year = searchParams.get("year");
|
const year = searchParams.get("year");
|
||||||
const period = searchParams.get("period");
|
const period = searchParams.get("period");
|
||||||
|
|
@ -39,7 +48,6 @@ export async function GET(req: NextRequest) {
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ AJOUT : Récupération de l'identification du client côté serveur
|
// ✅ AJOUT : Récupération de l'identification du client côté serveur
|
||||||
const sb = createRouteHandlerClient({ cookies });
|
|
||||||
const { data: { user }, error: userError } = await sb.auth.getUser();
|
const { data: { user }, error: userError } = await sb.auth.getUser();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|
|
||||||
473
lib/permissions.ts
Normal file
473
lib/permissions.ts
Normal file
|
|
@ -0,0 +1,473 @@
|
||||||
|
/**
|
||||||
|
* Système de permissions et habilitations pour l'Espace Paie Odentas
|
||||||
|
*
|
||||||
|
* Hiérarchie des rôles clients :
|
||||||
|
* - SUPER_ADMIN : Compte principal protégé (1 seul par org)
|
||||||
|
* - ADMIN : Accès total aux données
|
||||||
|
* - AGENT : Opérationnel (contrats, paies, salariés)
|
||||||
|
* - COMPTA : Lecture seule financière (cotisations, virements, facturation)
|
||||||
|
*
|
||||||
|
* STAFF : Accès total toutes organisations (réservé Odentas)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type UserRole = "SUPER_ADMIN" | "ADMIN" | "AGENT" | "COMPTA";
|
||||||
|
|
||||||
|
export interface UserPermissions {
|
||||||
|
userId: string;
|
||||||
|
role: UserRole;
|
||||||
|
orgId: string;
|
||||||
|
isStaff: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PermissionCheck {
|
||||||
|
allowed: boolean;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Type de documents pour COMPTA
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const COMPTA_ALLOWED_DOCUMENT_TYPES = [
|
||||||
|
"facture",
|
||||||
|
"devis",
|
||||||
|
"releve_cotisations",
|
||||||
|
"virement_salaires",
|
||||||
|
"justificatif_bancaire",
|
||||||
|
"bilan",
|
||||||
|
"comptable"
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Récupération des permissions utilisateur
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les permissions d'un utilisateur authentifié
|
||||||
|
* @returns UserPermissions ou null si non authentifié
|
||||||
|
*/
|
||||||
|
export async function getUserPermissions(
|
||||||
|
supabase?: SupabaseClient
|
||||||
|
): Promise<UserPermissions | null> {
|
||||||
|
const sb = supabase || createRouteHandlerClient({ cookies });
|
||||||
|
|
||||||
|
// 1. Vérifier l'authentification
|
||||||
|
const {
|
||||||
|
data: { user },
|
||||||
|
error: authError,
|
||||||
|
} = await sb.auth.getUser();
|
||||||
|
|
||||||
|
if (authError || !user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Vérifier si c'est un staff
|
||||||
|
const { data: staffData } = await (sb as any)
|
||||||
|
.from("staff_users")
|
||||||
|
.select("is_staff")
|
||||||
|
.eq("user_id", user.id)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
const isStaff = !!staffData?.is_staff;
|
||||||
|
|
||||||
|
// Si staff, retourner avec rôle SUPER_ADMIN et pas d'orgId
|
||||||
|
if (isStaff) {
|
||||||
|
return {
|
||||||
|
userId: user.id,
|
||||||
|
role: "SUPER_ADMIN",
|
||||||
|
orgId: "", // Staff n'a pas d'org spécifique
|
||||||
|
isStaff: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Récupérer le rôle dans une organisation
|
||||||
|
const { data: memberData } = await (sb as any)
|
||||||
|
.from("organization_members")
|
||||||
|
.select("role, org_id, revoked")
|
||||||
|
.eq("user_id", user.id)
|
||||||
|
.eq("revoked", false)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (!memberData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: user.id,
|
||||||
|
role: memberData.role as UserRole,
|
||||||
|
orgId: memberData.org_id,
|
||||||
|
isStaff: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Vérifications de permissions par domaine
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur peut accéder à la facturation
|
||||||
|
* Autorisé : SUPER_ADMIN, ADMIN, COMPTA
|
||||||
|
* Bloqué : AGENT
|
||||||
|
*/
|
||||||
|
export function canAccessFacturation(permissions: UserPermissions | null): PermissionCheck {
|
||||||
|
if (!permissions) {
|
||||||
|
return { allowed: false, reason: "Non authentifié" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.isStaff) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.role === "AGENT") {
|
||||||
|
return { allowed: false, reason: "Les agents n'ont pas accès à la facturation" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur peut MODIFIER la facturation
|
||||||
|
* Autorisé : SUPER_ADMIN, ADMIN
|
||||||
|
* Bloqué : AGENT, COMPTA
|
||||||
|
*/
|
||||||
|
export function canModifyFacturation(permissions: UserPermissions | null): PermissionCheck {
|
||||||
|
if (!permissions) {
|
||||||
|
return { allowed: false, reason: "Non authentifié" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.isStaff) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.role === "COMPTA") {
|
||||||
|
return { allowed: false, reason: "Le rôle COMPTA est en lecture seule" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.role === "AGENT") {
|
||||||
|
return { allowed: false, reason: "Les agents n'ont pas accès à la facturation" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true }; // SUPER_ADMIN ou ADMIN
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur peut accéder aux cotisations
|
||||||
|
* Autorisé : SUPER_ADMIN, ADMIN, COMPTA
|
||||||
|
* Bloqué : AGENT
|
||||||
|
*/
|
||||||
|
export function canAccessCotisations(permissions: UserPermissions | null): PermissionCheck {
|
||||||
|
if (!permissions) {
|
||||||
|
return { allowed: false, reason: "Non authentifié" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.isStaff) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.role === "AGENT") {
|
||||||
|
return { allowed: false, reason: "Les agents n'ont pas accès aux cotisations" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur peut accéder aux virements salaires
|
||||||
|
* Autorisé : SUPER_ADMIN, ADMIN, COMPTA
|
||||||
|
* Bloqué : AGENT
|
||||||
|
*/
|
||||||
|
export function canAccessVirements(permissions: UserPermissions | null): PermissionCheck {
|
||||||
|
if (!permissions) {
|
||||||
|
return { allowed: false, reason: "Non authentifié" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.isStaff) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.role === "AGENT") {
|
||||||
|
return { allowed: false, reason: "Les agents n'ont pas accès aux virements salaires" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur peut accéder aux contrats
|
||||||
|
* Autorisé : SUPER_ADMIN, ADMIN, AGENT
|
||||||
|
* Bloqué : COMPTA
|
||||||
|
*/
|
||||||
|
export function canAccessContrats(permissions: UserPermissions | null): PermissionCheck {
|
||||||
|
if (!permissions) {
|
||||||
|
return { allowed: false, reason: "Non authentifié" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.isStaff) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.role === "COMPTA") {
|
||||||
|
return { allowed: false, reason: "Le rôle COMPTA n'a pas accès aux contrats" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur peut MODIFIER les contrats
|
||||||
|
* Autorisé : SUPER_ADMIN, ADMIN, AGENT
|
||||||
|
* Bloqué : COMPTA
|
||||||
|
*/
|
||||||
|
export function canModifyContrats(permissions: UserPermissions | null): PermissionCheck {
|
||||||
|
// Même logique que canAccessContrats
|
||||||
|
return canAccessContrats(permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur peut accéder aux paies
|
||||||
|
* Autorisé : SUPER_ADMIN, ADMIN, AGENT
|
||||||
|
* Bloqué : COMPTA
|
||||||
|
*/
|
||||||
|
export function canAccessPaies(permissions: UserPermissions | null): PermissionCheck {
|
||||||
|
if (!permissions) {
|
||||||
|
return { allowed: false, reason: "Non authentifié" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.isStaff) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.role === "COMPTA") {
|
||||||
|
return { allowed: false, reason: "Le rôle COMPTA n'a pas accès aux paies" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur peut accéder aux salariés
|
||||||
|
* Autorisé : SUPER_ADMIN, ADMIN, AGENT
|
||||||
|
* Bloqué : COMPTA
|
||||||
|
*/
|
||||||
|
export function canAccessSalaries(permissions: UserPermissions | null): PermissionCheck {
|
||||||
|
if (!permissions) {
|
||||||
|
return { allowed: false, reason: "Non authentifié" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.isStaff) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.role === "COMPTA") {
|
||||||
|
return { allowed: false, reason: "Le rôle COMPTA n'a pas accès aux salariés" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur peut gérer les utilisateurs (accès)
|
||||||
|
* Autorisé : SUPER_ADMIN, ADMIN
|
||||||
|
* Bloqué : AGENT, COMPTA
|
||||||
|
*/
|
||||||
|
export function canManageUsers(permissions: UserPermissions | null): PermissionCheck {
|
||||||
|
if (!permissions) {
|
||||||
|
return { allowed: false, reason: "Non authentifié" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.isStaff) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!["SUPER_ADMIN", "ADMIN"].includes(permissions.role)) {
|
||||||
|
return { allowed: false, reason: "Seuls les administrateurs peuvent gérer les utilisateurs" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur peut modifier un autre utilisateur
|
||||||
|
* @param permissions Permissions de l'utilisateur actuel
|
||||||
|
* @param targetRole Rôle de l'utilisateur cible
|
||||||
|
*/
|
||||||
|
export function canModifyUser(
|
||||||
|
permissions: UserPermissions | null,
|
||||||
|
targetRole: UserRole
|
||||||
|
): PermissionCheck {
|
||||||
|
if (!permissions) {
|
||||||
|
return { allowed: false, reason: "Non authentifié" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.isStaff) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ADMIN ne peut pas modifier SUPER_ADMIN
|
||||||
|
if (permissions.role === "ADMIN" && targetRole === "SUPER_ADMIN") {
|
||||||
|
return { allowed: false, reason: "Seul le staff peut modifier un SUPER_ADMIN" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// AGENT et COMPTA ne peuvent pas gérer d'utilisateurs
|
||||||
|
if (!["SUPER_ADMIN", "ADMIN"].includes(permissions.role)) {
|
||||||
|
return { allowed: false, reason: "Seuls les administrateurs peuvent modifier les utilisateurs" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur peut créer un utilisateur avec un rôle donné
|
||||||
|
* @param permissions Permissions de l'utilisateur actuel
|
||||||
|
* @param newRole Rôle à assigner au nouvel utilisateur
|
||||||
|
*/
|
||||||
|
export function canCreateUserWithRole(
|
||||||
|
permissions: UserPermissions | null,
|
||||||
|
newRole: UserRole
|
||||||
|
): PermissionCheck {
|
||||||
|
if (!permissions) {
|
||||||
|
return { allowed: false, reason: "Non authentifié" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.isStaff) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seul le staff peut créer un SUPER_ADMIN
|
||||||
|
if (newRole === "SUPER_ADMIN") {
|
||||||
|
return { allowed: false, reason: "Seul le staff Odentas peut créer un SUPER_ADMIN" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ADMIN peut créer ADMIN, AGENT, COMPTA
|
||||||
|
if (permissions.role === "ADMIN") {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// SUPER_ADMIN peut tout créer (sauf SUPER_ADMIN, déjà vérifié)
|
||||||
|
if (permissions.role === "SUPER_ADMIN") {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: false, reason: "Seuls les administrateurs peuvent créer des utilisateurs" };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur peut modifier les informations de structure (SIRET, coordonnées, SEPA)
|
||||||
|
* Autorisé : SUPER_ADMIN uniquement (+ STAFF)
|
||||||
|
* Bloqué : ADMIN, AGENT, COMPTA
|
||||||
|
*/
|
||||||
|
export function canModifyStructureInfo(permissions: UserPermissions | null): PermissionCheck {
|
||||||
|
if (!permissions) {
|
||||||
|
return { allowed: false, reason: "Non authentifié" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.isStaff) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.role !== "SUPER_ADMIN") {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: "Seul le SUPER_ADMIN peut modifier les informations de structure",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur peut uploader des documents
|
||||||
|
* Autorisé : SUPER_ADMIN, ADMIN, AGENT
|
||||||
|
* Bloqué : COMPTA
|
||||||
|
*/
|
||||||
|
export function canUploadDocuments(permissions: UserPermissions | null): PermissionCheck {
|
||||||
|
if (!permissions) {
|
||||||
|
return { allowed: false, reason: "Non authentifié" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.isStaff) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.role === "COMPTA") {
|
||||||
|
return { allowed: false, reason: "Le rôle COMPTA ne peut pas uploader de documents" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur peut accéder à un type de document
|
||||||
|
* @param permissions Permissions de l'utilisateur
|
||||||
|
* @param documentType Type du document (ex: "facture", "contrat", etc.)
|
||||||
|
*/
|
||||||
|
export function canAccessDocumentType(
|
||||||
|
permissions: UserPermissions | null,
|
||||||
|
documentType: string
|
||||||
|
): PermissionCheck {
|
||||||
|
if (!permissions) {
|
||||||
|
return { allowed: false, reason: "Non authentifié" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.isStaff) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// COMPTA ne peut accéder qu'aux documents comptables
|
||||||
|
if (permissions.role === "COMPTA") {
|
||||||
|
if (!COMPTA_ALLOWED_DOCUMENT_TYPES.includes(documentType.toLowerCase())) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: "Le rôle COMPTA n'a accès qu'aux documents comptables et financiers",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Helpers pour les réponses HTTP
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne une réponse 401 Unauthorized
|
||||||
|
*/
|
||||||
|
export function unauthorizedResponse() {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: "Non authentifié", message: "Authentification requise" }),
|
||||||
|
{ status: 401, headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne une réponse 403 Forbidden avec un message personnalisé
|
||||||
|
*/
|
||||||
|
export function forbiddenResponse(reason?: string) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: "Accès refusé",
|
||||||
|
message: reason || "Vous n'avez pas les permissions nécessaires pour cette action",
|
||||||
|
}),
|
||||||
|
{ status: 403, headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie les permissions et retourne une réponse d'erreur si nécessaire
|
||||||
|
* @returns null si autorisé, Response d'erreur sinon
|
||||||
|
*/
|
||||||
|
export function checkPermissionOrRespond(check: PermissionCheck): Response | null {
|
||||||
|
if (!check.allowed) {
|
||||||
|
return forbiddenResponse(check.reason);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue