backend routes with zod schemas

This commit is contained in:
Oliver Bryan
2026-01-13 15:32:31 +00:00
parent e2cbe6bab3
commit 1ab5c80691
31 changed files with 345 additions and 609 deletions

View File

@@ -1,45 +1,32 @@
import { LoginRequestSchema } from "@issue/shared";
import type { BunRequest } from "bun";
import { buildAuthCookie, generateToken, verifyPassword } from "../../auth/utils";
import { createSession, getUserByUsername } from "../../db/queries";
const isNonEmptyString = (value: unknown): value is string =>
typeof value === "string" && value.trim().length > 0;
import { errorResponse, parseJsonBody } from "../../validation";
export default async function login(req: BunRequest) {
if (req.method !== "POST") {
return new Response("method not allowed", { status: 405 });
return errorResponse("method not allowed", "METHOD_NOT_ALLOWED", 405);
}
let body: unknown;
try {
body = await req.json();
} catch {
return new Response("invalid JSON", { status: 400 });
}
const parsed = await parseJsonBody(req, LoginRequestSchema);
if ("error" in parsed) return parsed.error;
if (!body || typeof body !== "object") {
return new Response("invalid request body", { status: 400 });
}
const { username, password } = body as Record<string, unknown>;
if (!isNonEmptyString(username) || !isNonEmptyString(password)) {
return new Response("username and password are required", { status: 400 });
}
const { username, password } = parsed.data;
const user = await getUserByUsername(username);
if (!user) {
return new Response("invalid credentials", { status: 401 });
return errorResponse("invalid credentials", "INVALID_CREDENTIALS", 401);
}
const ok = await verifyPassword(password, user.passwordHash);
if (!ok) {
return new Response("invalid credentials", { status: 401 });
return errorResponse("invalid credentials", "INVALID_CREDENTIALS", 401);
}
const session = await createSession(user.id);
if (!session) {
return new Response("failed to create session", { status: 500 });
return errorResponse("failed to create session", "SESSION_ERROR", 500);
}
const token = generateToken(session.id, user.id);

View File

@@ -1,62 +1,33 @@
import { RegisterRequestSchema } from "@issue/shared";
import type { BunRequest } from "bun";
import { buildAuthCookie, generateToken, hashPassword } from "../../auth/utils";
import { createSession, createUser, getUserByUsername } from "../../db/queries";
const isNonEmptyString = (value: unknown): value is string =>
typeof value === "string" && value.trim().length > 0;
import { errorResponse, parseJsonBody } from "../../validation";
export default async function register(req: BunRequest) {
if (req.method !== "POST") {
return new Response("method not allowed", { status: 405 });
return errorResponse("method not allowed", "METHOD_NOT_ALLOWED", 405);
}
let body: unknown;
try {
body = await req.json();
} catch {
return new Response("invalid JSON", { status: 400 });
}
const parsed = await parseJsonBody(req, RegisterRequestSchema);
if ("error" in parsed) return parsed.error;
if (!body || typeof body !== "object") {
return new Response("invalid request body", { status: 400 });
}
const { name, username, password, avatarURL } = body as Record<string, unknown>;
if (!isNonEmptyString(name) || !isNonEmptyString(username) || !isNonEmptyString(password)) {
return new Response("name, username, and password are required", { status: 400 });
}
if (username.length < 1 || username.length > 32) {
return new Response("username must be 1-32 characters", { status: 400 });
}
if (password.length < 8) {
return new Response("password must be at least 8 characters", { status: 400 });
}
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumber = /[0-9]/.test(password);
if (!hasUpperCase || !hasLowerCase || !hasNumber) {
return new Response("password must contain uppercase, lowercase, and numbers", { status: 400 });
}
const { name, username, password, avatarURL } = parsed.data;
const existing = await getUserByUsername(username);
if (existing) {
return new Response("username already taken", { status: 400 });
return errorResponse("username already taken", "USERNAME_TAKEN", 400);
}
const passwordHash = await hashPassword(password);
const user = await createUser(name, username, passwordHash, avatarURL as string | undefined);
const user = await createUser(name, username, passwordHash, avatarURL);
if (!user) {
return new Response("failed to create user", { status: 500 });
return errorResponse("failed to create user", "USER_CREATE_ERROR", 500);
}
const session = await createSession(user.id);
if (!session) {
return new Response("failed to create session", { status: 500 });
return errorResponse("failed to create session", "SESSION_ERROR", 500);
}
const token = generateToken(session.id, user.id);