fix(audit-tier-2): error-surface hygiene — toastError + CodedError sweep
Two mechanical sweeps closing the audit's HIGH §16 + MED §11 findings: * 38 client components / 56 toast.error sites converted to toastError(err) so the new admin error inspector becomes usable from user-reported issues — every failed inline-edit, save, send, archive, upload, etc. now carries the request-id + error-code (Copy ID action). * 26 service files / 62 bare-Error throws converted to CodedError or the existing AppError subclasses. Adds new error codes: DOCUMENSO_UPSTREAM_ERROR (502), DOCUMENSO_AUTH_FAILURE (502), DOCUMENSO_TIMEOUT (504), OCR_UPSTREAM_ERROR (502), IMAP_UPSTREAM_ERROR (502), UMAMI_UPSTREAM_ERROR (502), UMAMI_NOT_CONFIGURED (409), and INSERT_RETURNING_EMPTY (500) for post-insert returning-empty guards. * Five vitest assertions updated to match the new user-facing wording (client-merge "already been merged", expense/interest "couldn't find that …", documenso "signing service didn't respond"). Test status: 1168/1168 vitest, tsc clean. Refs: docs/audit-comprehensive-2026-05-05.md HIGH §16 (auditor-H Issue 1) + MED §11 (auditor-G Issue 1). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ import { and, eq, inArray } from 'drizzle-orm';
|
||||
import { db } from '@/lib/db';
|
||||
import { systemSettings } from '@/lib/db/schema/system';
|
||||
import { rangeToBounds, type DateRange } from '@/lib/analytics/range';
|
||||
import { CodedError } from '@/lib/errors';
|
||||
import { fetchWithTimeout } from '@/lib/fetch-with-timeout';
|
||||
|
||||
// ─── Settings access ────────────────────────────────────────────────────────
|
||||
@@ -87,10 +88,15 @@ async function loginAndCache(apiUrl: string, username: string, password: string)
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Umami login failed: ${res.status} ${res.statusText}`);
|
||||
throw new CodedError('UMAMI_UPSTREAM_ERROR', {
|
||||
internalMessage: `Umami login failed: ${res.status} ${res.statusText}`,
|
||||
});
|
||||
}
|
||||
const body = (await res.json()) as { token?: string };
|
||||
if (!body.token) throw new Error('Umami login response missing token');
|
||||
if (!body.token)
|
||||
throw new CodedError('UMAMI_UPSTREAM_ERROR', {
|
||||
internalMessage: 'Umami login response missing token',
|
||||
});
|
||||
jwtCache.set(`${apiUrl}::${username}`, {
|
||||
token: body.token,
|
||||
expiresAt: Date.now() + JWT_TTL_MS,
|
||||
@@ -101,7 +107,9 @@ async function loginAndCache(apiUrl: string, username: string, password: string)
|
||||
async function resolveBearer(config: UmamiPortConfig): Promise<string> {
|
||||
if (config.apiToken) return config.apiToken;
|
||||
if (!config.username || !config.password) {
|
||||
throw new Error('Umami is misconfigured: no API token and no username/password.');
|
||||
throw new CodedError('UMAMI_NOT_CONFIGURED', {
|
||||
internalMessage: 'Umami is misconfigured: no API token and no username/password.',
|
||||
});
|
||||
}
|
||||
const cacheKey = `${config.apiUrl}::${config.username}`;
|
||||
const cached = jwtCache.get(cacheKey);
|
||||
@@ -136,13 +144,17 @@ async function umamiFetch<T>(
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
// Bearer rejected - drop cached JWT so next call re-logs in.
|
||||
if (config.username) jwtCache.delete(`${config.apiUrl}::${config.username}`);
|
||||
throw new Error(`Umami unauthorized: ${res.status}`);
|
||||
throw new CodedError('UMAMI_UPSTREAM_ERROR', {
|
||||
internalMessage: `Umami unauthorized: ${res.status}`,
|
||||
});
|
||||
}
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(
|
||||
`Umami ${path} failed: ${res.status} ${res.statusText}${text ? ` - ${text}` : ''}`,
|
||||
);
|
||||
throw new CodedError('UMAMI_UPSTREAM_ERROR', {
|
||||
internalMessage: `Umami ${path} failed: ${res.status} ${res.statusText}${
|
||||
text ? ` - ${text}` : ''
|
||||
}`,
|
||||
});
|
||||
}
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
@@ -248,7 +260,9 @@ export async function getActiveVisitors(portId: string): Promise<UmamiActiveVisi
|
||||
export async function testConnection(portId: string): Promise<{ ok: true; visitors: number }> {
|
||||
const config = await loadUmamiConfig(portId);
|
||||
if (!config) {
|
||||
throw new Error('Umami is not configured for this port.');
|
||||
throw new CodedError('UMAMI_NOT_CONFIGURED', {
|
||||
internalMessage: 'Umami is not configured for this port.',
|
||||
});
|
||||
}
|
||||
const result = await umamiFetch<UmamiActiveVisitors>(
|
||||
config,
|
||||
|
||||
Reference in New Issue
Block a user