From 7d0e8df6a3729b9ded41f0b0d6260fbeca6591c7 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Fri, 16 Jan 2026 23:18:35 +0000 Subject: [PATCH] improved error messages --- .../frontend/src/components/login-form.tsx | 4 +- packages/shared/src/api-schemas.ts | 72 +++++++++---------- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/packages/frontend/src/components/login-form.tsx b/packages/frontend/src/components/login-form.tsx index 1406a23..882914b 100644 --- a/packages/frontend/src/components/login-form.tsx +++ b/packages/frontend/src/components/login-form.tsx @@ -98,7 +98,9 @@ export default function LogInForm() { } // bad request (probably a bad user input) else if (res.status === 400) { - setError(await res.text()); + const data = await res.json(); + const firstDetail = data.details ? Object.values(data.details).flat().find(Boolean) : ""; + setError(firstDetail || data.error || "Bad request"); } else { setError("An unknown error occured."); } diff --git a/packages/shared/src/api-schemas.ts b/packages/shared/src/api-schemas.ts index 8d038b2..ba4bfc8 100644 --- a/packages/shared/src/api-schemas.ts +++ b/packages/shared/src/api-schemas.ts @@ -24,25 +24,25 @@ export type ApiError = z.infer; // auth schemas export const LoginRequestSchema = z.object({ - username: z.string().min(1, "username is required").max(USER_USERNAME_MAX_LENGTH), - password: z.string().min(1, "password is required"), + username: z.string().min(1, "Username is required").max(USER_USERNAME_MAX_LENGTH), + password: z.string().min(1, "Password is required"), }); export type LoginRequest = z.infer; export const RegisterRequestSchema = z.object({ - name: z.string().min(1, "name is required").max(USER_NAME_MAX_LENGTH), + name: z.string().min(1, "Name is required").max(USER_NAME_MAX_LENGTH), username: z .string() - .min(1, "username is required") + .min(1, "Username is required") .max(USER_USERNAME_MAX_LENGTH) - .regex(/^[a-zA-Z0-9_-]+$/, "username can only contain letters, numbers, underscores, and hyphens"), + .regex(/^[a-zA-Z0-9_-]+$/, "Username can only contain letters, numbers, underscores, and hyphens"), password: z .string() - .min(8, "password must be at least 8 characters") - .regex(/[A-Z]/, "password must contain an uppercase letter") - .regex(/[a-z]/, "password must contain a lowercase letter") - .regex(/[0-9]/, "password must contain a number"), + .min(8, "Password must be at least 8 characters") + .regex(/[A-Z]/, "Password must contain an uppercase letter") + .regex(/[a-z]/, "Password must contain a lowercase letter") + .regex(/[0-9]/, "Password must contain a number"), avatarURL: z.string().url().nullable(), }); @@ -64,7 +64,7 @@ export type AuthResponse = z.infer; export const IssueCreateRequestSchema = z.object({ projectId: z.number().int().positive("projectId must be a positive integer"), - title: z.string().min(1, "title is required").max(ISSUE_TITLE_MAX_LENGTH), + title: z.string().min(1, "Title is required").max(ISSUE_TITLE_MAX_LENGTH), description: z.string().max(ISSUE_DESCRIPTION_MAX_LENGTH).default(""), status: z.string().max(ISSUE_STATUS_MAX_LENGTH).optional(), assigneeIds: z.array(z.number().int().positive()).optional(), @@ -75,7 +75,7 @@ export type IssueCreateRequest = z.infer; export const IssueUpdateRequestSchema = z.object({ id: z.number().int().positive("id must be a positive integer"), - title: z.string().min(1).max(ISSUE_TITLE_MAX_LENGTH).optional(), + title: z.string().min(1, "Title must be at least 1 character").max(ISSUE_TITLE_MAX_LENGTH).optional(), description: z.string().max(ISSUE_DESCRIPTION_MAX_LENGTH).optional(), status: z.string().max(ISSUE_STATUS_MAX_LENGTH).optional(), assigneeIds: z.array(z.number().int().positive()).nullable().optional(), @@ -98,7 +98,7 @@ export type IssuesByProjectQuery = z.infer; export const IssuesStatusCountQuerySchema = z.object({ organisationId: z.coerce.number().int().positive("organisationId must be a positive integer"), - status: z.string().min(1, "status is required").max(ISSUE_STATUS_MAX_LENGTH), + status: z.string().min(1, "Status is required").max(ISSUE_STATUS_MAX_LENGTH), }); export type IssuesStatusCountQuery = z.infer; @@ -114,12 +114,12 @@ export type IssuesReplaceStatusRequest = z.infer; export const OrgUpdateRequestSchema = z.object({ id: z.number().int().positive("id must be a positive integer"), - name: z.string().min(1).max(ORG_NAME_MAX_LENGTH).optional(), + name: z.string().min(1, "Name must be at least 1 character").max(ORG_NAME_MAX_LENGTH).optional(), description: z.string().max(ORG_DESCRIPTION_MAX_LENGTH).optional(), slug: z .string() - .min(1) + .min(1, "Slug must be at least 1 character") .max(ORG_SLUG_MAX_LENGTH) .regex(/^[a-z0-9-]+$/) .optional(), statuses: z .record(z.string()) - .refine((obj) => Object.keys(obj).length > 0, "statuses must have at least one entry") + .refine((obj) => Object.keys(obj).length > 0, "Statuses must have at least one entry") .refine( (obj) => Object.keys(obj).every((key) => key.length <= ISSUE_STATUS_MAX_LENGTH), - `status keys must be <= ${ISSUE_STATUS_MAX_LENGTH} characters`, + `Status keys must be <= ${ISSUE_STATUS_MAX_LENGTH} characters`, ) .optional(), }); @@ -191,11 +191,11 @@ export type OrgUpdateMemberRoleRequest = z.infer; export const ProjectUpdateRequestSchema = z.object({ id: z.number().int().positive("id must be a positive integer"), - name: z.string().min(1).max(PROJECT_NAME_MAX_LENGTH).optional(), + name: z.string().min(1, "Name must be at least 1 character").max(PROJECT_NAME_MAX_LENGTH).optional(), key: z .string() - .length(4) - .regex(/^[A-Z]{4}$/) + .length(4, "Key must be exactly 4 characters") + .regex(/^[A-Z]{4}$/, "Key must be 4 uppercase letters") .optional(), creatorId: z.number().int().positive().optional(), organisationId: z.number().int().positive().optional(), @@ -244,16 +244,16 @@ export type ProjectByCreatorQuery = z.infer; export const SprintCreateRequestSchema = z .object({ projectId: z.number().int().positive("projectId must be a positive integer"), - name: z.string().min(1, "name is required").max(64), + name: z.string().min(1, "Name is required").max(64, "Name must be at most 64 characters"), color: z .string() - .regex(/^#[0-9a-fA-F]{6}$/, "color must be a valid hex color") + .regex(/^#[0-9a-fA-F]{6}$/, "Color must be a valid hex color") .optional(), - startDate: z.string().datetime("startDate must be a valid ISO datetime"), - endDate: z.string().datetime("endDate must be a valid ISO datetime"), + startDate: z.string().datetime("Start date must be a valid date"), + endDate: z.string().datetime("End date must be a valid date"), }) .refine((data) => new Date(data.startDate) <= new Date(data.endDate), { - message: "endDate must be after startDate", + message: "End date must be after start date", path: ["endDate"], }); @@ -288,13 +288,13 @@ export type TimerGetQuery = z.infer; // user schemas export const UserUpdateRequestSchema = z.object({ - name: z.string().min(1).max(USER_NAME_MAX_LENGTH).optional(), + name: z.string().min(1, "Name must be at least 1 character").max(USER_NAME_MAX_LENGTH).optional(), password: z .string() - .min(8) - .regex(/[A-Z]/, "password must contain an uppercase letter") - .regex(/[a-z]/, "password must contain a lowercase letter") - .regex(/[0-9]/, "password must contain a number") + .min(8, "Password must be at least 8 characters") + .regex(/[A-Z]/, "Password must contain an uppercase letter") + .regex(/[a-z]/, "Password must contain a lowercase letter") + .regex(/[0-9]/, "Password must contain a number") .optional(), avatarURL: z.string().url().nullable().optional(), }); @@ -302,7 +302,7 @@ export const UserUpdateRequestSchema = z.object({ export type UserUpdateRequest = z.infer; export const UserByUsernameQuerySchema = z.object({ - username: z.string().min(1, "username is required"), + username: z.string().min(1, "Username is required"), }); export type UserByUsernameQuery = z.infer;