Files
banban/mini-program/app/service/wechat_login.py

126 lines
4.3 KiB
Python

import logging
from dataclasses import dataclass
import httpx
try:
from app.settings import settings
except ModuleNotFoundError:
from settings import settings
logger = logging.getLogger("app.wechat_login")
INVALID_CODE_ERRCODES = {40029, 40163}
MISCONFIGURED_APP_ERRCODES = {40013, 40125}
@dataclass(frozen=True)
class WechatCodeSession:
openid: str
session_key: str
unionid: str | None = None
class WechatAuthError(Exception):
def __init__(self, detail: str, *, status_code: int, errcode: int | None = None):
super().__init__(detail)
self.status_code = status_code
self.errcode = errcode
class WechatAuthService:
def __init__(self, client: httpx.Client | None = None):
self._client = client
def exchange_code(self, code: str) -> WechatCodeSession:
if not settings.wechat_app_id or not settings.wechat_app_secret:
raise WechatAuthError("wechat login is not configured", status_code=503)
response = self._request_code2session(code)
data = self._parse_response_json(response)
errcode = self._parse_errcode(data.get("errcode"))
if errcode not in (None, 0):
errmsg = data.get("errmsg")
logger.warning(
"wechat code2session rejected login code",
extra={
"event": "wechat_code2session_rejected",
"errcode": errcode,
"errmsg": errmsg,
},
)
raise self._map_exchange_error(errcode)
openid = data.get("openid")
session_key = data.get("session_key")
unionid = data.get("unionid")
if not isinstance(openid, str) or not openid:
raise WechatAuthError("wechat login response missing openid", status_code=502)
if not isinstance(session_key, str) or not session_key:
raise WechatAuthError("wechat login response missing session_key", status_code=502)
if not isinstance(unionid, str) or not unionid:
unionid = None
return WechatCodeSession(openid=openid, session_key=session_key, unionid=unionid)
def _request_code2session(self, code: str) -> httpx.Response:
params = {
"appid": settings.wechat_app_id,
"secret": settings.wechat_app_secret,
"js_code": code,
"grant_type": "authorization_code",
}
client = self._client
owns_client = client is None
if client is None:
client = httpx.Client(
base_url=settings.wechat_api_base_url.rstrip("/"),
timeout=settings.wechat_http_timeout_seconds,
)
try:
response = client.get("/sns/jscode2session", params=params)
response.raise_for_status()
return response
except httpx.HTTPError as exc:
logger.warning(
"wechat code2session request failed",
extra={"event": "wechat_code2session_request_failed"},
)
raise WechatAuthError("wechat login service unavailable", status_code=502) from exc
finally:
if owns_client:
client.close()
def _parse_response_json(self, response: httpx.Response) -> dict:
try:
data = response.json()
except ValueError as exc:
raise WechatAuthError("invalid response from wechat login service", status_code=502) from exc
if not isinstance(data, dict):
raise WechatAuthError("invalid response from wechat login service", status_code=502)
return data
def _parse_errcode(self, value: object) -> int | None:
if value is None:
return None
try:
return int(value)
except (TypeError, ValueError):
return None
def _map_exchange_error(self, errcode: int) -> WechatAuthError:
if errcode in INVALID_CODE_ERRCODES:
return WechatAuthError("invalid or expired wechat login code", status_code=401, errcode=errcode)
if errcode in MISCONFIGURED_APP_ERRCODES:
return WechatAuthError("wechat login is not configured correctly", status_code=503, errcode=errcode)
return WechatAuthError("wechat login service unavailable", status_code=502, errcode=errcode)
def get_wechat_auth_service() -> WechatAuthService:
return WechatAuthService()