refactor(types): move BackupRow and BackupsList out of route handler into src/types/backups.ts

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.
This commit is contained in:
Marc-André Bombeck committed 2026-05-21 17:10:38 +02:00
1 parent 4cca62dce9
commit bba38b8c66
6 files changed
+52 -28

No files matched your search

+4 -25
View File
@@ -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" } });
+3 -1
View File
@@ -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 } }),
@@ -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({
+2 -1
View File
@@ -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",
+1 -1
View File
@@ -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";
/**
+36
View File
@@ -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;
}