1import React, { useState, useEffect } from 'react';
2import { apiFetch } from '../lib/api';
5 * TenantSelector - A reusable component for root users to select which tenant they're acting on behalf of
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)
13export default function TenantSelector({ selectedTenantId, onTenantChange, style = {} }) {
14 const [tenants, setTenants] = useState([]);
18 // eslint-disable-next-line
21 async function fetchTenants() {
23 const token = localStorage.getItem('token');
24 const res = await apiFetch('/tenants', {
26 'Authorization': `Bearer ${token}`,
27 'X-Tenant-Subdomain': 'admin'
31 const data = await res.json();
32 setTenants(Array.isArray(data) ? data : (data.tenants || []));
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));
47 // Detect impersonation state
48 const token = localStorage.getItem('token');
49 let isImpersonated = false;
50 let impersonatedTenantName = '';
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 || '';
62 className="form-section"
64 marginBottom: '1.5rem',
65 backgroundColor: '#f8f9fa',
68 border: '2px solid #dee2e6',
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>
77 <div style={{ flex: '1', minWidth: '250px', maxWidth: '400px' }}>
81 style={{ minWidth: '180px', padding: '0.5rem', borderRadius: '4px', border: '1px solid #ced4da', textAlign: 'center' }}
82 onClick={async () => {
84 // Exit impersonation: call dedicated backend route
85 const token = localStorage.getItem('token');
86 const res = await apiFetch(`/auth/exit-impersonation`, {
89 'Authorization': `Bearer ${token}`,
90 'Content-Type': 'application/json'
95 const data = await res.json();
97 localStorage.setItem('token', data.token);
98 window.location.reload();
100 console.error('No token in exit-impersonation response');
101 window.location.reload();
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));
109 console.error('Error exiting impersonation:', err);
110 alert('Error exiting tenant: ' + err.message);
120 value={selectedTenantId || ''}
121 onChange={async (e) => {
122 const tid = e.target.value;
123 if (!tid) return; // Don't process empty selection
126 // Always request new JWT for impersonation
127 const token = localStorage.getItem('token');
128 const res = await apiFetch(`/auth/impersonate`, {
131 'Authorization': `Bearer ${token}`,
132 'Content-Type': 'application/json'
134 body: JSON.stringify({ tenant_id: tid })
138 const data = await res.json();
140 localStorage.setItem('token', data.token);
142 window.location.reload();
144 console.error('No token in impersonate response');
145 alert('Failed to impersonate tenant: No token received');
148 const errorText = await res.text();
149 console.error('Failed to impersonate:', res.status, errorText);
150 alert('Failed to impersonate tenant: ' + (errorText || res.statusText));
153 console.error('Error impersonating tenant:', err);
154 alert('Error impersonating tenant: ' + err.message);
157 style={{ width: '100%', padding: '0.5rem', borderRadius: '4px', border: '1px solid #ced4da' }}
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})
168 <small style={{ color: '#6c757d', flex: '1 1 100%', marginTop: '0.25rem' }}>
169 Acting on behalf of: <strong>{
171 ? (impersonatedTenantName || (selectedTenant && selectedTenant.name) || `Tenant #${selectedTenantId}`)
172 : selectedTenant && selectedTenant.name
173 ? selectedTenant.name
174 : 'Root Tenant (Admin)'