From 761a2d984f699bca2d7250af53a58e7f26dd0d8f Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Wed, 29 Jul 2026 20:37:16 -0700 Subject: [PATCH] fix(market): return 404 JSON for unknown API routes; document GitHub OAuth setup Unmatched /miniapp/api/v1/* paths used to fall through to the SPA's index.html (200 + HTML) because axum flattens nested routers and the outer catch-all won. Register an explicit wildcard inside the API router so unknown routes get the versioned error envelope. Also add an operations subsection to the deploy runbook covering GitHub OAuth App setup: no management API exists, secrets display only once, reuse-and-rotate guidance, read-only verification via health and /auth/github/start, and the SPA-fallback debugging pitfall. --- deploy/miniapp-market/README.md | 31 +++++++++++++++++++ .../miniapp-market-service/src/lib.rs | 18 +++++++++++ .../miniapp-market-service/src/routes.rs | 10 +++++- 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/deploy/miniapp-market/README.md b/deploy/miniapp-market/README.md index f035280ba3..a4f6371cdf 100644 --- a/deploy/miniapp-market/README.md +++ b/deploy/miniapp-market/README.md @@ -374,10 +374,41 @@ docker compose -f deploy/miniapp-market/docker-compose.yml \ up -d --no-build --force-recreate miniapp-market" ``` +### GitHub OAuth 配置 + GitHub OAuth App callback 必须精确为: `https://market.openbitfun.com/miniapp/api/v1/auth/github/callback` +创建或复用 OAuth App 时的实操要点: + +- OAuth App 在 GitHub → Settings → Developer settings → OAuth Apps 下创建, + 没有对应的管理 API,只能人工在网页操作。创建表单支持 URL 预填: + `https://github.com/settings/applications/new?oauth_application[name]=...&oauth_application[url]=...&oauth_application[callback_url]=...` +- Client Secret 只在生成那一刻显示一次,之后无法再查看。已有 App 拿不回旧 + secret 时,直接在原 App 上 "Generate a new client secret",不需要新建 App。 +- 凭据按上文流程用受控编辑器写入 `market.env`,然后 recreate 容器。不要把 + secret 以命令行参数形式传给脚本——它会进入 shell history 和进程列表。 + +配置生效的只读验证: + +```bash +curl -fsS https://market.openbitfun.com/miniapp/api/v1/health +# 预期包含 "githubAuthConfigured":true + +curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" \ + https://market.openbitfun.com/miniapp/api/v1/auth/github/start +# 预期 307,跳转 github.com/login/oauth/authorize, +# 且 client_id、redirect_uri 与注册的 App 一致 +``` + +排错提示:浏览器登录入口是 `/auth/github/start`,不存在 `/auth/github/login` +这类路径。旧版本中未匹配的 `/miniapp/api/v1/*` 路径会落到 SPA 返回 +200 + HTML,容易误判成"接口存在但行为异常";新版本已改为返回标准 +404 JSON 错误信封。 + +### 初次开放市场 + 初次开放市场时保持 `MARKET_PUBLIC_BROWSE=false`,先由管理员 GitHub ID `24753352` 登录、上传并批准样例,再用全新桌面客户端验证安装和手动更新。 全部通过后才可改为 `true` 并 recreate。审批完成不代表用户自动授予 MiniApp diff --git a/src/crates/services/miniapp-market-service/src/lib.rs b/src/crates/services/miniapp-market-service/src/lib.rs index 7769db32f9..7321e6eb8f 100644 --- a/src/crates/services/miniapp-market-service/src/lib.rs +++ b/src/crates/services/miniapp-market-service/src/lib.rs @@ -158,6 +158,24 @@ mod tests { .unwrap(); assert_eq!(response.status(), StatusCode::OK, "{uri}"); } + + // Unknown API paths must not fall through to the SPA's index.html. + let response = app + .clone() + .oneshot( + Request::builder() + .uri("/miniapp/api/v1/auth/github/login") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::NOT_FOUND); + let body = axum::body::to_bytes(response.into_body(), 1024) + .await + .unwrap(); + let body: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert_eq!(body["error"]["code"], "not_found"); } #[tokio::test] diff --git a/src/crates/services/miniapp-market-service/src/routes.rs b/src/crates/services/miniapp-market-service/src/routes.rs index ae52c32f1c..9addcf14e2 100644 --- a/src/crates/services/miniapp-market-service/src/routes.rs +++ b/src/crates/services/miniapp-market-service/src/routes.rs @@ -10,7 +10,7 @@ use axum::body::{Body, Bytes}; use axum::extract::{DefaultBodyLimit, Path, Query, State}; use axum::http::{header, HeaderMap, HeaderValue, StatusCode}; use axum::response::{IntoResponse, Redirect, Response}; -use axum::routing::{get, post, put}; +use axum::routing::{any, get, post, put}; use axum::{Json, Router}; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; @@ -195,10 +195,18 @@ pub(crate) fn api_router(state: Arc) -> Router { "/admin/listings/{listing_id}/unpublish", post(unpublish_listing), ) + // Unmatched API paths must return the versioned JSON error envelope. + // A nested fallback would lose to the outer SPA catch-all, so this has + // to be an explicit wildcard route that outranks `/miniapp/{*rest}`. + .route("/{*rest}", any(api_not_found)) .layer(DefaultBodyLimit::max(21 * 1024 * 1024)) .with_state(state) } +async fn api_not_found() -> MarketError { + MarketError::not_found("Unknown API route.") +} + async fn health(State(state): State>) -> impl IntoResponse { let database_ready = sqlx::query_scalar::<_, i64>("SELECT 1") .fetch_one(state.db.pool())