feat(provisioning): admin game-signup mode setting, invite link option, staff self-create
Follow-ups from live testing: - Game-account creation is now an admin Settings control (disabled / website / hybrid / game) instead of a hidden on/off flag. The site offers creation for website+hybrid; help text notes the shard's SignupMode (Bridge.cfg) has the final say. game_account_signup setting widened to a 4-value enum + validated on save. - Invites: the accept link is ALWAYS returned and shown with a Copy button, and a "Email the invitation" toggle lets an admin create a link-only invite (no email) or email it. Backend takes sendEmail (default true) and always returns acceptUrl. - Staff can create a game account from their own /admin/characters page too (POST /admin/shard/account → the shared createGameAccount controller), so the form is reachable in both the player and admin portals. Note: the admin Houses view (/admin/houses) already worked; the earlier failure was a stale Vite HMR state for the new route (needs a hard refresh). Client build clean; server routes load; swagger regenerated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -22,10 +22,14 @@ function acceptUrl(token) {
|
||||
return `${baseUrl()}/invite/${token}`
|
||||
}
|
||||
|
||||
// POST /admin/invites — create an invite and email it.
|
||||
// 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.' })
|
||||
}
|
||||
@@ -33,24 +37,26 @@ async function create(req, res) {
|
||||
const { invite, token } = await invites.create({ email, role, invitedBy: req.user.id })
|
||||
const url = acceptUrl(token)
|
||||
|
||||
// Send the email; if mail isn't configured, hand the link back so the admin
|
||||
// can share it manually. A send failure doesn't delete the invite — surface it.
|
||||
// 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
|
||||
try {
|
||||
const result = await mailer.sendInvite({ to: email, acceptUrl: url, role, invitedByName: req.user.username })
|
||||
emailed = Boolean(result.sent)
|
||||
} catch (err) {
|
||||
emailError = err.message
|
||||
log.warn('invite email failed (invite still created)', { id: invite.id, message: err.message })
|
||||
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 })
|
||||
|
||||
// The accept link is returned only when email did not deliver, so the admin
|
||||
// can copy it. When emailed, we don't echo the token.
|
||||
return res.status(201).json({ invite, emailed, acceptUrl: emailed ? undefined : url, emailError })
|
||||
// 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' })
|
||||
|
||||
Reference in New Issue
Block a user