security: bound Zod inputs, add SSE per-user cap and tRPC body limit (#51, PR #59)
CI / Architecture Guardrails (push) Successful in 3m38s
CI / Assistant Split Regression (push) Successful in 4m40s
CI / Lint (push) Successful in 5m17s
CI / Typecheck (push) Successful in 5m46s
CI / Build (push) Successful in 7m1s
CI / Unit Tests (push) Failing after 9m41s
CI / Release Images (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / E2E Tests (push) Has started running
CI / Architecture Guardrails (push) Successful in 3m38s
CI / Assistant Split Regression (push) Successful in 4m40s
CI / Lint (push) Successful in 5m17s
CI / Typecheck (push) Successful in 5m46s
CI / Build (push) Successful in 7m1s
CI / Unit Tests (push) Failing after 9m41s
CI / Release Images (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / E2E Tests (push) Has started running
Closes #51 (ESLint rule + conventions doc remain as follow-up). Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com> Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
This commit was merged in pull request #59.
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user