mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-05 08:55:54 +08:00
Add waitlist invitation sending from admin
This commit is contained in:
80
server/api/admin/waitlist/[id]/invite.post.ts
Normal file
80
server/api/admin/waitlist/[id]/invite.post.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { createError, defineEventHandler, readBody } from 'h3'
|
||||
import { requireAdmin } from '../../../../utils/auth'
|
||||
import { WaitlistEntry } from '../../../../models/WaitlistEntry'
|
||||
import { InvitationCode } from '../../../../models/InvitationCode'
|
||||
import { sendMail } from '../../../../utils/notifications'
|
||||
import { generateInvitationCode, renderInvitationEmail, renderInvitationText } from '../../../../utils/invitations'
|
||||
|
||||
interface SendInviteBody {
|
||||
resend?: boolean
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const admin = await requireAdmin(event)
|
||||
const id = event.context.params?.id
|
||||
if (!id) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Missing waitlist entry ID' })
|
||||
}
|
||||
|
||||
await readBody<SendInviteBody>(event).catch(() => ({}) as SendInviteBody)
|
||||
|
||||
const entry = await WaitlistEntry.findById(id).exec()
|
||||
if (!entry) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Waitlist entry not found' })
|
||||
}
|
||||
|
||||
const email = entry.email?.trim()
|
||||
if (!email) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Waitlist entry has no email address' })
|
||||
}
|
||||
|
||||
let invitation = entry.invitationCode
|
||||
? await InvitationCode.findById(entry.invitationCode)
|
||||
: null
|
||||
|
||||
if (invitation?.usedAt) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Invitation already used — user is registered' })
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
|
||||
if (!invitation) {
|
||||
const code = generateInvitationCode()
|
||||
invitation = await InvitationCode.create({
|
||||
code,
|
||||
createdAt: now,
|
||||
channel: 'admin',
|
||||
label: `Waitlist: ${email}`,
|
||||
createdBy: admin._id,
|
||||
})
|
||||
|
||||
entry.invitationCode = invitation._id
|
||||
}
|
||||
|
||||
entry.invitationSentAt = now
|
||||
await entry.save()
|
||||
|
||||
const html = await renderInvitationEmail(invitation.code)
|
||||
const text = renderInvitationText(invitation.code)
|
||||
|
||||
await sendMail({
|
||||
to: email,
|
||||
subject: 'Your OpenSquawk invite code',
|
||||
text,
|
||||
html,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
invitation: {
|
||||
id: String(invitation._id),
|
||||
code: invitation.code,
|
||||
channel: invitation.channel,
|
||||
label: invitation.label || undefined,
|
||||
createdAt: invitation.createdAt?.toISOString() ?? now.toISOString(),
|
||||
expiresAt: invitation.expiresAt ? invitation.expiresAt.toISOString() : undefined,
|
||||
sentAt: entry.invitationSentAt?.toISOString() ?? now.toISOString(),
|
||||
usedAt: invitation.usedAt ? invitation.usedAt.toISOString() : undefined,
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -2,6 +2,7 @@ import { defineEventHandler, getQuery } from 'h3'
|
||||
import type { FilterQuery } from 'mongoose'
|
||||
import { requireAdmin } from '../../../utils/auth'
|
||||
import { WaitlistEntry, type WaitlistEntryDocument } from '../../../models/WaitlistEntry'
|
||||
import type { InvitationCodeDocument } from '../../../models/InvitationCode'
|
||||
|
||||
type WaitlistListItem = {
|
||||
id: string
|
||||
@@ -13,23 +14,58 @@ type WaitlistListItem = {
|
||||
activatedAt?: string
|
||||
wantsProductUpdates: boolean
|
||||
updatesOptedInAt?: string
|
||||
invitationSentAt?: string
|
||||
invitation?: {
|
||||
id: string
|
||||
code: string
|
||||
channel: string
|
||||
label?: string
|
||||
createdAt: string
|
||||
expiresAt?: string
|
||||
usedAt?: string
|
||||
sentAt?: string
|
||||
}
|
||||
}
|
||||
|
||||
function escapeRegExp(input: string) {
|
||||
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
function normalizeDate(value?: Date | string | null) {
|
||||
if (!value) return undefined
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.valueOf())) return undefined
|
||||
return date.toISOString()
|
||||
}
|
||||
|
||||
function mapWaitlistEntry(doc: any): WaitlistListItem {
|
||||
const invitationDoc = doc.invitationCode as InvitationCodeDocument | undefined
|
||||
const sentAt = normalizeDate(doc.invitationSentAt)
|
||||
|
||||
return {
|
||||
id: String(doc._id),
|
||||
email: doc.email,
|
||||
name: doc.name || undefined,
|
||||
notes: doc.notes || undefined,
|
||||
source: doc.source || undefined,
|
||||
joinedAt: doc.joinedAt ? new Date(doc.joinedAt).toISOString() : new Date().toISOString(),
|
||||
activatedAt: doc.activatedAt ? new Date(doc.activatedAt).toISOString() : undefined,
|
||||
joinedAt: normalizeDate(doc.joinedAt) || new Date().toISOString(),
|
||||
activatedAt: normalizeDate(doc.activatedAt),
|
||||
wantsProductUpdates: Boolean(doc.wantsProductUpdates),
|
||||
updatesOptedInAt: doc.updatesOptedInAt ? new Date(doc.updatesOptedInAt).toISOString() : undefined,
|
||||
updatesOptedInAt: normalizeDate(doc.updatesOptedInAt),
|
||||
invitationSentAt: sentAt,
|
||||
invitation:
|
||||
invitationDoc && invitationDoc.code
|
||||
? {
|
||||
id: String(invitationDoc._id),
|
||||
code: invitationDoc.code,
|
||||
channel: invitationDoc.channel,
|
||||
label: invitationDoc.label || undefined,
|
||||
createdAt: normalizeDate(invitationDoc.createdAt) || new Date().toISOString(),
|
||||
expiresAt: normalizeDate(invitationDoc.expiresAt),
|
||||
usedAt: normalizeDate(invitationDoc.usedAt),
|
||||
sentAt,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +125,7 @@ export default defineEventHandler(async (event) => {
|
||||
.sort({ joinedAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(pageSize)
|
||||
.populate('invitationCode')
|
||||
.lean(),
|
||||
WaitlistEntry.countDocuments(),
|
||||
WaitlistEntry.countDocuments({ wantsProductUpdates: true }),
|
||||
|
||||
Reference in New Issue
Block a user