Skip to content

api-staff-member ​

Vue d'ensemble ​

Le service api-staff-member est le cœur du système RBAC de la plateforme Roomee. C'est le service le plus complexe avec 18 controllers différents gérant les membres, workspaces, rôles, permissions et toute la logique multi-tenant.

CaractéristiqueDétail
Port3001
StackJavaScript (Legacy), Express.js, Prisma
Base de donnéesMongoDB (roomee_members)
ArchitectureLegacy (JS + Repository Pattern)
Complexité⭐⭐⭐⭐⭐ (Le plus complexe)

Statistiques ​

MétriqueValeur
Controllers18
Routes120+
Modèles DB18
Middlewares8
Services15+

Architecture du Service ​

mermaid
graph TB
    subgraph "api-staff-member"
        ROUTES[Routes API]

        subgraph "Controllers (18)"
            MEMBER[MemberController]
            WORKSPACE[WorkspaceController]
            ROLE[RoleController]
            PERMISSION[PermissionController]
            TEAM[TeamController]
            DEPT[DepartmentController]
            ONBOARD[OnboardingController]
            ECOSYSTEM[EcosystemController]
        end

        subgraph "Services"
            MEMBER_SVC[MemberService]
            RBAC_SVC[RBACService]
            SYNC_SVC[SyncService]
        end

        subgraph "Repository Layer"
            MEMBER_REPO[MemberRepository]
            WORKSPACE_REPO[WorkspaceRepository]
            ROLE_REPO[RoleRepository]
        end

        ROUTES --> MEMBER
        ROUTES --> WORKSPACE
        ROUTES --> ROLE

        MEMBER --> MEMBER_SVC
        WORKSPACE --> MEMBER_SVC
        ROLE --> RBAC_SVC

        MEMBER_SVC --> MEMBER_REPO
        RBAC_SVC --> ROLE_REPO

        MEMBER_REPO --> PRISMA[(Prisma Client)]
    end

    subgraph "Services Externes"
        AUTH[api-authentication]
        NOTIF[api-notification]
    end

    MEMBER_SVC --> AUTH
    MEMBER_SVC --> NOTIF

Structure du Projet ​

api-staff-member/
├── app.js
├── loader.js
├── controllers/                    # 18 Controllers
│   ├── member.controller.js        # CRUD membres
│   ├── workspace.controller.js     # Gestion workspaces
│   ├── workspaceMember.controller.js # Membres dans workspaces
│   ├── role.controller.js          # Gestion rôles
│   ├── permission.controller.js    # Gestion permissions
│   ├── team.controller.js          # Équipes
│   ├── department.controller.js    # Départements
│   ├── onboarding.controller.js    # Processus d'intégration
│   ├── ecosystem.controller.js     # Écosystèmes
│   ├── ecosystemMembership.controller.js
│   ├── setting.controller.js       # Paramètres utilisateur
│   ├── preference.controller.js    # Préférences
│   ├── invitation.controller.js    # Invitations
│   ├── hierarchy.controller.js     # Hiérarchie manager/subordinés
│   ├── analytics.controller.js     # Statistiques
│   ├── sync.controller.js          # Synchronisation
│   ├── bulk.controller.js          # Opérations en masse
│   └── export.controller.js        # Export de données
├── routes/                         # Routes correspondantes
├── repository/                     # Couche d'accès données
├── services/                       # Business logic
│   ├── member.service.js
│   ├── rbac.service.js
│   ├── workspace.service.js
│   ├── hierarchy.service.js
│   ├── onboarding.service.js
│   └── sync.service.js
├── middlewares/
│   ├── auth.middleware.js
│   ├── permission.middleware.js    # Vérification permissions
│   ├── workspace.middleware.js     # Isolation workspace
│   └── rateLimiter.middleware.js
├── utils/
│   ├── ApiError.js
│   ├── catchAsync.js
│   ├── rbacHelper.js               # Helpers RBAC
│   └── hierarchyHelper.js
├── socket/
│   └── memberSocket.js             # Events temps réel
├── amqp/
│   └── memberEvents.js             # Events AMQP
├── prisma/
│   └── schema.prisma               # Schéma complexe (18 modèles)
└── __tests/

Modèles de Données (Sélection) ​

members ​

