- 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)
473 lines
13 KiB
TypeScript
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;
|
|
}
|