feat(auth): 停用账密登录 + 扫码会话有效期下发 + 叠加制身份模型

- /auth/login 全端口停用(403 引导手机号/小程序扫码登录)
- 扫码会话接口下发 expires_in(前端二维码倒计时)
- role_allowed 叠加制身份:所有账号自带 OPC 基础身份,operator/carrier 仅业务叠加,35 处 C 端端点统一放行
This commit is contained in:
Pine
2026-08-28 17:17:58 +08:00
parent 4f11931143
commit 9c4908fb87
3 changed files with 24 additions and 6 deletions
+15 -5
View File
@@ -96,7 +96,7 @@ def _client_ip(request: Request) -> str:
xff = request.headers.get("X-Forwarded-For", "")
if xff:
return xff.split(",")[0].strip()
return request.client.host if (request.client and request.client.address) else ""
return request.client.host if request.client else ""
# 账号统一使用手机号作为登录账号(11 位,1 开头)
_PHONE_RE = re.compile(r"^1\d{10}$")
@@ -198,6 +198,11 @@ async def login(req: LoginRequest, db: Database = Depends(get_db)):
- 多个身份:签发中性账号令牌,前端展示身份选择,经
``/auth/select-identity`` 切换到指定身份后进入对应端口。
"""
# 账号密码登录已全端口停用:统一手机号验证码 / 小程序扫码登录(身份叠加制,角色仅是账号上的业务绑定)
raise HTTPException(
status_code=403,
detail="账号密码登录已停用,请使用手机号验证码或小程序扫码登录",
)
user = await db.users.get_by_username(req.username)
if user is None or not await db.users.verify_password(user, req.password):
raise HTTPException(status_code=401, detail="Invalid username or password")
@@ -313,7 +318,12 @@ async def update_profile(
if new_password is not None and not new_password.strip():
raise HTTPException(status_code=400, detail="Password cannot be empty")
from ...infrastructure.oss import to_object_path
profile_updates = {k: payload[k] for k in _PROFILE_FIELDS if k in payload}
# 头像等资源字段入库归一化:完整 URL → 对象路径(/oss/<key>),出口再生成 URL
for _k in ("avatar", "company_avatar"):
if _k in profile_updates:
profile_updates[_k] = to_object_path(profile_updates[_k])
# 小程序契约字段 → users 列映射(name→nicknamestatus→opc_statustopics→JSON文本)
if "name" in payload:
profile_updates["nickname"] = payload["name"]
@@ -717,11 +727,11 @@ async def mp_qr_start(request: Request, db: Database = Depends(get_db)):
try:
png = await wechat.get_wxacode(scene, page="pages/scan-login/index")
b64 = base64.b64encode(png).decode("ascii")
return {"scene": scene, "qr_image": f"data:image/png;base64,{b64}", "mp_enabled": True}
return {"scene": scene, "qr_image": f"data:image/png;base64,{b64}", "mp_enabled": True, "expires_in": wx_qr_store.TTL_SECONDS}
except wechat.WechatError:
# 无凭据/失败兜底:返回可扫描的 URL,前端按 qr_url 渲染普通二维码
qr_url = f"https://opc.pinesound.cn/mp/login?scene={scene}"
return {"scene": scene, "qr_url": qr_url, "mp_enabled": False}
return {"scene": scene, "qr_url": qr_url, "mp_enabled": False, "expires_in": wx_qr_store.TTL_SECONDS}
@router.get("/mp-qr/poll", summary="轮询小程序扫码登录状态")
@@ -747,10 +757,10 @@ async def mp_qr_bind_start(request: Request, db: Database = Depends(get_db), use
try:
png = await wechat.get_wxacode(scene, page="pages/scan-login/index")
b64 = base64.b64encode(png).decode("ascii")
return {"scene": scene, "qr_image": f"data:image/png;base64,{b64}", "mp_enabled": True}
return {"scene": scene, "qr_image": f"data:image/png;base64,{b64}", "mp_enabled": True, "expires_in": wx_qr_store.TTL_SECONDS}
except wechat.WechatError:
qr_url = f"https://opc.pinesound.cn/mp/bind?scene={scene}"
return {"scene": scene, "qr_url": qr_url, "mp_enabled": False}
return {"scene": scene, "qr_url": qr_url, "mp_enabled": False, "expires_in": wx_qr_store.TTL_SECONDS}
@router.post("/mp-qr/confirm", summary="小程序内确认扫码登录/绑定")