prisma
model members {
  id              String                @id @default(auto()) @map("_id") @db.ObjectId
  userId          String?               @db.ObjectId // Lien vers User (api-authentication)

  // Informations personnelles
  firstName       String
  lastName        String
  email           String                @unique
  phone           String?
  photo_url       String?

  // Hiérarchie
  isSuperAdmin    Boolean               @default(false)
  superior        String?               @db.ObjectId // Manager ID
  manager         members?              @relation("ManagerToMembers", fields: [superior], references: [id], onDelete: NoAction, onUpdate: NoAction)
  subordinates    members[]             @relation("ManagerToMembers")

  // Paramètres
  dashboardAccess Boolean               @default(false)
  isActive        Boolean               @default(true)

  // Relations
  workspaces      workspaceMember[]
  ecosystems      ecosystemMembership[]
  teams           TeamMember[]
  departments     DepartmentMember[]
  settings        memberSettings?
  preferences     memberPreference[]

  // Metadata
  deleted_at      DateTime?
  createdAt       DateTime              @default(now())
  updatedAt       DateTime              @updatedAt

  @@index([email])
  @@index([userId])
  @@index([superior])
  @@index([isSuperAdmin])
}

workspaces ​

prisma
model workspaces {
  id                String              @id @default(auto()) @map("_id") @db.ObjectId
  name              String
  description       String?

  // Relations écosystème
  ecosystemId       String              @db.ObjectId // hotelId
  ecosystemType     String              @default("hotel") // hotel, resort, etc.

  // Configuration
  logo_url          String?
  settings          Json?               // Paramètres workspace
  isDefault         Boolean             @default(false)
  isActive          Boolean             @default(true)

  // Relations
  members           workspaceMember[]
  teams             Team[]
  departments       Department[]
  roles             workspaceRole[]

  // Metadata
  deleted_at        DateTime?
  createdAt         DateTime            @default(now())
  updatedAt         DateTime            @updatedAt

  @@index([ecosystemId])
  @@index([isActive])
}

workspaceMember ​

prisma
model workspaceMember {
  id              String              @id @default(auto()) @map("_id") @db.ObjectId

  memberId        String              @db.ObjectId
  member          members             @relation(fields: [memberId], references: [id], onDelete: Cascade)

  workspaceId     String              @db.ObjectId
  workspace       workspaces          @relation(fields: [workspaceId], references: [id], onDelete: Cascade)

  // Rôle dans le workspace
  workspaceRole   String              // "admin", "manager", "member", "viewer"
  permissionSlugs String[]            // Liste des permissions

  // Status
  status          String              @default("active") // active, pending, suspended
  isAdmin         Boolean             @default(false)

  // Dates
  joinedAt        DateTime            @default(now())
  leftAt          DateTime?

  createdAt       DateTime            @default(now())
  updatedAt       DateTime            @updatedAt

  @@unique([memberId, workspaceId])
  @@index([memberId])
  @@index([workspaceId])
  @@index([workspaceRole])
}

workspaceRole ​

prisma
model workspaceRole {
  id              String              @id @default(auto()) @map("_id") @db.ObjectId

  workspaceId     String              @db.ObjectId
  workspace       workspaces          @relation(fields: [workspaceId], references: [id], onDelete: Cascade)

  name            String              // "Manager", "Staff", "Supervisor"
  slug            String              // "manager", "staff"
  description     String?

  // Permissions
  permissions     rolePermission[]

  // Système ou custom
  isSystem        Boolean             @default(false) // Rôles système non modifiables
  isDefault       Boolean             @default(false)

  createdAt       DateTime            @default(now())
  updatedAt       DateTime            @updatedAt

  @@unique([workspaceId, slug])
  @@index([workspaceId])
}

permission ​

prisma
model permission {
  id              String              @id @default(auto()) @map("_id") @db.ObjectId

  slug            String              @unique // "manage_members", "create_content"
  name            String              // "Gérer les membres"
  description     String?

  // Catégorisation
  category        String              // "members", "content", "settings"
  resource        String              // "member", "page", "workspace"
  action          String              // "create", "read", "update", "delete", "manage"

  // Niveau
  level           String              @default("workspace") // "ecosystem", "workspace"

  // Relations
  rolePermissions rolePermission[]

  createdAt       DateTime            @default(now())
  updatedAt       DateTime            @updatedAt

  @@index([category])
  @@index([level])
}

