Files
EpicNext-Cms/src/lib/admin/guard.ts
T
SimoandCursor 084cca6ea4
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 1m31s
feat(mod): lite /mod panel + clarify ACL vs housekeeping legacy
Co-authored-by: Cursor <[email protected]>
2026-07-22 21:29:32 +02:00

93 lines
3.0 KiB
TypeScript

import { isStaff } from "@/lib/admin/is-staff";
import { redirectSafe } from "@/lib/foundation/security";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { clientIp, rateLimit } from "@/lib/rate-limit";
export { isStaff };
export interface StaffUser {
id: number;
rank: number;
username: string;
}
export async function requireStaff(): Promise<StaffUser> {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.ADMIN_DASHBOARD, session.user.rank))
redirectSafe("/", "/");
return {
id: session.user.id,
rank: session.user.rank,
username: session.user.username,
};
}
/**
* 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> {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.ADMIN_DASHBOARD, session.user.rank))
redirectSafe("/", "/");
if (!canAccess(permissions, permission, session.user.rank))
redirectSafe("/admin", "/admin");
return {
id: session.user.id,
rank: session.user.rank,
username: session.user.username,
};
}
export async function requirePermissionRateLimited(
permission: string,
): Promise<StaffUser> {
const staff = await requirePermission(permission);
const ip = await clientIp();
if (!(await rateLimit(`admin:${staff.id}:${ip}`, 30, 60_000)).ok)
redirectSafe("/admin?error=ratelimit", "/admin");
return staff;
}
export async function requireStaffRateLimited(): Promise<StaffUser> {
const staff = await requireStaff();
const ip = await clientIp();
if (!(await rateLimit(`admin:${staff.id}:${ip}`, 30, 60_000)).ok)
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;
}