7493383f6a
- 管理端/园区端创建用户:账号系统随机生成不可指定,手机号必填且全局唯一
- 管理端用户管理支持绑定/更换手机号(PATCH /admin/users/{id}/phone)
Co-Authored-By: Claude <noreply@anthropic.com>
84 lines
3.9 KiB
Python
84 lines
3.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""业务层 · 用户管理服务(角色分配提权防护 + 建号联动默认身份)。"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import secrets
|
|
|
|
from fastapi import HTTPException
|
|
|
|
from ..infrastructure.repositories import Database
|
|
|
|
# 11 位大陆手机号
|
|
PHONE_RE = re.compile(r"^1[3-9]\d{9}$")
|
|
|
|
|
|
def validate_phone(phone: str) -> str:
|
|
"""校验并归一化手机号;不合法抛 400。"""
|
|
p = (phone or "").strip()
|
|
if not PHONE_RE.match(p):
|
|
raise HTTPException(status_code=400, detail="手机号格式不正确(需为 11 位大陆手机号)")
|
|
return p
|
|
|
|
|
|
async def create_username(db: Database) -> str:
|
|
"""生成随机账号(u_<20hex>),全局唯一,账号不可由用户指定。"""
|
|
for _ in range(8):
|
|
uname = f"u_{secrets.token_hex(10)}"
|
|
if await db.users.get_by_username(uname) is None:
|
|
return uname
|
|
raise HTTPException(status_code=500, detail="账号生成失败,请重试")
|
|
|
|
class UserAdminService:
|
|
"""运营端账号/角色管理。"""
|
|
|
|
def __init__(self, db: Database):
|
|
self.db = db
|
|
|
|
async def _validate_role_assignment(self, actor: dict, role: str, sub_role: str | None) -> None:
|
|
"""白名单 + 提权防护:拒绝非法角色组合,及无权者创建/提升特权角色。"""
|
|
key = role if not sub_role else f"{role}|{sub_role}"
|
|
if not await self.db.roles.role_exists(role):
|
|
raise HTTPException(status_code=400, detail=f"无效角色: {role}")
|
|
if sub_role and not await self.db.roles.role_exists(f"{role}|{sub_role}"):
|
|
raise HTTPException(status_code=400, detail=f"无效角色组合: {key}")
|
|
if "action:role.grant_perm" in await self.db.roles.permissions_for(role, sub_role) \
|
|
and "action:role.grant_perm" not in actor.get("permissions", []):
|
|
raise HTTPException(status_code=403, detail="无权创建/分配特权角色(仅超级管理员)")
|
|
|
|
async def create_user(self, req, actor: dict) -> dict:
|
|
"""创建账号(系统随机生成)+ 自动创建默认端口身份(含提权防护)。
|
|
|
|
规则:手机号必填且全局唯一;账号(username)随机生成,不可指定。
|
|
"""
|
|
phone = validate_phone(getattr(req, "phone", ""))
|
|
if not req.password.strip():
|
|
raise HTTPException(status_code=400, detail="密码必填")
|
|
if await self.db.users.find_by_phone(phone) is not None:
|
|
raise HTTPException(status_code=400, detail="手机号已被使用")
|
|
await self._validate_role_assignment(actor, req.role, req.sub_role)
|
|
username = await create_username(self.db)
|
|
user = await self.db.users.create(
|
|
username, req.password, nickname=req.nickname,
|
|
phone=phone,
|
|
role=req.role, sub_role=req.sub_role, org_id=req.org_id, region_id=req.region_id,
|
|
source="admin", auth_type="admin",
|
|
)
|
|
return user
|
|
|
|
async def bind_phone(self, user_id: str, phone: str) -> dict:
|
|
"""为用户绑定/更换手机号(格式校验 + 全局唯一,排除自身)。"""
|
|
p = validate_phone(phone)
|
|
existing = await self.db.users.find_by_phone(p)
|
|
if existing is not None and existing["id"] != user_id:
|
|
raise HTTPException(status_code=400, detail="手机号已被其他用户使用")
|
|
u = await self.db.users.update_profile(user_id, {"phone": p})
|
|
if u is None:
|
|
raise HTTPException(status_code=404, detail="用户不存在")
|
|
return u
|
|
|
|
async def set_user_role(self, actor: dict, user_id: str, role: str, sub_role: str, org_id: str, region_id: str) -> dict:
|
|
"""分配角色(含提权防护,同步主身份并失效旧令牌)。"""
|
|
await self._validate_role_assignment(actor, role, sub_role)
|
|
return await self.db.users.set_role(user_id, role, sub_role, org_id, region_id)
|