espace-paie-odentas/lib/permissions.ts
odentas 78c43f0bfa 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)
2025-11-14 20:25:30 +01:00

473 lines
13 KiB
TypeScript

/**
* 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;
}