From cabc49eda95b5a40ef184edcd658eaa64510690c Mon Sep 17 00:00:00 2001 From: Pine Date: Wed, 26 Aug 2026 13:00:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(auth):=20=E5=90=8C=E4=B8=80=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E4=B8=8D=E5=90=8C=E7=99=BB=E5=BD=95=E6=96=B9=E5=BC=8F?= =?UTF-8?q?=E6=9D=83=E9=99=90=E4=B8=80=E8=87=B4=20=E2=80=94=E2=80=94=20?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E8=BA=AB=E4=BB=BD=E7=94=B1=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=E8=A7=92=E8=89=B2=E5=86=B3=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 _login_response_for_user: 保证 C端有opc_member身份, 但令牌角色取账号真实身份(多身份→中性token, role=users.role) - phone/wx/mp/扫码/绑定 登录统一走它, 不再强制 opc_member → 手机号绑定的operator账号手机号登录也是operator - 修「admin登录无权限, 刷新才有」根因(登录角色错配) --- app/api/routers/auth.py | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/app/api/routers/auth.py b/app/api/routers/auth.py index 0f3d788..5709a88 100644 --- a/app/api/routers/auth.py +++ b/app/api/routers/auth.py @@ -464,6 +464,22 @@ async def _login_response( ) +async def _login_response_for_user(db: Database, user: dict) -> LoginResponse: + """统一登录响应:令牌角色由**账号真实身份**决定,而非强制 opc_member。 + + 保证同一账号无论用 手机号/微信/小程序 哪个方式登录,拿到的角色与权限都一致 + (= 用账号密码登录的结果)。规则: + - 先 `_ensure_opc_identity` 确保 C 端也有 opc_member 身份(报名/政策等可用); + - 用 `list_for_user` 的真实身份集合决定令牌角色: + 多身份(如 operator+opc_member)→ 中性令牌,role 取 users.role(=operator); + 仅 opc_member → opc 令牌。 + """ + await _ensure_opc_identity(db, user["id"]) + identities = await db.identities.list_for_user(user["id"], active_only=True) + token_identity = identities[0] if len(identities) == 1 else None + return await _login_response(db, user, token_identity, identities) + + @router.post("/send-code", response_model=SendCodeResponse, summary="发送短信验证码") async def send_code(req: SendCodeRequest, db: Database = Depends(get_db)): """为手机号发送登录验证码。stub provider 仅在服务日志打印。""" @@ -503,12 +519,12 @@ async def phone_login(req: PhoneLoginRequest, request: Request, db: Database = D if user.get("status") != "active": raise HTTPException(status_code=403, detail="账号已禁用") - identity = await _ensure_opc_identity(db, user["id"]) + await _ensure_opc_identity(db, user["id"]) await db.audit.add( action="login", resource="auth", resource_id=user["id"], detail=f"phone-login {req.phone}", user_id=user["id"], ) - return await _login_response(db, user, identity) + return await _login_response_for_user(db, user) @router.post("/wx-login", response_model=LoginResponse, summary="微信扫码登录(预留)") @@ -543,12 +559,12 @@ async def wx_login(req: WxLoginRequest, request: Request, db: Database = Depends if user.get("status") != "active": raise HTTPException(status_code=403, detail="账号已禁用") - identity = await _ensure_opc_identity(db, user["id"]) + await _ensure_opc_identity(db, user["id"]) await db.audit.add( action="login", resource="auth", resource_id=user["id"], detail=f"wx-login {openid[:16]}", user_id=user["id"], ) - return await _login_response(db, user, identity) + return await _login_response_for_user(db, user) @router.post("/wx-phone", summary="微信补绑手机号") @@ -569,7 +585,7 @@ async def wx_phone(req: WxPhoneRequest, db: Database = Depends(get_db)): action="bind-phone", resource="auth", resource_id=user["id"], detail=f"wx bind phone {req.phone}", user_id=user["id"], ) - return await _login_response(db, user, await _ensure_opc_identity(db, user["id"])) + return await _login_response_for_user(db, user) # =========================================================================== @@ -616,13 +632,13 @@ async def bind_phone( except sms.SmsError as exc: raise HTTPException(status_code=401, detail=str(exc)) merged = await _bind_phone_merge(db, user, req.phone) - identity = await _ensure_opc_identity(db, merged["id"]) + await _ensure_opc_identity(db, merged["id"]) await db.audit.add( action="bind-phone", resource="auth", resource_id=merged["id"], detail=f"bind phone {req.phone} (merged)" if merged["id"] != user["id"] else f"bind phone {req.phone}", user_id=merged["id"], ) - return await _login_response(db, merged, identity) + return await _login_response_for_user(db, merged) @router.post("/unbind", summary="解绑登录方式(保留至少一种)") @@ -707,8 +723,8 @@ async def wx_callback_get( except wechat.WechatError as exc: raise HTTPException(status_code=401, detail=str(exc)) user = await _find_or_create_wx_user(openid, request, db) - identity = await _ensure_opc_identity(db, user["id"]) - resp = await _login_response(db, user, identity) + await _ensure_opc_identity(db, user["id"]) + resp = await _login_response_for_user(db, user) wx_qr_store.complete(state or openid, resp.token, resp.model_dump()) return _wx_success_html() @@ -728,8 +744,7 @@ async def wx_callback_post( except wechat.WechatError as exc: raise HTTPException(status_code=401, detail=str(exc)) user = await _find_or_create_wx_user(openid, request, db) - identity = await _ensure_opc_identity(db, user["id"]) - return await _login_response(db, user, identity) + return await _login_response_for_user(db, user) @router.get("/wx-qr/poll", summary="轮询扫码登录状态") @@ -847,8 +862,8 @@ async def mp_qr_confirm(request: Request, req: MpQrConfirmRequest, db: Database ) if user.get("status") != "active": raise HTTPException(status_code=403, detail="账号已禁用") - identity = await _ensure_opc_identity(db, user["id"]) - resp = await _login_response(db, user, identity) + await _ensure_opc_identity(db, user["id"]) + resp = await _login_response_for_user(db, user) wx_qr_store.complete(req.scene, resp.token, resp.model_dump()) return {"ok": True, "username": user["username"]}