From bba38b8c66e070b8922e3868623771403cd00acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 21 May 2026 17:10:38 +0200 Subject: [PATCH] refactor(types): move BackupRow and BackupsList out of route handler into src/types/backups.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves the single component → route-handler import in the codebase (`components/admin/backups-section.tsx ← app/api/admin/backups/route.ts`) flagged by the v1.4.39 org audit (recommendation #2). The DTOs now live in `src/types/backups.ts`; the route handler keeps owning the HTTP contract and re-imports the shared types from there. --- src/app/api/admin/backups/route.ts | 29 +++------------ src/app/api/admin/status/route.ts | 4 ++- .../api/doctor-report/availability/route.ts | 6 ++++ src/app/auth/login/page.tsx | 3 +- src/components/admin/backups-section.tsx | 2 +- src/types/backups.ts | 36 +++++++++++++++++++ 6 files changed, 52 insertions(+), 28 deletions(-) create mode 100644 src/types/backups.ts diff --git a/src/app/api/admin/backups/route.ts b/src/app/api/admin/backups/route.ts index 689c7a442..06fb9bf70 100644 --- a/src/app/api/admin/backups/route.ts +++ b/src/app/api/admin/backups/route.ts @@ -10,34 +10,13 @@ import { prisma } from "@/lib/db"; import { apiHandler, requireAdmin } from "@/lib/api-handler"; import { apiSuccess } from "@/lib/api-response"; import { annotate } from "@/lib/logging/context"; +// v1.4.41 W-ORG — `BackupRow` / `BackupsList` moved to `src/types/backups.ts` +// so callers (in particular `components/admin/backups-section.tsx`) don't +// have to reach across the component → route-handler layer boundary. +import type { BackupRow, BackupsList } from "@/types/backups"; export const dynamic = "force-dynamic"; -export interface BackupRow { - id: string; - userId: string; - username: string; - type: string; - /** - * Size of the encrypted blob in bytes — useful for capacity planning. - * The blob itself is never returned. - */ - sizeBytes: number; - createdAt: string; -} - -export interface BackupsList { - rows: BackupRow[]; - /** - * Soft retention hint — the worker is configured for weekly backups - * (see `DATA_BACKUP_CRON` in `src/lib/jobs/reminder-worker.ts`), and - * the model upserts in-place per (userId, type), so each user has - * exactly one current snapshot. The frontend uses this to label the - * grid; no server-side enforcement. - */ - retentionDays: number; -} - export const GET = apiHandler(async () => { await requireAdmin(); annotate({ action: { name: "admin.backups.list" } }); diff --git a/src/app/api/admin/status/route.ts b/src/app/api/admin/status/route.ts index 8bba954da..1fd192828 100644 --- a/src/app/api/admin/status/route.ts +++ b/src/app/api/admin/status/route.ts @@ -38,7 +38,9 @@ export const GET = apiHandler(async () => { appSettings, ] = await Promise.all([ prisma.user.count(), - prisma.measurement.count(), + // v1.4.41 W-DELETED-2 — exclude soft-deleted measurements from the + // admin status count so the dashboard tile reflects live data only. + prisma.measurement.count({ where: { deletedAt: null } }), prisma.medication.count(), prisma.medicationIntakeEvent.count(), prisma.apiToken.count({ where: { revoked: false } }), diff --git a/src/app/api/doctor-report/availability/route.ts b/src/app/api/doctor-report/availability/route.ts index ec175f797..9ac2c2b6a 100644 --- a/src/app/api/doctor-report/availability/route.ts +++ b/src/app/api/doctor-report/availability/route.ts @@ -60,6 +60,10 @@ export const POST = apiHandler(async (request: NextRequest) => { userId: user.id, measuredAt: { gte: start, lte: end }, type: { in: ["BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA"] }, + // v1.4.41 W-DELETED-2 — hide soft-deleted rows from the + // section-availability probe so the dialog does not offer a + // section that would render an empty PDF block. + deletedAt: null, }, }), prisma.measurement.count({ @@ -67,6 +71,7 @@ export const POST = apiHandler(async (request: NextRequest) => { userId: user.id, measuredAt: { gte: start, lte: end }, type: "WEIGHT", + deletedAt: null, }, }), prisma.measurement.count({ @@ -74,6 +79,7 @@ export const POST = apiHandler(async (request: NextRequest) => { userId: user.id, measuredAt: { gte: start, lte: end }, type: "PULSE", + deletedAt: null, }, }), prisma.moodEntry.count({ diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index b52e82d27..281d5a23c 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -12,6 +12,7 @@ import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { describePasskeyError } from "@/lib/passkey-errors"; import { useTranslations } from "@/lib/i18n/context"; +import { queryKeys } from "@/lib/query-keys"; export default function LoginPage() { const router = useRouter(); @@ -30,7 +31,7 @@ export default function LoginPage() { const errorId = useId(); const errorDescriptor = error ? errorId : undefined; const { data: registrationEnabled } = useQuery({ - queryKey: ["auth", "registration-status"], + queryKey: queryKeys.authRegistrationStatus(), queryFn: async () => { const res = await fetch("/api/auth/registration-status", { cache: "no-store", diff --git a/src/components/admin/backups-section.tsx b/src/components/admin/backups-section.tsx index be8d6eadb..374638108 100644 --- a/src/components/admin/backups-section.tsx +++ b/src/components/admin/backups-section.tsx @@ -41,7 +41,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { formatDateTime } from "@/lib/format"; import { useFormatters, useTranslations } from "@/lib/i18n/context"; -import type { BackupRow, BackupsList } from "@/app/api/admin/backups/route"; +import type { BackupRow, BackupsList } from "@/types/backups"; import { getApiErrorMessage } from "./_shared"; /** diff --git a/src/types/backups.ts b/src/types/backups.ts new file mode 100644 index 000000000..75b0cc66c --- /dev/null +++ b/src/types/backups.ts @@ -0,0 +1,36 @@ +/** + * v1.4.41 W-ORG (org-audit rec #2) — DTOs for `/api/admin/backups`. + * + * Pre-v1.4.41 these interfaces lived inside the route handler at + * `src/app/api/admin/backups/route.ts` and were imported directly from + * the route module by `src/components/admin/backups-section.tsx`. That + * was a textbook layer violation (component → route handler) and the + * only one of its kind in the codebase. Hoisting the shapes here gives + * both sides a route-handler-independent shared type home; the route + * keeps owning the HTTP contract, the component keeps owning the UI. + */ + +export interface BackupRow { + id: string; + userId: string; + username: string; + type: string; + /** + * Size of the encrypted blob in bytes — useful for capacity planning. + * The blob itself is never returned. + */ + sizeBytes: number; + createdAt: string; +} + +export interface BackupsList { + rows: BackupRow[]; + /** + * Soft retention hint — the worker is configured for weekly backups + * (see `DATA_BACKUP_CRON` in `src/lib/jobs/reminder-worker.ts`), and + * the model upserts in-place per (userId, type), so each user has + * exactly one current snapshot. The frontend uses this to label the + * grid; no server-side enforcement. + */ + retentionDays: number; +}