Fix job seeker document submission: hide raw storage URL, fix error masking
All checks were successful
build-and-release / build (push) Successful in 1m48s

Two issues in the job seeker (and shared profile) dashboard:
- The uploaded document's full Backblaze URL was rendered as visible text
  and linked directly; view-document now resolves a short-lived signed
  URL on demand instead of exposing the permanent storage link.
- "Submit for Verification" always showed a generic "Network error"
  message on any failure because request() throws on non-2xx responses,
  making the destructured status/data unreachable in the catch-free path
  (e.g. a 409 "verification already in progress" looked identical to a
  network failure). Now surfaces the real backend error message.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar Sivakumar 2026-07-27 18:07:37 +05:30
parent 83b5072011
commit cab192d946
3 changed files with 34 additions and 14 deletions

View file

@ -859,7 +859,7 @@ export default function ProfilePage(props: Props) {
setSubmitting(true); setSubmitting(true);
setSubmitMsg(""); setSubmitMsg("");
try { try {
const { data, status } = await request("/api/profile/submit-for-verification", { const { status } = await request("/api/profile/submit-for-verification", {
method: "POST", method: "POST",
body: { roleKey: props.roleKey, profile_data: { ...form(), ...docUrls() } }, body: { roleKey: props.roleKey, profile_data: { ...form(), ...docUrls() } },
}); });
@ -867,13 +867,13 @@ export default function ProfilePage(props: Props) {
setVerificationStatus("PENDING"); setVerificationStatus("PENDING");
props.onVerificationStatusChange?.("PENDING"); props.onVerificationStatusChange?.("PENDING");
setSubmitMsg("Submitted! We will review your profile and notify you."); setSubmitMsg("Submitted! We will review your profile and notify you.");
} else if (status === 409) {
setSubmitMsg(data?.error ?? "A verification is already in progress.");
} else {
setSubmitMsg(data?.error ?? "Submission failed. Please try again.");
} }
} catch { } catch (err: any) {
setSubmitMsg("Network error. Please try again."); // request() throws on any non-2xx response with the backend's actual error
// message (e.g. "A verification is already in progress...") — surface that
// instead of a generic message, otherwise every failure looks identical and
// unactionable regardless of cause.
setSubmitMsg(err?.message || "Submission failed. Please try again.");
} finally { } finally {
setSubmitting(false); setSubmitting(false);
} }
@ -1241,7 +1241,7 @@ export default function ProfilePage(props: Props) {
"border-radius": "6px", "border-radius": "6px",
}} }}
> >
{docUrls()[doc.key]} Uploaded
</span> </span>
<Show when={!isLocked()}> <Show when={!isLocked()}>
<button <button

View file

@ -8,6 +8,7 @@ import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
import { ShieldCheck, FileText, Activity } from 'lucide-solid'; import { ShieldCheck, FileText, Activity } from 'lucide-solid';
import { CARD, BTN_ORANGE, BTN_GHOST } from '~/components/DashboardShell'; import { CARD, BTN_ORANGE, BTN_GHOST } from '~/components/DashboardShell';
import { getDocFields } from '~/lib/profile-fields-config'; import { getDocFields } from '~/lib/profile-fields-config';
import { presignDocumentUrl } from '~/lib/api';
const NAVY = '#0D0D2A'; const NAVY = '#0D0D2A';
const ORANGE = '#FF5E13'; const ORANGE = '#FF5E13';
@ -478,14 +479,20 @@ export default function VerificationStatusPage(props: Props) {
<p style={{ margin: '4px 0 0', 'font-size': '12px', color: '#9CA3AF' }}>No file uploaded</p> <p style={{ margin: '4px 0 0', 'font-size': '12px', color: '#9CA3AF' }}>No file uploaded</p>
} }
> >
<a <button
href={url()!} type="button"
target="_blank" onClick={async () => {
rel="noopener noreferrer" const signed = await presignDocumentUrl(url()!);
style={{ margin: '4px 0 0', 'font-size': '12px', color: '#FF5E13', 'text-decoration': 'underline', display: 'inline-block' }} window.open(signed, '_blank', 'noopener,noreferrer');
}}
style={{
margin: '4px 0 0', 'font-size': '12px', color: '#FF5E13',
'text-decoration': 'underline', display: 'inline-block',
background: 'none', border: 'none', padding: '0', cursor: 'pointer',
}}
> >
View document View document
</a> </button>
</Show> </Show>
</div> </div>
<p style={{ <p style={{

View file

@ -90,6 +90,19 @@ export async function fetchDocuments(rolePrefix: string): Promise<any[]> {
return res?.data ?? []; return res?.data ?? [];
} }
/**
* Resolve a stored document reference into a short-lived, signed view URL.
* Never render a stored document URL directly (e.g. as an <a href> or <img src>)
* always exchange it for a fresh one through this endpoint first.
*/
export async function presignDocumentUrl(url: string): Promise<string> {
const res = await apiFetch('/api/profile/documents/presign', {
method: 'POST',
body: JSON.stringify({ url }),
});
return res?.url ?? url;
}
export async function submitProfileForVerification(rolePrefix: string): Promise<any> { export async function submitProfileForVerification(rolePrefix: string): Promise<any> {
return apiFetch(`/api/${rolePrefix}/profile/submit`, { return apiFetch(`/api/${rolePrefix}/profile/submit`, {
method: 'POST', method: 'POST',