fix(auth): 同一账号不同登录方式权限一致 —— 登录身份由账号真实角色决定

- 新增 _login_response_for_user: 保证 C端有opc_member身份, 但令牌角色取账号真实身份(多身份→中性token, role=users.role)
- phone/wx/mp/扫码/绑定 登录统一走它, 不再强制 opc_member → 手机号绑定的operator账号手机号登录也是operator
- 修「admin登录无权限, 刷新才有」根因(登录角色错配)
This commit is contained in:
Pine
2026-08-26 13:00:20 +08:00
parent 2bac67fb3c
commit cabc49eda9
+28 -13
View File
@@ -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"]}