diff --git a/FIX_STAFF_SALARIE_CREATION.md b/FIX_STAFF_SALARIE_CREATION.md new file mode 100644 index 0000000..00680c0 --- /dev/null +++ b/FIX_STAFF_SALARIE_CREATION.md @@ -0,0 +1,174 @@ +# Fix - Création de salarié en mode Staff + +## 🐛 Problème identifié + +En mode staff, lors de la création d'un nouveau salarié, l'erreur `no_organization` apparaissait même après avoir sélectionné une organisation dans le menu déroulant. + +### Cause racine + +La fonction `resolveActiveOrg()` retourne `null` pour les utilisateurs staff (comportement attendu), mais l'API `/api/salaries` POST rejetait toute demande avec un `null`, même quand l'utilisateur staff fournissait un `employer_id` valide dans le body de la requête. + +La logique de sécurité ne distinguait pas : +- **Utilisateurs clients** : doivent avoir une organisation associée via `organization_members` +- **Utilisateurs staff** : peuvent créer des salariés pour n'importe quelle organisation en fournissant un `employer_id` + +## ✅ Solution implémentée + +### Modifications dans `/app/api/salaries/route.ts` + +La logique de sécurité a été réorganisée pour gérer les deux cas différemment : + +#### 1. Vérification du statut staff en premier + +```typescript +// 🔒 SÉCURITÉ : Vérifier si l'utilisateur est staff +let isStaff = false; +try { + const { data: staffRow } = await sbAuth.from('staff_users').select('is_staff').eq('user_id', user.id).maybeSingle(); + isStaff = !!staffRow?.is_staff; +} catch { + // Fallback sur metadata + const userMeta = user?.user_metadata || {}; + const appMeta = user?.app_metadata || {}; + isStaff = Boolean((userMeta.is_staff === true || userMeta.role === 'staff') || (Array.isArray(appMeta.roles) && appMeta.roles.includes('staff'))); +} +``` + +#### 2. Logique différenciée selon le type d'utilisateur + +**Pour les utilisateurs STAFF :** +```typescript +if (isStaff) { + // Accepter l'employer_id fourni dans la requête + orgId = body.employer_id || null; + + if (!orgId) { + return NextResponse.json( + { ok: false, error: 'no_organization', message: 'Vous devez sélectionner une organisation' }, + { status: 400 } + ); + } + + // Vérifier que l'organisation existe + const { data: orgExists } = await sbAuth + .from('organizations') + .select('id') + .eq('id', orgId) + .maybeSingle(); + + if (!orgExists) { + return NextResponse.json( + { ok: false, error: 'invalid_organization', message: 'Organisation introuvable' }, + { status: 404 } + ); + } +} +``` + +**Pour les utilisateurs CLIENTS :** +```typescript +else { + // Utiliser l'organisation associée à l'utilisateur + const userOrgId = await resolveActiveOrg(sbAuth); + + if (!userOrgId) { + return NextResponse.json( + { ok: false, error: 'no_organization', message: 'Aucune organisation associée à votre compte' }, + { status: 403 } + ); + } + + // 🔒 SÉCURITÉ CRITIQUE : Vérifier que l'employer_id fourni correspond à l'org de l'utilisateur + if (body.employer_id && body.employer_id !== userOrgId) { + return NextResponse.json( + { ok: false, error: 'unauthorized_organization', message: 'Vous ne pouvez pas créer un salarié dans cette organisation' }, + { status: 403 } + ); + } + + orgId = userOrgId; +} +``` + +#### 3. Logs améliorés + +```typescript +console.log('✅ [SÉCURITÉ] Vérifications réussies'); +console.log(' - Utilisateur:', user.email); +console.log(' - Type:', isStaff ? 'STAFF' : 'CLIENT'); +console.log(' - Organisation:', orgId); +console.log(' - Email salarié validé:', body.email_salarie); +console.log(' - Rate limit:', `${rateLimitMap.get(user.id)?.count || 0}/${RATE_LIMIT_MAX}`); +``` + +## 🔒 Sécurité + +### Garanties maintenues + +1. **Utilisateurs clients** : Ne peuvent créer des salariés **QUE** dans leur propre organisation +2. **Utilisateurs staff** : Peuvent créer des salariés pour n'importe quelle organisation, mais : + - Doivent obligatoirement sélectionner une organisation (pas de création sans org) + - L'organisation doit exister dans la base de données +3. **Rate limiting** : Toujours en place (50 créations/heure par utilisateur) +4. **Authentification** : Requise pour tous les utilisateurs + +### Erreurs possibles + +| Code d'erreur | Status | Cas | +|---------------|--------|-----| +| `unauthorized` | 401 | Utilisateur non authentifié | +| `rate_limit_exceeded` | 429 | Limite de 50 créations/heure dépassée | +| `no_organization` | 400 (staff) / 403 (client) | Aucune organisation sélectionnée ou associée | +| `invalid_organization` | 404 | Organisation sélectionnée introuvable (staff) | +| `unauthorized_organization` | 403 | Tentative de créer un salarié dans une autre org (client) | + +## 🧪 Tests à effectuer + +### Test 1 : Staff - Création avec organisation valide +```bash +# En tant que staff, sélectionner une organisation et créer un salarié +✓ Devrait réussir +✓ Le salarié devrait être associé à l'organisation sélectionnée +``` + +### Test 2 : Staff - Création sans sélectionner d'organisation +```bash +# En tant que staff, ne pas sélectionner d'organisation +✗ Devrait échouer avec "no_organization" (400) +``` + +### Test 3 : Staff - Création avec organisation inexistante +```bash +# Manuellement envoyer un employer_id invalide +✗ Devrait échouer avec "invalid_organization" (404) +``` + +### Test 4 : Client - Création normale +```bash +# En tant que client, créer un salarié +✓ Devrait réussir +✓ Le salarié devrait être dans l'organisation du client +``` + +### Test 5 : Client - Tentative de création dans une autre org +```bash +# Manuellement envoyer un employer_id d'une autre organisation +✗ Devrait échouer avec "unauthorized_organization" (403) +``` + +## 📝 Fichiers modifiés + +- `/app/api/salaries/route.ts` - Logique de sécurité réorganisée + +## 📝 Fichiers non modifiés (déjà corrects) + +- `/app/(app)/salaries/nouveau/page.tsx` - Frontend déjà correct + - Charge la liste des organisations pour les staff + - Envoie `employer_id` quand staff ET organisation sélectionnée + - Validation client-side : `const orgOk = !isStaff || (isStaff && selectedOrg !== null);` + +## ✨ Améliorations futures possibles + +1. Ajouter un cache pour les vérifications staff (éviter une requête DB à chaque création) +2. Ajouter un audit log des créations de salariés par staff dans d'autres organisations +3. Permettre aux staff de créer des salariés dans plusieurs organisations en batch diff --git a/app/api/salaries/route.ts b/app/api/salaries/route.ts index 5fdb45f..adda54b 100644 --- a/app/api/salaries/route.ts +++ b/app/api/salaries/route.ts @@ -309,6 +309,23 @@ export async function POST(req: NextRequest) { ); } + // 🔒 SÉCURITÉ : Vérifier si l'utilisateur est staff + let isStaff = false; + try { + const { data: staffRow } = await sbAuth.from('staff_users').select('is_staff').eq('user_id', user.id).maybeSingle(); + isStaff = !!staffRow?.is_staff; + } catch { + const userMeta = user?.user_metadata || {}; + const appMeta = user?.app_metadata || {}; + isStaff = Boolean((userMeta.is_staff === true || userMeta.role === 'staff') || (Array.isArray(appMeta.roles) && appMeta.roles.includes('staff'))); + } + + console.log('🔍 [SÉCURITÉ] Vérification utilisateur:', { + email: user.email, + isStaff, + employer_id_fourni: body.employer_id + }); + // 🔒 SÉCURITÉ : Rate limiting (50 créations par heure par utilisateur) const now = Date.now(); const userRateLimit = rateLimitMap.get(user.id); @@ -346,45 +363,87 @@ export async function POST(req: NextRequest) { } } - // 🔒 SÉCURITÉ : Récupérer l'organisation de l'utilisateur authentifié - const userOrgId = await resolveActiveOrg(sbAuth); + // 🔒 SÉCURITÉ : Logique différente pour staff et clients + let orgId: string | null = null; - if (!userOrgId) { - console.error('❌ [SÉCURITÉ] Aucune organisation trouvée pour l\'utilisateur:', user.id); - return NextResponse.json( - { - ok: false, - error: 'no_organization', - message: 'Aucune organisation associée à votre compte' - }, - { status: 403 } - ); - } - - // 🔒 SÉCURITÉ CRITIQUE : Vérifier que l'employer_id fourni (si présent) correspond à l'organisation de l'utilisateur - let orgId: string | null = body.employer_id || null; - - if (orgId && orgId !== userOrgId) { - console.error('❌ [SÉCURITÉ CRITIQUE] Tentative de création salarié dans une autre organisation!'); - console.error(' - employer_id fourni:', orgId); - console.error(' - organisation utilisateur:', userOrgId); - console.error(' - utilisateur:', user.email); + if (isStaff) { + // Pour les utilisateurs staff : accepter l'employer_id fourni + orgId = body.employer_id || null; - return NextResponse.json( - { - ok: false, - error: 'unauthorized_organization', - message: 'Vous ne pouvez pas créer un salarié dans cette organisation' - }, - { status: 403 } - ); + if (!orgId) { + console.error('❌ [STAFF] employer_id manquant dans la requête'); + return NextResponse.json( + { + ok: false, + error: 'no_organization', + message: 'Vous devez sélectionner une organisation' + }, + { status: 400 } + ); + } + + // Vérifier que l'organisation existe + const { data: orgExists, error: orgError } = await sbAuth + .from('organizations') + .select('id') + .eq('id', orgId) + .maybeSingle(); + + if (orgError || !orgExists) { + console.error('❌ [STAFF] Organisation introuvable:', orgId); + return NextResponse.json( + { + ok: false, + error: 'invalid_organization', + message: 'Organisation introuvable' + }, + { status: 404 } + ); + } + + console.log('✅ [STAFF] Organisation validée:', orgId); + } else { + // Pour les utilisateurs clients : utiliser leur organisation associée + const userOrgId = await resolveActiveOrg(sbAuth); + + if (!userOrgId) { + console.error('❌ [CLIENT] Aucune organisation trouvée pour l\'utilisateur:', user.id); + return NextResponse.json( + { + ok: false, + error: 'no_organization', + message: 'Aucune organisation associée à votre compte' + }, + { status: 403 } + ); + } + + // 🔒 SÉCURITÉ CRITIQUE : Vérifier que l'employer_id fourni (si présent) correspond à l'organisation de l'utilisateur + if (body.employer_id && body.employer_id !== userOrgId) { + console.error('❌ [SÉCURITÉ CRITIQUE] Tentative de création salarié dans une autre organisation!'); + console.error(' - employer_id fourni:', body.employer_id); + console.error(' - organisation utilisateur:', userOrgId); + console.error(' - utilisateur:', user.email); + + return NextResponse.json( + { + ok: false, + error: 'unauthorized_organization', + message: 'Vous ne pouvez pas créer un salarié dans cette organisation' + }, + { status: 403 } + ); + } + + // Utiliser l'organisation de l'utilisateur authentifié + orgId = userOrgId; + + console.log('✅ [CLIENT] Organisation validée:', orgId); } - // Utiliser l'organisation de l'utilisateur authentifié - orgId = userOrgId; - console.log('✅ [SÉCURITÉ] Vérifications réussies'); console.log(' - Utilisateur:', user.email); + console.log(' - Type:', isStaff ? 'STAFF' : 'CLIENT'); console.log(' - Organisation:', orgId); console.log(' - Email salarié validé:', body.email_salarie); console.log(' - Rate limit:', `${rateLimitMap.get(user.id)?.count || 0}/${RATE_LIMIT_MAX}`)