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
+2 -1
View File
@@ -31,13 +31,14 @@
- the adjacent chargeability/computation read helpers now live in their own domain module, keeping the advanced financial transparency read models out of the monolithic router without changing the assistant contract
- the neighboring country and metro-city admin mutations now live in their own domain module, keeping more settings-side CRUD wiring out of the monolithic router without changing the assistant contract
- the adjacent management-level, utilization, calculation-rule, effort-rule, and experience-multiplier read helpers now live in their own domain module, further shrinking the monolithic assistant router without changing the assistant contract
- the remaining assistant user-admin helper cluster now lives in its own domain module, covering admin listing, user lifecycle mutations, permission overrides, resource linking, and MFA overrides without changing the assistant contract
- the authenticated user self-service assistant helpers now live in their own domain module, covering assignable users, dashboard preferences, favorites, column preferences, and MFA self-service without changing the assistant contract
## Next Up
Pin the next structural cleanup on the API side:
continue splitting `packages/api/src/router/assistant-tools.ts` into domain-oriented tool modules without changing the public tool contract.
The next clean slice should stay adjacent to the extracted domains and target one cohesive block such as the remaining admin-user helper cluster around `list_users` and user mutations, or the embedded estimate and project admin helper clusters that are still in the monolithic router.
The next clean slice should stay adjacent to the extracted domains and target one cohesive block such as the embedded notification/task helpers, or the remaining estimate and project admin helper clusters that are still in the monolithic router.
## Remaining Major Themes
+11 -388
View File
@@ -99,6 +99,10 @@ import {
createUserSelfServiceExecutors,
userSelfServiceToolDefinitions,
} from "./assistant-tools/user-self-service.js";
import {
createUserAdminExecutors,
userAdminToolDefinitions,
} from "./assistant-tools/user-admin.js";
import type { ToolContext, ToolDef, ToolExecutor } from "./assistant-tools/shared.js";
import { getCommentToolEntityDescription, getCommentToolScopeSentence } from "../lib/comment-entity-registry.js";
@@ -2785,170 +2789,8 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
},
...countryMetroAdminToolDefinitions,
...configReadmodelToolDefinitions,
{
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" },
},
},
},
},
...userAdminToolDefinitions,
...userSelfServiceToolDefinitions,
{
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"],
},
},
},
{
type: "function",
function: {
@@ -5335,237 +5177,18 @@ const executors = {
createExperienceMultiplierCaller,
createScopedCallerContext,
}),
async list_users(params: { limit?: number }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const users = await caller.list();
return users.slice(0, Math.min(params.limit ?? 50, 100));
},
...createUserAdminExecutors({
createUserCaller,
createScopedCallerContext,
toAssistantUserMutationError,
toAssistantUserResourceLinkError,
}),
...createUserSelfServiceExecutors({
createUserCaller,
createScopedCallerContext,
toAssistantTotpEnableError,
}),
async create_user(params: {
email: string;
name: string;
systemRole?: SystemRole;
password: string;
}, ctx: ToolContext) {
const caller = createUserCaller(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 = toAssistantUserMutationError(error, "create");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
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 = createUserCaller(createScopedCallerContext(ctx));
let result;
try {
result = await caller.setPassword(params);
} catch (error) {
const mapped = toAssistantUserMutationError(error, "password");
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["user"],
...result,
message: `Reset password for user ${params.userId}.`,
};
},
async update_user_role(params: { id: string; systemRole: SystemRole }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
let user;
try {
user = await caller.updateRole(params);
} catch (error) {
const mapped = toAssistantUserMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
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 = createUserCaller(createScopedCallerContext(ctx));
let user;
try {
user = await caller.updateName(params);
} catch (error) {
const mapped = toAssistantUserMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
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 = createUserCaller(createScopedCallerContext(ctx));
let result;
try {
result = await caller.linkResource({
userId: params.userId,
resourceId: params.resourceId ?? null,
});
} catch (error) {
const mapped = toAssistantUserResourceLinkError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
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 = createUserCaller(createScopedCallerContext(ctx));
const result = await caller.autoLinkAllByEmail();
return {
__action: "invalidate",
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 = createUserCaller(createScopedCallerContext(ctx));
let user;
try {
user = await caller.setPermissions({
userId: params.userId,
overrides: params.overrides ?? null,
});
} catch (error) {
const mapped = toAssistantUserMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
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 = createUserCaller(createScopedCallerContext(ctx));
let user;
try {
user = await caller.resetPermissions(params);
} catch (error) {
const mapped = toAssistantUserMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
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 = createUserCaller(createScopedCallerContext(ctx));
try {
return await caller.getEffectivePermissions(params);
} catch (error) {
const mapped = toAssistantUserMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
},
async disable_user_totp(params: { userId: string }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
let result;
try {
result = await caller.disableTotp(params);
} catch (error) {
const mapped = toAssistantUserMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["user"],
success: true,
...result,
message: `Disabled TOTP for user ${params.userId}.`,
};
},
async list_notifications(params: { unreadOnly?: boolean; limit?: number }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
return caller.list({
@@ -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}.`,
};
},
};
}