refactor(api): extract assistant user admin slice

This commit is contained in:
2026-03-30 21:33:49 +02:00
parent 7d3c6d978e
commit fec4aa2e23
3 changed files with 459 additions and 389 deletions
@@ -0,0 +1,446 @@
import { SystemRole } from "@capakraken/shared";
import type { TRPCContext } from "../../trpc.js";
import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js";
type AssistantToolErrorResult = { error: string };
type UserRecord = {
id: string;
name: string | null;
systemRole?: SystemRole | string;
};
type UserAdminDeps = {
createUserCaller: (ctx: TRPCContext) => {
list: () => Promise<unknown[]>;
create: (params: {
email: string;
name: string;
systemRole?: SystemRole;
password: string;
}) => Promise<UserRecord>;
setPassword: (params: { userId: string; password: string }) => Promise<Record<string, unknown>>;
updateRole: (params: { id: string; systemRole: SystemRole }) => Promise<UserRecord>;
updateName: (params: { id: string; name: string }) => Promise<UserRecord>;
linkResource: (
params: { userId: string; resourceId: string | null },
) => Promise<Record<string, unknown>>;
autoLinkAllByEmail: () => Promise<Record<string, unknown> & { linked: number }>;
setPermissions: (params: {
userId: string;
overrides: {
granted?: string[];
denied?: string[];
chapterIds?: string[];
} | null;
}) => Promise<UserRecord>;
resetPermissions: (params: { userId: string }) => Promise<UserRecord>;
getEffectivePermissions: (params: { userId: string }) => Promise<unknown>;
disableTotp: (params: { userId: string }) => Promise<Record<string, unknown>>;
};
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
toAssistantUserMutationError: (
error: unknown,
mode?: "create" | "password",
) => AssistantToolErrorResult | null;
toAssistantUserResourceLinkError: (
error: unknown,
) => AssistantToolErrorResult | null;
};
export const userAdminToolDefinitions: ToolDef[] = [
{
type: "function",
function: {
name: "list_users",
description: "List all system users via the admin user router, including role and MFA state. Admin role required.",
parameters: {
type: "object",
properties: {
limit: { type: "integer", description: "Max results. Default: 50" },
},
},
},
},
{
type: "function",
function: {
name: "create_user",
description: "Create a new system user and auto-link a matching resource by email when possible. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
email: { type: "string", description: "User email address." },
name: { type: "string", description: "Display name." },
systemRole: { type: "string", enum: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"], description: "Initial system role." },
password: { type: "string", description: "Initial password, minimum 8 characters." },
},
required: ["email", "name", "password"],
},
},
},
{
type: "function",
function: {
name: "set_user_password",
description: "Reset a user's password. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "User ID." },
password: { type: "string", description: "New password, minimum 8 characters." },
},
required: ["userId", "password"],
},
},
},
{
type: "function",
function: {
name: "update_user_role",
description: "Change a user's system role. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "User ID." },
systemRole: { type: "string", enum: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"] },
},
required: ["id", "systemRole"],
},
},
},
{
type: "function",
function: {
name: "update_user_name",
description: "Rename a user. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "User ID." },
name: { type: "string", description: "New display name." },
},
required: ["id", "name"],
},
},
},
{
type: "function",
function: {
name: "link_user_resource",
description: "Link or unlink a user to a resource. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "User ID." },
resourceId: { type: ["string", "null"], description: "Resource ID or null to unlink." },
},
required: ["userId"],
},
},
},
{
type: "function",
function: {
name: "auto_link_users_by_email",
description: "Auto-link all users without a resource to matching resources by email. Admin role required. Always confirm first.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "set_user_permissions",
description: "Set explicit permission overrides for a user. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "User ID." },
overrides: {
type: ["object", "null"],
properties: {
granted: { type: "array", items: { type: "string" } },
denied: { type: "array", items: { type: "string" } },
chapterIds: { type: "array", items: { type: "string" } },
},
description: "Permission override object or null to clear.",
},
},
required: ["userId"],
},
},
},
{
type: "function",
function: {
name: "reset_user_permissions",
description: "Reset a user's permission overrides back to role defaults. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "User ID." },
},
required: ["userId"],
},
},
},
{
type: "function",
function: {
name: "get_effective_user_permissions",
description: "Get a user's resolved permissions, role, and explicit overrides. Admin role required.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "User ID." },
},
required: ["userId"],
},
},
},
{
type: "function",
function: {
name: "disable_user_totp",
description: "Disable MFA TOTP for a user as an admin override. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "User ID." },
},
required: ["userId"],
},
},
},
];
export function createUserAdminExecutors(
deps: UserAdminDeps,
): Record<string, ToolExecutor> {
return {
async list_users(params: { limit?: number }, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
const users = await caller.list();
return users.slice(0, Math.min(params.limit ?? 50, 100));
},
async create_user(params: {
email: string;
name: string;
systemRole?: SystemRole;
password: string;
}, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
let user;
try {
user = await caller.create({
email: params.email,
name: params.name,
password: params.password,
...(params.systemRole !== undefined ? { systemRole: params.systemRole } : {}),
});
} catch (error) {
const mapped = deps.toAssistantUserMutationError(error, "create");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate" as const,
scope: ["user", "resource"],
success: true,
user,
userId: user.id,
message: `Created user ${user.name}.`,
};
},
async set_user_password(params: { userId: string; password: string }, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
let result;
try {
result = await caller.setPassword(params);
} catch (error) {
const mapped = deps.toAssistantUserMutationError(error, "password");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate" as const,
scope: ["user"],
...result,
message: `Reset password for user ${params.userId}.`,
};
},
async update_user_role(params: { id: string; systemRole: SystemRole }, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
let user;
try {
user = await caller.updateRole(params);
} catch (error) {
const mapped = deps.toAssistantUserMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate" as const,
scope: ["user"],
success: true,
user,
userId: user.id,
message: `Updated role for ${user.name} to ${user.systemRole}.`,
};
},
async update_user_name(params: { id: string; name: string }, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
let user;
try {
user = await caller.updateName(params);
} catch (error) {
const mapped = deps.toAssistantUserMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate" as const,
scope: ["user"],
success: true,
user,
userId: user.id,
message: `Updated user name to ${user.name}.`,
};
},
async link_user_resource(params: { userId: string; resourceId?: string | null }, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
let result;
try {
result = await caller.linkResource({
userId: params.userId,
resourceId: params.resourceId ?? null,
});
} catch (error) {
const mapped = deps.toAssistantUserResourceLinkError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate" as const,
scope: ["user", "resource"],
...result,
message: params.resourceId ? "Linked user to resource." : "Unlinked user resource.",
};
},
async auto_link_users_by_email(_params: Record<string, never>, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
const result = await caller.autoLinkAllByEmail();
return {
__action: "invalidate" as const,
scope: ["user", "resource"],
success: true,
...result,
message: `Auto-linked ${result.linked} user(s) by email.`,
};
},
async set_user_permissions(params: {
userId: string;
overrides?: {
granted?: string[];
denied?: string[];
chapterIds?: string[];
} | null;
}, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
let user;
try {
user = await caller.setPermissions({
userId: params.userId,
overrides: params.overrides ?? null,
});
} catch (error) {
const mapped = deps.toAssistantUserMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate" as const,
scope: ["user"],
success: true,
user,
userId: user.id,
message: params.overrides ? "Updated user permission overrides." : "Cleared user permission overrides.",
};
},
async reset_user_permissions(params: { userId: string }, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
let user;
try {
user = await caller.resetPermissions(params);
} catch (error) {
const mapped = deps.toAssistantUserMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate" as const,
scope: ["user"],
success: true,
user,
userId: user.id,
message: "Reset user permissions to role defaults.",
};
},
async get_effective_user_permissions(params: { userId: string }, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
try {
return await caller.getEffectivePermissions(params);
} catch (error) {
const mapped = deps.toAssistantUserMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
},
async disable_user_totp(params: { userId: string }, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
let result;
try {
result = await caller.disableTotp(params);
} catch (error) {
const mapped = deps.toAssistantUserMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate" as const,
scope: ["user"],
success: true,
...result,
message: `Disabled TOTP for user ${params.userId}.`,
};
},
};
}