import logging import uuid from datetime import datetime, timedelta from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel from .. import config, db, security, wechat from ..user import user_payload logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v1/auth", tags=["auth"]) class WeChatLoginRequest(BaseModel): code: str class BindPhoneRequest(BaseModel): code: str phone_code: str def get_current_user(request: Request): auth = request.headers.get("Authorization", "") if not auth.startswith("Bearer "): raise HTTPException(status_code=401, detail="缺少 token") token = auth[7:] session = db.fetch_one( "SELECT s.user_id AS id FROM auth_sessions s " "WHERE s.token=%s AND s.expires_at>NOW()", (token,), ) if not session: raise HTTPException(status_code=401, detail="token 无效或过期") user = db.fetch_one("SELECT * FROM auth_users WHERE id=%s", (session["id"],)) if not user: raise HTTPException(status_code=401, detail="用户不存在") return user def _record_session(user_id): """创建会话 token 并写入 auth_sessions。""" token = uuid.uuid4().hex + uuid.uuid4().hex[:16] expires = datetime.now() + timedelta(seconds=config.JWT_TTL_SECONDS) db.execute( "INSERT INTO auth_sessions (user_id, token, expires_at) VALUES (%s, %s, %s)", (user_id, token, expires), ) return token @router.post("/wechat-login") def wechat_login(req: WeChatLoginRequest): try: openid = wechat.code_to_openid(req.code) except wechat.WeChatError as exc: logger.warning("微信登录 code 换取 openid 失败: %s", exc) raise HTTPException(status_code=400, detail=str(exc)) user = db.fetch_one("SELECT * FROM auth_users WHERE wechat_openid=%s", (openid,)) if user: token = _record_session(user["id"]) return {"success": True, "is_new": False, "token": token, **user_payload(user)} return {"success": False, "need_phone": True, "error": "需要绑定手机号"} @router.post("/bind-phone") def bind_phone(req: BindPhoneRequest): try: openid = wechat.code_to_openid(req.code) phone = wechat.get_phone_number(req.phone_code) except wechat.WeChatError as exc: logger.warning("绑定手机号时获取微信信息失败: %s", exc) raise HTTPException(status_code=400, detail=str(exc)) if not phone: raise HTTPException(status_code=400, detail="获取手机号失败") encrypted = security.encrypt_phone(phone) user = db.fetch_one("SELECT * FROM auth_users WHERE phone_encrypted=%s", (encrypted,)) if not user: return {"success": False, "error": "手机号未匹配,无法登录"} db.execute("UPDATE auth_users SET wechat_openid=%s WHERE id=%s", (openid, user["id"])) user["wechat_openid"] = openid token = _record_session(user["id"]) return {"success": True, "is_new": True, "token": token, **user_payload(user)} @router.get("/verify") def verify(user=Depends(get_current_user)): return {"success": True, **user_payload(user)} @router.post("/logout") def logout(request: Request, user=Depends(get_current_user)): auth = request.headers.get("Authorization", "") if auth.startswith("Bearer "): db.execute("DELETE FROM auth_sessions WHERE token=%s", (auth[7:],)) # 退出登录时解除该用户手机号绑定的微信 openid,下次需重新授权手机号绑定 db.execute( "UPDATE auth_users SET wechat_openid=NULL WHERE id=%s", (user["id"],), ) return {"success": True}