Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/services/workspaceRoles.server.ts
T

145 lines
5.9 KiB
TypeScript

import type { TFunction } from "i18next";
import { Params } from "react-router";
import { getTranslations } from "~/locale/i18next.server";
import { createAdminLog } from "~/utils/db/logs.db.server";
import { getAllPermissions } from "~/utils/db/permissions/permissions.db.server";
import { setRolePermissions } from "~/utils/db/permissions/rolePermissions.db.server";
import { createRole, deleteRole, getNextRolesOrder, getRole, getTenantAppRoleByName, updateRole } from "~/utils/db/permissions/roles.db.server";
import { verifyUserHasPermission } from "~/utils/helpers/.server/PermissionsService";
export type WorkspaceRoleMutationInput = {
name: string;
description: string;
assignToNewUsers?: boolean;
type?: "admin" | "app";
permissionNames: string[];
};
export type WorkspaceRoleAccessArgs = {
request: Request;
params: Params;
tenantId: string;
};
const WORKSPACE_ROLES_PERMISSION = "app.settings.roles.set";
// Flag-Gate (workspace-roles) ENTFERNT 2026-06-22: Tenant-eigene Rollen sind P2-Kernfunktion fuer ALLE
// Tenants -> nur noch die Permission `app.settings.roles.set` entscheidet (kein Feature-Flag-Kill-Switch
// mehr). Hintergrund: der eigentliche Mehrwert (Fach-Rechte zum Zuweisen) waechst erst mit den Modulen ab
// P6-P9; der Builder selbst soll aber jedem Tenant offenstehen.
export async function requireWorkspaceRolesBuilderAccess({ request, tenantId }: WorkspaceRoleAccessArgs) {
await verifyUserHasPermission(request, WORKSPACE_ROLES_PERMISSION, tenantId);
return { tenantId };
}
export async function createWorkspaceRole({ request, tenantId, data }: { request: Request; tenantId: string; data: WorkspaceRoleMutationInput }) {
const { t } = await getTranslations(request);
assertAppRoleInput(data.type, t);
const name = normalizeRoleName(data.name, t);
await assertUniqueRoleName({ name, tenantId, t });
await assertAppPermissionNames(data.permissionNames, t);
const role = await createRole({
order: await getNextRolesOrder("app", tenantId),
name,
description: data.description,
assignToNewUsers: data.assignToNewUsers ?? false,
type: "app",
isDefault: false,
tenantId,
});
await setRolePermissions(role.id, data.permissionNames);
await logWorkspaceRoleMutation(request, "Workspace Role Created", tenantId, role.id, role.name, data.permissionNames.length);
return role;
}
export async function updateWorkspaceRole({ request, tenantId, roleId, data }: { request: Request; tenantId: string; roleId: string; data: WorkspaceRoleMutationInput }) {
const { t } = await getTranslations(request);
assertAppRoleInput(data.type, t);
const existing = await getExistingMutableAppRole(roleId, tenantId, t);
const name = normalizeRoleName(data.name, t);
await assertUniqueRoleName({ name, tenantId, currentRoleId: roleId, t });
await assertAppPermissionNames(data.permissionNames, t);
const role = await updateRole(roleId, {
name,
description: data.description,
assignToNewUsers: data.assignToNewUsers ?? false,
type: "app",
});
await setRolePermissions(existing.id, data.permissionNames);
await logWorkspaceRoleMutation(request, "Workspace Role Updated", tenantId, role.id, role.name, data.permissionNames.length);
return role;
}
export async function deleteWorkspaceRole({ request, tenantId, roleId }: { request: Request; tenantId: string; roleId: string }) {
const { t } = await getTranslations(request);
const existing = await getExistingMutableAppRole(roleId, tenantId, t);
const role = await deleteRole(existing.id);
await logWorkspaceRoleMutation(request, "Workspace Role Deleted", tenantId, role.id, role.name, existing.permissions.length);
return role;
}
function assertAppRoleInput(type: "admin" | "app" | undefined, t: TFunction) {
if (type === "admin") {
throw new Response(t("settings.rolesAndPermissions.errors.appRolesOnly"), { status: 400 });
}
}
function normalizeRoleName(name: string, t: TFunction) {
const normalized = name.trim();
if (!normalized) {
throw new Response(t("settings.rolesAndPermissions.errors.roleNameRequired"), { status: 400 });
}
return normalized;
}
async function assertUniqueRoleName({ name, tenantId, currentRoleId, t }: { name: string; tenantId: string; currentRoleId?: string; t: TFunction }) {
const existing = await getTenantAppRoleByName(name, tenantId);
if (existing && existing.id !== currentRoleId) {
throw new Response(t("settings.rolesAndPermissions.errors.existingRole", { name }), { status: 400 });
}
}
async function assertAppPermissionNames(permissionNames: string[], t: TFunction) {
const appPermissions = await getAllPermissions("app");
const allowedNames = new Set(appPermissions.map((permission) => permission.name));
const invalid = permissionNames.filter((name) => !allowedNames.has(name));
if (invalid.length > 0) {
throw new Response(t("settings.rolesAndPermissions.errors.invalidAppPermissions", { permissions: invalid.join(", ") }), { status: 400 });
}
}
async function getExistingMutableAppRole(roleId: string, tenantId: string, t: TFunction) {
const existing = await getRole(roleId);
if (!existing) {
throw new Response(t("settings.rolesAndPermissions.errors.roleNotFound"), { status: 404 });
}
if (existing.type !== "app") {
throw new Response(t("settings.rolesAndPermissions.errors.appRolesOnly"), { status: 400 });
}
if (existing.isDefault) {
throw new Response(t("settings.rolesAndPermissions.errors.defaultRoleImmutable"), { status: 400 });
}
if (existing.tenantId !== tenantId) {
throw new Response(t("settings.rolesAndPermissions.errors.roleNotFound"), { status: 404 });
}
return existing;
}
async function logWorkspaceRoleMutation(request: Request, action: string, tenantId: string, roleId: string, roleName: string, permissionCount: number) {
await createAdminLog(
request,
action,
JSON.stringify({
tenantId,
roleId,
roleName,
permissionCount,
})
);
}