rolePermission ​

prisma
model rolePermission {
  id              String              @id @default(auto()) @map("_id") @db.ObjectId

  roleId          String              @db.ObjectId
  role            workspaceRole       @relation(fields: [roleId], references: [id], onDelete: Cascade)

  permissionId    String              @db.ObjectId
  permission      permission          @relation(fields: [permissionId], references: [id], onDelete: Cascade)

  // Options
  conditions      Json?               // Conditions spécifiques

  createdAt       DateTime            @default(now())

  @@unique([roleId, permissionId])
  @@index([roleId])
  @@index([permissionId])
}

ecosystemMembership ​

prisma
model ecosystemMembership {
  id              String              @id @default(auto()) @map("_id") @db.ObjectId

  memberId        String              @db.ObjectId
  member          members             @relation(fields: [memberId], references: [id], onDelete: Cascade)

  ecosystemId     String              // hotelId
  ecosystemType   String              @default("hotel")

  // Permissions niveau écosystème (générales)
  permissions     String[]            // ["access_live_chat", "screenshot_permission"]

  role            String              @default("member") // "owner", "admin", "member"
  isOwner         Boolean             @default(false)

  createdAt       DateTime            @default(now())
  updatedAt       DateTime            @updatedAt

  @@unique([memberId, ecosystemId])
  @@index([memberId])
  @@index([ecosystemId])
}

Team ​

prisma
model Team {
  id              String              @id @default(auto()) @map("_id") @db.ObjectId

  workspaceId     String              @db.ObjectId
  workspace       workspaces          @relation(fields: [workspaceId], references: [id], onDelete: Cascade)

  name            String
  description     String?
  color           String?             // Couleur d'identification
  icon            String?

  // Relations
  members         TeamMember[]
  leaderId        String?             @db.ObjectId // Team leader

  isActive        Boolean             @default(true)
  deleted_at      DateTime?
  createdAt       DateTime            @default(now())
  updatedAt       DateTime            @updatedAt

  @@index([workspaceId])
}

Department ​

prisma
model Department {
  id              String              @id @default(auto()) @map("_id") @db.ObjectId

  workspaceId     String              @db.ObjectId
  workspace       workspaces          @relation(fields: [workspaceId], references: [id], onDelete: Cascade)

  name            String              // "Réception", "Housekeeping", "Restaurant"
  description     String?
  code            String?             // Code département

  // Hiérarchie
  parentId        String?             @db.ObjectId
  parent          Department?         @relation("DepartmentHierarchy", fields: [parentId], references: [id], onDelete: NoAction, onUpdate: NoAction)
  subDepartments  Department[]        @relation("DepartmentHierarchy")

  // Relations
  members         DepartmentMember[]
  managerId       String?             @db.ObjectId // Department manager

  isActive        Boolean             @default(true)
  deleted_at      DateTime?
  createdAt       DateTime            @default(now())
  updatedAt       DateTime            @updatedAt

  @@index([workspaceId])
  @@index([parentId])
}

onboarding ​

prisma
model onboarding {
  id              String              @id @default(auto()) @map("_id") @db.ObjectId

  // Membre invité
  email           String
  firstName       String?
  lastName        String?

  // Invitation
  token           String              @unique
  invitedBy       String              @db.ObjectId // ID du membre inviteur

  // Contexte
  workspaceId     String              @db.ObjectId
  ecosystemId     String
  workspaceRole   String              @default("member")
  permissions     String[]

  // Status
  status          String              @default("pending") // pending, accepted, expired, cancelled
  acceptedAt      DateTime?
  expiresAt       DateTime

  // Metadata
  invitationData  Json?               // Données supplémentaires

  createdAt       DateTime            @default(now())
  updatedAt       DateTime            @updatedAt

  @@index([email])
  @@index([token])
  @@index([workspaceId])
  @@index([status])
}

Routes API (Sélection des principales) ​

👤 Membres (/members) ​

POST /members/ ​

Créer un nouveau membre.

Request :

json
{
  "firstName": "Marie",
  "lastName": "Dubois",
  "email": "marie.dubois@hotel.com",
  "phone": "+33 6 12 34 56 78",
  "workspaceId": "65f1234567890abcdef12345",
  "workspaceRole": "manager",
  "superior": "65f9876543210fedcba98765",
  "dashboardAccess": true
}

