Files
EpicNext-Cms/src/actions/permissions.ts
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

272 lines
7.9 KiB
TypeScript

"use server";
import { and, count, eq, inArray, sql } from "drizzle-orm";
import type { ResultSetHeader } from "mysql2";
import { revalidateTag } from "next/cache";
import { z } from "zod";
import {
AclModelPermission,
AclModelRole,
AclPermission,
AclRole,
db,
User,
} from "@/lib/db";
import { PERMS } from "@/lib/permission-slugs";
import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared";
import {
createEmulatorRank,
deleteEmulatorRank,
updateEmulatorRank,
} from "@/lib/services/permission-ranks";
import { rcon } from "@/lib/services/rcon";
import { logStaffActivity } from "@/lib/services/staff-activity";
const createRankSchema = z.object({
rank_name: z.string().trim().min(1).max(25),
level: z.coerce.number().int().min(1),
});
export const createRank = adminAction(
{ schema: createRankSchema, permission: PERMS.PERMISSIONS_MANAGE },
async (ctx) => {
const id = await createEmulatorRank(db, ctx.data);
await db
.insert(AclRole)
.values({
slug: `rank_${id}`,
title: ctx.data.rank_name,
description: "CMS role synchronized from permission_ranks",
})
.onDuplicateKeyUpdate({ set: { title: ctx.data.rank_name } });
await logStaffActivity({
staffId: ctx.session.user.id,
action: "rank_create",
description: `Created rank #${id}`,
targetType: "rank",
targetId: id,
});
await rcon.send("updatepermissions");
revalidateTag("permissions", { expire: 0 });
return actionOk({ id });
},
);
const deleteRankSchema = z.object({ id: z.coerce.number().int().positive() });
export const deleteRank = adminAction(
{ schema: deleteRankSchema, permission: PERMS.PERMISSIONS_MANAGE },
async (ctx) => {
const [userCount] = await db
.select({ total: count() })
.from(User)
.where(eq(User.rank, ctx.data.id));
const users = userCount?.total ?? 0;
if (users > 0)
throw new ActionError(`Cannot delete: ${users} users have this rank`);
const [role] = await db
.select({ id: AclRole.id })
.from(AclRole)
.where(eq(AclRole.slug, `rank_${ctx.data.id}`))
.limit(1);
await deleteEmulatorRank(db, ctx.data.id);
if (role) {
await db.transaction(async (tx) => {
await tx
.delete(AclModelPermission)
.where(
and(
eq(AclModelPermission.modelId, role.id),
eq(AclModelPermission.modelType, "Role"),
),
);
await tx.delete(AclModelRole).where(eq(AclModelRole.roleId, role.id));
await tx.delete(AclRole).where(eq(AclRole.id, role.id));
});
}
await logStaffActivity({
staffId: ctx.session.user.id,
action: "rank_delete",
description: `Deleted rank #${ctx.data.id}`,
targetType: "rank",
targetId: ctx.data.id,
});
await rcon.send("updatepermissions");
revalidateTag("permissions", { expire: 0 });
return actionOk();
},
);
const saveRankSchema = z.object({
id: z.coerce.number().int().positive(),
fields: z.record(z.string(), z.union([z.string(), z.number()])),
});
export const saveRank = adminAction(
{ schema: saveRankSchema, permission: PERMS.PERMISSIONS_MANAGE },
async (ctx) => {
await updateEmulatorRank(db, ctx.data.id, ctx.data.fields);
if (typeof ctx.data.fields.rank_name === "string") {
await db
.update(AclRole)
.set({ title: ctx.data.fields.rank_name })
.where(eq(AclRole.slug, `rank_${ctx.data.id}`));
}
await logStaffActivity({
staffId: ctx.session.user.id,
action: "rank_update",
description: `Updated rank #${ctx.data.id}`,
targetType: "rank",
targetId: ctx.data.id,
});
await rcon.send("updatepermissions");
revalidateTag("permissions", { expire: 0 });
return actionOk();
},
);
const setCmsPermsSchema = z.object({
roleId: z.coerce.number().int().positive(),
permissionSlugs: z.array(z.string().trim().min(1)).max(500),
});
export const setCmsPermissions = adminAction(
{ schema: setCmsPermsSchema, permission: PERMS.PERMISSIONS_MANAGE },
async (ctx) => {
const [role] = await db
.select({ id: AclRole.id, slug: AclRole.slug })
.from(AclRole)
.where(eq(AclRole.id, ctx.data.roleId))
.limit(1);
if (!role) throw new ActionError("Role not found");
const permissions = await db
.select({ id: AclPermission.id })
.from(AclPermission)
.where(inArray(AclPermission.slug, ctx.data.permissionSlugs));
await db.transaction(async (tx) => {
await tx
.delete(AclModelPermission)
.where(
and(
eq(AclModelPermission.modelId, role.id),
eq(AclModelPermission.modelType, "Role"),
),
);
if (permissions.length) {
await tx.insert(AclModelPermission).values(
permissions.map((permission) => ({
modelId: role.id,
modelType: "Role",
permissionId: permission.id,
})),
);
}
});
await logStaffActivity({
staffId: ctx.session.user.id,
action: "acl_role_permissions_update",
description: `Updated ${permissions.length} permissions for ${role.slug}`,
targetType: "acl_role",
targetId: role.id,
});
revalidateTag("permissions", { expire: 0 });
return actionOk();
},
);
/**
* Re-apply the same grant repair as migration 0018:
* - ranks with admin.dashboard get all admin.*
* - ranks >= 6 get admin.*.view + dashboard
* - ranks >= 7 get edit/manage/execute tools used by the sidebar
*/
export const repairAdminNavAclGrants = adminAction(
{ permission: PERMS.PERMISSIONS_MANAGE },
async (ctx) => {
const [dashboardFillResult] = await db.execute(sql`
INSERT INTO \`acl_model_permissions\` (\`model_type\`, \`model_id\`, \`permission_id\`)
SELECT 'Role', ar.id, ap.id
FROM \`acl_roles\` ar
JOIN \`acl_permissions\` ap ON ap.slug LIKE 'admin.%'
WHERE EXISTS (
SELECT 1
FROM \`acl_model_permissions\` amp
JOIN \`acl_permissions\` apdash ON apdash.id = amp.permission_id
WHERE amp.model_type = 'Role'
AND amp.model_id = ar.id
AND apdash.slug = 'admin.dashboard'
)
AND NOT EXISTS (
SELECT 1
FROM \`acl_model_permissions\` amp2
WHERE amp2.model_type = 'Role'
AND amp2.model_id = ar.id
AND amp2.permission_id = ap.id
)
`);
const [midRankViewsResult] = await db.execute(sql`
INSERT INTO \`acl_model_permissions\` (\`model_type\`, \`model_id\`, \`permission_id\`)
SELECT 'Role', ar.id, ap.id
FROM \`permission_ranks\` pr
JOIN \`acl_roles\` ar ON ar.slug = CONCAT('rank_', pr.id)
JOIN \`acl_permissions\` ap ON (
ap.slug = 'admin.dashboard'
OR (ap.slug LIKE 'admin.%' AND ap.slug LIKE '%.view')
)
WHERE pr.id >= 6
AND NOT EXISTS (
SELECT 1
FROM \`acl_model_permissions\` amp
WHERE amp.model_type = 'Role'
AND amp.model_id = ar.id
AND amp.permission_id = ap.id
)
`);
const [highRankToolsResult] = await db.execute(sql`
INSERT INTO \`acl_model_permissions\` (\`model_type\`, \`model_id\`, \`permission_id\`)
SELECT 'Role', ar.id, ap.id
FROM \`permission_ranks\` pr
JOIN \`acl_roles\` ar ON ar.slug = CONCAT('rank_', pr.id)
JOIN \`acl_permissions\` ap ON (
(ap.slug LIKE 'admin.%' AND ap.slug LIKE '%.edit')
OR ap.slug IN (
'admin.permissions.manage',
'admin.rcon.execute',
'admin.assets.import',
'admin.export',
'admin.analytics.export',
'admin.users.ban',
'admin.users.reset_password',
'admin.room.delete'
)
)
WHERE pr.id >= 7
AND NOT EXISTS (
SELECT 1
FROM \`acl_model_permissions\` amp
WHERE amp.model_type = 'Role'
AND amp.model_id = ar.id
AND amp.permission_id = ap.id
)
`);
const inserted =
Number((dashboardFillResult as ResultSetHeader).affectedRows) +
Number((midRankViewsResult as ResultSetHeader).affectedRows) +
Number((highRankToolsResult as ResultSetHeader).affectedRows);
await logStaffActivity({
staffId: ctx.session.user.id,
action: "acl_nav_grants_repair",
description: `Repaired admin nav ACL grants (${inserted} rows inserted)`,
targetType: "acl",
targetId: 0,
});
revalidateTag("permissions", { expire: 0 });
return actionOk({ inserted });
},
);