OIDC Discovery

GET /.well-known/openid-configuration

Response example:

HTTP/1.1 200 OK
X-Request-Id: 08d967d9-cd5a-4777-97fd-e671c2a048b7
Content-Type: application/json
Cache-Control: max-age=3600, public
Content-Length: 1008
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
X-XSS-Protection: 0

{"issuer":"https://auth.ufotec.com","authorization_endpoint":"https://auth.ufotec.com/authorize","token_endpoint":"https://auth.ufotec.com/token","revocation_endpoint":"https://auth.ufotec.com/revoke","introspection_endpoint":"https://auth.ufotec.com/introspect","userinfo_endpoint":"https://auth.ufotec.com/userinfo","end_session_endpoint":"https://auth.ufotec.com/end_session","jwks_uri":"https://auth.ufotec.com/jwks","response_types_supported":["code"],"grant_types_supported":["authorization_code","refresh_token"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["ES256"],"token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post"],"revocation_endpoint_auth_methods_supported":["client_secret_basic"],"introspection_endpoint_auth_methods_supported":["client_secret_basic"],"scopes_supported":["openid","email","profile"],"code_challenge_methods_supported":["S256"],"claims_supported":["sub","email","email_verified","name","iat","exp","iss","aud"]}

Response fields:

Path Type Description

issuer

String

OIDC Issuer URL

authorization_endpoint

String

Authorization endpoint URL

token_endpoint

String

Token endpoint URL

revocation_endpoint

String

Token revocation endpoint URL (RFC 7009)

userinfo_endpoint

String

UserInfo endpoint URL

end_session_endpoint

String

RP-Initiated Logout endpoint URL (OIDC RP-Initiated Logout 1.0)

jwks_uri

String

JWKS endpoint URL

response_types_supported

Array

Supported response types

grant_types_supported

Array

Supported grant types

subject_types_supported

Array

Supported subject types

id_token_signing_alg_values_supported

Array

Supported signing algorithms

token_endpoint_auth_methods_supported

Array

Supported token endpoint auth methods

revocation_endpoint_auth_methods_supported

Array

Supported revocation endpoint auth methods

introspection_endpoint

String

Token introspection endpoint URL (RFC 7662)

introspection_endpoint_auth_methods_supported

Array

Supported introspection endpoint auth methods

scopes_supported

Array

Supported scopes

code_challenge_methods_supported

Array

Supported PKCE code challenge methods

claims_supported

Array

Supported claims

OIDC Authorization

GET /authorize

Request example:

GET /authorize?response_type=code&client_id=bff-user&redirect_uri=http://localhost:8080/api/auth/callback/testbed&scope=openid%20email%20profile&state=test-state-value&nonce=test-nonce-value&code_challenge=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk&code_challenge_method=S256 HTTP/1.1
Host: localhost:8080

Query parameters:

Parameter Description

response_type

必須為 code

client_id

已註冊的 OIDC Client ID

redirect_uri

授權完成後的回調 URI

scope

請求的 scope(必須包含 openid

state

CSRF 防護用的隨機值

nonce

防重放攻擊的隨機值

code_challenge

PKCE code challenge(Base64url-encoded SHA-256)

code_challenge_method

必須為 S256

Response example (login page):

HTTP/1.1 200 OK
X-Request-Id: 7ebbb5a9-51b5-440f-bab2-a854a9a57b27
X-RateLimit-Remaining: 27
Content-Type: text/html
Content-Length: 3403
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-XSS-Protection: 0
Set-Cookie: oidc_request=encrypted-oidc-request-data; Path=/; Max-Age=600; Expires=Sun, 29 Mar 2026 13:19:57 GMT; Secure; HttpOnly; SameSite=Lax

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Sign In - Testbed Auth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Noto+Serif+TC:wght@400;600&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,600&display=swap" rel="stylesheet">
  <style>
    /* Warm Paper Theme tokens — synced from frontend/packages/ui/src/styles/globals.css */
    :root {
      --background: oklch(0.9885 0.0057 84.57);
      --foreground: oklch(0.2161 0.0061 56.04);
      --muted-foreground: oklch(0.5534 0.0116 58.07);
      --border: oklch(0.88 0.003 48.72);
      --accent: oklch(0.9576 0.0086 67.72);
      --accent-foreground: oklch(0.3741 0.0087 67.56);
      --ring: oklch(0.4444 0.035 73.64);
    }

    *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }

    body {
      font-family: "Source Serif 4", "Noto Serif TC", Georgia, "Times New Roman", serif;
      background: var(--background);
      color: var(--foreground);
      line-height: 1.75;
      -webkit-font-smoothing: antialiased;
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
      padding: 1rem;
    }

    .container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 1.5rem;
    }

    .subtitle {
      font-size: 0.875rem;
      color: var(--muted-foreground);
    }

    .btn {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 0.5rem;
      height: 2.75rem;
      padding: 0 1.5rem;
      border: 1px solid var(--border);
      border-radius: 2px;
      background: transparent;
      color: var(--foreground);
      font-family: inherit;
      font-size: 1rem;
      text-decoration: none;
      cursor: pointer;
      transition: background-color 0.15s, color 0.15s;
    }

    .btn:hover {
      background: var(--accent);
      color: var(--accent-foreground);
    }

    .btn:focus-visible {
      outline: 2px solid var(--ring);
      outline-offset: 2px;
    }

    .btn svg { flex-shrink: 0; }

    /* Match frontend's desaturated Google icon (saturate-[0.55]) */
    .google-icon { filter: saturate(0.55); }
  </style>
</head>
<body>
  <div class="container">
    <p class="subtitle">Sign in to continue</p>
    <a class="btn" href="/login/google"><svg class="google-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 20 20" fill="none" aria-hidden="true"><g clip-path="url(#gg)"><path d="M10 8.18V12.05h5.38c-.24 1.25-.93 2.3-2 3.01l3.25 2.52c1.89-1.75 2.98-4.31 2.98-7.36 0-.71-.06-1.39-.18-2.05H10z" fill="#1976D2"/><path d="M4.4 11.9l-.73.56-2.6 2.02C2.72 17.75 6.1 20 10 20c2.7 0 4.96-.91 6.62-2.42l-3.25-2.52c-.89.6-2.03.96-3.37.96-2.6 0-4.81-1.75-5.6-4.12z" fill="#4CAF50"/><path d="M1.07 5.52A9.97 9.97 0 000 10c0 1.62.39 3.14 1.07 4.48l3.33-2.58a5.94 5.94 0 010-3.8L1.07 5.52z" fill="#FFC107"/><path d="M10 3.98c1.47 0 2.78.51 3.83 1.49l2.86-2.86C14.95.99 12.7 0 10 0 6.09 0 2.72 2.25 1.07 5.52L4.4 8.1C5.19 5.74 7.4 3.98 10 3.98z" fill="#FF3D00"/></g><defs><clipPath id="gg"><rect width="19.6" height="20" fill="white"/></clipPath></defs></svg>Google</a>
  </div>
</body>
</html>

GET /callback/{provider}

Request example:

GET /callback/google?code=test-upstream-code&state=test-upstream-state HTTP/1.1
Host: localhost:8080
Cookie: upstream_state=test-upstream-state; oidc_request=encrypted-oidc-request

Path parameters: ./callback/{provider}

Parameter Description

provider

上游 OAuth 提供者 (e.g. google)

Query parameters:

Parameter Description

code

上游提供者回傳的授權碼

state

CSRF 防護用的狀態值(需與 upstream_state cookie 一致)

Response example (redirect):

HTTP/1.1 307 Temporary Redirect
X-Request-Id: 81561157-853e-44f5-8153-a0ffaa6c1348
Location: http://localhost:8080/api/auth/callback/testbed?code=generated-authorization-code&state=original-client-state
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-XSS-Protection: 0
Set-Cookie: idp_session=idp-session-id; Path=/; Max-Age=86400; Expires=Mon, 30 Mar 2026 13:10:01 GMT; Secure; HttpOnly; SameSite=Lax
Set-Cookie: oidc_request=; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly; SameSite=Lax
Set-Cookie: upstream_state=; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly; SameSite=Lax

