feat(auth): wire AuthProvider + RequireAuth into app shell
This commit is contained in:
+105
-165
@@ -43,6 +43,10 @@ import type {
|
||||
UnmatchedResponse,
|
||||
BatchSummary as ParserBatchSummary,
|
||||
} from "@/types";
|
||||
import {
|
||||
authedFetch,
|
||||
authedFetchResponse,
|
||||
} from "@/auth/api";
|
||||
|
||||
const BASE_URL = (import.meta.env.VITE_API_BASE_URL as string | undefined) ?? "";
|
||||
|
||||
@@ -167,10 +171,6 @@ export class ApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function joinUrl(path: string): string {
|
||||
return `${BASE_URL.replace(/\/$/, "")}${path}`;
|
||||
}
|
||||
|
||||
async function readErrorBody(res: Response): Promise<string> {
|
||||
try {
|
||||
const t = await res.text();
|
||||
@@ -296,11 +296,18 @@ async function parse837(
|
||||
form.append("file", file, file.name);
|
||||
|
||||
const accept = onProgress ? "application/x-ndjson" : "application/json";
|
||||
const res = await fetch(joinUrl(`/api/parse-837?${params.toString()}`), {
|
||||
method: "POST",
|
||||
body: form,
|
||||
headers: { Accept: accept },
|
||||
});
|
||||
// authedFetchResponse still hits fetch() (and still does the
|
||||
// 401-redirect for any non-/api/auth path), but hands back the raw
|
||||
// Response so we can stream NDJSON line-by-line below. For non-stream
|
||||
// callers it returns the parsed JSON.
|
||||
const res = await authedFetchResponse(
|
||||
`/api/parse-837?${params.toString()}`,
|
||||
{
|
||||
method: "POST",
|
||||
body: form,
|
||||
headers: { Accept: accept },
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
@@ -332,11 +339,17 @@ async function parse835(
|
||||
form.append("file", file, file.name);
|
||||
|
||||
const accept = onProgress ? "application/x-ndjson" : "application/json";
|
||||
const res = await fetch(joinUrl(`/api/parse-835?${params.toString()}`), {
|
||||
method: "POST",
|
||||
body: form,
|
||||
headers: { Accept: accept },
|
||||
});
|
||||
// See parse837 for why this uses authedFetchResponse instead of
|
||||
// authedFetch — NDJSON streaming needs the raw Response so we can
|
||||
// iterate `res.body` one chunk at a time.
|
||||
const res = await authedFetchResponse(
|
||||
`/api/parse-835?${params.toString()}`,
|
||||
{
|
||||
method: "POST",
|
||||
body: form,
|
||||
headers: { Accept: accept },
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
@@ -362,16 +375,7 @@ async function parse999(
|
||||
if (!isConfigured) throw notConfiguredError();
|
||||
const form = new FormData();
|
||||
form.append("file", file, file.name);
|
||||
const res = await fetch(joinUrl("/api/parse-999"), {
|
||||
method: "POST",
|
||||
body: form,
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
throw new ApiError(res.status, detail || res.statusText);
|
||||
}
|
||||
const body = (await res.json()) as {
|
||||
const body = await authedFetch<{
|
||||
ack: {
|
||||
id: number;
|
||||
accepted_count: number;
|
||||
@@ -382,7 +386,14 @@ async function parse999(
|
||||
raw_999_text: string;
|
||||
};
|
||||
parsed: unknown;
|
||||
};
|
||||
}>("/api/parse-999", {
|
||||
method: "POST",
|
||||
body: form,
|
||||
// Don't set Content-Type — the browser sets the multipart
|
||||
// boundary when the body is FormData. authedFetch only adds a
|
||||
// default Accept, which we override here.
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
return {
|
||||
ack: {
|
||||
id: body.ack.id,
|
||||
@@ -403,14 +414,14 @@ async function parse999(
|
||||
|
||||
async function health(): Promise<HealthResponse | null> {
|
||||
if (!isConfigured) return null;
|
||||
const res = await fetch(joinUrl("/api/health"));
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
throw new Error(
|
||||
`${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
|
||||
);
|
||||
// health() is best-effort — used by the side-scan ping. Don't let a
|
||||
// 401 from authedFetch redirect the operator; just return null when
|
||||
// anything goes wrong.
|
||||
try {
|
||||
return await authedFetch<HealthResponse>("/api/health");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return (await res.json()) as HealthResponse;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -423,48 +434,26 @@ async function listBatches(limit?: number): Promise<BatchSummary[]> {
|
||||
if (!isConfigured) throw notConfiguredError();
|
||||
const params: Record<string, unknown> = {};
|
||||
if (limit !== undefined) params.limit = limit;
|
||||
const res = await fetch(joinUrl(`/api/batches${qs(params)}`), {
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
throw new Error(
|
||||
`${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
|
||||
);
|
||||
}
|
||||
const body = (await res.json()) as { items: BatchSummary[] };
|
||||
const body = await authedFetch<{ items: BatchSummary[] }>(
|
||||
`/api/batches${qs(params)}`
|
||||
);
|
||||
return body.items;
|
||||
}
|
||||
|
||||
async function getBatch(id: string): Promise<ParseResult837 | ParseResult835> {
|
||||
if (!isConfigured) throw notConfiguredError();
|
||||
const res = await fetch(joinUrl(`/api/batches/${encodeURIComponent(id)}`), {
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
throw new Error(
|
||||
`${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
|
||||
);
|
||||
}
|
||||
return (await res.json()) as ParseResult837 | ParseResult835;
|
||||
return authedFetch<ParseResult837 | ParseResult835>(
|
||||
`/api/batches/${encodeURIComponent(id)}`
|
||||
);
|
||||
}
|
||||
|
||||
async function listClaims<T = unknown>(
|
||||
params: ListClaimsParams
|
||||
): Promise<PaginatedResponse<T>> {
|
||||
if (!isConfigured) throw notConfiguredError();
|
||||
const res = await fetch(
|
||||
joinUrl(`/api/claims${qs(params as Record<string, unknown>)}`),
|
||||
{ headers: { Accept: "application/json" } }
|
||||
return authedFetch<PaginatedResponse<T>>(
|
||||
`/api/claims${qs(params as Record<string, unknown>)}`
|
||||
);
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
throw new Error(
|
||||
`${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
|
||||
);
|
||||
}
|
||||
return (await res.json()) as PaginatedResponse<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -480,14 +469,31 @@ async function listClaims<T = unknown>(
|
||||
*/
|
||||
async function getClaimDetail(id: string): Promise<ClaimDetail> {
|
||||
if (!isConfigured) throw notConfiguredError();
|
||||
const res = await fetch(joinUrl(`/api/claims/${encodeURIComponent(id)}`), {
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
throw new ApiError(res.status, detail || res.statusText);
|
||||
try {
|
||||
return await authedFetch<ClaimDetail>(
|
||||
`/api/claims/${encodeURIComponent(id)}`
|
||||
);
|
||||
} catch (err) {
|
||||
// authedFetch throws the richer auth/api.ts ApiError (carries
|
||||
// `status` + `code` + `detail`). The drawer code branches on
|
||||
// `.status === 404` for the "claim doesn't exist" state, and
|
||||
// existing callers use `instanceof ApiError` (the lib/api.ts one)
|
||||
// to detect structured failures. Re-wrap into the lib/api.ts
|
||||
// ApiError so both keep working unchanged.
|
||||
if (
|
||||
err &&
|
||||
typeof err === "object" &&
|
||||
"status" in err &&
|
||||
typeof (err as { status: unknown }).status === "number"
|
||||
) {
|
||||
const e = err as { status: number; code?: string; detail?: string };
|
||||
throw new ApiError(
|
||||
e.status,
|
||||
e.detail ?? e.code ?? "error"
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return (await res.json()) as ClaimDetail;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -509,8 +515,14 @@ async function serializeClaim837(
|
||||
id: string,
|
||||
): Promise<{ text: string; filename: string }> {
|
||||
if (!isConfigured) throw notConfiguredError();
|
||||
const res = await fetch(
|
||||
joinUrl(`/api/claims/${encodeURIComponent(id)}/serialize-837`),
|
||||
// authedFetchText gives us the body as a string with the same
|
||||
// 401-redirect + error-shape behavior as authedFetch. We need the
|
||||
// Response object for the Content-Disposition header, so this
|
||||
// function delegates to authedFetchResponse and reads text +
|
||||
// headers itself. No Accept negotiation — the endpoint always
|
||||
// returns text/x12.
|
||||
const res = await authedFetchResponse(
|
||||
`/api/claims/${encodeURIComponent(id)}/serialize-837`
|
||||
);
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
@@ -530,17 +542,9 @@ async function listRemittances<T = unknown>(
|
||||
params: ListRemittancesParams
|
||||
): Promise<PaginatedResponse<T>> {
|
||||
if (!isConfigured) throw notConfiguredError();
|
||||
const res = await fetch(
|
||||
joinUrl(`/api/remittances${qs(params as Record<string, unknown>)}`),
|
||||
{ headers: { Accept: "application/json" } }
|
||||
return authedFetch<PaginatedResponse<T>>(
|
||||
`/api/remittances${qs(params as Record<string, unknown>)}`
|
||||
);
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
throw new Error(
|
||||
`${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
|
||||
);
|
||||
}
|
||||
return (await res.json()) as PaginatedResponse<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -549,14 +553,7 @@ async function listRemittances<T = unknown>(
|
||||
*/
|
||||
async function getRemittance<T = unknown>(id: string): Promise<T> {
|
||||
if (!isConfigured) throw notConfiguredError();
|
||||
const res = await fetch(joinUrl(`/api/remittances/${encodeURIComponent(id)}`), {
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
throw new ApiError(res.status, detail || res.statusText);
|
||||
}
|
||||
return (await res.json()) as T;
|
||||
return authedFetch<T>(`/api/remittances/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -580,34 +577,16 @@ async function getBatchDiff(a: string, b: string): Promise<BatchDiff> {
|
||||
if (typeof b !== "string" || b.length === 0) {
|
||||
throw new ApiError(400, "Missing param: ?b=<batch_id> is required.");
|
||||
}
|
||||
const res = await fetch(
|
||||
joinUrl(
|
||||
`/api/batch-diff?${qs({ a, b })}`,
|
||||
),
|
||||
{ headers: { Accept: "application/json" } },
|
||||
);
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
throw new ApiError(res.status, detail || res.statusText);
|
||||
}
|
||||
return (await res.json()) as BatchDiff;
|
||||
return authedFetch<BatchDiff>(`/api/batch-diff?${qs({ a, b })}`);
|
||||
}
|
||||
|
||||
async function listProviders<T = unknown>(
|
||||
params: ListProvidersParams = {}
|
||||
): Promise<PaginatedResponse<T>> {
|
||||
if (!isConfigured) throw notConfiguredError();
|
||||
const res = await fetch(
|
||||
joinUrl(`/api/providers${qs(params as Record<string, unknown>)}`),
|
||||
{ headers: { Accept: "application/json" } }
|
||||
return authedFetch<PaginatedResponse<T>>(
|
||||
`/api/providers${qs(params as Record<string, unknown>)}`
|
||||
);
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
throw new Error(
|
||||
`${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
|
||||
);
|
||||
}
|
||||
return (await res.json()) as PaginatedResponse<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -674,17 +653,9 @@ async function listActivity<T = unknown>(
|
||||
params: ListActivityParams = {}
|
||||
): Promise<PaginatedResponse<T>> {
|
||||
if (!isConfigured) throw notConfiguredError();
|
||||
const res = await fetch(
|
||||
joinUrl(`/api/activity${qs(params as Record<string, unknown>)}`),
|
||||
{ headers: { Accept: "application/json" } }
|
||||
return authedFetch<PaginatedResponse<T>>(
|
||||
`/api/activity${qs(params as Record<string, unknown>)}`
|
||||
);
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
throw new Error(
|
||||
`${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`
|
||||
);
|
||||
}
|
||||
return (await res.json()) as PaginatedResponse<T>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -695,14 +666,7 @@ async function listActivity<T = unknown>(
|
||||
|
||||
async function listUnmatched(): Promise<UnmatchedResponse> {
|
||||
if (!isConfigured) throw notConfiguredError();
|
||||
const res = await fetch(joinUrl(`/api/reconciliation/unmatched`), {
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
throw new ApiError(res.status, detail || res.statusText);
|
||||
}
|
||||
return (await res.json()) as UnmatchedResponse;
|
||||
return authedFetch<UnmatchedResponse>(`/api/reconciliation/unmatched`);
|
||||
}
|
||||
|
||||
async function matchRemit(
|
||||
@@ -710,36 +674,20 @@ async function matchRemit(
|
||||
remitId: string
|
||||
): Promise<MatchResponse> {
|
||||
if (!isConfigured) throw notConfiguredError();
|
||||
const res = await fetch(joinUrl(`/api/reconciliation/match`), {
|
||||
return authedFetch<MatchResponse>(`/api/reconciliation/match`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ claim_id: claimId, remit_id: remitId }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
throw new ApiError(res.status, detail || res.statusText);
|
||||
}
|
||||
return (await res.json()) as MatchResponse;
|
||||
}
|
||||
|
||||
async function unmatchClaim(claimId: string): Promise<{ claim: UnmatchedClaim }> {
|
||||
if (!isConfigured) throw notConfiguredError();
|
||||
const res = await fetch(joinUrl(`/api/reconciliation/unmatch`), {
|
||||
return authedFetch<{ claim: UnmatchedClaim }>(`/api/reconciliation/unmatch`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ claim_id: claimId }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
throw new ApiError(res.status, detail || res.statusText);
|
||||
}
|
||||
return (await res.json()) as { claim: UnmatchedClaim };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -778,15 +726,12 @@ async function listAcks(params: { limit?: number } = {}): Promise<PaginatedRespo
|
||||
if (!isConfigured) throw notConfiguredError();
|
||||
const query: Record<string, unknown> = {};
|
||||
if (params.limit !== undefined) query.limit = params.limit;
|
||||
const res = await fetch(
|
||||
joinUrl(`/api/acks${qs(query)}`),
|
||||
{ headers: { Accept: "application/json" } }
|
||||
);
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
throw new ApiError(res.status, detail || res.statusText);
|
||||
}
|
||||
const body = (await res.json()) as { items: RawAckRow[]; total: number; returned: number; has_more: boolean };
|
||||
const body = await authedFetch<{
|
||||
items: RawAckRow[];
|
||||
total: number;
|
||||
returned: number;
|
||||
has_more: boolean;
|
||||
}>(`/api/acks${qs(query)}`);
|
||||
return {
|
||||
items: body.items.map(mapAck),
|
||||
total: body.total,
|
||||
@@ -797,14 +742,9 @@ async function listAcks(params: { limit?: number } = {}): Promise<PaginatedRespo
|
||||
|
||||
async function getAck(id: number): Promise<Ack & { rawJson: unknown }> {
|
||||
if (!isConfigured) throw notConfiguredError();
|
||||
const res = await fetch(joinUrl(`/api/acks/${encodeURIComponent(String(id))}`), {
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
throw new ApiError(res.status, detail || res.statusText);
|
||||
}
|
||||
const row = (await res.json()) as RawAckRow & { raw_json: unknown };
|
||||
const row = await authedFetch<RawAckRow & { raw_json: unknown }>(
|
||||
`/api/acks/${encodeURIComponent(String(id))}`
|
||||
);
|
||||
return { ...mapAck(row), rawJson: row.raw_json };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user