Response :

json
{
  "id": "65faaaaaaaaaaaaaaaaaaaaa",
  "firstName": "Marie",
  "lastName": "Dubois",
  "email": "marie.dubois@hotel.com",
  "workspaces": [
    {
      "workspaceId": "65f1234567890abcdef12345",
      "workspaceRole": "manager",
      "permissionSlugs": ["manage_team", "view_reports"]
    }
  ],
  "superior": "65f9876543210fedcba98765",
  "dashboardAccess": true
}

GET /members/get/all ​

Récupérer tous les membres (avec filtres).

Query Params :

  • workspaceId : Filtrer par workspace
  • workspaceRole : Filtrer par rôle
  • isActive : Filtrer par statut
  • search : Recherche nom/email
  • page, limit : Pagination

Response :

json
{
  "data": [
    {
      "id": "65faaaaaaaaaaaaaaaaaaaaa",
      "firstName": "Marie",
      "lastName": "Dubois",
      "email": "marie.dubois@hotel.com",
      "photo_url": "https://...",
      "workspaces": [...],
      "isActive": true
    }
  ],
  "meta": {
    "total": 150,
    "page": 1,
    "limit": 20
  }
}

GET /members/get/:id ​

Récupérer un membre par son ID avec toutes ses relations.

Response :

json
{
  "id": "65faaaaaaaaaaaaaaaaaaaaa",
  "firstName": "Marie",
  "lastName": "Dubois",
  "email": "marie.dubois@hotel.com",
  "phone": "+33 6 12 34 56 78",
  "photo_url": "https://...",
  "isSuperAdmin": false,
  "dashboardAccess": true,

  "manager": {
    "id": "65f9876543210fedcba98765",
    "firstName": "Jean",
    "lastName": "Martin"
  },

  "subordinates": [
    {
      "id": "65fbbbbbbbbbbbbbbbbbbbbb",
      "firstName": "Paul",
      "lastName": "Durand"
    }
  ],

  "workspaces": [
    {
      "workspaceId": "65f1234567890abcdef12345",
      "workspaceName": "Grand Hotel Paris",
      "workspaceRole": "manager",
      "permissions": ["manage_team", "view_reports"],
      "isAdmin": false
    }
  ],

  "ecosystems": [
    {
      "ecosystemId": "65fccccccccccccccccccccc",
      "role": "admin",
      "permissions": ["access_live_chat", "screenshot_permission"]
    }
  ],

  "teams": [
    {
      "teamId": "65fddddddddddddddddddddd",
      "teamName": "Équipe Réception",
      "role": "member"
    }
  ],

  "departments": [
    {
      "departmentId": "65feeeeeeeeeeeeeeeeeeeeee",
      "departmentName": "Réception",
      "role": "staff"
    }
  ]
}

PATCH /members/update/:id ​

Mettre à jour un membre.


DELETE /members/delete/:id ​

Supprimer un membre (soft delete).


GET /members/get/with/:email ​

Récupérer un membre par son email (utilisé par api-authentication).


POST /members/add/hotel/member ​

Créer un administrateur d'hôtel (appelé par api-hotel).

Request :

json
{
  "hotelId": "65f1111111111111111111111",
  "email": "admin@hotel.com",
  "firstName": "Admin",
  "lastName": "Hotel",
  "dashboardAccess": true,
  "isAdmin": true
}

GET /members/get/by/hotel_id/:id ​

Récupérer tous les membres d'un hôtel.


GET /members/hierarchy/:id ​

Récupérer la hiérarchie complète d'un membre (manager + subordinés récursifs).

Response :

json
{
  "member": {
    "id": "65faaaaaaaaaaaaaaaaaaaaa",
    "firstName": "Marie",
    "lastName": "Dubois"
  },
  "manager": {
    "id": "65f9876543210fedcba98765",
    "firstName": "Jean",
    "lastName": "Martin"
  },
  "subordinates": [
    {
      "id": "65fbbbbbbbbbbbbbbbbbbbbb",
      "firstName": "Paul",
      "lastName": "Durand",
      "subordinates": [...]
    }
  ]
}

🏢 Workspaces (/workspaces) ​

POST /workspaces/ ​

Créer un nouveau workspace.

Request :

json
{
  "name": "Grand Hotel Lyon",
  "description": "Workspace principal",
  "ecosystemId": "65f1111111111111111111111",
  "ecosystemType": "hotel",
  "isDefault": true,
  "settings": {
    "timezone": "Europe/Paris",
    "language": "fr"
  }
}

GET /workspaces/get/all ​

Récupérer tous les workspaces.


GET /workspaces/get/:id ​

Récupérer un workspace avec ses membres.

Response :

json
{
  "id": "65f1234567890abcdef12345",
  "name": "Grand Hotel Lyon",
  "description": "Workspace principal",
  "ecosystemId": "65f1111111111111111111111",
  "logo_url": "https://...",
  "isActive": true,

  "members": [
    {
      "memberId": "65faaaaaaaaaaaaaaaaaaaaa",
      "firstName": "Marie",
      "lastName": "Dubois",
      "workspaceRole": "admin",
      "permissions": ["manage_members", "manage_content"]
    }
  ],

  "teams": [
    {
      "id": "65fddddddddddddddddddddd",
      "name": "Équipe Réception",
      "membersCount": 5
    }
  ],

  "departments": [
    {
      "id": "65feeeeeeeeeeeeeeeeeeeeee",
      "name": "Réception",
      "membersCount": 8
    }
  ],

  "roles": [
    {
      "id": "65fffffffffffffffffffff",
      "name": "Manager",
      "slug": "manager",
      "isSystem": true
    }
  ]
}

POST /workspaces/:id/add-member ​

Ajouter un membre à un workspace.

Request :

json
{
  "memberId": "65faaaaaaaaaaaaaaaaaaaaa",
  "workspaceRole": "manager",
  "permissions": ["manage_team", "view_reports"]
}

DELETE /workspaces/:workspaceId/remove-member/:memberId ​

Retirer un membre d'un workspace.


🎭 Rôles (/roles) ​

POST /roles/ ​

Créer un rôle personnalisé.

Request :

json
{
  "workspaceId": "65f1234567890abcdef12345",
  "name": "Superviseur",
  "slug": "supervisor",
  "description": "Supervise une équipe",
  "permissions": [
    "view_members",
    "view_reports",
    "manage_team"
  ]
}

GET /roles/workspace/:workspaceId ​

Récupérer tous les rôles d'un workspace.

Response :

json
{
  "data": [
    {
      "id": "65fffffffffffffffffffff",
      "name": "Admin",
      "slug": "admin",
      "isSystem": true,
      "isDefault": false,
      "permissions": [
        {
          "slug": "manage_members",
          "name": "Gérer les membres",
          "category": "members"
        },
        {
          "slug": "manage_content",
          "name": "Gérer le contenu",
          "category": "content"
        }
      ]
    }
  ]
}

PATCH /roles/:id ​

Mettre à jour un rôle (seulement les rôles custom).


POST /roles/:id/add-permission ​

Ajouter une permission à un rôle.

Request :

json
{
  "permissionSlug": "create_content"
}

DELETE /roles/:id/remove-permission/:permissionSlug ​

Retirer une permission d'un rôle.


🔑 Permissions (/permissions) ​

GET /permissions/all ​

Récupérer toutes les permissions disponibles.

Response :

json
{
  "data": [
    {
      "id": "65f0000000000000000000001",
      "slug": "manage_members",
      "name": "Gérer les membres",
      "description": "Créer, modifier et supprimer des membres",
      "category": "members",
      "resource": "member",
      "action": "manage",
      "level": "workspace"
    },
    {
      "id": "65f0000000000000000000002",
      "slug": "create_content",
      "name": "Créer du contenu",
      "category": "content",
      "resource": "page",
      "action": "create",
      "level": "workspace"
    }
  ],
  "categories": {
    "members": ["manage_members", "view_members"],
    "content": ["create_content", "edit_content", "delete_content"],
    "settings": ["manage_settings", "view_settings"],
    "communication": ["send_notifications", "access_chat"]
  }
}

POST /permissions/ ​

Créer une permission personnalisée.


GET /permissions/member/:memberId/workspace/:workspaceId ​

Récupérer toutes les permissions effectives d'un membre dans un workspace.

Response :

