Aller au contenu principal

Multi-Tenant Back Office Implementation Guide

Overview

This document describes the Back Office (React) implementation for the multi-tenant architecture. The Back Office now supports two distinct user flows based on user type.

User Flows

Inleed User Flow (Internal Administrators)

1. Login → Authentication
2. Redirect to Tenant Selection Page
3. Select a tenant from paginated list
4. Redirect to Dashboard (scoped to selected tenant)
5. Can switch tenants at any time

Client User Flow (Regular Customers)

1. Login → Authentication
2. Direct redirect to Dashboard (scoped to their tenant)
3. Cannot access tenant selection
4. Cannot switch tenants

Architecture Components

1. Tenant Context

Location: Baldr-Bo/app/context/tenant.context.tsx

Purpose: Manages the currently selected tenant for Inleed users.

Key Features:

  • Stores selected tenant in state and localStorage
  • Persists tenant selection across page reloads
  • Provides methods to switch tenants
  • Automatically loads saved tenant on mount

API:

interface TenantContextType {
currentTenant: ITenant | null;
setCurrentTenant: (tenant: ITenant | null) => void;
refreshTenant: () => Promise<void>;
clearTenant: () => void;
}

Usage Example:

import { useTenant } from "~/context/tenant.context";

function MyComponent() {
const { currentTenant, setCurrentTenant } = useTenant();

// Check if tenant is selected
if (!currentTenant) {
return <div>Please select a tenant</div>;
}

// Use tenant ID for API calls
fetchData(currentTenant._id);
}

2. Updated User Context

Location: Baldr-Bo/app/context/user.context.tsx

Changes:

  • Added isInleed check to user object
  • Updated authentication redirect logic
  • Inleed users → Tenant Selection Page
  • Client users → Dashboard

New Behavior:

// After successful login
if (user.isInleed) {
navigate("/tenants/selection");
} else {
navigate("/dashboard");
}

3. Tenant Selection Page

Location: Baldr-Bo/app/pages/tenants/tenantSelection.page.tsx

Features:

  • Paginated tenant list (default 20 per page)
  • Search by name, slug, or email (debounced 500ms)
  • Status indicators (active/suspended/inactive)
  • Click to select tenant
  • Only accessible by Inleed users

Component Structure:

