Let staff link their own characters + share the game-accounts UI
- Backend: /admin/shard/{link,accounts,roster/:account,vendors/:account} —
staff self-service, reusing the player/shard controller (it keys off
req.user.id, so the same handlers serve any logged-in role). Swagger under
Admin · Account; spec regenerated.
- components/GameAccounts.jsx: the link-prompt + character-roster UI extracted
into one reusable component parametrized by an api scope and a charTo(serial)
route builder.
- PlayerCharacters now renders it (player scope → /player/char/:serial).
- Admin: "My Characters" nav item + /admin/characters (AdminCharacters) and
/admin/characters/:serial (AdminCharacter, in-shell sheet), using the admin
self-service scope. api.admin.shard.* added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qPmpmVH1xGCiZoz9m9vW3
This commit is contained in:
@@ -12,6 +12,7 @@ const authProviders = require('./authProviders.controller')
|
||||
const discordBot = require('./discordBot.controller')
|
||||
const emailConfig = require('./emailConfig.controller')
|
||||
const uoLink = require('./uoLink.controller')
|
||||
const selfShard = require('../player/shard.controller')
|
||||
const moderation = require('./moderation.controller')
|
||||
const pagesCtrl = require('./pages.controller')
|
||||
const { isLoggedIn, requireRole } = require('../../../utils/auth')
|
||||
@@ -110,6 +111,55 @@ adminRouter.delete(
|
||||
account.unlinkIdentity,
|
||||
)
|
||||
|
||||
// ── Game account linking (self-service, any staff role) ───────────────
|
||||
// Staff link their OWN in-game account here, exactly like players do under
|
||||
// /player/shard. The controller keys off req.user.id, so the same handlers work.
|
||||
const SHARD_ACCOUNT_RE = /^[A-Za-z0-9_.-]{1,120}$/
|
||||
adminRouter.post(
|
||||
'/shard/link',
|
||||
// #swagger.tags = ['Admin · Account']
|
||||
// #swagger.summary = 'Link an in-game account with a one-time code (self)'
|
||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||||
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/ShardLinkRequest" } } } } */
|
||||
/* #swagger.responses[200] = { description: 'Linked', content: { "application/json": { schema: { $ref: "#/components/schemas/ShardLinkResult" } } } } */
|
||||
/* #swagger.responses[400] = { description: 'Unknown or expired code', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
body('code').isString().trim().isLength({ min: 4, max: 32 }),
|
||||
validate,
|
||||
selfShard.link,
|
||||
)
|
||||
adminRouter.get(
|
||||
'/shard/accounts',
|
||||
// #swagger.tags = ['Admin · Account']
|
||||
// #swagger.summary = 'List the caller’s linked game accounts (self)'
|
||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||||
/* #swagger.responses[200] = { description: 'Linked accounts', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/ShardLink" } } } } } */
|
||||
selfShard.listAccounts,
|
||||
)
|
||||
adminRouter.get(
|
||||
'/shard/roster/:account',
|
||||
// #swagger.tags = ['Admin · Account']
|
||||
// #swagger.summary = 'Character roster for a linked account (self)'
|
||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||||
// #swagger.parameters['account'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'A game account linked to the caller.' }
|
||||
/* #swagger.responses[200] = { description: 'Account roster', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
|
||||
/* #swagger.responses[403] = { description: 'Account not linked to the caller', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
param('account').matches(SHARD_ACCOUNT_RE),
|
||||
validate,
|
||||
selfShard.roster,
|
||||
)
|
||||
adminRouter.get(
|
||||
'/shard/vendors/:account',
|
||||
// #swagger.tags = ['Admin · Account']
|
||||
// #swagger.summary = 'Player vendors for a linked account (self)'
|
||||
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
||||
// #swagger.parameters['account'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'A game account linked to the caller.' }
|
||||
/* #swagger.responses[200] = { description: 'Vendor snapshot', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
|
||||
/* #swagger.responses[403] = { description: 'Account not linked to the caller', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
||||
param('account').matches(SHARD_ACCOUNT_RE),
|
||||
validate,
|
||||
selfShard.vendors,
|
||||
)
|
||||
|
||||
// ── Image uploads (screenshots/gallery) ───────────────────────────────
|
||||
const UPLOAD_DIR =
|
||||
process.env.UPLOAD_DIR || path.join(__dirname, '..', '..', '..', '..', 'uploads')
|
||||
|
||||
Reference in New Issue
Block a user