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
104 lines
2.4 KiB
TypeScript
104 lines
2.4 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
|
|
import type { H3Event } from 'h3'
|
|
|
|
import { deleteCookie, getCookie, getRequestURL, setCookie } from 'h3'
|
|
|
|
import { randomToken } from './base64url'
|
|
import { getRedisClient } from './redis'
|
|
|
|
const DEFAULT_SESSION_TTL_SECONDS = 60 * 60 * 24 * 7
|
|
const SHORT_SESSION_TTL_SECONDS = 60 * 60 * 24
|
|
|
|
interface StoredSession {
|
|
id: string
|
|
userId: string
|
|
remember: boolean
|
|
createdAt: string
|
|
}
|
|
|
|
function getSessionCookieName() {
|
|
const config = useRuntimeConfig()
|
|
return config.sessionCookieName || 'dinner_ticket_session'
|
|
}
|
|
|
|
function getSessionStorageKey(token: string) {
|
|
return `auth:session:${token}`
|
|
}
|
|
|
|
function shouldUseSecureCookies(event: H3Event) {
|
|
const url = getRequestURL(event)
|
|
return url.protocol === 'https:'
|
|
}
|
|
|
|
function getSessionTtl(remember: boolean) {
|
|
return remember ? DEFAULT_SESSION_TTL_SECONDS : SHORT_SESSION_TTL_SECONDS
|
|
}
|
|
|
|
export async function createUserSession(event: H3Event, input: {
|
|
userId: string
|
|
remember: boolean
|
|
}) {
|
|
const token = randomToken(32)
|
|
const session: StoredSession = {
|
|
id: randomUUID(),
|
|
userId: input.userId,
|
|
remember: input.remember,
|
|
createdAt: new Date().toISOString()
|
|
}
|
|
const redis = await getRedisClient()
|
|
const ttl = getSessionTtl(input.remember)
|
|
|
|
await redis.set(getSessionStorageKey(token), JSON.stringify(session), {
|
|
expiration: {
|
|
type: 'EX',
|
|
value: ttl
|
|
}
|
|
})
|
|
|
|
setCookie(event, getSessionCookieName(), token, {
|
|
httpOnly: true,
|
|
sameSite: 'lax',
|
|
secure: shouldUseSecureCookies(event),
|
|
path: '/',
|
|
maxAge: ttl
|
|
})
|
|
|
|
return session
|
|
}
|
|
|
|
export async function getUserSession(event: H3Event): Promise<StoredSession | null> {
|
|
const token = getCookie(event, getSessionCookieName())
|
|
|
|
if (!token) {
|
|
return null
|
|
}
|
|
|
|
const redis = await getRedisClient()
|
|
const raw = await redis.get(getSessionStorageKey(token))
|
|
|
|
if (!raw) {
|
|
deleteCookie(event, getSessionCookieName(), { path: '/' })
|
|
return null
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(raw) as StoredSession
|
|
} catch {
|
|
await redis.del(getSessionStorageKey(token))
|
|
deleteCookie(event, getSessionCookieName(), { path: '/' })
|
|
return null
|
|
}
|
|
}
|
|
|
|
export async function destroyUserSession(event: H3Event) {
|
|
const token = getCookie(event, getSessionCookieName())
|
|
|
|
if (token) {
|
|
const redis = await getRedisClient()
|
|
await redis.del(getSessionStorageKey(token))
|
|
}
|
|
|
|
deleteCookie(event, getSessionCookieName(), { path: '/' })
|
|
}
|