feat: implement manual user validation and user deletion flow

This commit is contained in:
Christian Vidal Wolf
2026-05-20 13:33:37 +02:00
parent 1d105e19ae
commit 47b9303202
22 changed files with 2613 additions and 93 deletions
+231
View File
@@ -0,0 +1,231 @@
const SUPABASE_URL = process.env.SUPABASE_URL || 'https://hwithddwaapyhnfwcesj.supabase.co';
const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_KEY;
const SUPABASE_ANON_KEY = 'sb_publishable_fGXkh0bSrAOqSk2jWKAzSg_NJD9YPCv';
const ALLOWED_ORIGIN = 'https://craze-data-check.vercel.app';
const MASTER_USERS = new Set([
'christian.vidal@craze-group.com',
'jingying.shi@craze-group.com',
]);
function setCors(req, res) {
const origin = req.headers.origin;
if (origin === ALLOWED_ORIGIN || (origin && (origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:')))) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, apikey');
res.setHeader('Access-Control-Max-Age', '86400');
res.setHeader('Vary', 'Origin');
}
export default async function handler(req, res) {
setCors(req, res);
if (req.method === 'OPTIONS') {
return res.status(204).end();
}
try {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
// 1. Authenticate caller
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Unauthorized: Missing token' });
}
const token = authHeader.split(' ')[1];
const userRes = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
headers: {
'apikey': SUPABASE_ANON_KEY,
'Authorization': `Bearer ${token}`
}
});
if (!userRes.ok) {
return res.status(401).json({ error: 'Unauthorized: Invalid token' });
}
const user = await userRes.json();
const callerEmail = user.email;
const callerId = user.id;
if (!callerEmail) {
return res.status(401).json({ error: 'Unauthorized: Invalid user payload' });
}
const isMaster = MASTER_USERS.has(callerEmail.toLowerCase());
const { action } = req.body;
if (!action) {
return res.status(400).json({ error: 'Missing action' });
}
// --- Action: Check Validation Status ---
if (action === 'check-status') {
if (isMaster) {
return res.json({ validated: true });
}
const approvalsRes = await fetch(`${SUPABASE_URL}/rest/v1/user_approvals?id=eq.${callerId}&select=validated`, {
headers: {
'apikey': SUPABASE_SERVICE_KEY,
'Authorization': `Bearer ${SUPABASE_SERVICE_KEY}`
}
});
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' });
}
const approvals = await approvalsRes.json();
const isApproved = approvals.length > 0 && approvals[0].validated === true;
return res.json({ validated: isApproved });
}
// --- Admin-only Actions ---
if (!isMaster) {
return res.status(403).json({ error: 'Forbidden: Admin access required' });
}
if (action === 'list') {
// Fetch all users from GoTrue Admin API
const usersRes = await fetch(`${SUPABASE_URL}/auth/v1/admin/users`, {
headers: {
'apikey': SUPABASE_SERVICE_KEY,
'Authorization': `Bearer ${SUPABASE_SERVICE_KEY}`
}
});
if (!usersRes.ok) {
const errText = await usersRes.text();
console.error('Failed to fetch auth users:', errText);
return res.status(500).json({ error: 'Failed to fetch users from authentication' });
}
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 mergedUsers = authUsers.map(u => {
const email = u.email;
const id = u.id;
const createdAt = u.created_at;
let status = 'Pending';
if (MASTER_USERS.has(email?.toLowerCase())) {
status = 'Master';
} else if (approvalMap.has(id)) {
status = approvalMap.get(id) ? 'Validated' : 'Pending';
}
return { id, email, created_at: createdAt, status };
});
return res.json({ users: mergedUsers });
}
if (action === 'validate') {
const { targetUserId, email, validated } = req.body;
if (!targetUserId || !email) {
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()
})
});
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' });
}
return res.json({ success: true });
}
if (action === 'delete') {
const { targetUserId } = req.body;
if (!targetUserId) {
return res.status(400).json({ error: 'Missing targetUserId' });
}
// 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(() => ({})));
if (targetUserRes && MASTER_USERS.has(targetUserRes.email?.toLowerCase())) {
return res.status(400).json({ error: 'Cannot delete a master user account' });
}
// 1. Delete user from auth
const deleteAuthRes = await fetch(`${SUPABASE_URL}/auth/v1/admin/users/${targetUserId}`, {
method: 'DELETE',
headers: {
'apikey': SUPABASE_SERVICE_KEY,
'Authorization': `Bearer ${SUPABASE_SERVICE_KEY}`
}
});
if (!deleteAuthRes.ok) {
const errText = await deleteAuthRes.text();
console.error('Failed to delete auth user:', errText);
return res.status(500).json({ error: 'Failed to delete user from authentication' });
}
// 2. Delete user approval record from public.user_approvals if exists
await fetch(`${SUPABASE_URL}/rest/v1/user_approvals?id=eq.${targetUserId}`, {
method: 'DELETE',
headers: {
'apikey': SUPABASE_SERVICE_KEY,
'Authorization': `Bearer ${SUPABASE_SERVICE_KEY}`
}
});
return res.json({ success: true });
}
return res.status(400).json({ error: 'Invalid action' });
} catch (err) {
console.error('Error in users-admin function:', err);
return res.status(500).json({ error: err.message || 'Internal server error' });
}
}