export default function TenantSelectionPage() {
// Fetch tenants with pagination
const [tenants, setTenants] = useState<ITenant[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [searchQuery, setSearchQuery] = useState("");

// Handle tenant selection
const handleTenantSelect = (tenant: ITenant) => {
setCurrentTenant(tenant);
navigate("/dashboard");
};

return (
<DataTable
value={tenants}
paginator
lazy
// ... configuration
/>
);
}

4. Tenant API

Location: Baldr-Bo/app/api/tenant.api.ts

Methods:

class TenantApi {
// List all tenants (Inleed only)
async getAll(params?: {
page?: number;
limit?: number;
search?: string;
status?: TenantStatus;
}): Promise<ITenantListResponse>;

// Get tenant by ID
async getById(id: string): Promise<ITenant>;

// Create tenant (Inleed only)
async create(data: {...}): Promise<ITenant>;

// Update tenant (Inleed only)
async update(id: string, data: {...}): Promise<ITenant>;

// Delete tenant (Inleed only)
async delete(id: string): Promise<void>;

// Get tenant statistics
async getStats(id: string): Promise<{...}>;
}

5. Updated Credential Interface

Location: Baldr-Bo/app/interfaces/credential.interface.ts

New Fields:

export interface ICredential {
_id: string;

// Multi-tenant fields
tenantId?: string; // Present for client users
isInleed: boolean; // true for internal admins

// ... existing fields
}

Routing Configuration

Protected Routes

All routes except public ones require authentication. Add the tenant selection route:

// In routes configuration
const routes = [
// Public routes
{ path: "/connexion", element: <LoginPage /> },
{ path: "/mot-de-passe-oublie", element: <ForgotPasswordPage /> },
{ path: "/reinitialisation-mot-de-passe", element: <ResetPasswordPage /> },

// Tenant selection (Inleed only)
{ path: "/tenants/selection", element: <TenantSelectionPage /> },

// Protected routes
{ path: "/dashboard", element: <Dashboard /> },
// ... other routes
];

Route Guards

Implement route guards to protect tenant-specific routes:

function TenantGuard({ children }: { children: ReactNode }) {
const { user } = useUser();
const { currentTenant } = useTenant();
const navigate = useNavigate();

useEffect(() => {
// Inleed users must have selected a tenant
if (user?.isInleed && !currentTenant) {
navigate("/tenants/selection");
}
}, [user, currentTenant, navigate]);

return <>{children}</>;
}

// Usage
<Route path="/dashboard" element={
<TenantGuard>
<Dashboard />
</TenantGuard>
} />

API Call Patterns

For Inleed Users

When making API calls, Inleed users need to specify the tenant context:

// Option 1: Via query parameter (recommended)
const response = await fetch(
`/api/products?tenantId=${currentTenant._id}`,
{
headers: getAuthHeaders(),
}
);

// Option 2: Via header
const response = await fetch("/api/products", {
headers: {
...getAuthHeaders(),
"X-Tenant-Id": currentTenant._id,
},
});

For Client Users

Client users don't need to specify tenantId - it's automatically set by the middleware:

const response = await fetch("/api/products", {
headers: getAuthHeaders(),
});

Updating Base API Class

Update the BaseApi class to automatically include tenant context:

// In base.api.ts
import { useTenant } from "~/context/tenant.context";

class BaseApi<T> {
protected getHeaders() {
const headers: HeadersInit = {
"Content-Type": "application/json",
Authorization: `Bearer ${getToken()}`,
};

// Add tenant header if available
const currentTenant = localStorage.getItem("baldr_selected_tenant");
if (currentTenant) {
headers["X-Tenant-Id"] = currentTenant;
}

return headers;
}
}

UI Components

Tenant Indicator

Create a component to show current tenant in the header:

function TenantIndicator() {
const { user } = useUser();
const { currentTenant, clearTenant } = useTenant();
const navigate = useNavigate();

if (!user?.isInleed || !currentTenant) {
return null;
}

const handleChangeTenant = () => {
clearTenant();
navigate("/tenants/selection");
};

return (
<div className="flex items-center gap-2 p-2 bg-blue-50 rounded">
<i className="pi pi-building" />
<span className="font-semibold">{currentTenant.name}</span>
<Button
icon="pi pi-sync"
size="small"
text
onClick={handleChangeTenant}
tooltip="Switch tenant"
/>
</div>
);
}

Client User Dashboard

For client users, optionally show their tenant info:

function ClientDashboard() {
const { user } = useUser();
const [tenant, setTenant] = useState<ITenant | null>(null);

useEffect(() => {
if (user?.tenantId) {
TenantApi.getById(user.tenantId).then(setTenant);
}
}, [user]);

return (
<div>
<h1>Welcome to {tenant?.name}</h1>
{/* Dashboard content */}
</div>
);
}

Testing Multi-Tenant UX

Test Scenario 1: Inleed User Login

  1. Login with Inleed credentials
  2. Should redirect to /tenants/selection
  3. Should see paginated list of tenants
  4. Select a tenant
  5. Should redirect to /dashboard
  6. Should see tenant indicator in header
  7. All data should be scoped to selected tenant
  8. Click "Switch Tenant" → back to selection page

Test Scenario 2: Client User Login

  1. Login with client credentials
  2. Should redirect directly to /dashboard
  3. Should NOT see tenant selection page
  4. Should NOT see tenant switching option
  5. All data automatically scoped to their tenant
  6. Cannot access /tenants/selection (auto-redirect)

Test Scenario 3: Tenant Persistence

  1. Login as Inleed user
  2. Select a tenant
  3. Refresh the page
  4. Should remain in the same tenant context
  5. Logout and login again
  6. Should still have the same tenant selected

Migration Checklist for Existing Components

For each existing component that fetches data:

  • Check if user is Inleed
  • If Inleed, ensure tenant is selected before fetching
  • Include tenantId in API calls for Inleed users
  • Handle "no tenant selected" state gracefully
  • Update TypeScript interfaces if needed

Example Migration:

// Before
function ProductList() {
const [products, setProducts] = useState([]);

useEffect(() => {
ProductApi.getAll().then(setProducts);
}, []);

return <div>{/* ... */}</div>;
}

// After
function ProductList() {
const { user } = useUser();
const { currentTenant } = useTenant();
const [products, setProducts] = useState([]);

useEffect(() => {
// For Inleed users, ensure tenant is selected
if (user?.isInleed && !currentTenant) {
return; // Wait for tenant selection
}

ProductApi.getAll().then(setProducts);
}, [user, currentTenant]);

// Show message if Inleed user hasn't selected tenant
if (user?.isInleed && !currentTenant) {
return <div>Please select a tenant first</div>;
}

return <div>{/* ... */}</div>;
}

Security Considerations

Frontend Checks

  1. Route Protection: Ensure tenant selection page is only accessible by Inleed users
  2. UI Restrictions: Hide tenant switching UI for client users
  3. Data Display: Validate user permissions before showing sensitive data

Remember

⚠️ Frontend security is NOT sufficient. All security enforcement happens on the backend via middleware. Frontend checks are only for UX purposes.

Performance Optimization

Tenant Caching

// Cache tenant list for Inleed users
const [tenantCache, setTenantCache] = useState<Map<string, ITenant>>(new Map());

const getCachedTenant = async (id: string) => {
if (tenantCache.has(id)) {
return tenantCache.get(id)!;
}

const tenant = await TenantApi.getById(id);
setTenantCache(prev => new Map(prev).set(id, tenant));
return tenant;
};

Lazy Loading

Load tenant selection page only when needed:

const TenantSelectionPage = lazy(() => 
import("~/pages/tenants/tenantSelection.page")
);

Troubleshooting

Issue: Inleed user stuck in redirect loop

Solution: Clear localStorage and check authentication token

localStorage.removeItem("baldr_selected_tenant");

Issue: Client user can access tenant selection

Solution: Add route guard to check user.isInleed

Issue: API returns 403 for tenant data

Solution: Ensure X-Tenant-Id header is included in requests

Next Steps

  1. ✅ Create tenant context
  2. ✅ Build tenant selection page
  3. ✅ Update authentication flow
  4. ⚠️ Add tenant indicator to header
  5. ⚠️ Update all existing pages to handle tenant context
  6. ⚠️ Add route guards for tenant-specific pages
  7. ⚠️ Test both user flows thoroughly
  8. ⚠️ Add analytics for tenant usage

Additional Resources

  • API Documentation: /BaldrTs/documents/MULTI_TENANT_MIGRATION.md
  • User Context: /Baldr-Bo/app/context/user.context.tsx
  • Tenant Context: /Baldr-Bo/app/context/tenant.context.tsx