54 lines
1.8 KiB
TypeScript
54 lines
1.8 KiB
TypeScript
import { createHmac, timingSafeEqual } from 'node:crypto'
|
|
import type { H3Event } from 'h3'
|
|
import { getCookie, setCookie, deleteCookie, createError } from 'h3'
|
|
import { prisma } from './db'
|
|
|
|
const cookieName = 'money_session'
|
|
|
|
function secret() {
|
|
return useRuntimeConfig().sessionSecret
|
|
}
|
|
|
|
function sign(value: string) {
|
|
return createHmac('sha256', secret()).update(value).digest('hex')
|
|
}
|
|
|
|
export function createSessionCookie(userId: string) {
|
|
const payload = Buffer.from(JSON.stringify({ userId })).toString('base64url')
|
|
return `${payload}.${sign(payload)}`
|
|
}
|
|
|
|
export function setSession(event: H3Event, userId: string) {
|
|
setCookie(event, cookieName, createSessionCookie(userId), {
|
|
httpOnly: true,
|
|
sameSite: 'lax',
|
|
secure: process.env.NODE_ENV === 'production',
|
|
maxAge: 60 * 60 * 24 * 180,
|
|
path: '/'
|
|
})
|
|
}
|
|
|
|
export function clearAuthSession(event: H3Event) {
|
|
deleteCookie(event, cookieName, { path: '/' })
|
|
}
|
|
|
|
export async function requireUser(event: H3Event) {
|
|
const raw = getCookie(event, cookieName)
|
|
if (!raw) throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
|
|
|
|
const [payload, signature] = raw.split('.')
|
|
if (!payload || !signature) throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
|
|
|
|
const expected = sign(payload)
|
|
const isValid = timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
|
|
if (!isValid) throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
|
|
|
|
const parsed = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as { userId: string }
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: parsed.userId },
|
|
select: { id: true, email: true, name: true }
|
|
})
|
|
if (!user) throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
|
|
return user
|
|
}
|