Fix "View" showing the nxtgauge logo instead of the submitted document
All checks were successful
build-and-release / build (push) Successful in 1m1s

The verification queue read row.payload?.documents (never populated —
the real per-document array is a separate top-level `documents` field
that was dropped during row mapping) and expected a {title, url} shape
that doesn't match what the backend actually sends ({type, value,
status}). Both mismatches meant every submission fell through to the
hardcoded placeholder-logo stub. Capture v.documents into each row,
read the real fields, and resolve the stored URL through the presign
endpoint before displaying it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar Sivakumar 2026-07-27 23:02:01 +05:30
parent 69479291c9
commit cb99b90774

View file

@ -27,6 +27,7 @@ type VerificationRow = {
userId: string; userId: string;
roleKey: string; roleKey: string;
payload?: any; payload?: any;
documents?: any[];
}; };
type SubmittedDocument = { type SubmittedDocument = {
@ -97,6 +98,31 @@ const API = '';
const toTitle = (value: string) => String(value || '').replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); const toTitle = (value: string) => String(value || '').replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
// Submitted documents are stored as permanent Backblaze URLs. Never render one
// directly (img src / iframe src) — always exchange it for a short-lived signed
// URL through this endpoint first, on demand.
async function presignDocUrl(url: string): Promise<string> {
try {
const accessToken = typeof sessionStorage !== 'undefined'
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
: '';
const res = await fetch(`${API}/api/admin/verifications/presign`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
},
credentials: 'include',
body: JSON.stringify({ url }),
});
if (!res.ok) return url;
const data = await res.json().catch(() => ({}));
return data?.url ?? url;
} catch {
return url;
}
}
const statusUi = (status: VerificationStatus) => { const statusUi = (status: VerificationStatus) => {
if (status === 'APPROVED') return { bg: '#ECFDF3', border: '#BBF7D0', text: '#166534', label: 'Approved' }; if (status === 'APPROVED') return { bg: '#ECFDF3', border: '#BBF7D0', text: '#166534', label: 'Approved' };
if (status === 'UNDER_REVIEW') return { bg: '#EEF2FF', border: '#C7D2FE', text: '#3730A3', label: 'Under Review' }; if (status === 'UNDER_REVIEW') return { bg: '#EEF2FF', border: '#C7D2FE', text: '#3730A3', label: 'Under Review' };
@ -213,6 +239,7 @@ export default function VerificationManagementPage() {
userId: v.user_id, userId: v.user_id,
roleKey: v.role_key, roleKey: v.role_key,
payload, payload,
documents: Array.isArray(v.documents) ? v.documents : [],
}; };
}); });
setRows(mergedRows); setRows(mergedRows);
@ -354,19 +381,26 @@ export default function VerificationManagementPage() {
]; ];
} }
const roleSpecKey = normalizeRoleSpecKey(row.roleKey || row.userType); const roleSpecKey = normalizeRoleSpecKey(row.roleKey || row.userType);
const fromPayload = Array.isArray(row.payload?.documents) ? row.payload.documents : []; // Real submissions: verifications.documents, built server-side as
if (fromPayload.length) { // [{ type: "<field key>", value: "<stored file url>", status }] — not the
return fromPayload.slice(0, 8).map((doc: any, idx: number) => ({ // {title, url} shape this UI used to assume, which is why "View" always
id: String(doc.id || `doc-${idx + 1}`), // fell through to the ROLE_DOCUMENTS placeholder stub below.
title: String(doc.title || doc.name || `Document ${idx + 1}`), const fromDocuments = Array.isArray(row.documents) ? row.documents : [];
type: String(doc.type || '').toUpperCase().includes('PDF') ? 'PDF' : 'IMAGE', if (fromDocuments.length) {
url: String(doc.url || '/nxtgauge-logo.png'), return fromDocuments.slice(0, 8).map((doc: any, idx: number) => {
status: String(doc.status || '').toUpperCase() === 'MISSING' const value = String(doc.value ?? doc.url ?? '');
? 'MISSING' return {
: String(doc.status || '').toUpperCase() === 'INVALID' id: String(doc.id || doc.type || `doc-${idx + 1}`),
? 'INVALID' title: String(doc.title || toTitle(doc.type) || `Document ${idx + 1}`),
: 'SUBMITTED', type: /\.(jpg|jpeg|png|gif|webp)(\?|$)/i.test(value) ? 'IMAGE' : 'PDF',
})); url: value,
status: String(doc.status || '').toUpperCase() === 'MISSING'
? 'MISSING'
: String(doc.status || '').toUpperCase() === 'INVALID'
? 'INVALID'
: 'SUBMITTED',
};
});
} }
const docs = ROLE_DOCUMENTS[roleSpecKey] || ROLE_DOCUMENTS.PROFESSIONAL; const docs = ROLE_DOCUMENTS[roleSpecKey] || ROLE_DOCUMENTS.PROFESSIONAL;
return docs.map((title, idx) => ({ return docs.map((title, idx) => ({
@ -913,7 +947,10 @@ export default function VerificationManagementPage() {
<td style="padding:12px 16px"> <td style="padding:12px 16px">
<button <button
type="button" type="button"
onClick={() => setViewer({ open: true, title: doc.title, type: doc.type, url: doc.url })} onClick={async () => {
const signed = doc.url ? await presignDocUrl(doc.url) : doc.url;
setViewer({ open: true, title: doc.title, type: doc.type, url: signed });
}}
style="height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;font-weight:700;color:#374151;cursor:pointer" style="height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;font-weight:700;color:#374151;cursor:pointer"
> >
View View