refactor: centralize validation, error handling, and formatting logic

Extract shared auth logic and validation rules to shared/auth.ts
Introduce utility functions for HTTP errors and user input parsing
Standardize error messages and date formatting across the app
This commit is contained in:
2026-04-12 20:29:39 +08:00
parent 377a9617be
commit 07e5d42005
23 changed files with 294 additions and 267 deletions

View File

@@ -1,14 +1,10 @@
import type { H3Event } from 'h3'
import type { UserRole } from '~~/shared/auth'
import { normalizeUsername, type UserRole } from '~~/shared/auth'
import { createUserSession, destroyUserSession, getUserSession } from './session'
import { getUserById, updateLastLogin, type UserAuthRecord } from './user-repository'
export function normalizeUsername(value: string) {
return value.trim().toLowerCase()
}
export async function getAuthContext(event: H3Event): Promise<{
session: Awaited<ReturnType<typeof getUserSession>>
user: UserAuthRecord

46
server/utils/http.ts Normal file
View File

@@ -0,0 +1,46 @@
import type { H3Event } from 'h3'
import { getRouterParam } from 'h3'
export function httpError(statusCode: number, statusMessage: string): never {
throw createError({
statusCode,
statusMessage
})
}
export function assertHttp(
condition: unknown,
statusCode: number,
statusMessage: string
): asserts condition {
if (!condition) {
httpError(statusCode, statusMessage)
}
}
export function assertBadRequest(condition: unknown, statusMessage: string): asserts condition {
assertHttp(condition, 400, statusMessage)
}
export function getRequiredRouteParam(event: H3Event, name: string, label = name) {
const value = getRouterParam(event, name)
assertBadRequest(value, `${label} is required`)
return value
}
export function mapDatabaseError(
error: unknown,
handlers: Partial<Record<string, { statusCode: number, statusMessage: string }>>
): never {
const code = (error as { code?: string } | null)?.code
const handler = code ? handlers[code] : undefined
if (handler) {
httpError(handler.statusCode, handler.statusMessage)
}
throw error
}

View File

@@ -56,11 +56,7 @@ export interface PasskeyRecord {
lastUsedAt: string | null
}
function parseCount(value: number | string): number {
return typeof value === 'number' ? value : Number.parseInt(value, 10)
}
function parseCounter(value: number | string): number {
function parseInteger(value: number | string): number {
return typeof value === 'number' ? value : Number.parseInt(value, 10)
}
@@ -78,7 +74,7 @@ function parseTransports(value: AuthenticatorTransportFuture[] | string): Authen
}
function mapAuthUser(row: DbUserRow): AuthUser {
const passkeyCount = parseCount(row.passkey_count)
const passkeyCount = parseInteger(row.passkey_count)
return {
id: row.id,
@@ -116,7 +112,7 @@ function mapPasskeyRecord(row: DbPasskeyRow): PasskeyRecord {
userId: row.user_id,
credentialId: row.credential_id,
publicKey: row.public_key,
counter: parseCounter(row.counter),
counter: parseInteger(row.counter),
deviceType: row.device_type,
backedUp: row.backed_up,
transports: parseTransports(row.transports),

75
server/utils/users.ts Normal file
View File

@@ -0,0 +1,75 @@
import type { H3Event } from 'h3'
import {
hasValidFullName,
isUserRole,
isValidPhoneNumber,
isValidUsername,
normalizeFullName,
normalizePhoneNumber,
normalizeUsername,
type UserRole
} from '~~/shared/auth'
import { assertBadRequest, getRequiredRouteParam, httpError } from './http'
import { getUserById } from './user-repository'
export function requireUserIdParam(event: H3Event) {
return getRequiredRouteParam(event, 'id', 'User id')
}
export async function requireExistingUser(userId: string, statusMessage = 'User not found') {
const user = await getUserById(userId)
if (!user) {
httpError(404, statusMessage)
}
return user
}
export function parseCreateUserInput(body: {
username?: string
fullName?: string
phoneNumber?: string
role?: UserRole
}) {
const username = normalizeUsername(body.username || '')
const fullName = normalizeFullName(body.fullName || '')
const phoneNumber = normalizePhoneNumber(body.phoneNumber || '')
const role = body.role === 'super_admin' ? 'super_admin' : 'staff'
assertBadRequest(
isValidUsername(username),
'Username must be 3 to 32 characters using lowercase letters, numbers, dot, dash, or underscore'
)
assertBadRequest(hasValidFullName(fullName), 'Full name must be at least 2 characters')
assertBadRequest(isValidPhoneNumber(phoneNumber), 'Phone number must contain 8 to 15 digits')
return {
username,
fullName,
phoneNumber,
role
}
}
export function parseUserProfileInput(body: {
fullName?: string
phoneNumber?: string
role?: UserRole
}) {
const fullName = normalizeFullName(body.fullName || '')
const phoneNumber = normalizePhoneNumber(body.phoneNumber || '')
const role = body.role
assertBadRequest(hasValidFullName(fullName), 'Display name must be at least 2 characters')
assertBadRequest(isValidPhoneNumber(phoneNumber), 'Phone number must contain 8 to 15 digits')
assertBadRequest(isUserRole(role), 'Role is invalid')
return {
fullName,
phoneNumber,
role
}
}