Files
zkxw/backend/app/api/public.py
T
2026-05-17 08:36:37 +08:00

290 lines
9.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.
"""公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/zkxw.sql 基线)"""
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.exc import OperationalError
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.security import verify_password
from app.core.auth import (
create_access_token,
create_refresh_token,
decode_token,
get_current_user_by_token,
extend_session,
is_refresh_token_valid,
is_session_valid,
revoke_refresh_token,
revoke_session,
store_refresh_token,
store_session,
)
from app.core.response import success_response, error_response
from app.models.models import SysUser
from app.crud.user import user_crud
from app.core.utils import generate_uuid
from app.core import login_crypto
from app.services.web_auth_roles import iter_user_menu_perm_codes, resolve_web_role_for_user
router = APIRouter(prefix="", tags=["公共接口"])
def _avatar_url_field(user: SysUser) -> Optional[str]:
raw = getattr(user, "avatar", None)
if not raw:
return None
s = str(raw).strip()
return s if s else None
def _build_user_info_payload(user: SysUser, web_role: str) -> dict:
"""与 web-admin `userInfo` 字段约定一致。"""
phone = (getattr(user, "phone_no", None) or "").strip() or None
return {
"id": user.id,
"username": user.user_no,
"realName": user.nick_name,
"phone": phone,
"email": (getattr(user, "email", None) or "").strip() or None,
"role": web_role,
"regionId": user.dept_id,
"regionName": None,
"avatarUrl": _avatar_url_field(user),
}
def _jwt_user_payload(user: SysUser, web_role: str) -> dict:
return {
"sub": user.id,
"username": user.user_no,
"role": web_role,
"region_id": user.dept_id,
"real_name": user.nick_name,
}
@router.get("/web/auth/login-rsa-public-key", response_model=dict)
async def web_login_rsa_public_key():
"""Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致)"""
login_crypto.ensure_initialized()
return success_response({"publicKeyPem": login_crypto.get_public_pem()})
@router.post("/web/auth/login", response_model=dict)
async def web_login(
request: Request,
body: dict,
db: Session = Depends(get_db),
):
"""Web 端登录:账号为 sys_user.user_no,密码为 sys_user.password(bcrypt)。"""
login_crypto.ensure_initialized()
user_no, password, cred_err = login_crypto.extract_login_credentials(body)
if cred_err:
return error_response(400, cred_err)
user = user_crud.get_by_user_no(db, user_no)
if not user:
return error_response(401, "用户名或密码错误")
pwd_hash = getattr(user, "password", None) or ""
if not verify_password(password, pwd_hash):
return error_response(401, "用户名或密码错误")
if user.status != "1":
return error_response(403, "账号已被停用,请联系管理员")
web_role = resolve_web_role_for_user(db, user.id)
session_id = generate_uuid()
access_jti = generate_uuid()
refresh_jti = generate_uuid()
access_token = create_access_token(
data=_jwt_user_payload(user, web_role),
session_id=session_id,
token_jti=access_jti,
)
refresh_token = create_refresh_token(
data={
"sub": user.id,
"username": user.user_no,
"role": web_role,
},
session_id=session_id,
refresh_jti=refresh_jti,
)
await store_session(session_id=session_id, user_id=user.id)
await store_refresh_token(refresh_jti=refresh_jti, user_id=user.id)
return success_response(
{
"token": access_token,
"refreshToken": refresh_token,
"expiresIn": 7200,
"userInfo": _build_user_info_payload(user, web_role),
}
)
@router.post("/web/auth/refresh-token", response_model=dict)
async def refresh_token(body: dict, db: Session = Depends(get_db)):
"""刷新 Token"""
refresh_token_val = body.get("refreshToken")
if not refresh_token_val:
return error_response(400, "refreshToken不能为空")
try:
payload = decode_token(refresh_token_val)
if payload.get("type") != "refresh":
return error_response(401, "无效的refreshToken")
session_id = payload.get("sid")
refresh_jti = payload.get("jti")
if not await is_session_valid(session_id=session_id):
return error_response(401, "refreshToken已失效,请重新登录")
if not await is_refresh_token_valid(refresh_jti=refresh_jti):
return error_response(401, "无效的refreshToken")
user_id = payload.get("sub")
user = user_crud.get(db, user_id)
if not user or user.del_flag != 0 or user.status != "1":
return error_response(401, "用户不存在或已禁用")
web_role = resolve_web_role_for_user(db, user.id)
await revoke_refresh_token(refresh_jti=refresh_jti)
await extend_session(session_id=session_id)
new_access_jti = generate_uuid()
new_refresh_jti = generate_uuid()
new_access_token = create_access_token(
data=_jwt_user_payload(user, web_role),
session_id=session_id,
token_jti=new_access_jti,
)
new_refresh_token = create_refresh_token(
data={
"sub": user.id,
"username": user.user_no,
"role": web_role,
},
session_id=session_id,
refresh_jti=new_refresh_jti,
)
await store_refresh_token(refresh_jti=new_refresh_jti, user_id=user.id)
return success_response(
{
"token": new_access_token,
"refreshToken": new_refresh_token,
"expiresIn": 7200,
}
)
except HTTPException:
return error_response(401, "refreshToken已过期")
@router.post("/web/auth/logout", response_model=dict)
async def logout(request: Request):
"""登出:吊销 Redis 会话(若已配置 REDIS_HOST)。"""
authorization = request.headers.get("Authorization", "")
if not authorization.startswith("Bearer "):
return error_response(401, "未授权")
token = authorization.replace("Bearer ", "")
try:
payload = decode_token(token)
session_id = payload.get("sid")
if session_id:
await revoke_session(session_id=session_id)
except HTTPException:
return error_response(401, "Token无效或已过期")
return success_response({"message": "登出成功"})
@router.get("/web/auth/current-user", response_model=dict)
async def get_current_user_profile(request: Request, db: Session = Depends(get_db)):
"""获取当前用户信息(与 sys_user 字段对应)。"""
authorization = request.headers.get("Authorization", "")
if not authorization.startswith("Bearer "):
return error_response(401, "未授权")
token = authorization.replace("Bearer ", "")
try:
current_user = await get_current_user_by_token(token=token, db=db)
au = current_user.get("avatar_url")
avatar_url = str(au).strip() if au else None
return success_response(
{
"id": current_user.get("user_id"),
"username": current_user.get("username"),
"realName": current_user.get("real_name"),
"phone": current_user.get("phone"),
"email": current_user.get("email"),
"role": current_user.get("role"),
"isSuperAdmin": current_user.get("role") == "super_admin",
"regionId": current_user.get("region_id"),
"regionName": None,
"regionIds": [],
"isActive": current_user.get("is_active"),
"avatarUrl": avatar_url if avatar_url else None,
}
)
except HTTPException:
return error_response(401, "Token无效或已过期")
@router.get("/web/auth/menu-codes", response_model=dict)
async def get_web_auth_menu_codes(request: Request, db: Session = Depends(get_db)):
"""获取当前用户菜单权限标识(sys_menu.perms,经角色继承)。"""
authorization = request.headers.get("Authorization", "")
if not authorization.startswith("Bearer "):
return error_response(401, "未授权")
token = authorization.replace("Bearer ", "")
try:
current_user = await get_current_user_by_token(token=token, db=db)
role = current_user.get("role")
if role == "super_admin":
return success_response(
{
"isSuperAdmin": True,
"all": True,
"menuCodes": [],
}
)
try:
codes = iter_user_menu_perm_codes(db, current_user.get("user_id") or "")
menu_codes = sorted(codes)
except OperationalError:
menu_codes = []
if role == "admin" and not menu_codes:
return success_response(
{
"isSuperAdmin": False,
"all": True,
"menuCodes": [],
}
)
return success_response(
{
"isSuperAdmin": False,
"all": False,
"menuCodes": menu_codes,
}
)
except HTTPException:
return error_response(401, "Token无效或已过期")