mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-10 19:36:04 +08:00
feat: add manual invitation generator
This commit is contained in:
63
server/api/service/invitations/manual.post.ts
Normal file
63
server/api/service/invitations/manual.post.ts
Normal 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,
|
||||
}
|
||||
})
|
||||
@@ -9,7 +9,7 @@ export interface InvitationCodeDocument extends mongoose.Document {
|
||||
expiresAt?: Date
|
||||
usedBy?: mongoose.Types.ObjectId
|
||||
usedAt?: Date
|
||||
channel: 'user' | 'bootstrap'
|
||||
channel: 'user' | 'bootstrap' | 'manual'
|
||||
label?: string
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const invitationSchema = new mongoose.Schema<InvitationCodeDocument>({
|
||||
expiresAt: { type: Date },
|
||||
usedBy: { type: Schema.Types.ObjectId, ref: 'User' },
|
||||
usedAt: { type: Date },
|
||||
channel: { type: String, enum: ['user', 'bootstrap'], default: 'user' },
|
||||
channel: { type: String, enum: ['user', 'bootstrap', 'manual'], default: 'user' },
|
||||
label: { type: String, trim: true },
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user