fix: complete acl management
This commit is contained in:
1 parent
667ec9af4c
commit
4b596226e0
6 files changed
+187
-75
No files matched your search
@@ -0,0 +1,35 @@
|
|||||||
|
UPDATE `acl_model_permissions` SET `model_type` = 'Role' WHERE LOWER(`model_type`) = 'role';
|
||||||
|
UPDATE `acl_model_roles` SET `model_type` = 'User' WHERE LOWER(`model_type`) = 'user';
|
||||||
|
|
||||||
|
INSERT INTO `acl_permissions` (`slug`, `title`) VALUES
|
||||||
|
('admin.assets.import', 'Import assets'),
|
||||||
|
('admin.permissions.manage', 'Manage permissions')
|
||||||
|
ON DUPLICATE KEY UPDATE `title` = VALUES(`title`);
|
||||||
|
|
||||||
|
INSERT INTO `acl_roles` (`slug`, `title`, `description`)
|
||||||
|
SELECT CONCAT('rank_', pr.id), COALESCE(NULLIF(pr.rank_name, ''), CONCAT('Rank ', pr.id)), 'CMS role synchronized from permission_ranks'
|
||||||
|
FROM `permission_ranks` pr
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `acl_roles` ar WHERE ar.slug = CONCAT('rank_', pr.id));
|
||||||
|
|
||||||
|
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)
|
||||||
|
CROSS JOIN `acl_permissions` ap
|
||||||
|
WHERE pr.id = (SELECT MAX(id) FROM `permission_ranks`)
|
||||||
|
AND ap.slug IN ('admin.permissions.manage', 'admin.assets.import')
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `acl_model_permissions` (`model_type`, `model_id`, `permission_id`)
|
||||||
|
SELECT 'Role', ar.id, ap.id
|
||||||
|
FROM `website_housekeeping_permissions` whp
|
||||||
|
JOIN `acl_permissions` ap ON ap.slug = whp.permission
|
||||||
|
JOIN `permission_ranks` pr ON pr.id >= whp.min_rank
|
||||||
|
JOIN `acl_roles` ar ON ar.slug = CONCAT('rank_', pr.id)
|
||||||
|
WHERE 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
|
||||||
|
);
|
||||||
+95
-64
@@ -1,71 +1,102 @@
|
|||||||
"use server";
|
'use server'
|
||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidateTag } from 'next/cache'
|
||||||
import { requireStaff } from "@/lib/admin/guard";
|
import { z } from 'zod'
|
||||||
import { prisma } from "@/lib/prisma";
|
import { PERMS } from '@/lib/permission-slugs'
|
||||||
import { logStaffActivity } from "@/lib/services/staff-activity";
|
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 createRankSchema = z.object({
|
||||||
const staff = await requireStaff();
|
rank_name: z.string().trim().min(1).max(25),
|
||||||
const setClauses = Object.entries(fields)
|
level: z.coerce.number().int().min(1),
|
||||||
.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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setCmsPermissions({ roleId, permissionSlugs }: { roleId: number; permissionSlugs: string[] }) {
|
export const createRank = adminAction(
|
||||||
await requireStaff();
|
{ schema: createRankSchema, permission: PERMS.PERMISSIONS_MANAGE },
|
||||||
const existing = await prisma.websiteHousekeepingPermissions.findMany({
|
async (ctx) => {
|
||||||
where: { minRank: roleId },
|
const id = await createEmulatorRank(prisma, ctx.data)
|
||||||
select: { permission: true },
|
await prisma.aclRole.upsert({
|
||||||
});
|
where: { slug: `rank_${id}` },
|
||||||
const existingSlugs = new Set(existing.map((p) => p.permission));
|
create: { slug: `rank_${id}`, title: ctx.data.rank_name, description: 'CMS role synchronized from permission_ranks' },
|
||||||
const toAdd = permissionSlugs.filter((s) => !existingSlugs.has(s));
|
update: { title: ctx.data.rank_name },
|
||||||
const toRemove = existing.filter((p) => !permissionSlugs.includes(p.permission));
|
})
|
||||||
|
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) {
|
const deleteRankSchema = z.object({ id: z.coerce.number().int().positive() })
|
||||||
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 },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 saveRankSchema = z.object({
|
||||||
const staff = await requireStaff();
|
id: z.coerce.number().int().positive(),
|
||||||
const created = await prisma.websiteTeams.create({
|
fields: z.record(z.string(), z.union([z.string(), z.number()])),
|
||||||
data: {
|
})
|
||||||
rankName: rank_name,
|
|
||||||
hiddenRank: false,
|
|
||||||
badge: null,
|
|
||||||
jobDescription: null,
|
|
||||||
staffColor: "#ffffff",
|
|
||||||
staffBackground: "#000000",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return { ok: true as const, data: { id: Number(created.id) } };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteRank({ id }: { id: number }) {
|
export const saveRank = adminAction(
|
||||||
await requireStaff();
|
{ schema: saveRankSchema, permission: PERMS.PERMISSIONS_MANAGE },
|
||||||
await prisma.websiteTeams.delete({ where: { id: BigInt(id) } });
|
async (ctx) => {
|
||||||
return { ok: true as const };
|
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()
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -20,17 +20,6 @@ export default async function PermissionsPage(_props: { params: Promise<{ locale
|
|||||||
countMap[Number(row.rank)] = Number(row.cnt)
|
countMap[Number(row.rank)] = Number(row.cnt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-create CMS roles for ranks >= 4
|
|
||||||
for (const rank of ranks) {
|
|
||||||
if (rank.id >= 4) {
|
|
||||||
await prisma.aclRole.upsert({
|
|
||||||
where: { slug: `rank_${rank.id}` },
|
|
||||||
create: { slug: `rank_${rank.id}`, title: rank.rank_name || `Rank ${rank.id}` },
|
|
||||||
update: {},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ranksWithCounts = ranks.map((rank) => ({
|
const ranksWithCounts = ranks.map((rank) => ({
|
||||||
...rank,
|
...rank,
|
||||||
userCount: countMap[rank.id] ?? 0,
|
userCount: countMap[rank.id] ?? 0,
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { revalidateTag } from 'next/cache'
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { withAdmin } from '@/lib/api-handler'
|
||||||
|
import { apiError } from '@/lib/api-response'
|
||||||
|
import { PERMS } from '@/lib/permissions'
|
||||||
|
import { prisma } from '@/lib/prisma'
|
||||||
|
|
||||||
|
export const POST = withAdmin({ permission: PERMS.PERMISSIONS_MANAGE }, async (request) => {
|
||||||
|
const body = await request.json()
|
||||||
|
const userId = Number(body.userId)
|
||||||
|
const roleId = Number(body.roleId)
|
||||||
|
if (!Number.isInteger(userId) || userId <= 0 || !Number.isInteger(roleId) || roleId <= 0) {
|
||||||
|
return apiError('Valid userId and roleId required', 400)
|
||||||
|
}
|
||||||
|
if (body.action === 'remove') {
|
||||||
|
await prisma.aclModelRole.deleteMany({ where: { modelType: 'User', modelId: userId, roleId } })
|
||||||
|
} else {
|
||||||
|
const existing = await prisma.aclModelRole.findFirst({ where: { modelType: 'User', modelId: userId, roleId } })
|
||||||
|
if (!existing) await prisma.aclModelRole.create({ data: { modelType: 'User', modelId: userId, roleId } })
|
||||||
|
}
|
||||||
|
revalidateTag('permissions', { expire: 0 })
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
})
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { revalidateTag } from 'next/cache'
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { withAdmin } from '@/lib/api-handler'
|
||||||
|
import { apiError } from '@/lib/api-response'
|
||||||
|
import { PERMS } from '@/lib/permissions'
|
||||||
|
import { prisma } from '@/lib/prisma'
|
||||||
|
|
||||||
|
export const POST = withAdmin({ permission: PERMS.PERMISSIONS_MANAGE }, async (request) => {
|
||||||
|
const body = await request.json()
|
||||||
|
const slug = String(body.slug ?? '').trim().toLowerCase()
|
||||||
|
const title = String(body.title ?? '').trim()
|
||||||
|
if (!/^[a-z0-9._-]{2,64}$/.test(slug) || !title) return apiError('Valid slug and title required', 400)
|
||||||
|
const role = await prisma.aclRole.create({ data: { slug, title } })
|
||||||
|
revalidateTag('permissions', { expire: 0 })
|
||||||
|
return NextResponse.json({ role })
|
||||||
|
})
|
||||||
|
|
||||||
|
export const DELETE = withAdmin({ permission: PERMS.PERMISSIONS_MANAGE }, async (request) => {
|
||||||
|
const id = Number(request.nextUrl.searchParams.get('id'))
|
||||||
|
if (!Number.isInteger(id) || id <= 0) return apiError('Valid ID required', 400)
|
||||||
|
const role = await prisma.aclRole.findUnique({ where: { id }, select: { slug: true } })
|
||||||
|
if (!role) return apiError('Role not found', 404)
|
||||||
|
if (role.slug.startsWith('rank_')) return apiError('Rank roles must be managed through emulator ranks', 409)
|
||||||
|
await prisma.$transaction([
|
||||||
|
prisma.aclModelPermission.deleteMany({ where: { modelType: 'Role', modelId: id } }),
|
||||||
|
prisma.aclModelRole.deleteMany({ where: { roleId: id } }),
|
||||||
|
prisma.aclRole.delete({ where: { id } }),
|
||||||
|
])
|
||||||
|
revalidateTag('permissions', { expire: 0 })
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
})
|
||||||
@@ -110,6 +110,9 @@ export class RconClient {
|
|||||||
updateCatalog() {
|
updateCatalog() {
|
||||||
return this.send("updatecatalog");
|
return this.send("updatecatalog");
|
||||||
}
|
}
|
||||||
|
updateItems() {
|
||||||
|
return this.send("updateitems");
|
||||||
|
}
|
||||||
updateConfig() {
|
updateConfig() {
|
||||||
return this.send("updateconfig");
|
return this.send("updateconfig");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user