feat: implement auth system, passkeys, and user management

Add PostgreSQL and Redis integration for users and sessions
Implement password and WebAuthn passkey login flows
Add Docker stack, super-admin seeding, and protected routes
This commit is contained in:
2026-04-12 20:16:43 +08:00
parent a649c509c2
commit 377a9617be
45 changed files with 3620 additions and 104 deletions

View File

@@ -0,0 +1,74 @@
import { verifyRegistrationResponse, type RegistrationResponseJSON } from '@simplewebauthn/server'
import { requireAuth } from '../../../../utils/auth'
import { createUserPasskey, getUserById, listUserPasskeys } from '../../../../utils/user-repository'
import { buildPasskeyLabel, consumeRegistrationChallenge, getWebAuthnConfig } from '../../../../utils/webauthn'
export default defineEventHandler(async (event) => {
const auth = await requireAuth(event)
const body = await readBody<{
response?: RegistrationResponseJSON
}>(event)
if (!body.response) {
throw createError({
statusCode: 400,
statusMessage: '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.'
})
}
const config = getWebAuthnConfig(event)
const verification = await verifyRegistrationResponse({
response: body.response,
expectedChallenge,
expectedOrigin: config.origin,
expectedRPID: config.rpID
})
if (!verification.verified || !verification.registrationInfo) {
throw createError({
statusCode: 400,
statusMessage: 'Passkey registration could not be verified'
})
}
try {
await createUserPasskey({
userId: auth.user.id,
credentialId: verification.registrationInfo.credential.id,
publicKey: verification.registrationInfo.credential.publicKey,
counter: verification.registrationInfo.credential.counter,
deviceType: verification.registrationInfo.credentialDeviceType,
backedUp: verification.registrationInfo.credentialBackedUp,
transports: body.response.response.transports || [],
label: buildPasskeyLabel()
})
} catch (error: any) {
if (error?.code === '23505') {
throw createError({
statusCode: 409,
statusMessage: 'This passkey is already registered'
})
}
throw error
}
const updatedUser = await getUserById(auth.user.id)
const passkeys = await listUserPasskeys(auth.user.id)
return {
ok: true,
user: updatedUser,
passkeys
}
})