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 |
|---|---|---|
|
|
OIDC Issuer URL |
|
|
Authorization endpoint URL |
|
|
Token endpoint URL |
|
|
Token revocation endpoint URL (RFC 7009) |
|
|
UserInfo endpoint URL |
|
|
RP-Initiated Logout endpoint URL (OIDC RP-Initiated Logout 1.0) |
|
|
JWKS endpoint URL |
|
|
Supported response types |
|
|
Supported grant types |
|
|
Supported subject types |
|
|
Supported signing algorithms |
|
|
Supported token endpoint auth methods |
|
|
Supported revocation endpoint auth methods |
|
|
Token introspection endpoint URL (RFC 7662) |
|
|
Supported introspection endpoint auth methods |
|
|
Supported scopes |
|
|
Supported PKCE code challenge methods |
|
|
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 |
|---|---|
|
必須為 |
|
已註冊的 OIDC Client ID |
|
授權完成後的回調 URI |
|
請求的 scope(必須包含 |
|
CSRF 防護用的隨機值 |
|
防重放攻擊的隨機值 |
|
PKCE code challenge(Base64url-encoded SHA-256) |
|
必須為 |
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 |
|---|---|
|
上游 OAuth 提供者 (e.g. |
Query parameters:
| Parameter | Description |
|---|---|
|
上游提供者回傳的授權碼 |
|
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 |
|---|---|
|
授權類型: |
|
授權碼(由 /authorize 或 /callback 流程取得) |
|
必須與授權請求時一致的回調 URI |
|
PKCE code verifier(與 code_challenge 對應) |
|
瀏覽器 User-Agent(由 BFF/Admin 轉傳,用於裝置綁定) |
|
ISO 3166-1 alpha-2 國碼(由 BFF 從 Cloudflare headers 提取) |
|
城市名稱 |
|
IANA 時區(如 |
|
緯度 |
|
經度 |
|
地區代碼(如 |
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 類型,固定為 |
|
|
OIDC ID Token(JWT) |
|
|
Access Token(JWT) |
|
|
Refresh Token |
|
|
Access Token 有效秒數 |
|
|
核准的 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 |
|---|---|---|
|
|
Subject(帳號 ID) |
|
|
使用者名稱(email local part) |
|
|
Email 地址 |
|
|
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(用於識別 session,可過期) |
|
登出後的重導向 URI(需在 client 註冊清單中) |
|
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 |
|---|---|---|
|
|
JWK 陣列 |
|
|
Key type (EC) |
|
|
Curve (P-256) |
|
|
Key ID |
|
|
Key usage (sig) |
|
|
Algorithm (ES256) |
|
|
EC public key x coordinate (Base64url) |
|
|
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 |
|---|---|---|
|
|
是否成功 |
|
|
錯誤代碼 (AppErrorCode) |
|
|
錯誤訊息 |
|
|
發生時間戳記 |
|
|
回傳資料 |
|
|
詳細錯誤資訊 (Map) |
|
|
Identity 列表 |
|
|
Provider 名稱 |
|
|
Identity 關聯 Email |
|
|
建立時間戳記 |
|
|
最後登入時間戳記 |
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 |
|---|---|---|
|
|
Identity provider (e.g. google) |
|
|
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 |
|---|---|---|
|
|
是否成功 |
|
|
錯誤代碼 (AppErrorCode) |
|
|
錯誤訊息 |
|
|
發生時間戳記 |
|
|
回傳資料 |
|
|
詳細錯誤資訊 (Map) |
|
|
Provider 名稱 |
|
|
Identity 關聯 Email |
|
|
建立時間戳記 |
|
|
最後登入時間戳記 |
|
|
是否為新綁定 |
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 |
|---|---|
|
要解除綁定的 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 |
|---|---|---|
|
|
是否成功 |
|
|
錯誤代碼 (AppErrorCode) |
|
|
錯誤訊息 |
|
|
發生時間戳記 |
|
|
回傳資料 |
|
|
詳細錯誤資訊 (Map) |
|
|
Active session 列表 |
|
|
Session ID |
|
|
Client ID |
|
|
User-Agent 字串 |
|
|
Session 建立時間戳記 |
|
|
最後活動時間戳記 |
|
|
ISO 3166-1 alpha-2 國碼 |
|
|
城市名稱 |
|
|
IANA 時區 |
|
|
緯度 |
|
|
經度 |
|
|
地區代碼 |
Admin Account Management
GET /admin/accounts/statistics
Get account counts grouped by status (requires ADMIN role).
Request headers:
| Name | Description |
|---|---|
|
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 |
|---|---|---|
|
|
是否成功 |
|
|
錯誤代碼 (AppErrorCode) |
|
|
錯誤訊息 |
|
|
發生時間戳記 |
|
|
回傳資料 |
|
|
詳細錯誤資訊 (Map) |
|
|
帳號總數 |
|
|
啟用中帳號數 |
|
|
待核准帳號數 |
|
|
已停權帳號數 |
GET /admin/accounts/{id}
Get a single account by ID (requires ADMIN role).
Request headers:
| Name | Description |
|---|---|
|
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 |
|---|---|
|
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 |
|---|---|---|
|
|
是否成功 |
|
|
錯誤代碼 (AppErrorCode) |
|
|
錯誤訊息 |
|
|
發生時間戳記 |
|
|
回傳資料 |
|
|
詳細錯誤資訊 (Map) |
|
|
Account ID |
|
|
Email 地址 |
|
|
帳號狀態(ACTIVE / PENDING / SUSPENDED) |
|
|
帳號角色(USER / ADMIN) |
|
|
建立時間戳記 |
|
|
最後更新時間戳記 |
Admin Session Management
GET /admin/accounts/{id}/sessions
List active sessions for a specific account (requires ADMIN role).
Request headers:
| Name | Description |
|---|---|
|
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 |
|---|---|
|
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 |
|---|---|---|
|
|
是否成功 |
|
|
錯誤代碼 (AppErrorCode) |
|
|
錯誤訊息 |
|
|
發生時間戳記 |
|
|
回傳資料 |
|
|
詳細錯誤資訊 (Map) |
|
|
Active session 列表 |
|
|
Session ID |
|
|
Client ID |
|
|
User-Agent 字串 |
|
|
Session 建立時間戳記 |
|
|
最後活動時間戳記 |
|
|
ISO 3166-1 alpha-2 國碼 |
|
|
城市名稱 |
|
|
IANA 時區 |
|
|
緯度 |
|
|
經度 |
|
|
地區代碼 |
POST /admin/sessions/revoke
Revoke a single session (requires ADMIN role).
Request headers:
| Name | Description |
|---|---|
|
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 |
|---|---|---|
|
|
要撤銷的 Session ID |
|
|
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 |
|---|---|---|
|
|
是否成功 |
|
|
錯誤代碼 (AppErrorCode) |
|
|
錯誤訊息 |
|
|
發生時間戳記 |
|
|
回傳資料 |
|
|
詳細錯誤資訊 (Map) |
|
|
操作是否成功 |
POST /admin/sessions/revoke-all
Revoke all sessions for an account (requires ADMIN role).
Request headers:
| Name | Description |
|---|---|
|
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 |
|---|---|---|
|
|
要撤銷所有 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 |
|---|---|---|
|
|
是否成功 |
|
|
錯誤代碼 (AppErrorCode) |
|
|
錯誤訊息 |
|
|
發生時間戳記 |
|
|
回傳資料 |
|
|
詳細錯誤資訊 (Map) |
|
|
操作是否成功 |
Admin Geo Statistics
GET /admin/statistics/geo
Get session geo distribution as H3 hexagon aggregation (requires ADMIN role).
Request headers:
| Name | Description |
|---|---|
|
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 |
|---|---|---|
|
|
是否成功 |
|
|
錯誤代碼 (AppErrorCode) |
|
|
錯誤訊息 |
|
|
發生時間戳記 |
|
|
回傳資料 |
|
|
詳細錯誤資訊 (Map) |
|
|
Session 總數 |
|
|
H3 解析度 |
|
|
H3 六角格聚合資料 |
|
|
H3 cell 索引 |
|
|
該 cell 的 session 數量 |
|
|
按國家分組統計 |
|
|
ISO 3166-1 alpha-2 國碼 |
|
|
該國家的 session 數量 |
|
|
佔總 session 的百分比 |
|
|
按時區分組統計 |
|
|
IANA 時區名稱 |
|
|
該時區的 session 數量 |