97 lines
3.4 KiB
Python
97 lines
3.4 KiB
Python
"""OAuth коннекторы — GitHub, Gmail, Google Drive, Yandex Disk, Google Calendar"""
|
||
from __future__ import annotations
|
||
import secrets, json
|
||
from fastapi import APIRouter
|
||
from pydantic import BaseModel
|
||
|
||
router = APIRouter(prefix="/api/v1/connectors", tags=["connectors"])
|
||
|
||
# In-memory OAuth state store (заменить на БД в production)
|
||
_pending: dict[str, dict] = {}
|
||
_connections: dict[str, list[dict]] = {}
|
||
|
||
OAUTH_CONFIG = {
|
||
"github": {
|
||
"name": "GitHub",
|
||
"auth_url": "https://github.com/login/oauth/authorize",
|
||
"scope": "repo,user",
|
||
},
|
||
"gmail": {
|
||
"name": "Gmail",
|
||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||
"scope": "https://www.googleapis.com/auth/gmail.readonly",
|
||
},
|
||
"gdrive": {
|
||
"name": "Google Drive",
|
||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||
"scope": "https://www.googleapis.com/auth/drive.readonly",
|
||
},
|
||
"gcal": {
|
||
"name": "Google Calendar",
|
||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||
"scope": "https://www.googleapis.com/auth/calendar.readonly",
|
||
},
|
||
"ydisk": {
|
||
"name": "Yandex Disk",
|
||
"auth_url": "https://oauth.yandex.ru/authorize",
|
||
"scope": "cloud_api:disk.read",
|
||
},
|
||
}
|
||
|
||
class OAuthInitRequest(BaseModel):
|
||
provider: str
|
||
project_id: str
|
||
|
||
@router.post("/oauth/init")
|
||
def oauth_init(req: OAuthInitRequest):
|
||
if req.provider not in OAUTH_CONFIG:
|
||
return {"error": "unknown_provider"}
|
||
|
||
cfg = OAUTH_CONFIG[req.provider]
|
||
state = secrets.token_urlsafe(16)
|
||
_pending[state] = {"provider": req.provider, "project_id": req.project_id}
|
||
|
||
# В production здесь формируется полный URL с client_id и redirect_uri
|
||
client_id = "ACC_CLIENT_ID_PLACEHOLDER"
|
||
redirect_uri = "http://localhost:8100/api/v1/connectors/oauth/callback"
|
||
auth_url = f"{cfg['auth_url']}?client_id={client_id}&redirect_uri={redirect_uri}&scope={cfg['scope']}&state={state}&response_type=code"
|
||
|
||
return {"auth_url": auth_url, "state": state, "provider": req.provider}
|
||
|
||
@router.get("/oauth/callback")
|
||
def oauth_callback(code: str, state: str):
|
||
"""OAuth callback — обработка кода авторизации"""
|
||
if state not in _pending:
|
||
return {"error": "invalid_state"}
|
||
|
||
pending = _pending.pop(state)
|
||
provider = pending["provider"]
|
||
|
||
# В production: обменять code на access_token через POST к token endpoint
|
||
connection = {
|
||
"provider": provider,
|
||
"account": f"{provider}_user",
|
||
"label": OAUTH_CONFIG[provider]["name"],
|
||
"token_valid": True,
|
||
"state": state,
|
||
}
|
||
|
||
if pending["project_id"] not in _connections:
|
||
_connections[pending["project_id"]] = []
|
||
_connections[pending["project_id"]].append(connection)
|
||
|
||
return {"connected": True, "provider": provider, **connection}
|
||
|
||
@router.get("/oauth/check")
|
||
def oauth_check(state: str):
|
||
"""Проверка статуса OAuth-авторизации (polling)"""
|
||
if state in _pending:
|
||
return {"connected": False, "status": "pending"}
|
||
|
||
# Найти connection по state
|
||
for conns in _connections.values():
|
||
for c in conns:
|
||
if c.get("state") == state:
|
||
return {"connected": True, **c}
|
||
|
||
return {"connected": False, "status": "expired"}
|