From 7abdbaa2eca6b971b36216bf5a7d4501ba449186 Mon Sep 17 00:00:00 2001 From: Xenon010101 Date: Fri, 5 Jun 2026 00:56:31 +0530 Subject: [PATCH 1/2] fix: reject negative/zero work hours in attendance regularization (#1337) - Add Zod refinements to regularizeSchema: checkOut > checkIn, workHours <= 24 - Add runtime guards in regularize() service method - Clamp HALF_DAY/PRESENT logic to positive work hours only --- server/src/module/attendance/attendance.service.ts | 3 +++ server/src/module/attendance/attendance.validation.ts | 10 +++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/server/src/module/attendance/attendance.service.ts b/server/src/module/attendance/attendance.service.ts index a51c5986a..8d4bcd7fd 100644 --- a/server/src/module/attendance/attendance.service.ts +++ b/server/src/module/attendance/attendance.service.ts @@ -133,6 +133,9 @@ export class AttendanceService { const checkOut = new Date(data.checkOut); const workHours = (checkOut.getTime() - checkIn.getTime()) / 3600000; + if (workHours <= 0) throw new Error("checkOut must be after checkIn"); + if (workHours > 24) throw new Error("Work hours must not exceed 24"); + return prisma.attendanceRecord.upsert({ where: { employeeId_date: { employeeId: data.employeeId, date } }, create: { diff --git a/server/src/module/attendance/attendance.validation.ts b/server/src/module/attendance/attendance.validation.ts index 4443fdd29..d4c411f2b 100644 --- a/server/src/module/attendance/attendance.validation.ts +++ b/server/src/module/attendance/attendance.validation.ts @@ -10,13 +10,21 @@ export const checkOutSchema = z.object({ notes: z.string().max(500).optional(), }); +const MAX_WORK_HOURS = 24; + export const regularizeSchema = z.object({ employeeId: z.number().int().positive(), date: z.string().datetime(), checkIn: z.string().datetime(), checkOut: z.string().datetime(), notes: z.string().min(1, "Reason for regularization is required").max(500), -}); +}).refine((data) => { + const workHours = (new Date(data.checkOut).getTime() - new Date(data.checkIn).getTime()) / 3600000; + return workHours > 0; +}, { message: "checkOut must be after checkIn" }).refine((data) => { + const workHours = (new Date(data.checkOut).getTime() - new Date(data.checkIn).getTime()) / 3600000; + return workHours <= MAX_WORK_HOURS; +}, { message: `Work hours must not exceed ${MAX_WORK_HOURS}` }); export const attendanceQuerySchema = z.object({ page: z.coerce.number().int().positive().default(1), From b74504b07affd6c7b1fe535b439bd4511badda45 Mon Sep 17 00:00:00 2001 From: Xenon010101 Date: Fri, 5 Jun 2026 01:22:17 +0530 Subject: [PATCH 2/2] refactor: consolidate Zod validations and share work-hours constant - Replace duplicate .refine() with single .superRefine() - Extract MAX_WORK_HOURS to shared attendance.constants.ts - Import constant in both validation.ts and service.ts - Return HTTP 400 for service validation errors in controller --- .../module/attendance/attendance.constants.ts | 1 + .../attendance/attendance.controller.ts | 5 +++++ .../module/attendance/attendance.service.ts | 3 ++- .../attendance/attendance.validation.ts | 19 +++++++++++-------- 4 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 server/src/module/attendance/attendance.constants.ts diff --git a/server/src/module/attendance/attendance.constants.ts b/server/src/module/attendance/attendance.constants.ts new file mode 100644 index 000000000..00a793a6f --- /dev/null +++ b/server/src/module/attendance/attendance.constants.ts @@ -0,0 +1 @@ +export const MAX_WORK_HOURS = 24; diff --git a/server/src/module/attendance/attendance.controller.ts b/server/src/module/attendance/attendance.controller.ts index b7039f16e..74dec4972 100644 --- a/server/src/module/attendance/attendance.controller.ts +++ b/server/src/module/attendance/attendance.controller.ts @@ -90,6 +90,11 @@ export class AttendanceController { const record = await this.attendanceService.regularize(result.data); return res.json({ message: "Attendance regularized", record }); } catch (error) { + if (error instanceof Error) { + const msg = error.message; + if (msg.includes("must be after") || msg.includes("must not exceed") || msg.includes("Cannot regularize") || msg.includes("cannot be in the future")) + return res.status(400).json({ message: msg }); + } console.error(error); return res.status(500).json({ message: "Internal Server Error" }); } diff --git a/server/src/module/attendance/attendance.service.ts b/server/src/module/attendance/attendance.service.ts index 8d4bcd7fd..c2f069596 100644 --- a/server/src/module/attendance/attendance.service.ts +++ b/server/src/module/attendance/attendance.service.ts @@ -1,5 +1,6 @@ import { prisma } from "../../database/db.js"; import type { AttendanceStatus, Prisma } from "@prisma/client"; +import { MAX_WORK_HOURS } from "./attendance.constants.js"; interface AttendanceQuery { page: number; @@ -134,7 +135,7 @@ export class AttendanceService { const workHours = (checkOut.getTime() - checkIn.getTime()) / 3600000; if (workHours <= 0) throw new Error("checkOut must be after checkIn"); - if (workHours > 24) throw new Error("Work hours must not exceed 24"); + if (workHours > MAX_WORK_HOURS) throw new Error("Work hours must not exceed 24"); return prisma.attendanceRecord.upsert({ where: { employeeId_date: { employeeId: data.employeeId, date } }, diff --git a/server/src/module/attendance/attendance.validation.ts b/server/src/module/attendance/attendance.validation.ts index d4c411f2b..c6035880e 100644 --- a/server/src/module/attendance/attendance.validation.ts +++ b/server/src/module/attendance/attendance.validation.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { MAX_WORK_HOURS } from "./attendance.constants.js"; export const checkInSchema = z.object({ employeeId: z.number().int().positive(), @@ -10,21 +11,23 @@ export const checkOutSchema = z.object({ notes: z.string().max(500).optional(), }); -const MAX_WORK_HOURS = 24; - export const regularizeSchema = z.object({ employeeId: z.number().int().positive(), date: z.string().datetime(), checkIn: z.string().datetime(), checkOut: z.string().datetime(), notes: z.string().min(1, "Reason for regularization is required").max(500), -}).refine((data) => { - const workHours = (new Date(data.checkOut).getTime() - new Date(data.checkIn).getTime()) / 3600000; - return workHours > 0; -}, { message: "checkOut must be after checkIn" }).refine((data) => { +}).superRefine((data, ctx) => { const workHours = (new Date(data.checkOut).getTime() - new Date(data.checkIn).getTime()) / 3600000; - return workHours <= MAX_WORK_HOURS; -}, { message: `Work hours must not exceed ${MAX_WORK_HOURS}` }); + + if (workHours <= 0) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "checkOut must be after checkIn", path: ["checkOut"] }); + } + + if (workHours > MAX_WORK_HOURS) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Work hours must not exceed ${MAX_WORK_HOURS}`, path: ["checkOut"] }); + } +}); export const attendanceQuerySchema = z.object({ page: z.coerce.number().int().positive().default(1),