Files
EpicNext-Cms/src/actions/permissions.ts
T
Simo c325c53774
CI / check (pull_request) Successful in 33s
CI / release (pull_request) Skipped
CI / deploy (pull_request) Skipped
fix(housekeeping): harden system workflow boundaries
2026-08-29 00:25:47 +02:00

123 lines
3.7 KiB
TypeScript

"use server";
import { revalidateTag } from "next/cache";
import { z } from "zod";
import {
type SystemMutationContext,
type SystemMutationOperation,
systemMutationService,
} from "@/features/housekeeping/domains/system/services/mutations";
import { createHousekeepingCapabilityContext } from "@/features/housekeeping/foundation/capability-context";
import type { AdminActionContext } from "@/lib/foundation/types";
import { PERMS } from "@/lib/permission-slugs";
import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared";
function mutationContext(
ctx: Pick<AdminActionContext, "session" | "permissions" | "requestId">,
): SystemMutationContext {
return {
capability: createHousekeepingCapabilityContext(
{
id: Number(ctx.session.user.id),
rank: Number(ctx.session.user.rank),
username: ctx.session.user.username,
},
ctx.permissions,
),
correlationId: String(ctx.requestId),
};
}
async function runAccessMutation(
ctx: Pick<AdminActionContext, "session" | "permissions" | "requestId">,
operation: SystemMutationOperation,
input: unknown,
): Promise<unknown> {
const result = await systemMutationService.execute(
mutationContext(ctx),
operation,
input,
);
if (result.ok) return result.data;
if (result.error.messageKey === "errors.housekeeping.system.rankInUse") {
const users = Number(result.error.fieldErrors?.rank?.[0]);
if (Number.isInteger(users) && users > 0) {
throw new ActionError(`Cannot delete: ${users} users have this rank`);
}
}
if (result.error.messageKey === "errors.housekeeping.system.roleNotFound") {
throw new ActionError("Role not found");
}
throw new Error(result.error.messageKey);
}
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 result = (await runAccessMutation(ctx, "access.rank.create", {
name: ctx.data.rank_name,
level: ctx.data.level,
})) as { id: number };
revalidateTag("permissions", { expire: 0 });
return actionOk({ id: result.id });
},
);
const deleteRankSchema = z.object({ id: z.coerce.number().int().positive() });
export const deleteRank = adminAction(
{ schema: deleteRankSchema, permission: PERMS.PERMISSIONS_MANAGE },
async (ctx) => {
await runAccessMutation(ctx, "access.rank.delete", ctx.data);
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 runAccessMutation(ctx, "access.rank.update", ctx.data);
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) => {
await runAccessMutation(ctx, "access.permissions.update", ctx.data);
revalidateTag("permissions", { expire: 0 });
return actionOk();
},
);
export const repairAdminNavAclGrants = adminAction(
{ permission: PERMS.PERMISSIONS_MANAGE },
async (ctx) => {
const result = (await runAccessMutation(
ctx,
"access.permissions.repair",
{},
)) as { inserted: number };
revalidateTag("permissions", { expire: 0 });
return actionOk({ inserted: result.inserted });
},
);