feat(auth): role-agnostic self-service surface under /auth/me
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:
@@ -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 account’s 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 account’s 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 caller’s 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": {
|
||||
|
||||
Reference in New Issue
Block a user