Files
EpicNext-Cms/src/app/admin/users/page.tsx
T
openhands 56061e41d4
CI / check (push) Failing after 12s
CI / deploy (push) Skipped
CI / release (push) Skipped
refactor: replace Prisma ORM runtime with Drizzle ORM facade
- Replace Prisma client runtime with Drizzle ORM (zero Prisma engine/query engine in production)
- Add Prisma-compatible facade (@/lib/prisma-facade.ts) backed by Drizzle for backwards compatibility
- Runtime queries route through Drizzle ORM; @prisma/client is now devDependency (types only)
- Remove @prisma/adapter-mariadb dependency; delete prisma-pool.ts and types/prisma.ts
- New Drizzle schema layer: src/db/schema.ts (176 tables) and src/lib/db.ts (connection)
- Update README documenting the dual-layer ORM architecture
- Restore src/generated/ gitignore (build artifact for local type generation)
- 0 TypeScript errors, 583 tests passing

The facade intentionally uses `any` types to match the Prisma Client API surface,
allowing existing code to run unmodified while routing queries through Drizzle at runtime.
2026-07-31 14:11:03 +02:00

106 lines
3.0 KiB
TypeScript

import { and, count, desc, eq, gte, like, or } from "drizzle-orm";
import { Users } from "lucide-react";
import { getTranslations } from "next-intl/server";
import { AdminPageShell } from "@/components/admin/admin-page-shell";
import { fetchAdminList } from "@/lib/admin-list";
import { db, User } from "@/lib/db";
import { canAccess, PERMS } from "@/lib/permissions";
import { CreateUserDialog } from "./_components/create-user-dialog";
import { UsersTable } from "./users-table";
export default async function UsersPage({
params: paramsPromise,
searchParams,
}: {
params: Promise<{ locale: string }>;
searchParams: Promise<Record<string, string>>;
}) {
const result = await fetchAdminList(
{
permission: PERMS.USERS_VIEW,
fetch: async ({ search, page, perPage, rawParams }) => {
const conditions = [];
if (search) {
const ors = [
like(User.username, `%${search}%`),
like(User.mail, `%${search}%`),
like(User.ipRegister, `%${search}%`),
like(User.ipCurrent, `%${search}%`),
];
const asId = Number(search);
if (Number.isFinite(asId) && asId > 0) {
ors.push(eq(User.id, asId));
}
conditions.push(or(...ors));
}
const rank = Number(rawParams.filter_rank);
if (Number.isFinite(rank)) {
// For rank 7 the preset means "super admins" — match ≥ 7 instead of equals.
conditions.push(rank >= 7 ? gte(User.rank, 7) : eq(User.rank, rank));
}
if (rawParams.filter_online) {
conditions.push(eq(User.online, rawParams.filter_online));
}
const where = conditions.length > 0 ? and(...conditions) : undefined;
const [rows, totals] = await Promise.all([
db
.select({
id: User.id,
username: User.username,
mail: User.mail,
rank: User.rank,
credits: User.credits,
pixels: User.pixels,
online: User.online,
motto: User.motto,
look: User.look,
ipRegister: User.ipRegister,
accountCreated: User.accountCreated,
lastLogin: User.lastLogin,
})
.from(User)
.where(where)
.orderBy(desc(User.id))
.limit(perPage)
.offset((page - 1) * perPage),
db.select({ total: count() }).from(User).where(where),
]);
return { rows, total: totals[0]?.total ?? 0 };
},
},
paramsPromise,
searchParams,
);
const t = await getTranslations("pages.admin.users");
const canEdit = canAccess(result.permissions, PERMS.USERS_EDIT, result.rank);
const canBan = canAccess(result.permissions, PERMS.USERS_BAN, result.rank);
return (
<AdminPageShell
icon={Users}
title={t("title")}
subtitle={t("subtitle")}
actions={
canEdit ? <CreateUserDialog sessionRank={result.rank} /> : undefined
}
>
<p className="text-muted-foreground m-0">
{result.total.toLocaleString()} users total
</p>
<UsersTable
data={{
rows: result.rows,
total: result.total,
page: result.page,
perPage: result.perPage,
lastPage: result.lastPage,
}}
canEdit={canEdit}
canBan={canBan}
/>
</AdminPageShell>
);
}