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())