// ── Admin: email invites ─────────────────────────────────────────────────── // // Admin-only. A staff member invites someone by email at a pre-chosen access // level; the invitee accepts via a tokened link (auth/invite.controller) which // creates their website user at that role. The plaintext token exists only in the // emailed link and in the create response (so the admin can copy the link if email // isn't configured); the DB stores only its hash. const invites = require('../../../model/invites/invites.model') const activity = require('../../../model/activity/activity.model') const mailer = require('../../../utils/mailer') const log = require('../../../utils/logger')('admin-invites') const ROLES = ['admin', 'editor', 'moderator', 'player'] function baseUrl() { return (process.env.APP_BASE_URL || 'http://localhost:5173').replace(/\/+$/, '') } function acceptUrl(token) { return `${baseUrl()}/invite/${token}` } // POST /admin/invites — create an invite. Optionally email it (sendEmail, default // true); the copyable accept link is ALWAYS returned so the admin can hand it over // directly. The token is single-use + expiring and the caller is the authenticated // admin who made it, so echoing the link back to them is safe. async function create(req, res) { const email = String(req.body.email || '').trim() const role = req.body.role const sendEmail = req.body.sendEmail !== false // default true if (!email || !ROLES.includes(role)) { return res.status(400).json({ message: 'A valid email and role are required.' }) } try { const { invite, token } = await invites.create({ email, role, invitedBy: req.user.id }) const url = acceptUrl(token) // Send the email only if asked. A send failure doesn't delete the invite — the // link is still returned so the admin can share it manually. let emailed = false let emailError = null if (sendEmail) { try { const result = await mailer.sendInvite({ to: email, acceptUrl: url, role, invitedByName: req.user.username }) emailed = Boolean(result.sent) if (!result.sent && result.reason === 'NOT_CONFIGURED') emailError = 'email is not configured' } catch (err) { emailError = err.message log.warn('invite email failed (invite still created)', { id: invite.id, message: err.message }) } } await activity.log({ req, userId: req.user.id, action: 'invite.create', detail: { email, role, emailed } }) log.info('invite created', { id: invite.id, email, role, emailed, by: req.user.username }) // acceptUrl is always returned (copyable link); emailed says whether it also went out. return res.status(201).json({ invite, emailed, acceptUrl: url, emailError }) } catch (err) { log.error('create invite', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // GET /admin/invites — recent invites (no tokens). async function list(req, res) { try { return res.json(await invites.list(req.query.limit)) } catch (err) { log.error('list invites', err) return res.status(500).json({ message: 'Internal Server Error' }) } } // DELETE /admin/invites/:id — revoke a pending invite. async function revoke(req, res) { const id = Number(req.params.id) try { const changed = await invites.revoke(id) if (!changed) return res.status(404).json({ message: 'No pending invite to revoke.' }) await activity.log({ req, userId: req.user.id, action: 'invite.revoke', detail: { id } }) return res.json({ id, revoked: true }) } catch (err) { log.error('revoke invite', err) return res.status(500).json({ message: 'Internal Server Error' }) } } module.exports = { create, list, revoke }