Path 3's WRITE half. The resolver landed in phase 2; this is who may hand access
out, to whom, and what stops a leader turning a Team forum into open hosting on
the operator's site.
Two authorities, and not one authority with different reach. Staff may act on any
Team, uncapped, and may revoke anything. A leader may grant and revoke ordinary
access on their own Team, is capped at `teams_max_grants_per_team` (default 50),
is rate-limited, and may NOT revoke a staff-issued grant — which is what stops a
leader undoing a moderation decision. The issuer's role is checked at revoke time
rather than stored, so an account that has since lost its staff role stops
protecting the grants it made.
Nothing on this path writes team_members, in either direction. A grant may name any
account, including one with no linked game identity — that is the point of it — and
that account stays off the roster, out of every count, and ineligible for external
platforms.
Announcements are a degenerate thread rather than their own object, so phase 5 adds
no migration. Moderation records WHICH authority was exercised: a staff action also
writes activity_log, a leader's writes only the Team's own ledger. Merging the two
would make a guild leader locking a thread an appealable Discord sanction.
Every forum route answers 404 while the switch is off, and 404 — never 403 — to a
caller with no access: in a private room the contents and the existence are the
same secret. The grant routes deliberately answer even while the forum is OFF,
because a toggle-off revokes no grant and the access list has to stay manageable.
Under /player rather than /admin: a leader is a player, and the /admin tier gate is
requireRole('admin','editor','moderator') — putting a leader endpoint behind it
would mean widening that gate.
Co-Authored-By: Claude <noreply@anthropic.com>
51 lines
2.4 KiB
JavaScript
51 lines
2.4 KiB
JavaScript
// /api/v1/player — the player self-service surface, assembled from
|
|
// per-capability routers.
|
|
//
|
|
// This file owns exactly two things: the gate every player route shares, and the
|
|
// mount table. No route is declared here. Each capability router mounts at the
|
|
// prefix it already owned inside the old monolithic player.routes.js, so the
|
|
// emitted URL set is byte-identical — proved by a zero-line diff in
|
|
// server/routes.manifest.json (`npm run routes:manifest`).
|
|
//
|
|
// **Staff are a superset of players.** This group is open to any authenticated
|
|
// account, not just role 'player': every read/write is self-scoped to req.user.id,
|
|
// and a staff member has every player ability plus their staff tools on top.
|
|
// Adding a requireRole('player') here would 403 an admin off their own characters
|
|
// (it happened once — see docs/website/BACKEND_DESIGN.md). Staff also reach the
|
|
// identical self-scoped handlers under /admin/shard and /auth/me/account; those
|
|
// are alternative URLs onto the same controllers, not duplicated logic — and
|
|
// both of those live in module-uo now, which changes where they are defined and
|
|
// nothing about which URLs answer.
|
|
//
|
|
// See docs/website/API_V2_PLAN.md § Phase 2 for the split.
|
|
|
|
const express = require('express')
|
|
|
|
const { requireAuth } = require('../../../auth/session.middleware')
|
|
const noindex = require('../../../middleware/noindex')
|
|
|
|
const accountRouter = require('./account.router')
|
|
const appealsRouter = require('./appeals.router')
|
|
const teamsRouter = require('./teams.router')
|
|
const teamForumRouter = require('./teamForum.router')
|
|
|
|
const playerRouter = express.Router()
|
|
|
|
// Group gate: authenticated only (no role restriction). Keep it out of search
|
|
// indexes. requireAuth also enforces the account status check (a disabled/banned
|
|
// account is rejected here with 403 before any handler runs).
|
|
//
|
|
// It lives here, ahead of every mount, so a capability router added later cannot
|
|
// silently ship without it.
|
|
playerRouter.use(noindex, requireAuth)
|
|
|
|
playerRouter.use('/account', accountRouter)
|
|
playerRouter.use('/appeals', appealsRouter)
|
|
playerRouter.use('/teams', teamsRouter)
|
|
// Same prefix, second router. The forum and the leader-exercised grant flow are a
|
|
// different capability from "the caller's own Teams", and splitting them keeps
|
|
// each file about one thing; no path in the two collides.
|
|
playerRouter.use('/teams', teamForumRouter)
|
|
|
|
module.exports = playerRouter
|