feat(auth): role-agnostic self-service surface under /auth/me
All checks were successful
PR Checks / server-tests (pull_request) Successful in 9m27s
PR Checks / client-build (pull_request) Successful in 10m23s
PR Checks / bot-install (pull_request) Successful in 9m19s

Add /auth/me/account* — the canonical "me" endpoints for every authenticated
role (Android app §6.4/§8.1). Reuses the existing account.controller handlers
(getAccount, changeUsername, changePassword, TOTP setup/enable/disable, list/
unlink identities) verbatim behind requireAuth (any role) — no logic
duplication. The app gets one self surface and never has to touch /admin; the
old /player/account/* and /admin/account/* routes stay for web back-compat.

New routes (all bearer- or cookie-auth, any active role):
- GET    /auth/me/account
- PATCH  /auth/me/account/username
- PATCH  /auth/me/account/password
- POST   /auth/me/account/totp/setup|enable|disable
- GET    /auth/me/account/identities
- DELETE /auth/me/account/identities/:provider

Mounted as a sub-router; the bare GET /auth/me is unchanged. Swagger
regenerated with #swagger annotations. Adds test/authMe.test.js (the group
gate rejects unauthenticated callers with 401).

Verified end-to-end against MariaDB: a player and an editor both drive the
same surface (role-agnostic), username/password changes work, a password
change revokes the caller's old bearer token, and validation/401 paths behave.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
This commit is contained in:
2026-07-19 04:55:01 -05:00
parent 715eaedd74
commit fc5255da99
4 changed files with 693 additions and 2 deletions

View File

@@ -3,7 +3,7 @@
"info": {
"title": "Runic Gateway API",
"version": "1.0.0",
"description": "REST API for the Runic Gateway website, wiki and admin panel — a private Ultima Online shard.\n\n### Authentication\n- **Web / admin panel** uses an httpOnly session cookie (`uomm_token`) issued by `POST /api/v1/auth/login` (plus `/login/totp` when 2FA is enabled).\n- **Native / mobile clients** use bearer access tokens from `POST /api/v1/auth/mobile/login`, refreshed via `/auth/mobile/refresh`.\n\nEndpoints under `/api/v1/admin/**` require a valid session; some are further restricted to the `admin` role (editors are limited to content)."
"description": "REST API for the Runic Gateway website, wiki and admin panel — a private Ultima Online shard.\n\n### Authentication\n- **Web / admin panel** uses an httpOnly session cookie (`rg_token`) issued by `POST /api/v1/auth/login` (plus `/login/totp` when 2FA is enabled).\n- **Native / mobile clients** use bearer access tokens from `POST /api/v1/auth/mobile/login`, refreshed via `/auth/mobile/refresh`.\n\nEndpoints under `/api/v1/admin/**` require a valid session; some are further restricted to the `admin` role (editors are limited to content)."
},
"servers": [
{
@@ -1130,6 +1130,507 @@
}
}
},
"/api/v1/auth/me/account": {
"get": {
"tags": [
"Auth · Me"
],
"summary": "Get the current account (self, any role)",
"description": "",
"responses": {
"200": {
"description": "The current account",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PlayerAccount"
}
}
}
},
"401": {
"description": "Not authenticated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Account not active",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error"
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
]
}
},
"/api/v1/auth/me/account/username": {
"patch": {
"tags": [
"Auth · Me"
],
"summary": "Change the current accounts username (self, any role)",
"description": "",
"responses": {
"200": {
"description": "Updated username (session re-issued)",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"username": {
"type": "string"
}
}
}
}
}
},
"400": {
"description": "Validation error or unavailable username",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationError"
}
}
}
},
"401": {
"description": "Not authenticated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden"
},
"409": {
"description": "Username already taken",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"429": {
"description": "Too many changes (rate limited)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error"
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChangeUsernameRequest"
}
}
}
}
}
},
"/api/v1/auth/me/account/password": {
"patch": {
"tags": [
"Auth · Me"
],
"summary": "Change or set the current accounts password (self, any role)",
"description": "If the account already has a password, currentPassword is required and verified. SSO-provisioned accounts with no password may set an initial one without a current password. On success the callers own session is re-issued (they stay logged in) while older web sessions are revoked.",
"responses": {
"200": {
"description": "Password changed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OkFlag"
}
}
}
},
"400": {
"description": "Validation error or wrong current password",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Not authenticated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden"
},
"429": {
"description": "Too many changes (rate limited)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error"
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChangePasswordRequest"
}
}
}
}
}
},
"/api/v1/auth/me/account/totp/setup": {
"post": {
"tags": [
"Auth · Me"
],
"summary": "Begin 2FA enrollment (returns secret + QR)",
"description": "",
"responses": {
"200": {
"description": "otpauth URL and QR data to scan",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TotpSetup"
}
}
}
},
"401": {
"description": "Not authenticated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden"
},
"409": {
"description": "Two-factor already enabled",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error"
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
]
}
},
"/api/v1/auth/me/account/totp/enable": {
"post": {
"tags": [
"Auth · Me"
],
"summary": "Enable 2FA by confirming a code",
"description": "",
"responses": {
"200": {
"description": "2FA enabled",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TotpState"
}
}
}
},
"400": {
"description": "Setup not started, or invalid code",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"409": {
"description": "Two-factor already enabled",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error"
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TotpCodeRequest"
}
}
}
}
}
},
"/api/v1/auth/me/account/totp/disable": {
"post": {
"tags": [
"Auth · Me"
],
"summary": "Disable 2FA by confirming a code",
"description": "Requires a valid current authenticator code (proves control of the authenticator); it does not take a password.",
"responses": {
"200": {
"description": "2FA disabled",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TotpState"
}
}
}
},
"400": {
"description": "Not enabled, or invalid code",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"500": {
"description": "Internal Server Error"
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TotpCodeRequest"
}
}
}
}
}
},
"/api/v1/auth/me/account/identities": {
"get": {
"tags": [
"Auth · Me"
],
"summary": "List linked SSO identities (self)",
"description": "",
"responses": {
"200": {
"description": "Linked identities",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/LinkedIdentity"
}
}
}
}
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"500": {
"description": "Internal Server Error"
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
]
}
},
"/api/v1/auth/me/account/identities/{provider}": {
"delete": {
"tags": [
"Auth · Me"
],
"summary": "Unlink an SSO identity (self)",
"description": "",
"parameters": [
{
"name": "provider",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "Provider id."
}
],
"responses": {
"200": {
"description": "Unlinked",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnlinkedFlag"
}
}
}
},
"400": {
"description": "Bad Request"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "No linked account for that provider",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error"
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
]
}
},
"/api/v1/public/settings": {
"get": {
"tags": [
@@ -9599,7 +10100,7 @@
"cookieAuth": {
"type": "apiKey",
"in": "cookie",
"name": "uomm_token",
"name": "rg_token",
"description": "Session JWT set as an httpOnly cookie by POST /api/v1/auth/login."
},
"bearerAuth": {