fix: Corriger création salarié en mode staff avec employer_id
- Réorganiser la logique de sécurité pour distinguer staff/client - Staff: accepter employer_id fourni et vérifier existence de l'org - Client: utiliser l'org associée et bloquer toute tentative cross-org - Améliorer les logs pour identifier le type d'utilisateur - Corriger le retour no_organization inapproprié pour les staff
This commit is contained in:
parent
8cdd133d68
commit
2760ea2b39
2 changed files with 266 additions and 33 deletions
174
FIX_STAFF_SALARIE_CREATION.md
Normal file
174
FIX_STAFF_SALARIE_CREATION.md
Normal file
|
|
@ -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
|
||||||
|
|
@ -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)
|
// 🔒 SÉCURITÉ : Rate limiting (50 créations par heure par utilisateur)
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const userRateLimit = rateLimitMap.get(user.id);
|
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é
|
// 🔒 SÉCURITÉ : Logique différente pour staff et clients
|
||||||
const userOrgId = await resolveActiveOrg(sbAuth);
|
let orgId: string | null = null;
|
||||||
|
|
||||||
if (!userOrgId) {
|
if (isStaff) {
|
||||||
console.error('❌ [SÉCURITÉ] Aucune organisation trouvée pour l\'utilisateur:', user.id);
|
// Pour les utilisateurs staff : accepter l'employer_id fourni
|
||||||
return NextResponse.json(
|
orgId = body.employer_id || null;
|
||||||
{
|
|
||||||
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);
|
|
||||||
|
|
||||||
return NextResponse.json(
|
if (!orgId) {
|
||||||
{
|
console.error('❌ [STAFF] employer_id manquant dans la requête');
|
||||||
ok: false,
|
return NextResponse.json(
|
||||||
error: 'unauthorized_organization',
|
{
|
||||||
message: 'Vous ne pouvez pas créer un salarié dans cette organisation'
|
ok: false,
|
||||||
},
|
error: 'no_organization',
|
||||||
{ status: 403 }
|
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('✅ [SÉCURITÉ] Vérifications réussies');
|
||||||
console.log(' - Utilisateur:', user.email);
|
console.log(' - Utilisateur:', user.email);
|
||||||
|
console.log(' - Type:', isStaff ? 'STAFF' : 'CLIENT');
|
||||||
console.log(' - Organisation:', orgId);
|
console.log(' - Organisation:', orgId);
|
||||||
console.log(' - Email salarié validé:', body.email_salarie);
|
console.log(' - Email salarié validé:', body.email_salarie);
|
||||||
console.log(' - Rate limit:', `${rateLimitMap.get(user.id)?.count || 0}/${RATE_LIMIT_MAX}`)
|
console.log(' - Rate limit:', `${rateLimitMap.get(user.id)?.count || 0}/${RATE_LIMIT_MAX}`)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue