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