REFACTORED - Replace Prisma with Drizzle ORM
CI / quality (push) Waiting to run

- Add lib/db (schema, postgres.js client, enums, seed, migrations) on Drizzle
- Rewrite all lib and admin action queries from Prisma to Drizzle
- Keep existing table/column names so no data migration is needed
- Preserve signed-cookie admin auth unchanged
- Map unique-violation handling from Prisma P2002 to SQLSTATE 23505
- Swap deps, scripts, Makefile, and Dockerfile from Prisma to Drizzle
This commit is contained in:
Moh
2026-08-08 02:09:03 +02:00
parent 0f48381894
commit ba63f75ea8
38 changed files with 3964 additions and 2520 deletions
+26 -22
View File
@@ -1,8 +1,10 @@
import { createHash, createHmac, timingSafeEqual } from "crypto";
import { and, eq, like, lt } from "drizzle-orm";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { prisma } from "./prisma";
import { db } from "./db";
import { appConfig } from "./db/schema";
import { getAdminAppPath } from "./admin-routing";
export const ADMIN_SESSION_COOKIE = "moh_admin_session";
@@ -137,11 +139,11 @@ 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,11 +218,12 @@ 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 state = parseFailState(config?.value);
const rows = await db
.select({ value: appConfig.value })
.from(appConfig)
.where(eq(appConfig.key, key))
.limit(1);
const state = parseFailState(rows[0]?.value);
const now = Date.now();
if (state.lockUntil > now) {
@@ -244,23 +247,24 @@ export async function registerFailedAdminAttempt(): Promise<{ locked: boolean; r
await cleanupExpiredLockouts();
const config = await prisma.appConfig.findUnique({
where: { key },
select: { value: true },
});
const rows = await db
.select({ value: appConfig.value })
.from(appConfig)
.where(eq(appConfig.key, key))
.limit(1);
const current = parseFailState(config?.value);
const current = parseFailState(rows[0]?.value);
// If a previous lockout has expired, reset the counter.
const baseAttempts = current.lockUntil > 0 && current.lockUntil < now ? 0 : current.attempts;
const attempts = baseAttempts + 1;
const locked = attempts >= MAX_FAILED_ATTEMPTS;
const lockUntil = locked ? now + LOCKOUT_SECONDS * 1000 : 0;
const value = JSON.stringify({ attempts, lockUntil });
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 })
.onConflictDoUpdate({ target: appConfig.key, set: { value, updatedAt: new Date() } });
return {
locked,
@@ -276,7 +280,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.
}