PR 4 of the in-place admin router split (docs/website/API_V2_PLAN.md § Phase 2),
and the last admin one: it moves the entire residual 33 and DELETES
admin.routes.js. Every one of the 110 admin routes is now declared in a
capability router. No URL, gate or handler changes.
shard.router.js (16) /admin/shard
uoLink.router.js ( 5) /admin/uo-link
email.router.js ( 6) /admin/email
discordBot.router.js ( 2) /admin/discord-bot
settings.router.js ( 2) /admin/settings
dashboard.router.js ( 2) GET /dashboard + PUT /site-mode, at the group root
admin.routes.js deleted, was 33
No gate moved to router level. Every adminOnly in the residual file was
per-route, and modAccess on /shard must stay per-route because half that router
must not have it — which keeps the per-route handler count intact, the one
number routes.guards.json can actually check.
/shard is the first prefix where two tiers share one router: 7 self-service
account-linking routes (no extra gate, served by the same player/shard
controller handlers, tagged `Admin · Account`) alongside 9 in-game staff ops on
modAccess. Prefix ownership beats tag grouping — splitting by tag would put two
routers under one prefix for no gain. The tag mismatch stays; retagging is a
real spec diff and belongs in a PR about tags.
dashboard.router.js is the one router mounted at the group root rather than a
prefix: GET /dashboard and PUT /site-mode share no path segment. That is safe
only because the file declares no router-level middleware — a bare use(gate) in
a root-mounted router would run for every request passing through toward
another mount. The file carries a comment saying so.
Acceptance — all four gates zero-diff:
routes.manifest.json unchanged (200 public + 2 internal)
routes.guards.json unchanged (no route lost or gained a gate)
swagger-output.json unchanged (198 operations)
api-route-inventory.json already in sync
plus 434 server tests green.
Verified separately, because no gate can catch it: introspecting the built
stack, all 59 literal admin paths still dispatch to their own layer — nothing
is captured first by a /:param sibling. The manifest sorts its entries, so
declaration order is invisible to it.
Also repoints the comments that referenced admin.routes.js by name
(botActivity/moderation controllers, the town-crier cap mirror in
announceJobs.logic.js) and generalizes the "the path is on the line after
router.get(" rationale in routeManifest.js, README.md and pr-checks.yml, which
was never about that one file.
Co-Authored-By: Claude <noreply@anthropic.com>
97 lines
5.8 KiB
JavaScript
97 lines
5.8 KiB
JavaScript
// Admin · Email — outbound mail delivery via Gmail OAuth2.
|
|
//
|
|
// Mounted at /api/v1/admin/email by admin/index.js, which already applied
|
|
// `noindex, isLoggedIn, staffOnly`. The modern replacement for env SMTP: the
|
|
// refresh token is captured by the connect flow below and is write-only over
|
|
// this API (stored encrypted by utils/secretBox.js, never returned).
|
|
//
|
|
// Admin-only, and kept as a per-route gate rather than a router-level `use` so
|
|
// the middleware chain each route carries is unchanged by the move.
|
|
|
|
const express = require('express')
|
|
const { body } = require('express-validator')
|
|
|
|
const emailConfig = require('./emailConfig.controller')
|
|
const { requireRole } = require('../../../utils/auth')
|
|
const validate = require('../../../middleware/validate')
|
|
|
|
const emailRouter = express.Router()
|
|
const adminOnly = requireRole('admin')
|
|
|
|
emailRouter.get(
|
|
'/config',
|
|
// #swagger.tags = ['Admin · Email']
|
|
// #swagger.summary = 'Get email delivery config + status (admin only)'
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
/* #swagger.responses[200] = { description: 'Config (refresh token stripped) + status', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
adminOnly,
|
|
emailConfig.getConfig,
|
|
)
|
|
emailRouter.put(
|
|
'/config',
|
|
// #swagger.tags = ['Admin · Email']
|
|
// #swagger.summary = 'Update email delivery config (admin only)'
|
|
// #swagger.description = 'Set the From display name and enabled toggle. Enabling requires a connected Gmail account.'
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
/* #swagger.requestBody = { content: { "application/json": { schema: { type: "object", properties: { senderName: { type: "string" }, enabled: { type: "boolean" } } } } } } */
|
|
/* #swagger.responses[200] = { description: 'Updated config', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
|
|
/* #swagger.responses[400] = { description: 'Cannot enable before connecting a mailbox', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
adminOnly,
|
|
body('senderName').optional({ values: 'null' }).isString().trim().isLength({ max: 120 }),
|
|
body('enabled').optional().isBoolean(),
|
|
validate,
|
|
emailConfig.saveConfig,
|
|
)
|
|
emailRouter.get(
|
|
'/connect/start',
|
|
// #swagger.tags = ['Admin · Email']
|
|
// #swagger.summary = 'Begin the Gmail OAuth2 connect flow (admin only)'
|
|
// #swagger.description = 'Returns { url } to redirect the browser to Google. Reuses the google SSO OAuth client.'
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
/* #swagger.responses[200] = { description: 'Authorization URL', content: { "application/json": { schema: { type: "object", properties: { url: { type: "string" } } } } } } */
|
|
/* #swagger.responses[400] = { description: 'Google OAuth client not configured', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
adminOnly,
|
|
emailConfig.connectStart,
|
|
)
|
|
emailRouter.get(
|
|
'/connect/callback',
|
|
// #swagger.tags = ['Admin · Email']
|
|
// #swagger.summary = 'OAuth2 callback — stores the refresh token, redirects to Settings'
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
/* #swagger.responses[302] = { description: 'Redirect back to /admin/settings' } */
|
|
adminOnly,
|
|
emailConfig.connectCallback,
|
|
)
|
|
emailRouter.post(
|
|
'/test',
|
|
// #swagger.tags = ['Admin · Email']
|
|
// #swagger.summary = 'Send a test email (admin only)'
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
/* #swagger.requestBody = { content: { "application/json": { schema: { type: "object", properties: { to: { type: "string", format: "email" } } } } } } */
|
|
/* #swagger.responses[200] = { description: 'Sent', content: { "application/json": { schema: { type: "object", properties: { sent: { type: "boolean" }, to: { type: "string" } } } } } } */
|
|
/* #swagger.responses[502] = { description: 'Send failed / not configured', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
adminOnly,
|
|
body('to').optional({ values: 'falsy' }).isEmail().isLength({ max: 255 }),
|
|
validate,
|
|
emailConfig.testSend,
|
|
)
|
|
emailRouter.post(
|
|
'/disconnect',
|
|
// #swagger.tags = ['Admin · Email']
|
|
// #swagger.summary = 'Disconnect Gmail and disable email (admin only)'
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
/* #swagger.responses[200] = { description: 'Disconnected config', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
adminOnly,
|
|
emailConfig.disconnect,
|
|
)
|
|
|
|
module.exports = emailRouter
|