fix: complete acl management

This commit is contained in:
Simo committed 2026-07-12 19:12:24 +02:00
1 parent 667ec9af4c
commit 4b596226e0
6 files changed
+187 -75

No files matched your search

+95 -64
View File
@@ -1,71 +1,102 @@
"use server";
'use server'
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { prisma } from "@/lib/prisma";
import { logStaffActivity } from "@/lib/services/staff-activity";
import { revalidateTag } from 'next/cache'
import { z } from 'zod'
import { PERMS } from '@/lib/permission-slugs'
import { prisma } from '@/lib/prisma'
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'
export async function saveRank({ id, fields }: { id: number; fields: Record<string, string> }) {
const staff = await requireStaff();
const setClauses = Object.entries(fields)
.map(([key]) => `\`${key}\` = ?`)
.join(", ");
const values = Object.values(fields);
await prisma.$executeRawUnsafe(
`UPDATE permissions SET ${setClauses} WHERE id = ?`,
...values,
id,
);
await logStaffActivity({
staffId: staff.id,
action: "rank_update",
description: `Updated rank #${id}`,
targetType: "rank",
targetId: id,
});
}
const createRankSchema = z.object({
rank_name: z.string().trim().min(1).max(25),
level: z.coerce.number().int().min(1),
})
export async function setCmsPermissions({ roleId, permissionSlugs }: { roleId: number; permissionSlugs: string[] }) {
await requireStaff();
const existing = await prisma.websiteHousekeepingPermissions.findMany({
where: { minRank: roleId },
select: { permission: true },
});
const existingSlugs = new Set(existing.map((p) => p.permission));
const toAdd = permissionSlugs.filter((s) => !existingSlugs.has(s));
const toRemove = existing.filter((p) => !permissionSlugs.includes(p.permission));
export const createRank = adminAction(
{ schema: createRankSchema, permission: PERMS.PERMISSIONS_MANAGE },
async (ctx) => {
const id = await createEmulatorRank(prisma, ctx.data)
await prisma.aclRole.upsert({
where: { slug: `rank_${id}` },
create: { slug: `rank_${id}`, title: ctx.data.rank_name, description: 'CMS role synchronized from permission_ranks' },
update: { 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 })
},
)
for (const slug of toAdd) {
await prisma.websiteHousekeepingPermissions.create({
data: { permission: slug, minRank: roleId, description: null },
});
}
for (const p of toRemove) {
await prisma.websiteHousekeepingPermissions.deleteMany({
where: { permission: p.permission, minRank: roleId },
});
}
const deleteRankSchema = z.object({ id: z.coerce.number().int().positive() })
return { ok: true as const };
}
export const deleteRank = adminAction(
{ schema: deleteRankSchema, permission: PERMS.PERMISSIONS_MANAGE },
async (ctx) => {
const users = await prisma.user.count({ where: { rank: ctx.data.id } })
if (users > 0) throw new ActionError(`Cannot delete: ${users} users have this rank`)
const role = await prisma.aclRole.findFirst({ where: { slug: `rank_${ctx.data.id}` } })
await deleteEmulatorRank(prisma, ctx.data.id)
if (role) {
await prisma.$transaction([
prisma.aclModelPermission.deleteMany({ where: { modelId: role.id, modelType: 'Role' } }),
prisma.aclModelRole.deleteMany({ where: { roleId: role.id } }),
prisma.aclRole.delete({ where: { 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()
},
)
export async function createRank({ rank_name, level }: { rank_name: string; level: number }) {
const staff = await requireStaff();
const created = await prisma.websiteTeams.create({
data: {
rankName: rank_name,
hiddenRank: false,
badge: null,
jobDescription: null,
staffColor: "#ffffff",
staffBackground: "#000000",
},
});
return { ok: true as const, data: { id: Number(created.id) } };
}
const saveRankSchema = z.object({
id: z.coerce.number().int().positive(),
fields: z.record(z.string(), z.union([z.string(), z.number()])),
})
export async function deleteRank({ id }: { id: number }) {
await requireStaff();
await prisma.websiteTeams.delete({ where: { id: BigInt(id) } });
return { ok: true as const };
}
export const saveRank = adminAction(
{ schema: saveRankSchema, permission: PERMS.PERMISSIONS_MANAGE },
async (ctx) => {
await updateEmulatorRank(prisma, ctx.data.id, ctx.data.fields)
if (typeof ctx.data.fields.rank_name === 'string') {
await prisma.aclRole.updateMany({ where: { slug: `rank_${ctx.data.id}` }, data: { title: ctx.data.fields.rank_name } })
}
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 prisma.aclRole.findUnique({ where: { id: ctx.data.roleId }, select: { id: true, slug: true } })
if (!role) throw new ActionError('Role not found')
const permissions = await prisma.aclPermission.findMany({
where: { slug: { in: ctx.data.permissionSlugs } },
select: { id: true },
})
await prisma.$transaction(async (tx) => {
await tx.aclModelPermission.deleteMany({ where: { modelId: role.id, modelType: 'Role' } })
if (permissions.length) {
await tx.aclModelPermission.createMany({
data: 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()
},
)