diff --git a/server/api/admin/members/invite.post.js b/server/api/admin/members/invite.post.js
new file mode 100644
index 0000000..e4685bf
--- /dev/null
+++ b/server/api/admin/members/invite.post.js
@@ -0,0 +1,88 @@
+import jwt from 'jsonwebtoken'
+import { Resend } from 'resend'
+import Member from '../../../models/member.js'
+import { connectDB } from '../../../utils/mongoose.js'
+
+const resend = new Resend(process.env.RESEND_API_KEY)
+
+export default defineEventHandler(async (event) => {
+ await requireAdmin(event)
+ const { memberIds, emailTemplate } = await validateBody(event, memberInviteSchema)
+ await connectDB()
+
+ const config = useRuntimeConfig(event)
+ const headers = getHeaders(event)
+ const baseUrl =
+ process.env.BASE_URL ||
+ `${headers.host?.includes('localhost') ? 'http' : 'https'}://${headers.host}`
+
+ const members = await Member.find({ _id: { $in: memberIds } })
+
+ if (members.length === 0) {
+ throw createError({
+ statusCode: 404,
+ statusMessage: 'No members found for the provided IDs'
+ })
+ }
+
+ const results = []
+
+ for (const member of members) {
+ try {
+ // Generate 48-hour magic login token (same format as login.post.js)
+ const token = jwt.sign(
+ { memberId: member._id },
+ config.jwtSecret,
+ { expiresIn: '48h' }
+ )
+
+ const loginLink = `${baseUrl}/api/auth/verify?token=${token}`
+
+ // Interpolate template variables
+ const emailText = emailTemplate
+ .replace(/\{name\}/g, member.name)
+ .replace(/\{loginLink\}/g, loginLink)
+ .replace(/\{circle\}/g, member.circle)
+
+ // Build HTML version: escape user content, linkify plain URLs, then insert button (unescaped) last
+ const loginButton = `Sign in to Ghost Guild`
+ const emailHtml = emailTemplate
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/\{name\}/g, member.name)
+ .replace(/\{circle\}/g, member.circle)
+ .replace(/(https?:\/\/[^\s<]+)/g, '$1')
+ .replace(/\n/g, '
')
+ .replace(/\{loginLink\}/g, loginButton)
+
+ const { error: sendError } = await resend.emails.send({
+ from: 'Ghost Guild ',
+ to: [member.email],
+ subject: 'You\'re invited to Ghost Guild',
+ text: emailText,
+ html: emailHtml
+ })
+
+ if (sendError) {
+ results.push({ memberId: member._id, email: member.email, success: false, error: sendError.message })
+ continue
+ }
+
+ // Mark member as active and record invite sent
+ member.status = 'active'
+ member.inviteEmailSent = true
+ member.inviteEmailSentAt = new Date()
+ await member.save()
+
+ results.push({ memberId: member._id, email: member.email, success: true })
+ } catch (err) {
+ results.push({ memberId: member._id, email: member.email, success: false, error: err.message })
+ }
+ }
+
+ const sent = results.filter(r => r.success).length
+ const failed = results.filter(r => !r.success).length
+
+ return { sent, failed, results }
+})
diff --git a/server/api/auth/verify.get.js b/server/api/auth/verify.get.js
index 16655e2..57dda9a 100644
--- a/server/api/auth/verify.get.js
+++ b/server/api/auth/verify.get.js
@@ -52,8 +52,8 @@ export default defineEventHandler(async (event) => {
maxAge: 60 * 60 * 24 * 7 // 7 days
})
- // Redirect to the members dashboard or home page
- await sendRedirect(event, '/members', 302)
+ // Redirect to the wiki
+ await sendRedirect(event, 'https://wiki.ghostguild.org', 302)
} catch (err) {
if (err.statusCode && err.statusCode !== 401) {