OAuth 로그인 버튼으로 provider 연결 + 제공자 연결 패널 그리드 이동
Some checks failed
API contract / OpenAPI type drift (push) Failing after 59s
Some checks failed
API contract / OpenAPI type drift (push) Failing after 59s
claude(setup-token과 동일한 PKCE, code#state 수동 흐름)와 openrouter(헤드리스 코드 표시 모드)는 OAuth 로그인 버튼으로 연결한다. 관리자가 제공자 로그인 뒤 표시되는 인증 코드를 붙여넣으면 서버가 토큰/키로 교환해 저장·게이트웨이 push한다. state는 서버 발급·15분 단일 사용으로 CSRF를 막는다. codex·agy는 붙여넣기 방식을 유지한다. 제공자 연결 패널은 우측 사이드에서 메인 컬럼의 Provider·모델별 사용 원장 아래로 옮기고 반응형 그리드(너비에 따라 1~n열)로 배치한다.
This commit is contained in:
parent
e3a9fc717c
commit
cb4d45579d
9 changed files with 752 additions and 39 deletions
|
|
@ -39,6 +39,7 @@ from ..engine_client import engine_client
|
|||
from ..runtime_policy import require_runtime_fallback_allowed
|
||||
from ..services import evaluator, notifications, rag
|
||||
from ..services import provider_credentials as provider_credentials_service
|
||||
from ..services import provider_oauth as provider_oauth_service
|
||||
from ..services.llm_pricing import (
|
||||
estimate_reference_cost,
|
||||
provider_uses_reference_cost,
|
||||
|
|
@ -350,6 +351,7 @@ class AdminProviderCredentialStatus(BaseModel):
|
|||
token_page_label: str
|
||||
guide: str
|
||||
allowed_auth_kinds: list[str]
|
||||
oauth_supported: bool = False
|
||||
|
||||
|
||||
class AdminProvidersResponse(BaseModel):
|
||||
|
|
@ -384,6 +386,18 @@ class AdminProviderVerifyResponse(BaseModel):
|
|||
verified_at: float | None = None
|
||||
|
||||
|
||||
class AdminProviderOAuthStartResponse(BaseModel):
|
||||
provider: str
|
||||
authorize_url: str
|
||||
state: str
|
||||
expires_in: int
|
||||
|
||||
|
||||
class AdminProviderOAuthFinishRequest(BaseModel):
|
||||
code: str = Field(min_length=4, max_length=8192)
|
||||
state: str = Field(min_length=8, max_length=256)
|
||||
|
||||
|
||||
class AdminTicketPatch(BaseModel):
|
||||
status: TicketStatus | None = None
|
||||
priority: TicketPriority | None = None
|
||||
|
|
@ -2391,6 +2405,7 @@ async def list_providers(principal: AdminPrincipal) -> AdminProvidersResponse:
|
|||
token_page_label=meta.token_page_label,
|
||||
guide=meta.guide,
|
||||
allowed_auth_kinds=list(meta.auth_kinds),
|
||||
oauth_supported=provider_oauth_service.oauth_supported(meta.code),
|
||||
)
|
||||
)
|
||||
return AdminProvidersResponse(
|
||||
|
|
@ -2487,6 +2502,54 @@ async def verify_provider_credential(
|
|||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/providers/{provider}/oauth/start",
|
||||
response_model=AdminProviderOAuthStartResponse,
|
||||
)
|
||||
async def start_provider_oauth(
|
||||
provider: str,
|
||||
principal: AdminPrincipal,
|
||||
) -> AdminProviderOAuthStartResponse:
|
||||
"""OAuth 로그인 시작 — 제공자 인증 화면 URL을 발급한다."""
|
||||
try:
|
||||
result = provider_oauth_service.start_oauth(provider, principal.email)
|
||||
except provider_oauth_service.ProviderCredentialError as exc:
|
||||
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
||||
return AdminProviderOAuthStartResponse(
|
||||
provider=provider,
|
||||
authorize_url=result["authorize_url"],
|
||||
state=result["state"],
|
||||
expires_in=result["expires_in"],
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/providers/{provider}/oauth/finish",
|
||||
response_model=AdminProviderCredentialResponse,
|
||||
)
|
||||
async def finish_provider_oauth(
|
||||
provider: str,
|
||||
body: AdminProviderOAuthFinishRequest,
|
||||
principal: AdminPrincipal,
|
||||
) -> AdminProviderCredentialResponse:
|
||||
"""붙여넣은 인증 코드를 토큰으로 교환해 저장하고 게이트웨이에 push한다."""
|
||||
try:
|
||||
result = await provider_oauth_service.finish_oauth(
|
||||
provider, body.code, body.state, principal.email
|
||||
)
|
||||
except provider_oauth_service.ProviderCredentialError as exc:
|
||||
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
||||
stored = result["stored"]
|
||||
return AdminProviderCredentialResponse(
|
||||
provider=stored.provider,
|
||||
token_hint=stored.token_hint,
|
||||
auth_kind=stored.auth_kind,
|
||||
updated_by=stored.updated_by,
|
||||
updated_at=stored.updated_at,
|
||||
gateway_sync=_provider_sync_response(result["gateway_sync"]),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/users", response_model=AdminUsersResponse)
|
||||
async def list_users(principal: AdminPrincipal) -> AdminUsersResponse:
|
||||
"""Return users observed by the server-side auth/session boundary."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue