Files
Aether/_deprecated_py_src/api/oauth/admin.py
fawney19 1d9c77522a refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/
- 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层
- 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构
- 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations)
- 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image
- 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
2026-04-03 16:26:16 +08:00

296 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""OAuth 管理端点(管理员)。"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field, ValidationError
from sqlalchemy.orm import Session
from src.api.base.admin_adapter import AdminApiAdapter
from src.api.base.context import ApiRequestContext
from src.api.base.pipeline import get_pipeline
from src.core.exceptions import InvalidRequestException, translate_pydantic_error
from src.database import get_db
from src.models.database import OAuthProvider
from src.services.auth.oauth.registry import get_oauth_provider_registry
from src.services.auth.oauth.service import OAuthService
router = APIRouter(prefix="/api/admin/oauth", tags=["Admin - OAuth"])
pipeline = get_pipeline()
_OAUTH_ADMIN_LEGACY_DETAIL = "OAuth admin routes are retired; use Rust maintenance backend"
def _raise_oauth_admin_legacy_unavailable() -> None:
raise HTTPException(status_code=503, detail=_OAUTH_ADMIN_LEGACY_DETAIL)
class SupportedOAuthType(BaseModel):
provider_type: str
display_name: str
default_authorization_url: str
default_token_url: str
default_userinfo_url: str
default_scopes: list[str]
class OAuthProviderUpsertRequest(BaseModel):
display_name: str = Field(..., min_length=1, max_length=100)
client_id: str = Field(..., min_length=1, max_length=255)
client_secret: str | None = Field(None, max_length=2048)
authorization_url_override: str | None = Field(None, max_length=500)
token_url_override: str | None = Field(None, max_length=500)
userinfo_url_override: str | None = Field(None, max_length=500)
scopes: list[str] | None = None
redirect_uri: str = Field(..., min_length=1, max_length=500)
frontend_callback_url: str = Field(..., min_length=1, max_length=500)
attribute_mapping: dict[str, Any] | None = None
extra_config: dict[str, Any] | None = None
is_enabled: bool = False
force: bool = False
class OAuthProviderAdminResponse(BaseModel):
provider_type: str
display_name: str
client_id: str
has_secret: bool
authorization_url_override: str | None = None
token_url_override: str | None = None
userinfo_url_override: str | None = None
scopes: list[str] | None = None
redirect_uri: str
frontend_callback_url: str
attribute_mapping: dict[str, Any] | None = None
extra_config: dict[str, Any] | None = None
is_enabled: bool
class OAuthProviderTestResponse(BaseModel):
authorization_url_reachable: bool
token_url_reachable: bool
secret_status: str
details: str = ""
class OAuthProviderTestRequest(BaseModel):
"""测试请求,使用表单数据而非数据库配置"""
client_id: str = Field(..., min_length=1)
client_secret: str | None = None
authorization_url_override: str | None = None
token_url_override: str | None = None
redirect_uri: str = Field(..., min_length=1)
@router.get("/supported-types", response_model=list[SupportedOAuthType])
async def get_supported_types(request: Request, db: Session = Depends(get_db)) -> Any:
_ = request, db
_raise_oauth_admin_legacy_unavailable()
adapter = GetSupportedTypesAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.get("/providers", response_model=list[OAuthProviderAdminResponse])
async def list_provider_configs(request: Request, db: Session = Depends(get_db)) -> Any:
_ = request, db
_raise_oauth_admin_legacy_unavailable()
adapter = ListOAuthProviderConfigsAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.get("/providers/{provider_type}", response_model=OAuthProviderAdminResponse)
async def get_provider_config(
provider_type: str, request: Request, db: Session = Depends(get_db)
) -> Any:
_ = provider_type, request, db
_raise_oauth_admin_legacy_unavailable()
adapter = GetOAuthProviderConfigAdapter(provider_type=provider_type)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.put("/providers/{provider_type}", response_model=OAuthProviderAdminResponse)
async def upsert_provider_config(
provider_type: str, request: Request, db: Session = Depends(get_db)
) -> Any:
_ = provider_type, request, db
_raise_oauth_admin_legacy_unavailable()
adapter = UpsertOAuthProviderConfigAdapter(provider_type=provider_type)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.delete("/providers/{provider_type}")
async def delete_provider_config(
provider_type: str, request: Request, db: Session = Depends(get_db)
) -> Any:
_ = provider_type, request, db
_raise_oauth_admin_legacy_unavailable()
adapter = DeleteOAuthProviderConfigAdapter(provider_type=provider_type)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.post("/providers/{provider_type}/test", response_model=OAuthProviderTestResponse)
async def test_provider_config(
provider_type: str, request: Request, db: Session = Depends(get_db)
) -> Any:
_ = provider_type, request, db
_raise_oauth_admin_legacy_unavailable()
adapter = TestOAuthProviderConfigAdapter(provider_type=provider_type)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
class GetSupportedTypesAdapter(AdminApiAdapter):
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
registry = get_oauth_provider_registry()
types = registry.get_supported_types()
return [
SupportedOAuthType(
provider_type=t.provider_type,
display_name=t.display_name,
default_authorization_url=t.default_authorization_url,
default_token_url=t.default_token_url,
default_userinfo_url=t.default_userinfo_url,
default_scopes=list(t.default_scopes),
).model_dump()
for t in types
]
class ListOAuthProviderConfigsAdapter(AdminApiAdapter):
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
rows = context.db.query(OAuthProvider).order_by(OAuthProvider.provider_type.asc()).all()
return [
OAuthProviderAdminResponse(
provider_type=str(row.provider_type or ""),
display_name=str(row.display_name or ""),
client_id=str(row.client_id or ""),
has_secret=bool(row.client_secret_encrypted),
authorization_url_override=row.authorization_url_override,
token_url_override=row.token_url_override,
userinfo_url_override=row.userinfo_url_override,
scopes=row.scopes,
redirect_uri=str(row.redirect_uri or ""),
frontend_callback_url=str(row.frontend_callback_url or ""),
attribute_mapping=row.attribute_mapping,
extra_config=row.extra_config,
is_enabled=bool(row.is_enabled),
).model_dump()
for row in rows
]
class GetOAuthProviderConfigAdapter(AdminApiAdapter):
def __init__(self, provider_type: str):
self.provider_type = provider_type
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
row = (
context.db.query(OAuthProvider)
.filter(OAuthProvider.provider_type == self.provider_type)
.first()
)
if not row:
raise InvalidRequestException("Provider 配置不存在")
return OAuthProviderAdminResponse(
provider_type=str(row.provider_type or ""),
display_name=str(row.display_name or ""),
client_id=str(row.client_id or ""),
has_secret=bool(row.client_secret_encrypted),
authorization_url_override=row.authorization_url_override,
token_url_override=row.token_url_override,
userinfo_url_override=row.userinfo_url_override,
scopes=row.scopes,
redirect_uri=str(row.redirect_uri or ""),
frontend_callback_url=str(row.frontend_callback_url or ""),
attribute_mapping=row.attribute_mapping,
extra_config=row.extra_config,
is_enabled=bool(row.is_enabled),
).model_dump()
class UpsertOAuthProviderConfigAdapter(AdminApiAdapter):
def __init__(self, provider_type: str):
self.provider_type = provider_type
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
payload = context.ensure_json_body()
try:
req = OAuthProviderUpsertRequest.model_validate(payload)
except ValidationError as exc:
errors = exc.errors()
if errors:
raise InvalidRequestException(translate_pydantic_error(errors[0]))
raise InvalidRequestException("请求数据验证失败")
row = await OAuthService.upsert_provider_config(
db=context.db,
provider_type=self.provider_type,
data=req,
)
return OAuthProviderAdminResponse(
provider_type=str(row.provider_type or ""),
display_name=str(row.display_name or ""),
client_id=str(row.client_id or ""),
has_secret=bool(row.client_secret_encrypted),
authorization_url_override=row.authorization_url_override,
token_url_override=row.token_url_override,
userinfo_url_override=row.userinfo_url_override,
scopes=row.scopes,
redirect_uri=str(row.redirect_uri or ""),
frontend_callback_url=str(row.frontend_callback_url or ""),
attribute_mapping=row.attribute_mapping,
extra_config=row.extra_config,
is_enabled=bool(row.is_enabled),
).model_dump()
class DeleteOAuthProviderConfigAdapter(AdminApiAdapter):
def __init__(self, provider_type: str):
self.provider_type = provider_type
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
await OAuthService.delete_provider_config(context.db, self.provider_type)
return {"message": "删除成功"}
class TestOAuthProviderConfigAdapter(AdminApiAdapter):
def __init__(self, provider_type: str):
self.provider_type = provider_type
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
payload = context.ensure_json_body()
try:
req = OAuthProviderTestRequest.model_validate(payload)
except ValidationError as exc:
errors = exc.errors()
if errors:
raise InvalidRequestException(translate_pydantic_error(errors[0]))
raise InvalidRequestException("请求数据验证失败")
# 如果没有提供 client_secret尝试从数据库获取已保存的
client_secret = req.client_secret
if not client_secret:
existing = (
context.db.query(OAuthProvider)
.filter(OAuthProvider.provider_type == self.provider_type)
.first()
)
if existing and existing.client_secret_encrypted:
client_secret = existing.get_client_secret()
result = await OAuthService.test_provider_config_with_data(
provider_type=self.provider_type,
client_id=req.client_id,
client_secret=client_secret,
authorization_url_override=req.authorization_url_override,
token_url_override=req.token_url_override,
redirect_uri=req.redirect_uri,
)
return OAuthProviderTestResponse(**result).model_dump()