json
{
  "memberId": "65faaaaaaaaaaaaaaaaaaaaa",
  "workspaceId": "65f1234567890abcdef12345",
  "workspaceRole": "manager",
  "permissions": [
    "manage_team",
    "view_reports",
    "create_content",
    "edit_content"
  ],
  "source": {
    "role": ["manage_team", "view_reports"],
    "direct": ["create_content", "edit_content"]
  }
}

👥 Équipes (/teams) ​

POST /teams/ ​

Créer une équipe.

Request :

json
{
  "workspaceId": "65f1234567890abcdef12345",
  "name": "Équipe Réception",
  "description": "Équipe de la réception",
  "color": "#3B82F6",
  "icon": "🏨",
  "leaderId": "65faaaaaaaaaaaaaaaaaaaaa"
}

GET /teams/workspace/:workspaceId ​

Récupérer toutes les équipes d'un workspace.


POST /teams/:id/add-member ​

Ajouter un membre à une équipe.


GET /teams/:id/members ​

Récupérer les membres d'une équipe.


🏢 Départements (/departments) ​

POST /departments/ ​

Créer un département.

Request :

json
{
  "workspaceId": "65f1234567890abcdef12345",
  "name": "Réception",
  "description": "Service de réception",
  "code": "RCP",
  "parentId": null,
  "managerId": "65faaaaaaaaaaaaaaaaaaaaa"
}

GET /departments/workspace/:workspaceId ​

Récupérer tous les départements d'un workspace (hiérarchie).

Response :

json
{
  "data": [
    {
      "id": "65feeeeeeeeeeeeeeeeeeeeee",
      "name": "Réception",
      "code": "RCP",
      "manager": {
        "id": "65faaaaaaaaaaaaaaaaaaaaa",
        "firstName": "Marie",
        "lastName": "Dubois"
      },
      "membersCount": 8,
      "subDepartments": [
        {
          "id": "65fffffffffffffffffffffe",
          "name": "Réception - Nuit",
          "code": "RCP-N",
          "membersCount": 3
        }
      ]
    }
  ]
}

🎓 Onboarding (/onboarding) ​

POST /onboarding/invite ​

Envoyer une invitation d'intégration.

Request :

json
{
  "email": "nouveau@hotel.com",
  "firstName": "Nouveau",
  "lastName": "Membre",
  "workspaceId": "65f1234567890abcdef12345",
  "ecosystemId": "65f1111111111111111111111",
  "workspaceRole": "member",
  "permissions": ["view_content"]
}

Response :

json
{
  "id": "65f2222222222222222222222",
  "email": "nouveau@hotel.com",
  "token": "onboarding-token-abc123",
  "status": "pending",
  "expiresAt": "2025-01-27T10:00:00.000Z",
  "invitationLink": "https://app.roomee.io/onboarding?token=..."
}

Logique :

  1. Création du record onboarding
  2. Génération d'un token unique (7 jours)
  3. Émission événement AMQP member.onboarding
  4. api-notification envoie l'email d'invitation

GET /onboarding/get/all ​

Récupérer toutes les invitations (filtres par status).


POST /onboarding/accept/:token ​

Accepter une invitation (appelé après inscription user).


DELETE /onboarding/cancel/:id ​

Annuler une invitation.


⚙️ Paramètres (/settings) ​

GET /settings/member/:id ​

Récupérer les paramètres d'un membre.


PATCH /settings/member/:id ​

Mettre à jour les paramètres.


📊 Analytics (/analytics) ​

GET /analytics/workspace/:id ​

Statistiques d'un workspace.

Response :

json
{
  "workspaceId": "65f1234567890abcdef12345",
  "members": {
    "total": 45,
    "active": 42,
    "pending": 3,
    "byRole": {
      "admin": 2,
      "manager": 8,
      "member": 35
    }
  },
  "teams": {
    "total": 6,
    "averageSize": 7.5
  },
  "departments": {
    "total": 4
  },
  "activity": {
    "newMembersThisMonth": 5,
    "activeToday": 38
  }
}

🔄 Synchronisation (/sync) ​

POST /sync/members ​

Synchroniser les membres (appelé par api-news et api-notification).

Request :

json
{
  "workspaceId": "65f1234567890abcdef12345"
}

Response :

