Restrict public presence to staff + let admins view any character

Public "Online now" now lists only players whose game account is linked
to a STAFF website user (admin/editor/moderator) — linked players are no
longer exposed publicly with their name and location. listOnlineLinked
joins through to users and filters on role; the section is relabeled
"Staff online".

Character/roster/vendor reads gain an admin bypass: admins may view any
character's data, while players (and editor/moderator staff) stay limited
to accounts they have personally linked. The bypass lives in the shared
player controller and only ever widens access for genuine admins.

Also finalizes the uo-link character/vendor front end (player + admin
character sheets, VendorSales component, ShardChar removed) and
regenerates swagger-output.json.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kj5s1QCKobuFPYmqxjy1q
This commit is contained in:
2026-07-11 09:15:18 -05:00
parent 49d0c1bd11
commit c4245e3f6a
20 changed files with 643 additions and 216 deletions

View File

@@ -1415,7 +1415,7 @@
"tags": [
"Public · Shard"
],
"summary": "Players online now (name + serial + map only)",
"summary": "Staff online now (linked staff accounts; name + serial + map only)",
"description": "",
"responses": {
"200": {
@@ -1464,75 +1464,6 @@
}
}
},
"/api/v1/public/shard/char/{serial}": {
"get": {
"tags": [
"Public · Shard"
],
"summary": "Live character sheet by serial (cached; degrades on shard restart)",
"description": "",
"parameters": [
{
"name": "serial",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "Mobile serial, e.g. 0x24C."
}
],
"responses": {
"200": {
"description": "Character profile",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"400": {
"description": "Invalid serial",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Character not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error"
},
"502": {
"description": "Bad Gateway"
},
"503": {
"description": "Shard restarting — retry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/api/v1/public/shard/stream": {
"get": {
"tags": [
@@ -1984,7 +1915,7 @@
"tags": [
"Admin · Account"
],
"summary": "Character roster for a linked account (self)",
"summary": "Character roster for an account (self; admins: any account)",
"description": "",
"parameters": [
{
@@ -2038,7 +1969,7 @@
"tags": [
"Admin · Account"
],
"summary": "Player vendors for a linked account (self)",
"summary": "Player vendors for an account (self; admins: any account)",
"description": "",
"parameters": [
{
@@ -2087,6 +2018,107 @@
]
}
},
"/api/v1/admin/shard/char/{serial}": {
"get": {
"tags": [
"Admin · Account"
],
"summary": "Character sheet (self-linked characters; admins: any character)",
"description": "",
"parameters": [
{
"name": "serial",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "Mobile serial, e.g. 0x24C."
}
],
"responses": {
"200": {
"description": "Character profile",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"400": {
"description": "Bad Request"
},
"403": {
"description": "Character not on an account linked to the caller",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found"
},
"500": {
"description": "Internal Server Error"
},
"502": {
"description": "Bad Gateway"
},
"503": {
"description": "Service Unavailable"
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
]
}
},
"/api/v1/admin/shard/sales": {
"get": {
"tags": [
"Admin · Account"
],
"summary": "Recent player-vendor sales for the callers linked accounts (self)",
"description": "",
"responses": {
"200": {
"description": "Vendor sales",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ShardVendorSale"
}
}
}
}
},
"500": {
"description": "Internal Server Error"
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
]
}
},
"/api/v1/admin/dashboard": {
"get": {
"tags": [
@@ -7002,6 +7034,123 @@
}
]
}
},
"/api/v1/player/shard/char/{serial}": {
"get": {
"tags": [
"Player · Shard"
],
"summary": "Character sheet — only for a character on the callers linked account",
"description": "",
"parameters": [
{
"name": "serial",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "Mobile serial, e.g. 0x24C."
}
],
"responses": {
"200": {
"description": "Character profile",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"400": {
"description": "Bad Request"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Character not on an account linked to the caller",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Not Found"
},
"500": {
"description": "Internal Server Error"
},
"502": {
"description": "Bad Gateway"
},
"503": {
"description": "Shard unavailable — retry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
]
}
},
"/api/v1/player/shard/sales": {
"get": {
"tags": [
"Player · Shard"
],
"summary": "Recent player-vendor sales for the callers linked accounts",
"description": "",
"responses": {
"200": {
"description": "Vendor sales",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ShardVendorSale"
}
}
}
}
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"500": {
"description": "Internal Server Error"
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
]
}
}
},
"components": {
@@ -10544,7 +10693,7 @@
},
"description": {
"type": "string",
"example": "A player online now (redacted for the public list)."
"example": "A LINKED player online now (only accounts linked to a website user are listed)."
},
"properties": {
"type": "object",
@@ -10591,6 +10740,161 @@
"example": "Trammel"
}
}
},
"x": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "integer"
},
"nullable": {
"type": "boolean",
"example": true
},
"example": {
"type": "number",
"example": 1402
}
}
},
"y": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "integer"
},
"nullable": {
"type": "boolean",
"example": true
},
"example": {
"type": "number",
"example": 1604
}
}
},
"z": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "integer"
},
"nullable": {
"type": "boolean",
"example": true
},
"example": {
"type": "number",
"example": 0
}
}
}
}
}
}
},
"ShardVendorSale": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "object"
},
"description": {
"type": "string",
"example": "A player-vendor sale (visible only to the linked owner)."
},
"properties": {
"type": "object",
"properties": {
"t": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "integer"
},
"description": {
"type": "string",
"example": "Sale time, epoch ms."
},
"example": {
"type": "number",
"example": 1783720195626
}
}
},
"itemType": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "Longsword"
}
}
},
"amount": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "integer"
},
"example": {
"type": "number",
"example": 1
}
}
},
"price": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "integer"
},
"example": {
"type": "number",
"example": 100
}
}
},
"commission": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "integer"
},
"nullable": {
"type": "boolean",
"example": true
},
"example": {
"type": "number",
"example": 5
}
}
},
"ownerAcct": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "whitlocktech"
}
}
}
}
}

View File

@@ -546,11 +546,26 @@ const doc = {
},
ShardOnlinePlayer: {
type: 'object',
description: 'A player online now (redacted for the public list).',
description: 'A LINKED player online now (only accounts linked to a website user are listed).',
properties: {
serial: { type: 'string', example: '0x24C' },
name: { type: 'string', example: 'Darrow' },
map: { type: 'string', nullable: true, example: 'Trammel' },
x: { type: 'integer', nullable: true, example: 1402 },
y: { type: 'integer', nullable: true, example: 1604 },
z: { type: 'integer', nullable: true, example: 0 },
},
},
ShardVendorSale: {
type: 'object',
description: 'A player-vendor sale (visible only to the linked owner).',
properties: {
t: { type: 'integer', description: 'Sale time, epoch ms.', example: 1783720195626 },
itemType: { type: 'string', example: 'Longsword' },
amount: { type: 'integer', example: 1 },
price: { type: 'integer', example: 100 },
commission: { type: 'integer', nullable: true, example: 5 },
ownerAcct: { type: 'string', example: 'whitlocktech' },
},
},
ShardHouse: {