mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-30 20:05:42 +08:00
严格检查全部宿主源码,清零类型错误预算并移除按模块类型豁免。 按原因、所属方法、错误码及语法指纹锁定必要兼容例外和站点类型桩, CI 阻断新增忽略、检查范围退化、本地桩替代真实接口及新的类型错误。 修复空值、异步和供应方类型边界,保持插件与 API 兼容合同, 并补充存储、订阅、通知、Agent 和工作流边界的回归覆盖。
307 lines
10 KiB
Python
307 lines
10 KiB
Python
"""
|
||
GitHub Token 授权接口及首次初始化专用授权接口。
|
||
"""
|
||
|
||
from dataclasses import asdict
|
||
from typing import Literal, cast
|
||
|
||
from fastapi import Depends, HTTPException
|
||
|
||
from app.api.dependencies.auth import get_current_active_superuser_async, get_user_service
|
||
from app.api.dependencies.github import get_community_medal_service, get_github_auth_service
|
||
from app.api.principal import ApiPrincipal
|
||
from app.api.response import ResponseAPIRouter
|
||
from app.application.community.medals import CommunityMedalService
|
||
from app.application.github_auth import (
|
||
GithubAuthError,
|
||
GithubAuthService,
|
||
GithubAuthStatus,
|
||
)
|
||
from app.application.security.user import UserService
|
||
from app.schemas.github import (
|
||
GithubCommunityProfile,
|
||
GithubDeviceAuthPoll,
|
||
GithubDeviceAuthPollRequest,
|
||
GithubDeviceAuthStart,
|
||
GithubManualTokenRequest,
|
||
GithubTokenStatus,
|
||
)
|
||
from app.schemas.response import Response as _SchemaResponse
|
||
|
||
router = ResponseAPIRouter()
|
||
initialization_router = ResponseAPIRouter()
|
||
|
||
|
||
@router.get(
|
||
"/community",
|
||
summary="查询当前 GitHub 用户的社区勋章",
|
||
response_model=_SchemaResponse[GithubCommunityProfile],
|
||
)
|
||
async def get_community_profile(
|
||
refresh: bool = False,
|
||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||
community: CommunityMedalService = Depends(get_community_medal_service),
|
||
) -> _SchemaResponse[GithubCommunityProfile]:
|
||
"""
|
||
仅向管理此实例凭据的超级管理员展示其 GitHub 身份的勋章,不继承给普通用户。
|
||
"""
|
||
profile = await community.profile(refresh=refresh)
|
||
return _SchemaResponse(success=True, data=GithubCommunityProfile.model_validate(asdict(profile)))
|
||
|
||
|
||
def _status_model(status: GithubAuthStatus) -> GithubTokenStatus:
|
||
"""
|
||
将应用层状态投影为不含 Token 原文的 API 模型。
|
||
"""
|
||
return GithubTokenStatus(
|
||
configured=status.configured,
|
||
valid=status.valid,
|
||
source=cast(Literal["oauth", "manual"] | None, status.source),
|
||
login=status.login,
|
||
masked_token=status.masked_token,
|
||
expires_at=status.expires_at,
|
||
needs_reauthorization=status.needs_reauthorization,
|
||
)
|
||
|
||
|
||
def _raise_auth_error(error: GithubAuthError) -> None:
|
||
"""
|
||
把授权应用错误转换为稳定的客户端错误,不返回外部响应原文。
|
||
"""
|
||
raise HTTPException(status_code=400, detail=str(error)) from error
|
||
|
||
|
||
async def _ensure_initialization_open(service: UserService) -> None:
|
||
"""
|
||
限制未认证授权接口只能在首次初始化窗口内使用。
|
||
"""
|
||
if await service.is_initialized():
|
||
raise HTTPException(status_code=409, detail="系统已经完成初始化")
|
||
|
||
|
||
@router.get(
|
||
"/auth/status",
|
||
summary="查询 GitHub Token 状态",
|
||
response_model=_SchemaResponse[GithubTokenStatus],
|
||
)
|
||
async def get_auth_status(
|
||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||
github_auth: GithubAuthService = Depends(get_github_auth_service),
|
||
) -> _SchemaResponse[GithubTokenStatus]:
|
||
"""
|
||
返回当前管理员可见的 GitHub Token 脱敏状态。
|
||
"""
|
||
return _SchemaResponse(success=True, data=_status_model(await github_auth.status()))
|
||
|
||
|
||
@router.post(
|
||
"/auth/start",
|
||
summary="启动 GitHub 设备授权",
|
||
response_model=_SchemaResponse[GithubDeviceAuthStart],
|
||
)
|
||
async def start_auth(
|
||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||
github_auth: GithubAuthService = Depends(get_github_auth_service),
|
||
) -> _SchemaResponse[GithubDeviceAuthStart]:
|
||
"""
|
||
为已登录管理员申请 GitHub Device Flow 设备码。
|
||
"""
|
||
try:
|
||
result = await github_auth.start_device_auth()
|
||
except GithubAuthError as error:
|
||
_raise_auth_error(error)
|
||
return _SchemaResponse(
|
||
success=True,
|
||
data=GithubDeviceAuthStart(
|
||
session_id=result.session_id,
|
||
verification_uri=result.verification_uri,
|
||
user_code=result.user_code,
|
||
expires_in=result.expires_in,
|
||
interval_seconds=result.interval_seconds,
|
||
),
|
||
)
|
||
|
||
|
||
@router.post(
|
||
"/auth/poll",
|
||
summary="轮询 GitHub 设备授权",
|
||
response_model=_SchemaResponse[GithubDeviceAuthPoll],
|
||
)
|
||
async def poll_auth(
|
||
payload: GithubDeviceAuthPollRequest,
|
||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||
github_auth: GithubAuthService = Depends(get_github_auth_service),
|
||
) -> _SchemaResponse[GithubDeviceAuthPoll]:
|
||
"""
|
||
轮询管理员发起的 GitHub Device Flow。
|
||
"""
|
||
try:
|
||
result = await github_auth.poll_device_auth(payload.session_id)
|
||
except GithubAuthError as error:
|
||
_raise_auth_error(error)
|
||
status = _status_model(result.status) if result.status else None
|
||
return _SchemaResponse(
|
||
success=True,
|
||
data=GithubDeviceAuthPoll(
|
||
state=cast(Literal["pending", "authorized", "slow_down", "denied", "expired", "failed"], result.state),
|
||
message=result.message,
|
||
retry_after=result.retry_after,
|
||
status=status,
|
||
),
|
||
)
|
||
|
||
|
||
@router.post(
|
||
"/auth/manual",
|
||
summary="保存手动 GitHub Token",
|
||
response_model=_SchemaResponse[GithubTokenStatus],
|
||
)
|
||
async def save_manual_token(
|
||
payload: GithubManualTokenRequest,
|
||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||
github_auth: GithubAuthService = Depends(get_github_auth_service),
|
||
) -> _SchemaResponse[GithubTokenStatus]:
|
||
"""
|
||
保存兼容 PAT 的手动 GitHub Token,并返回脱敏状态。
|
||
"""
|
||
try:
|
||
status = await github_auth.set_manual_token(payload.token)
|
||
except GithubAuthError as error:
|
||
_raise_auth_error(error)
|
||
return _SchemaResponse(success=True, data=_status_model(status))
|
||
|
||
|
||
@router.delete(
|
||
"/auth/token",
|
||
summary="清除 GitHub Token",
|
||
response_model=_SchemaResponse[None],
|
||
)
|
||
async def clear_token(
|
||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||
github_auth: GithubAuthService = Depends(get_github_auth_service),
|
||
) -> _SchemaResponse[None]:
|
||
"""
|
||
清除管理员配置的 GitHub Token。
|
||
"""
|
||
try:
|
||
await github_auth.clear_token()
|
||
except GithubAuthError as error:
|
||
_raise_auth_error(error)
|
||
return _SchemaResponse(success=True)
|
||
|
||
|
||
@initialization_router.get(
|
||
"/status",
|
||
summary="查询初始化阶段 GitHub Token 状态",
|
||
response_model=_SchemaResponse[GithubTokenStatus],
|
||
)
|
||
async def get_initialization_auth_status(
|
||
service: UserService = Depends(get_user_service),
|
||
github_auth: GithubAuthService = Depends(get_github_auth_service),
|
||
) -> _SchemaResponse[GithubTokenStatus]:
|
||
"""
|
||
在实例尚未初始化时返回 GitHub Token 脱敏状态。
|
||
"""
|
||
await _ensure_initialization_open(service)
|
||
return _SchemaResponse(success=True, data=_status_model(await github_auth.status()))
|
||
|
||
|
||
@initialization_router.post(
|
||
"/start",
|
||
summary="从初始化页启动 GitHub 设备授权",
|
||
response_model=_SchemaResponse[GithubDeviceAuthStart],
|
||
)
|
||
async def start_initialization_auth(
|
||
service: UserService = Depends(get_user_service),
|
||
github_auth: GithubAuthService = Depends(get_github_auth_service),
|
||
) -> _SchemaResponse[GithubDeviceAuthStart]:
|
||
"""
|
||
限制在首次初始化窗口内启动 GitHub Device Flow。
|
||
"""
|
||
await _ensure_initialization_open(service)
|
||
try:
|
||
result = await github_auth.start_device_auth()
|
||
except GithubAuthError as error:
|
||
_raise_auth_error(error)
|
||
return _SchemaResponse(
|
||
success=True,
|
||
data=GithubDeviceAuthStart(
|
||
session_id=result.session_id,
|
||
verification_uri=result.verification_uri,
|
||
user_code=result.user_code,
|
||
expires_in=result.expires_in,
|
||
interval_seconds=result.interval_seconds,
|
||
),
|
||
)
|
||
|
||
|
||
@initialization_router.post(
|
||
"/poll",
|
||
summary="从初始化页轮询 GitHub 设备授权",
|
||
response_model=_SchemaResponse[GithubDeviceAuthPoll],
|
||
)
|
||
async def poll_initialization_auth(
|
||
payload: GithubDeviceAuthPollRequest,
|
||
service: UserService = Depends(get_user_service),
|
||
github_auth: GithubAuthService = Depends(get_github_auth_service),
|
||
) -> _SchemaResponse[GithubDeviceAuthPoll]:
|
||
"""
|
||
限制在首次初始化窗口内轮询 GitHub Device Flow。
|
||
"""
|
||
await _ensure_initialization_open(service)
|
||
try:
|
||
result = await github_auth.poll_device_auth(payload.session_id)
|
||
except GithubAuthError as error:
|
||
_raise_auth_error(error)
|
||
status = _status_model(result.status) if result.status else None
|
||
return _SchemaResponse(
|
||
success=True,
|
||
data=GithubDeviceAuthPoll(
|
||
state=cast(Literal["pending", "authorized", "slow_down", "denied", "expired", "failed"], result.state),
|
||
message=result.message,
|
||
retry_after=result.retry_after,
|
||
status=status,
|
||
),
|
||
)
|
||
|
||
|
||
@initialization_router.post(
|
||
"/manual",
|
||
summary="从初始化页保存手动 GitHub Token",
|
||
response_model=_SchemaResponse[GithubTokenStatus],
|
||
)
|
||
async def save_initialization_manual_token(
|
||
payload: GithubManualTokenRequest,
|
||
service: UserService = Depends(get_user_service),
|
||
github_auth: GithubAuthService = Depends(get_github_auth_service),
|
||
) -> _SchemaResponse[GithubTokenStatus]:
|
||
"""
|
||
限制在首次初始化窗口内保存兼容 PAT 的手动 Token。
|
||
"""
|
||
await _ensure_initialization_open(service)
|
||
try:
|
||
status = await github_auth.set_manual_token(payload.token)
|
||
except GithubAuthError as error:
|
||
_raise_auth_error(error)
|
||
return _SchemaResponse(success=True, data=_status_model(status))
|
||
|
||
|
||
@initialization_router.delete(
|
||
"/token",
|
||
summary="从初始化页清除 GitHub Token",
|
||
response_model=_SchemaResponse[None],
|
||
)
|
||
async def clear_initialization_token(
|
||
service: UserService = Depends(get_user_service),
|
||
github_auth: GithubAuthService = Depends(get_github_auth_service),
|
||
) -> _SchemaResponse[None]:
|
||
"""
|
||
限制在首次初始化窗口内清除 GitHub Token。
|
||
"""
|
||
await _ensure_initialization_open(service)
|
||
try:
|
||
await github_auth.clear_token()
|
||
except GithubAuthError as error:
|
||
_raise_auth_error(error)
|
||
return _SchemaResponse(success=True)
|