Files
cma-management/backend/app/api/auth.py
T
Hermes CI Fix b59de7c476 feat: 账套模式API统一 — entity_id参数全部走get_entity_id + 登录页/切换器接口打通
- kpis/cash/bsc_layers/growth_quality/tax_compliance/data/predict: entity_id参数统一为Depends(get_entity_id),token优先隔离(原Query(1)/None会被前端显式传参覆盖,存在越权面)
- auth: 新增GET /auth/login-entities(公开,登录页按用户名查授权企业) + /auth/my-entities(切换器) + /me带当前账套
- 前端: 拦截器删除自动附加X-Entity-Id/entity_id; Login公司选择器; MainLayout/ReportCenter切换器改switch-entity重新签发token+整页刷新
2026-08-11 11:29:43 +08:00

209 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""用户认证"""
import hashlib
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import User, UserEntity, Entity, OperationLog
from app.auth_middleware import create_token, require_auth, ROLES
router = APIRouter(prefix="/api/cma/auth", tags=["认证"])
def _user_entity_ids(db: Session, user_id: int) -> list[int]:
"""返回用户被授权的企业ID列表"""
rows = db.query(UserEntity.entity_id).filter(UserEntity.user_id == user_id).all()
return [r[0] for r in rows]
def _ensure_default_grants(db: Session, user_id: int) -> None:
"""存量用户兼容:若用户没有任何授权记录,则授予所有active企业(不锁死老账号)"""
cnt = db.query(UserEntity).filter(UserEntity.user_id == user_id).count()
if cnt > 0:
return
entities = db.query(Entity).filter(Entity.status == "active").all()
for e in entities:
db.add(UserEntity(user_id=user_id, entity_id=e.id, granted_by=1))
db.commit()
@router.post("/login")
def login(data: dict, db: Session = Depends(get_db)):
username = data.get("username", "")
password = data.get("password", "")
user = db.query(User).filter(User.username == username).first()
if not user or user.password_hash != hashlib.sha256(password.encode()).hexdigest():
raise HTTPException(401, "用户名或密码错误")
# 账套模式:登录必须指定企业(entity_id)
entity_id = data.get("entity_id")
if entity_id is None:
raise HTTPException(400, "账套模式:请选择登录企业(entity_id")
# 存量兼容:无授权记录时自动授予active企业
_ensure_default_grants(db, user.id)
allowed = _user_entity_ids(db, user.id)
if int(entity_id) not in allowed:
raise HTTPException(403, f"该账号未被授权访问企业 entity_id={entity_id}")
entity = db.query(Entity).filter(Entity.id == int(entity_id)).first()
if not entity or entity.status != "active":
raise HTTPException(403, f"企业 entity_id={entity_id} 不存在或未激活")
token = create_token(user.id, int(entity_id))
return {
"token": token,
"entity_id": int(entity_id),
"entity_name": entity.name,
"entity_short_name": entity.short_name,
"user": {
"id": user.id,
"username": user.username,
"name": user.name,
"role": user.role,
"role_name": ROLES.get(user.role, {}).get("name", user.role),
}
}
@router.post("/register")
def register(data: dict, db: Session = Depends(get_db)):
exist = db.query(User).filter(User.username == data.get("username")).first()
if exist:
raise HTTPException(400, "用户名已存在")
user = User(
username=data["username"],
password_hash=hashlib.sha256(data["password"].encode()).hexdigest(),
name=data.get("name", data["username"]),
role=data.get("role", "business"),
)
db.add(user)
db.commit()
# 新用户默认授予所有active企业
_ensure_default_grants(db, user.id)
return {"message": "注册成功"}
@router.get("/me")
def get_me(request: Request, current_user: User = Depends(require_auth), db: Session = Depends(get_db)):
"""获取当前用户信息(含当前账套)"""
from app.auth_middleware import extract_bearer_token, get_token_entity_id
token = extract_bearer_token(request)
eid = get_token_entity_id(token) if token else None
ent = db.query(Entity).filter(Entity.id == eid).first() if eid else None
d = {
"id": current_user.id,
"username": current_user.username,
"name": current_user.name,
"role": current_user.role,
"role_name": ROLES.get(current_user.role, {}).get("name", current_user.role),
"phone": current_user.phone,
}
if ent:
d["entity_id"] = ent.id
d["entity_name"] = ent.name
d["entity_short_name"] = ent.short_name
return d
@router.get("/roles")
def list_roles():
"""返回角色列表(给前端用)"""
return {
"data": [
{"code": k, "name": v["name"], "priority": v["priority"]}
for k, v in ROLES.items()
]
}
@router.get("/entities")
def my_entities(current_user: User = Depends(require_auth), db: Session = Depends(get_db)):
"""当前用户被授权的企业列表(登录页下拉/切换器数据源)"""
_ensure_default_grants(db, current_user.id)
ids = _user_entity_ids(db, current_user.id)
entities = db.query(Entity).filter(
Entity.id.in_(ids),
Entity.status == "active",
).order_by(Entity.id).all()
return {
"data": [
{"id": e.id, "name": e.name, "short_name": e.short_name, "industry": e.industry}
for e in entities
]
}
@router.get("/my-entities")
def my_entities_alias(current_user: User = Depends(require_auth), db: Session = Depends(get_db)):
"""别名:/auth/my-entities(前端切换器调用)"""
return my_entities(current_user, db)
@router.get("/login-entities")
def login_entities_options(username: str = None, db: Session = Depends(get_db)):
"""登录页公司选择器:按用户名返回授权企业(无鉴权,登录前调用;不暴露用户名是否存在)"""
if not username:
return {"data": []}
user = db.query(User).filter(User.username == username).first()
if not user:
return {"data": []}
_ensure_default_grants(db, user.id)
ids = _user_entity_ids(db, user.id)
ents = db.query(Entity).filter(
Entity.id.in_(ids),
Entity.status == "active",
).order_by(Entity.id).all()
return {"data": [{"id": e.id, "name": e.name, "short_name": e.short_name} for e in ents]}
@router.post("/switch-entity")
def switch_entity(
data: dict,
request: Request,
current_user: User = Depends(require_auth),
db: Session = Depends(get_db),
):
"""账套切换:校验授权 → 重新签发token → 写操作日志(方案B:无缝刷新token"""
entity_id = data.get("entity_id")
if not entity_id:
raise HTTPException(400, "缺少entity_id")
_ensure_default_grants(db, current_user.id)
allowed = _user_entity_ids(db, current_user.id)
if int(entity_id) not in allowed:
raise HTTPException(403, f"该账号未被授权访问企业 entity_id={entity_id}")
entity = db.query(Entity).filter(Entity.id == int(entity_id)).first()
if not entity or entity.status != "active":
raise HTTPException(403, f"企业 entity_id={entity_id} 不存在或未激活")
# 旧token失效:删除当前请求的旧token(切换即下线旧账套凭据)
auth = request.headers.get("Authorization", "")
if auth.startswith("Bearer "):
try:
import redis as redis_lib
r = redis_lib.Redis(host="127.0.0.1", port=6379, db=1, decode_responses=True)
r.delete(f"cma:token:{auth[7:]}")
except Exception:
pass
token = create_token(current_user.id, int(entity_id))
# 切换留痕
db.add(OperationLog(
user_id=current_user.id,
action="switch_entity",
target_type="entity",
target_id=int(entity_id),
detail={"entity_id": int(entity_id), "entity_name": entity.name, "from": "account-switch"},
))
db.commit()
return {
"token": token,
"entity_id": int(entity_id),
"entity_name": entity.name,
"entity_short_name": entity.short_name,
}