REFACTORED - migrate the data layer from Prisma to Drizzle (unify the stack)

- Add lib/db (Drizzle schema: 7 tables + 6 enums + relations, postgres.js client),
  drizzle.config.ts, lib/db/enums.ts (Prisma-compatible enum objects)
- Rewrite all 14 app consumers + 4 admin components to Drizzle
- Move the test DB layer to Drizzle + in-process PGlite; convert all 8 integration
  test files + factories (371 tests green)
- Remove Prisma: deps, prisma/ schema+migrations, lib/prisma.ts, Dockerfile prisma
  generate; wire db:generate/migrate/push/studio to drizzle-kit; update Makefile
- Preserve the old seed as scripts/legacy-prisma-seed.cjs (needs a Drizzle rewrite)
This commit is contained in:
MOH
2026-08-07 14:18:41 +02:00
parent e377877e7e
commit 0a5f77d8de
48 changed files with 3765 additions and 1358 deletions
+25 -20
View File
@@ -2,7 +2,10 @@ import { createHash, createHmac, timingSafeEqual } from "crypto";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { prisma } from "./prisma";
import { and, eq, like, lt } from "drizzle-orm";
import { db } from "./db";
import { appConfig } from "./db/schema";
import { getAdminAppPath } from "./admin-routing";
export const ADMIN_SESSION_COOKIE = "moh_admin_session";
@@ -137,11 +140,9 @@ function getLockoutKey(ip: string): string {
async function cleanupExpiredLockouts(): Promise<void> {
try {
const cutoff = new Date(Date.now() - LOCKOUT_SECONDS * 2 * 1000);
await prisma.$executeRaw`
DELETE FROM "AppConfig"
WHERE key LIKE ${`${ADMIN_LOCKOUT_KEY_PREFIX}:%`}
AND "updatedAt" < ${cutoff}
`;
await db
.delete(appConfig)
.where(and(like(appConfig.key, `${ADMIN_LOCKOUT_KEY_PREFIX}:%`), lt(appConfig.updatedAt, cutoff)));
} catch {
// Non-critical — ignore cleanup errors.
}
@@ -216,10 +217,11 @@ export async function getAdminLockState(): Promise<{ locked: boolean; remainingS
try {
const ip = await getClientIp();
const key = getLockoutKey(ip);
const config = await prisma.appConfig.findUnique({
where: { key },
select: { value: true },
});
const [config] = await db
.select({ value: appConfig.value })
.from(appConfig)
.where(eq(appConfig.key, key))
.limit(1);
const state = parseFailState(config?.value);
const now = Date.now();
@@ -244,10 +246,11 @@ export async function registerFailedAdminAttempt(): Promise<{ locked: boolean; r
await cleanupExpiredLockouts();
const config = await prisma.appConfig.findUnique({
where: { key },
select: { value: true },
});
const [config] = await db
.select({ value: appConfig.value })
.from(appConfig)
.where(eq(appConfig.key, key))
.limit(1);
const current = parseFailState(config?.value);
// If a previous lockout has expired, reset the counter.
@@ -256,11 +259,13 @@ export async function registerFailedAdminAttempt(): Promise<{ locked: boolean; r
const locked = attempts >= MAX_FAILED_ATTEMPTS;
const lockUntil = locked ? now + LOCKOUT_SECONDS * 1000 : 0;
await prisma.appConfig.upsert({
where: { key },
update: { value: JSON.stringify({ attempts, lockUntil }) },
create: { key, value: JSON.stringify({ attempts, lockUntil }) },
});
await db
.insert(appConfig)
.values({ key, value: JSON.stringify({ attempts, lockUntil }) })
.onConflictDoUpdate({
target: appConfig.key,
set: { value: JSON.stringify({ attempts, lockUntil }) },
});
return {
locked,
@@ -276,7 +281,7 @@ export async function resetAdminFailedAttempts(): Promise<void> {
try {
const ip = await getClientIp();
const key = getLockoutKey(ip);
await prisma.appConfig.deleteMany({ where: { key } });
await db.delete(appConfig).where(eq(appConfig.key, key));
} catch {
// Non-critical — ignore.
}