EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
TenantSelector.jsx
Go to the documentation of this file.
1import React, { useState, useEffect } from 'react';
2import { apiFetch } from '../lib/api';
3
4/**
5 * TenantSelector - A reusable component for root users to select which tenant they're acting on behalf of
6 *
7 * @param {string} selectedTenantId - The currently selected tenant ID
8 * @param {function} onTenantChange - Callback when tenant selection changes
9 * @param {object} style - Optional additional styles for the container
10 * @param {boolean} isRoot - Whether the user is root (passed from parent)
11 */
12
13export default function TenantSelector({ selectedTenantId, onTenantChange, style = {} }) {
14 const [tenants, setTenants] = useState([]);
15
16 useEffect(() => {
17 fetchTenants();
18 // eslint-disable-next-line
19 }, []);
20
21 async function fetchTenants() {
22 try {
23 const token = localStorage.getItem('token');
24 const res = await apiFetch('/tenants', {
25 headers: {
26 'Authorization': `Bearer ${token}`,
27 'X-Tenant-Subdomain': 'admin'
28 },
29 });
30 if (res.ok) {
31 const data = await res.json();
32 setTenants(Array.isArray(data) ? data : (data.tenants || []));
33 }
34 } catch (err) {
35 setTenants([]);
36 }
37 }
38
39 // Only allow tenants with valid UUIDs
40 const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
41 const validTenants = tenants.filter(t => uuidRegex.test(String(t.tenant_id)));
42 let selectedTenant = null;
43 if (selectedTenantId) {
44 selectedTenant = validTenants.find(t => String(t.tenant_id) === String(selectedTenantId));
45 }
46
47 // Detect impersonation state
48 const token = localStorage.getItem('token');
49 let isImpersonated = false;
50 let impersonatedTenantName = '';
51 if (token) {
52 try {
53 const payload = token.split('.')[1];
54 const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
55 isImpersonated = !!decoded.impersonated;
56 impersonatedTenantName = decoded.impersonated_tenant_name || '';
57 } catch {}
58 }
59
60 return (
61 <div
62 className="form-section"
63 style={{
64 marginBottom: '1.5rem',
65 backgroundColor: '#f8f9fa',
66 padding: '1rem',
67 borderRadius: '8px',
68 border: '2px solid #dee2e6',
69 ...style
70 }}
71 >
72 <div style={{ display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
73 <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', color: '#6c757d', fontWeight: 500 }}>
74 <span className="material-symbols-outlined" style={{ fontSize: '20px' }}>admin_panel_settings</span>
75 <span>Root Tenant Control:</span>
76 </div>
77 <div style={{ flex: '1', minWidth: '250px', maxWidth: '400px' }}>
78 {isImpersonated ? (
79 <button
80 className="btn"
81 style={{ minWidth: '180px', padding: '0.5rem', borderRadius: '4px', border: '1px solid #ced4da', textAlign: 'center' }}
82 onClick={async () => {
83 try {
84 // Exit impersonation: call dedicated backend route
85 const token = localStorage.getItem('token');
86 const res = await apiFetch(`/auth/exit-impersonation`, {
87 method: 'POST',
88 headers: {
89 'Authorization': `Bearer ${token}`,
90 'Content-Type': 'application/json'
91 }
92 });
93
94 if (res.ok) {
95 const data = await res.json();
96 if (data.token) {
97 localStorage.setItem('token', data.token);
98 window.location.reload();
99 } else {
100 console.error('No token in exit-impersonation response');
101 window.location.reload();
102 }
103 } else {
104 const errorText = await res.text();
105 console.error('Failed to exit impersonation:', res.status, errorText);
106 alert('Failed to exit tenant: ' + (errorText || res.statusText));
107 }
108 } catch (err) {
109 console.error('Error exiting impersonation:', err);
110 alert('Error exiting tenant: ' + err.message);
111 }
112 }
113 onTenantChange('');
114 }}
115 >
116 Exit Tenant
117 </button>
118 ) : (
119 <select
120 value={selectedTenantId || ''}
121 onChange={async (e) => {
122 const tid = e.target.value;
123 if (!tid) return; // Don't process empty selection
124
125 try {
126 // Always request new JWT for impersonation
127 const token = localStorage.getItem('token');
128 const res = await apiFetch(`/auth/impersonate`, {
129 method: 'POST',
130 headers: {
131 'Authorization': `Bearer ${token}`,
132 'Content-Type': 'application/json'
133 },
134 body: JSON.stringify({ tenant_id: tid })
135 });
136
137 if (res.ok) {
138 const data = await res.json();
139 if (data.token) {
140 localStorage.setItem('token', data.token);
141 onTenantChange(tid);
142 window.location.reload();
143 } else {
144 console.error('No token in impersonate response');
145 alert('Failed to impersonate tenant: No token received');
146 }
147 } else {
148 const errorText = await res.text();
149 console.error('Failed to impersonate:', res.status, errorText);
150 alert('Failed to impersonate tenant: ' + (errorText || res.statusText));
151 }
152 } catch (err) {
153 console.error('Error impersonating tenant:', err);
154 alert('Error impersonating tenant: ' + err.message);
155 }
156 }}
157 style={{ width: '100%', padding: '0.5rem', borderRadius: '4px', border: '1px solid #ced4da' }}
158 >
159 <option value="">Root Tenant (Admin)</option>
160 {validTenants.map(t => (
161 <option key={t.tenant_id} value={t.tenant_id}>
162 {t.name} ({t.subdomain})
163 </option>
164 ))}
165 </select>
166 )}
167 </div>
168 <small style={{ color: '#6c757d', flex: '1 1 100%', marginTop: '0.25rem' }}>
169 Acting on behalf of: <strong>{
170 isImpersonated
171 ? (impersonatedTenantName || (selectedTenant && selectedTenant.name) || `Tenant #${selectedTenantId}`)
172 : selectedTenant && selectedTenant.name
173 ? selectedTenant.name
174 : 'Root Tenant (Admin)'
175 }</strong>
176 </small>
177 </div>
178 </div>
179 );
180}