mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 13:45:24 +02:00
fix user validation fallback
This commit is contained in:
+107
-47
@@ -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,12 +249,7 @@ 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' });
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
Reference in New Issue
Block a user