feat: add manual invitation generator

This commit is contained in:
Remi
2025-09-17 20:26:33 +02:00
parent f1d18a267b
commit 3e4c1093b8
6 changed files with 221 additions and 2 deletions

View File

@@ -0,0 +1,63 @@
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto'
import { createError, readBody } from 'h3'
import { useRuntimeConfig } from '#imports'
import { InvitationCode } from '../../../models/InvitationCode'
interface ManualInviteRequestBody {
password?: string
label?: string
}
interface ManualInviteResponse {
success: true
code: string
expiresAt: string
label: string | null
}
function generateCode() {
return randomBytes(4).toString('hex').toUpperCase()
}
function safeComparePassword(provided: string, expected: string) {
const key = 'opensquawk-manual-invite'
const providedDigest = createHmac('sha256', key).update(provided).digest()
const expectedDigest = createHmac('sha256', key).update(expected).digest()
return timingSafeEqual(providedDigest, expectedDigest)
}
export default defineEventHandler<ManualInviteResponse>(async (event) => {
const config = useRuntimeConfig()
const expectedPassword = (config.manualInvitePassword as string | undefined)?.trim() || ''
if (!expectedPassword) {
throw createError({ statusCode: 500, statusMessage: 'Konfiguration für manuellen Einladungscode fehlt' })
}
const body = await readBody<ManualInviteRequestBody>(event).catch(() => ({}) as ManualInviteRequestBody)
const providedPassword = body.password?.trim() || ''
if (!providedPassword || !safeComparePassword(providedPassword, expectedPassword)) {
throw createError({ statusCode: 401, statusMessage: 'Ungültiges Passwort' })
}
const now = new Date()
const code = generateCode()
const expiresAt = new Date(now.getTime() + 1000 * 60 * 60 * 24 * 30)
const label = body.label?.trim() || undefined
await InvitationCode.create({
code,
createdAt: now,
expiresAt,
channel: 'manual',
label,
})
return {
success: true,
code,
expiresAt: expiresAt.toISOString(),
label: label ?? null,
}
})