feat(mod): lite /mod panel + clarify ACL vs housekeeping legacy
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 1m31s

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-22 21:29:32 +02:00
1 parent a384c9a8bb
commit 084cca6ea4
18 files changed
+931 -77

No files matched your search

+35 -1
View File
@@ -26,7 +26,9 @@ export async function requireStaff(): Promise<StaffUser> {
* Staff gate plus a specific ACL slug. Use for FormData server actions that
* must not stop at admin.dashboard alone (RCON, user edits, settings writes).
*/
export async function requirePermission(permission: string): Promise<StaffUser> {
export async function requirePermission(
permission: string,
): Promise<StaffUser> {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.ADMIN_DASHBOARD, session.user.rank))
redirectSafe("/", "/");
@@ -56,3 +58,35 @@ export async function requireStaffRateLimited(): Promise<StaffUser> {
redirectSafe("/admin?error=ratelimit", "/admin");
return staff;
}
/**
* Mod-lite gate: needs any mod.* / moderation ACL, not admin.dashboard.
* Use for `/mod` layout so mid-ranks can access without full housekeeping.
*/
export async function requireMod(): Promise<StaffUser> {
const { session, permissions } = await getAdminContext();
const ok =
canAccess(permissions, PERMS.MOD_DASHBOARD, session.user.rank) ||
canAccess(permissions, PERMS.MODERATION_VIEW, session.user.rank) ||
canAccess(permissions, PERMS.MOD_CFH_VIEW, session.user.rank) ||
canAccess(permissions, PERMS.MOD_ACTIONS, session.user.rank) ||
canAccess(permissions, PERMS.MOD_BANS_VIEW, session.user.rank);
if (!ok) redirectSafe("/", "/");
return {
id: session.user.id,
rank: session.user.rank,
username: session.user.username,
};
}
/** Mod-lite page gate: any-of permission list (no admin.dashboard required). */
export async function requireModPermission(
permission: string | readonly string[],
): Promise<StaffUser> {
const staff = await requireMod();
const { permissions } = await getAdminContext();
const needed = Array.isArray(permission) ? permission : [permission];
const ok = needed.some((slug) => canAccess(permissions, slug, staff.rank));
if (!ok) redirectSafe("/mod", "/mod");
return staff;
}
+10 -9
View File
@@ -43,7 +43,8 @@ function fail(
export { fail as actionError, ok as actionOk };
interface AdminOpts<TSchema extends z.ZodType | undefined> {
permission?: string;
/** Single slug, or any-of list (e.g. admin.moderation.edit OR mod.actions). */
permission?: string | readonly string[];
schema?: TSchema;
rateLimitKey?: string;
rateLimitMax?: number;
@@ -75,19 +76,19 @@ export function adminAction<TSchema extends z.ZodType | undefined = undefined>(
setContextUserId(Number(apiCtx.session.user.id) as never);
if (opts.permission) {
if (
!canAccess(
apiCtx.permissions,
opts.permission,
apiCtx.session.user.rank,
)
) {
const needed = Array.isArray(opts.permission)
? opts.permission
: [opts.permission];
const allowed = needed.some((slug) =>
canAccess(apiCtx.permissions, slug, apiCtx.session.user.rank),
);
if (!allowed) {
await logAuthorizationEvent({
kind: "permission.denied",
userId: Number(apiCtx.session.user.id),
username: apiCtx.session.user.name ?? undefined,
rank: apiCtx.session.user.rank,
permission: opts.permission,
permission: needed.join("|"),
source: "adminAction",
reason: "Permission check denied",
});