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)
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.");
}

View File

@@ -24,25 +24,25 @@ export type ApiError = z.infer<typeof ApiErrorSchema>;
// 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<typeof LoginRequestSchema>;
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<typeof AuthResponseSchema>;
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<typeof IssueCreateRequestSchema>;
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<typeof IssuesByProjectQuerySchema>;
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<typeof IssuesStatusCountQuerySchema>;
@@ -114,12 +114,12 @@ export type IssuesReplaceStatusRequest = z.infer<typeof IssuesReplaceStatusReque
// organisation schemas
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
.string()
.min(1, "slug is required")
.min(1, "Slug is required")
.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(),
});
@@ -127,20 +127,20 @@ export type OrgCreateRequest = z.infer<typeof OrgCreateRequestSchema>;
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<typeof OrgUpdateMemberRoleReque
// project schemas
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
.string()
.length(4, "key must be exactly 4 characters")
.regex(/^[A-Z]{4}$/, "key must be 4 uppercase letters"),
.length(4, "Key must be exactly 4 characters")
.regex(/^[A-Z]{4}$/, "Key must be 4 uppercase letters"),
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({
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<typeof ProjectByCreatorQuerySchema>;
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<typeof TimerGetQuerySchema>;
// 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<typeof UserUpdateRequestSchema>;
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>;