fix user validation fallback

This commit is contained in:
Christian Vidal Wolf
2026-05-20 14:04:02 +02:00
parent 396ff23d49
commit d17db2761b
2 changed files with 132 additions and 48 deletions
+108 -48
View File
@@ -9,6 +9,75 @@ const MASTER_USERS = new Set([
'jingying.shi@craze-group.com', '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) { function setCors(req, res) {
applyCors(req, res, 'POST, OPTIONS'); applyCors(req, res, 'POST, OPTIONS');
} }
@@ -79,11 +148,13 @@ export default async function handler(req, res) {
if (!approvalsRes.ok) { if (!approvalsRes.ok) {
const errText = await approvalsRes.text(); const errText = await approvalsRes.text();
console.error('Failed to query user approvals:', errText); 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 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 }); return res.json({ validated: isApproved });
} }
@@ -110,22 +181,7 @@ export default async function handler(req, res) {
const usersData = await usersRes.json(); const usersData = await usersRes.json();
const authUsers = usersData.users || []; const authUsers = usersData.users || [];
// Fetch validation mappings from public.user_approvals const { approvalMap, warning } = await 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 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 mergedUsers = authUsers.map(u => { const mergedUsers = authUsers.map(u => {
const email = u.email; const email = u.email;
@@ -135,14 +191,16 @@ export default async function handler(req, res) {
let status = 'Pending'; let status = 'Pending';
if (MASTER_USERS.has(email?.toLowerCase())) { if (MASTER_USERS.has(email?.toLowerCase())) {
status = 'Master'; status = 'Master';
} else if (approvalMap.has(id)) { } else if (approvalMap?.has(id)) {
status = approvalMap.get(id) ? 'Validated' : 'Pending'; status = approvalMap.get(id) ? 'Validated' : 'Pending';
} else if (getValidatedFromMetadata(u)) {
status = 'Validated';
} }
return { id, email, created_at: createdAt, status }; return { id, email, created_at: createdAt, status };
}); });
return res.json({ users: mergedUsers }); return res.json({ users: mergedUsers, warning });
} }
if (action === 'validate') { if (action === 'validate') {
@@ -151,30 +209,37 @@ export default async function handler(req, res) {
return res.status(400).json({ error: 'Missing targetUserId or email' }); return res.status(400).json({ error: 'Missing targetUserId or email' });
} }
// Upsert into user_approvals table let tableWarning = null;
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) { try {
const errText = await upsertRes.text(); const upsertRes = await fetch(`${SUPABASE_URL}/rest/v1/user_approvals`, {
console.error('Failed to upsert approval:', errText); method: 'POST',
return res.status(500).json({ error: 'Failed to update approval status' }); 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') { if (action === 'delete') {
@@ -184,13 +249,8 @@ export default async function handler(req, res) {
} }
// Prevent master user self-deletion via API // Prevent master user self-deletion via API
const { data: targetUserRes } = await fetch(`${SUPABASE_URL}/auth/v1/admin/users/${targetUserId}`, { const targetUserRes = await fetchAdminUser(targetUserId).catch(() => null);
headers: {
'apikey': SUPABASE_SERVICE_KEY,
'Authorization': `Bearer ${SUPABASE_SERVICE_KEY}`
}
}).then(r => r.json().catch(() => ({})));
if (targetUserRes && MASTER_USERS.has(targetUserRes.email?.toLowerCase())) { if (targetUserRes && MASTER_USERS.has(targetUserRes.email?.toLowerCase())) {
return res.status(400).json({ error: 'Cannot delete a master user account' }); return res.status(400).json({ error: 'Cannot delete a master user account' });
} }
+24
View File
@@ -17,6 +17,7 @@ export function UserManagementView({ session }: UserManagementViewProps) {
const [users, setUsers] = useState<User[]>([]); const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [warning, setWarning] = useState<string | null>(null);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [processingId, setProcessingId] = useState<string | null>(null); const [processingId, setProcessingId] = useState<string | null>(null);
const [deleteConfirmUser, setDeleteConfirmUser] = useState<User | null>(null); const [deleteConfirmUser, setDeleteConfirmUser] = useState<User | null>(null);
@@ -24,6 +25,7 @@ export function UserManagementView({ session }: UserManagementViewProps) {
const fetchUsers = async () => { const fetchUsers = async () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
setWarning(null);
try { try {
const res = await fetch('/api/users-admin', { const res = await fetch('/api/users-admin', {
method: 'POST', method: 'POST',
@@ -41,6 +43,7 @@ export function UserManagementView({ session }: UserManagementViewProps) {
const data = await res.json(); const data = await res.json();
setUsers(data.users || []); setUsers(data.users || []);
setWarning(data.warning || null);
} catch (err: any) { } catch (err: any) {
console.error('[UserManagement] fetch error:', err); console.error('[UserManagement] fetch error:', err);
setError(err.message || 'Error fetching user list.'); setError(err.message || 'Error fetching user list.');
@@ -56,6 +59,7 @@ export function UserManagementView({ session }: UserManagementViewProps) {
const handleToggleValidation = async (user: User, approve: boolean) => { const handleToggleValidation = async (user: User, approve: boolean) => {
setProcessingId(user.id); setProcessingId(user.id);
setError(null); setError(null);
setWarning(null);
try { try {
const res = await fetch('/api/users-admin', { const res = await fetch('/api/users-admin', {
method: 'POST', method: 'POST',
@@ -76,6 +80,8 @@ export function UserManagementView({ session }: UserManagementViewProps) {
throw new Error(data.error || 'Failed to update user approval status.'); throw new Error(data.error || 'Failed to update user approval status.');
} }
const data = await res.json().catch(() => ({}));
// Update local state // Update local state
setUsers(prev => prev.map(u => { setUsers(prev => prev.map(u => {
if (u.id === user.id) { if (u.id === user.id) {
@@ -83,6 +89,9 @@ export function UserManagementView({ session }: UserManagementViewProps) {
} }
return u; return u;
})); }));
if (data.warning) {
setWarning(data.warning);
}
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Error updating approval status.'); setError(err.message || 'Error updating approval status.');
} finally { } finally {
@@ -183,6 +192,21 @@ export function UserManagementView({ session }: UserManagementViewProps) {
</div> </div>
)} )}
{warning && (
<div className="mb-6 p-4 bg-amber-950/30 border border-amber-500/30 rounded-lg flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-amber-400 shrink-0 mt-0.5" />
<div className="flex-1 text-sm text-amber-200">
<span className="font-semibold">Aviso:</span> {warning}
</div>
<button
onClick={() => setWarning(null)}
className="text-amber-300 hover:text-amber-200 text-xs font-semibold px-2 py-1 rounded"
>
Descartar
</button>
</div>
)}
{loading ? ( {loading ? (
<div className="h-64 flex flex-col items-center justify-center text-slate-400"> <div className="h-64 flex flex-col items-center justify-center text-slate-400">
<Loader2 className="w-8 h-8 animate-spin text-blue-500 mb-3" /> <Loader2 className="w-8 h-8 animate-spin text-blue-500 mb-3" />