mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
feat(oauth): 新增 Codex account_user_id 和 organizations 字段采集、展示与判重
- 从 Codex id_token claims 和 token_response 中提取 account_user_id 和 organizations - OAuth 判重逻辑改为优先按 account_user_id 匹配,支持同用户不同 Team 不误判 - 号池和 Provider 详情页展示组织标签、account ID 和 account_user_id - 前端重复的 OAuth identity 工具函数提取到 utils/oauthIdentity.ts - 后端重复的 normalize_oauth_organizations 提取到 core/provider_oauth_utils.py
This commit is contained in:
@@ -18,8 +18,10 @@ def test_parse_codex_id_token_extracts_auth_claim_fields() -> None:
|
||||
"email": "u@example.com",
|
||||
"https://api.openai.com/auth": {
|
||||
"chatgpt_account_id": "acc-1",
|
||||
"chatgpt_account_user_id": "user-1__acc-1",
|
||||
"chatgpt_plan_type": "team",
|
||||
"chatgpt_user_id": "user-1",
|
||||
"organizations": [{"id": "org-1", "title": "Personal", "is_default": True}],
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -29,8 +31,10 @@ def test_parse_codex_id_token_extracts_auth_claim_fields() -> None:
|
||||
assert parsed == {
|
||||
"email": "u@example.com",
|
||||
"account_id": "acc-1",
|
||||
"account_user_id": "user-1__acc-1",
|
||||
"plan_type": "team",
|
||||
"user_id": "user-1",
|
||||
"organizations": [{"id": "org-1", "title": "Personal", "is_default": True}],
|
||||
}
|
||||
|
||||
|
||||
|
||||
55
tests/unit/test_provider_key_response_builder.py
Normal file
55
tests/unit/test_provider_key_response_builder.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from src.models.database import ProviderAPIKey
|
||||
from src.services.provider_keys import response_builder as module
|
||||
from src.services.provider_keys.response_builder import build_key_response
|
||||
|
||||
|
||||
def test_build_key_response_includes_codex_identity_metadata(
|
||||
monkeypatch: "pytest.MonkeyPatch",
|
||||
) -> None:
|
||||
key = ProviderAPIKey(
|
||||
id="key-1",
|
||||
provider_id="provider-1",
|
||||
api_formats=["openai:chat"],
|
||||
auth_type="oauth",
|
||||
api_key="enc-access-token",
|
||||
auth_config='{"email":"u@example.com","plan_type":"team","account_id":"acc-1","account_user_id":"user-1__acc-1","organizations":[{"id":"org-1","title":"Personal","is_default":true,"role":"owner"}],"expires_at":123456}',
|
||||
name="codex-user",
|
||||
)
|
||||
now = datetime.now(timezone.utc)
|
||||
key.success_count = 0
|
||||
key.request_count = 0
|
||||
key.error_count = 0
|
||||
key.total_response_time_ms = 0
|
||||
key.rpm_limit = None
|
||||
key.global_priority_by_format = None
|
||||
key.allowed_models = None
|
||||
key.capabilities = None
|
||||
key.is_active = True
|
||||
key.created_at = now
|
||||
key.updated_at = now
|
||||
key.cache_ttl_minutes = 5
|
||||
key.max_probe_interval_minutes = 32
|
||||
key.health_by_format = None
|
||||
key.circuit_breaker_by_format = None
|
||||
key.oauth_invalid_at = None
|
||||
key.oauth_invalid_reason = None
|
||||
key.note = None
|
||||
key.last_used_at = None
|
||||
|
||||
monkeypatch.setattr(module.crypto_service, "decrypt", lambda value: value)
|
||||
|
||||
result = build_key_response(key)
|
||||
|
||||
assert result.oauth_email == "u@example.com"
|
||||
assert result.oauth_plan_type == "team"
|
||||
assert result.oauth_account_id == "acc-1"
|
||||
assert result.oauth_account_user_id == "user-1__acc-1"
|
||||
assert len(result.oauth_organizations) == 1
|
||||
assert result.oauth_organizations[0].title == "Personal"
|
||||
assert result.oauth_organizations[0].is_default is True
|
||||
@@ -1,17 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from src.api.admin import provider_oauth as module
|
||||
from src.core.exceptions import InvalidRequestException
|
||||
|
||||
|
||||
def test_parse_standard_oauth_import_entries_keeps_codex_hints() -> None:
|
||||
entries = module._parse_standard_oauth_import_entries(
|
||||
'[{"refresh_token":"rt_1","accountId":"acc-1","planType":"TEAM","userId":"u-1","email":"u@example.com"}]'
|
||||
'[{"refresh_token":"rt_1","accountId":"acc-1","chatgptAccountUserId":"u-1__acc-1","planType":"TEAM","userId":"u-1","email":"u@example.com"}]'
|
||||
)
|
||||
|
||||
assert entries == [
|
||||
{
|
||||
"refresh_token": "rt_1",
|
||||
"account_id": "acc-1",
|
||||
"account_user_id": "u-1__acc-1",
|
||||
"plan_type": "team",
|
||||
"user_id": "u-1",
|
||||
"email": "u@example.com",
|
||||
@@ -32,6 +39,7 @@ def test_apply_codex_import_hints_only_fills_missing_fields() -> None:
|
||||
module._apply_codex_import_hints(
|
||||
auth_config,
|
||||
{
|
||||
"account_user_id": "u-1__acc-1",
|
||||
"account_id": "acc-1",
|
||||
"plan_type": "plus",
|
||||
"user_id": "user-1",
|
||||
@@ -40,6 +48,106 @@ def test_apply_codex_import_hints_only_fills_missing_fields() -> None:
|
||||
)
|
||||
|
||||
assert auth_config["account_id"] == "existing-account"
|
||||
assert auth_config["account_user_id"] == "u-1__acc-1"
|
||||
assert auth_config["plan_type"] == "plus"
|
||||
assert auth_config["user_id"] == "user-1"
|
||||
assert auth_config["email"] == "u@example.com"
|
||||
|
||||
|
||||
class _DummyQuery:
|
||||
def __init__(self, keys: list[SimpleNamespace]) -> None:
|
||||
self._keys = keys
|
||||
|
||||
def filter(self, *_args: object, **_kwargs: object) -> "_DummyQuery":
|
||||
return self
|
||||
|
||||
def all(self) -> list[SimpleNamespace]:
|
||||
return self._keys
|
||||
|
||||
|
||||
class _DummyDB:
|
||||
def __init__(self, keys: list[SimpleNamespace]) -> None:
|
||||
self._keys = keys
|
||||
|
||||
def query(self, _model: object) -> _DummyQuery:
|
||||
return _DummyQuery(self._keys)
|
||||
|
||||
|
||||
def _make_oauth_key(*, key_id: str, name: str, auth_config: dict[str, object]) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
id=key_id,
|
||||
name=name,
|
||||
provider_id="provider-1",
|
||||
auth_type="oauth",
|
||||
auth_config=json.dumps(auth_config),
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
|
||||
def test_check_duplicate_oauth_account_codex_allows_same_user_different_account_id(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(module.crypto_service, "decrypt", lambda value, silent=True: value)
|
||||
|
||||
existing_key = _make_oauth_key(
|
||||
key_id="key-1",
|
||||
name="existing",
|
||||
auth_config={
|
||||
"provider_type": "codex",
|
||||
"email": "u@example.com",
|
||||
"user_id": "user-1",
|
||||
"account_id": "acc-1",
|
||||
"account_user_id": "user-1__acc-1",
|
||||
"plan_type": "team",
|
||||
},
|
||||
)
|
||||
db = _DummyDB([existing_key])
|
||||
|
||||
result = module._check_duplicate_oauth_account(
|
||||
db, # type: ignore[arg-type]
|
||||
"provider-1",
|
||||
{
|
||||
"provider_type": "codex",
|
||||
"email": "u@example.com",
|
||||
"user_id": "user-1",
|
||||
"account_id": "acc-2",
|
||||
"account_user_id": "user-1__acc-2",
|
||||
"plan_type": "team",
|
||||
},
|
||||
)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_check_duplicate_oauth_account_codex_rejects_same_account_user_identity(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(module.crypto_service, "decrypt", lambda value, silent=True: value)
|
||||
|
||||
existing_key = _make_oauth_key(
|
||||
key_id="key-1",
|
||||
name="existing",
|
||||
auth_config={
|
||||
"provider_type": "codex",
|
||||
"email": "u@example.com",
|
||||
"user_id": "user-1",
|
||||
"account_id": "acc-1",
|
||||
"account_user_id": "user-1__acc-1",
|
||||
"plan_type": "team",
|
||||
},
|
||||
)
|
||||
db = _DummyDB([existing_key])
|
||||
|
||||
with pytest.raises(InvalidRequestException, match="已存在"):
|
||||
module._check_duplicate_oauth_account(
|
||||
db, # type: ignore[arg-type]
|
||||
"provider-1",
|
||||
{
|
||||
"provider_type": "codex",
|
||||
"email": "u@example.com",
|
||||
"user_id": "user-1",
|
||||
"account_id": "acc-1",
|
||||
"account_user_id": "user-1__acc-1",
|
||||
"plan_type": "team",
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user