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"]}