751 lines
26 KiB
Python
751 lines
26 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Auth API - 认证相关接口
|
|
"""
|
|
from datetime import timedelta, datetime, timezone
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
|
from fastapi.security import OAuth2PasswordRequestForm
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.config import settings
|
|
from app.database import get_db
|
|
from app.base_schema import ResponseModel
|
|
from utils.redis import RedisClient
|
|
from core.auth.schema import (
|
|
LoginRequest,
|
|
TokenResponse,
|
|
RefreshTokenRequest,
|
|
LoginUserInfo,
|
|
)
|
|
from core.user.service import UserService
|
|
from core.login_log.service import LoginLogService
|
|
from utils.client_info import get_client_info, get_device_id
|
|
from utils.security import (
|
|
create_access_token,
|
|
create_refresh_token,
|
|
verify_refresh_token,
|
|
get_current_user,
|
|
)
|
|
|
|
router = APIRouter(prefix="", tags=["认证管理"])
|
|
|
|
# Redis中存储refresh token的key前缀
|
|
REFRESH_TOKEN_PREFIX = "refresh_token:"
|
|
# Redis中存储token黑名单的key前缀
|
|
TOKEN_BLACKLIST_PREFIX = "token_blacklist:"
|
|
|
|
|
|
@router.post("/login", response_model=TokenResponse, summary="用户登录")
|
|
async def login(
|
|
request: Request,
|
|
data: LoginRequest,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
用户登录(JSON格式,供前端使用)
|
|
|
|
- **username**: 用户名
|
|
- **password**: 密码
|
|
|
|
返回access_token和refresh_token
|
|
"""
|
|
# 获取客户端信息和设备标识
|
|
client_info = get_client_info(request)
|
|
device_id = get_device_id(request)
|
|
|
|
# 验证用户
|
|
user = await UserService.authenticate(db, data.username, data.password)
|
|
if not user:
|
|
# 记录登录失败日志
|
|
await LoginLogService.record_login(
|
|
db=db,
|
|
username=data.username,
|
|
status=0,
|
|
login_ip=client_info["login_ip"],
|
|
failure_reason=2, # 密码错误
|
|
failure_message="用户名或密码错误",
|
|
user_agent=client_info["user_agent"],
|
|
browser_type=client_info["browser_type"],
|
|
os_type=client_info["os_type"],
|
|
device_type=client_info["device_type"],
|
|
login_type="password",
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="用户名或密码错误",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
# 检查用户状态
|
|
if not user.is_active:
|
|
# 记录登录失败日志
|
|
await LoginLogService.record_login(
|
|
db=db,
|
|
username=data.username,
|
|
user_id=user.id,
|
|
status=0,
|
|
login_ip=client_info["login_ip"],
|
|
failure_reason=3, # 用户已禁用
|
|
failure_message="用户已被禁用",
|
|
user_agent=client_info["user_agent"],
|
|
browser_type=client_info["browser_type"],
|
|
os_type=client_info["os_type"],
|
|
device_type=client_info["device_type"],
|
|
login_type="password",
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="用户已被禁用"
|
|
)
|
|
|
|
if user.user_status != 1:
|
|
status_msg = {0: "用户已禁用", 2: "用户已锁定"}.get(user.user_status, "用户状态异常")
|
|
failure_reason = 3 if user.user_status == 0 else 4 # 3=禁用, 4=锁定
|
|
# 记录登录失败日志
|
|
await LoginLogService.record_login(
|
|
db=db,
|
|
username=data.username,
|
|
user_id=user.id,
|
|
status=0,
|
|
login_ip=client_info["login_ip"],
|
|
failure_reason=failure_reason,
|
|
failure_message=status_msg,
|
|
user_agent=client_info["user_agent"],
|
|
browser_type=client_info["browser_type"],
|
|
os_type=client_info["os_type"],
|
|
device_type=client_info["device_type"],
|
|
login_type="password",
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=status_msg
|
|
)
|
|
|
|
# 生成token(token中只存身份标识,不存角色等动态信息)
|
|
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
refresh_token_expires = timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
|
|
|
|
token_data = {
|
|
"sub": user.id,
|
|
"username": user.username,
|
|
}
|
|
access_token = create_access_token(token_data, access_token_expires, device_id=device_id)
|
|
refresh_token = create_refresh_token(token_data, refresh_token_expires, device_id=device_id)
|
|
|
|
# 将用户动态信息缓存到 Redis(中间件从此处获取角色等信息)
|
|
from utils.user_info_cache import set_cached_user_info
|
|
role_ids = await UserService.get_user_role_ids(db, user.id)
|
|
await set_cached_user_info(user.id, role_ids, user.dept_id, user.is_superuser)
|
|
|
|
# 将refresh token存入Redis
|
|
redis = await RedisClient.get_client()
|
|
|
|
# 如果不允许多设备登录,删除该用户的所有旧设备token
|
|
if not settings.ALLOW_MULTI_DEVICE_LOGIN:
|
|
# 查找并删除该用户的所有refresh token和access token
|
|
refresh_pattern = f"{REFRESH_TOKEN_PREFIX}{user.id}:*"
|
|
access_pattern = f"access_token:{user.id}:*"
|
|
|
|
# 删除所有refresh token
|
|
cursor = 0
|
|
while True:
|
|
cursor, keys = await redis.scan(cursor, match=refresh_pattern, count=100)
|
|
if keys:
|
|
await redis.delete(*keys)
|
|
if cursor == 0:
|
|
break
|
|
|
|
# 删除所有access token
|
|
cursor = 0
|
|
while True:
|
|
cursor, keys = await redis.scan(cursor, match=access_pattern, count=100)
|
|
if keys:
|
|
await redis.delete(*keys)
|
|
if cursor == 0:
|
|
break
|
|
|
|
# 存储新的refresh token
|
|
await redis.set(
|
|
f"{REFRESH_TOKEN_PREFIX}{user.id}:{device_id}",
|
|
refresh_token,
|
|
ex=int(refresh_token_expires.total_seconds())
|
|
)
|
|
|
|
# 存储 access token(用于判断设备在线状态)
|
|
await redis.set(
|
|
f"access_token:{user.id}:{device_id}",
|
|
access_token,
|
|
ex=int(access_token_expires.total_seconds())
|
|
)
|
|
|
|
# 存储设备信息
|
|
device_info_key = f"device_info:{user.id}:{device_id}"
|
|
await redis.hset(device_info_key, mapping={
|
|
"device_type": client_info["device_type"],
|
|
"browser_type": client_info["browser_type"] or "Unknown",
|
|
"os_type": client_info["os_type"] or "Unknown",
|
|
"ip_address": client_info["login_ip"],
|
|
"last_active_time": datetime.now(timezone.utc).isoformat()
|
|
})
|
|
await redis.expire(device_info_key, int(refresh_token_expires.total_seconds()))
|
|
|
|
# 更新最后登录时间和IP
|
|
await UserService.update_login_info(db, user.id, login_type="password")
|
|
|
|
# 记录登录成功日志
|
|
await LoginLogService.record_login(
|
|
db=db,
|
|
username=user.username,
|
|
user_id=user.id,
|
|
status=1,
|
|
login_ip=client_info["login_ip"],
|
|
user_agent=client_info["user_agent"],
|
|
browser_type=client_info["browser_type"],
|
|
os_type=client_info["os_type"],
|
|
device_type=client_info["device_type"],
|
|
login_type="password",
|
|
)
|
|
|
|
return TokenResponse(
|
|
accessToken=access_token,
|
|
refreshToken=refresh_token,
|
|
tokenType="bearer",
|
|
expireTime=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
|
)
|
|
|
|
|
|
@router.post("/login/oauth2", response_model=TokenResponse, summary="OAuth2登录(OAuth2)", include_in_schema=True)
|
|
async def login_oauth2(
|
|
request: Request,
|
|
form_data: OAuth2PasswordRequestForm = Depends(),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
用户登录(OAuth2表单格式,供Swagger使用)
|
|
|
|
- **username**: 用户名
|
|
- **password**: 密码
|
|
|
|
返回access_token和refresh_token(标准OAuth2格式)
|
|
"""
|
|
# 获取客户端信息和设备标识
|
|
client_info = get_client_info(request)
|
|
device_id = get_device_id(request)
|
|
|
|
# 验证用户
|
|
user = await UserService.authenticate(db, form_data.username, form_data.password)
|
|
if not user:
|
|
# 记录登录失败日志
|
|
await LoginLogService.record_login(
|
|
db=db,
|
|
username=form_data.username,
|
|
status=0,
|
|
login_ip=client_info["login_ip"],
|
|
failure_reason=2,
|
|
failure_message="用户名或密码错误",
|
|
user_agent=client_info["user_agent"],
|
|
browser_type=client_info["browser_type"],
|
|
os_type=client_info["os_type"],
|
|
device_type=client_info["device_type"],
|
|
login_type="password",
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="用户名或密码错误",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
# 检查用户状态
|
|
if not user.is_active:
|
|
await LoginLogService.record_login(
|
|
db=db,
|
|
username=form_data.username,
|
|
user_id=user.id,
|
|
status=0,
|
|
login_ip=client_info["login_ip"],
|
|
failure_reason=3,
|
|
failure_message="用户已被禁用",
|
|
user_agent=client_info["user_agent"],
|
|
browser_type=client_info["browser_type"],
|
|
os_type=client_info["os_type"],
|
|
device_type=client_info["device_type"],
|
|
login_type="password",
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="用户已被禁用"
|
|
)
|
|
|
|
if user.user_status != 1:
|
|
status_msg = {0: "用户已禁用", 2: "用户已锁定"}.get(user.user_status, "用户状态异常")
|
|
failure_reason = 3 if user.user_status == 0 else 4
|
|
await LoginLogService.record_login(
|
|
db=db,
|
|
username=form_data.username,
|
|
user_id=user.id,
|
|
status=0,
|
|
login_ip=client_info["login_ip"],
|
|
failure_reason=failure_reason,
|
|
failure_message=status_msg,
|
|
user_agent=client_info["user_agent"],
|
|
browser_type=client_info["browser_type"],
|
|
os_type=client_info["os_type"],
|
|
device_type=client_info["device_type"],
|
|
login_type="password",
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=status_msg
|
|
)
|
|
|
|
# 生成token(token中只存身份标识,不存角色等动态信息)
|
|
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
refresh_token_expires = timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
|
|
|
|
token_data = {
|
|
"sub": user.id,
|
|
"username": user.username,
|
|
}
|
|
access_token = create_access_token(token_data, access_token_expires, device_id=device_id)
|
|
refresh_token = create_refresh_token(token_data, refresh_token_expires, device_id=device_id)
|
|
|
|
# 将用户动态信息缓存到 Redis(中间件从此处获取角色等信息)
|
|
from utils.user_info_cache import set_cached_user_info
|
|
role_ids = await UserService.get_user_role_ids(db, user.id)
|
|
await set_cached_user_info(user.id, role_ids, user.dept_id, user.is_superuser)
|
|
|
|
# 将refresh token存入Redis
|
|
redis = await RedisClient.get_client()
|
|
|
|
# 如果不允许多设备登录,删除该用户的所有旧设备token
|
|
if not settings.ALLOW_MULTI_DEVICE_LOGIN:
|
|
# 查找并删除该用户的所有refresh token和access token
|
|
refresh_pattern = f"{REFRESH_TOKEN_PREFIX}{user.id}:*"
|
|
access_pattern = f"access_token:{user.id}:*"
|
|
|
|
# 删除所有refresh token
|
|
cursor = 0
|
|
while True:
|
|
cursor, keys = await redis.scan(cursor, match=refresh_pattern, count=100)
|
|
if keys:
|
|
await redis.delete(*keys)
|
|
if cursor == 0:
|
|
break
|
|
|
|
# 删除所有access token
|
|
cursor = 0
|
|
while True:
|
|
cursor, keys = await redis.scan(cursor, match=access_pattern, count=100)
|
|
if keys:
|
|
await redis.delete(*keys)
|
|
if cursor == 0:
|
|
break
|
|
|
|
# 存储新的refresh token
|
|
await redis.set(
|
|
f"{REFRESH_TOKEN_PREFIX}{user.id}:{device_id}",
|
|
refresh_token,
|
|
ex=int(refresh_token_expires.total_seconds())
|
|
)
|
|
|
|
# 存储 access token(用于判断设备在线状态)
|
|
await redis.set(
|
|
f"access_token:{user.id}:{device_id}",
|
|
access_token,
|
|
ex=int(access_token_expires.total_seconds())
|
|
)
|
|
|
|
# 存储设备信息
|
|
device_info_key = f"device_info:{user.id}:{device_id}"
|
|
await redis.hset(device_info_key, mapping={
|
|
"device_type": client_info["device_type"],
|
|
"browser_type": client_info["browser_type"] or "Unknown",
|
|
"os_type": client_info["os_type"] or "Unknown",
|
|
"ip_address": client_info["login_ip"],
|
|
"last_active_time": datetime.now(timezone.utc).isoformat()
|
|
})
|
|
await redis.expire(device_info_key, int(refresh_token_expires.total_seconds()))
|
|
|
|
# 更新最后登录时间
|
|
await UserService.update_login_info(db, user.id, login_type="password")
|
|
|
|
# 记录登录成功日志
|
|
await LoginLogService.record_login(
|
|
db=db,
|
|
username=user.username,
|
|
user_id=user.id,
|
|
status=1,
|
|
login_ip=client_info["login_ip"],
|
|
user_agent=client_info["user_agent"],
|
|
browser_type=client_info["browser_type"],
|
|
os_type=client_info["os_type"],
|
|
device_type=client_info["device_type"],
|
|
login_type="password",
|
|
)
|
|
|
|
return TokenResponse(
|
|
accessToken=access_token,
|
|
refreshToken=refresh_token,
|
|
tokenType="bearer",
|
|
expireTime=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
|
)
|
|
|
|
|
|
@router.post("/refresh_token", response_model=TokenResponse, summary="刷新Token")
|
|
async def refresh_token(
|
|
request: Request,
|
|
data: RefreshTokenRequest,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
使用refresh_token获取新的access_token
|
|
|
|
- **refresh_token**: 刷新令牌(在请求体中传递)
|
|
|
|
返回新的access_token和refresh_token
|
|
"""
|
|
print(f"[刷新Token] 收到请求")
|
|
|
|
# 验证refresh token
|
|
payload = verify_refresh_token(data.refresh_token)
|
|
if not payload:
|
|
print(f"[刷新Token] ❌ JWT验证失败")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="无效的刷新令牌",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
user_id = payload.get("sub")
|
|
device_id = payload.get("device_id")
|
|
print(f"[刷新Token] JWT验证成功: user_id={user_id}, device_id={device_id}")
|
|
|
|
if not user_id:
|
|
print(f"[刷新Token] ❌ user_id为空")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="无效的刷新令牌",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
# 如果 token 中没有 device_id,使用当前请求生成(兼容旧版本)
|
|
if not device_id:
|
|
device_id = get_device_id(request)
|
|
print(f"[刷新Token] 生成device_id: {device_id}")
|
|
|
|
# 检查refresh token是否在Redis中(防止已登出的token被使用)
|
|
redis = await RedisClient.get_client()
|
|
redis_key = f"{REFRESH_TOKEN_PREFIX}{user_id}:{device_id}"
|
|
redis_key_prev = f"{REFRESH_TOKEN_PREFIX}{user_id}:{device_id}:prev"
|
|
|
|
stored_token = await redis.get(redis_key)
|
|
stored_prev_token = await redis.get(redis_key_prev)
|
|
|
|
print(f"[刷新Token] Redis检查: current={stored_token is not None}, prev={stored_prev_token is not None}")
|
|
|
|
# 如果Redis中没有任何token,说明用户已登出
|
|
if not stored_token:
|
|
print(f"[刷新Token] ❌ Redis中没有token,用户可能已登出")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="刷新令牌已失效,请重新登录",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
# 验证token:当前token或上一个token都可以(处理并发刷新)
|
|
is_current_token = (stored_token == data.refresh_token)
|
|
is_prev_token = (stored_prev_token == data.refresh_token)
|
|
|
|
print(f"[刷新Token] Token匹配: current={is_current_token}, prev={is_prev_token}")
|
|
|
|
if not is_current_token and not is_prev_token:
|
|
print(f"[刷新Token] ❌ Token不匹配")
|
|
print(f"[刷新Token] 请求token前50字符: {data.refresh_token[:50]}")
|
|
print(f"[刷新Token] Redis current前50字符: {stored_token[:50] if stored_token else 'None'}")
|
|
print(f"[刷新Token] Redis prev前50字符: {stored_prev_token[:50] if stored_prev_token else 'None'}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="刷新令牌已失效",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
# 检查用户是否存在且有效
|
|
user = await UserService.get_by_id(db, user_id)
|
|
if not user or not user.is_active or user.user_status != 1:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="用户不存在或已被禁用",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
# 生成新的token(token中只存身份标识)
|
|
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
refresh_token_expires = timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
|
|
|
|
token_data = {
|
|
"sub": user.id,
|
|
"username": user.username,
|
|
}
|
|
new_access_token = create_access_token(token_data, access_token_expires, device_id=device_id)
|
|
new_refresh_token = create_refresh_token(token_data, refresh_token_expires, device_id=device_id)
|
|
|
|
# 刷新 Redis 用户信息缓存(确保角色等信息是最新的)
|
|
from utils.user_info_cache import set_cached_user_info
|
|
role_ids = await UserService.get_user_role_ids(db, user.id)
|
|
await set_cached_user_info(user.id, role_ids, user.dept_id, user.is_superuser)
|
|
|
|
# 更新Redis中的refresh token(使用设备标识)
|
|
redis = await RedisClient.get_client()
|
|
|
|
# 保存当前token为上一个token(用于处理并发刷新)
|
|
# 只有当使用的是当前token时才保存,避免旧token被重复使用
|
|
if is_current_token and stored_token:
|
|
await redis.set(
|
|
f"{REFRESH_TOKEN_PREFIX}{user.id}:{device_id}:prev",
|
|
stored_token,
|
|
ex=60 # 上一个token只保留60秒,足够处理并发请求
|
|
)
|
|
|
|
# 存储新的refresh token
|
|
await redis.set(
|
|
f"{REFRESH_TOKEN_PREFIX}{user.id}:{device_id}",
|
|
new_refresh_token,
|
|
ex=int(refresh_token_expires.total_seconds())
|
|
)
|
|
|
|
# 更新 access token(用于判断设备在线状态)
|
|
await redis.set(
|
|
f"access_token:{user.id}:{device_id}",
|
|
new_access_token,
|
|
ex=int(access_token_expires.total_seconds())
|
|
)
|
|
|
|
return TokenResponse(
|
|
accessToken=new_access_token,
|
|
refreshToken=new_refresh_token,
|
|
tokenType="bearer",
|
|
expireTime=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
|
)
|
|
|
|
|
|
@router.get("/logout", response_model=ResponseModel, summary="用户登出")
|
|
async def logout(request: Request):
|
|
"""
|
|
用户登出
|
|
|
|
需要携带有效的access_token
|
|
"""
|
|
user_id = request.state.user_id
|
|
device_id = get_device_id(request)
|
|
|
|
# 删除Redis中的 refresh token 和 access token(仅删除当前设备的token)
|
|
redis = await RedisClient.get_client()
|
|
await redis.delete(f"{REFRESH_TOKEN_PREFIX}{user_id}:{device_id}")
|
|
await redis.delete(f"{REFRESH_TOKEN_PREFIX}{user_id}:{device_id}:prev")
|
|
await redis.delete(f"access_token:{user_id}:{device_id}")
|
|
|
|
return ResponseModel(message="登出成功")
|
|
|
|
|
|
@router.get("/userinfo", response_model=LoginUserInfo, summary="获取当前用户信息")
|
|
async def get_me(
|
|
current_user = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
获取当前登录用户信息
|
|
|
|
需要携带有效的access_token
|
|
"""
|
|
# 获取岗位名称
|
|
post_name = None
|
|
if hasattr(current_user, 'post') and current_user.post:
|
|
post_name = current_user.post.name
|
|
|
|
# 获取角色ID列表
|
|
role_ids = await UserService.get_user_role_ids(db, current_user.id)
|
|
|
|
return LoginUserInfo(
|
|
id=current_user.id,
|
|
username=current_user.username,
|
|
email=current_user.email,
|
|
mobile=current_user.mobile,
|
|
avatar=current_user.avatar,
|
|
name=current_user.name,
|
|
gender=current_user.gender if current_user.gender is not None else 0,
|
|
gender_display=current_user.get_gender_display(),
|
|
user_type=current_user.user_type if current_user.user_type is not None else 1,
|
|
user_type_display=current_user.get_user_type_display(),
|
|
user_status=current_user.user_status if current_user.user_status is not None else 1,
|
|
user_status_display=current_user.get_user_status_display(),
|
|
birthday=current_user.birthday,
|
|
city=current_user.city,
|
|
address=current_user.address,
|
|
bio=current_user.bio,
|
|
is_superuser=current_user.is_superuser,
|
|
is_active=current_user.is_active,
|
|
dept_id=current_user.dept_id,
|
|
post_id=current_user.post_id,
|
|
post_name=post_name,
|
|
manager_id=current_user.manager_id,
|
|
role_ids=role_ids,
|
|
last_login=current_user.last_login,
|
|
last_login_ip=current_user.last_login_ip,
|
|
last_login_type=current_user.last_login_type,
|
|
sort=current_user.sort,
|
|
is_deleted=current_user.is_deleted,
|
|
sys_create_datetime=current_user.sys_create_datetime,
|
|
sys_update_datetime=current_user.sys_update_datetime,
|
|
)
|
|
|
|
|
|
@router.get("/menus", response_model=dict, summary="获取当前用户的菜单")
|
|
async def get_user_menus(
|
|
current_user = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
获取当前用户有权访问的菜单树
|
|
|
|
- 超级管理员返回所有菜单
|
|
- 普通用户返回角色关联的菜单
|
|
"""
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import selectinload
|
|
from core.menu.model import Menu
|
|
from core.role.model import Role
|
|
|
|
# 超级管理员获取所有菜单
|
|
if current_user.is_superuser:
|
|
result = await db.execute(
|
|
select(Menu).where(
|
|
Menu.is_deleted == False # noqa: E712
|
|
).order_by(Menu.sort, Menu.sys_create_datetime)
|
|
)
|
|
all_menus = list(result.scalars().all())
|
|
else:
|
|
# 普通用户获取角色关联的菜单(支持多角色)
|
|
from core.user.service import UserService
|
|
role_ids = await UserService.get_user_role_ids(db, current_user.id)
|
|
|
|
if not role_ids:
|
|
return {"menus": [], "home": None}
|
|
|
|
# 获取所有角色的菜单并合并(去重)
|
|
all_menus_dict = {}
|
|
for role_id in role_ids:
|
|
result = await db.execute(
|
|
select(Role)
|
|
.options(selectinload(Role.menus))
|
|
.where(
|
|
Role.id == role_id,
|
|
Role.status == True, # noqa: E712
|
|
Role.is_deleted == False # noqa: E712
|
|
)
|
|
)
|
|
role = result.scalar_one_or_none()
|
|
|
|
if role and role.menus:
|
|
for menu in role.menus:
|
|
if not menu.is_deleted:
|
|
all_menus_dict[menu.id] = menu
|
|
|
|
all_menus = list(all_menus_dict.values())
|
|
|
|
# 构建菜单树
|
|
menu_map = {}
|
|
root_menus = []
|
|
|
|
for menu in all_menus:
|
|
menu_node = {
|
|
"id": menu.id,
|
|
"name": menu.name,
|
|
"title": menu.title,
|
|
"path": menu.path,
|
|
"component": menu.component,
|
|
"icon": menu.icon,
|
|
"menu_type": menu.type,
|
|
"parent_id": menu.parent_id,
|
|
"sort": menu.sort,
|
|
"is_hidden": menu.hideInMenu,
|
|
"is_cache": menu.keepAlive,
|
|
"is_affix": menu.affixTab,
|
|
"redirect": menu.redirect,
|
|
"children": [],
|
|
}
|
|
menu_map[menu.id] = menu_node
|
|
|
|
# 建立父子关系
|
|
for menu in all_menus:
|
|
if menu.parent_id and menu.parent_id in menu_map:
|
|
menu_map[menu.parent_id]["children"].append(menu_map[menu.id])
|
|
else:
|
|
root_menus.append(menu_map[menu.id])
|
|
|
|
# 按sort排序
|
|
def sort_menus(menus):
|
|
menus.sort(key=lambda x: x.get("sort", 0))
|
|
for menu in menus:
|
|
if menu.get("children"):
|
|
sort_menus(menu["children"])
|
|
|
|
sort_menus(root_menus)
|
|
|
|
# 获取首页
|
|
home = None
|
|
for menu in all_menus:
|
|
if menu.affixTab and menu.type == "menu":
|
|
home = menu.path
|
|
break
|
|
|
|
return {"menus": root_menus, "home": home}
|
|
|
|
|
|
@router.get("/permissions", response_model=dict, summary="获取当前用户的权限")
|
|
async def get_user_permissions(
|
|
current_user = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
获取当前用户的权限码列表
|
|
|
|
- 超级管理员返回 ["*"](代表所有权限)
|
|
- 普通用户返回角色关联的权限码列表
|
|
"""
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import selectinload
|
|
from core.role.model import Role
|
|
|
|
# 超级管理员拥有所有权限
|
|
if current_user.is_superuser:
|
|
return {"permissions": ["*"], "is_superuser": True}
|
|
|
|
# 普通用户获取所有角色关联的权限(支持多角色)
|
|
from core.user.service import UserService
|
|
role_ids = await UserService.get_user_role_ids(db, current_user.id)
|
|
|
|
if not role_ids:
|
|
return {"permissions": [], "is_superuser": False}
|
|
|
|
# 遍历所有角色,收集权限码并去重
|
|
permission_codes_set = set()
|
|
for role_id in role_ids:
|
|
result = await db.execute(
|
|
select(Role)
|
|
.options(selectinload(Role.permissions))
|
|
.where(
|
|
Role.id == role_id,
|
|
Role.status == True, # noqa: E712
|
|
Role.is_deleted == False # noqa: E712
|
|
)
|
|
)
|
|
role = result.scalar_one_or_none()
|
|
if role and role.permissions:
|
|
for perm in role.permissions:
|
|
if perm.is_active:
|
|
permission_codes_set.add(perm.code)
|
|
|
|
return {"permissions": list(permission_codes_set), "is_superuser": False}
|