improved error messages

This commit is contained in:
Oliver Bryan
2026-01-16 23:18:35 +00:00
parent 28d464299f
commit 7d0e8df6a3
2 changed files with 39 additions and 37 deletions

View File

@@ -98,7 +98,9 @@ export default function LogInForm() {
} }
// bad request (probably a bad user input) // bad request (probably a bad user input)
else if (res.status === 400) { 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 { } else {
setError("An unknown error occured."); setError("An unknown error occured.");
} }

View File

@@ -24,25 +24,25 @@ export type ApiError = z.infer<typeof ApiErrorSchema>;
// auth schemas // auth schemas
export const LoginRequestSchema = z.object({ export const LoginRequestSchema = z.object({
username: z.string().min(1, "username is required").max(USER_USERNAME_MAX_LENGTH), username: z.string().min(1, "Username is required").max(USER_USERNAME_MAX_LENGTH),
password: z.string().min(1, "password is required"), password: z.string().min(1, "Password is required"),
}); });
export type LoginRequest = z.infer<typeof LoginRequestSchema>; export type LoginRequest = z.infer<typeof LoginRequestSchema>;
export const RegisterRequestSchema = z.object({ 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 username: z
.string() .string()
.min(1, "username is required") .min(1, "Username is required")
.max(USER_USERNAME_MAX_LENGTH) .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 password: z
.string() .string()
.min(8, "password must be at least 8 characters") .min(8, "Password must be at least 8 characters")
.regex(/[A-Z]/, "password must contain an uppercase letter") .regex(/[A-Z]/, "Password must contain an uppercase letter")
.regex(/[a-z]/, "password must contain a lowercase letter") .regex(/[a-z]/, "Password must contain a lowercase letter")
.regex(/[0-9]/, "password must contain a number"), .regex(/[0-9]/, "Password must contain a number"),
avatarURL: z.string().url().nullable(), avatarURL: z.string().url().nullable(),
}); });
@@ -64,7 +64,7 @@ export type AuthResponse = z.infer<typeof AuthResponseSchema>;
export const IssueCreateRequestSchema = z.object({ export const IssueCreateRequestSchema = z.object({
projectId: z.number().int().positive("projectId must be a positive integer"), 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(""), description: z.string().max(ISSUE_DESCRIPTION_MAX_LENGTH).default(""),
status: z.string().max(ISSUE_STATUS_MAX_LENGTH).optional(), status: z.string().max(ISSUE_STATUS_MAX_LENGTH).optional(),
assigneeIds: z.array(z.number().int().positive()).optional(), assigneeIds: z.array(z.number().int().positive()).optional(),
@@ -75,7 +75,7 @@ export type IssueCreateRequest = z.infer<typeof IssueCreateRequestSchema>;
export const IssueUpdateRequestSchema = z.object({ export const IssueUpdateRequestSchema = z.object({
id: z.number().int().positive("id must be a positive integer"), 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(), description: z.string().max(ISSUE_DESCRIPTION_MAX_LENGTH).optional(),
status: z.string().max(ISSUE_STATUS_MAX_LENGTH).optional(), status: z.string().max(ISSUE_STATUS_MAX_LENGTH).optional(),
assigneeIds: z.array(z.number().int().positive()).nullable().optional(), assigneeIds: z.array(z.number().int().positive()).nullable().optional(),
@@ -98,7 +98,7 @@ export type IssuesByProjectQuery = z.infer<typeof IssuesByProjectQuerySchema>;
export const IssuesStatusCountQuerySchema = z.object({ export const IssuesStatusCountQuerySchema = z.object({
organisationId: z.coerce.number().int().positive("organisationId must be a positive integer"), 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<typeof IssuesStatusCountQuerySchema>; export type IssuesStatusCountQuery = z.infer<typeof IssuesStatusCountQuerySchema>;
@@ -114,12 +114,12 @@ export type IssuesReplaceStatusRequest = z.infer<typeof IssuesReplaceStatusReque
// organisation schemas // organisation schemas
export const OrgCreateRequestSchema = z.object({ export const OrgCreateRequestSchema = z.object({
name: z.string().min(1, "name is required").max(ORG_NAME_MAX_LENGTH), name: z.string().min(1, "Name is required").max(ORG_NAME_MAX_LENGTH),
slug: z slug: z
.string() .string()
.min(1, "slug is required") .min(1, "Slug is required")
.max(ORG_SLUG_MAX_LENGTH) .max(ORG_SLUG_MAX_LENGTH)
.regex(/^[a-z0-9-]+$/, "slug can only contain lowercase letters, numbers, and hyphens"), .regex(/^[a-z0-9-]+$/, "Slug can only contain lowercase letters, numbers, and hyphens"),
description: z.string().max(ORG_DESCRIPTION_MAX_LENGTH).optional(), description: z.string().max(ORG_DESCRIPTION_MAX_LENGTH).optional(),
}); });
@@ -127,20 +127,20 @@ export type OrgCreateRequest = z.infer<typeof OrgCreateRequestSchema>;
export const OrgUpdateRequestSchema = z.object({ export const OrgUpdateRequestSchema = z.object({
id: z.number().int().positive("id must be a positive integer"), 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(), description: z.string().max(ORG_DESCRIPTION_MAX_LENGTH).optional(),
slug: z slug: z
.string() .string()
.min(1) .min(1, "Slug must be at least 1 character")
.max(ORG_SLUG_MAX_LENGTH) .max(ORG_SLUG_MAX_LENGTH)
.regex(/^[a-z0-9-]+$/) .regex(/^[a-z0-9-]+$/)
.optional(), .optional(),
statuses: z statuses: z
.record(z.string()) .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( .refine(
(obj) => Object.keys(obj).every((key) => key.length <= ISSUE_STATUS_MAX_LENGTH), (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(), .optional(),
}); });
@@ -191,11 +191,11 @@ export type OrgUpdateMemberRoleRequest = z.infer<typeof OrgUpdateMemberRoleReque
// project schemas // project schemas
export const ProjectCreateRequestSchema = z.object({ export const ProjectCreateRequestSchema = z.object({
name: z.string().min(1, "name is required").max(PROJECT_NAME_MAX_LENGTH), name: z.string().min(1, "Name is required").max(PROJECT_NAME_MAX_LENGTH),
key: z key: z
.string() .string()
.length(4, "key must be exactly 4 characters") .length(4, "Key must be exactly 4 characters")
.regex(/^[A-Z]{4}$/, "key must be 4 uppercase letters"), .regex(/^[A-Z]{4}$/, "Key must be 4 uppercase letters"),
organisationId: z.number().int().positive("organisationId must be a positive integer"), organisationId: z.number().int().positive("organisationId must be a positive integer"),
}); });
@@ -203,11 +203,11 @@ export type ProjectCreateRequest = z.infer<typeof ProjectCreateRequestSchema>;
export const ProjectUpdateRequestSchema = z.object({ export const ProjectUpdateRequestSchema = z.object({
id: z.number().int().positive("id must be a positive integer"), 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 key: z
.string() .string()
.length(4) .length(4, "Key must be exactly 4 characters")
.regex(/^[A-Z]{4}$/) .regex(/^[A-Z]{4}$/, "Key must be 4 uppercase letters")
.optional(), .optional(),
creatorId: z.number().int().positive().optional(), creatorId: z.number().int().positive().optional(),
organisationId: z.number().int().positive().optional(), organisationId: z.number().int().positive().optional(),
@@ -244,16 +244,16 @@ export type ProjectByCreatorQuery = z.infer<typeof ProjectByCreatorQuerySchema>;
export const SprintCreateRequestSchema = z export const SprintCreateRequestSchema = z
.object({ .object({
projectId: z.number().int().positive("projectId must be a positive integer"), 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 color: z
.string() .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(), .optional(),
startDate: z.string().datetime("startDate must be a valid ISO datetime"), startDate: z.string().datetime("Start date must be a valid date"),
endDate: z.string().datetime("endDate must be a valid ISO datetime"), endDate: z.string().datetime("End date must be a valid date"),
}) })
.refine((data) => new Date(data.startDate) <= new Date(data.endDate), { .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"], path: ["endDate"],
}); });
@@ -288,13 +288,13 @@ export type TimerGetQuery = z.infer<typeof TimerGetQuerySchema>;
// user schemas // user schemas
export const UserUpdateRequestSchema = z.object({ 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 password: z
.string() .string()
.min(8) .min(8, "Password must be at least 8 characters")
.regex(/[A-Z]/, "password must contain an uppercase letter") .regex(/[A-Z]/, "Password must contain an uppercase letter")
.regex(/[a-z]/, "password must contain a lowercase letter") .regex(/[a-z]/, "Password must contain a lowercase letter")
.regex(/[0-9]/, "password must contain a number") .regex(/[0-9]/, "Password must contain a number")
.optional(), .optional(),
avatarURL: z.string().url().nullable().optional(), avatarURL: z.string().url().nullable().optional(),
}); });
@@ -302,7 +302,7 @@ export const UserUpdateRequestSchema = z.object({
export type UserUpdateRequest = z.infer<typeof UserUpdateRequestSchema>; export type UserUpdateRequest = z.infer<typeof UserUpdateRequestSchema>;
export const UserByUsernameQuerySchema = z.object({ 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<typeof UserByUsernameQuerySchema>; export type UserByUsernameQuery = z.infer<typeof UserByUsernameQuerySchema>;