OIDC Token

POST /token

Request example:

POST /token HTTP/1.1
Authorization: Basic YmZmLXVzZXI6dGVzdC1iZmYtc2VjcmV0
Content-Type: application/x-www-form-urlencoded
Content-Length: 193
Host: localhost:8080

grant_type=authorization_code&code=test-authorization-code&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fapi%2Fauth%2Fcallback%2Ftestbed&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

Form parameters:

Parameter Description

grant_type

授權類型:authorization_coderefresh_token

code

授權碼(由 /authorize 或 /callback 流程取得)

redirect_uri

必須與授權請求時一致的回調 URI

code_verifier

PKCE code verifier(與 code_challenge 對應)

user_agent

瀏覽器 User-Agent(由 BFF/Admin 轉傳,用於裝置綁定)

geo_country

ISO 3166-1 alpha-2 國碼(由 BFF 從 Cloudflare headers 提取)

geo_city

城市名稱

geo_timezone

IANA 時區(如 Asia/Taipei

geo_latitude

緯度

geo_longitude

經度

geo_region_code

地區代碼(如 TPE

Response example:

HTTP/1.1 200 OK
X-Request-Id: c8c50e8e-c5f3-41df-a941-150349c7ae67
X-RateLimit-Remaining: 8
Content-Type: application/json
Content-Length: 219
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-XSS-Protection: 0

{"token_type":"Bearer","id_token":"eyJhbGciOiJFUzI1NiJ9.test-id-token","access_token":"eyJhbGciOiJFUzI1NiJ9.test-access-token","refresh_token":"test-refresh-token-value","expires_in":3600,"scope":"openid email profile"}

Response fields:

Path Type Description

token_type

String

Token 類型,固定為 Bearer

id_token

String

OIDC ID Token(JWT)

access_token

String

Access Token(JWT)

refresh_token

String

Refresh Token

expires_in

Number

Access Token 有效秒數

scope

String

核准的 scope

OIDC UserInfo

GET /userinfo

Request example:

GET /userinfo HTTP/1.1
Authorization: Bearer eyJraWQiOiJ0ZXN0LWtleS0xIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJ0ZXN0LWFjY291bnQtaWQiLCJpc3MiOiJodHRwczovL2F1dGgudWZvdGVjLmNvbSIsImF1ZCI6WyJodHRwczovL2FwaS51Zm90ZWMuY29tIl0sImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInJvbGVzIjpbIlVTRVIiXSwiaWF0IjoxNzc0Nzg5ODExLCJleHAiOjE3NzQ3ODk4NzF9.JP78G9SCh-WvophCjpBvPgC4Cs2d4Be4zewAvbU3AIZyKHf2UtZIcg4sfHkmzJ4svw9eMVUuWhoVa_GXRWMF3A
Host: localhost:8080

Response example:

HTTP/1.1 200 OK
X-Request-Id: f5ad9b93-ecc0-4440-b03e-7329bad7928c
Content-Type: application/json
Cache-Control: no-store
Content-Length: 88
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
X-XSS-Protection: 0

{"sub":"test-account-id","name":"test","email":"test@example.com","email_verified":true}

Response fields:

Path Type Description

sub

String

Subject(帳號 ID)

name

String

使用者名稱(email local part)

email

String

Email 地址

email_verified

Boolean

Email 是否已驗證

OIDC End Session

GET /end_session (redirect)

RP-Initiated Logout(OIDC RP-Initiated Logout 1.0)。有合法 post_logout_redirect_uri 時回傳 303 redirect。

Query parameters:

Parameter Description

id_token_hint

先前取得的 ID Token(用於識別 session,可過期)

post_logout_redirect_uri

登出後的重導向 URI(需在 client 註冊清單中)

state

RP 傳入的 opaque 值,會附加到重導向 URI 的 query string

Response example:

HTTP/1.1 303 See Other
X-Request-Id: 5726655b-af51-40ec-9484-29b95946e238
Location: http://localhost:8080/?state=random-state
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-XSS-Protection: 0
Set-Cookie: idp_session=; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly; SameSite=Lax

GET /end_session (no redirect)

id_token_hint 或未註冊的 redirect URI 時,回傳簡單 HTML 頁面。

Response example:

HTTP/1.1 200 OK
X-Request-Id: be7dea0a-0d2e-4926-8ab2-f6ed152d9ba2
Content-Type: text/html
Content-Length: 111
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-XSS-Protection: 0
Set-Cookie: idp_session=; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly; SameSite=Lax

<!DOCTYPE html><html><head><title>Logged Out</title></head><body><p>You have been logged out.</p></body></html>

JWKS

GET /jwks

Response example:

HTTP/1.1 200 OK
X-Request-Id: 5a85bcd5-3499-4b39-bccb-e22b2a3e9fa3
Content-Type: application/json
Cache-Control: max-age=3600, public
Content-Length: 182
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
X-XSS-Protection: 0

{"keys":[{"kty":"EC","use":"sig","crv":"P-256","kid":"test-key-1","x":"tAwqLlA-7AYD8OCtKBbLHT5rQ_fC4dJmEQr1BPXUEus","y":"L7w-qh6fpmtEPVer8h0z932lZm6254EPHjUAsguu8Cg","alg":"ES256"}]}

Response fields:

Path Type Description

keys[]

Array

JWK 陣列

keys[].kty

String

Key type (EC)

keys[].crv

String

Curve (P-256)

keys[].kid

String

Key ID

keys[].use

String

Key usage (sig)

keys[].alg

String

Algorithm (ES256)

keys[].x

String

EC public key x coordinate (Base64url)

keys[].y

String

EC public key y coordinate (Base64url)

Account / Identity Management

GET /account/identities

Response example:

HTTP/1.1 200 OK
X-Request-Id: 1686b44f-2e1e-4da2-ac99-b7eb4c5705bd
Content-Type: application/json
Content-Length: 199
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-XSS-Protection: 0

{"success":true,"code":"00000","message":"Success","data":{"identities":[{"provider":"google","email":"user@google.com","createdAt":1000,"lastLoginAt":2000}]},"errors":null,"timestamp":1774789780223}

Response fields:

Path Type Description

success

Boolean

是否成功

code

String

錯誤代碼 (AppErrorCode)

message

String

錯誤訊息

timestamp

Number

發生時間戳記

data

Object

回傳資料

errors

Object

詳細錯誤資訊 (Map)

data.identities[]

Array

Identity 列表

data.identities[].provider

String

Provider 名稱

data.identities[].email

String

Identity 關聯 Email

data.identities[].createdAt

Number

建立時間戳記

data.identities[].lastLoginAt

Number

最後登入時間戳記

POST /account/identities

Request example:

POST /account/identities HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJraWQiOiJ0ZXN0LWtleS0xIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJ0ZXN0LWFjY291bnQtaWQiLCJpc3MiOiJodHRwczovL2F1dGgudWZvdGVjLmNvbSIsImF1ZCI6WyJodHRwczovL2FwaS51Zm90ZWMuY29tIl0sImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInJvbGVzIjpbIlVTRVIiXSwiaWF0IjoxNzc0Nzg5NzgwLCJleHAiOjE3NzQ3ODk4NDB9.DPHNge4OrEhX8BLW00MkTYcsQAusyID8BuO2yBVeKkkUlVf_EW9ghQAV1xsLCfyFmgKAHe_7z4nivPuhk666og
Content-Length: 45
Host: localhost:8080

{"provider":"google","idToken":"valid-token"}

Request fields:

Path Type Description

provider

String

Identity provider (e.g. google)

idToken

String

ID Token from the provider

Response example:

HTTP/1.1 201 Created
X-Request-Id: ca8526ea-b480-4f9a-8a83-64fd5a7e15e5
Content-Type: application/json
Content-Length: 208
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-XSS-Protection: 0

{"success":true,"code":"00000","message":"Success","data":{"identity":{"provider":"google","email":"new@example.com","createdAt":1000,"lastLoginAt":1000},"isNew":true},"errors":null,"timestamp":1774789780596}

Response fields:

Path Type Description

success

Boolean

是否成功

code

String

錯誤代碼 (AppErrorCode)

message

String

錯誤訊息

timestamp

Number

發生時間戳記

data

Object

回傳資料

errors

Object

詳細錯誤資訊 (Map)

data.identity.provider

String

Provider 名稱

data.identity.email

String

Identity 關聯 Email

data.identity.createdAt

Number

建立時間戳記

data.identity.lastLoginAt

Number

最後登入時間戳記

data.isNew

Boolean

是否為新綁定

DELETE /account/identities/{provider}

Request example:

DELETE /account/identities/google HTTP/1.1
Authorization: Bearer eyJraWQiOiJ0ZXN0LWtleS0xIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJ0ZXN0LWFjY291bnQtaWQiLCJpc3MiOiJodHRwczovL2F1dGgudWZvdGVjLmNvbSIsImF1ZCI6WyJodHRwczovL2FwaS51Zm90ZWMuY29tIl0sImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInJvbGVzIjpbIlVTRVIiXSwiaWF0IjoxNzc0Nzg5NzgwLCJleHAiOjE3NzQ3ODk4NDB9.MHQcjpPfnOqmzkn0AlAlY_p9QtExC_Cn4bG3i2htYlGjuwHstqegovziSKtlQyG7uQiTaa3_pBZPW5nNvzJ2SA
Host: localhost:8080

Path parameters: ./account/identities/{provider}

Parameter Description

provider

要解除綁定的 Identity Provider (e.g. google)

Response example:

HTTP/1.1 204 No Content
X-Request-Id: 3902dcf6-ded5-4009-8f75-c2dd82dde9c9
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-XSS-Protection: 0

Account Session Management

GET /account/sessions

List the authenticated user’s active sessions.

Response example:

HTTP/1.1 200 OK
X-Request-Id: 9ad5292c-6db5-494b-8f63-404a861a91b0
Content-Type: application/json
Content-Length: 403
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-XSS-Protection: 0

{"success":true,"code":"00000","message":"Success","data":{"sessions":[{"sessionId":"session-abc-123","clientId":"bff-user","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)","createdAt":1700000000000,"lastActivityAt":1700003600000,"countryCode":"TW","city":"Taipei","timezone":"Asia/Taipei","latitude":25.033,"longitude":121.565,"regionCode":"TPE"}]},"errors":null,"timestamp":1774789780414}

Response fields:

Path Type Description

success

Boolean

是否成功

code

String

錯誤代碼 (AppErrorCode)

message

String

錯誤訊息

timestamp

Number

發生時間戳記

data

Object

回傳資料

errors

Object

詳細錯誤資訊 (Map)

data.sessions[]

Array

Active session 列表

data.sessions[].sessionId

String

Session ID

data.sessions[].clientId

String

Client ID

data.sessions[].userAgent

String

User-Agent 字串

data.sessions[].createdAt

Number

Session 建立時間戳記

data.sessions[].lastActivityAt

Number

最後活動時間戳記

data.sessions[].countryCode

String

ISO 3166-1 alpha-2 國碼

data.sessions[].city

String

城市名稱

data.sessions[].timezone

String

IANA 時區

data.sessions[].latitude

Number

緯度

data.sessions[].longitude

Number

經度

data.sessions[].regionCode

String

地區代碼

Admin Account Management

GET /admin/accounts/statistics

Get account counts grouped by status (requires ADMIN role).

Request headers:

Name Description

Authorization

Bearer JWT(需要 ADMIN 角色)

Request example:

GET /admin/accounts/statistics HTTP/1.1
Authorization: Bearer eyJraWQiOiJ0ZXN0LWtleS0xIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJhZG1pbi1hY2NvdW50LWlkIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLnVmb3RlYy5jb20iLCJhdWQiOlsiaHR0cHM6Ly9hcGkudWZvdGVjLmNvbSJdLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwicm9sZXMiOlsiQURNSU4iXSwiaWF0IjoxNzc0Nzg5Nzg0LCJleHAiOjE3NzQ3ODk4NDR9.W2Eta4QlsuSB_w8RYYXoO3wGCTn3BZF8RmqxNvrXOAeJzDxC8pDrTL84GXswo9Fjaiy5RM9NXY2Jmcs37sSNlg
Host: localhost:8080

Response example:

HTTP/1.1 200 OK
X-Request-Id: 0d85fbe2-ebee-4ccf-9ed9-a5ac0e941ca2
Content-Type: application/json
Content-Length: 149
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-XSS-Protection: 0

{"success":true,"code":"00000","message":"Success","data":{"total":15,"active":10,"pending":3,"suspended":2},"errors":null,"timestamp":1774789784886}

Response fields:

Path Type Description

success

Boolean

是否成功

code

String

錯誤代碼 (AppErrorCode)

message

String

錯誤訊息

timestamp

Number

發生時間戳記

data

Object

回傳資料

errors

Object

詳細錯誤資訊 (Map)

data.total

Number

帳號總數

data.active

Number

啟用中帳號數

data.pending

Number

待核准帳號數

data.suspended

Number

已停權帳號數

GET /admin/accounts/{id}

Get a single account by ID (requires ADMIN role).

Request headers:

Name Description

Authorization

Bearer JWT(需要 ADMIN 角色)

Request example:

GET /admin/accounts/account-123 HTTP/1.1
Authorization: Bearer eyJraWQiOiJ0ZXN0LWtleS0xIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJhZG1pbi1hY2NvdW50LWlkIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLnVmb3RlYy5jb20iLCJhdWQiOlsiaHR0cHM6Ly9hcGkudWZvdGVjLmNvbSJdLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwicm9sZXMiOlsiQURNSU4iXSwiaWF0IjoxNzc0Nzg5Nzg0LCJleHAiOjE3NzQ3ODk4NDR9.hKIL0bXMlU1iK0zHtwuVvf5rgNYjKcxCJxTZrp2arKaHhV0rNrh9MvnAk_ODbN_DzPZLL6YxmimhAU3FMJlI1w
Host: localhost:8080

Path parameters: ./admin/accounts/{id}

Parameter Description

id

Account ID

Response example:

HTTP/1.1 200 OK
X-Request-Id: f5256e42-7e17-4c0a-8c03-dff9d53e3a87
Content-Type: application/json
Content-Length: 230
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-XSS-Protection: 0

{"success":true,"code":"00000","message":"Success","data":{"id":"account-123","email":"user@example.com","status":"ACTIVE","role":"USER","createdAt":1700000000000,"updatedAt":1700003600000},"errors":null,"timestamp":1774789784607}

Response fields:

Path Type Description

success

Boolean

是否成功

code

String

錯誤代碼 (AppErrorCode)

message

String

錯誤訊息

timestamp

Number

發生時間戳記

data

Object

回傳資料

errors

Object

詳細錯誤資訊 (Map)

data.id

String

Account ID

data.email

String

Email 地址

data.status

String

帳號狀態(ACTIVE / PENDING / SUSPENDED)

data.role

String

帳號角色(USER / ADMIN)

data.createdAt

Number

建立時間戳記

data.updatedAt

Number

最後更新時間戳記

Admin Session Management

GET /admin/accounts/{id}/sessions

List active sessions for a specific account (requires ADMIN role).

Request headers:

Name Description

Authorization

Bearer JWT(需要 ADMIN 角色)

Request example:

GET /admin/accounts/account-123/sessions HTTP/1.1
Authorization: Bearer eyJraWQiOiJ0ZXN0LWtleS0xIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJhZG1pbi1hY2NvdW50LWlkIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLnVmb3RlYy5jb20iLCJhdWQiOlsiaHR0cHM6Ly9hcGkudWZvdGVjLmNvbSJdLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwicm9sZXMiOlsiQURNSU4iXSwiaWF0IjoxNzc0Nzg5NzkxLCJleHAiOjE3NzQ3ODk4NTF9.I-I_y4BVFz1F957c88jANyYPdOb1gAwb-5b5Gdp8uaPeoU3kmD2fXQvEoqbukA2SHYypANaXo3K9OpNvJoe97A
Host: localhost:8080

Path parameters: ./admin/accounts/{id}/sessions

Parameter Description

id

Account ID

Response example:

HTTP/1.1 200 OK
X-Request-Id: 34143e35-abf7-4461-a6a2-ef75f889f66c
Content-Type: application/json
Content-Length: 397
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-XSS-Protection: 0

{"success":true,"code":"00000","message":"Success","data":{"sessions":[{"sessionId":"session-xyz-789","clientId":"bff-user","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)","createdAt":1700000000000,"lastActivityAt":1700003600000,"countryCode":"TW","city":"Taipei","timezone":"Asia/Taipei","latitude":25.033,"longitude":121.565,"regionCode":"TPE"}]},"errors":null,"timestamp":1774789791432}

Response fields:

Path Type Description

success

Boolean

是否成功

code

String

錯誤代碼 (AppErrorCode)

message

String

錯誤訊息

timestamp

Number

發生時間戳記

data

Object

回傳資料

errors

Object

詳細錯誤資訊 (Map)

data.sessions[]

Array

Active session 列表

data.sessions[].sessionId

String

Session ID

data.sessions[].clientId

String

Client ID

data.sessions[].userAgent

String

User-Agent 字串

data.sessions[].createdAt

Number

Session 建立時間戳記

data.sessions[].lastActivityAt

Number

最後活動時間戳記

data.sessions[].countryCode

String

ISO 3166-1 alpha-2 國碼

data.sessions[].city

String

城市名稱

data.sessions[].timezone

String

IANA 時區

data.sessions[].latitude

Number

緯度

data.sessions[].longitude

Number

經度

data.sessions[].regionCode

String

地區代碼

POST /admin/sessions/revoke

Revoke a single session (requires ADMIN role).

Request headers:

Name Description

Authorization

Bearer JWT(需要 ADMIN 角色)

Request example:

POST /admin/sessions/revoke HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJraWQiOiJ0ZXN0LWtleS0xIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJhZG1pbi1hY2NvdW50LWlkIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLnVmb3RlYy5jb20iLCJhdWQiOlsiaHR0cHM6Ly9hcGkudWZvdGVjLmNvbSJdLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwicm9sZXMiOlsiQURNSU4iXSwiaWF0IjoxNzc0Nzg5NzkxLCJleHAiOjE3NzQ3ODk4NTF9.mZd2Jsd6OZe9e_tAH15jY23c-uD4oQBd7qqt6VKU_HbNcPfufpkuh7Iaq6_JKICWqmUqWSodjfYP_3iVq22zvQ
Content-Length: 53
Host: localhost:8080

{"sessionId":"session-456","accountId":"account-123"}

Request fields:

Path Type Description

sessionId

String

要撤銷的 Session ID

accountId

String

Session 所屬的 Account ID

Response example:

HTTP/1.1 200 OK
X-Request-Id: e3e59c11-dba6-4423-abe8-7406dbab9765
Content-Type: application/json
Content-Length: 115
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-XSS-Protection: 0

{"success":true,"code":"00000","message":"Success","data":{"success":true},"errors":null,"timestamp":1774789791355}

Response fields:

Path Type Description

success

Boolean

是否成功

code

String

錯誤代碼 (AppErrorCode)

message

String

錯誤訊息

timestamp

Number

發生時間戳記

data

Object

回傳資料

errors

Object

詳細錯誤資訊 (Map)

data.success

Boolean

操作是否成功

POST /admin/sessions/revoke-all

Revoke all sessions for an account (requires ADMIN role).

Request headers:

Name Description

Authorization

Bearer JWT(需要 ADMIN 角色)

Request example:

POST /admin/sessions/revoke-all HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJraWQiOiJ0ZXN0LWtleS0xIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJhZG1pbi1hY2NvdW50LWlkIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLnVmb3RlYy5jb20iLCJhdWQiOlsiaHR0cHM6Ly9hcGkudWZvdGVjLmNvbSJdLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwicm9sZXMiOlsiQURNSU4iXSwiaWF0IjoxNzc0Nzg5NzkxLCJleHAiOjE3NzQ3ODk4NTF9.m80gqRPdfMaQif20i3Cyfu_X2xPtG5SAad4VWK53Pgvlpy_g4jinJlFm-s8t1jvEcz_kk-tLTM6DLEEFlb2DiA
Content-Length: 27
Host: localhost:8080

{"accountId":"account-123"}

Request fields:

Path Type Description

accountId

String

要撤銷所有 Session 的 Account ID

Response example:

HTTP/1.1 200 OK
X-Request-Id: 93148618-a13e-4de1-b126-cd674eb000fd
Content-Type: application/json
Content-Length: 115
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-XSS-Protection: 0

{"success":true,"code":"00000","message":"Success","data":{"success":true},"errors":null,"timestamp":1774789791202}

Response fields:

Path Type Description

success

Boolean

是否成功

code

String

錯誤代碼 (AppErrorCode)

message

String

錯誤訊息

timestamp

Number

發生時間戳記

data

Object

回傳資料

errors

Object

詳細錯誤資訊 (Map)

data.success

Boolean

操作是否成功

Admin Geo Statistics

GET /admin/statistics/geo

Get session geo distribution as H3 hexagon aggregation (requires ADMIN role).

Request headers:

Name Description

Authorization

Bearer JWT(需要 ADMIN 角色)

Request example:

GET /admin/statistics/geo HTTP/1.1
Authorization: Bearer eyJraWQiOiJ0ZXN0LWtleS0xIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJhZG1pbi1hY2NvdW50LWlkIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLnVmb3RlYy5jb20iLCJhdWQiOlsiaHR0cHM6Ly9hcGkudWZvdGVjLmNvbSJdLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwicm9sZXMiOlsiQURNSU4iXSwiaWF0IjoxNzc0Nzg5Nzg4LCJleHAiOjE3NzQ3ODk4NDh9.e1ojnG2L68AbbhtbwC2Nv9dHqwUEmr7UC8eEg7cV4tmmKDNkQh9DVuathdluByD0cXR_Q6FPwzG_TLxhoVwA-g
Host: localhost:8080

Response example:

HTTP/1.1 200 OK
X-Request-Id: ec16fb87-9e1f-44dc-aa03-0803f073995e
Content-Type: application/json
Content-Length: 435
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-XSS-Protection: 0

{"success":true,"code":"00000","message":"Success","data":{"totalSessions":42,"resolution":4,"hexagons":[{"h3Index":"841f91dffffffff","count":30},{"h3Index":"8429a1dffffffff","count":12}],"byCountry":[{"countryCode":"TW","count":30,"percentage":71.43},{"countryCode":"JP","count":12,"percentage":28.57}],"byTimezone":[{"timezone":"Asia/Taipei","count":30},{"timezone":"Asia/Tokyo","count":12}]},"errors":null,"timestamp":1774789788169}

Response fields:

Path Type Description

success

Boolean

是否成功

code

String

錯誤代碼 (AppErrorCode)

message

String

錯誤訊息

timestamp

Number

發生時間戳記

data

Object

回傳資料

errors

Object

詳細錯誤資訊 (Map)

data.totalSessions

Number

Session 總數

data.resolution

Number

H3 解析度

data.hexagons[]

Array

H3 六角格聚合資料

data.hexagons[].h3Index

String

H3 cell 索引

data.hexagons[].count

Number

該 cell 的 session 數量

data.byCountry[]

Array

按國家分組統計

data.byCountry[].countryCode

String

ISO 3166-1 alpha-2 國碼

data.byCountry[].count

Number

該國家的 session 數量

data.byCountry[].percentage

Number

佔總 session 的百分比

data.byTimezone[]

Array

按時區分組統計

data.byTimezone[].timezone

String

IANA 時區名稱

data.byTimezone[].count

Number

該時區的 session 數量