json
{
  "members": [
    {
      "id": "65faaaaaaaaaaaaaaaaaaaaa",
      "firstName": "Marie",
      "lastName": "Dubois",
      "email": "marie.dubois@hotel.com",
      "photo_url": "https://..."
    }
  ],
  "syncedAt": "2025-01-21T10:00:00.000Z"
}

📥 Export (/export) ​

GET /export/members/workspace/:workspaceId ​

Exporter les membres d'un workspace (CSV/Excel).

Query Params :

  • format : csv ou xlsx

🔢 Bulk Operations (/bulk) ​

POST /bulk/add-members ​

Ajouter plusieurs membres en masse.

Request :

json
{
  "workspaceId": "65f1234567890abcdef12345",
  "members": [
    {
      "email": "user1@hotel.com",
      "firstName": "User",
      "lastName": "One",
      "workspaceRole": "member"
    },
    {
      "email": "user2@hotel.com",
      "firstName": "User",
      "lastName": "Two",
      "workspaceRole": "member"
    }
  ]
}

POST /bulk/update-roles ​

Modifier les rôles de plusieurs membres.


POST /bulk/assign-permissions ​

Assigner des permissions à plusieurs membres.


Middlewares ​

Permission Middleware ​

javascript
// middlewares/permission.middleware.js
const requirePermission = (permissionSlug) => {
  return async (req, res, next) => {
    const { userId } = req.user
    const { workspaceId } = req.params || req.body

    // Récupérer les permissions du membre dans le workspace
    const permissions = await rbacService.getMemberPermissions(
      userId,
      workspaceId
    )

    if (!permissions.includes(permissionSlug)) {
      throw new ApiError(403, 'Permission denied')
    }

    next()
  }
}

// Usage
router.post(
  '/members/',
  authMiddleware.middleware,
  requirePermission('manage_members'),
  memberController.create
)

Workspace Isolation Middleware ​

javascript
// middlewares/workspace.middleware.js
const ensureWorkspaceAccess = async (req, res, next) => {
  const { userId } = req.user
  const { workspaceId } = req.params || req.body

  const hasAccess = await workspaceService.hasMemberAccess(
    userId,
    workspaceId
  )

  if (!hasAccess) {
    throw new ApiError(403, 'Workspace access denied')
  }

  next()
}

Système RBAC ​

Architecture à 2 Niveaux ​

mermaid
graph TB
    subgraph "Niveau 1: Écosystème"
        ECO_PERM[Permissions Générales]
        LIVE_CHAT[access_live_chat]
        SCREENSHOT[screenshot_permission]
        ECOSYSTEM_MANAGE[manage_ecosystem]
    end

    subgraph "Niveau 2: Workspace"
        WS_ROLE[Rôle Workspace]
        WS_PERM[Permissions Workspace]

        ADMIN[Admin]
        MANAGER[Manager]
        MEMBER[Member]

        MANAGE_MEMBERS[manage_members]
        CREATE_CONTENT[create_content]
        VIEW_REPORTS[view_reports]
    end

    ECO_PERM --> LIVE_CHAT
    ECO_PERM --> SCREENSHOT

    WS_ROLE --> ADMIN
    WS_ROLE --> MANAGER
    WS_ROLE --> MEMBER

    ADMIN --> MANAGE_MEMBERS
    ADMIN --> CREATE_CONTENT
    MANAGER --> CREATE_CONTENT
    MANAGER --> VIEW_REPORTS

Catégories de Permissions ​

javascript
const PERMISSION_CATEGORIES = {
  members: [
    'manage_members',      // CRUD membres
    'view_members',        // Voir les membres
    'invite_members',      // Inviter des membres
    'remove_members',      // Retirer des membres
    'manage_roles'         // Gérer les rôles
  ],

  content: [
    'create_content',      // Créer du contenu
    'edit_content',        // Modifier du contenu
    'delete_content',      // Supprimer du contenu
    'publish_content',     // Publier du contenu
    'moderate_comments'    // Modérer les commentaires
  ],

  settings: [
    'manage_settings',     // Gérer les paramètres
    'view_settings',       // Voir les paramètres
    'manage_integrations'  // Gérer les intégrations
  ],

  communication: [
    'send_notifications',  // Envoyer des notifications
    'access_chat',         // Accéder au chat
    'manage_channels'      // Gérer les canaux
  ],

  analytics: [
    'view_reports',        // Voir les rapports
    'export_data',         // Exporter les données
    'view_analytics'       // Voir les analytics
  ]
}

