Aller au contenu principal

Multi-Tenant Architecture - Role-Based Access

Overview

The multi-tenant architecture uses role families to determine user type, not a separate isInleed boolean flag. This ensures consistency and maintainability.

Role System

Role Families

There are two role families defined in configs/roles.config.ts:

INLEED: {
inleedMaster: { lvl: 0, description: "Inleed user with full access" },
inleedSlave: { lvl: 1, description: "Inleed user with limited access" }
},
USER: {
commonMaster: { lvl: 2, description: "Common user with full access" },
commonSlave: { lvl: 3, description: "Common user with limited access" }
}

User Type Determination

  • Inleed Users: Users with roles in the INLEED family (inleedMaster, inleedSlave)
  • Client Users: Users with roles in the USER family (commonMaster, commonSlave)

Backend Implementation

Database Field: isInleed

The Credential model has an isInleed boolean field, but it's denormalized data computed from role:

// In credential.model.ts
isInleed: {
type: Boolean,
required: true,
default: false, // Defaults to client user
index: true,
}

Why keep this field?

  • Performance: Avoids role family lookup on every request
  • Database queries: Easier to query and index
  • Middleware efficiency: Fast access in tenant isolation middleware

Important: This field should ALWAYS be computed from the role, never set manually.

Automatic Role-to-isInleed Mapping

When creating/updating credentials, isInleed is automatically set based on role:

// In credential.service.ts - createCredential
const inleedRoles = Object.keys(rolesList("INLEED"));
const isInleed = inleedRoles.includes(role);

const credential = await Credential.create({
// ... other fields
role,
isInleed, // Computed from role
});

Tenant Isolation Middleware

Uses req.isInleed (from user.isInleed) to determine tenant access rules:

// In tenantIsolation.middleware.ts
req.isInleed = user.isInleed || false;

if (req.isInleed) {
// Inleed users can override tenantId via query/header
req.tenantId = req.query.tenantId || req.headers['x-tenant-id'] || user.tenantId;
} else {
// Client users must use their credential's tenantId
req.tenantId = user.tenantId;
}

Frontend Implementation

Interface

The frontend ICredential interface includes isInleed since the backend returns it:

export interface ICredential {
_id: string;
tenantId?: string;
isInleed: boolean; // From backend, computed from role
role: string;
// ... other fields
}

Using Role-Based Checks

Best Practice: Use the useRoleAccess hook instead of directly checking user.isInleed:

import { useRoleAccess } from "~/hooks/useRoleAccess.hook";

function MyComponent() {
const { isInleedUser } = useRoleAccess();

if (isInleedUser) {
// Show Inleed-specific features
}
}

Why?

  • Single source of truth: Role family check is consistent
  • Flexibility: Easy to add more role families in the future
  • Type safety: Hook provides computed values based on current user

useRoleAccess Hook

The hook provides convenient role checking utilities:

const {
// User info
user,
userRole,
userLevel,

// Role checks
hasRole,
hasAnyRole,
hasMinLevel,
isInFamily,

// Computed flags
isInleedUser, // Checks if role in INLEED family
isCommonUser, // Checks if role in USER family
isInleedMaster,
isInleedSlave,
isCommonMaster,
isCommonSlave,
} = useRoleAccess();

Implementation:

const isInleedUser = useMemo(() => {
return isInFamily("INLEED");
}, [user]);

This checks if the user's role belongs to the INLEED family by comparing against rolesList("INLEED").

Migration from isInleed to Role-Based

Backend Migration Script

The migration script sets isInleed based on role:

// In migrate-to-multitenant.ts
const MIGRATION_CONFIG = {
inleedRoles: ["inleedMaster", "inleedSlave"],
};

// Mark Inleed users
await Credential.updateMany(
{ role: { $in: MIGRATION_CONFIG.inleedRoles } },
{ $set: { isInleed: true } }
);

// Mark client users
await Credential.updateMany(
{ role: { $nin: MIGRATION_CONFIG.inleedRoles } },
{ $set: { isInleed: false } }
);

Frontend Components Updated

All components now use useRoleAccess hook:

  1. TenantGuard - Checks isInleedUser before requiring tenant selection
  2. TenantIndicator - Only shows for isInleedUser
  3. TenantSelectionPage - Redirects non-Inleed users
  4. UserContext - Uses response.isInleed for routing logic

Best Practices

When Creating New Credentials

Always compute isInleed from role:

// ✅ CORRECT
const inleedRoles = Object.keys(rolesList("INLEED"));
const isInleed = inleedRoles.includes(newUser.role);

await Credential.create({
userName,
email,
role,
isInleed, // Computed
});

// ❌ WRONG
await Credential.create({
userName,
email,
role: "inleedMaster",
isInleed: false, // NEVER hardcode or manually set!
});

When Checking User Type

Use role-based checks in frontend:

// ✅ CORRECT - Uses hook
const { isInleedUser } = useRoleAccess();
if (isInleedUser) { /* ... */ }

// ✅ ALSO CORRECT - Direct role check
const { isInFamily } = useRoleAccess();
if (isInFamily("INLEED")) { /* ... */ }

// ⚠️ WORKS but less flexible
if (user.isInleed) { /* ... */ }

When Adding New Roles

  1. Add role to appropriate family in roles.config.ts
  2. Backend will automatically compute isInleed correctly
  3. Frontend useRoleAccess hook will work without changes

Summary

  • Backend: isInleed is a denormalized database field computed from role family
  • Frontend: Use useRoleAccess hook for role-based checks
  • Source of Truth: Role family (INLEED vs USER)
  • Benefit: Consistent, maintainable, and flexible architecture

The isInleed field exists for performance but should always reflect the role family membership.