Files
chongming-plugins/auth_oauth/providers/github.py
T

75 lines
2.7 KiB
Python

"""GitHub OAuth 提供方对接(授权码模式)。
HTTP 调用独立成模块函数,便于测试替身(monkeypatch)注入。
"""
import logging
import httpx
logger = logging.getLogger("plugins.auth_oauth.github")
_TOKEN_URL = "https://github.com/login/oauth/access_token"
_USER_API = "https://api.github.com/user"
_USER_EMAILS_API = "https://api.github.com/user/emails"
async def exchange_code(client_id: str, client_secret: str, code: str,
redirect_uri: str) -> str:
"""授权码换 access_token,失败抛 RuntimeError(业务语义,调用方转 fail 响应)。"""
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.post(
_TOKEN_URL,
json={
"client_id": client_id,
"client_secret": client_secret,
"code": code,
"redirect_uri": redirect_uri,
},
headers={"Accept": "application/json"},
)
data = resp.json()
if "access_token" not in data:
detail = data.get("error_description") or data.get("error") or data
raise RuntimeError(f"GitHub 授权码换取失败: {detail}")
return data["access_token"]
async def fetch_user(access_token: str) -> dict:
"""拉取 GitHub 用户并归一化为建号 profile。
Returns:
{uid, username, email, full_name, avatar_url}
Raises:
RuntimeError: API 失败或拿不到邮箱(users.email 非空唯一,必须可得)。
"""
async with httpx.AsyncClient(timeout=10.0) as client:
headers = {
"Authorization": f"Bearer {access_token}",
"Accept": "application/vnd.github+json",
}
resp = await client.get(_USER_API, headers=headers)
if resp.status_code != 200:
raise RuntimeError(f"GitHub 用户信息获取失败: HTTP {resp.status_code}")
u = resp.json()
email = u.get("email")
if not email:
# 私密邮箱:走 /user/emails 取主邮箱
er = await client.get(_USER_EMAILS_API, headers=headers)
if er.status_code == 200 and isinstance(er.json(), list):
emails = er.json()
primary = next(
(e for e in emails if e.get("primary")), emails[0] if emails else None
)
email = primary.get("email") if primary else None
if not email:
raise RuntimeError("GitHub 账号未提供可用邮箱,无法创建账号")
return {
"uid": str(u["id"]),
"username": u.get("login") or f"github_{u['id']}",
"email": email,
"full_name": u.get("name"),
"avatar_url": u.get("avatar_url"),
}