test(api): harden assistant tool error handling
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { AllocationStatus, PermissionKey, SystemRole } from "@capakraken/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
vi.mock("@capakraken/application", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@capakraken/application")>();
|
||||
@@ -893,6 +894,75 @@ describe("assistant advanced tools and scoping", () => {
|
||||
expect(db.assignment.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns a stable conflict error for quick-assign timeline mutations", async () => {
|
||||
const db = {
|
||||
project: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
id: "project_1",
|
||||
name: "Gelddruckmaschine",
|
||||
shortCode: "GDM",
|
||||
status: "ACTIVE",
|
||||
responsiblePerson: null,
|
||||
}),
|
||||
},
|
||||
resource: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
id: "resource_1",
|
||||
eid: "E-001",
|
||||
displayName: "Alice",
|
||||
lcrCents: 5000,
|
||||
availability: {
|
||||
monday: 8,
|
||||
tuesday: 8,
|
||||
wednesday: 8,
|
||||
thursday: 8,
|
||||
friday: 8,
|
||||
saturday: 0,
|
||||
sunday: 0,
|
||||
},
|
||||
}),
|
||||
},
|
||||
assignment: {
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
create: vi.fn().mockRejectedValue(
|
||||
new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Resource is already assigned to this project with overlapping dates",
|
||||
}),
|
||||
),
|
||||
},
|
||||
vacation: {
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
auditLog: {
|
||||
create: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
$transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
|
||||
};
|
||||
const ctx = createToolContext(
|
||||
db,
|
||||
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
|
||||
SystemRole.MANAGER,
|
||||
);
|
||||
|
||||
const result = await executeTool(
|
||||
"quick_assign_timeline_resource",
|
||||
JSON.stringify({
|
||||
resourceIdentifier: "resource_1",
|
||||
projectIdentifier: "project_1",
|
||||
startDate: "2026-03-16",
|
||||
endDate: "2026-03-20",
|
||||
hoursPerDay: 8,
|
||||
}),
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(result.action).toBeUndefined();
|
||||
expect(JSON.parse(result.content)).toEqual({
|
||||
error: "Resource is already assigned to this project with overlapping dates",
|
||||
});
|
||||
});
|
||||
|
||||
it("batch quick-assigns timeline resources through the real timeline router mutation", async () => {
|
||||
const db = {
|
||||
project: {
|
||||
@@ -975,6 +1045,60 @@ describe("assistant advanced tools and scoping", () => {
|
||||
expect(db.assignment.create).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("returns a structured batch assignment resolver error for an unknown resource", async () => {
|
||||
const db = {
|
||||
project: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
id: "project_1",
|
||||
shortCode: "GDM",
|
||||
name: "Gelddruckmaschine",
|
||||
status: "ACTIVE",
|
||||
responsiblePerson: null,
|
||||
startDate: new Date("2026-03-16"),
|
||||
endDate: new Date("2026-03-20"),
|
||||
}),
|
||||
findFirst: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
resource: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
findFirst: vi.fn().mockResolvedValue(null),
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
assignment: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
};
|
||||
const ctx = createToolContext(
|
||||
db,
|
||||
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
|
||||
SystemRole.MANAGER,
|
||||
);
|
||||
|
||||
const result = await executeTool(
|
||||
"batch_quick_assign_timeline_resources",
|
||||
JSON.stringify({
|
||||
assignments: [
|
||||
{
|
||||
resourceIdentifier: "missing_resource",
|
||||
projectIdentifier: "project_1",
|
||||
startDate: "2026-03-16",
|
||||
endDate: "2026-03-20",
|
||||
hoursPerDay: 8,
|
||||
},
|
||||
],
|
||||
}),
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(result.action).toBeUndefined();
|
||||
expect(JSON.parse(result.content)).toEqual({
|
||||
error: "assignments[0].resourceIdentifier: Resource not found: missing_resource",
|
||||
field: "assignments[0].resourceIdentifier",
|
||||
index: 0,
|
||||
});
|
||||
expect(db.assignment.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("applies timeline project shifts through the real timeline router mutation", async () => {
|
||||
const { listAssignmentBookings } = await import("@capakraken/application");
|
||||
vi.mocked(listAssignmentBookings).mockResolvedValueOnce([]);
|
||||
@@ -1046,6 +1170,56 @@ describe("assistant advanced tools and scoping", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("returns a stable project-not-found error if the timeline shift target disappears mid-mutation", async () => {
|
||||
const { listAssignmentBookings } = await import("@capakraken/application");
|
||||
vi.mocked(listAssignmentBookings).mockResolvedValueOnce([]);
|
||||
|
||||
const db = {
|
||||
project: {
|
||||
findUnique: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
id: "project_1",
|
||||
name: "Gelddruckmaschine",
|
||||
shortCode: "GDM",
|
||||
status: "ACTIVE",
|
||||
responsiblePerson: null,
|
||||
budgetCents: 100000,
|
||||
winProbability: 100,
|
||||
startDate: new Date("2026-03-16"),
|
||||
endDate: new Date("2026-03-20"),
|
||||
})
|
||||
.mockResolvedValueOnce(null),
|
||||
},
|
||||
demandRequirement: {
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
assignment: {
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
};
|
||||
const ctx = createToolContext(
|
||||
db,
|
||||
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
|
||||
SystemRole.MANAGER,
|
||||
);
|
||||
|
||||
const result = await executeTool(
|
||||
"apply_timeline_project_shift",
|
||||
JSON.stringify({
|
||||
projectIdentifier: "project_1",
|
||||
newStartDate: "2026-03-23",
|
||||
newEndDate: "2026-03-27",
|
||||
}),
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(result.action).toBeUndefined();
|
||||
expect(JSON.parse(result.content)).toEqual({
|
||||
error: "Project not found with the given criteria.",
|
||||
});
|
||||
});
|
||||
|
||||
it("batch-shifts timeline allocations through the real timeline router mutation", async () => {
|
||||
const existingAssignment = {
|
||||
id: "assignment_1",
|
||||
@@ -1133,6 +1307,73 @@ describe("assistant advanced tools and scoping", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("returns a stable allocation-not-found error for inline timeline updates", async () => {
|
||||
const db = {
|
||||
allocation: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
demandRequirement: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
assignment: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
};
|
||||
const ctx = createToolContext(
|
||||
db,
|
||||
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
|
||||
SystemRole.MANAGER,
|
||||
);
|
||||
|
||||
const result = await executeTool(
|
||||
"update_timeline_allocation_inline",
|
||||
JSON.stringify({
|
||||
allocationId: "assignment_missing",
|
||||
hoursPerDay: 6,
|
||||
}),
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(result.action).toBeUndefined();
|
||||
expect(JSON.parse(result.content)).toEqual({
|
||||
error: "Allocation not found with the given criteria.",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a stable allocation-not-found error for batch timeline shifts without matches", async () => {
|
||||
const db = {
|
||||
allocation: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
demandRequirement: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
assignment: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
};
|
||||
const ctx = createToolContext(
|
||||
db,
|
||||
[PermissionKey.MANAGE_ALLOCATIONS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS],
|
||||
SystemRole.MANAGER,
|
||||
);
|
||||
|
||||
const result = await executeTool(
|
||||
"batch_shift_timeline_allocations",
|
||||
JSON.stringify({
|
||||
allocationIds: ["assignment_missing"],
|
||||
daysDelta: 2,
|
||||
mode: "move",
|
||||
}),
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(result.action).toBeUndefined();
|
||||
expect(JSON.parse(result.content)).toEqual({
|
||||
error: "Allocation not found with the given criteria.",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns the chargeability report readmodel through the assistant", async () => {
|
||||
const { listAssignmentBookings } = await import("@capakraken/application");
|
||||
vi.mocked(listAssignmentBookings).mockResolvedValue([
|
||||
@@ -1494,7 +1735,7 @@ describe("assistant advanced tools and scoping", () => {
|
||||
|
||||
expect(JSON.parse(result.content)).toEqual(
|
||||
expect.objectContaining({
|
||||
error: expect.stringContaining("Admin role required"),
|
||||
error: "You do not have permission to perform this action.",
|
||||
}),
|
||||
);
|
||||
expect(findMany).not.toHaveBeenCalled();
|
||||
|
||||
@@ -122,7 +122,7 @@ describe("assistant audit tools", () => {
|
||||
|
||||
expect(JSON.parse(result.content)).toEqual(
|
||||
expect.objectContaining({
|
||||
error: expect.stringContaining("Controller access required"),
|
||||
error: "You do not have permission to perform this action.",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -129,6 +129,25 @@ describe("assistant country tools", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a stable error when a country cannot be resolved", async () => {
|
||||
const ctx = createToolContext({
|
||||
country: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
findFirst: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await executeTool(
|
||||
"get_country",
|
||||
JSON.stringify({ identifier: "Atlantis" }),
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(JSON.parse(result.content)).toEqual({
|
||||
error: "Country not found with the given criteria.",
|
||||
});
|
||||
});
|
||||
|
||||
it("creates a country for admin users and returns an invalidation action", async () => {
|
||||
const ctx = createToolContext({
|
||||
country: {
|
||||
@@ -162,6 +181,46 @@ describe("assistant country tools", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a stable error when creating a country with a duplicate code", async () => {
|
||||
const ctx = createToolContext({
|
||||
country: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
id: "country_es_existing",
|
||||
code: "ES",
|
||||
name: "Existing Spain",
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await executeTool(
|
||||
"create_country",
|
||||
JSON.stringify({ code: "ES", name: "Spain", dailyWorkingHours: 8 }),
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(JSON.parse(result.content)).toEqual({
|
||||
error: "A country with this code already exists.",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a stable error when updating a missing country", async () => {
|
||||
const ctx = createToolContext({
|
||||
country: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await executeTool(
|
||||
"update_country",
|
||||
JSON.stringify({ id: "country_missing", data: { name: "Atlantis" } }),
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(JSON.parse(result.content)).toEqual({
|
||||
error: "Country not found with the given criteria.",
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses country mutations for non-admin users", async () => {
|
||||
const ctx = createToolContext({ country: {} }, [], SystemRole.MANAGER);
|
||||
|
||||
@@ -203,4 +262,44 @@ describe("assistant country tools", () => {
|
||||
message: "Deleted metro city: Hamburg",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a stable error when updating a missing metro city", async () => {
|
||||
const ctx = createToolContext({
|
||||
metroCity: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await executeTool(
|
||||
"update_metro_city",
|
||||
JSON.stringify({ id: "city_missing", data: { name: "Hamburg-Mitte" } }),
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(JSON.parse(result.content)).toEqual({
|
||||
error: "Metro city not found with the given criteria.",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a stable error when deleting a metro city that is still assigned", async () => {
|
||||
const ctx = createToolContext({
|
||||
metroCity: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
id: "city_ham",
|
||||
name: "Hamburg",
|
||||
_count: { resources: 3 },
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await executeTool(
|
||||
"delete_metro_city",
|
||||
JSON.stringify({ id: "city_ham" }),
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(JSON.parse(result.content)).toEqual({
|
||||
error: "Metro city cannot be deleted while it is still assigned to resources.",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -320,6 +320,100 @@ describe("assistant holiday tools", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("returns stable assistant errors for holiday calendar and entry mutations", async () => {
|
||||
const cases = [
|
||||
{
|
||||
name: "invalid holiday calendar scope",
|
||||
toolName: "create_holiday_calendar",
|
||||
db: {
|
||||
country: {
|
||||
findUnique: vi.fn().mockResolvedValue({ id: "country_de", code: "DE", name: "Deutschland" }),
|
||||
},
|
||||
holidayCalendar: {
|
||||
findFirst: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
},
|
||||
payload: {
|
||||
name: "Ungueltiger Kalender",
|
||||
scopeType: "STATE",
|
||||
countryId: "country_de",
|
||||
},
|
||||
expected: "Holiday calendar scope is invalid.",
|
||||
},
|
||||
{
|
||||
name: "duplicate holiday calendar scope",
|
||||
toolName: "create_holiday_calendar",
|
||||
db: {
|
||||
country: {
|
||||
findUnique: vi.fn().mockResolvedValue({ id: "country_de", code: "DE", name: "Deutschland" }),
|
||||
},
|
||||
holidayCalendar: {
|
||||
findFirst: vi.fn().mockResolvedValue({ id: "cal_existing" }),
|
||||
},
|
||||
},
|
||||
payload: {
|
||||
name: "Bayern Feiertage",
|
||||
scopeType: "STATE",
|
||||
countryId: "country_de",
|
||||
stateCode: "BY",
|
||||
},
|
||||
expected: "A holiday calendar for this scope already exists.",
|
||||
},
|
||||
{
|
||||
name: "holiday calendar not found on delete",
|
||||
toolName: "delete_holiday_calendar",
|
||||
db: {
|
||||
holidayCalendar: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
},
|
||||
payload: { id: "cal_missing" },
|
||||
expected: "Holiday calendar not found with the given criteria.",
|
||||
},
|
||||
{
|
||||
name: "holiday calendar entry not found on delete",
|
||||
toolName: "delete_holiday_calendar_entry",
|
||||
db: {
|
||||
holidayCalendarEntry: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
},
|
||||
payload: { id: "entry_missing" },
|
||||
expected: "Holiday calendar entry not found with the given criteria.",
|
||||
},
|
||||
{
|
||||
name: "duplicate holiday calendar entry date",
|
||||
toolName: "create_holiday_calendar_entry",
|
||||
db: {
|
||||
holidayCalendar: {
|
||||
findUnique: vi.fn().mockResolvedValue({ id: "cal_by", name: "Bayern Feiertage" }),
|
||||
},
|
||||
holidayCalendarEntry: {
|
||||
findFirst: vi.fn().mockResolvedValue({ id: "entry_existing" }),
|
||||
},
|
||||
},
|
||||
payload: {
|
||||
holidayCalendarId: "cal_by",
|
||||
date: "2026-01-06",
|
||||
name: "Heilige Drei Koenige",
|
||||
},
|
||||
expected: "A holiday entry for this calendar and date already exists.",
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const testCase of cases) {
|
||||
const result = await executeTool(
|
||||
testCase.toolName,
|
||||
JSON.stringify(testCase.payload),
|
||||
createToolContext(testCase.db),
|
||||
);
|
||||
|
||||
expect(JSON.parse(result.content)).toEqual({
|
||||
error: testCase.expected,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("calculates chargeability with regional holidays excluded from booked and available hours", async () => {
|
||||
const resourceRecord = {
|
||||
id: "res_1",
|
||||
@@ -763,6 +857,37 @@ describe("assistant holiday tools", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("returns a stable assistant error when staffing suggestions receive an invalid optional start date", async () => {
|
||||
const db = {
|
||||
project: {
|
||||
findUnique: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValueOnce({
|
||||
id: "project_1",
|
||||
name: "Holiday Project",
|
||||
shortCode: "HP",
|
||||
status: "ACTIVE",
|
||||
responsiblePerson: null,
|
||||
startDate: new Date("2026-01-05T00:00:00.000Z"),
|
||||
endDate: new Date("2026-01-16T00:00:00.000Z"),
|
||||
}),
|
||||
findFirst: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
};
|
||||
const ctx = createToolContext(db);
|
||||
|
||||
const result = await executeTool(
|
||||
"get_staffing_suggestions",
|
||||
JSON.stringify({ projectId: "project_1", startDate: "2026-99-01" }),
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(JSON.parse(result.content)).toEqual({
|
||||
error: "Invalid startDate: 2026-99-01",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses holiday-aware assignment hours for assistant shoring ratio", async () => {
|
||||
const db = {
|
||||
project: {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -619,6 +619,13 @@ export const notificationRouter = createTRPCRouter({
|
||||
senderId,
|
||||
);
|
||||
|
||||
if (recipientIds.length === 0) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "No recipients matched the broadcast target.",
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Create individual notifications for each recipient
|
||||
const isTask = input.category === "TASK" || input.category === "APPROVAL";
|
||||
|
||||
|
||||
@@ -135,10 +135,13 @@ export const userRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const user = await ctx.db.user.findUniqueOrThrow({
|
||||
where: { id: input.userId },
|
||||
select: { id: true, name: true, email: true },
|
||||
});
|
||||
const user = await findUniqueOrThrow(
|
||||
ctx.db.user.findUnique({
|
||||
where: { id: input.userId },
|
||||
select: { id: true, name: true, email: true },
|
||||
}),
|
||||
"User",
|
||||
);
|
||||
|
||||
const { hash } = await import("@node-rs/argon2");
|
||||
const passwordHash = await hash(input.password);
|
||||
@@ -170,10 +173,13 @@ export const userRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const before = await ctx.db.user.findUniqueOrThrow({
|
||||
where: { id: input.id },
|
||||
select: { id: true, name: true, email: true, systemRole: true },
|
||||
});
|
||||
const before = await findUniqueOrThrow(
|
||||
ctx.db.user.findUnique({
|
||||
where: { id: input.id },
|
||||
select: { id: true, name: true, email: true, systemRole: true },
|
||||
}),
|
||||
"User",
|
||||
);
|
||||
|
||||
const updated = await ctx.db.user.update({
|
||||
where: { id: input.id },
|
||||
@@ -205,10 +211,13 @@ export const userRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const before = await ctx.db.user.findUniqueOrThrow({
|
||||
where: { id: input.id },
|
||||
select: { id: true, name: true, email: true },
|
||||
});
|
||||
const before = await findUniqueOrThrow(
|
||||
ctx.db.user.findUnique({
|
||||
where: { id: input.id },
|
||||
select: { id: true, name: true, email: true },
|
||||
}),
|
||||
"User",
|
||||
);
|
||||
|
||||
const updated = await ctx.db.user.update({
|
||||
where: { id: input.id },
|
||||
@@ -237,7 +246,23 @@ export const userRouter = createTRPCRouter({
|
||||
linkResource: adminProcedure
|
||||
.input(z.object({ userId: z.string(), resourceId: z.string().nullable() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await findUniqueOrThrow(
|
||||
ctx.db.user.findUnique({
|
||||
where: { id: input.userId },
|
||||
select: { id: true },
|
||||
}),
|
||||
"User",
|
||||
);
|
||||
|
||||
if (input.resourceId) {
|
||||
await findUniqueOrThrow(
|
||||
ctx.db.resource.findUnique({
|
||||
where: { id: input.resourceId },
|
||||
select: { id: true },
|
||||
}),
|
||||
"Resource",
|
||||
);
|
||||
|
||||
// Unlink any resource previously linked to this user
|
||||
await ctx.db.resource.updateMany({
|
||||
where: { userId: input.userId },
|
||||
@@ -345,10 +370,13 @@ export const userRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const before = await ctx.db.user.findUniqueOrThrow({
|
||||
where: { id: input.userId },
|
||||
select: { id: true, name: true, email: true, permissionOverrides: true },
|
||||
});
|
||||
const before = await findUniqueOrThrow(
|
||||
ctx.db.user.findUnique({
|
||||
where: { id: input.userId },
|
||||
select: { id: true, name: true, email: true, permissionOverrides: true },
|
||||
}),
|
||||
"User",
|
||||
);
|
||||
|
||||
const user = await ctx.db.user.update({
|
||||
where: { id: input.userId },
|
||||
@@ -376,10 +404,13 @@ export const userRouter = createTRPCRouter({
|
||||
resetPermissions: adminProcedure
|
||||
.input(z.object({ userId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const before = await ctx.db.user.findUniqueOrThrow({
|
||||
where: { id: input.userId },
|
||||
select: { id: true, name: true, email: true, permissionOverrides: true },
|
||||
});
|
||||
const before = await findUniqueOrThrow(
|
||||
ctx.db.user.findUnique({
|
||||
where: { id: input.userId },
|
||||
select: { id: true, name: true, email: true, permissionOverrides: true },
|
||||
}),
|
||||
"User",
|
||||
);
|
||||
|
||||
const updated = await ctx.db.user.update({
|
||||
where: { id: input.userId },
|
||||
@@ -453,10 +484,13 @@ export const userRouter = createTRPCRouter({
|
||||
getEffectivePermissions: adminProcedure
|
||||
.input(z.object({ userId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const user = await ctx.db.user.findUniqueOrThrow({
|
||||
where: { id: input.userId },
|
||||
select: { systemRole: true, permissionOverrides: true },
|
||||
});
|
||||
const user = await findUniqueOrThrow(
|
||||
ctx.db.user.findUnique({
|
||||
where: { id: input.userId },
|
||||
select: { systemRole: true, permissionOverrides: true },
|
||||
}),
|
||||
"User",
|
||||
);
|
||||
const permissions = resolvePermissions(
|
||||
user.systemRole as SystemRole,
|
||||
user.permissionOverrides as PermissionOverrides | null,
|
||||
@@ -547,10 +581,13 @@ export const userRouter = createTRPCRouter({
|
||||
disableTotp: adminProcedure
|
||||
.input(z.object({ userId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const user = await ctx.db.user.findUniqueOrThrow({
|
||||
where: { id: input.userId },
|
||||
select: { id: true, name: true, email: true, totpEnabled: true },
|
||||
});
|
||||
const user = await findUniqueOrThrow(
|
||||
ctx.db.user.findUnique({
|
||||
where: { id: input.userId },
|
||||
select: { id: true, name: true, email: true, totpEnabled: true },
|
||||
}),
|
||||
"User",
|
||||
);
|
||||
|
||||
await ctx.db.user.update({
|
||||
where: { id: input.userId },
|
||||
|
||||
Reference in New Issue
Block a user