security: bound Zod inputs, add SSE per-user cap and tRPC body limit (#51)
CI / Architecture Guardrails (pull_request) Successful in 2m6s
CI / Lint (pull_request) Successful in 7m29s
CI / Typecheck (pull_request) Successful in 8m3s
CI / Unit Tests (pull_request) Successful in 8m11s
CI / Build (pull_request) Successful in 5m24s
CI / E2E Tests (pull_request) Successful in 5m25s
CI / Fresh-Linux Docker Deploy (pull_request) Successful in 6m30s
CI / Release Images (pull_request) Has been skipped
CI / Assistant Split Regression (pull_request) Successful in 3m47s
CI / Architecture Guardrails (pull_request) Successful in 2m6s
CI / Lint (pull_request) Successful in 7m29s
CI / Typecheck (pull_request) Successful in 8m3s
CI / Unit Tests (pull_request) Successful in 8m11s
CI / Build (pull_request) Successful in 5m24s
CI / E2E Tests (pull_request) Successful in 5m25s
CI / Fresh-Linux Docker Deploy (pull_request) Successful in 6m30s
CI / Release Images (pull_request) Has been skipped
CI / Assistant Split Regression (pull_request) Successful in 3m47s
Mechanical .max() bounds across 9 router schemas per the convention in #51: IDs at 64, names at 200, search/filter strings at 500, arrays at 100-5000 depending on domain. Webhook secret bounded at min(16)/max(256). Reports route now validates startDate/endDate via zod with year bounds and rejects end<start. SSE timeline route enforces a per-user connection cap of 8 (returns 429 with Retry-After). tRPC route rejects bodies over 2 MiB via Content-Length check before auth/DB work. Covers 12 call-sites listed in #51. ESLint rule and zod conventions doc remain as follow-up.
This commit is contained in:
@@ -1,21 +1,21 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const auditLogListInputSchema = z.object({
|
||||
entityType: z.string().optional(),
|
||||
entityId: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
action: z.string().optional(),
|
||||
source: z.string().optional(),
|
||||
entityType: z.string().max(64).optional(),
|
||||
entityId: z.string().max(64).optional(),
|
||||
userId: z.string().max(64).optional(),
|
||||
action: z.string().max(32).optional(),
|
||||
source: z.string().max(32).optional(),
|
||||
startDate: z.date().optional(),
|
||||
endDate: z.date().optional(),
|
||||
search: z.string().optional(),
|
||||
search: z.string().max(200).optional(),
|
||||
limit: z.number().min(1).max(100).default(50),
|
||||
cursor: z.string().optional(),
|
||||
cursor: z.string().max(64).optional(),
|
||||
});
|
||||
|
||||
export const auditLogByEntityInputSchema = z.object({
|
||||
entityType: z.string(),
|
||||
entityId: z.string(),
|
||||
entityType: z.string().max(64),
|
||||
entityId: z.string().max(64),
|
||||
limit: z.number().min(1).max(200).default(50),
|
||||
});
|
||||
|
||||
|
||||
@@ -12,9 +12,21 @@ type ImportExportMutationContext = ImportExportReadContext & {
|
||||
|
||||
type ImportRow = Record<string, string>;
|
||||
|
||||
const CSV_CELL_MAX = 4000;
|
||||
const CSV_COLUMNS_MAX = 100;
|
||||
const CSV_ROWS_MAX = 10_000;
|
||||
|
||||
export const importCsvInputSchema = z.object({
|
||||
entityType: z.enum(["resources", "projects", "allocations"]),
|
||||
rows: z.array(z.record(z.string(), z.string())),
|
||||
rows: z
|
||||
.array(
|
||||
z
|
||||
.record(z.string().max(200), z.string().max(CSV_CELL_MAX))
|
||||
.refine((row) => Object.keys(row).length <= CSV_COLUMNS_MAX, {
|
||||
message: `CSV row exceeds ${CSV_COLUMNS_MAX} columns`,
|
||||
}),
|
||||
)
|
||||
.max(CSV_ROWS_MAX),
|
||||
dryRun: z.boolean().default(true),
|
||||
});
|
||||
|
||||
@@ -32,7 +44,10 @@ function resolveVisibleBlueprintFields(fieldDefs: unknown): BlueprintFieldDefini
|
||||
}
|
||||
|
||||
function buildCsv(headers: unknown[], rows: unknown[][]) {
|
||||
return [headers.map(escapeCsvValue).join(","), ...rows.map((row) => row.map(escapeCsvValue).join(","))].join("\n");
|
||||
return [
|
||||
headers.map(escapeCsvValue).join(","),
|
||||
...rows.map((row) => row.map(escapeCsvValue).join(",")),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export async function exportResourcesCsv(ctx: ImportExportReadContext) {
|
||||
@@ -168,7 +183,10 @@ export async function importCsv(ctx: ImportExportMutationContext, input: ImportC
|
||||
|
||||
try {
|
||||
if (input.entityType === "resources") {
|
||||
const outcome = await importResourceRow({ ...ctx, db: tx as unknown as typeof ctx.db }, row);
|
||||
const outcome = await importResourceRow(
|
||||
{ ...ctx, db: tx as unknown as typeof ctx.db },
|
||||
row,
|
||||
);
|
||||
if (outcome.updated) {
|
||||
results.updated += 1;
|
||||
} else if (outcome.error) {
|
||||
|
||||
@@ -5,7 +5,10 @@ import { sendEmail } from "../lib/email.js";
|
||||
import { emitTaskAssigned } from "../sse/event-bus.js";
|
||||
import type { TRPCContext } from "../trpc.js";
|
||||
|
||||
export type NotificationProcedureContext = Pick<TRPCContext, "db" | "dbUser" | "roleDefaults" | "session">;
|
||||
export type NotificationProcedureContext = Pick<
|
||||
TRPCContext,
|
||||
"db" | "dbUser" | "roleDefaults" | "session"
|
||||
>;
|
||||
|
||||
export function requireNotificationDbUser(ctx: NotificationProcedureContext) {
|
||||
if (!ctx.dbUser) {
|
||||
@@ -89,17 +92,15 @@ export function rethrowNotificationReferenceError(
|
||||
recipientContext: "notification" | "task" | "broadcast" = "notification",
|
||||
): never {
|
||||
for (const candidate of getNotificationErrorCandidates(error)) {
|
||||
const fieldName = typeof candidate.meta?.field_name === "string"
|
||||
? candidate.meta.field_name.toLowerCase()
|
||||
: "";
|
||||
const modelName = typeof candidate.meta?.modelName === "string"
|
||||
? candidate.meta.modelName.toLowerCase()
|
||||
: "";
|
||||
const fieldName =
|
||||
typeof candidate.meta?.field_name === "string" ? candidate.meta.field_name.toLowerCase() : "";
|
||||
const modelName =
|
||||
typeof candidate.meta?.modelName === "string" ? candidate.meta.modelName.toLowerCase() : "";
|
||||
|
||||
if (
|
||||
typeof candidate.code === "string"
|
||||
&& (candidate.code === "P2003" || candidate.code === "P2025")
|
||||
&& fieldName.includes("assignee")
|
||||
typeof candidate.code === "string" &&
|
||||
(candidate.code === "P2003" || candidate.code === "P2025") &&
|
||||
fieldName.includes("assignee")
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
@@ -109,9 +110,9 @@ export function rethrowNotificationReferenceError(
|
||||
}
|
||||
|
||||
if (
|
||||
typeof candidate.code === "string"
|
||||
&& (candidate.code === "P2003" || candidate.code === "P2025")
|
||||
&& fieldName.includes("sender")
|
||||
typeof candidate.code === "string" &&
|
||||
(candidate.code === "P2003" || candidate.code === "P2025") &&
|
||||
fieldName.includes("sender")
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
@@ -121,15 +122,16 @@ export function rethrowNotificationReferenceError(
|
||||
}
|
||||
|
||||
if (
|
||||
typeof candidate.code === "string"
|
||||
&& (candidate.code === "P2003" || candidate.code === "P2025")
|
||||
&& fieldName.includes("userid")
|
||||
typeof candidate.code === "string" &&
|
||||
(candidate.code === "P2003" || candidate.code === "P2025") &&
|
||||
fieldName.includes("userid")
|
||||
) {
|
||||
const message = recipientContext === "broadcast"
|
||||
? "Broadcast recipient user not found"
|
||||
: recipientContext === "task"
|
||||
? "Task recipient user not found"
|
||||
: "Notification recipient user not found";
|
||||
const message =
|
||||
recipientContext === "broadcast"
|
||||
? "Broadcast recipient user not found"
|
||||
: recipientContext === "task"
|
||||
? "Task recipient user not found"
|
||||
: "Notification recipient user not found";
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message,
|
||||
@@ -138,13 +140,11 @@ export function rethrowNotificationReferenceError(
|
||||
}
|
||||
|
||||
if (
|
||||
typeof candidate.code === "string"
|
||||
&& (candidate.code === "P2003" || candidate.code === "P2025")
|
||||
&& (
|
||||
modelName.includes("notificationbroadcast")
|
||||
|| fieldName.includes("broadcast")
|
||||
|| fieldName.includes("sourceid")
|
||||
)
|
||||
typeof candidate.code === "string" &&
|
||||
(candidate.code === "P2003" || candidate.code === "P2025") &&
|
||||
(modelName.includes("notificationbroadcast") ||
|
||||
fieldName.includes("broadcast") ||
|
||||
fieldName.includes("sourceid"))
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
@@ -203,11 +203,11 @@ export const ListNotificationTasksInputSchema = z.object({
|
||||
});
|
||||
|
||||
export const NotificationIdInputSchema = z.object({
|
||||
id: z.string(),
|
||||
id: z.string().max(64),
|
||||
});
|
||||
|
||||
export const UpdateNotificationTaskStatusInputSchema = z.object({
|
||||
id: z.string(),
|
||||
id: z.string().max(64),
|
||||
status: taskStatusEnum,
|
||||
});
|
||||
|
||||
@@ -216,13 +216,13 @@ export const CreateReminderInputSchema = z.object({
|
||||
body: z.string().max(2000).optional(),
|
||||
remindAt: z.date(),
|
||||
recurrence: recurrenceEnum.optional(),
|
||||
entityId: z.string().optional(),
|
||||
entityType: z.string().optional(),
|
||||
link: z.string().optional(),
|
||||
entityId: z.string().max(64).optional(),
|
||||
entityType: z.string().max(64).optional(),
|
||||
link: z.string().max(2048).optional(),
|
||||
});
|
||||
|
||||
export const UpdateReminderInputSchema = z.object({
|
||||
id: z.string(),
|
||||
id: z.string().max(64),
|
||||
title: z.string().min(1).max(200).optional(),
|
||||
body: z.string().max(2000).optional(),
|
||||
remindAt: z.date().optional(),
|
||||
@@ -236,14 +236,14 @@ export const ListRemindersInputSchema = z.object({
|
||||
export const CreateBroadcastInputSchema = z.object({
|
||||
title: z.string().min(1).max(200),
|
||||
body: z.string().max(2000).optional(),
|
||||
link: z.string().optional(),
|
||||
link: z.string().max(2048).optional(),
|
||||
category: categoryEnum.default("NOTIFICATION"),
|
||||
priority: priorityEnum.default("NORMAL"),
|
||||
channel: channelEnum.default("in_app"),
|
||||
targetType: targetTypeEnum,
|
||||
targetValue: z.string().optional(),
|
||||
targetValue: z.string().max(200).optional(),
|
||||
scheduledAt: z.date().optional(),
|
||||
taskAction: z.string().optional(),
|
||||
taskAction: z.string().max(64).optional(),
|
||||
dueDate: z.date().optional(),
|
||||
});
|
||||
|
||||
@@ -252,21 +252,21 @@ export const ListBroadcastsInputSchema = z.object({
|
||||
});
|
||||
|
||||
export const CreateTaskInputSchema = z.object({
|
||||
userId: z.string(),
|
||||
userId: z.string().max(64),
|
||||
title: z.string().min(1).max(200),
|
||||
body: z.string().max(2000).optional(),
|
||||
priority: priorityEnum.default("NORMAL"),
|
||||
dueDate: z.date().optional(),
|
||||
taskAction: z.string().optional(),
|
||||
entityId: z.string().optional(),
|
||||
entityType: z.string().optional(),
|
||||
link: z.string().optional(),
|
||||
taskAction: z.string().max(64).optional(),
|
||||
entityId: z.string().max(64).optional(),
|
||||
entityType: z.string().max(64).optional(),
|
||||
link: z.string().max(2048).optional(),
|
||||
channel: channelEnum.default("in_app"),
|
||||
});
|
||||
|
||||
export const AssignTaskInputSchema = z.object({
|
||||
id: z.string(),
|
||||
assigneeId: z.string(),
|
||||
id: z.string().max(64),
|
||||
assigneeId: z.string().max(64),
|
||||
});
|
||||
|
||||
export type BroadcastRecipientNotification = { id: string; userId: string };
|
||||
@@ -411,9 +411,9 @@ export async function deleteNotification(
|
||||
}
|
||||
|
||||
if (
|
||||
(existing.category === "TASK" || existing.category === "APPROVAL")
|
||||
&& existing.senderId
|
||||
&& existing.senderId !== userId
|
||||
(existing.category === "TASK" || existing.category === "APPROVAL") &&
|
||||
existing.senderId &&
|
||||
existing.senderId !== userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
|
||||
@@ -438,7 +438,7 @@ export const resourceMutationProcedures = {
|
||||
}),
|
||||
|
||||
batchHardDelete: adminProcedure
|
||||
.input(z.object({ ids: z.array(z.string()).min(1) }))
|
||||
.input(z.object({ ids: z.array(z.string().max(64)).min(1).max(500) }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const resources = await ctx.db.resource.findMany({
|
||||
where: { id: { in: input.ids } },
|
||||
|
||||
@@ -2,13 +2,18 @@ import { PermissionKey, SkillEntrySchema } from "@capakraken/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
import { adminProcedure, managerProcedure, protectedProcedure, requirePermission } from "../trpc.js";
|
||||
import {
|
||||
adminProcedure,
|
||||
managerProcedure,
|
||||
protectedProcedure,
|
||||
requirePermission,
|
||||
} from "../trpc.js";
|
||||
|
||||
const employeeInfoSchema = z
|
||||
.object({
|
||||
roleId: z.string().optional(),
|
||||
yearsOfExperience: z.number().optional(),
|
||||
portfolioUrl: z.string().url().optional().or(z.literal("")),
|
||||
roleId: z.string().max(64).optional(),
|
||||
yearsOfExperience: z.number().min(0).max(100).optional(),
|
||||
portfolioUrl: z.string().url().max(2048).optional().or(z.literal("")),
|
||||
})
|
||||
.optional();
|
||||
|
||||
@@ -16,7 +21,7 @@ export const resourceSkillImportProcedures = {
|
||||
importSkillMatrix: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
skills: z.array(SkillEntrySchema),
|
||||
skills: z.array(SkillEntrySchema).max(2000),
|
||||
employeeInfo: employeeInfoSchema,
|
||||
}),
|
||||
)
|
||||
@@ -40,7 +45,9 @@ export const resourceSkillImportProcedures = {
|
||||
...(input.employeeInfo?.portfolioUrl !== undefined
|
||||
? { portfolioUrl: input.employeeInfo.portfolioUrl || null }
|
||||
: {}),
|
||||
...(input.employeeInfo?.roleId !== undefined ? { roleId: input.employeeInfo.roleId } : {}),
|
||||
...(input.employeeInfo?.roleId !== undefined
|
||||
? { roleId: input.employeeInfo.roleId }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -50,8 +57,8 @@ export const resourceSkillImportProcedures = {
|
||||
importSkillMatrixForResource: managerProcedure
|
||||
.input(
|
||||
z.object({
|
||||
resourceId: z.string(),
|
||||
skills: z.array(SkillEntrySchema),
|
||||
resourceId: z.string().max(64),
|
||||
skills: z.array(SkillEntrySchema).max(2000),
|
||||
employeeInfo: employeeInfoSchema,
|
||||
}),
|
||||
)
|
||||
@@ -70,7 +77,9 @@ export const resourceSkillImportProcedures = {
|
||||
...(input.employeeInfo?.portfolioUrl !== undefined
|
||||
? { portfolioUrl: input.employeeInfo.portfolioUrl || null }
|
||||
: {}),
|
||||
...(input.employeeInfo?.roleId !== undefined ? { roleId: input.employeeInfo.roleId } : {}),
|
||||
...(input.employeeInfo?.roleId !== undefined
|
||||
? { roleId: input.employeeInfo.roleId }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -80,13 +89,15 @@ export const resourceSkillImportProcedures = {
|
||||
batchImportSkillMatrices: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
entries: z.array(
|
||||
z.object({
|
||||
eid: z.string(),
|
||||
skills: z.array(SkillEntrySchema),
|
||||
employeeInfo: employeeInfoSchema,
|
||||
}),
|
||||
),
|
||||
entries: z
|
||||
.array(
|
||||
z.object({
|
||||
eid: z.string().max(64),
|
||||
skills: z.array(SkillEntrySchema).max(2000),
|
||||
employeeInfo: employeeInfoSchema,
|
||||
}),
|
||||
)
|
||||
.max(5000),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
@@ -110,7 +121,9 @@ export const resourceSkillImportProcedures = {
|
||||
...(entry.employeeInfo?.portfolioUrl !== undefined
|
||||
? { portfolioUrl: entry.employeeInfo.portfolioUrl || null }
|
||||
: {}),
|
||||
...(entry.employeeInfo?.roleId !== undefined ? { roleId: entry.employeeInfo.roleId } : {}),
|
||||
...(entry.employeeInfo?.roleId !== undefined
|
||||
? { roleId: entry.employeeInfo.roleId }
|
||||
: {}),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -397,8 +397,8 @@ async function queryStaffingSuggestions(
|
||||
});
|
||||
}
|
||||
const GetProjectStaffingSuggestionsInputSchema = z.object({
|
||||
projectId: z.string().min(1),
|
||||
roleName: z.string().optional(),
|
||||
projectId: z.string().min(1).max(64),
|
||||
roleName: z.string().max(200).optional(),
|
||||
startDate: z.coerce.date().optional(),
|
||||
endDate: z.coerce.date().optional(),
|
||||
limit: z.number().int().min(1).max(50).optional().default(5),
|
||||
@@ -408,14 +408,14 @@ export const staffingSuggestionsReadProcedures = {
|
||||
getSuggestions: planningReadProcedure
|
||||
.input(
|
||||
z.object({
|
||||
requiredSkills: z.array(z.string()),
|
||||
preferredSkills: z.array(z.string()).optional(),
|
||||
requiredSkills: z.array(z.string().max(200)).max(200),
|
||||
preferredSkills: z.array(z.string().max(200)).max(200).optional(),
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
hoursPerDay: z.number().min(0).max(24),
|
||||
budgetLcrCentsPerHour: z.number().optional(),
|
||||
chapter: z.string().optional(),
|
||||
skillCategory: z.string().optional(),
|
||||
budgetLcrCentsPerHour: z.number().int().min(0).max(1_000_000_00).optional(),
|
||||
chapter: z.string().max(100).optional(),
|
||||
skillCategory: z.string().max(100).optional(),
|
||||
mainSkillsOnly: z.boolean().optional(),
|
||||
minProficiency: z.number().min(1).max(5).optional(),
|
||||
}),
|
||||
|
||||
@@ -1,35 +1,40 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const idFilter = () => z.array(z.string().max(64)).max(500);
|
||||
const chapterFilter = () => z.array(z.string().max(100)).max(100);
|
||||
const countryFilter = () => z.array(z.string().max(8)).max(300);
|
||||
const dateStr = () => z.string().max(32);
|
||||
|
||||
export const TimelineWindowFiltersSchema = z.object({
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
resourceIds: z.array(z.string()).optional(),
|
||||
projectIds: z.array(z.string()).optional(),
|
||||
clientIds: z.array(z.string()).optional(),
|
||||
chapters: z.array(z.string()).optional(),
|
||||
eids: z.array(z.string()).optional(),
|
||||
countryCodes: z.array(z.string()).optional(),
|
||||
resourceIds: idFilter().optional(),
|
||||
projectIds: idFilter().optional(),
|
||||
clientIds: idFilter().optional(),
|
||||
chapters: chapterFilter().optional(),
|
||||
eids: idFilter().optional(),
|
||||
countryCodes: countryFilter().optional(),
|
||||
});
|
||||
|
||||
export const TimelineDetailFiltersSchema = z.object({
|
||||
startDate: z.string().optional(),
|
||||
endDate: z.string().optional(),
|
||||
startDate: dateStr().optional(),
|
||||
endDate: dateStr().optional(),
|
||||
durationDays: z.number().int().min(1).max(366).optional(),
|
||||
resourceIds: z.array(z.string()).optional(),
|
||||
projectIds: z.array(z.string()).optional(),
|
||||
clientIds: z.array(z.string()).optional(),
|
||||
chapters: z.array(z.string()).optional(),
|
||||
eids: z.array(z.string()).optional(),
|
||||
countryCodes: z.array(z.string()).optional(),
|
||||
resourceIds: idFilter().optional(),
|
||||
projectIds: idFilter().optional(),
|
||||
clientIds: idFilter().optional(),
|
||||
chapters: chapterFilter().optional(),
|
||||
eids: idFilter().optional(),
|
||||
countryCodes: countryFilter().optional(),
|
||||
});
|
||||
|
||||
export const TimelineProjectContextDetailSchema = z.object({
|
||||
projectId: z.string(),
|
||||
startDate: z.string().optional(),
|
||||
endDate: z.string().optional(),
|
||||
projectId: z.string().max(64),
|
||||
startDate: dateStr().optional(),
|
||||
endDate: dateStr().optional(),
|
||||
durationDays: z.number().int().min(1).max(366).optional(),
|
||||
});
|
||||
|
||||
export const TimelineProjectIdSchema = z.object({
|
||||
projectId: z.string(),
|
||||
projectId: z.string().max(64),
|
||||
});
|
||||
|
||||
@@ -13,45 +13,45 @@ import type { TRPCContext } from "../trpc.js";
|
||||
import { invalidateRoleDefaultsCache } from "../trpc.js";
|
||||
|
||||
export const CreateUserInputSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1),
|
||||
email: z.string().email().max(320),
|
||||
name: z.string().min(1).max(200),
|
||||
systemRole: z.nativeEnum(SystemRole).default(SystemRole.USER),
|
||||
password: z.string().min(PASSWORD_MIN_LENGTH, PASSWORD_POLICY_MESSAGE).max(PASSWORD_MAX_LENGTH),
|
||||
});
|
||||
|
||||
export const SetUserPasswordInputSchema = z.object({
|
||||
userId: z.string(),
|
||||
userId: z.string().max(64),
|
||||
password: z.string().min(PASSWORD_MIN_LENGTH, PASSWORD_POLICY_MESSAGE).max(PASSWORD_MAX_LENGTH),
|
||||
});
|
||||
|
||||
export const UpdateUserRoleInputSchema = z.object({
|
||||
id: z.string(),
|
||||
id: z.string().max(64),
|
||||
systemRole: z.nativeEnum(SystemRole),
|
||||
});
|
||||
|
||||
export const UpdateUserNameInputSchema = z.object({
|
||||
id: z.string(),
|
||||
id: z.string().max(64),
|
||||
name: z.string().min(1, "Name is required").max(200),
|
||||
});
|
||||
|
||||
export const LinkUserResourceInputSchema = z.object({
|
||||
userId: z.string(),
|
||||
resourceId: z.string().nullable(),
|
||||
userId: z.string().max(64),
|
||||
resourceId: z.string().max(64).nullable(),
|
||||
});
|
||||
|
||||
export const SetUserPermissionsInputSchema = z.object({
|
||||
userId: z.string(),
|
||||
userId: z.string().max(64),
|
||||
overrides: z
|
||||
.object({
|
||||
granted: z.array(z.string()).optional(),
|
||||
denied: z.array(z.string()).optional(),
|
||||
chapterIds: z.array(z.string()).optional(),
|
||||
granted: z.array(z.string().max(128)).max(500).optional(),
|
||||
denied: z.array(z.string().max(128)).max(500).optional(),
|
||||
chapterIds: z.array(z.string().max(64)).max(500).optional(),
|
||||
})
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
export const UserIdInputSchema = z.object({
|
||||
userId: z.string(),
|
||||
userId: z.string().max(64),
|
||||
});
|
||||
|
||||
type UserReadContext = Pick<TRPCContext, "db" | "dbUser">;
|
||||
|
||||
@@ -6,17 +6,17 @@ export const webhookEventEnum = z.enum(WEBHOOK_EVENTS as unknown as [string, ...
|
||||
|
||||
export const createWebhookInputSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
url: z.string().url(),
|
||||
secret: z.string().optional(),
|
||||
events: z.array(webhookEventEnum).min(1),
|
||||
url: z.string().url().max(2048),
|
||||
secret: z.string().min(16).max(256).optional(),
|
||||
events: z.array(webhookEventEnum).min(1).max(100),
|
||||
isActive: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export const updateWebhookInputSchema = z.object({
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
url: z.string().url().optional(),
|
||||
secret: z.string().nullish(),
|
||||
events: z.array(webhookEventEnum).min(1).optional(),
|
||||
url: z.string().url().max(2048).optional(),
|
||||
secret: z.string().min(16).max(256).nullish(),
|
||||
events: z.array(webhookEventEnum).min(1).max(100).optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
@@ -35,9 +35,7 @@ type WebhookDb = {
|
||||
};
|
||||
};
|
||||
|
||||
export function buildWebhookCreateData(
|
||||
input: z.infer<typeof createWebhookInputSchema>,
|
||||
) {
|
||||
export function buildWebhookCreateData(input: z.infer<typeof createWebhookInputSchema>) {
|
||||
return {
|
||||
name: input.name,
|
||||
url: input.url,
|
||||
@@ -47,9 +45,7 @@ export function buildWebhookCreateData(
|
||||
};
|
||||
}
|
||||
|
||||
export function buildWebhookUpdateData(
|
||||
input: z.infer<typeof updateWebhookInputSchema>,
|
||||
) {
|
||||
export function buildWebhookUpdateData(input: z.infer<typeof updateWebhookInputSchema>) {
|
||||
return {
|
||||
...(input.name !== undefined ? { name: input.name } : {}),
|
||||
...(input.url !== undefined ? { url: input.url } : {}),
|
||||
@@ -59,10 +55,7 @@ export function buildWebhookUpdateData(
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadWebhookOrThrow(
|
||||
db: WebhookDb,
|
||||
id: string,
|
||||
) {
|
||||
export async function loadWebhookOrThrow(db: WebhookDb, id: string) {
|
||||
const webhook = await db.webhook.findUnique({ where: { id } });
|
||||
if (!webhook) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "Webhook not found" });
|
||||
|
||||
Reference in New Issue
Block a user