// Player · Shard — game-account linking and the caller's own roster / vendors / // characters / sales / houses, ownership-checked against the local link mirror. // // Mounted at /api/v1/player/shard by player/index.js, which already applied // `noindex, requireAuth`. No extra gate: every handler is self-scoped to // req.user.id. // // These are the *same* handlers (player/shard.controller) that admin/shard.router.js // serves under /admin/shard for the seven self-service routes — staff are a // superset of players, and the controller keys off req.user.id either way. Two // URL surfaces, one implementation. const express = require('express') const { body, param } = require('express-validator') const shard = require('./shard.controller') const validate = require('../../../middleware/validate') const { accountChangeLimiter } = require('../../../middleware/rateLimit') const shardRouter = express.Router() // Link an in-game account with a one-time code from [link, then read the // account's roster / vendors (ownership-checked against the local link mirror). const ACCOUNT_RE = /^[A-Za-z0-9_.-]{1,120}$/ shardRouter.post( '/link', // #swagger.tags = ['Player · Shard'] // #swagger.summary = 'Link an in-game account with a one-time code' // #swagger.description = 'The player runs [link in game to get a code, then submits it here. The server confirms it with the sidecar and mirrors the link.' // #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" } } } } */ /* #swagger.responses[503] = { description: 'Shard unavailable — retry', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ body('code').isString().trim().isLength({ min: 4, max: 32 }), validate, shard.link, ) shardRouter.post( '/account', // #swagger.tags = ['Player · Shard'] // #swagger.summary = 'Create a game account (hybrid signup) and link it to the caller' // #swagger.description = 'Provisions a new game account with its own username + password and auto-links it to the signed-in website user. Available only when game_account_signup is enabled and the shard accepts website signups. The password is hashed on the shard and never stored or logged by the site.' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", required: ["account","password"], properties: { account: { type: "string" }, password: { type: "string" } } } } } */ /* #swagger.responses[201] = { description: 'Account created and linked', content: { "application/json": { schema: { type: "object", properties: { account: { type: "string" }, linked: { type: "boolean" } } } } } } */ /* #swagger.responses[400] = { description: 'Validation error or rejected name/password', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */ /* #swagger.responses[403] = { description: 'Game-account signup unavailable (site or shard)', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[409] = { description: 'Account name already taken', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[429] = { description: 'Per-IP account cap reached', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[503] = { description: 'Shard unavailable — retry', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ accountChangeLimiter, body('account').matches(/^[A-Za-z0-9][A-Za-z0-9_.-]{2,29}$/), body('password').isString().isLength({ min: 8, max: 64 }), validate, shard.createGameAccount, ) shardRouter.get( '/accounts', // #swagger.tags = ['Player · Shard'] // #swagger.summary = 'List the caller’s linked game accounts' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'Linked accounts', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/ShardLink" } } } } } */ shard.listAccounts, ) shardRouter.get( '/roster/:account', // #swagger.tags = ['Player · Shard'] // #swagger.summary = 'Character roster for a linked account' // #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" } } } } */ /* #swagger.responses[503] = { description: 'Shard unavailable — retry', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ param('account').matches(ACCOUNT_RE), validate, shard.roster, ) shardRouter.get( '/vendors/:account', // #swagger.tags = ['Player · Shard'] // #swagger.summary = 'Player vendors for a linked account' // #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" } } } } */ /* #swagger.responses[503] = { description: 'Shard unavailable — retry', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ param('account').matches(ACCOUNT_RE), validate, shard.vendors, ) shardRouter.get( '/char/:serial', // #swagger.tags = ['Player · Shard'] // #swagger.summary = 'Character sheet — only for a character on the caller’s linked account' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['serial'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Mobile serial, e.g. 0x24C.' } /* #swagger.responses[200] = { description: 'Character profile', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */ /* #swagger.responses[403] = { description: 'Character not on an account linked to the caller', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[503] = { description: 'Shard unavailable — retry', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ param('serial').matches(/^0x[0-9a-fA-F]+$/), validate, shard.getChar, ) shardRouter.get( '/sales', // #swagger.tags = ['Player · Shard'] // #swagger.summary = 'Recent player-vendor sales for the caller’s linked accounts' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'Vendor sales', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/ShardVendorSale" } } } } } */ shard.getSales, ) shardRouter.get( '/houses', // #swagger.tags = ['Player · Shard'] // #swagger.summary = 'The caller’s own houses (home status)' // #swagger.description = 'Houses owned by the caller’s linked accounts, with decay/IDOC status. Only the caller’s own houses — never anyone else’s.' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] /* #swagger.responses[200] = { description: 'The caller’s houses', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/ShardHouse" } } } } } */ shard.getHouses, ) module.exports = shardRouter