diff --git a/api/users-admin.js b/api/users-admin.js index 3714e47..4572393 100644 --- a/api/users-admin.js +++ b/api/users-admin.js @@ -9,6 +9,75 @@ const MASTER_USERS = new Set([ 'jingying.shi@craze-group.com', ]); +function getValidatedFromMetadata(user) { + return user?.app_metadata?.validated === true || user?.user_metadata?.validated === true; +} + +async function fetchAdminUser(userId) { + const res = await fetch(`${SUPABASE_URL}/auth/v1/admin/users/${userId}`, { + headers: { + 'apikey': SUPABASE_SERVICE_KEY, + 'Authorization': `Bearer ${SUPABASE_SERVICE_KEY}` + } + }); + + if (!res.ok) { + const errText = await res.text(); + throw new Error(`Failed to fetch auth user: ${errText}`); + } + + const payload = await res.json(); + return payload?.user || payload; +} + +async function updateAuthValidationMetadata(userId, validated) { + const currentUser = await fetchAdminUser(userId); + const res = await fetch(`${SUPABASE_URL}/auth/v1/admin/users/${userId}`, { + method: 'PUT', + headers: { + 'apikey': SUPABASE_SERVICE_KEY, + 'Authorization': `Bearer ${SUPABASE_SERVICE_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + app_metadata: { + ...(currentUser?.app_metadata || {}), + validated, + }, + user_metadata: currentUser?.user_metadata || {}, + }) + }); + + if (!res.ok) { + const errText = await res.text(); + throw new Error(`Failed to update auth metadata: ${errText}`); + } +} + +async function fetchApprovalsMap() { + const approvalsRes = await fetch(`${SUPABASE_URL}/rest/v1/user_approvals?select=*`, { + headers: { + 'apikey': SUPABASE_SERVICE_KEY, + 'Authorization': `Bearer ${SUPABASE_SERVICE_KEY}` + } + }); + + if (!approvalsRes.ok) { + const errText = await approvalsRes.text(); + console.error('Failed to fetch approvals:', errText); + return { + approvalMap: null, + warning: 'Validation table unavailable; using Auth metadata fallback.' + }; + } + + const approvals = await approvalsRes.json(); + return { + approvalMap: new Map(approvals.map(a => [a.id, a.validated])), + warning: null, + }; +} + function setCors(req, res) { applyCors(req, res, 'POST, OPTIONS'); } @@ -79,11 +148,13 @@ export default async function handler(req, res) { if (!approvalsRes.ok) { const errText = await approvalsRes.text(); console.error('Failed to query user approvals:', errText); - return res.status(500).json({ error: 'Failed to query database' }); + return res.json({ validated: getValidatedFromMetadata(user) }); } const approvals = await approvalsRes.json(); - const isApproved = approvals.length > 0 && approvals[0].validated === true; + const isApproved = approvals.length > 0 + ? approvals[0].validated === true + : getValidatedFromMetadata(user); return res.json({ validated: isApproved }); } @@ -110,22 +181,7 @@ export default async function handler(req, res) { const usersData = await usersRes.json(); const authUsers = usersData.users || []; - // Fetch validation mappings from public.user_approvals - const approvalsRes = await fetch(`${SUPABASE_URL}/rest/v1/user_approvals?select=*`, { - headers: { - 'apikey': SUPABASE_SERVICE_KEY, - 'Authorization': `Bearer ${SUPABASE_SERVICE_KEY}` - } - }); - - if (!approvalsRes.ok) { - const errText = await approvalsRes.text(); - console.error('Failed to fetch approvals:', errText); - return res.status(500).json({ error: 'Failed to fetch user approvals' }); - } - - const approvals = await approvalsRes.json(); - const approvalMap = new Map(approvals.map(a => [a.id, a.validated])); + const { approvalMap, warning } = await fetchApprovalsMap(); const mergedUsers = authUsers.map(u => { const email = u.email; @@ -135,14 +191,16 @@ export default async function handler(req, res) { let status = 'Pending'; if (MASTER_USERS.has(email?.toLowerCase())) { status = 'Master'; - } else if (approvalMap.has(id)) { + } else if (approvalMap?.has(id)) { status = approvalMap.get(id) ? 'Validated' : 'Pending'; + } else if (getValidatedFromMetadata(u)) { + status = 'Validated'; } return { id, email, created_at: createdAt, status }; }); - return res.json({ users: mergedUsers }); + return res.json({ users: mergedUsers, warning }); } if (action === 'validate') { @@ -151,30 +209,37 @@ export default async function handler(req, res) { return res.status(400).json({ error: 'Missing targetUserId or email' }); } - // Upsert into user_approvals table - const upsertRes = await fetch(`${SUPABASE_URL}/rest/v1/user_approvals`, { - method: 'POST', - headers: { - 'apikey': SUPABASE_SERVICE_KEY, - 'Authorization': `Bearer ${SUPABASE_SERVICE_KEY}`, - 'Content-Type': 'application/json', - 'Prefer': 'resolution=merge-duplicates,return=representation' - }, - body: JSON.stringify({ - id: targetUserId, - email, - validated, - created_at: new Date().toISOString() - }) - }); + let tableWarning = null; - if (!upsertRes.ok) { - const errText = await upsertRes.text(); - console.error('Failed to upsert approval:', errText); - return res.status(500).json({ error: 'Failed to update approval status' }); + try { + const upsertRes = await fetch(`${SUPABASE_URL}/rest/v1/user_approvals`, { + method: 'POST', + headers: { + 'apikey': SUPABASE_SERVICE_KEY, + 'Authorization': `Bearer ${SUPABASE_SERVICE_KEY}`, + 'Content-Type': 'application/json', + 'Prefer': 'resolution=merge-duplicates,return=representation' + }, + body: JSON.stringify({ + id: targetUserId, + email, + validated, + created_at: new Date().toISOString() + }) + }); + + if (!upsertRes.ok) { + const errText = await upsertRes.text(); + console.error('Failed to upsert approval:', errText); + tableWarning = 'Validation table unavailable; Auth metadata was updated instead.'; + } + } catch (err) { + console.error('Approval table update threw:', err); + tableWarning = 'Validation table unavailable; Auth metadata was updated instead.'; } - return res.json({ success: true }); + await updateAuthValidationMetadata(targetUserId, validated); + return res.json({ success: true, warning: tableWarning }); } if (action === 'delete') { @@ -184,13 +249,8 @@ export default async function handler(req, res) { } // Prevent master user self-deletion via API - const { data: targetUserRes } = await fetch(`${SUPABASE_URL}/auth/v1/admin/users/${targetUserId}`, { - headers: { - 'apikey': SUPABASE_SERVICE_KEY, - 'Authorization': `Bearer ${SUPABASE_SERVICE_KEY}` - } - }).then(r => r.json().catch(() => ({}))); - + const targetUserRes = await fetchAdminUser(targetUserId).catch(() => null); + if (targetUserRes && MASTER_USERS.has(targetUserRes.email?.toLowerCase())) { return res.status(400).json({ error: 'Cannot delete a master user account' }); } diff --git a/src/components/UserManagementView.tsx b/src/components/UserManagementView.tsx index 27f07ce..1404783 100644 --- a/src/components/UserManagementView.tsx +++ b/src/components/UserManagementView.tsx @@ -17,6 +17,7 @@ export function UserManagementView({ session }: UserManagementViewProps) { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [warning, setWarning] = useState(null); const [search, setSearch] = useState(''); const [processingId, setProcessingId] = useState(null); const [deleteConfirmUser, setDeleteConfirmUser] = useState(null); @@ -24,6 +25,7 @@ export function UserManagementView({ session }: UserManagementViewProps) { const fetchUsers = async () => { setLoading(true); setError(null); + setWarning(null); try { const res = await fetch('/api/users-admin', { method: 'POST', @@ -41,6 +43,7 @@ export function UserManagementView({ session }: UserManagementViewProps) { const data = await res.json(); setUsers(data.users || []); + setWarning(data.warning || null); } catch (err: any) { console.error('[UserManagement] fetch error:', err); setError(err.message || 'Error fetching user list.'); @@ -56,6 +59,7 @@ export function UserManagementView({ session }: UserManagementViewProps) { const handleToggleValidation = async (user: User, approve: boolean) => { setProcessingId(user.id); setError(null); + setWarning(null); try { const res = await fetch('/api/users-admin', { method: 'POST', @@ -76,6 +80,8 @@ export function UserManagementView({ session }: UserManagementViewProps) { throw new Error(data.error || 'Failed to update user approval status.'); } + const data = await res.json().catch(() => ({})); + // Update local state setUsers(prev => prev.map(u => { if (u.id === user.id) { @@ -83,6 +89,9 @@ export function UserManagementView({ session }: UserManagementViewProps) { } return u; })); + if (data.warning) { + setWarning(data.warning); + } } catch (err: any) { setError(err.message || 'Error updating approval status.'); } finally { @@ -183,6 +192,21 @@ export function UserManagementView({ session }: UserManagementViewProps) { )} + {warning && ( +
+ +
+ Aviso: {warning} +
+ +
+ )} + {loading ? (