Vérification des Permissions ​

javascript
// services/rbac.service.js
class RBACService {
  async getMemberPermissions(memberId, workspaceId) {
    // 1. Récupérer le workspace member
    const workspaceMember = await prisma.workspaceMember.findUnique({
      where: {
        memberId_workspaceId: { memberId, workspaceId }
      },
      include: {
        workspace: {
          include: {
            roles: {
              where: { slug: workspaceMember.workspaceRole },
              include: {
                permissions: {
                  include: { permission: true }
                }
              }
            }
          }
        }
      }
    })

    // 2. Permissions du rôle
    const rolePermissions = workspaceMember.workspace.roles
      .flatMap(role => role.permissions.map(rp => rp.permission.slug))

    // 3. Permissions directes
    const directPermissions = workspaceMember.permissionSlugs || []

    // 4. Fusionner et dédupliquer
    return [...new Set([...rolePermissions, ...directPermissions])]
  }

  async hasPermission(memberId, workspaceId, permissionSlug) {
    const permissions = await this.getMemberPermissions(memberId, workspaceId)
    return permissions.includes(permissionSlug)
  }
}

Événements AMQP ​

Événements Émis ​

Routing KeyPayloadDescription
roomee.notification.member.registered{ memberId, email, workspaceId }Nouveau membre
roomee.notification.member.updated{ memberId, changes }Membre mis à jour
roomee.notification.member.deleted{ memberId, email }Membre supprimé
roomee.notification.workspace.created{ workspaceId, name, ecosystemId }Workspace créé
roomee.notification.workspace.member_joined{ memberId, workspaceId }Membre rejoint workspace
roomee.notification.workspace.member_left{ memberId, workspaceId }Membre quitté workspace

Socket.IO Events ​

javascript
// socket/memberSocket.js
io.on('connection', (socket) => {
  // Rejoindre les rooms workspace
  socket.on('joinWorkspaces', (workspaceIds) => {
    workspaceIds.forEach(id => {
      socket.join(`workspace:${id}`)
    })
  })

  // Rejoindre la room personnelle
  socket.on('joinMember', (memberId) => {
    socket.join(`member:${memberId}`)
  })
})

// Émissions serveur
io.to(`workspace:${workspaceId}`).emit('member:joined', memberData)
io.to(`member:${memberId}`).emit('role:updated', roleData)

Configuration ​

bash
# Server
PORT=3001

# Database
DATABASE_URL=mongodb://localhost:27017/roomee_members

# JWT
JWT_ACCESS_SECRET=your-jwt-secret

# AMQP
AMQP_GATEWAY_URL=amqp://localhost:5672
AMQP_EXCHANGE_NAME=roomee_events
AMQP_ROUTING_KEY_BASE=roomee.notification.member

# Service URLs
AUTH_API_URL=http://localhost:3000

# API Keys
AUTH_SERVICE_API_KEY=your-auth-api-key

Bonnes Pratiques ​

1. Toujours vérifier l'isolation workspace ​

javascript
const members = await prisma.members.findMany({
  where: {
    workspaces: {
      some: {
        workspaceId: req.user.workspaceId
      }
    },
    deleted_at: null
  }
})

2. Vérifier les permissions avant chaque action ​

javascript
const hasPermission = await rbacService.hasPermission(
  req.user.userId,
  workspaceId,
  'manage_members'
)

if (!hasPermission) {
  throw new ApiError(403, 'Permission denied')
}

3. Gérer la hiérarchie manager/subordinés ​

javascript
// Un manager ne peut gérer que ses subordinés
const isSubordinate = await hierarchyService.isSubordinate(
  managerId,
  memberId
)

if (!isSubordinate) {
  throw new ApiError(403, 'Cannot manage this member')
}

Améliorations Futures ​

  • [ ] Cache Redis pour les permissions (TTL 5 min)
  • [ ] Graph API pour les hiérarchies complexes
  • [ ] Webhooks pour les événements membres
  • [ ] Audit trail complet des actions RBAC
  • [ ] Permissions temporaires (time-bound)
  • [ ] Délégation de permissions
  • [ ] SSO avec SAML/LDAP pour entreprises

Documentation technique Roomee Services