Harden API, data layer, and autosave; add validation, pagination, migrations
Build and Push Docker Image / build (push) Successful in 2m26s
Build and Push Docker Image / build (push) Successful in 2m26s
- Enforce task ownership on PATCH/DELETE /api/tasks (was: any signed-in user could edit or delete anyone's tasks) - Validate all API request bodies with zod; escape user content and restrict links to http(s) in the Drive export; block reverting a SUBMITTED report - Add @@unique([userId, date]) on Report with upsert to eliminate the duplicate-daily-report race; switch startup from `prisma db push --accept-data-loss` to `prisma migrate deploy` with automatic baselining of existing databases (dedup migration merges any pre-existing duplicates) - Autosave: re-queue and retry failed task saves with a visible saving/error indicator instead of silently dropping edits - Paginate and filter GET /api/reports (?date, ?mine, ?q, ?take, ?cursor); report form fetches only today's report, admin dashboard uses server-side search + Load more - Type the frontend and lib layer (DTOs in src/types/api.ts); zero eslint errors - Update README and Unraid guide for migrations, upgrade path, and API Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { z } from "zod";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export const TASK_STATUSES = ["PENDING", "DONE", "IN_PROGRESS", "CANCELLED"] as const;
|
||||
export const REPORT_STATUSES = ["IN_PROGRESS", "COMPLETED", "SUBMITTED"] as const;
|
||||
|
||||
const httpLink = z
|
||||
.string()
|
||||
.trim()
|
||||
.max(2000)
|
||||
.refine((v) => v === "" || /^https?:\/\//i.test(v), {
|
||||
message: "Link must start with http:// or https://",
|
||||
});
|
||||
|
||||
export const createReportSchema = z.object({
|
||||
managerName: z.string().trim().min(1).max(200),
|
||||
date: z
|
||||
.string()
|
||||
.regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD")
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const updateReportSchema = z.object({
|
||||
managerName: z.string().trim().min(1).max(200).optional(),
|
||||
status: z.enum(REPORT_STATUSES).optional(),
|
||||
});
|
||||
|
||||
export const createTaskSchema = z.object({
|
||||
reportId: z.string().min(1),
|
||||
type: z.enum(["PLANNED", "COMPLETED"]),
|
||||
description: z.string().max(2000).default(""),
|
||||
timeEstimate: z.string().max(100).nullish(),
|
||||
notes: z.string().max(2000).nullish(),
|
||||
link: httpLink.nullish(),
|
||||
status: z.enum(TASK_STATUSES).nullish(),
|
||||
});
|
||||
|
||||
export const updateTaskSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
type: z.enum(["PLANNED", "COMPLETED"]).optional(),
|
||||
description: z.string().max(2000).optional(),
|
||||
timeEstimate: z.string().max(100).nullish(),
|
||||
notes: z.string().max(2000).nullish(),
|
||||
link: httpLink.nullish(),
|
||||
status: z.enum(TASK_STATUSES).nullish(),
|
||||
});
|
||||
|
||||
export const updateSettingSchema = z.object({
|
||||
key: z.enum(["GOOGLE_DRIVE_FOLDER_ID"]),
|
||||
value: z.string().trim().max(500),
|
||||
});
|
||||
|
||||
export const updateUserRoleSchema = z.object({
|
||||
userId: z.string().min(1),
|
||||
role: z.enum(["EMPLOYEE", "ADMIN"]),
|
||||
});
|
||||
|
||||
/**
|
||||
* Parse and validate a request body. Returns the parsed data, or a
|
||||
* NextResponse 400 that the route should return immediately.
|
||||
*/
|
||||
export async function parseBody<T extends z.ZodTypeAny>(
|
||||
req: Request,
|
||||
schema: T
|
||||
): Promise<{ data: z.infer<T>; error: null } | { data: null; error: NextResponse }> {
|
||||
let json: unknown;
|
||||
try {
|
||||
json = await req.json();
|
||||
} catch {
|
||||
return {
|
||||
data: null,
|
||||
error: NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }),
|
||||
};
|
||||
}
|
||||
|
||||
const result = schema.safeParse(json);
|
||||
if (!result.success) {
|
||||
return {
|
||||
data: null,
|
||||
error: NextResponse.json(
|
||||
{ error: "Validation failed", details: result.error.flatten().fieldErrors },
|
||||
{ status: 400 }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return { data: result.data, error: null };
|
||||
}
|
||||
Reference in New Issue
Block a user