diff --git a/backend/app/api/wecom.py b/backend/app/api/wecom.py index 59b52ab..b207aab 100644 --- a/backend/app/api/wecom.py +++ b/backend/app/api/wecom.py @@ -7,7 +7,7 @@ from typing import Optional from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from defusedxml import ElementTree as ET from fastapi import APIRouter, Depends, HTTPException, Query, Request -from fastapi.responses import PlainTextResponse +from fastapi.responses import PlainTextResponse, HTMLResponse from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select @@ -248,12 +248,13 @@ async def test_message( @router.post("/setup-menu") async def setup_menu(current_user: dict = Depends(require_director)): """Create the standard app menu (开始填报 view + 绑定账号 click).""" + oauth_url = _build_oauth_url() buttons = [ - {"type": "view", "name": "开始填报", "url": "https://qj.dhdx.fun/m"}, + {"type": "view", "name": "开始填报", "url": oauth_url}, {"type": "click", "name": "绑定账号", "key": "BIND_ACCOUNT"}, ] success = await wecom_client.create_menu(buttons) - return {"success": success, "message": "菜单已部署 (view + click)" if success else "菜单创建失败"} + return {"success": success, "message": "菜单已部署 (含静默登录)" if success else "菜单创建失败"} @router.get("/menu") @@ -271,3 +272,90 @@ async def trigger_daily_check( """Manually trigger the daily reporting check (for testing or manual use).""" result = await check_daily_reporting(db) return result + + +# ── OAuth silent login ── + +import urllib.parse +from app.services.auth import build_token_for_user + + +def _build_oauth_url(redirect_path: str = "/m") -> str: + """Build the WeChat Work OAuth2 authorize URL (snsapi_base = silent). + The redirect target is encoded in the 'state' param to avoid query-string issues + with the OAuth redirect_uri validation.""" + if not settings.WECOM_CORP_ID: + return f"https://qj.dhdx.fun{redirect_path}" + params = urllib.parse.urlencode({ + "appid": settings.WECOM_CORP_ID, + "redirect_uri": "https://qj.dhdx.fun/api/wecom/oauth-callback", + "response_type": "code", + "scope": "snsapi_base", + "agentid": settings.WECOM_AGENT_ID, + "state": f"r={urllib.parse.quote(redirect_path)}", + }) + return f"https://open.weixin.qq.com/connect/oauth2/authorize?{params}#wechat_redirect" + + +@router.get("/oauth-url") +async def get_oauth_url(redirect: str = Query("/m")): + """Redirect to the WeChat Work OAuth authorize URL (silent login).""" + from fastapi.responses import RedirectResponse + return RedirectResponse(url=_build_oauth_url(redirect)) + + +@router.get("/oauth-callback") +async def oauth_callback( + code: str = Query(...), + state: str = Query(""), + db: AsyncSession = Depends(get_db), +): + """WeChat Work OAuth2 callback — exchange code for userid, find bound user, auto-login.""" + # Parse redirect target from state param: "r=%2Fm" + redirect = "/m" + if state.startswith("r="): + redirect = urllib.parse.unquote(state[2:]) + + # Exchange code for wecom userinfo + userinfo = await wecom_client.get_userinfo_by_code(code) + if not userinfo: + raise HTTPException(status_code=400, detail="Failed to exchange code with WeChat Work") + + wecom_userid = userinfo.get("UserId") or userinfo.get("userid") + if not wecom_userid: + raise HTTPException(status_code=400, detail="Could not get userid from WeChat Work") + + # Find bound user (case-insensitive — WeChat Work userids may vary in case) + from sqlalchemy import func as sa_func + result = await db.execute( + select(User).where(sa_func.lower(User.wecom_userid) == wecom_userid.lower()) + ) + user = result.scalar_one_or_none() + if not user: + # Not bound — redirect to bind page + bind_url = f"https://qj.dhdx.fun/wecom-bind?wecom_userid={wecom_userid}" + from fastapi.responses import RedirectResponse + return RedirectResponse(url=bind_url) + + # Issue JWT and auto-login via HTML page — saves credentials to localStorage, then redirects + token = build_token_for_user(user) + import json as _json + auth_data = _json.dumps({ + "token": token, + "userId": str(user.id), + "userName": user.name, + "userRole": user.role, + "theme": user.theme or "editorial", + }, ensure_ascii=False) + frontend_url = f"https://qj.dhdx.fun{redirect}" + html = f"""