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,8 +1,10 @@
import { verifyAuthenticationResponse, type AuthenticationResponseJSON } from '@simplewebauthn/server'
import { getUserById, getCredentialForVerification, updateCredentialCounter } from '../../../../utils/user-repository'
import { consumeLoginChallenge, getWebAuthnConfig, toWebAuthnCredential } from '../../../../utils/webauthn'
import { signInUser } from '../../../../utils/auth'
import { assertBadRequest, httpError } from '../../../../utils/http'
import { getCredentialForVerification, updateCredentialCounter } from '../../../../utils/user-repository'
import { requireExistingUser } from '../../../../utils/users'
import { consumeLoginChallenge, getWebAuthnConfig, toWebAuthnCredential } from '../../../../utils/webauthn'
export default defineEventHandler(async (event) => {
const body = await readBody<{
@@ -15,29 +17,19 @@ export default defineEventHandler(async (event) => {
const challengeToken = body.challengeToken?.trim() || ''
const remember = body.remember !== false
if (!response || !challengeToken) {
throw createError({
statusCode: 400,
statusMessage: 'Passkey login payload is incomplete'
})
}
assertBadRequest(response, 'Passkey login payload is incomplete')
assertBadRequest(challengeToken, 'Passkey login payload is incomplete')
const expectedChallenge = await consumeLoginChallenge(challengeToken)
if (!expectedChallenge) {
throw createError({
statusCode: 400,
statusMessage: 'Passkey login challenge expired. Try again.'
})
httpError(400, 'Passkey login challenge expired. Try again.')
}
const storedCredential = await getCredentialForVerification(response.id)
if (!storedCredential) {
throw createError({
statusCode: 401,
statusMessage: 'Passkey is not recognized'
})
httpError(401, 'Passkey is not recognized')
}
const config = getWebAuthnConfig(event)
@@ -50,19 +42,13 @@ export default defineEventHandler(async (event) => {
})
if (!verification.verified) {
throw createError({
statusCode: 401,
statusMessage: 'Passkey authentication failed'
})
httpError(401, 'Passkey authentication failed')
}
const user = await getUserById(storedCredential.userId)
const user = await requireExistingUser(storedCredential.userId, 'User account is not available')
if (!user || !user.isActive) {
throw createError({
statusCode: 401,
statusMessage: 'User account is not available'
})
if (!user.isActive) {
httpError(401, 'User account is not available')
}
await updateCredentialCounter({

View File

@@ -1,7 +1,9 @@
import { verifyRegistrationResponse, type RegistrationResponseJSON } from '@simplewebauthn/server'
import { requireAuth } from '../../../../utils/auth'
import { createUserPasskey, getUserById, listUserPasskeys } from '../../../../utils/user-repository'
import { assertBadRequest, httpError, mapDatabaseError } from '../../../../utils/http'
import { createUserPasskey, listUserPasskeys } from '../../../../utils/user-repository'
import { requireExistingUser } from '../../../../utils/users'
import { buildPasskeyLabel, consumeRegistrationChallenge, getWebAuthnConfig } from '../../../../utils/webauthn'
export default defineEventHandler(async (event) => {
@@ -10,20 +12,12 @@ export default defineEventHandler(async (event) => {
response?: RegistrationResponseJSON
}>(event)
if (!body.response) {
throw createError({
statusCode: 400,
statusMessage: 'Passkey registration response is required'
})
}
assertBadRequest(body.response, 'Passkey registration response is required')
const expectedChallenge = await consumeRegistrationChallenge(auth.user.id)
if (!expectedChallenge) {
throw createError({
statusCode: 400,
statusMessage: 'Passkey registration challenge expired. Try again.'
})
httpError(400, 'Passkey registration challenge expired. Try again.')
}
const config = getWebAuthnConfig(event)
@@ -35,10 +29,7 @@ export default defineEventHandler(async (event) => {
})
if (!verification.verified || !verification.registrationInfo) {
throw createError({
statusCode: 400,
statusMessage: 'Passkey registration could not be verified'
})
httpError(400, 'Passkey registration could not be verified')
}
try {
@@ -52,18 +43,16 @@ export default defineEventHandler(async (event) => {
transports: body.response.response.transports || [],
label: buildPasskeyLabel()
})
} catch (error: any) {
if (error?.code === '23505') {
throw createError({
} catch (error) {
mapDatabaseError(error, {
'23505': {
statusCode: 409,
statusMessage: 'This passkey is already registered'
})
}
throw error
}
})
}
const updatedUser = await getUserById(auth.user.id)
const updatedUser = await requireExistingUser(auth.user.id, 'Unable to load updated user')
const passkeys = await listUserPasskeys(auth.user.id)
return {