From db517333836197822b601419df0fa4a6b3c4f3b4 Mon Sep 17 00:00:00 2001 From: mangmang <362165265@qq.com> Date: Wed, 22 Jul 2026 00:49:32 +0800 Subject: [PATCH] fix: clarify New-API authentication failures --- backend/app/services/upstream_client.py | 20 +++++++++++++ backend/test_upstream_auth_headers.py | 16 +++++++++- backend/test_upstream_token_refresh.py | 40 ++++++++++++++++++++++++- frontend/src/views/Upstreams.vue | 17 +++++++++++ 4 files changed, 91 insertions(+), 2 deletions(-) diff --git a/backend/app/services/upstream_client.py b/backend/app/services/upstream_client.py index b5d4464..0a1ffcf 100644 --- a/backend/app/services/upstream_client.py +++ b/backend/app/services/upstream_client.py @@ -176,6 +176,12 @@ def _clean_auth_header_value(value: Any, field_name: str) -> str: return text +def _is_dashboard_jwt(value: Any) -> bool: + """识别 New-API 面板登录 JWT,避免将其当作长期 API Token 使用。""" + token = str(value or "").strip() + return len(token) > 512 and token.count(".") == 2 + + def _find_user_id(value: Any) -> str: if isinstance(value, dict): for key in ("id", "user_id", "userId"): @@ -537,6 +543,11 @@ class UpstreamClient: headers["Nox-Api-User"] = user_id elif self.auth_type == "new_api_token": token = _clean_auth_header_value(self.auth_config.get("token", ""), "New-API access token") + if _is_dashboard_jwt(token): + raise UpstreamError( + "New-API token 是面板登录 JWT,不是用户 API Token;" + "请在上游 Token 页面重新生成 Access Token" + ) user_id = self._user_header_value() if token: headers["Authorization"] = f"Bearer {token}" @@ -1095,6 +1106,15 @@ class UpstreamClient: if not email or not password: raise UpstreamError("login_password auth requires email and password in auth_config") resp = self._request("POST", login_path, {username_field: email, "password": password}, auth=False) + data = _unwrap_data(resp) + if isinstance(data, dict) and data.get("require_2fa"): + raise UpstreamError( + "New-API 账号启用了二次验证,SmartUp 暂不支持密码登录 2FA;" + "请改用 Access Token 或 Cookie" + ) + if not _is_success_response(resp): + message = _response_message(resp, "New-API 登录失败") + raise UpstreamError(message) token = _find_token(resp) if token: self._token = token diff --git a/backend/test_upstream_auth_headers.py b/backend/test_upstream_auth_headers.py index c6514b0..056e71c 100644 --- a/backend/test_upstream_auth_headers.py +++ b/backend/test_upstream_auth_headers.py @@ -1,4 +1,6 @@ -from app.services.upstream_client import UpstreamClient +import pytest + +from app.services.upstream_client import UpstreamClient, UpstreamError def test_new_api_token_headers_include_authorization_and_new_api_user(): @@ -16,6 +18,18 @@ def test_new_api_token_headers_include_authorization_and_new_api_user(): assert "Nox-Api-User" not in headers +def test_new_api_token_rejects_dashboard_jwt(): + client = UpstreamClient( + base_url="http://newapi.local", + api_prefix="", + auth_type="new_api_token", + auth_config={"token": "a" * 1200 + ".payload.signature", "user_id": "7"}, + ) + + with pytest.raises(UpstreamError, match="面板登录 JWT"): + client._headers() + + def test_nox_token_headers_include_authorization_and_nox_api_user(): client = UpstreamClient( base_url="http://nox.local", diff --git a/backend/test_upstream_token_refresh.py b/backend/test_upstream_token_refresh.py index 79b2210..0254eb0 100644 --- a/backend/test_upstream_token_refresh.py +++ b/backend/test_upstream_token_refresh.py @@ -8,7 +8,7 @@ import httpx import pytest from app.services import upstream_client -from app.services.upstream_client import UpstreamClient +from app.services.upstream_client import UpstreamClient, UpstreamError class FakeHttpClient: @@ -412,3 +412,41 @@ def test_login_password_empty_prefix_defaults_to_user_api_login(monkeypatch): assert headers["Authorization"] == "Bearer nox-access" assert headers["Nox-Api-User"] == "9" assert headers["New-Api-User"] == "9" + + +def test_login_password_surfaces_business_error_response(monkeypatch): + def handler(request: httpx.Request, kwargs): + return _response(request, 200, { + "success": False, + "message": "用户名或密码错误", + }) + + _install_fake_client(monkeypatch, handler) + client = UpstreamClient( + base_url="http://newapi.local", + api_prefix="", + auth_type="login_password", + auth_config={"email": "admin", "password": "wrong"}, + ) + + with pytest.raises(UpstreamError, match="用户名或密码错误"): + client.login() + + +def test_login_password_reports_unsupported_two_factor(monkeypatch): + def handler(request: httpx.Request, kwargs): + return _response(request, 200, { + "success": True, + "data": {"require_2fa": True, "flow_token": "flow"}, + }) + + _install_fake_client(monkeypatch, handler) + client = UpstreamClient( + base_url="http://newapi.local", + api_prefix="", + auth_type="login_password", + auth_config={"email": "admin", "password": "secret"}, + ) + + with pytest.raises(UpstreamError, match="二次验证"): + client.login() diff --git a/frontend/src/views/Upstreams.vue b/frontend/src/views/Upstreams.vue index 92af907..59cbb18 100644 --- a/frontend/src/views/Upstreams.vue +++ b/frontend/src/views/Upstreams.vue @@ -698,6 +698,17 @@ function openAuthCapture() { authCaptureVisible.value = true } +function isDashboardJwt(value: string): boolean { + const token = value.trim() + return token.length > 512 && token.split('.').length === 3 +} + +function rejectNewApiDashboardJwt(value: string): boolean { + if (quickPlatform.value !== 'new-api-user' || !isDashboardJwt(value)) return false + ElMessage.warning('提取到的是 New-API 面板登录 JWT,不是长期 Access Token;请在上游 Token 页面生成 API Token') + return true +} + function handleAuthCaptureSelect(candidate: { type: string value: string @@ -720,6 +731,7 @@ function handleAuthCaptureSelect(candidate: { } ElMessage.success('已填入 Nox Access Token') } else if (quickPlatform.value === 'new-api-user') { + if (rejectNewApiDashboardJwt(candidate.value)) return form.value.auth_type = 'new_api_token' form.value.auth_config.token = candidate.value form.value.auth_config.provider = 'new-api' @@ -793,6 +805,7 @@ function handleAuthCaptureSelect(candidate: { ElMessage.success('已填入 API Key') } else if (candidate.type === 'credential') { // Try to guess — if value starts with 'sk-', treat as bearer + if (rejectNewApiDashboardJwt(candidate.value)) return if (candidate.value.startsWith('sk-')) { form.value.auth_type = quickPlatform.value === 'nox-api' ? 'nox_token' : quickPlatform.value === 'new-api-user' ? 'new_api_token' : 'bearer' form.value.auth_config.token = candidate.value @@ -1164,6 +1177,10 @@ function openEdit(row: UpstreamData) { async function handleSave() { const valid = await formRef.value?.validate().catch(() => false) if (!valid) return + if (form.value.auth_type === 'new_api_token' && isDashboardJwt(String(form.value.auth_config.token || ''))) { + ElMessage.warning('New-API 面板登录 JWT 不能作为长期 Access Token;请在上游 Token 页面生成 API Token') + return + } saving.value = true try { const payload = {