fix(console): add account api fallback proxy
This commit is contained in:
parent
343a93864f
commit
5c84390b90
332
config/feature_flags.yaml
Normal file
332
config/feature_flags.yaml
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
# Feature flag inventory for the dashboard and public site.
|
||||||
|
# This file is a human-readable catalog of page-level and module-level flags.
|
||||||
|
|
||||||
|
meta:
|
||||||
|
app: console.svc.plus
|
||||||
|
scope:
|
||||||
|
- public-site
|
||||||
|
- dashboard
|
||||||
|
- xworkmate
|
||||||
|
- cloud-iac
|
||||||
|
- docs
|
||||||
|
source:
|
||||||
|
- src/config/feature-toggles.json
|
||||||
|
- src/modules/extensions/builtin
|
||||||
|
- src/app
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
current_implementation:
|
||||||
|
toggles_json: src/config/feature-toggles.json
|
||||||
|
loader: src/lib/featureToggles.ts
|
||||||
|
extension_flags: src/lib/featureFlags.ts
|
||||||
|
notes:
|
||||||
|
- "feature-toggles.json is the active runtime source for path-based gating."
|
||||||
|
- "This YAML is an inventory and planning file; it does not currently drive runtime behavior."
|
||||||
|
|
||||||
|
sections:
|
||||||
|
globalNavigation:
|
||||||
|
enabled: true
|
||||||
|
description: Top-level navigation and auth entry points.
|
||||||
|
default_channel: stable
|
||||||
|
routes:
|
||||||
|
- path: /
|
||||||
|
status: enabled
|
||||||
|
channel: stable
|
||||||
|
- path: /docs
|
||||||
|
status: enabled
|
||||||
|
channel: beta
|
||||||
|
- path: /blogs
|
||||||
|
status: enabled
|
||||||
|
note: Public content listing, not currently toggle-gated in code.
|
||||||
|
- path: /services
|
||||||
|
status: enabled
|
||||||
|
note: Public service directory.
|
||||||
|
- path: /prices
|
||||||
|
status: enabled
|
||||||
|
- path: /support
|
||||||
|
status: enabled
|
||||||
|
- path: /about
|
||||||
|
status: enabled
|
||||||
|
- path: /login
|
||||||
|
status: enabled
|
||||||
|
channel: stable
|
||||||
|
- path: /register
|
||||||
|
status: enabled
|
||||||
|
channel: stable
|
||||||
|
- path: /panel
|
||||||
|
status: enabled
|
||||||
|
channel: stable
|
||||||
|
- path: /panel/management
|
||||||
|
status: enabled
|
||||||
|
note: Shown only to admin/operator users.
|
||||||
|
- path: /cloud_iac
|
||||||
|
status: enabled
|
||||||
|
channel: develop
|
||||||
|
- path: /download
|
||||||
|
status: enabled
|
||||||
|
channel: stable
|
||||||
|
- path: /insight
|
||||||
|
status: enabled
|
||||||
|
channel: develop
|
||||||
|
- path: /xworkmate
|
||||||
|
status: enabled
|
||||||
|
|
||||||
|
appModules:
|
||||||
|
enabled: true
|
||||||
|
description: Path-based module gating used by route handlers and content loaders.
|
||||||
|
routes:
|
||||||
|
- path: /docs
|
||||||
|
status: enabled
|
||||||
|
uses: src/app/docs/page.tsx
|
||||||
|
- path: /docs/[collection]
|
||||||
|
status: enabled
|
||||||
|
uses: src/app/docs/[collection]/page.tsx
|
||||||
|
- path: /docs/[collection]/[...slug]
|
||||||
|
status: enabled
|
||||||
|
uses: src/app/docs/[collection]/[...slug]/page.tsx
|
||||||
|
- path: /download
|
||||||
|
status: enabled
|
||||||
|
uses: src/app/download/page.tsx
|
||||||
|
- path: /download/[...segments]
|
||||||
|
status: enabled
|
||||||
|
uses: src/app/download/[...segments]/page.tsx
|
||||||
|
- path: /cloud_iac
|
||||||
|
status: enabled
|
||||||
|
uses: src/app/cloud_iac/page.tsx
|
||||||
|
- path: /cloud_iac/[provider]
|
||||||
|
status: enabled
|
||||||
|
uses: src/app/cloud_iac/[provider]/page.tsx
|
||||||
|
- path: /cloud_iac/[provider]/[service]
|
||||||
|
status: enabled
|
||||||
|
uses: src/app/cloud_iac/[provider]/[service]/page.tsx
|
||||||
|
- path: /insight
|
||||||
|
status: enabled
|
||||||
|
uses: src/app/services/insight/page.tsx
|
||||||
|
- path: /editor
|
||||||
|
status: enabled
|
||||||
|
uses: src/app/editor/page.tsx
|
||||||
|
- path: /editor/wechat
|
||||||
|
status: enabled
|
||||||
|
- path: /editor/xiaohongshu
|
||||||
|
status: enabled
|
||||||
|
- path: /xworkmate
|
||||||
|
status: enabled
|
||||||
|
uses: src/app/xworkmate/page.tsx
|
||||||
|
- path: /xworkmate/admin
|
||||||
|
status: enabled
|
||||||
|
uses: src/app/xworkmate/admin/page.tsx
|
||||||
|
- path: /xworkmate/integrations
|
||||||
|
status: enabled
|
||||||
|
uses: src/app/xworkmate/integrations/page.tsx
|
||||||
|
|
||||||
|
cmsExperience:
|
||||||
|
enabled: true
|
||||||
|
description: CMS/homepage experience gating.
|
||||||
|
routes:
|
||||||
|
- path: /homepage
|
||||||
|
status: enabled
|
||||||
|
children:
|
||||||
|
- path: /homepage/dynamic
|
||||||
|
status: enabled
|
||||||
|
|
||||||
|
extensions:
|
||||||
|
builtin.user-center:
|
||||||
|
enabled: true
|
||||||
|
description: Core dashboard user center and account management extension.
|
||||||
|
routes:
|
||||||
|
- path: /panel
|
||||||
|
id: dashboard
|
||||||
|
enabled: true
|
||||||
|
sidebar_section: workspace
|
||||||
|
- path: /panel/agent
|
||||||
|
id: agents
|
||||||
|
enabled: true
|
||||||
|
env_var: NEXT_PUBLIC_FEATURE_AGENT_MODULE
|
||||||
|
default_enabled: true
|
||||||
|
sidebar_section: productivity
|
||||||
|
- path: /panel/api
|
||||||
|
id: apis
|
||||||
|
enabled: true
|
||||||
|
env_var: NEXT_PUBLIC_FEATURE_API_MODULE
|
||||||
|
default_enabled: true
|
||||||
|
sidebar_section: productivity
|
||||||
|
- path: /panel/account
|
||||||
|
id: accounts
|
||||||
|
enabled: true
|
||||||
|
sidebar_section: management
|
||||||
|
- path: /panel/subscription
|
||||||
|
id: subscription
|
||||||
|
enabled: true
|
||||||
|
env_var: NEXT_PUBLIC_FEATURE_SUBSCRIPTION_MODULE
|
||||||
|
default_enabled: true
|
||||||
|
sidebar_section: management
|
||||||
|
- path: /panel/ldp
|
||||||
|
id: ldp
|
||||||
|
enabled: true
|
||||||
|
env_var: NEXT_PUBLIC_FEATURE_LDP_MODULE
|
||||||
|
default_enabled: false
|
||||||
|
sidebar_section: management
|
||||||
|
- path: /panel/appearance
|
||||||
|
id: appearance
|
||||||
|
enabled: true
|
||||||
|
sidebar_section: preferences
|
||||||
|
- path: /panel/management
|
||||||
|
id: management
|
||||||
|
enabled: true
|
||||||
|
roles:
|
||||||
|
- admin
|
||||||
|
- operator
|
||||||
|
permissions:
|
||||||
|
- admin.settings.read
|
||||||
|
- admin.users.metrics.read
|
||||||
|
- admin.users.list.read
|
||||||
|
- admin.agents.status.read
|
||||||
|
- admin.blacklist.read
|
||||||
|
sidebar_hidden: true
|
||||||
|
|
||||||
|
builtin.infra:
|
||||||
|
enabled: true
|
||||||
|
description: Infrastructure and ops extension.
|
||||||
|
routes:
|
||||||
|
- path: /panel/deployments
|
||||||
|
id: deployments
|
||||||
|
enabled: true
|
||||||
|
sidebar_section: infra
|
||||||
|
- path: /panel/resources
|
||||||
|
id: resources
|
||||||
|
enabled: true
|
||||||
|
sidebar_section: infra
|
||||||
|
- path: /panel/api-keys
|
||||||
|
id: apiKeys
|
||||||
|
enabled: true
|
||||||
|
sidebar_section: infra
|
||||||
|
- path: /panel/observability
|
||||||
|
id: logs
|
||||||
|
enabled: true
|
||||||
|
sidebar_section: infra
|
||||||
|
- path: /panel/settings
|
||||||
|
id: settings
|
||||||
|
enabled: true
|
||||||
|
sidebar_section: preferences
|
||||||
|
|
||||||
|
pages:
|
||||||
|
public:
|
||||||
|
- path: /
|
||||||
|
component: src/app/page.tsx
|
||||||
|
status: enabled
|
||||||
|
- path: /services
|
||||||
|
component: src/app/services/page.tsx
|
||||||
|
status: enabled
|
||||||
|
- path: /about
|
||||||
|
component: src/app/about/page.tsx
|
||||||
|
status: enabled
|
||||||
|
- path: /prices
|
||||||
|
component: src/app/prices/page.tsx
|
||||||
|
status: enabled
|
||||||
|
- path: /support
|
||||||
|
component: src/app/support/page.tsx
|
||||||
|
status: enabled
|
||||||
|
- path: /support/discussions
|
||||||
|
component: src/app/support/discussions/page.tsx
|
||||||
|
status: enabled
|
||||||
|
- path: /blogs
|
||||||
|
component: src/app/blogs/page.tsx
|
||||||
|
status: enabled
|
||||||
|
- path: /blogs/[...slug]
|
||||||
|
component: src/app/blogs/[...slug]/page.tsx
|
||||||
|
status: enabled
|
||||||
|
- path: /terms
|
||||||
|
component: src/app/terms/page.tsx
|
||||||
|
status: enabled
|
||||||
|
- path: /privacy
|
||||||
|
component: src/app/privacy/page.tsx
|
||||||
|
status: enabled
|
||||||
|
- path: /download
|
||||||
|
component: src/app/download/page.tsx
|
||||||
|
status: gated_by_appModules
|
||||||
|
- path: /download/[...segments]
|
||||||
|
component: src/app/download/[...segments]/page.tsx
|
||||||
|
status: gated_by_appModules
|
||||||
|
- path: /docs
|
||||||
|
component: src/app/docs/page.tsx
|
||||||
|
status: gated_by_appModules
|
||||||
|
- path: /docs/[collection]
|
||||||
|
component: src/app/docs/[collection]/page.tsx
|
||||||
|
status: gated_by_appModules
|
||||||
|
- path: /docs/[collection]/[...slug]
|
||||||
|
component: src/app/docs/[collection]/[...slug]/page.tsx
|
||||||
|
status: gated_by_appModules
|
||||||
|
- path: /cloud_iac
|
||||||
|
component: src/app/cloud_iac/page.tsx
|
||||||
|
status: gated_by_appModules
|
||||||
|
- path: /cloud_iac/[provider]
|
||||||
|
component: src/app/cloud_iac/[provider]/page.tsx
|
||||||
|
status: gated_by_appModules
|
||||||
|
- path: /cloud_iac/[provider]/[service]
|
||||||
|
component: src/app/cloud_iac/[provider]/[service]/page.tsx
|
||||||
|
status: gated_by_appModules
|
||||||
|
- path: /editor
|
||||||
|
component: src/app/editor/page.tsx
|
||||||
|
status: redirect_external
|
||||||
|
- path: /editor/wechat
|
||||||
|
component: src/app/editor/wechat/page.tsx
|
||||||
|
status: enabled
|
||||||
|
- path: /editor/xiaohongshu
|
||||||
|
component: src/app/editor/xiaohongshu/page.tsx
|
||||||
|
status: enabled
|
||||||
|
- path: /xworkmate
|
||||||
|
component: src/app/xworkmate/page.tsx
|
||||||
|
status: enabled
|
||||||
|
- path: /xworkmate/admin
|
||||||
|
component: src/app/xworkmate/admin/page.tsx
|
||||||
|
status: enabled
|
||||||
|
- path: /xworkmate/integrations
|
||||||
|
component: src/app/xworkmate/integrations/page.tsx
|
||||||
|
status: enabled
|
||||||
|
|
||||||
|
auth:
|
||||||
|
- path: /login
|
||||||
|
component: src/app/(auth)/login/page.tsx
|
||||||
|
status: gated_by_globalNavigation
|
||||||
|
- path: /register
|
||||||
|
component: src/app/(auth)/register/page.tsx
|
||||||
|
status: gated_by_globalNavigation
|
||||||
|
- path: /email-verification
|
||||||
|
component: src/app/(auth)/email-verification/page.tsx
|
||||||
|
status: gated_by_globalNavigation
|
||||||
|
- path: /logout
|
||||||
|
component: src/app/logout/page.tsx
|
||||||
|
status: enabled
|
||||||
|
|
||||||
|
panel:
|
||||||
|
- path: /panel
|
||||||
|
component: src/app/panel/page.tsx
|
||||||
|
status: extension_route
|
||||||
|
- path: /panel/account
|
||||||
|
component: src/app/panel/account/page.tsx
|
||||||
|
status: extension_route
|
||||||
|
- path: /panel/agent
|
||||||
|
component: src/app/panel/agent/page.tsx
|
||||||
|
status: extension_route
|
||||||
|
- path: /panel/api
|
||||||
|
component: src/app/panel/api/page.tsx
|
||||||
|
status: extension_route
|
||||||
|
- path: /panel/appearance
|
||||||
|
component: src/app/panel/appearance/page.tsx
|
||||||
|
status: extension_route
|
||||||
|
- path: /panel/ldp
|
||||||
|
component: src/app/panel/ldp/page.tsx
|
||||||
|
status: extension_route
|
||||||
|
- path: /panel/management
|
||||||
|
component: src/app/panel/management/page.tsx
|
||||||
|
status: extension_route
|
||||||
|
- path: /panel/subscription
|
||||||
|
component: src/app/panel/subscription/page.tsx
|
||||||
|
status: extension_route
|
||||||
|
- path: /panel/[...segments]
|
||||||
|
component: src/app/panel/[...segments]/page.tsx
|
||||||
|
status: catch_all_extension
|
||||||
|
|
||||||
|
recommendations:
|
||||||
|
- "If this file is intended to become runtime-configurable, wire it into src/lib/featureToggles.ts and keep feature-toggles.json as the generated artifact."
|
||||||
|
- "If this file is intended only for documentation, keep it synchronized with feature-toggles.json and extension definitions."
|
||||||
@ -1,22 +1,22 @@
|
|||||||
# Compose settings
|
# Compose settings
|
||||||
FRONTEND_IMAGE=ghcr.io/cloud-neutral-toolkit/dashboard:replace-me
|
FRONTEND_IMAGE=ghcr.io/cloud-neutral-toolkit/dashboard:replace-me
|
||||||
PRIMARY_DOMAIN=cn.svc.plus
|
PRIMARY_DOMAIN=cn-console.svc.plus
|
||||||
SECONDARY_DOMAIN=cn.onwalk.net
|
SECONDARY_DOMAIN=cn-console.onwalk.net
|
||||||
|
|
||||||
# Frontend runtime
|
# Frontend runtime
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
RUNTIME_ENV=prod
|
RUNTIME_ENV=prod
|
||||||
REGION=cn
|
REGION=cn
|
||||||
APP_BASE_URL=https://cn.svc.plus
|
APP_BASE_URL=https://cn-console.svc.plus
|
||||||
NEXT_PUBLIC_APP_BASE_URL=https://cn.svc.plus
|
NEXT_PUBLIC_APP_BASE_URL=https://cn-console.svc.plus
|
||||||
NEXT_PUBLIC_SITE_URL=https://cn.svc.plus
|
NEXT_PUBLIC_SITE_URL=https://cn-console.svc.plus
|
||||||
NEXT_PUBLIC_LOGIN_URL=https://cn.svc.plus/login
|
NEXT_PUBLIC_LOGIN_URL=https://cn-console.svc.plus/login
|
||||||
NEXT_PUBLIC_DOCS_BASE_URL=https://cn.svc.plus/docs
|
NEXT_PUBLIC_DOCS_BASE_URL=https://cn-console.svc.plus/docs
|
||||||
SESSION_COOKIE_SECURE=true
|
SESSION_COOKIE_SECURE=true
|
||||||
NEXT_PUBLIC_SESSION_COOKIE_SECURE=true
|
NEXT_PUBLIC_SESSION_COOKIE_SECURE=true
|
||||||
RUNTIME_HOSTNAME=cn.svc.plus
|
RUNTIME_HOSTNAME=cn-console.svc.plus
|
||||||
DEPLOYMENT_HOSTNAME=cn.svc.plus
|
DEPLOYMENT_HOSTNAME=cn-console.svc.plus
|
||||||
NEXT_PUBLIC_RUNTIME_ENVIRONMENT=prod
|
NEXT_PUBLIC_RUNTIME_ENVIRONMENT=prod
|
||||||
NEXT_PUBLIC_RUNTIME_REGION=cn
|
NEXT_PUBLIC_RUNTIME_REGION=cn
|
||||||
|
|
||||||
|
|||||||
89
docs/architecture/web-console/overview.md
Normal file
89
docs/architecture/web-console/overview.md
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# console.svc.plus Web Console Architecture
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
`console.svc.plus` is the browser-facing control plane. It is a Next.js App Router application that combines public pages, docs browsing, account/admin panels, and a BFF layer that forwards requests to downstream services.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph Pages["src/app pages"]
|
||||||
|
Root["/ -> landing page"]
|
||||||
|
Docs["/docs, /docs/[collection], /docs/[collection]/[...slug]\nDocs reader"]
|
||||||
|
Auth["/login, /register, /email-verification, /logout\nAuth flows"]
|
||||||
|
Panel["/panel/*\nUser / admin console"]
|
||||||
|
Tools["/editor/*, /download/*, /cloud_iac/*, /xworkmate/*\nProduct tools"]
|
||||||
|
Content["/blogs/*, /services/*, /support/*, /prices, /about, /privacy, /terms, /[slug]"]
|
||||||
|
Admin["/dashboard/cms\nCMS/admin entry"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph BFF["src/app/api route handlers"]
|
||||||
|
AuthAPI["/api/auth/*"]
|
||||||
|
AdminAPI["/api/admin/*"]
|
||||||
|
AgentAPI["/api/agent-server/[...segments]\n/api/agent/[...segments]"]
|
||||||
|
RagAPI["/api/rag/query\n/api/askai"]
|
||||||
|
UtilAPI["/api/users\n/api/ping\n/api/content-meta\n/api/render-markdown\n/api/dl-index/*\n/api/marketing/home-stats\n/api/integrations/*\n/api/moltbot/chat\n/api/openclaw/assistant\n/api/task/[...segments]\n/api/xworkmate/profile"]
|
||||||
|
SandboxAPI["/api/sandbox/*"]
|
||||||
|
end
|
||||||
|
|
||||||
|
Accounts["accounts.svc.plus"]
|
||||||
|
Rag["rag-server.svc.plus"]
|
||||||
|
DocsSvc["docs.svc.plus"]
|
||||||
|
External["Other upstream services"]
|
||||||
|
|
||||||
|
AuthAPI --> Accounts
|
||||||
|
AdminAPI --> Accounts
|
||||||
|
AgentAPI --> Accounts
|
||||||
|
RagAPI --> Rag
|
||||||
|
UtilAPI --> DocsSvc
|
||||||
|
UtilAPI --> External
|
||||||
|
SandboxAPI --> Accounts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend Routes
|
||||||
|
|
||||||
|
| Route family | Path | Purpose |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Home | `/` | Public landing page and site entry |
|
||||||
|
| Docs | `/docs`, `/docs/[collection]`, `/docs/[collection]/[...slug]` | Documentation reader, sidebar, and TOC |
|
||||||
|
| Auth | `/login`, `/register`, `/email-verification`, `/logout` | Sign in / sign up / email verification / session cleanup |
|
||||||
|
| Panel | `/panel`, `/panel/account`, `/panel/agent`, `/panel/api`, `/panel/appearance`, `/panel/ldp`, `/panel/management`, `/panel/subscription`, `/panel/[...segments]` | Signed-in account and admin console |
|
||||||
|
| Tools | `/editor`, `/editor/wechat`, `/editor/xiaohongshu`, `/download`, `/download/[...segments]`, `/cloud_iac`, `/cloud_iac/[provider]`, `/cloud_iac/[provider]/[service]`, `/xworkmate`, `/xworkmate/admin`, `/xworkmate/integrations` | Product tools and service explorers |
|
||||||
|
| Content | `/blogs`, `/blogs/[...slug]`, `/services`, `/services/openclaw`, `/services/insight`, `/support`, `/support/discussions`, `/about`, `/prices`, `/privacy`, `/terms`, `/[slug]` | Marketing / informational pages |
|
||||||
|
| Admin / CMS | `/dashboard/cms` | CMS or content-management entry |
|
||||||
|
| Error pages | `/404`, `/500` | Static error surfaces |
|
||||||
|
|
||||||
|
## BFF / API Routes
|
||||||
|
|
||||||
|
| API family | Path | Purpose | Upstream target |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Auth | `/api/auth/login`, `/api/auth/register`, `/api/auth/register/send`, `/api/auth/register/verify`, `/api/auth/verify-email`, `/api/auth/verify-email/send`, `/api/auth/session`, `/api/auth/token/exchange` | Login, registration, token exchange, session lookup | `accounts.svc.plus/api/auth/*` |
|
||||||
|
| MFA | `/api/auth/mfa/status`, `/api/auth/mfa/setup`, `/api/auth/mfa/verify`, `/api/auth/mfa/disable` | TOTP setup and verification | `accounts.svc.plus/api/auth/*` |
|
||||||
|
| OAuth / billing | `/api/auth/oauth/login/[provider]`, `/api/auth/stripe/checkout`, `/api/auth/stripe/portal`, `/api/auth/subscriptions`, `/api/auth/subscriptions/cancel` | OAuth redirects and billing actions | `accounts.svc.plus/api/auth/*` |
|
||||||
|
| Admin | `/api/admin/settings`, `/api/admin/homepage-video`, `/api/admin/users/*`, `/api/admin/blacklist/*`, `/api/admin/sandbox/*` | Account admin operations | `accounts.svc.plus/api/*` |
|
||||||
|
| Agent bridge | `/api/agent-server/[...segments]`, `/api/agent/[...segments]` | Agent registry/status and legacy alias | `accounts.svc.plus` |
|
||||||
|
| RAG | `/api/rag/query`, `/api/askai` | Retrieval and answer generation | `rag-server.svc.plus` |
|
||||||
|
| Sandbox / session shaping | `/api/sandbox/assume`, `/api/sandbox/assume/revert`, `/api/sandbox/assume/status`, `/api/sandbox/binding` | Guest / demo identity switching | `accounts.svc.plus/api/auth/*` and internal sandbox reads |
|
||||||
|
| Content / docs | `/api/content-meta`, `/api/render-markdown`, `/api/blogs/latest`, `/api/dl-index/*` | Docs/content rendering and download manifests | docs / CDN / download service |
|
||||||
|
| Integrations | `/api/integrations/defaults`, `/api/integrations/probe`, `/api/marketing/home-stats` | Integration defaults, health probes, marketing metrics | config-dependent external services |
|
||||||
|
| Misc | `/api/ping`, `/api/users`, `/api/xworkmate/profile`, `/api/task/[...segments]`, `/api/openclaw/assistant`, `/api/moltbot/chat`, `/api/render-markdown` | Health, user lookup, profile, task and assistant proxies | `accounts.svc.plus`, internal API, task services |
|
||||||
|
|
||||||
|
## Auth and Session Notes
|
||||||
|
|
||||||
|
- Browser calls use the session cookie and BFF logic in `src/server/account/session.ts`.
|
||||||
|
- Service-to-service calls use `INTERNAL_SERVICE_TOKEN` when configured.
|
||||||
|
- `api/agent-server/[...segments]` keeps caller `Authorization` untouched when an agent token is already present.
|
||||||
|
- `api/agent-server/[...segments]` forwards the dashboard session token for browser-driven calls.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `accounts.svc.plus` for identity, profile, sandbox, billing, and admin actions.
|
||||||
|
- `rag-server.svc.plus` for RAG query and AskAI.
|
||||||
|
- `docs.svc.plus` for docs content and navigation data.
|
||||||
|
- CDN / external providers for content, analytics, and integration checks.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Route groups in parentheses, such as `(auth)`, are Next.js organizational folders and do not appear in the public URL.
|
||||||
|
- The BFF layer is the main place where console-specific auth shaping, cookie management, and upstream proxying happen.
|
||||||
@ -3,10 +3,10 @@
|
|||||||
## Production Baseline
|
## Production Baseline
|
||||||
|
|
||||||
- Runtime: `Caddy + Docker Compose`
|
- Runtime: `Caddy + Docker Compose`
|
||||||
- Deploy host: `47.120.61.35`
|
- Deploy host: `root@cn-console.svc.plus`
|
||||||
- Domains:
|
- Domains:
|
||||||
- `cn.svc.plus`
|
- `cn-console.svc.plus`
|
||||||
- `cn.onwalk.net`
|
- `cn-console.onwalk.net`
|
||||||
- Frontend release workflow: `.github/workflows/service_release_frontend-deploy.yml`
|
- Frontend release workflow: `.github/workflows/service_release_frontend-deploy.yml`
|
||||||
|
|
||||||
## Operating Model
|
## Operating Model
|
||||||
@ -21,10 +21,9 @@ The stack is static-first:
|
|||||||
- The Next.js standalone container serves dynamic HTML, auth endpoints, and API proxy routes. Static assets and hashed CSS/JS files are extracted by the `frontend-assets` helper task, so the runtime no longer needs to compile anything on the single-node host.
|
- The Next.js standalone container serves dynamic HTML, auth endpoints, and API proxy routes. Static assets and hashed CSS/JS files are extracted by the `frontend-assets` helper task, so the runtime no longer needs to compile anything on the single-node host.
|
||||||
- `docs.svc.plus` is the source of truth for rendered docs/blog pages; the browser does not call it directly.
|
- `docs.svc.plus` is the source of truth for rendered docs/blog pages; the browser does not call it directly.
|
||||||
|
|
||||||
Releases are orchestrated through `.github/workflows/service_release_frontend-deploy.yml`. That workflow clones the knowledge repository, runs the Docker build/push sequence, renders `.env.runtime`, and ships `docker-compose.yml`, `Caddyfile`, and the runtime env file to the host. The control-plane workflow `.github/workflows/service_release_apiserver-deploy.yml` then updates Cloudflare DNS for the release domain (via `scripts/github-actions/update-release-dns.sh`) so `cn.svc.plus` and the redirected alias `cn.onwalk.net` point at the new environment.
|
Releases are orchestrated through `.github/workflows/service_release_frontend-deploy.yml`. That workflow builds/pushes the image, renders `.env.runtime` including `DOCS_SERVICE_URL` / `DOCS_SERVICE_INTERNAL_URL`, and ships `docker-compose.yml`, `Caddyfile`, and the runtime env file to the host. The control-plane workflow `.github/workflows/service_release_apiserver-deploy.yml` then updates Cloudflare DNS for the release domain (via `scripts/github-actions/update-release-dns.sh`) so `cn-console.svc.plus` and the redirected alias `cn-console.onwalk.net` point at the new environment.
|
||||||
Releases are orchestrated through `.github/workflows/service_release_frontend-deploy.yml`. That workflow builds/pushes the image, renders `.env.runtime` including `DOCS_SERVICE_URL` / `DOCS_SERVICE_INTERNAL_URL`, and ships `docker-compose.yml`, `Caddyfile`, and the runtime env file to the host. The control-plane workflow `.github/workflows/service_release_apiserver-deploy.yml` then updates Cloudflare DNS for the release domain (via `scripts/github-actions/update-release-dns.sh`) so `cn.svc.plus` and the redirected alias `cn.onwalk.net` point at the new environment.
|
|
||||||
|
|
||||||
This baseline is intentional for the weak-IO single-node host (47.120.61.35). No images are built on the target machine, keeping the deployment lightweight: the host only logs into GHCR, pulls the `dashboard` image, extracts assets into `frontend_static`, and starts `dashboard` plus `caddy` containers via `docker compose`.
|
This baseline is intentional for the weak-IO single-node host `root@cn-console.svc.plus`. No images are built on the target machine, keeping the deployment lightweight: the host only logs into GHCR, pulls the `dashboard` image, extracts assets into `frontend_static`, and starts `dashboard` plus `caddy` containers via `docker compose`.
|
||||||
|
|
||||||
`docs.svc.plus` is now the dedicated docs/blog service for the frontend delivery path.
|
`docs.svc.plus` is now the dedicated docs/blog service for the frontend delivery path.
|
||||||
|
|
||||||
|
|||||||
@ -3,10 +3,10 @@
|
|||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
- Repository: `console.svc.plus`
|
- Repository: `console.svc.plus`
|
||||||
- Target host: `root@47.120.61.35`
|
- Target host: `root@cn-console.svc.plus`
|
||||||
- Public domains:
|
- Public domains:
|
||||||
- `cn.svc.plus`
|
- `cn-console.svc.plus`
|
||||||
- `cn.onwalk.net`
|
- `cn-console.onwalk.net`
|
||||||
- Delivery mode: `GitHub Actions + GHCR + Caddy + Docker Compose`
|
- Delivery mode: `GitHub Actions + GHCR + Caddy + Docker Compose`
|
||||||
|
|
||||||
This document defines the deployment baseline for the China-facing frontend node. The source of truth is this upstream repository. The control-plane repository may consume the repo through git submodule, but should not become the primary place where this deployment design lives.
|
This document defines the deployment baseline for the China-facing frontend node. The source of truth is this upstream repository. The control-plane repository may consume the repo through git submodule, but should not become the primary place where this deployment design lives.
|
||||||
@ -26,7 +26,7 @@ The result should support repeatable releases, quick rollback by image tag, and
|
|||||||
|
|
||||||
### Host constraints
|
### Host constraints
|
||||||
|
|
||||||
- `47.120.61.35` is a single-node host
|
- `cn-console.svc.plus` is a single-node host
|
||||||
- deployment user is `root`
|
- deployment user is `root`
|
||||||
- local image build on the host is explicitly disallowed
|
- local image build on the host is explicitly disallowed
|
||||||
- IO pressure should be minimized during release
|
- IO pressure should be minimized during release
|
||||||
@ -140,16 +140,16 @@ Temporary nature:
|
|||||||
|
|
||||||
Primary domain:
|
Primary domain:
|
||||||
|
|
||||||
- `cn.svc.plus`
|
- `cn-console.svc.plus`
|
||||||
|
|
||||||
Secondary domain:
|
Secondary domain:
|
||||||
|
|
||||||
- `cn.onwalk.net`
|
- `cn-console.onwalk.net`
|
||||||
|
|
||||||
Current routing decision:
|
Current routing decision:
|
||||||
|
|
||||||
- Caddy accepts both domains
|
- Caddy accepts both domains
|
||||||
- requests for `cn.onwalk.net` are redirected permanently to `cn.svc.plus`
|
- requests for `cn-console.onwalk.net` are redirected permanently to `cn-console.svc.plus`
|
||||||
|
|
||||||
Reason:
|
Reason:
|
||||||
|
|
||||||
@ -200,7 +200,7 @@ Rollback steps:
|
|||||||
1. set `FRONTEND_IMAGE` to a previous known-good tag
|
1. set `FRONTEND_IMAGE` to a previous known-good tag
|
||||||
2. rerun `frontend-assets`
|
2. rerun `frontend-assets`
|
||||||
3. restart `dashboard` and `caddy`
|
3. restart `dashboard` and `caddy`
|
||||||
4. verify `cn.svc.plus`
|
4. verify `cn-console.svc.plus`
|
||||||
|
|
||||||
This avoids rebuilding and keeps rollback cheap on the weak-IO host.
|
This avoids rebuilding and keeps rollback cheap on the weak-IO host.
|
||||||
|
|
||||||
@ -261,7 +261,7 @@ Mitigation:
|
|||||||
### Near term
|
### Near term
|
||||||
|
|
||||||
- populate required GitHub `vars` and `secrets`
|
- populate required GitHub `vars` and `secrets`
|
||||||
- run the workflow against `47.120.61.35`
|
- run the workflow against `root@cn-console.svc.plus`
|
||||||
- validate DNS, TLS, static assets, login flow, and upstream API proxy behavior
|
- validate DNS, TLS, static assets, login flow, and upstream API proxy behavior
|
||||||
|
|
||||||
### Later
|
### Later
|
||||||
|
|||||||
@ -4,11 +4,11 @@
|
|||||||
|
|
||||||
- Runtime: `console.svc.plus`
|
- Runtime: `console.svc.plus`
|
||||||
- Topology: `Caddy + Docker Compose + GitHub Actions`
|
- Topology: `Caddy + Docker Compose + GitHub Actions`
|
||||||
- Deploy host: `root@47.120.61.35`
|
- Deploy host: `root@cn-console.svc.plus`
|
||||||
- Public domains:
|
- Public domains:
|
||||||
- `https://cn.svc.plus`
|
- `https://cn-console.svc.plus`
|
||||||
- `https://cn.onwalk.net`
|
- `https://cn-console.onwalk.net`
|
||||||
- Primary origin: `https://cn.svc.plus`
|
- Primary origin: `https://cn-console.svc.plus`
|
||||||
|
|
||||||
## Current Delivery Model
|
## Current Delivery Model
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ This is intentionally static-first for the current weak-IO single-node host. Dyn
|
|||||||
|
|
||||||
## Control Plane & DNS Stage
|
## Control Plane & DNS Stage
|
||||||
|
|
||||||
The control repo (`github-org-x-evor`) tracks `console.svc.plus` through `console.svc.plus.code-workspace` and keeps the `subrepos/accounts.svc.plus` pointer in sync via `skills/cross-repo-upstream-submodule-sync`. Releases resolve metadata with that workspace and the `config/single-node-release` manifests. After `.github/workflows/service_release_frontend-deploy.yml` finishes pushing the new image, the control-plane workflow `.github/workflows/service_release_apiserver-deploy.yml` calls `scripts/github-actions/update-release-dns.sh` to update Cloudflare DNS so the new endpoint is reachable under `cn.svc.plus` and `cn.onwalk.net`.
|
The control repo (`github-org-x-evor`) tracks `console.svc.plus` through `console.svc.plus.code-workspace` and keeps the `subrepos/accounts.svc.plus` pointer in sync via `skills/cross-repo-upstream-submodule-sync`. Releases resolve metadata with that workspace and the `config/single-node-release` manifests. After `.github/workflows/service_release_frontend-deploy.yml` finishes pushing the new image, the control-plane workflow `.github/workflows/service_release_apiserver-deploy.yml` calls `scripts/github-actions/update-release-dns.sh` to update Cloudflare DNS so the new endpoint is reachable under `cn-console.svc.plus` and `cn-console.onwalk.net`.
|
||||||
|
|
||||||
## Runtime Layout
|
## Runtime Layout
|
||||||
|
|
||||||
@ -95,10 +95,10 @@ Repository/environment variables recommended:
|
|||||||
2. Docker builds the frontend image with the public `NEXT_PUBLIC_*` values needed at build time.
|
2. Docker builds the frontend image with the public `NEXT_PUBLIC_*` values needed at build time.
|
||||||
3. The image is pushed to GHCR.
|
3. The image is pushed to GHCR.
|
||||||
4. The workflow runs a matrix DNS stage, updating one public domain per job.
|
4. The workflow runs a matrix DNS stage, updating one public domain per job.
|
||||||
5. The workflow renders `.env.runtime`, including docs service runtime endpoints.
|
5. The workflow renders `.env.runtime`, including docs service runtime endpoints and the `cn-console` origin settings.
|
||||||
6. The workflow uploads `docker-compose.yml`, `Caddyfile`, and `.env.runtime` to the host.
|
6. The workflow uploads `docker-compose.yml`, `Caddyfile`, and `.env.runtime` to the host.
|
||||||
7. The host pulls the new image, refreshes the static asset volume, and starts `dashboard + caddy`.
|
7. The host pulls the new image, refreshes the static asset volume, and starts `dashboard + caddy`.
|
||||||
8. The workflow verifies `cn.svc.plus` and `cn.onwalk.net`.
|
8. The workflow verifies `cn-console.svc.plus` and `cn-console.onwalk.net`.
|
||||||
|
|
||||||
## Verification Commands
|
## Verification Commands
|
||||||
|
|
||||||
@ -122,10 +122,10 @@ PY
|
|||||||
Remote checks:
|
Remote checks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh root@47.120.61.35 "cd /opt/console-svc-plus && docker compose --env-file .env.runtime ps"
|
ssh root@cn-console.svc.plus "cd /opt/console-svc-plus && docker compose --env-file .env.runtime ps"
|
||||||
ssh root@47.120.61.35 "curl -fsSI -H 'Host: cn.svc.plus' http://127.0.0.1/"
|
ssh root@cn-console.svc.plus "curl -fsSI -H 'Host: cn-console.svc.plus' http://127.0.0.1/"
|
||||||
curl -fsSIL https://cn.svc.plus
|
curl -fsSIL https://cn-console.svc.plus
|
||||||
curl -fsSIL https://cn.onwalk.net
|
curl -fsSIL https://cn-console.onwalk.net
|
||||||
```
|
```
|
||||||
|
|
||||||
## Failure Signatures
|
## Failure Signatures
|
||||||
@ -134,9 +134,9 @@ curl -fsSIL https://cn.onwalk.net
|
|||||||
The workflow token or package visibility is wrong.
|
The workflow token or package visibility is wrong.
|
||||||
- `frontend-assets` fails
|
- `frontend-assets` fails
|
||||||
The image layout changed and no longer contains `/app/dashboard/static` or `/app/dashboard/public`.
|
The image layout changed and no longer contains `/app/dashboard/static` or `/app/dashboard/public`.
|
||||||
- `cn.svc.plus` returns `502`
|
- `cn-console.svc.plus` returns `502`
|
||||||
Caddy is up, but the `dashboard` container failed or is not reachable on port `3000`.
|
Caddy is up, but the `dashboard` container failed or is not reachable on port `3000`.
|
||||||
- `cn.onwalk.net` does not redirect
|
- `cn-console.onwalk.net` does not redirect
|
||||||
Check the deployed `Caddyfile` and domain DNS.
|
Check the deployed `Caddyfile` and domain DNS.
|
||||||
|
|
||||||
## Rollback
|
## Rollback
|
||||||
@ -146,8 +146,8 @@ curl -fsSIL https://cn.onwalk.net
|
|||||||
3. Restart the services:
|
3. Restart the services:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh root@47.120.61.35 "cd /opt/console-svc-plus && docker compose --env-file .env.runtime run --rm frontend-assets"
|
ssh root@cn-console.svc.plus "cd /opt/console-svc-plus && docker compose --env-file .env.runtime run --rm frontend-assets"
|
||||||
ssh root@47.120.61.35 "cd /opt/console-svc-plus && docker compose --env-file .env.runtime up -d dashboard caddy"
|
ssh root@cn-console.svc.plus "cd /opt/console-svc-plus && docker compose --env-file .env.runtime up -d dashboard caddy"
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Verify `https://cn.svc.plus` again before closing the incident.
|
4. Verify `https://cn-console.svc.plus` again before closing the incident.
|
||||||
|
|||||||
@ -3,10 +3,10 @@
|
|||||||
## 生产基线
|
## 生产基线
|
||||||
|
|
||||||
- 运行拓扑: `Caddy + Docker Compose`
|
- 运行拓扑: `Caddy + Docker Compose`
|
||||||
- 目标主机: `47.120.61.35`
|
- 目标主机: `root@cn-console.svc.plus`
|
||||||
- 域名:
|
- 域名:
|
||||||
- `cn.svc.plus`
|
- `cn-console.svc.plus`
|
||||||
- `cn.onwalk.net`
|
- `cn-console.onwalk.net`
|
||||||
- 前端独立发布流水线: `.github/workflows/service_release_frontend-deploy.yml`
|
- 前端独立发布流水线: `.github/workflows/service_release_frontend-deploy.yml`
|
||||||
|
|
||||||
## 运行方式
|
## 运行方式
|
||||||
@ -21,9 +21,9 @@
|
|||||||
- Next.js standalone 容器只承接动态页面、认证接口和代理接口,`frontend-assets` 任务会把所有静态文件(包括哈希后的 CSS/JS)拷贝到 `frontend_static`。
|
- Next.js standalone 容器只承接动态页面、认证接口和代理接口,`frontend-assets` 任务会把所有静态文件(包括哈希后的 CSS/JS)拷贝到 `frontend_static`。
|
||||||
- `docs.svc.plus` 是 docs/blog 的运行时内容源,浏览器不会直接访问它。
|
- `docs.svc.plus` 是 docs/blog 的运行时内容源,浏览器不会直接访问它。
|
||||||
|
|
||||||
发布由 `.github/workflows/service_release_frontend-deploy.yml` 驱动,CI 构建/推送镜像、渲染包含 `DOCS_SERVICE_URL` / `DOCS_SERVICE_INTERNAL_URL` 的 `.env.runtime`,然后将 `docker-compose.yml`、`Caddyfile` 与运行时环境文件发送到主机。随后控制平面工作流 `.github/workflows/service_release_apiserver-deploy.yml` 通过 `scripts/github-actions/update-release-dns.sh` 更新 Cloudflare DNS,使 `cn.svc.plus` 与别名 `cn.onwalk.net` 指向更新后的环境。
|
发布由 `.github/workflows/service_release_frontend-deploy.yml` 驱动,CI 构建/推送镜像、渲染包含 `DOCS_SERVICE_URL` / `DOCS_SERVICE_INTERNAL_URL` 的 `.env.runtime`,然后将 `docker-compose.yml`、`Caddyfile` 与运行时环境文件发送到主机。随后控制平面工作流 `.github/workflows/service_release_apiserver-deploy.yml` 通过 `scripts/github-actions/update-release-dns.sh` 更新 Cloudflare DNS,使 `cn-console.svc.plus` 与别名 `cn-console.onwalk.net` 指向更新后的环境。
|
||||||
|
|
||||||
这是针对弱 IO 单机主机 `47.120.61.35` 的部署权衡:主机不会在本地构建镜像,只需登录 GHCR、拉取 `dashboard` 镜像、解包静态资源到 `frontend_static`,再通过 `docker compose` 启动 `dashboard` 与 `caddy`。
|
这是针对弱 IO 单机主机 `root@cn-console.svc.plus` 的部署权衡:主机不会在本地构建镜像,只需登录 GHCR、拉取 `dashboard` 镜像、解包静态资源到 `frontend_static`,再通过 `docker compose` 启动 `dashboard` 与 `caddy`。
|
||||||
|
|
||||||
`docs.svc.plus` 已经是前端 docs/blog 内容的独立服务。
|
`docs.svc.plus` 已经是前端 docs/blog 内容的独立服务。
|
||||||
|
|
||||||
|
|||||||
62
src/app/api/account/[...segments]/route.ts
Normal file
62
src/app/api/account/[...segments]/route.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
import type { NextRequest } from 'next/server'
|
||||||
|
|
||||||
|
import { createUpstreamProxyHandler } from '@lib/apiProxy'
|
||||||
|
import { getAccountSession } from '@server/account/session'
|
||||||
|
import { getAccountServiceBaseUrl } from '@server/serviceConfig'
|
||||||
|
|
||||||
|
const ACCOUNT_PREFIX = '/api/account'
|
||||||
|
|
||||||
|
function createHandler() {
|
||||||
|
const upstreamBaseUrl = getAccountServiceBaseUrl()
|
||||||
|
return createUpstreamProxyHandler({
|
||||||
|
upstreamBaseUrl,
|
||||||
|
upstreamPathPrefix: ACCOUNT_PREFIX,
|
||||||
|
getAdditionalHeaders: async (request) => {
|
||||||
|
if (request.headers.get('authorization')) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await getAccountSession(request)
|
||||||
|
if (!session.token) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
authorization: `Bearer ${session.token}`,
|
||||||
|
'x-account-session': session.token,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = createHandler()
|
||||||
|
|
||||||
|
export function GET(request: NextRequest) {
|
||||||
|
return handler(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function POST(request: NextRequest) {
|
||||||
|
return handler(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PUT(request: NextRequest) {
|
||||||
|
return handler(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PATCH(request: NextRequest) {
|
||||||
|
return handler(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DELETE(request: NextRequest) {
|
||||||
|
return handler(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HEAD(request: NextRequest) {
|
||||||
|
return handler(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OPTIONS(request: NextRequest) {
|
||||||
|
return handler(request)
|
||||||
|
}
|
||||||
28
src/app/api/admin/collector/[...segments]/route.ts
Normal file
28
src/app/api/admin/collector/[...segments]/route.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
import type { NextRequest } from 'next/server'
|
||||||
|
|
||||||
|
import { createUpstreamProxyHandler } from '@lib/apiProxy'
|
||||||
|
import { getAccountSession } from '@server/account/session'
|
||||||
|
import { getAccountServiceBaseUrl } from '@server/serviceConfig'
|
||||||
|
|
||||||
|
const ADMIN_COLLECTOR_PREFIX = '/api/admin/collector'
|
||||||
|
|
||||||
|
const handler = createUpstreamProxyHandler({
|
||||||
|
upstreamBaseUrl: getAccountServiceBaseUrl(),
|
||||||
|
upstreamPathPrefix: ADMIN_COLLECTOR_PREFIX,
|
||||||
|
getAdditionalHeaders: async (request) => {
|
||||||
|
const session = await getAccountSession(request)
|
||||||
|
if (!session.token) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
authorization: `Bearer ${session.token}`,
|
||||||
|
'x-account-session': session.token,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export function GET(request: NextRequest) {
|
||||||
|
return handler(request)
|
||||||
|
}
|
||||||
28
src/app/api/admin/scheduler/[...segments]/route.ts
Normal file
28
src/app/api/admin/scheduler/[...segments]/route.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
import type { NextRequest } from 'next/server'
|
||||||
|
|
||||||
|
import { createUpstreamProxyHandler } from '@lib/apiProxy'
|
||||||
|
import { getAccountSession } from '@server/account/session'
|
||||||
|
import { getAccountServiceBaseUrl } from '@server/serviceConfig'
|
||||||
|
|
||||||
|
const ADMIN_SCHEDULER_PREFIX = '/api/admin/scheduler'
|
||||||
|
|
||||||
|
const handler = createUpstreamProxyHandler({
|
||||||
|
upstreamBaseUrl: getAccountServiceBaseUrl(),
|
||||||
|
upstreamPathPrefix: ADMIN_SCHEDULER_PREFIX,
|
||||||
|
getAdditionalHeaders: async (request) => {
|
||||||
|
const session = await getAccountSession(request)
|
||||||
|
if (!session.token) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
authorization: `Bearer ${session.token}`,
|
||||||
|
'x-account-session': session.token,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export function GET(request: NextRequest) {
|
||||||
|
return handler(request)
|
||||||
|
}
|
||||||
28
src/app/api/admin/traffic/[...segments]/route.ts
Normal file
28
src/app/api/admin/traffic/[...segments]/route.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
import type { NextRequest } from 'next/server'
|
||||||
|
|
||||||
|
import { createUpstreamProxyHandler } from '@lib/apiProxy'
|
||||||
|
import { getAccountSession } from '@server/account/session'
|
||||||
|
import { getAccountServiceBaseUrl } from '@server/serviceConfig'
|
||||||
|
|
||||||
|
const ADMIN_TRAFFIC_PREFIX = '/api/admin/traffic'
|
||||||
|
|
||||||
|
const handler = createUpstreamProxyHandler({
|
||||||
|
upstreamBaseUrl: getAccountServiceBaseUrl(),
|
||||||
|
upstreamPathPrefix: ADMIN_TRAFFIC_PREFIX,
|
||||||
|
getAdditionalHeaders: async (request) => {
|
||||||
|
const session = await getAccountSession(request)
|
||||||
|
if (!session.token) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
authorization: `Bearer ${session.token}`,
|
||||||
|
'x-account-session': session.token,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export function GET(request: NextRequest) {
|
||||||
|
return handler(request)
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import useSWR from "swr";
|
|||||||
|
|
||||||
import { openStripePortal } from "@components/billing/stripe-client";
|
import { openStripePortal } from "@components/billing/stripe-client";
|
||||||
import Card from "../components/Card";
|
import Card from "../components/Card";
|
||||||
|
import { fetchAccountPolicy, fetchAccountUsageSummary } from "../lib/fetchAccountUsage";
|
||||||
|
|
||||||
const fetcher = (url: string) =>
|
const fetcher = (url: string) =>
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
@ -48,6 +49,8 @@ export default function SubscriptionPanel() {
|
|||||||
"/api/auth/subscriptions",
|
"/api/auth/subscriptions",
|
||||||
fetcher,
|
fetcher,
|
||||||
);
|
);
|
||||||
|
const { data: usageSummary } = useSWR("account-usage-summary", fetchAccountUsageSummary);
|
||||||
|
const { data: accountPolicy } = useSWR("account-policy", fetchAccountPolicy);
|
||||||
const [submitting, setSubmitting] = useState<string | null>(null);
|
const [submitting, setSubmitting] = useState<string | null>(null);
|
||||||
const [portalLoading, setPortalLoading] = useState(false);
|
const [portalLoading, setPortalLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@ -124,6 +127,49 @@ export default function SubscriptionPanel() {
|
|||||||
|
|
||||||
{error ? <p className="mt-3 text-sm text-red-600">{error}</p> : null}
|
{error ? <p className="mt-3 text-sm text-red-600">{error}</p> : null}
|
||||||
|
|
||||||
|
{usageSummary ? (
|
||||||
|
<div className="mt-4 grid gap-3 md:grid-cols-3">
|
||||||
|
<div className="rounded-xl border border-[color:var(--color-surface-border)] bg-[color:var(--color-surface)] p-4 shadow-sm">
|
||||||
|
<p className="text-xs uppercase tracking-wide text-[var(--color-primary)]">
|
||||||
|
Authoritative Usage
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-2xl font-semibold text-[var(--color-heading)]">
|
||||||
|
{usageSummary.totalBytes.toLocaleString()} B
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm text-[var(--color-text-subtle)]">
|
||||||
|
统计由 accounts.svc.plus 汇总,非本地客户端计数。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-[color:var(--color-surface-border)] bg-[color:var(--color-surface)] p-4 shadow-sm">
|
||||||
|
<p className="text-xs uppercase tracking-wide text-[var(--color-primary)]">
|
||||||
|
Balance / Quota
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-2xl font-semibold text-[var(--color-heading)]">
|
||||||
|
{typeof usageSummary.currentBalance === "number"
|
||||||
|
? usageSummary.currentBalance.toFixed(2)
|
||||||
|
: "—"}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm text-[var(--color-text-subtle)]">
|
||||||
|
剩余配额 {typeof usageSummary.remainingIncludedQuota === "number"
|
||||||
|
? `${usageSummary.remainingIncludedQuota.toLocaleString()} B`
|
||||||
|
: "—"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-[color:var(--color-surface-border)] bg-[color:var(--color-surface)] p-4 shadow-sm">
|
||||||
|
<p className="text-xs uppercase tracking-wide text-[var(--color-primary)]">
|
||||||
|
Policy / Sync
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-base font-semibold text-[var(--color-heading)]">
|
||||||
|
{accountPolicy?.preferredStrategy || "—"}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm text-[var(--color-text-subtle)]">
|
||||||
|
统计延迟约 {usageSummary.syncDelaySeconds ?? 0} 秒,策略组{" "}
|
||||||
|
{accountPolicy?.eligibleNodeGroups?.join(", ") || "—"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<p className="mt-4 text-sm text-[var(--color-text-subtle)]">
|
<p className="mt-4 text-sm text-[var(--color-text-subtle)]">
|
||||||
加载订阅中…
|
加载订阅中…
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { fetchAccountPolicy, fetchAccountUsageSummary } from './fetchAccountUsage'
|
||||||
|
|
||||||
|
describe('fetchAccountUsage', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads the authoritative usage summary from the account api', async () => {
|
||||||
|
vi.spyOn(global, 'fetch').mockResolvedValueOnce(
|
||||||
|
new Response(JSON.stringify({ accountUuid: 'acct-1', totalBytes: 384 }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(fetchAccountUsageSummary()).resolves.toEqual({
|
||||||
|
accountUuid: 'acct-1',
|
||||||
|
totalBytes: 384,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads the authoritative policy snapshot from the account api', async () => {
|
||||||
|
vi.spyOn(global, 'fetch').mockResolvedValueOnce(
|
||||||
|
new Response(JSON.stringify({ accountUuid: 'acct-1', preferredStrategy: 'ewma' }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(fetchAccountPolicy()).resolves.toEqual({
|
||||||
|
accountUuid: 'acct-1',
|
||||||
|
preferredStrategy: 'ewma',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
type AccountUsageError = Error & {
|
||||||
|
status?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AccountUsageSummary = {
|
||||||
|
accountUuid: string
|
||||||
|
totalBytes: number
|
||||||
|
uplinkBytes?: number
|
||||||
|
downlinkBytes?: number
|
||||||
|
currentBalance?: number
|
||||||
|
remainingIncludedQuota?: number
|
||||||
|
syncDelaySeconds?: number
|
||||||
|
suspendState?: string
|
||||||
|
throttleState?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AccountPolicy = {
|
||||||
|
accountUuid: string
|
||||||
|
preferredStrategy: string
|
||||||
|
eligibleNodeGroups?: string[]
|
||||||
|
authState?: string
|
||||||
|
degradeMode?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function toError(payload: unknown, status: number): AccountUsageError {
|
||||||
|
const message =
|
||||||
|
payload && typeof payload === 'object' && 'message' in payload && typeof payload.message === 'string'
|
||||||
|
? payload.message
|
||||||
|
: payload && typeof payload === 'object' && 'error' in payload && typeof payload.error === 'string'
|
||||||
|
? payload.error
|
||||||
|
: `Request failed (${status})`
|
||||||
|
const error = new Error(message) as AccountUsageError
|
||||||
|
error.status = status
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestJSON<T>(url: string): Promise<T> {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
credentials: 'include',
|
||||||
|
cache: 'no-store',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const payload = await response.json().catch(() => null)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw toError(payload, response.status)
|
||||||
|
}
|
||||||
|
return payload as T
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchAccountUsageSummary(): Promise<AccountUsageSummary> {
|
||||||
|
return requestJSON<AccountUsageSummary>('/api/account/usage/summary')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchAccountPolicy(): Promise<AccountPolicy> {
|
||||||
|
return requestJSON<AccountPolicy>('/api/account/policy')
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user