checkpoint: 权限即时生效——角色/用户变更自动踢下线 + roleless 用户自读修复
- online/service: kick_user_sessions 扫描 USER_SESSION 匹配目标用户,删三键使登录时固化的权限快照即时失效 - role service+controller: 更新/删除/改状态/改权限后踢下线绑定该角色的用户 - user service+controller: 停用/改密等变更后踢下线(对应用户即时 401 重登) - permission: 无角色用户读自身记录时按 id 匹配放行(修复 409 真 bug)
This commit is contained in:
@@ -78,6 +78,52 @@ class OnlineService:
|
|||||||
await RedisCURD(redis).delete(f"{RedisInitKeyConfig.USER_SESSION.key}:{session_id}")
|
await RedisCURD(redis).delete(f"{RedisInitKeyConfig.USER_SESSION.key}:{session_id}")
|
||||||
logger.info(f"强制下线用户会话: {session_id}")
|
logger.info(f"强制下线用户会话: {session_id}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def kick_user_sessions(redis: Redis, user_ids: list[int]) -> int:
|
||||||
|
"""强制下线指定用户的全部在线会话(权限变更/停用/改密后自动踢人)。
|
||||||
|
|
||||||
|
扫描 USER_SESSION:{session_id} 匹配 user_id,删除该会话的
|
||||||
|
ACCESS_TOKEN / REFRESH_TOKEN / USER_SESSION 三键,使登录时固化的
|
||||||
|
权限快照即时失效(用户下次请求 401,重新登录拿新权限)。
|
||||||
|
返回踢下线会话数;redis 不可用时仅记日志,不影响业务主流程。
|
||||||
|
"""
|
||||||
|
if not user_ids:
|
||||||
|
return 0
|
||||||
|
target = {int(uid) for uid in user_ids}
|
||||||
|
keys = await RedisCURD(redis).scan_keys(f"{RedisInitKeyConfig.USER_SESSION.key}:*")
|
||||||
|
if not keys:
|
||||||
|
return 0
|
||||||
|
raws = await RedisCURD(redis).mget(keys)
|
||||||
|
session_ids: list[str] = []
|
||||||
|
for key, raw in zip(keys, raws, strict=False):
|
||||||
|
if not raw:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
info = json.loads(raw)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
uid = int(info.get("user_id"))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if uid in target:
|
||||||
|
key_str = key.decode() if isinstance(key, bytes) else key
|
||||||
|
session_ids.append(key_str.split(":")[-1])
|
||||||
|
if not session_ids:
|
||||||
|
return 0
|
||||||
|
delete_keys: list[str] = []
|
||||||
|
for sid in session_ids:
|
||||||
|
delete_keys.extend(
|
||||||
|
[
|
||||||
|
f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{sid}",
|
||||||
|
f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:{sid}",
|
||||||
|
f"{RedisInitKeyConfig.USER_SESSION.key}:{sid}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
await RedisCURD(redis).delete(*delete_keys)
|
||||||
|
logger.info(f"权限变更自动踢下线: user_ids={sorted(target)} sessions={len(session_ids)}")
|
||||||
|
return len(session_ids)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def clear_online(redis: Redis) -> None:
|
async def clear_online(redis: Redis) -> None:
|
||||||
await RedisCURD(redis).clear(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:*")
|
await RedisCURD(redis).clear(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:*")
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ from typing import Annotated
|
|||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||||
from fastapi.responses import JSONResponse, StreamingResponse
|
from fastapi.responses import JSONResponse, StreamingResponse
|
||||||
|
from redis.asyncio.client import Redis
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.common.response import ResponseSchema, StreamResponse, SuccessResponse
|
from app.common.response import ResponseSchema, StreamResponse, SuccessResponse
|
||||||
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema, PaginationQueryParam
|
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema, PaginationQueryParam
|
||||||
from app.core.dependencies import AuthPermission, db_getter
|
from app.core.dependencies import AuthPermission, db_getter, redis_getter
|
||||||
from app.core.router_class import OperationLogRoute
|
from app.core.router_class import OperationLogRoute
|
||||||
from app.utils.common_util import bytes2file_response
|
from app.utils.common_util import bytes2file_response
|
||||||
|
|
||||||
@@ -56,10 +57,11 @@ async def create_role_controller(
|
|||||||
async def update_role_controller(
|
async def update_role_controller(
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:update"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:update"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
|
redis: Annotated[Redis, Depends(redis_getter)],
|
||||||
id: Annotated[int, Path(description="角色ID", ge=1)],
|
id: Annotated[int, Path(description="角色ID", ge=1)],
|
||||||
data: Annotated[RoleUpdateSchema, Body(description="角色修改参数")],
|
data: Annotated[RoleUpdateSchema, Body(description="角色修改参数")],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
result_dict = await RoleService(auth, db).update(id=id, data=data)
|
result_dict = await RoleService(auth, db).update(id=id, data=data, redis=redis)
|
||||||
return SuccessResponse(data=result_dict, msg="修改角色成功")
|
return SuccessResponse(data=result_dict, msg="修改角色成功")
|
||||||
|
|
||||||
|
|
||||||
@@ -67,9 +69,10 @@ async def update_role_controller(
|
|||||||
async def delete_role_controller(
|
async def delete_role_controller(
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:delete"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:delete"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
|
redis: Annotated[Redis, Depends(redis_getter)],
|
||||||
ids: Annotated[list[int], Body(description="ID列表")],
|
ids: Annotated[list[int], Body(description="ID列表")],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
await RoleService(auth, db).delete(ids=ids)
|
await RoleService(auth, db).delete(ids=ids, redis=redis)
|
||||||
return SuccessResponse(msg="删除角色成功")
|
return SuccessResponse(msg="删除角色成功")
|
||||||
|
|
||||||
|
|
||||||
@@ -77,9 +80,10 @@ async def delete_role_controller(
|
|||||||
async def batch_set_available_role_controller(
|
async def batch_set_available_role_controller(
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:patch"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:patch"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
|
redis: Annotated[Redis, Depends(redis_getter)],
|
||||||
data: Annotated[BatchSetAvailable, Body(description="状态设置")],
|
data: Annotated[BatchSetAvailable, Body(description="状态设置")],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
await RoleService(auth, db).set_available(data=data)
|
await RoleService(auth, db).set_available(data=data, redis=redis)
|
||||||
return SuccessResponse(msg="批量修改角色状态成功")
|
return SuccessResponse(msg="批量修改角色状态成功")
|
||||||
|
|
||||||
|
|
||||||
@@ -87,9 +91,10 @@ async def batch_set_available_role_controller(
|
|||||||
async def set_role_permission_controller(
|
async def set_role_permission_controller(
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:permission"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:permission"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
|
redis: Annotated[Redis, Depends(redis_getter)],
|
||||||
data: Annotated[RolePermissionSettingSchema, Body(description="角色授权参数")],
|
data: Annotated[RolePermissionSettingSchema, Body(description="角色授权参数")],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
await RoleService(auth, db).set_permission(data=data)
|
await RoleService(auth, db).set_permission(data=data, redis=redis)
|
||||||
return SuccessResponse(msg="授权角色成功")
|
return SuccessResponse(msg="授权角色成功")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
|
|
||||||
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
|
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
|
||||||
from app.core.exceptions import CustomException
|
from app.core.exceptions import CustomException
|
||||||
|
from app.core.logger import logger
|
||||||
from app.utils.common_util import search_to_dict
|
from app.utils.common_util import search_to_dict
|
||||||
from app.utils.excel_util import ExcelUtil
|
from app.utils.excel_util import ExcelUtil
|
||||||
|
|
||||||
@@ -110,12 +111,13 @@ class RoleService:
|
|||||||
new_role = await RoleCRUD(self.auth, self.db).create(data=data)
|
new_role = await RoleCRUD(self.auth, self.db).create(data=data)
|
||||||
return await self.detail(id=new_role.id)
|
return await self.detail(id=new_role.id)
|
||||||
|
|
||||||
async def update(self, id: int, data: RoleUpdateSchema) -> RoleOutSchema:
|
async def update(self, id: int, data: RoleUpdateSchema, redis: Any | None = None) -> RoleOutSchema:
|
||||||
"""更新角色
|
"""更新角色
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- id (int): 角色ID
|
- id (int): 角色ID
|
||||||
- data (RoleUpdateSchema): 更新角色模型
|
- data (RoleUpdateSchema): 更新角色模型
|
||||||
|
- redis (Any | None): Redis 客户端(非 None 时踢下线绑定该角色的用户)
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- RoleOutSchema: 更新后的角色响应模型
|
- RoleOutSchema: 更新后的角色响应模型
|
||||||
@@ -128,13 +130,16 @@ class RoleService:
|
|||||||
if exist_code and exist_code.id != id:
|
if exist_code and exist_code.id != id:
|
||||||
raise CustomException(msg="更新失败,角色编码已存在")
|
raise CustomException(msg="更新失败,角色编码已存在")
|
||||||
await RoleCRUD(self.auth, self.db).update(id=id, data=data)
|
await RoleCRUD(self.auth, self.db).update(id=id, data=data)
|
||||||
|
if redis is not None:
|
||||||
|
await self._kick_users(redis, [id])
|
||||||
return await self.detail(id=id)
|
return await self.detail(id=id)
|
||||||
|
|
||||||
async def delete(self, ids: list[int]) -> None:
|
async def delete(self, ids: list[int], redis: Any | None = None) -> None:
|
||||||
"""删除角色
|
"""删除角色
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- ids (list[int]): 角色ID列表
|
- ids (list[int]): 角色ID列表
|
||||||
|
- redis (Any | None): Redis 客户端(非 None 时踢下线绑定该角色的用户)
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- None
|
- None
|
||||||
@@ -148,12 +153,15 @@ class RoleService:
|
|||||||
raise CustomException(msg="删除失败,部分ID不存在")
|
raise CustomException(msg="删除失败,部分ID不存在")
|
||||||
|
|
||||||
await RoleCRUD(self.auth, self.db).delete(ids=ids)
|
await RoleCRUD(self.auth, self.db).delete(ids=ids)
|
||||||
|
if redis is not None:
|
||||||
|
await self._kick_users(redis, ids)
|
||||||
|
|
||||||
async def set_permission(self, data: RolePermissionSettingSchema) -> None:
|
async def set_permission(self, data: RolePermissionSettingSchema, redis: Any | None = None) -> None:
|
||||||
"""设置角色权限
|
"""设置角色权限
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- data (RolePermissionSettingSchema): 角色权限设置模型
|
- data (RolePermissionSettingSchema): 角色权限设置模型
|
||||||
|
- redis (Any | None): Redis 客户端(非 None 时踢下线绑定角色的用户)
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- None
|
- None
|
||||||
@@ -164,12 +172,15 @@ class RoleService:
|
|||||||
# 设置数据权限范围(自定义部门关联已废弃,直接清空)
|
# 设置数据权限范围(自定义部门关联已废弃,直接清空)
|
||||||
await RoleCRUD(self.auth, self.db).set(ids=data.role_ids, data_scope=data.data_scope)
|
await RoleCRUD(self.auth, self.db).set(ids=data.role_ids, data_scope=data.data_scope)
|
||||||
await RoleCRUD(self.auth, self.db).set_role_depts_crud(role_ids=data.role_ids, dept_ids=[])
|
await RoleCRUD(self.auth, self.db).set_role_depts_crud(role_ids=data.role_ids, dept_ids=[])
|
||||||
|
if redis is not None:
|
||||||
|
await self._kick_users(redis, data.role_ids)
|
||||||
|
|
||||||
async def set_available(self, data: BatchSetAvailable) -> None:
|
async def set_available(self, data: BatchSetAvailable, redis: Any | None = None) -> None:
|
||||||
"""设置角色可用状态
|
"""设置角色可用状态
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- data (BatchSetAvailable): 批量设置可用状态模型
|
- data (BatchSetAvailable): 批量设置可用状态模型
|
||||||
|
- redis (Any | None): Redis 客户端(非 None 时踢下线绑定该角色的用户)
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- None
|
- None
|
||||||
@@ -180,6 +191,23 @@ class RoleService:
|
|||||||
if rid not in role_map:
|
if rid not in role_map:
|
||||||
raise CustomException(msg="该数据不存在")
|
raise CustomException(msg="该数据不存在")
|
||||||
await RoleCRUD(self.auth, self.db).set(ids=data.ids, status=data.status)
|
await RoleCRUD(self.auth, self.db).set(ids=data.ids, status=data.status)
|
||||||
|
if redis is not None:
|
||||||
|
await self._kick_users(redis, data.ids)
|
||||||
|
|
||||||
|
async def _kick_users(self, redis: Any, role_ids: list[int]) -> int:
|
||||||
|
"""踢下线绑定指定角色的全部用户的会话(角色权限/状态/删除后调用)。"""
|
||||||
|
roles = await RoleCRUD(self.auth, self.db).get_list(search={"id": ("in", role_ids)}, preload=["users"])
|
||||||
|
user_ids = sorted({u.id for r in roles for u in r.users})
|
||||||
|
if not user_ids:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
from app.api.v1.module_monitor.online.service import OnlineService
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await OnlineService.kick_user_sessions(redis=redis, user_ids=user_ids)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"角色变更自动踢下线失败: role_ids={role_ids}, err={e!s}")
|
||||||
|
return 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def export_list(role_list: list[dict[str, Any]]) -> bytes:
|
def export_list(role_list: list[dict[str, Any]]) -> bytes:
|
||||||
|
|||||||
@@ -56,9 +56,10 @@ async def update_current_user_info_controller(
|
|||||||
async def change_current_user_password_controller(
|
async def change_current_user_password_controller(
|
||||||
auth: Annotated[AuthSchema, Depends(get_current_user)],
|
auth: Annotated[AuthSchema, Depends(get_current_user)],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
|
redis: Annotated[Redis, Depends(redis_getter)],
|
||||||
data: Annotated[UserChangePasswordSchema, Body(description="修改用户密码参数")],
|
data: Annotated[UserChangePasswordSchema, Body(description="修改用户密码参数")],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
result_dict = await UserService(auth, db).change_password(data=data)
|
result_dict = await UserService(auth, db).change_password(data=data, redis=redis)
|
||||||
return SuccessResponse(data=result_dict, msg="修改密码成功, 请重新登录")
|
return SuccessResponse(data=result_dict, msg="修改密码成功, 请重新登录")
|
||||||
|
|
||||||
|
|
||||||
@@ -66,11 +67,12 @@ async def change_current_user_password_controller(
|
|||||||
async def reset_password_controller(
|
async def reset_password_controller(
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:update"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:update"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
|
redis: Annotated[Redis, Depends(redis_getter)],
|
||||||
id: Annotated[int, Path(description="用户ID", ge=1)],
|
id: Annotated[int, Path(description="用户ID", ge=1)],
|
||||||
data: Annotated[ResetPasswordSchema, Body(description="重置用户密码参数")],
|
data: Annotated[ResetPasswordSchema, Body(description="重置用户密码参数")],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
data.id = id
|
data.id = id
|
||||||
result_dict = await UserService(auth, db).reset_password(data=data)
|
result_dict = await UserService(auth, db).reset_password(data=data, redis=redis)
|
||||||
return SuccessResponse(data=result_dict, msg="重置密码成功")
|
return SuccessResponse(data=result_dict, msg="重置密码成功")
|
||||||
|
|
||||||
|
|
||||||
@@ -159,10 +161,11 @@ async def create_user_controller(
|
|||||||
async def update_user_controller(
|
async def update_user_controller(
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:update"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:update"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
|
redis: Annotated[Redis, Depends(redis_getter)],
|
||||||
id: Annotated[int, Path(description="用户ID")],
|
id: Annotated[int, Path(description="用户ID")],
|
||||||
data: Annotated[UserUpdateSchema, Body(description="修改用户参数")],
|
data: Annotated[UserUpdateSchema, Body(description="修改用户参数")],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
result_dict = await UserService(auth, db).update(id=id, data=data)
|
result_dict = await UserService(auth, db).update(id=id, data=data, redis=redis)
|
||||||
return SuccessResponse(data=result_dict, msg="修改用户成功")
|
return SuccessResponse(data=result_dict, msg="修改用户成功")
|
||||||
|
|
||||||
|
|
||||||
@@ -170,9 +173,10 @@ async def update_user_controller(
|
|||||||
async def delete_user_controller(
|
async def delete_user_controller(
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:delete"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:delete"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
|
redis: Annotated[Redis, Depends(redis_getter)],
|
||||||
ids: Annotated[list[int], Body(description="ID列表")],
|
ids: Annotated[list[int], Body(description="ID列表")],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
await UserService(auth, db).delete(ids=ids)
|
await UserService(auth, db).delete(ids=ids, redis=redis)
|
||||||
return SuccessResponse(msg="删除用户成功")
|
return SuccessResponse(msg="删除用户成功")
|
||||||
|
|
||||||
|
|
||||||
@@ -180,9 +184,10 @@ async def delete_user_controller(
|
|||||||
async def batch_set_available_user_controller(
|
async def batch_set_available_user_controller(
|
||||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:patch"]))],
|
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:patch"]))],
|
||||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||||
|
redis: Annotated[Redis, Depends(redis_getter)],
|
||||||
data: Annotated[BatchSetAvailable, Body(description="状态设置")],
|
data: Annotated[BatchSetAvailable, Body(description="状态设置")],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
await UserService(auth, db).set_available(data=data)
|
await UserService(auth, db).set_available(data=data, redis=redis)
|
||||||
return SuccessResponse(msg="批量修改用户状态成功")
|
return SuccessResponse(msg="批量修改用户状态成功")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ from app.api.v1.module_system.menu.crud import MenuCRUD
|
|||||||
from app.api.v1.module_system.menu.schema import MenuOutSchema, MenuTreeOutSchema
|
from app.api.v1.module_system.menu.schema import MenuOutSchema, MenuTreeOutSchema
|
||||||
from app.api.v1.module_system.position.crud import PositionCRUD
|
from app.api.v1.module_system.position.crud import PositionCRUD
|
||||||
from app.api.v1.module_system.role.crud import RoleCRUD
|
from app.api.v1.module_system.role.crud import RoleCRUD
|
||||||
|
from app.common.enums import RedisInitKeyConfig
|
||||||
from app.config.setting import settings
|
from app.config.setting import settings
|
||||||
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
|
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
|
||||||
from app.core.email import send_reset_code_email
|
from app.core.email import send_reset_code_email
|
||||||
from app.core.exceptions import CustomException
|
from app.core.exceptions import CustomException
|
||||||
from app.core.logger import logger
|
from app.core.logger import logger
|
||||||
from app.common.enums import RedisInitKeyConfig
|
|
||||||
from app.core.redis_crud import RedisCURD
|
from app.core.redis_crud import RedisCURD
|
||||||
from app.utils.common_util import search_to_dict, traversal_to_tree
|
from app.utils.common_util import search_to_dict, traversal_to_tree
|
||||||
from app.utils.excel_util import ExcelUtil
|
from app.utils.excel_util import ExcelUtil
|
||||||
@@ -133,7 +133,7 @@ class UserService:
|
|||||||
await UserCRUD(self.auth, self.db).set_user_positions(user_ids=[new_user.id], position_ids=data.position_ids)
|
await UserCRUD(self.auth, self.db).set_user_positions(user_ids=[new_user.id], position_ids=data.position_ids)
|
||||||
return await self.detail(id=new_user.id)
|
return await self.detail(id=new_user.id)
|
||||||
|
|
||||||
async def update(self, id: int, data: UserUpdateSchema) -> UserOutSchema:
|
async def update(self, id: int, data: UserUpdateSchema, redis: Redis | None = None) -> UserOutSchema:
|
||||||
if data.username:
|
if data.username:
|
||||||
if exist_user := await UserCRUD(self.auth, self.db).get(username=data.username):
|
if exist_user := await UserCRUD(self.auth, self.db).get(username=data.username):
|
||||||
if exist_user.id != id:
|
if exist_user.id != id:
|
||||||
@@ -174,9 +174,13 @@ class UserService:
|
|||||||
raise CustomException(msg="更新失败,部分岗位已被禁用")
|
raise CustomException(msg="更新失败,部分岗位已被禁用")
|
||||||
await UserCRUD(self.auth, self.db).set_user_positions(user_ids=[id], position_ids=data.position_ids)
|
await UserCRUD(self.auth, self.db).set_user_positions(user_ids=[id], position_ids=data.position_ids)
|
||||||
|
|
||||||
|
# 角色/状态变更使登录时固化的权限快照失效,自动踢下线让其重登生效
|
||||||
|
if redis is not None and (data.role_ids is not None or data.status is not None):
|
||||||
|
await self._kick_users(redis, [id])
|
||||||
|
|
||||||
return await self.detail(id=id)
|
return await self.detail(id=id)
|
||||||
|
|
||||||
async def delete(self, ids: list[int]) -> None:
|
async def delete(self, ids: list[int], redis: Redis | None = None) -> None:
|
||||||
if not ids:
|
if not ids:
|
||||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||||
users = await UserCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)})
|
users = await UserCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)})
|
||||||
@@ -199,6 +203,8 @@ class UserService:
|
|||||||
await UserCRUD(self.auth, self.db).set_user_roles(user_ids=ids, role_ids=[])
|
await UserCRUD(self.auth, self.db).set_user_roles(user_ids=ids, role_ids=[])
|
||||||
await UserCRUD(self.auth, self.db).set_user_positions(user_ids=ids, position_ids=[])
|
await UserCRUD(self.auth, self.db).set_user_positions(user_ids=ids, position_ids=[])
|
||||||
await UserCRUD(self.auth, self.db).delete(ids=ids)
|
await UserCRUD(self.auth, self.db).delete(ids=ids)
|
||||||
|
if redis is not None:
|
||||||
|
await self._kick_users(redis, ids)
|
||||||
|
|
||||||
async def current_info(self, check_data_scope: bool = True) -> CurrentUserOutSchema:
|
async def current_info(self, check_data_scope: bool = True) -> CurrentUserOutSchema:
|
||||||
user_id = self.auth.user.id
|
user_id = self.auth.user.id
|
||||||
@@ -262,14 +268,16 @@ class UserService:
|
|||||||
await UserCRUD(self.auth, self.db).update(id=user_id, data=user_update_data)
|
await UserCRUD(self.auth, self.db).update(id=user_id, data=user_update_data)
|
||||||
return await self.detail(id=user_id)
|
return await self.detail(id=user_id)
|
||||||
|
|
||||||
async def set_available(self, data: BatchSetAvailable) -> None:
|
async def set_available(self, data: BatchSetAvailable, redis: Redis | None = None) -> None:
|
||||||
users = await UserCRUD(self.auth, self.db).get_list(search={"id": ("in", data.ids)})
|
users = await UserCRUD(self.auth, self.db).get_list(search={"id": ("in", data.ids)})
|
||||||
for user in users:
|
for user in users:
|
||||||
if user.is_superuser:
|
if user.is_superuser:
|
||||||
raise CustomException(msg="超级管理员状态不能修改")
|
raise CustomException(msg="超级管理员状态不能修改")
|
||||||
await UserCRUD(self.auth, self.db).set(ids=data.ids, status=data.status)
|
await UserCRUD(self.auth, self.db).set(ids=data.ids, status=data.status)
|
||||||
|
if redis is not None:
|
||||||
|
await self._kick_users(redis, data.ids)
|
||||||
|
|
||||||
async def change_password(self, data: UserChangePasswordSchema) -> UserOutSchema:
|
async def change_password(self, data: UserChangePasswordSchema, redis: Redis | None = None) -> UserOutSchema:
|
||||||
user_id = self.auth.user.id
|
user_id = self.auth.user.id
|
||||||
if not user_id:
|
if not user_id:
|
||||||
raise CustomException(msg="该数据不存在")
|
raise CustomException(msg="该数据不存在")
|
||||||
@@ -280,17 +288,32 @@ class UserService:
|
|||||||
|
|
||||||
new_password_hash = PwdUtil.hash_password(password=data.new_password)
|
new_password_hash = PwdUtil.hash_password(password=data.new_password)
|
||||||
await UserCRUD(self.auth, self.db).change_password(id=user_id, password_hash=new_password_hash)
|
await UserCRUD(self.auth, self.db).change_password(id=user_id, password_hash=new_password_hash)
|
||||||
|
if redis is not None:
|
||||||
|
await self._kick_users(redis, [user_id])
|
||||||
return await self.detail(id=user_id)
|
return await self.detail(id=user_id)
|
||||||
|
|
||||||
async def reset_password(self, data: ResetPasswordSchema) -> UserOutSchema:
|
async def reset_password(self, data: ResetPasswordSchema, redis: Redis | None = None) -> UserOutSchema:
|
||||||
user = await UserCRUD(self.auth, self.db).get_or_404(id=data.id)
|
user = await UserCRUD(self.auth, self.db).get_or_404(id=data.id)
|
||||||
if user.is_superuser:
|
if user.is_superuser:
|
||||||
raise CustomException(msg="超级管理员密码不能重置")
|
raise CustomException(msg="超级管理员密码不能重置")
|
||||||
|
|
||||||
new_password_hash = PwdUtil.hash_password(password=data.password)
|
new_password_hash = PwdUtil.hash_password(password=data.password)
|
||||||
await UserCRUD(self.auth, self.db).change_password(id=data.id, password_hash=new_password_hash)
|
await UserCRUD(self.auth, self.db).change_password(id=data.id, password_hash=new_password_hash)
|
||||||
|
if redis is not None:
|
||||||
|
await self._kick_users(redis, [data.id])
|
||||||
return await self.detail(id=data.id)
|
return await self.detail(id=data.id)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _kick_users(redis: Redis, user_ids: list[int]) -> int:
|
||||||
|
"""踢下线指定用户的全部会话(权限/状态/密码变更后调用)。"""
|
||||||
|
from app.api.v1.module_monitor.online.service import OnlineService
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await OnlineService.kick_user_sessions(redis=redis, user_ids=user_ids)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"自动踢下线失败: user_ids={user_ids}, err={e!s}")
|
||||||
|
return 0
|
||||||
|
|
||||||
async def forget_password(self, data: UserForgetPasswordSchema) -> str:
|
async def forget_password(self, data: UserForgetPasswordSchema) -> str:
|
||||||
"""忘记密码前置校验:返回绑定邮箱(脱敏),供前端确认;不存在或未绑定邮箱则异常。"""
|
"""忘记密码前置校验:返回绑定邮箱(脱敏),供前端确认;不存在或未绑定邮箱则异常。"""
|
||||||
user = await UserCRUD(self.auth, self.db).get_or_404(username=data.username)
|
user = await UserCRUD(self.auth, self.db).get_or_404(username=data.username)
|
||||||
@@ -330,6 +353,7 @@ class UserService:
|
|||||||
new_password_hash = PwdUtil.hash_password(password=data.new_password)
|
new_password_hash = PwdUtil.hash_password(password=data.new_password)
|
||||||
await UserCRUD(self.auth, self.db).change_password(id=user.id, password_hash=new_password_hash)
|
await UserCRUD(self.auth, self.db).change_password(id=user.id, password_hash=new_password_hash)
|
||||||
await RedisCURD(redis).delete(redis_key)
|
await RedisCURD(redis).delete(redis_key)
|
||||||
|
await self._kick_users(redis, [user.id])
|
||||||
|
|
||||||
async def register(self, data: UserRegisterSchema) -> UserOutSchema:
|
async def register(self, data: UserRegisterSchema) -> UserOutSchema:
|
||||||
"""用户注册"""
|
"""用户注册"""
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ class Permission:
|
|||||||
roles = result.scalars().all()
|
roles = result.scalars().all()
|
||||||
|
|
||||||
if not roles:
|
if not roles:
|
||||||
|
if self.model.__name__ == "UserModel":
|
||||||
|
id_attr = getattr(self.model, "id", None)
|
||||||
|
if id_attr is not None and self.auth.user and self.auth.user.id:
|
||||||
|
return id_attr == self.auth.user.id
|
||||||
created_id_attr = getattr(self.model, "created_id", None)
|
created_id_attr = getattr(self.model, "created_id", None)
|
||||||
if created_id_attr is not None and self.auth.user and self.auth.user.id:
|
if created_id_attr is not None and self.auth.user and self.auth.user.id:
|
||||||
return created_id_attr == self.auth.user.id
|
return created_id_attr == self.auth.user.id
|
||||||
|
|||||||
Reference in New Issue
Block a user