-
Notifications
You must be signed in to change notification settings - Fork 19
Expand file tree
/
Copy pathTenantContext.tsx
More file actions
127 lines (113 loc) · 3.76 KB
/
TenantContext.tsx
File metadata and controls
127 lines (113 loc) · 3.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/**
* TenantContext - React context for multi-tenancy support.
*
* The tenant ID can come from multiple sources (in order of precedence):
* 1. URL path parameter (/:tenantId/*) - for path-based routing
* 2. Explicitly passed tenantId prop - for components that know the tenant
* 3. SessionStorage - cached from previous login/registration
*
* Usage:
* // In App.tsx, wrap tenant-scoped routes:
* <Route path="/:tenantId/*" element={<TenantProvider><TenantRoutes /></TenantProvider>} />
*
* // In components:
* const { tenantId } = useTenant();
* api.signupWebauthn(name, keystore, ..., tenantId);
*
* See go-wallet-backend/docs/adr/011-multi-tenancy.md for full design.
*/
import React, { createContext, useContext, useEffect, useMemo, ReactNode } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { getStoredTenant, setStoredTenant, clearStoredTenant } from './tenant';
export interface TenantContextValue {
/** Current tenant ID (from URL, prop, or storage) */
tenantId: string | undefined;
/** Whether we're in a tenant-scoped context */
isMultiTenant: boolean;
/** Switch to a different tenant (navigates to new tenant's home) */
switchTenant: (newTenantId: string) => void;
/** Clear tenant context (on logout) */
clearTenant: () => void;
}
const TenantContext = createContext<TenantContextValue | null>(null);
interface TenantProviderProps {
children: ReactNode;
/** Optional explicit tenant ID (overrides URL parsing) */
tenantId?: string;
}
/**
* TenantProvider extracts tenant from URL path and provides it to children.
*
* For path-based routing, the URL structure is:
* /{tenantId}/settings
* /{tenantId}/add
* /{tenantId}/cb?code=...
*
* The provider:
* 1. Reads tenantId from URL params (useParams)
* 2. Falls back to sessionStorage if not in URL
* 3. Stores tenant in sessionStorage when found in URL
*/
export function TenantProvider({ children, tenantId: propTenantId }: TenantProviderProps) {
const navigate = useNavigate();
// Get tenant from URL path parameter
// This requires the route to be defined as /:tenantId/*
const { tenantId: urlTenantId } = useParams<{ tenantId: string }>();
// Determine effective tenant ID (prop > URL > storage)
const effectiveTenantId = propTenantId || urlTenantId || getStoredTenant();
// Sync URL tenant to storage when available
useEffect(() => {
if (urlTenantId) {
setStoredTenant(urlTenantId);
}
}, [urlTenantId]);
const switchTenant = (newTenantId: string) => {
setStoredTenant(newTenantId);
navigate(`/${newTenantId}/`);
};
const clearTenant = () => {
clearStoredTenant();
};
const value = useMemo<TenantContextValue>(() => ({
tenantId: effectiveTenantId,
isMultiTenant: !!effectiveTenantId,
switchTenant,
clearTenant,
}), [effectiveTenantId]);
return (
<TenantContext.Provider value={value}>
{children}
</TenantContext.Provider>
);
}
/**
* Hook to access tenant context.
* Must be used within a TenantProvider.
*/
export function useTenant(): TenantContextValue {
const context = useContext(TenantContext);
if (!context) {
// Return a default context for components outside TenantProvider
// This allows the app to work in single-tenant mode
return {
tenantId: getStoredTenant(),
isMultiTenant: false,
switchTenant: () => {
console.warn('switchTenant called outside TenantProvider');
},
clearTenant: clearStoredTenant,
};
}
return context;
}
/**
* Hook to get tenant ID, throwing if not available.
* Use this when tenant is required (e.g., in tenant-scoped routes).
*/
export function useRequiredTenant(): string {
const { tenantId } = useTenant();
if (!tenantId) {
throw new Error('Tenant ID is required but not available. Ensure this component is within a tenant-scoped route.');
}
return tenantId;
}