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:
@@ -1,6 +1,7 @@
|
||||
import { renderToBuffer } from "@react-pdf/renderer";
|
||||
import { createElement } from "react";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { buildSplitAllocationReadModel } from "@capakraken/application";
|
||||
import { anonymizeResource, getAnonymizationDirectory } from "@capakraken/api";
|
||||
import { prisma } from "@capakraken/db";
|
||||
@@ -11,6 +12,17 @@ import { createWorkbookArrayBuffer } from "~/lib/workbook-export.js";
|
||||
|
||||
const ALLOWED_ROLES = new Set(["ADMIN", "MANAGER", "CONTROLLER"]);
|
||||
|
||||
// Reject fantasy dates from clients — years outside [2000, 2100] are almost
|
||||
// certainly malformed input and would generate nonsensical SQL range scans.
|
||||
const DATE_MIN = new Date("2000-01-01T00:00:00.000Z");
|
||||
const DATE_MAX = new Date("2100-01-01T00:00:00.000Z");
|
||||
|
||||
const queryParamsSchema = z.object({
|
||||
startDate: z.coerce.date().min(DATE_MIN).max(DATE_MAX).optional(),
|
||||
endDate: z.coerce.date().min(DATE_MIN).max(DATE_MAX).optional(),
|
||||
format: z.enum(["pdf", "xlsx"]).default("pdf"),
|
||||
});
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user) {
|
||||
@@ -23,9 +35,20 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const startDate = searchParams.get("startDate") ? new Date(searchParams.get("startDate")!) : new Date();
|
||||
const endDate = searchParams.get("endDate") ? new Date(searchParams.get("endDate")!) : new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
|
||||
const format = searchParams.get("format") ?? "pdf";
|
||||
const parsed = queryParamsSchema.safeParse({
|
||||
startDate: searchParams.get("startDate") ?? undefined,
|
||||
endDate: searchParams.get("endDate") ?? undefined,
|
||||
format: searchParams.get("format") ?? undefined,
|
||||
});
|
||||
if (!parsed.success) {
|
||||
return new NextResponse("Invalid query parameters", { status: 400 });
|
||||
}
|
||||
const startDate = parsed.data.startDate ?? new Date();
|
||||
const endDate = parsed.data.endDate ?? new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
|
||||
if (endDate < startDate) {
|
||||
return new NextResponse("endDate must be >= startDate", { status: 400 });
|
||||
}
|
||||
const format = parsed.data.format;
|
||||
|
||||
const [demandRequirements, assignments] = await Promise.all([
|
||||
prisma.demandRequirement.findMany({
|
||||
@@ -62,21 +85,25 @@ export async function GET(request: Request) {
|
||||
const assignmentRows = allocationView.assignments.slice(0, 500);
|
||||
const directory = await getAnonymizationDirectory(prisma);
|
||||
|
||||
const rows = assignmentRows.map((a: AllocationLike & {
|
||||
resource?: { id: string; displayName?: string | null } | null;
|
||||
project?: { shortCode: string; name: string } | null;
|
||||
}) => {
|
||||
const resource = a.resource ? anonymizeResource(a.resource, directory) : null;
|
||||
return {
|
||||
resourceName: resource?.displayName ?? "Unknown",
|
||||
projectName: a.project ? `${a.project.shortCode} — ${a.project.name}` : "Unknown project",
|
||||
role: a.role ?? "",
|
||||
startDate: new Date(a.startDate).toLocaleDateString("en-GB"),
|
||||
endDate: new Date(a.endDate).toLocaleDateString("en-GB"),
|
||||
hoursPerDay: a.hoursPerDay,
|
||||
dailyCostCents: a.dailyCostCents,
|
||||
};
|
||||
});
|
||||
const rows = assignmentRows.map(
|
||||
(
|
||||
a: AllocationLike & {
|
||||
resource?: { id: string; displayName?: string | null } | null;
|
||||
project?: { shortCode: string; name: string } | null;
|
||||
},
|
||||
) => {
|
||||
const resource = a.resource ? anonymizeResource(a.resource, directory) : null;
|
||||
return {
|
||||
resourceName: resource?.displayName ?? "Unknown",
|
||||
projectName: a.project ? `${a.project.shortCode} — ${a.project.name}` : "Unknown project",
|
||||
role: a.role ?? "",
|
||||
startDate: new Date(a.startDate).toLocaleDateString("en-GB"),
|
||||
endDate: new Date(a.endDate).toLocaleDateString("en-GB"),
|
||||
hoursPerDay: a.hoursPerDay,
|
||||
dailyCostCents: a.dailyCostCents,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const ts = Date.now();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user