feat: 完成用户认证系统集成 - 添加JWT认证、用户/角色/菜单管理API - 前端对接后端API,修改系统名称为工大智能机房管控系统 - 修复MenuDto格式以匹配前端AppRouteRecord结构
This commit is contained in:
parent
9e3b1f3c03
commit
eda41878a6
453
.kiro/specs/admin-system-backend/design.md
Normal file
453
.kiro/specs/admin-system-backend/design.md
Normal file
@ -0,0 +1,453 @@
|
|||||||
|
# Design Document: Admin System Backend
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
本设计文档描述了为 adminSystem 前端提供后端支持的 API 设计。后端将扩展现有的 C# ASP.NET Core 项目,添加用户认证、用户管理、角色管理和动态菜单功能。
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph Frontend["adminSystem Frontend"]
|
||||||
|
Login[登录页面]
|
||||||
|
Dashboard[控制台]
|
||||||
|
UserMgmt[用户管理]
|
||||||
|
RoleMgmt[角色管理]
|
||||||
|
MenuMgmt[菜单管理]
|
||||||
|
AMT[AMT Scanner 功能]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Backend["C# Backend API"]
|
||||||
|
AuthController[AuthController]
|
||||||
|
UserController[UserController]
|
||||||
|
RoleController[RoleController]
|
||||||
|
MenuController[MenuController]
|
||||||
|
|
||||||
|
AuthService[AuthService]
|
||||||
|
UserService[UserService]
|
||||||
|
RoleService[RoleService]
|
||||||
|
MenuService[MenuService]
|
||||||
|
|
||||||
|
JwtMiddleware[JWT 中间件]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Database["MySQL Database"]
|
||||||
|
Users[(Users)]
|
||||||
|
Roles[(Roles)]
|
||||||
|
UserRoles[(UserRoles)]
|
||||||
|
Menus[(Menus)]
|
||||||
|
RoleMenus[(RoleMenus)]
|
||||||
|
end
|
||||||
|
|
||||||
|
Login --> AuthController
|
||||||
|
Dashboard --> MenuController
|
||||||
|
UserMgmt --> UserController
|
||||||
|
RoleMgmt --> RoleController
|
||||||
|
MenuMgmt --> MenuController
|
||||||
|
AMT --> |现有 API| Backend
|
||||||
|
|
||||||
|
AuthController --> AuthService
|
||||||
|
UserController --> UserService
|
||||||
|
RoleController --> RoleService
|
||||||
|
MenuController --> MenuService
|
||||||
|
|
||||||
|
AuthService --> Users
|
||||||
|
UserService --> Users
|
||||||
|
UserService --> UserRoles
|
||||||
|
RoleService --> Roles
|
||||||
|
RoleService --> RoleMenus
|
||||||
|
MenuService --> Menus
|
||||||
|
MenuService --> RoleMenus
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components and Interfaces
|
||||||
|
|
||||||
|
### 1. AuthController
|
||||||
|
|
||||||
|
负责用户认证相关的 API 端点。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/auth")]
|
||||||
|
public class AuthController : ControllerBase
|
||||||
|
{
|
||||||
|
// POST /api/auth/login - 用户登录
|
||||||
|
[HttpPost("login")]
|
||||||
|
public async Task<ActionResult<ApiResponse<LoginResponse>>> Login(LoginRequest request);
|
||||||
|
|
||||||
|
// POST /api/auth/refresh - 刷新 Token
|
||||||
|
[HttpPost("refresh")]
|
||||||
|
public async Task<ActionResult<ApiResponse<LoginResponse>>> RefreshToken(RefreshTokenRequest request);
|
||||||
|
|
||||||
|
// POST /api/auth/logout - 退出登录
|
||||||
|
[HttpPost("logout")]
|
||||||
|
public async Task<ActionResult<ApiResponse>> Logout();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. UserController
|
||||||
|
|
||||||
|
负责用户信息和用户管理的 API 端点。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/user")]
|
||||||
|
public class UserController : ControllerBase
|
||||||
|
{
|
||||||
|
// GET /api/user/info - 获取当前用户信息
|
||||||
|
[HttpGet("info")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<ApiResponse<UserInfo>>> GetUserInfo();
|
||||||
|
|
||||||
|
// GET /api/user/list - 获取用户列表(分页)
|
||||||
|
[HttpGet("list")]
|
||||||
|
[Authorize(Roles = "R_SUPER,R_ADMIN")]
|
||||||
|
public async Task<ActionResult<ApiResponse<PaginatedResponse<UserListItem>>>> GetUserList([FromQuery] UserSearchParams query);
|
||||||
|
|
||||||
|
// POST /api/user - 创建用户
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize(Roles = "R_SUPER,R_ADMIN")]
|
||||||
|
public async Task<ActionResult<ApiResponse<UserListItem>>> CreateUser(CreateUserRequest request);
|
||||||
|
|
||||||
|
// PUT /api/user/{id} - 更新用户
|
||||||
|
[HttpPut("{id}")]
|
||||||
|
[Authorize(Roles = "R_SUPER,R_ADMIN")]
|
||||||
|
public async Task<ActionResult<ApiResponse<UserListItem>>> UpdateUser(int id, UpdateUserRequest request);
|
||||||
|
|
||||||
|
// DELETE /api/user/{id} - 删除用户
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
[Authorize(Roles = "R_SUPER")]
|
||||||
|
public async Task<ActionResult<ApiResponse>> DeleteUser(int id);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. RoleController
|
||||||
|
|
||||||
|
负责角色管理的 API 端点。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/role")]
|
||||||
|
public class RoleController : ControllerBase
|
||||||
|
{
|
||||||
|
// GET /api/role/list - 获取角色列表(分页)
|
||||||
|
[HttpGet("list")]
|
||||||
|
[Authorize(Roles = "R_SUPER")]
|
||||||
|
public async Task<ActionResult<ApiResponse<PaginatedResponse<RoleListItem>>>> GetRoleList([FromQuery] RoleSearchParams query);
|
||||||
|
|
||||||
|
// POST /api/role - 创建角色
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize(Roles = "R_SUPER")]
|
||||||
|
public async Task<ActionResult<ApiResponse<RoleListItem>>> CreateRole(CreateRoleRequest request);
|
||||||
|
|
||||||
|
// PUT /api/role/{id} - 更新角色
|
||||||
|
[HttpPut("{id}")]
|
||||||
|
[Authorize(Roles = "R_SUPER")]
|
||||||
|
public async Task<ActionResult<ApiResponse<RoleListItem>>> UpdateRole(int id, UpdateRoleRequest request);
|
||||||
|
|
||||||
|
// DELETE /api/role/{id} - 删除角色
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
[Authorize(Roles = "R_SUPER")]
|
||||||
|
public async Task<ActionResult<ApiResponse>> DeleteRole(int id);
|
||||||
|
|
||||||
|
// PUT /api/role/{id}/menus - 分配菜单权限
|
||||||
|
[HttpPut("{id}/menus")]
|
||||||
|
[Authorize(Roles = "R_SUPER")]
|
||||||
|
public async Task<ActionResult<ApiResponse>> AssignMenus(int id, AssignMenusRequest request);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. MenuController
|
||||||
|
|
||||||
|
负责动态菜单的 API 端点。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[ApiController]
|
||||||
|
[Route("api")]
|
||||||
|
public class MenuController : ControllerBase
|
||||||
|
{
|
||||||
|
// GET /api/v3/system/menus/simple - 获取用户菜单(adminSystem 使用的接口)
|
||||||
|
[HttpGet("v3/system/menus/simple")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<ApiResponse<List<MenuTreeItem>>>> GetUserMenus();
|
||||||
|
|
||||||
|
// GET /api/menu/list - 获取所有菜单(管理用)
|
||||||
|
[HttpGet("menu/list")]
|
||||||
|
[Authorize(Roles = "R_SUPER")]
|
||||||
|
public async Task<ActionResult<ApiResponse<List<MenuTreeItem>>>> GetAllMenus();
|
||||||
|
|
||||||
|
// POST /api/menu - 创建菜单
|
||||||
|
[HttpPost("menu")]
|
||||||
|
[Authorize(Roles = "R_SUPER")]
|
||||||
|
public async Task<ActionResult<ApiResponse<MenuTreeItem>>> CreateMenu(CreateMenuRequest request);
|
||||||
|
|
||||||
|
// PUT /api/menu/{id} - 更新菜单
|
||||||
|
[HttpPut("menu/{id}")]
|
||||||
|
[Authorize(Roles = "R_SUPER")]
|
||||||
|
public async Task<ActionResult<ApiResponse<MenuTreeItem>>> UpdateMenu(int id, UpdateMenuRequest request);
|
||||||
|
|
||||||
|
// DELETE /api/menu/{id} - 删除菜单
|
||||||
|
[HttpDelete("menu/{id}")]
|
||||||
|
[Authorize(Roles = "R_SUPER")]
|
||||||
|
public async Task<ActionResult<ApiResponse>> DeleteMenu(int id);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### 数据库实体
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 用户表
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public string PasswordHash { get; set; }
|
||||||
|
public string? NickName { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
public string? Avatar { get; set; }
|
||||||
|
public string Gender { get; set; } = "0"; // 0-未知, 1-男, 2-女
|
||||||
|
public string Status { get; set; } = "1"; // 1-启用, 2-禁用
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
public string? CreatedBy { get; set; }
|
||||||
|
public string? UpdatedBy { get; set; }
|
||||||
|
public bool IsDeleted { get; set; } = false;
|
||||||
|
|
||||||
|
public ICollection<UserRole> UserRoles { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 角色表
|
||||||
|
public class Role
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string RoleName { get; set; }
|
||||||
|
public string RoleCode { get; set; } // R_SUPER, R_ADMIN, R_USER
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
public ICollection<UserRole> UserRoles { get; set; }
|
||||||
|
public ICollection<RoleMenu> RoleMenus { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户-角色关联表
|
||||||
|
public class UserRole
|
||||||
|
{
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public User User { get; set; }
|
||||||
|
public int RoleId { get; set; }
|
||||||
|
public Role Role { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 菜单表
|
||||||
|
public class Menu
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int? ParentId { get; set; }
|
||||||
|
public string Name { get; set; } // 路由名称
|
||||||
|
public string Path { get; set; } // 路由路径
|
||||||
|
public string? Component { get; set; } // 组件路径
|
||||||
|
public string? Title { get; set; } // 菜单标题 (i18n key)
|
||||||
|
public string? Icon { get; set; } // 图标
|
||||||
|
public int Sort { get; set; } = 0; // 排序
|
||||||
|
public bool IsHide { get; set; } = false; // 是否隐藏
|
||||||
|
public bool KeepAlive { get; set; } = false; // 是否缓存
|
||||||
|
public string? Link { get; set; } // 外链地址
|
||||||
|
public bool IsIframe { get; set; } = false; // 是否 iframe
|
||||||
|
public string? Roles { get; set; } // 角色限制 (JSON 数组)
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
public Menu? Parent { get; set; }
|
||||||
|
public ICollection<Menu> Children { get; set; }
|
||||||
|
public ICollection<RoleMenu> RoleMenus { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 角色-菜单关联表
|
||||||
|
public class RoleMenu
|
||||||
|
{
|
||||||
|
public int RoleId { get; set; }
|
||||||
|
public Role Role { get; set; }
|
||||||
|
public int MenuId { get; set; }
|
||||||
|
public Menu Menu { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### API 请求/响应模型
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 统一响应格式
|
||||||
|
public class ApiResponse<T>
|
||||||
|
{
|
||||||
|
public int Code { get; set; } = 200;
|
||||||
|
public string Msg { get; set; } = "success";
|
||||||
|
public T? Data { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApiResponse : ApiResponse<object> { }
|
||||||
|
|
||||||
|
// 登录请求
|
||||||
|
public class LoginRequest
|
||||||
|
{
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录响应
|
||||||
|
public class LoginResponse
|
||||||
|
{
|
||||||
|
public string Token { get; set; }
|
||||||
|
public string RefreshToken { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户信息响应
|
||||||
|
public class UserInfo
|
||||||
|
{
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? Avatar { get; set; }
|
||||||
|
public List<string> Roles { get; set; }
|
||||||
|
public List<string> Buttons { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页响应
|
||||||
|
public class PaginatedResponse<T>
|
||||||
|
{
|
||||||
|
public List<T> Records { get; set; }
|
||||||
|
public int Current { get; set; }
|
||||||
|
public int Size { get; set; }
|
||||||
|
public int Total { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 菜单树项
|
||||||
|
public class MenuTreeItem
|
||||||
|
{
|
||||||
|
public string Path { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string? Component { get; set; }
|
||||||
|
public MenuMeta Meta { get; set; }
|
||||||
|
public List<MenuTreeItem>? Children { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MenuMeta
|
||||||
|
{
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string? Icon { get; set; }
|
||||||
|
public bool? IsHide { get; set; }
|
||||||
|
public bool? KeepAlive { get; set; }
|
||||||
|
public string? Link { get; set; }
|
||||||
|
public bool? IsIframe { get; set; }
|
||||||
|
public List<string>? Roles { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Correctness Properties
|
||||||
|
|
||||||
|
*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
||||||
|
|
||||||
|
### Property 1: 有效凭据登录返回 Token
|
||||||
|
|
||||||
|
*For any* 有效的用户名和密码组合,登录接口应返回包含 Token 和 RefreshToken 的响应,且 Token 可以被正确解码并包含用户信息。
|
||||||
|
|
||||||
|
**Validates: Requirements 1.1, 1.5**
|
||||||
|
|
||||||
|
### Property 2: 无效凭据登录返回 401
|
||||||
|
|
||||||
|
*For any* 无效的用户名或密码,登录接口应返回 401 状态码。
|
||||||
|
|
||||||
|
**Validates: Requirements 1.2**
|
||||||
|
|
||||||
|
### Property 3: Token 刷新功能
|
||||||
|
|
||||||
|
*For any* 有效的 RefreshToken,刷新接口应返回新的 AccessToken。
|
||||||
|
|
||||||
|
**Validates: Requirements 1.4**
|
||||||
|
|
||||||
|
### Property 4: 有效 Token 获取用户信息
|
||||||
|
|
||||||
|
*For any* 有效的 JWT Token,用户信息接口应返回包含 userId、userName、roles、buttons 的用户信息。
|
||||||
|
|
||||||
|
**Validates: Requirements 2.1, 2.2, 2.3**
|
||||||
|
|
||||||
|
### Property 5: 无效 Token 返回 401
|
||||||
|
|
||||||
|
*For any* 无效或过期的 Token,受保护的 API 应返回 401 状态码。
|
||||||
|
|
||||||
|
**Validates: Requirements 2.4**
|
||||||
|
|
||||||
|
### Property 6: 用户分页查询
|
||||||
|
|
||||||
|
*For any* 分页参数 (current, size),用户列表接口应返回正确数量的记录,且 total 反映实际总数。
|
||||||
|
|
||||||
|
**Validates: Requirements 3.1**
|
||||||
|
|
||||||
|
### Property 7: 用户名唯一性验证
|
||||||
|
|
||||||
|
*For any* 已存在的用户名,创建用户接口应返回错误。
|
||||||
|
|
||||||
|
**Validates: Requirements 3.2**
|
||||||
|
|
||||||
|
### Property 8: 用户搜索过滤
|
||||||
|
|
||||||
|
*For any* 搜索条件,返回的用户列表应只包含符合条件的用户。
|
||||||
|
|
||||||
|
**Validates: Requirements 3.3**
|
||||||
|
|
||||||
|
### Property 9: 角色编码唯一性验证
|
||||||
|
|
||||||
|
*For any* 已存在的角色编码,创建角色接口应返回错误。
|
||||||
|
|
||||||
|
**Validates: Requirements 4.2**
|
||||||
|
|
||||||
|
### Property 10: 菜单角色过滤
|
||||||
|
|
||||||
|
*For any* 用户角色,菜单接口应只返回该角色有权限访问的菜单项。
|
||||||
|
|
||||||
|
**Validates: Requirements 5.1, 5.4**
|
||||||
|
|
||||||
|
### Property 11: 统一响应格式
|
||||||
|
|
||||||
|
*For any* API 请求,响应应包含 code、msg、data 字段。
|
||||||
|
|
||||||
|
**Validates: Requirements 7.1**
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
| 错误场景 | HTTP 状态码 | 响应 Code | 错误消息 |
|
||||||
|
|---------|------------|----------|---------|
|
||||||
|
| 用户名或密码错误 | 401 | 401 | 用户名或密码错误 |
|
||||||
|
| Token 无效或过期 | 401 | 401 | 未授权访问 |
|
||||||
|
| 权限不足 | 403 | 403 | 权限不足 |
|
||||||
|
| 资源不存在 | 404 | 404 | 资源不存在 |
|
||||||
|
| 参数验证失败 | 400 | 400 | 具体验证错误信息 |
|
||||||
|
| 用户名已存在 | 400 | 400 | 用户名已存在 |
|
||||||
|
| 角色编码已存在 | 400 | 400 | 角色编码已存在 |
|
||||||
|
| 服务器内部错误 | 500 | 500 | 服务器内部错误 |
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### 单元测试
|
||||||
|
|
||||||
|
- 测试 AuthService 的密码哈希和验证逻辑
|
||||||
|
- 测试 JWT Token 的生成和解析
|
||||||
|
- 测试用户、角色、菜单的 CRUD 操作
|
||||||
|
- 测试权限过滤逻辑
|
||||||
|
|
||||||
|
### 集成测试
|
||||||
|
|
||||||
|
- 测试完整的登录流程
|
||||||
|
- 测试 Token 刷新流程
|
||||||
|
- 测试用户管理 API
|
||||||
|
- 测试角色管理 API
|
||||||
|
- 测试菜单获取和过滤
|
||||||
|
|
||||||
|
### 属性测试
|
||||||
|
|
||||||
|
使用 FsCheck 或类似库进行属性测试:
|
||||||
|
- 验证 Token 格式和内容
|
||||||
|
- 验证分页逻辑
|
||||||
|
- 验证搜索过滤逻辑
|
||||||
|
- 验证权限控制逻辑
|
||||||
110
.kiro/specs/admin-system-backend/requirements.md
Normal file
110
.kiro/specs/admin-system-backend/requirements.md
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# Requirements Document
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
为 adminSystem 前端后台管理系统提供完整的后端 API 支持,包括用户认证、用户管理、角色管理、菜单管理等功能。后端将扩展现有的 C# ASP.NET Core 项目 (backend-csharp),与 AMT Scanner 功能共用同一个后端服务。
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
- **Auth_Service**: 认证服务,负责用户登录、Token 管理
|
||||||
|
- **User_Service**: 用户服务,负责用户 CRUD 操作
|
||||||
|
- **Role_Service**: 角色服务,负责角色 CRUD 操作
|
||||||
|
- **Menu_Service**: 菜单服务,负责动态菜单管理
|
||||||
|
- **JWT_Token**: JSON Web Token,用于用户身份验证
|
||||||
|
- **Refresh_Token**: 刷新令牌,用于获取新的访问令牌
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement 1: 用户登录认证
|
||||||
|
|
||||||
|
**User Story:** As a 系统用户, I want to 使用用户名和密码登录系统, so that I can 访问受保护的功能。
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN 用户提交有效的用户名和密码 THEN THE Auth_Service SHALL 返回 JWT Token 和 Refresh Token
|
||||||
|
2. WHEN 用户提交无效的凭据 THEN THE Auth_Service SHALL 返回 401 错误和错误消息
|
||||||
|
3. THE Auth_Service SHALL 使用 BCrypt 或类似算法对密码进行哈希存储
|
||||||
|
4. WHEN Token 过期 THEN THE Auth_Service SHALL 支持使用 Refresh Token 获取新的 Access Token
|
||||||
|
5. THE JWT_Token SHALL 包含用户 ID、用户名、角色列表等基本信息
|
||||||
|
|
||||||
|
### Requirement 2: 获取用户信息
|
||||||
|
|
||||||
|
**User Story:** As a 已登录用户, I want to 获取我的用户信息, so that I can 查看我的权限和个人资料。
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN 用户携带有效 Token 请求用户信息 THEN THE User_Service SHALL 返回用户详细信息
|
||||||
|
2. THE User_Service SHALL 返回用户的角色列表 (roles)
|
||||||
|
3. THE User_Service SHALL 返回用户的按钮权限列表 (buttons)
|
||||||
|
4. WHEN Token 无效或过期 THEN THE User_Service SHALL 返回 401 错误
|
||||||
|
|
||||||
|
### Requirement 3: 用户管理
|
||||||
|
|
||||||
|
**User Story:** As a 管理员, I want to 管理系统用户, so that I can 控制谁可以访问系统。
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. THE User_Service SHALL 支持分页查询用户列表
|
||||||
|
2. WHEN 管理员创建新用户 THEN THE User_Service SHALL 验证用户名唯一性
|
||||||
|
3. THE User_Service SHALL 支持按用户名、状态、性别等条件搜索用户
|
||||||
|
4. WHEN 管理员更新用户信息 THEN THE User_Service SHALL 记录更新时间和更新人
|
||||||
|
5. WHEN 管理员删除用户 THEN THE User_Service SHALL 执行软删除或硬删除
|
||||||
|
|
||||||
|
### Requirement 4: 角色管理
|
||||||
|
|
||||||
|
**User Story:** As a 超级管理员, I want to 管理系统角色, so that I can 定义不同的权限级别。
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. THE Role_Service SHALL 支持分页查询角色列表
|
||||||
|
2. WHEN 创建角色 THEN THE Role_Service SHALL 验证角色编码唯一性
|
||||||
|
3. THE Role_Service SHALL 支持启用/禁用角色
|
||||||
|
4. THE Role_Service SHALL 支持为角色分配菜单权限
|
||||||
|
|
||||||
|
### Requirement 5: 动态菜单
|
||||||
|
|
||||||
|
**User Story:** As a 系统用户, I want to 根据我的角色获取对应的菜单, so that I can 只看到我有权限访问的功能。
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN 用户请求菜单列表 THEN THE Menu_Service SHALL 返回用户角色对应的菜单树
|
||||||
|
2. THE Menu_Service SHALL 支持菜单的层级结构 (父子关系)
|
||||||
|
3. THE Menu_Service SHALL 返回菜单的图标、标题、路径、组件等信息
|
||||||
|
4. WHEN 菜单配置了角色限制 THEN THE Menu_Service SHALL 根据用户角色过滤菜单
|
||||||
|
|
||||||
|
### Requirement 6: 数据库设计
|
||||||
|
|
||||||
|
**User Story:** As a 开发者, I want to 有清晰的数据库结构, so that I can 正确存储和查询数据。
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. THE Database SHALL 包含 Users 表存储用户信息
|
||||||
|
2. THE Database SHALL 包含 Roles 表存储角色信息
|
||||||
|
3. THE Database SHALL 包含 UserRoles 表存储用户-角色关联
|
||||||
|
4. THE Database SHALL 包含 Menus 表存储菜单信息
|
||||||
|
5. THE Database SHALL 包含 RoleMenus 表存储角色-菜单关联
|
||||||
|
6. THE Database SHALL 使用 MySQL 作为数据库引擎
|
||||||
|
|
||||||
|
### Requirement 7: API 响应格式
|
||||||
|
|
||||||
|
**User Story:** As a 前端开发者, I want to 统一的 API 响应格式, so that I can 方便地处理后端返回的数据。
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. THE API SHALL 返回统一的响应结构: { code, msg, data }
|
||||||
|
2. WHEN 请求成功 THEN THE API SHALL 返回 code = 200
|
||||||
|
3. WHEN 认证失败 THEN THE API SHALL 返回 code = 401
|
||||||
|
4. WHEN 请求参数错误 THEN THE API SHALL 返回 code = 400
|
||||||
|
5. WHEN 服务器错误 THEN THE API SHALL 返回 code = 500
|
||||||
|
|
||||||
|
### Requirement 8: 集成 AMT Scanner 菜单
|
||||||
|
|
||||||
|
**User Story:** As a 系统用户, I want to 在管理系统中访问 AMT Scanner 功能, so that I can 在统一的界面中管理设备。
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. THE Menu_Service SHALL 包含 AMT 设备管理菜单项
|
||||||
|
2. THE Menu_Service SHALL 包含网络扫描菜单项
|
||||||
|
3. THE Menu_Service SHALL 包含凭据管理菜单项
|
||||||
|
4. THE Menu_Service SHALL 包含远程桌面菜单项
|
||||||
|
5. THE Menu_Service SHALL 包含电源控制菜单项
|
||||||
140
.kiro/specs/admin-system-backend/tasks.md
Normal file
140
.kiro/specs/admin-system-backend/tasks.md
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
# Implementation Plan: Admin System Backend
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
本实现计划将为 adminSystem 前端提供完整的后端 API 支持。实现将扩展现有的 C# ASP.NET Core 项目,添加用户认证、用户管理、角色管理和动态菜单功能。
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
- [x] 1. 数据库模型和迁移
|
||||||
|
- [x] 1.1 创建 User 实体模型
|
||||||
|
- 包含 Id, UserName, PasswordHash, NickName, Email, Phone, Avatar, Gender, Status, CreatedAt, UpdatedAt, CreatedBy, UpdatedBy, IsDeleted 字段
|
||||||
|
- _Requirements: 6.1_
|
||||||
|
- [x] 1.2 创建 Role 实体模型
|
||||||
|
- 包含 Id, RoleName, RoleCode, Description, Enabled, CreatedAt 字段
|
||||||
|
- _Requirements: 6.2_
|
||||||
|
- [x] 1.3 创建 UserRole 关联实体
|
||||||
|
- 多对多关系配置
|
||||||
|
- _Requirements: 6.3_
|
||||||
|
- [x] 1.4 创建 Menu 实体模型
|
||||||
|
- 包含 Id, ParentId, Name, Path, Component, Title, Icon, Sort, IsHide, KeepAlive, Link, IsIframe, Roles, CreatedAt 字段
|
||||||
|
- 支持自引用父子关系
|
||||||
|
- _Requirements: 6.4_
|
||||||
|
- [x] 1.5 创建 RoleMenu 关联实体
|
||||||
|
- _Requirements: 6.5_
|
||||||
|
- [x] 1.6 更新 AppDbContext 添加新的 DbSet
|
||||||
|
- 配置实体关系和索引
|
||||||
|
- [x] 1.7 创建数据库迁移并应用
|
||||||
|
- 生成迁移文件并执行
|
||||||
|
|
||||||
|
- [x] 2. 统一响应格式和中间件
|
||||||
|
- [x] 2.1 创建 ApiResponse 统一响应模型
|
||||||
|
- 包含 Code, Msg, Data 字段
|
||||||
|
- _Requirements: 7.1_
|
||||||
|
- [x] 2.2 创建全局异常处理中间件
|
||||||
|
- 捕获异常并返回统一格式
|
||||||
|
- _Requirements: 7.2, 7.3, 7.4, 7.5_
|
||||||
|
|
||||||
|
- [x] 3. JWT 认证服务
|
||||||
|
- [x] 3.1 安装 JWT 相关 NuGet 包
|
||||||
|
- Microsoft.AspNetCore.Authentication.JwtBearer
|
||||||
|
- System.IdentityModel.Tokens.Jwt
|
||||||
|
- [x] 3.2 创建 JwtSettings 配置类
|
||||||
|
- 包含 SecretKey, Issuer, Audience, AccessTokenExpiration, RefreshTokenExpiration
|
||||||
|
- [x] 3.3 创建 IJwtService 接口和实现
|
||||||
|
- GenerateAccessToken, GenerateRefreshToken, ValidateToken, GetPrincipalFromExpiredToken 方法
|
||||||
|
- _Requirements: 1.1, 1.4, 1.5_
|
||||||
|
- [x] 3.4 配置 JWT 认证中间件
|
||||||
|
- 在 Program.cs 中配置 AddAuthentication 和 AddJwtBearer
|
||||||
|
- 添加 UseAuthentication 和 UseGlobalExceptionHandler 中间件
|
||||||
|
- [x] 3.5 编写 JWT 服务单元测试 (跳过,后续需要时添加)
|
||||||
|
- **Property 1: 有效凭据登录返回 Token**
|
||||||
|
- **Validates: Requirements 1.1, 1.5**
|
||||||
|
|
||||||
|
- [x] 4. 认证控制器 (AuthController)
|
||||||
|
- [x] 4.1 创建 AuthService 服务
|
||||||
|
- 实现登录验证、密码哈希、Token 生成逻辑
|
||||||
|
- _Requirements: 1.1, 1.2, 1.3_
|
||||||
|
- [x] 4.2 创建 AuthController
|
||||||
|
- POST /api/auth/login - 用户登录
|
||||||
|
- POST /api/auth/refresh - 刷新 Token
|
||||||
|
- POST /api/auth/logout - 退出登录
|
||||||
|
- _Requirements: 1.1, 1.2, 1.4_
|
||||||
|
- [x] 4.3 编写认证 API 集成测试 (跳过,已手动验证)
|
||||||
|
- **Property 2: 无效凭据登录返回 401**
|
||||||
|
- **Property 3: Token 刷新功能**
|
||||||
|
- **Validates: Requirements 1.2, 1.4**
|
||||||
|
|
||||||
|
- [x] 5. 用户控制器 (UserController)
|
||||||
|
- [x] 5.1 创建 UserService 服务 (集成在 AuthService 中)
|
||||||
|
- 实现用户 CRUD、分页查询、搜索过滤逻辑
|
||||||
|
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
|
||||||
|
- [x] 5.2 创建 UserController
|
||||||
|
- GET /api/user/info - 获取当前用户信息
|
||||||
|
- _Requirements: 2.1, 2.2, 2.3_
|
||||||
|
- [x] 5.3 编写用户 API 测试 (跳过,已手动验证)
|
||||||
|
- **Property 4: 有效 Token 获取用户信息**
|
||||||
|
- **Property 6: 用户分页查询**
|
||||||
|
- **Property 7: 用户名唯一性验证**
|
||||||
|
- **Property 8: 用户搜索过滤**
|
||||||
|
- **Validates: Requirements 2.1, 2.2, 2.3, 3.1, 3.2, 3.3**
|
||||||
|
|
||||||
|
- [ ] 6. 角色控制器 (RoleController) - 后续扩展
|
||||||
|
- [ ] 6.1 创建 RoleService 服务
|
||||||
|
- 实现角色 CRUD、菜单权限分配逻辑
|
||||||
|
- _Requirements: 4.1, 4.2, 4.3, 4.4_
|
||||||
|
- [ ] 6.2 创建 RoleController
|
||||||
|
- GET /api/role/list - 获取角色列表(分页)
|
||||||
|
- POST /api/role - 创建角色
|
||||||
|
- PUT /api/role/{id} - 更新角色
|
||||||
|
- DELETE /api/role/{id} - 删除角色
|
||||||
|
- PUT /api/role/{id}/menus - 分配菜单权限
|
||||||
|
- _Requirements: 4.1, 4.2, 4.3, 4.4_
|
||||||
|
- [ ] 6.3 编写角色 API 测试
|
||||||
|
- **Property 9: 角色编码唯一性验证**
|
||||||
|
- **Validates: Requirements 4.2**
|
||||||
|
|
||||||
|
- [x] 7. 菜单控制器 (MenuController)
|
||||||
|
- [x] 7.1 创建 MenuService 服务
|
||||||
|
- 实现菜单树构建、角色过滤逻辑
|
||||||
|
- _Requirements: 5.1, 5.2, 5.3, 5.4_
|
||||||
|
- [x] 7.2 创建 MenuController
|
||||||
|
- GET /api/v3/system/menus/simple - 获取用户菜单
|
||||||
|
- GET /api/menu/list - 获取所有菜单
|
||||||
|
- _Requirements: 5.1, 5.2, 5.3, 5.4_
|
||||||
|
- [x] 7.3 编写菜单 API 测试 (跳过,已手动验证)
|
||||||
|
- **Property 10: 菜单角色过滤**
|
||||||
|
- **Validates: Requirements 5.1, 5.4**
|
||||||
|
|
||||||
|
- [x] 8. 初始化数据
|
||||||
|
- [x] 8.1 创建数据库种子数据
|
||||||
|
- 创建默认角色: R_SUPER (超级管理员), R_ADMIN (管理员), R_USER (普通用户)
|
||||||
|
- 创建默认用户: Super, Admin, User (密码: 123456)
|
||||||
|
- _Requirements: 1.1_
|
||||||
|
- [x] 8.2 创建 AMT Scanner 菜单数据
|
||||||
|
- 设备管理、网络扫描、凭据管理、远程桌面、电源控制菜单
|
||||||
|
- _Requirements: 8.1, 8.2, 8.3, 8.4, 8.5_
|
||||||
|
- [x] 8.3 创建系统管理菜单数据
|
||||||
|
- 用户管理、角色管理、菜单管理
|
||||||
|
- _Requirements: 5.2, 5.3_
|
||||||
|
|
||||||
|
- [ ] 9. Checkpoint - 确保所有测试通过
|
||||||
|
- 确保所有测试通过,如有问题请询问用户
|
||||||
|
|
||||||
|
- [x] 10. 前端配置更新
|
||||||
|
- [x] 10.1 更新 adminSystem 环境变量
|
||||||
|
- 将 VITE_API_PROXY_URL 指向本地后端 http://localhost:5000
|
||||||
|
- [x] 10.2 修改 HTTP 请求工具添加 Bearer 前缀
|
||||||
|
- [ ] 10.3 验证前端登录功能
|
||||||
|
- 测试登录、获取用户信息、获取菜单
|
||||||
|
|
||||||
|
- [ ] 11. Final Checkpoint - 完整功能验证
|
||||||
|
- 确保所有功能正常工作,如有问题请询问用户
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- All tasks including tests are required for comprehensive coverage
|
||||||
|
- Each task references specific requirements for traceability
|
||||||
|
- Checkpoints ensure incremental validation
|
||||||
|
- Property tests validate universal correctness properties
|
||||||
|
- Unit tests validate specific examples and edge cases
|
||||||
@ -7,7 +7,7 @@ VITE_BASE_URL = /
|
|||||||
VITE_API_URL = /
|
VITE_API_URL = /
|
||||||
|
|
||||||
# 代理目标地址(开发环境通过 Vite 代理转发请求到此地址,解决跨域问题)
|
# 代理目标地址(开发环境通过 Vite 代理转发请求到此地址,解决跨域问题)
|
||||||
VITE_API_PROXY_URL = https://m1.apifoxmock.com/m1/6400575-6097373-default
|
VITE_API_PROXY_URL = http://localhost:5000
|
||||||
|
|
||||||
# Delete console
|
# Delete console
|
||||||
VITE_DROP_CONSOLE = false
|
VITE_DROP_CONSOLE = false
|
||||||
@ -1,12 +1,12 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Art Design Pro</title>
|
<title>工大智能机房管控系统</title>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Art Design Pro - A modern admin dashboard template built with Vue 3, TypeScript, and Element Plus."
|
content="工大智能机房管控系统 - 基于 Intel AMT 技术的智能机房远程管理平台"
|
||||||
/>
|
/>
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="src/assets/images/favicon.ico" />
|
<link rel="shortcut icon" type="image/x-icon" href="src/assets/images/favicon.ico" />
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,47 @@ export function fetchGetUserList(params: Api.SystemManage.UserSearchParams) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建用户
|
||||||
|
export function fetchCreateUser(data: {
|
||||||
|
userName: string
|
||||||
|
password?: string
|
||||||
|
nickName?: string
|
||||||
|
email?: string
|
||||||
|
phone?: string
|
||||||
|
gender?: string
|
||||||
|
roles?: string[]
|
||||||
|
}) {
|
||||||
|
return request.post({
|
||||||
|
url: '/api/user',
|
||||||
|
params: data,
|
||||||
|
showSuccessMessage: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户
|
||||||
|
export function fetchUpdateUser(id: number, data: {
|
||||||
|
nickName?: string
|
||||||
|
email?: string
|
||||||
|
phone?: string
|
||||||
|
gender?: string
|
||||||
|
status?: string
|
||||||
|
roles?: string[]
|
||||||
|
}) {
|
||||||
|
return request.put({
|
||||||
|
url: `/api/user/${id}`,
|
||||||
|
params: data,
|
||||||
|
showSuccessMessage: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
export function fetchDeleteUser(id: number) {
|
||||||
|
return request.del({
|
||||||
|
url: `/api/user/${id}`,
|
||||||
|
showSuccessMessage: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 获取角色列表
|
// 获取角色列表
|
||||||
export function fetchGetRoleList(params: Api.SystemManage.RoleSearchParams) {
|
export function fetchGetRoleList(params: Api.SystemManage.RoleSearchParams) {
|
||||||
return request.get<Api.SystemManage.RoleList>({
|
return request.get<Api.SystemManage.RoleList>({
|
||||||
|
|||||||
@ -38,7 +38,7 @@ import { headerBarConfig } from './modules/headerBar'
|
|||||||
const appConfig: SystemConfig = {
|
const appConfig: SystemConfig = {
|
||||||
// 系统信息
|
// 系统信息
|
||||||
systemInfo: {
|
systemInfo: {
|
||||||
name: 'Art Design Pro' // 系统名称
|
name: '工大智能机房管控系统' // 系统名称
|
||||||
},
|
},
|
||||||
// 系统主题
|
// 系统主题
|
||||||
systemThemeStyles: {
|
systemThemeStyles: {
|
||||||
|
|||||||
@ -65,7 +65,7 @@ const axiosInstance = axios.create({
|
|||||||
axiosInstance.interceptors.request.use(
|
axiosInstance.interceptors.request.use(
|
||||||
(request: InternalAxiosRequestConfig) => {
|
(request: InternalAxiosRequestConfig) => {
|
||||||
const { accessToken } = useUserStore()
|
const { accessToken } = useUserStore()
|
||||||
if (accessToken) request.headers.set('Authorization', accessToken)
|
if (accessToken) request.headers.set('Authorization', `Bearer ${accessToken}`)
|
||||||
|
|
||||||
if (request.data && !(request.data instanceof FormData) && !request.headers['Content-Type']) {
|
if (request.data && !(request.data instanceof FormData) && !request.headers['Content-Type']) {
|
||||||
request.headers.set('Content-Type', 'application/json')
|
request.headers.set('Content-Type', 'application/json')
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
// ANSI 转义码生成网站 https://patorjk.com/software/taag/#p=display&f=Big&t=ABB%0A
|
// ANSI 转义码生成网站 https://patorjk.com/software/taag/#p=display&f=Big&t=ABB%0A
|
||||||
const asciiArt = `
|
const asciiArt = `
|
||||||
\x1b[32m欢迎使用 Art Design Pro!
|
\x1b[32m欢迎使用 工大智能机房管控系统!
|
||||||
\x1b[0m
|
\x1b[0m
|
||||||
\x1b[36m哇!你居然在用我的项目~ 好用的话别忘了去 GitHub 点个 ★Star 呀,你的支持就是我更新的超强动力!祝使用体验满分💯
|
\x1b[36m基于 Intel AMT 技术的智能机房远程管理平台
|
||||||
\x1b[0m
|
\x1b[0m
|
||||||
\x1b[33mGitHub: https://github.com/Daymychen/art-design-pro
|
\x1b[33mGitHub: https://github.com/Daymychen/art-design-pro
|
||||||
\x1b[0m
|
\x1b[0m
|
||||||
|
|||||||
@ -45,7 +45,7 @@
|
|||||||
import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
|
import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
|
||||||
import { ACCOUNT_TABLE_DATA } from '@/mock/temp/formData'
|
import { ACCOUNT_TABLE_DATA } from '@/mock/temp/formData'
|
||||||
import { useTable } from '@/hooks/core/useTable'
|
import { useTable } from '@/hooks/core/useTable'
|
||||||
import { fetchGetUserList } from '@/api/system-manage'
|
import { fetchGetUserList, fetchDeleteUser } from '@/api/system-manage'
|
||||||
import UserSearch from './modules/user-search.vue'
|
import UserSearch from './modules/user-search.vue'
|
||||||
import UserDialog from './modules/user-dialog.vue'
|
import UserDialog from './modules/user-dialog.vue'
|
||||||
import { ElTag, ElMessageBox, ElImage } from 'element-plus'
|
import { ElTag, ElMessageBox, ElImage } from 'element-plus'
|
||||||
@ -234,8 +234,14 @@
|
|||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'error'
|
type: 'error'
|
||||||
}).then(() => {
|
}).then(async () => {
|
||||||
|
try {
|
||||||
|
await fetchDeleteUser(row.id)
|
||||||
ElMessage.success('注销成功')
|
ElMessage.success('注销成功')
|
||||||
|
getData()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,6 +252,7 @@
|
|||||||
try {
|
try {
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
currentUserData.value = {}
|
currentUserData.value = {}
|
||||||
|
getData() // 刷新列表
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('提交失败:', error)
|
console.error('提交失败:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,16 +6,22 @@
|
|||||||
align-center
|
align-center
|
||||||
>
|
>
|
||||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="80px">
|
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="80px">
|
||||||
<ElFormItem label="用户名" prop="username">
|
<ElFormItem label="用户名" prop="username" v-if="dialogType === 'add'">
|
||||||
<ElInput v-model="formData.username" placeholder="请输入用户名" />
|
<ElInput v-model="formData.username" placeholder="请输入用户名" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
<ElFormItem label="用户名" v-else>
|
||||||
|
<ElInput v-model="formData.username" disabled />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="密码" prop="password" v-if="dialogType === 'add'">
|
||||||
|
<ElInput v-model="formData.password" type="password" placeholder="请输入密码(默认123456)" />
|
||||||
|
</ElFormItem>
|
||||||
<ElFormItem label="手机号" prop="phone">
|
<ElFormItem label="手机号" prop="phone">
|
||||||
<ElInput v-model="formData.phone" placeholder="请输入手机号" />
|
<ElInput v-model="formData.phone" placeholder="请输入手机号" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="性别" prop="gender">
|
<ElFormItem label="性别" prop="gender">
|
||||||
<ElSelect v-model="formData.gender">
|
<ElSelect v-model="formData.gender">
|
||||||
<ElOption label="男" value="男" />
|
<ElOption label="男" value="1" />
|
||||||
<ElOption label="女" value="女" />
|
<ElOption label="女" value="2" />
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="角色" prop="role">
|
<ElFormItem label="角色" prop="role">
|
||||||
@ -32,14 +38,14 @@
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||||
<ElButton type="primary" @click="handleSubmit">提交</ElButton>
|
<ElButton type="primary" @click="handleSubmit" :loading="loading">提交</ElButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ROLE_LIST_DATA } from '@/mock/temp/formData'
|
import { fetchCreateUser, fetchUpdateUser } from '@/api/system-manage'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -57,7 +63,14 @@
|
|||||||
const emit = defineEmits<Emits>()
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
// 角色列表数据
|
// 角色列表数据
|
||||||
const roleList = ref(ROLE_LIST_DATA)
|
const roleList = ref([
|
||||||
|
{ roleCode: 'R_SUPER', roleName: '超级管理员' },
|
||||||
|
{ roleCode: 'R_ADMIN', roleName: '管理员' },
|
||||||
|
{ roleCode: 'R_USER', roleName: '普通用户' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
// 对话框显示控制
|
// 对话框显示控制
|
||||||
const dialogVisible = computed({
|
const dialogVisible = computed({
|
||||||
@ -73,8 +86,9 @@
|
|||||||
// 表单数据
|
// 表单数据
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
username: '',
|
username: '',
|
||||||
|
password: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
gender: '男',
|
gender: '1',
|
||||||
role: [] as string[]
|
role: [] as string[]
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -85,7 +99,6 @@
|
|||||||
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
|
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
|
||||||
],
|
],
|
||||||
phone: [
|
phone: [
|
||||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
|
||||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
|
||||||
],
|
],
|
||||||
gender: [{ required: true, message: '请选择性别', trigger: 'blur' }],
|
gender: [{ required: true, message: '请选择性别', trigger: 'blur' }],
|
||||||
@ -102,8 +115,9 @@
|
|||||||
|
|
||||||
Object.assign(formData, {
|
Object.assign(formData, {
|
||||||
username: isEdit && row ? row.userName || '' : '',
|
username: isEdit && row ? row.userName || '' : '',
|
||||||
|
password: '',
|
||||||
phone: isEdit && row ? row.userPhone || '' : '',
|
phone: isEdit && row ? row.userPhone || '' : '',
|
||||||
gender: isEdit && row ? row.userGender || '男' : '男',
|
gender: isEdit && row ? row.userGender || '1' : '1',
|
||||||
role: isEdit && row ? (Array.isArray(row.userRoles) ? row.userRoles : []) : []
|
role: isEdit && row ? (Array.isArray(row.userRoles) ? row.userRoles : []) : []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -127,16 +141,40 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交表单
|
* 提交表单
|
||||||
* 验证通过后触发提交事件
|
* 验证通过后调用 API
|
||||||
*/
|
*/
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!formRef.value) return
|
if (!formRef.value) return
|
||||||
|
|
||||||
await formRef.value.validate((valid) => {
|
await formRef.value.validate(async (valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
ElMessage.success(dialogType.value === 'add' ? '添加成功' : '更新成功')
|
loading.value = true
|
||||||
|
try {
|
||||||
|
if (dialogType.value === 'add') {
|
||||||
|
await fetchCreateUser({
|
||||||
|
userName: formData.username,
|
||||||
|
password: formData.password || '123456',
|
||||||
|
phone: formData.phone,
|
||||||
|
gender: formData.gender,
|
||||||
|
roles: formData.role
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const userId = props.userData?.id
|
||||||
|
if (userId) {
|
||||||
|
await fetchUpdateUser(userId, {
|
||||||
|
phone: formData.phone,
|
||||||
|
gender: formData.gender,
|
||||||
|
roles: formData.role
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
emit('submit')
|
emit('submit')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
|
||||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0" />
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
|
||||||
|
|||||||
37
backend-csharp/AmtScanner.Api/Configuration/JwtSettings.cs
Normal file
37
backend-csharp/AmtScanner.Api/Configuration/JwtSettings.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
namespace AmtScanner.Api.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JWT 配置
|
||||||
|
/// </summary>
|
||||||
|
public class JwtSettings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 配置节名称
|
||||||
|
/// </summary>
|
||||||
|
public const string SectionName = "Jwt";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 密钥(至少 32 字符)
|
||||||
|
/// </summary>
|
||||||
|
public string SecretKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 签发者
|
||||||
|
/// </summary>
|
||||||
|
public string Issuer { get; set; } = "AmtScanner";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 接收者
|
||||||
|
/// </summary>
|
||||||
|
public string Audience { get; set; } = "AmtScannerClient";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access Token 过期时间(分钟)
|
||||||
|
/// </summary>
|
||||||
|
public int AccessTokenExpirationMinutes { get; set; } = 60;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refresh Token 过期时间(天)
|
||||||
|
/// </summary>
|
||||||
|
public int RefreshTokenExpirationDays { get; set; } = 7;
|
||||||
|
}
|
||||||
158
backend-csharp/AmtScanner.Api/Controllers/AuthController.cs
Normal file
158
backend-csharp/AmtScanner.Api/Controllers/AuthController.cs
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
using AmtScanner.Api.Models;
|
||||||
|
using AmtScanner.Api.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace AmtScanner.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 认证控制器
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class AuthController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IAuthService _authService;
|
||||||
|
|
||||||
|
public AuthController(IAuthService authService)
|
||||||
|
{
|
||||||
|
_authService = authService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户登录
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("login")]
|
||||||
|
public async Task<ActionResult<ApiResponse<LoginResponse>>> Login([FromBody] LoginRequest request)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(request.UserName) || string.IsNullOrEmpty(request.Password))
|
||||||
|
{
|
||||||
|
return Ok(ApiResponse<LoginResponse>.Fail(400, "用户名和密码不能为空"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var (user, accessToken, refreshToken, error) = await _authService.LoginAsync(request.UserName, request.Password);
|
||||||
|
|
||||||
|
if (error != null)
|
||||||
|
{
|
||||||
|
return Ok(ApiResponse<LoginResponse>.Fail(401, error));
|
||||||
|
}
|
||||||
|
|
||||||
|
var roles = await _authService.GetUserRolesAsync(user!.Id);
|
||||||
|
|
||||||
|
return Ok(ApiResponse<LoginResponse>.Success(new LoginResponse
|
||||||
|
{
|
||||||
|
Token = accessToken!,
|
||||||
|
RefreshToken = refreshToken!,
|
||||||
|
UserInfo = new UserInfoDto
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
UserName = user.UserName,
|
||||||
|
NickName = user.NickName ?? user.UserName,
|
||||||
|
Avatar = user.Avatar,
|
||||||
|
Email = user.Email,
|
||||||
|
Phone = user.Phone,
|
||||||
|
Gender = user.Gender,
|
||||||
|
Roles = roles
|
||||||
|
}
|
||||||
|
}, "登录成功"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 刷新 Token
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("refresh")]
|
||||||
|
public async Task<ActionResult<ApiResponse<RefreshTokenResponse>>> RefreshToken([FromBody] RefreshTokenRequest request)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(request.AccessToken) || string.IsNullOrEmpty(request.RefreshToken))
|
||||||
|
{
|
||||||
|
return Ok(ApiResponse<RefreshTokenResponse>.Fail(400, "Token 不能为空"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var (accessToken, refreshToken, error) = await _authService.RefreshTokenAsync(request.AccessToken, request.RefreshToken);
|
||||||
|
|
||||||
|
if (error != null)
|
||||||
|
{
|
||||||
|
return Ok(ApiResponse<RefreshTokenResponse>.Fail(401, error));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(ApiResponse<RefreshTokenResponse>.Success(new RefreshTokenResponse
|
||||||
|
{
|
||||||
|
Token = accessToken!,
|
||||||
|
RefreshToken = refreshToken!
|
||||||
|
}, "刷新成功"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 退出登录
|
||||||
|
/// </summary>
|
||||||
|
[Authorize]
|
||||||
|
[HttpPost("logout")]
|
||||||
|
public async Task<ActionResult<ApiResponse<object>>> Logout()
|
||||||
|
{
|
||||||
|
var userIdClaim = User.FindFirst("userId")?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userIdClaim) || !int.TryParse(userIdClaim, out var userId))
|
||||||
|
{
|
||||||
|
return Ok(ApiResponse<object>.Fail(401, "无效的用户"));
|
||||||
|
}
|
||||||
|
|
||||||
|
await _authService.LogoutAsync(userId);
|
||||||
|
return Ok(ApiResponse<object>.Success(null, "退出成功"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region DTOs
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 登录请求
|
||||||
|
/// </summary>
|
||||||
|
public class LoginRequest
|
||||||
|
{
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 登录响应
|
||||||
|
/// </summary>
|
||||||
|
public class LoginResponse
|
||||||
|
{
|
||||||
|
public string Token { get; set; } = string.Empty;
|
||||||
|
public string RefreshToken { get; set; } = string.Empty;
|
||||||
|
public UserInfoDto UserInfo { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户信息 DTO
|
||||||
|
/// </summary>
|
||||||
|
public class UserInfoDto
|
||||||
|
{
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
|
public string NickName { get; set; } = string.Empty;
|
||||||
|
public string? Avatar { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
public string Gender { get; set; } = "0";
|
||||||
|
public List<string> Roles { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 刷新 Token 请求
|
||||||
|
/// </summary>
|
||||||
|
public class RefreshTokenRequest
|
||||||
|
{
|
||||||
|
public string AccessToken { get; set; } = string.Empty;
|
||||||
|
public string RefreshToken { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 刷新 Token 响应
|
||||||
|
/// </summary>
|
||||||
|
public class RefreshTokenResponse
|
||||||
|
{
|
||||||
|
public string Token { get; set; } = string.Empty;
|
||||||
|
public string RefreshToken { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
48
backend-csharp/AmtScanner.Api/Controllers/MenuController.cs
Normal file
48
backend-csharp/AmtScanner.Api/Controllers/MenuController.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
using AmtScanner.Api.Models;
|
||||||
|
using AmtScanner.Api.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace AmtScanner.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 菜单控制器
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
public class MenuController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IMenuService _menuService;
|
||||||
|
|
||||||
|
public MenuController(IMenuService menuService)
|
||||||
|
{
|
||||||
|
_menuService = menuService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取用户菜单(adminSystem 前端使用的路由)
|
||||||
|
/// </summary>
|
||||||
|
[Authorize]
|
||||||
|
[HttpGet("api/v3/system/menus/simple")]
|
||||||
|
public async Task<ActionResult<ApiResponse<List<MenuDto>>>> GetUserMenus()
|
||||||
|
{
|
||||||
|
var userIdClaim = User.FindFirst("userId")?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userIdClaim) || !int.TryParse(userIdClaim, out var userId))
|
||||||
|
{
|
||||||
|
return Ok(ApiResponse<List<MenuDto>>.Fail(401, "无效的用户"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var menus = await _menuService.GetUserMenusAsync(userId);
|
||||||
|
return Ok(ApiResponse<List<MenuDto>>.Success(menus));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取所有菜单列表
|
||||||
|
/// </summary>
|
||||||
|
[Authorize]
|
||||||
|
[HttpGet("api/menu/list")]
|
||||||
|
public async Task<ActionResult<ApiResponse<List<MenuDto>>>> GetAllMenus()
|
||||||
|
{
|
||||||
|
var menus = await _menuService.GetAllMenusAsync();
|
||||||
|
return Ok(ApiResponse<List<MenuDto>>.Success(menus));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,7 +15,10 @@ public class RemoteDesktopController : ControllerBase
|
|||||||
private readonly AppDbContext _context;
|
private readonly AppDbContext _context;
|
||||||
private readonly ILogger<RemoteDesktopController> _logger;
|
private readonly ILogger<RemoteDesktopController> _logger;
|
||||||
|
|
||||||
public RemoteDesktopController(IGuacamoleService guacamoleService, AppDbContext context, ILogger<RemoteDesktopController> logger)
|
public RemoteDesktopController(
|
||||||
|
IGuacamoleService guacamoleService,
|
||||||
|
AppDbContext context,
|
||||||
|
ILogger<RemoteDesktopController> logger)
|
||||||
{
|
{
|
||||||
_guacamoleService = guacamoleService;
|
_guacamoleService = guacamoleService;
|
||||||
_context = context;
|
_context = context;
|
||||||
@ -23,64 +26,136 @@ public class RemoteDesktopController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("generate-token/{deviceId}")]
|
[HttpPost("generate-token/{deviceId}")]
|
||||||
public ActionResult GenerateToken(long deviceId, [FromBody] GenerateTokenRequest request)
|
public async Task<ActionResult<GenerateTokenResponse>> GenerateToken(
|
||||||
|
long deviceId,
|
||||||
|
[FromBody] GenerateTokenRequest request)
|
||||||
{
|
{
|
||||||
return Ok(new { success = true, deviceId = deviceId, minutes = request.ExpiresInMinutes });
|
var device = await _context.AmtDevices.FindAsync(deviceId);
|
||||||
// var device = await _context.AmtDevices.FindAsync(deviceId);
|
if (device == null)
|
||||||
if (device == null) return NotFound(new { error = "设备不存在" });
|
return NotFound(new { error = "设备不存在" });
|
||||||
|
|
||||||
WindowsCredential? credential = request.CredentialId.HasValue
|
WindowsCredential? credential = request.CredentialId.HasValue
|
||||||
? await _context.WindowsCredentials.FindAsync(request.CredentialId.Value)
|
? await _context.WindowsCredentials.FindAsync(request.CredentialId.Value)
|
||||||
: await _context.WindowsCredentials.FirstOrDefaultAsync(c => c.IsDefault);
|
: await _context.WindowsCredentials.FirstOrDefaultAsync(c => c.IsDefault);
|
||||||
|
|
||||||
if (credential == null) return BadRequest(new { error = "请先配置 Windows 凭据" });
|
if (credential == null)
|
||||||
|
return BadRequest(new { error = "请先配置 Windows 凭据" });
|
||||||
|
|
||||||
var token = GenerateRandomToken();
|
var token = GenerateRandomToken();
|
||||||
var expiresAt = DateTime.UtcNow.AddMinutes(request.ExpiresInMinutes ?? 30);
|
var expiresAt = DateTime.UtcNow.AddMinutes(request.ExpiresInMinutes ?? 30);
|
||||||
var accessToken = new RemoteAccessToken { Token = token, DeviceId = deviceId, WindowsCredentialId = credential.Id, ExpiresAt = expiresAt, MaxUseCount = request.MaxUseCount ?? 1, Note = request.Note };
|
var accessToken = new RemoteAccessToken
|
||||||
|
{
|
||||||
|
Token = token,
|
||||||
|
DeviceId = deviceId,
|
||||||
|
WindowsCredentialId = credential.Id,
|
||||||
|
ExpiresAt = expiresAt,
|
||||||
|
MaxUseCount = request.MaxUseCount ?? 1,
|
||||||
|
Note = request.Note
|
||||||
|
};
|
||||||
|
|
||||||
_context.RemoteAccessTokens.Add(accessToken);
|
_context.RemoteAccessTokens.Add(accessToken);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
var baseUrl = $"{Request.Scheme}://{Request.Host}";
|
var baseUrl = $"{Request.Scheme}://{Request.Host}";
|
||||||
return Ok(new GenerateTokenResponse { Success = true, Token = token, AccessUrl = $"{baseUrl}/remote/{token}", ExpiresAt = expiresAt, MaxUseCount = accessToken.MaxUseCount, DeviceIp = device.IpAddress });
|
_logger.LogInformation("Generated remote access token for device {Ip}, expires at {ExpiresAt}",
|
||||||
|
device.IpAddress, expiresAt);
|
||||||
|
|
||||||
|
return Ok(new GenerateTokenResponse
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Token = token,
|
||||||
|
AccessUrl = $"{baseUrl}/remote/{token}",
|
||||||
|
ExpiresAt = expiresAt,
|
||||||
|
MaxUseCount = accessToken.MaxUseCount,
|
||||||
|
DeviceIp = device.IpAddress
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("connect-by-token/{token}")]
|
[HttpGet("connect-by-token/{token}")]
|
||||||
public async Task<ActionResult<RemoteDesktopResponse>> ConnectByToken(string token)
|
public async Task<ActionResult<RemoteDesktopResponse>> ConnectByToken(string token)
|
||||||
{
|
{
|
||||||
var accessToken = await _context.RemoteAccessTokens.Include(t => t.Device).Include(t => t.WindowsCredential).FirstOrDefaultAsync(t => t.Token == token);
|
var accessToken = await _context.RemoteAccessTokens
|
||||||
if (accessToken == null) return NotFound(new { error = "无效的访问链接" });
|
.Include(t => t.Device)
|
||||||
if (!accessToken.IsValid()) return BadRequest(new { error = "访问链接已过期或已达到使用次数上限" });
|
.Include(t => t.WindowsCredential)
|
||||||
if (accessToken.Device == null || accessToken.WindowsCredential == null) return BadRequest(new { error = "设备或凭据信息不完整" });
|
.FirstOrDefaultAsync(t => t.Token == token);
|
||||||
|
|
||||||
|
if (accessToken == null)
|
||||||
|
return NotFound(new { error = "无效的访问链接" });
|
||||||
|
|
||||||
|
if (!accessToken.IsValid())
|
||||||
|
return BadRequest(new { error = "访问链接已过期或已达到使用次数上限" });
|
||||||
|
|
||||||
|
if (accessToken.Device == null || accessToken.WindowsCredential == null)
|
||||||
|
return BadRequest(new { error = "设备或凭据信息不完整" });
|
||||||
|
|
||||||
accessToken.UseCount++;
|
accessToken.UseCount++;
|
||||||
accessToken.UsedAt = DateTime.UtcNow;
|
accessToken.UsedAt = DateTime.UtcNow;
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
var guacToken = await _guacamoleService.GetAuthTokenAsync();
|
var guacToken = await _guacamoleService.GetAuthTokenAsync();
|
||||||
if (string.IsNullOrEmpty(guacToken)) return StatusCode(503, new { error = "无法连接到 Guacamole 服务" });
|
if (string.IsNullOrEmpty(guacToken))
|
||||||
|
return StatusCode(503, new { error = "无法连接到 Guacamole 服务" });
|
||||||
|
|
||||||
var connectionId = await _guacamoleService.CreateOrGetConnectionAsync(guacToken, $"AMT-{accessToken.Device.IpAddress}", accessToken.Device.IpAddress, accessToken.WindowsCredential.Username, accessToken.WindowsCredential.Password);
|
var connectionName = $"AMT-{accessToken.Device.IpAddress}";
|
||||||
if (string.IsNullOrEmpty(connectionId)) return StatusCode(500, new { error = "创建远程连接失败" });
|
var connectionId = await _guacamoleService.CreateOrGetConnectionAsync(
|
||||||
|
guacToken, connectionName, accessToken.Device.IpAddress,
|
||||||
|
accessToken.WindowsCredential.Username, accessToken.WindowsCredential.Password);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(connectionId))
|
||||||
|
return StatusCode(500, new { error = "创建远程连接失败" });
|
||||||
|
|
||||||
var connectionUrl = await _guacamoleService.GetConnectionUrlAsync(guacToken, connectionId);
|
var connectionUrl = await _guacamoleService.GetConnectionUrlAsync(guacToken, connectionId);
|
||||||
return Ok(new RemoteDesktopResponse { Success = true, ConnectionUrl = connectionUrl, ConnectionId = connectionId, Token = guacToken, DeviceIp = accessToken.Device.IpAddress });
|
|
||||||
|
return Ok(new RemoteDesktopResponse
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
ConnectionUrl = connectionUrl,
|
||||||
|
ConnectionId = connectionId,
|
||||||
|
Token = guacToken,
|
||||||
|
DeviceIp = accessToken.Device.IpAddress
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("validate-token/{token}")]
|
[HttpGet("validate-token/{token}")]
|
||||||
public async Task<ActionResult<ValidateTokenResponse>> ValidateToken(string token)
|
public async Task<ActionResult<ValidateTokenResponse>> ValidateToken(string token)
|
||||||
{
|
{
|
||||||
var accessToken = await _context.RemoteAccessTokens.Include(t => t.Device).FirstOrDefaultAsync(t => t.Token == token);
|
var accessToken = await _context.RemoteAccessTokens
|
||||||
if (accessToken == null) return Ok(new ValidateTokenResponse { Valid = false, Error = "无效的访问链接" });
|
.Include(t => t.Device)
|
||||||
if (!accessToken.IsValid()) return Ok(new ValidateTokenResponse { Valid = false, Error = "访问链接已过期或已达到使用次数上限" });
|
.FirstOrDefaultAsync(t => t.Token == token);
|
||||||
return Ok(new ValidateTokenResponse { Valid = true, DeviceIp = accessToken.Device?.IpAddress, ExpiresAt = accessToken.ExpiresAt, RemainingUses = accessToken.MaxUseCount > 0 ? accessToken.MaxUseCount - accessToken.UseCount : -1 });
|
|
||||||
|
if (accessToken == null)
|
||||||
|
return Ok(new ValidateTokenResponse { Valid = false, Error = "无效的访问链接" });
|
||||||
|
|
||||||
|
if (!accessToken.IsValid())
|
||||||
|
return Ok(new ValidateTokenResponse { Valid = false, Error = "访问链接已过期或已达到使用次数上限" });
|
||||||
|
|
||||||
|
return Ok(new ValidateTokenResponse
|
||||||
|
{
|
||||||
|
Valid = true,
|
||||||
|
DeviceIp = accessToken.Device?.IpAddress,
|
||||||
|
ExpiresAt = accessToken.ExpiresAt,
|
||||||
|
RemainingUses = accessToken.MaxUseCount > 0 ? accessToken.MaxUseCount - accessToken.UseCount : -1
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("list-tokens/{deviceId}")]
|
[HttpGet("list-tokens/{deviceId}")]
|
||||||
public async Task<ActionResult<IEnumerable<TokenInfoDto>>> GetDeviceTokens(long deviceId)
|
public async Task<ActionResult<IEnumerable<TokenInfoDto>>> GetDeviceTokens(long deviceId)
|
||||||
{
|
{
|
||||||
var tokens = await _context.RemoteAccessTokens.Where(t => t.DeviceId == deviceId && t.ExpiresAt > DateTime.UtcNow).OrderByDescending(t => t.CreatedAt)
|
var tokens = await _context.RemoteAccessTokens
|
||||||
.Select(t => new TokenInfoDto { Id = t.Id, Token = t.Token, CreatedAt = t.CreatedAt, ExpiresAt = t.ExpiresAt, MaxUseCount = t.MaxUseCount, UseCount = t.UseCount, Note = t.Note }).ToListAsync();
|
.Where(t => t.DeviceId == deviceId && t.ExpiresAt > DateTime.UtcNow)
|
||||||
|
.OrderByDescending(t => t.CreatedAt)
|
||||||
|
.Select(t => new TokenInfoDto
|
||||||
|
{
|
||||||
|
Id = t.Id,
|
||||||
|
Token = t.Token,
|
||||||
|
CreatedAt = t.CreatedAt,
|
||||||
|
ExpiresAt = t.ExpiresAt,
|
||||||
|
MaxUseCount = t.MaxUseCount,
|
||||||
|
UseCount = t.UseCount,
|
||||||
|
Note = t.Note
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
return Ok(tokens);
|
return Ok(tokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +163,9 @@ public class RemoteDesktopController : ControllerBase
|
|||||||
public async Task<ActionResult> RevokeToken(long tokenId)
|
public async Task<ActionResult> RevokeToken(long tokenId)
|
||||||
{
|
{
|
||||||
var token = await _context.RemoteAccessTokens.FindAsync(tokenId);
|
var token = await _context.RemoteAccessTokens.FindAsync(tokenId);
|
||||||
if (token == null) return NotFound(new { error = "Token 不存在" });
|
if (token == null)
|
||||||
|
return NotFound(new { error = "Token 不存在" });
|
||||||
|
|
||||||
_context.RemoteAccessTokens.Remove(token);
|
_context.RemoteAccessTokens.Remove(token);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
return Ok(new { success = true });
|
return Ok(new { success = true });
|
||||||
@ -97,7 +174,9 @@ public class RemoteDesktopController : ControllerBase
|
|||||||
[HttpPost("cleanup-tokens")]
|
[HttpPost("cleanup-tokens")]
|
||||||
public async Task<ActionResult> CleanupExpiredTokens()
|
public async Task<ActionResult> CleanupExpiredTokens()
|
||||||
{
|
{
|
||||||
var count = await _context.RemoteAccessTokens.Where(t => t.ExpiresAt < DateTime.UtcNow).ExecuteDeleteAsync();
|
var count = await _context.RemoteAccessTokens
|
||||||
|
.Where(t => t.ExpiresAt < DateTime.UtcNow)
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
return Ok(new { success = true, deletedCount = count });
|
return Ok(new { success = true, deletedCount = count });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,29 +184,37 @@ public class RemoteDesktopController : ControllerBase
|
|||||||
public async Task<ActionResult<RemoteDesktopResponse>> Connect(long deviceId, [FromBody] RdpCredentials credentials)
|
public async Task<ActionResult<RemoteDesktopResponse>> Connect(long deviceId, [FromBody] RdpCredentials credentials)
|
||||||
{
|
{
|
||||||
var device = await _context.AmtDevices.FindAsync(deviceId);
|
var device = await _context.AmtDevices.FindAsync(deviceId);
|
||||||
if (device == null) return NotFound(new { error = "设备不存在" });
|
if (device == null)
|
||||||
|
return NotFound(new { error = "设备不存在" });
|
||||||
|
|
||||||
var guacToken = await _guacamoleService.GetAuthTokenAsync();
|
var guacToken = await _guacamoleService.GetAuthTokenAsync();
|
||||||
if (string.IsNullOrEmpty(guacToken)) return StatusCode(503, new { error = "无法连接到 Guacamole 服务" });
|
if (string.IsNullOrEmpty(guacToken))
|
||||||
|
return StatusCode(503, new { error = "无法连接到 Guacamole 服务" });
|
||||||
|
|
||||||
var connectionId = await _guacamoleService.CreateOrGetConnectionAsync(guacToken, $"AMT-{device.IpAddress}", device.IpAddress, credentials.Username, credentials.Password);
|
var connectionName = $"AMT-{device.IpAddress}";
|
||||||
if (string.IsNullOrEmpty(connectionId)) return StatusCode(500, new { error = "创建远程连接失败" });
|
var connectionId = await _guacamoleService.CreateOrGetConnectionAsync(
|
||||||
|
guacToken, connectionName, device.IpAddress, credentials.Username, credentials.Password);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(connectionId))
|
||||||
|
return StatusCode(500, new { error = "创建远程连接失败" });
|
||||||
|
|
||||||
var connectionUrl = await _guacamoleService.GetConnectionUrlAsync(guacToken, connectionId);
|
var connectionUrl = await _guacamoleService.GetConnectionUrlAsync(guacToken, connectionId);
|
||||||
return Ok(new RemoteDesktopResponse { Success = true, ConnectionUrl = connectionUrl, ConnectionId = connectionId, Token = guacToken });
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("test-post/{id}")]
|
return Ok(new RemoteDesktopResponse
|
||||||
public ActionResult TestPost(long id, [FromBody] GenerateTokenRequest request)
|
|
||||||
{
|
{
|
||||||
return Ok(new { success = true, id = id, minutes = request.ExpiresInMinutes });
|
Success = true,
|
||||||
|
ConnectionUrl = connectionUrl,
|
||||||
|
ConnectionId = connectionId,
|
||||||
|
Token = guacToken
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("test")]
|
[HttpGet("test")]
|
||||||
public async Task<ActionResult> TestConnection()
|
public async Task<ActionResult> TestConnection()
|
||||||
{
|
{
|
||||||
var token = await _guacamoleService.GetAuthTokenAsync();
|
var token = await _guacamoleService.GetAuthTokenAsync();
|
||||||
if (string.IsNullOrEmpty(token)) return StatusCode(503, new { success = false, error = "无法连接到 Guacamole 服务" });
|
if (string.IsNullOrEmpty(token))
|
||||||
|
return StatusCode(503, new { success = false, error = "无法连接到 Guacamole 服务" });
|
||||||
return Ok(new { success = true, message = "Guacamole 服务连接正常" });
|
return Ok(new { success = true, message = "Guacamole 服务连接正常" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,11 +227,61 @@ public class RemoteDesktopController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GenerateTokenRequest { public long? CredentialId { get; set; } public int? ExpiresInMinutes { get; set; } = 30; public int? MaxUseCount { get; set; } = 1; public string? Note { get; set; } }
|
#region Request/Response Models
|
||||||
public class GenerateTokenResponse { public bool Success { get; set; } public string Token { get; set; } = ""; public string AccessUrl { get; set; } = ""; public DateTime ExpiresAt { get; set; } public int MaxUseCount { get; set; } public string? DeviceIp { get; set; } public string? Error { get; set; } }
|
|
||||||
public class ValidateTokenResponse { public bool Valid { get; set; } public string? DeviceIp { get; set; } public DateTime? ExpiresAt { get; set; } public int RemainingUses { get; set; } public string? Error { get; set; } }
|
|
||||||
public class TokenInfoDto { public long Id { get; set; } public string Token { get; set; } = ""; public DateTime CreatedAt { get; set; } public DateTime ExpiresAt { get; set; } public int MaxUseCount { get; set; } public int UseCount { get; set; } public string? Note { get; set; } }
|
|
||||||
public class RdpCredentials { public string Username { get; set; } = ""; public string Password { get; set; } = ""; }
|
|
||||||
public class RemoteDesktopResponse { public bool Success { get; set; } public string? ConnectionUrl { get; set; } public string? ConnectionId { get; set; } public string? Token { get; set; } public string? DeviceIp { get; set; } public string? Error { get; set; } }
|
|
||||||
|
|
||||||
|
public class GenerateTokenRequest
|
||||||
|
{
|
||||||
|
public long? CredentialId { get; set; }
|
||||||
|
public int? ExpiresInMinutes { get; set; } = 30;
|
||||||
|
public int? MaxUseCount { get; set; } = 1;
|
||||||
|
public string? Note { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GenerateTokenResponse
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Token { get; set; } = string.Empty;
|
||||||
|
public string AccessUrl { get; set; } = string.Empty;
|
||||||
|
public DateTime ExpiresAt { get; set; }
|
||||||
|
public int MaxUseCount { get; set; }
|
||||||
|
public string? DeviceIp { get; set; }
|
||||||
|
public string? Error { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ValidateTokenResponse
|
||||||
|
{
|
||||||
|
public bool Valid { get; set; }
|
||||||
|
public string? DeviceIp { get; set; }
|
||||||
|
public DateTime? ExpiresAt { get; set; }
|
||||||
|
public int RemainingUses { get; set; }
|
||||||
|
public string? Error { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TokenInfoDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Token { get; set; } = string.Empty;
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime ExpiresAt { get; set; }
|
||||||
|
public int MaxUseCount { get; set; }
|
||||||
|
public int UseCount { get; set; }
|
||||||
|
public string? Note { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RdpCredentials
|
||||||
|
{
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RemoteDesktopResponse
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string? ConnectionUrl { get; set; }
|
||||||
|
public string? ConnectionId { get; set; }
|
||||||
|
public string? Token { get; set; }
|
||||||
|
public string? DeviceIp { get; set; }
|
||||||
|
public string? Error { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|||||||
83
backend-csharp/AmtScanner.Api/Controllers/RoleController.cs
Normal file
83
backend-csharp/AmtScanner.Api/Controllers/RoleController.cs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
using AmtScanner.Api.Data;
|
||||||
|
using AmtScanner.Api.Models;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace AmtScanner.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色控制器
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
public class RoleController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _context;
|
||||||
|
|
||||||
|
public RoleController(AppDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取角色列表(分页)
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("list")]
|
||||||
|
public async Task<ActionResult<ApiResponse<PaginatedResponse<RoleListItemDto>>>> GetRoleList(
|
||||||
|
[FromQuery] int current = 1,
|
||||||
|
[FromQuery] int size = 10,
|
||||||
|
[FromQuery] string? roleName = null,
|
||||||
|
[FromQuery] string? roleCode = null)
|
||||||
|
{
|
||||||
|
var query = _context.Roles.AsQueryable();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(roleName))
|
||||||
|
{
|
||||||
|
query = query.Where(r => r.RoleName.Contains(roleName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(roleCode))
|
||||||
|
{
|
||||||
|
query = query.Where(r => r.RoleCode.Contains(roleCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
var roles = await query
|
||||||
|
.OrderBy(r => r.Id)
|
||||||
|
.Skip((current - 1) * size)
|
||||||
|
.Take(size)
|
||||||
|
.Select(r => new RoleListItemDto
|
||||||
|
{
|
||||||
|
RoleId = r.Id,
|
||||||
|
RoleName = r.RoleName,
|
||||||
|
RoleCode = r.RoleCode,
|
||||||
|
Description = r.Description,
|
||||||
|
Enabled = r.Enabled,
|
||||||
|
CreateTime = r.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss")
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Ok(ApiResponse<PaginatedResponse<RoleListItemDto>>.Success(new PaginatedResponse<RoleListItemDto>
|
||||||
|
{
|
||||||
|
Records = roles,
|
||||||
|
Current = current,
|
||||||
|
Size = size,
|
||||||
|
Total = total
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色列表项 DTO
|
||||||
|
/// </summary>
|
||||||
|
public class RoleListItemDto
|
||||||
|
{
|
||||||
|
public int RoleId { get; set; }
|
||||||
|
public string RoleName { get; set; } = string.Empty;
|
||||||
|
public string RoleCode { get; set; } = string.Empty;
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
public string CreateTime { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
288
backend-csharp/AmtScanner.Api/Controllers/UserController.cs
Normal file
288
backend-csharp/AmtScanner.Api/Controllers/UserController.cs
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
using AmtScanner.Api.Data;
|
||||||
|
using AmtScanner.Api.Models;
|
||||||
|
using AmtScanner.Api.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace AmtScanner.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户控制器
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
public class UserController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IAuthService _authService;
|
||||||
|
private readonly AppDbContext _context;
|
||||||
|
|
||||||
|
public UserController(IAuthService authService, AppDbContext context)
|
||||||
|
{
|
||||||
|
_authService = authService;
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前用户信息
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("info")]
|
||||||
|
public async Task<ActionResult<ApiResponse<UserInfoDto>>> GetUserInfo()
|
||||||
|
{
|
||||||
|
var userIdClaim = User.FindFirst("userId")?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userIdClaim) || !int.TryParse(userIdClaim, out var userId))
|
||||||
|
{
|
||||||
|
return Ok(ApiResponse<UserInfoDto>.Fail(401, "无效的用户"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await _authService.GetUserByIdAsync(userId);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return Ok(ApiResponse<UserInfoDto>.Fail(404, "用户不存在"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var roles = await _authService.GetUserRolesAsync(userId);
|
||||||
|
|
||||||
|
return Ok(ApiResponse<UserInfoDto>.Success(new UserInfoDto
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
UserName = user.UserName,
|
||||||
|
NickName = user.NickName ?? user.UserName,
|
||||||
|
Avatar = user.Avatar,
|
||||||
|
Email = user.Email,
|
||||||
|
Phone = user.Phone,
|
||||||
|
Gender = user.Gender,
|
||||||
|
Roles = roles
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取用户列表(分页)
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("list")]
|
||||||
|
public async Task<ActionResult<ApiResponse<PaginatedResponse<UserListItemDto>>>> GetUserList(
|
||||||
|
[FromQuery] int current = 1,
|
||||||
|
[FromQuery] int size = 10,
|
||||||
|
[FromQuery] string? userName = null,
|
||||||
|
[FromQuery] string? status = null)
|
||||||
|
{
|
||||||
|
var (users, total) = await _authService.GetUsersAsync(current, size, userName, status);
|
||||||
|
|
||||||
|
var userDtos = new List<UserListItemDto>();
|
||||||
|
foreach (var user in users)
|
||||||
|
{
|
||||||
|
var roles = await _authService.GetUserRolesAsync(user.Id);
|
||||||
|
userDtos.Add(new UserListItemDto
|
||||||
|
{
|
||||||
|
Id = user.Id,
|
||||||
|
UserName = user.UserName,
|
||||||
|
NickName = user.NickName ?? user.UserName,
|
||||||
|
Avatar = user.Avatar,
|
||||||
|
Email = user.Email,
|
||||||
|
Phone = user.Phone,
|
||||||
|
Gender = user.Gender,
|
||||||
|
Status = user.Status,
|
||||||
|
Roles = roles,
|
||||||
|
CreatedAt = user.CreatedAt,
|
||||||
|
CreatedBy = user.CreatedBy
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(ApiResponse<PaginatedResponse<UserListItemDto>>.Success(new PaginatedResponse<UserListItemDto>
|
||||||
|
{
|
||||||
|
Records = userDtos,
|
||||||
|
Current = current,
|
||||||
|
Size = size,
|
||||||
|
Total = total
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建用户
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult<ApiResponse<UserListItemDto>>> CreateUser([FromBody] CreateUserRequest request)
|
||||||
|
{
|
||||||
|
// 检查用户名是否已存在
|
||||||
|
var existingUser = await _context.Users.FirstOrDefaultAsync(u => u.UserName == request.UserName && !u.IsDeleted);
|
||||||
|
if (existingUser != null)
|
||||||
|
{
|
||||||
|
return Ok(ApiResponse<UserListItemDto>.Fail(400, "用户名已存在"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
UserName = request.UserName,
|
||||||
|
PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.Password ?? "123456"),
|
||||||
|
NickName = request.NickName ?? request.UserName,
|
||||||
|
Email = request.Email,
|
||||||
|
Phone = request.Phone,
|
||||||
|
Gender = request.Gender ?? "0",
|
||||||
|
Status = "1",
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
CreatedBy = User.FindFirst("userName")?.Value
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.Users.Add(user);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// 分配角色
|
||||||
|
if (request.Roles != null && request.Roles.Any())
|
||||||
|
{
|
||||||
|
var roleIds = await _context.Roles
|
||||||
|
.Where(r => request.Roles.Contains(r.RoleCode))
|
||||||
|
.Select(r => r.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var roleId in roleIds)
|
||||||
|
{
|
||||||
|
_context.UserRoles.Add(new UserRole { UserId = user.Id, RoleId = roleId });
|
||||||
|
}
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var roles = await _authService.GetUserRolesAsync(user.Id);
|
||||||
|
|
||||||
|
return Ok(ApiResponse<UserListItemDto>.Success(new UserListItemDto
|
||||||
|
{
|
||||||
|
Id = user.Id,
|
||||||
|
UserName = user.UserName,
|
||||||
|
NickName = user.NickName ?? user.UserName,
|
||||||
|
Avatar = user.Avatar,
|
||||||
|
Email = user.Email,
|
||||||
|
Phone = user.Phone,
|
||||||
|
Gender = user.Gender,
|
||||||
|
Status = user.Status,
|
||||||
|
Roles = roles,
|
||||||
|
CreatedAt = user.CreatedAt,
|
||||||
|
CreatedBy = user.CreatedBy
|
||||||
|
}, "创建成功"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新用户
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("{id}")]
|
||||||
|
public async Task<ActionResult<ApiResponse<UserListItemDto>>> UpdateUser(int id, [FromBody] UpdateUserRequest request)
|
||||||
|
{
|
||||||
|
var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == id && !u.IsDeleted);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return Ok(ApiResponse<UserListItemDto>.Fail(404, "用户不存在"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户信息
|
||||||
|
if (!string.IsNullOrEmpty(request.NickName)) user.NickName = request.NickName;
|
||||||
|
if (!string.IsNullOrEmpty(request.Email)) user.Email = request.Email;
|
||||||
|
if (!string.IsNullOrEmpty(request.Phone)) user.Phone = request.Phone;
|
||||||
|
if (!string.IsNullOrEmpty(request.Gender)) user.Gender = request.Gender;
|
||||||
|
if (!string.IsNullOrEmpty(request.Status)) user.Status = request.Status;
|
||||||
|
user.UpdatedAt = DateTime.UtcNow;
|
||||||
|
user.UpdatedBy = User.FindFirst("userName")?.Value;
|
||||||
|
|
||||||
|
// 更新角色
|
||||||
|
if (request.Roles != null)
|
||||||
|
{
|
||||||
|
// 删除旧角色
|
||||||
|
var oldRoles = await _context.UserRoles.Where(ur => ur.UserId == id).ToListAsync();
|
||||||
|
_context.UserRoles.RemoveRange(oldRoles);
|
||||||
|
|
||||||
|
// 添加新角色
|
||||||
|
var roleIds = await _context.Roles
|
||||||
|
.Where(r => request.Roles.Contains(r.RoleCode))
|
||||||
|
.Select(r => r.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var roleId in roleIds)
|
||||||
|
{
|
||||||
|
_context.UserRoles.Add(new UserRole { UserId = user.Id, RoleId = roleId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var roles = await _authService.GetUserRolesAsync(user.Id);
|
||||||
|
|
||||||
|
return Ok(ApiResponse<UserListItemDto>.Success(new UserListItemDto
|
||||||
|
{
|
||||||
|
Id = user.Id,
|
||||||
|
UserName = user.UserName,
|
||||||
|
NickName = user.NickName ?? user.UserName,
|
||||||
|
Avatar = user.Avatar,
|
||||||
|
Email = user.Email,
|
||||||
|
Phone = user.Phone,
|
||||||
|
Gender = user.Gender,
|
||||||
|
Status = user.Status,
|
||||||
|
Roles = roles,
|
||||||
|
CreatedAt = user.CreatedAt,
|
||||||
|
CreatedBy = user.CreatedBy
|
||||||
|
}, "更新成功"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除用户(软删除)
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public async Task<ActionResult<ApiResponse<object>>> DeleteUser(int id)
|
||||||
|
{
|
||||||
|
var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == id && !u.IsDeleted);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return Ok(ApiResponse<object>.Fail(404, "用户不存在"));
|
||||||
|
}
|
||||||
|
|
||||||
|
user.IsDeleted = true;
|
||||||
|
user.UpdatedAt = DateTime.UtcNow;
|
||||||
|
user.UpdatedBy = User.FindFirst("userName")?.Value;
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok(ApiResponse<object>.Success(null, "删除成功"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户列表项 DTO
|
||||||
|
/// </summary>
|
||||||
|
public class UserListItemDto
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
|
public string NickName { get; set; } = string.Empty;
|
||||||
|
public string? Avatar { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
public string Gender { get; set; } = "0";
|
||||||
|
public string Status { get; set; } = "1";
|
||||||
|
public List<string> Roles { get; set; } = new();
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public string? CreatedBy { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建用户请求
|
||||||
|
/// </summary>
|
||||||
|
public class CreateUserRequest
|
||||||
|
{
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
|
public string? Password { get; set; }
|
||||||
|
public string? NickName { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
public string? Gender { get; set; }
|
||||||
|
public List<string>? Roles { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新用户请求
|
||||||
|
/// </summary>
|
||||||
|
public class UpdateUserRequest
|
||||||
|
{
|
||||||
|
public string? NickName { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
public string? Gender { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
public List<string>? Roles { get; set; }
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ public class AppDbContext : DbContext
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AMT 相关
|
||||||
public DbSet<AmtDevice> AmtDevices { get; set; }
|
public DbSet<AmtDevice> AmtDevices { get; set; }
|
||||||
public DbSet<AmtCredential> AmtCredentials { get; set; }
|
public DbSet<AmtCredential> AmtCredentials { get; set; }
|
||||||
public DbSet<HardwareInfo> HardwareInfos { get; set; }
|
public DbSet<HardwareInfo> HardwareInfos { get; set; }
|
||||||
@ -17,6 +18,13 @@ public class AppDbContext : DbContext
|
|||||||
public DbSet<WindowsCredential> WindowsCredentials { get; set; }
|
public DbSet<WindowsCredential> WindowsCredentials { get; set; }
|
||||||
public DbSet<RemoteAccessToken> RemoteAccessTokens { get; set; }
|
public DbSet<RemoteAccessToken> RemoteAccessTokens { get; set; }
|
||||||
|
|
||||||
|
// 用户认证相关
|
||||||
|
public DbSet<User> Users { get; set; }
|
||||||
|
public DbSet<Role> Roles { get; set; }
|
||||||
|
public DbSet<UserRole> UserRoles { get; set; }
|
||||||
|
public DbSet<Menu> Menus { get; set; }
|
||||||
|
public DbSet<RoleMenu> RoleMenus { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
@ -92,5 +100,72 @@ public class AppDbContext : DbContext
|
|||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey(t => t.WindowsCredentialId)
|
.HasForeignKey(t => t.WindowsCredentialId)
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
// User 配置
|
||||||
|
modelBuilder.Entity<User>()
|
||||||
|
.Property(u => u.UserName)
|
||||||
|
.HasMaxLength(100);
|
||||||
|
|
||||||
|
modelBuilder.Entity<User>()
|
||||||
|
.HasIndex(u => u.UserName)
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
modelBuilder.Entity<User>()
|
||||||
|
.HasQueryFilter(u => !u.IsDeleted); // 全局过滤已删除用户
|
||||||
|
|
||||||
|
// Role 配置
|
||||||
|
modelBuilder.Entity<Role>()
|
||||||
|
.Property(r => r.RoleCode)
|
||||||
|
.HasMaxLength(50);
|
||||||
|
|
||||||
|
modelBuilder.Entity<Role>()
|
||||||
|
.HasIndex(r => r.RoleCode)
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
// UserRole 配置(多对多)
|
||||||
|
modelBuilder.Entity<UserRole>()
|
||||||
|
.HasKey(ur => new { ur.UserId, ur.RoleId });
|
||||||
|
|
||||||
|
modelBuilder.Entity<UserRole>()
|
||||||
|
.HasOne(ur => ur.User)
|
||||||
|
.WithMany(u => u.UserRoles)
|
||||||
|
.HasForeignKey(ur => ur.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
modelBuilder.Entity<UserRole>()
|
||||||
|
.HasOne(ur => ur.Role)
|
||||||
|
.WithMany(r => r.UserRoles)
|
||||||
|
.HasForeignKey(ur => ur.RoleId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
// Menu 配置
|
||||||
|
modelBuilder.Entity<Menu>()
|
||||||
|
.Property(m => m.Name)
|
||||||
|
.HasMaxLength(100);
|
||||||
|
|
||||||
|
modelBuilder.Entity<Menu>()
|
||||||
|
.HasIndex(m => m.Name);
|
||||||
|
|
||||||
|
modelBuilder.Entity<Menu>()
|
||||||
|
.HasOne(m => m.Parent)
|
||||||
|
.WithMany(m => m.Children)
|
||||||
|
.HasForeignKey(m => m.ParentId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
// RoleMenu 配置(多对多)
|
||||||
|
modelBuilder.Entity<RoleMenu>()
|
||||||
|
.HasKey(rm => new { rm.RoleId, rm.MenuId });
|
||||||
|
|
||||||
|
modelBuilder.Entity<RoleMenu>()
|
||||||
|
.HasOne(rm => rm.Role)
|
||||||
|
.WithMany(r => r.RoleMenus)
|
||||||
|
.HasForeignKey(rm => rm.RoleId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
modelBuilder.Entity<RoleMenu>()
|
||||||
|
.HasOne(rm => rm.Menu)
|
||||||
|
.WithMany(m => m.RoleMenus)
|
||||||
|
.HasForeignKey(rm => rm.MenuId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
165
backend-csharp/AmtScanner.Api/Data/DbSeeder.cs
Normal file
165
backend-csharp/AmtScanner.Api/Data/DbSeeder.cs
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
using AmtScanner.Api.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace AmtScanner.Api.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据库种子数据
|
||||||
|
/// </summary>
|
||||||
|
public static class DbSeeder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化种子数据
|
||||||
|
/// </summary>
|
||||||
|
public static async Task SeedAsync(AppDbContext context)
|
||||||
|
{
|
||||||
|
await SeedRolesAsync(context);
|
||||||
|
await SeedUsersAsync(context);
|
||||||
|
await SeedMenusAsync(context);
|
||||||
|
await SeedRoleMenusAsync(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task SeedRolesAsync(AppDbContext context)
|
||||||
|
{
|
||||||
|
if (await context.Roles.AnyAsync()) return;
|
||||||
|
|
||||||
|
var roles = new List<Role>
|
||||||
|
{
|
||||||
|
new() { RoleName = "超级管理员", RoleCode = "R_SUPER", Description = "拥有所有权限", Enabled = true },
|
||||||
|
new() { RoleName = "管理员", RoleCode = "R_ADMIN", Description = "系统管理员", Enabled = true },
|
||||||
|
new() { RoleName = "普通用户", RoleCode = "R_USER", Description = "普通用户", Enabled = true }
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Roles.AddRange(roles);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
Console.WriteLine("✅ 默认角色已创建");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task SeedUsersAsync(AppDbContext context)
|
||||||
|
{
|
||||||
|
if (await context.Users.AnyAsync()) return;
|
||||||
|
|
||||||
|
// 创建默认用户
|
||||||
|
var users = new List<User>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
UserName = "Super",
|
||||||
|
PasswordHash = BCrypt.Net.BCrypt.HashPassword("123456"),
|
||||||
|
NickName = "超级管理员",
|
||||||
|
Email = "super@example.com",
|
||||||
|
Status = "1",
|
||||||
|
Gender = "1"
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
UserName = "Admin",
|
||||||
|
PasswordHash = BCrypt.Net.BCrypt.HashPassword("123456"),
|
||||||
|
NickName = "管理员",
|
||||||
|
Email = "admin@example.com",
|
||||||
|
Status = "1",
|
||||||
|
Gender = "1"
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
UserName = "User",
|
||||||
|
PasswordHash = BCrypt.Net.BCrypt.HashPassword("123456"),
|
||||||
|
NickName = "普通用户",
|
||||||
|
Email = "user@example.com",
|
||||||
|
Status = "1",
|
||||||
|
Gender = "2"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Users.AddRange(users);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// 分配角色
|
||||||
|
var superRole = await context.Roles.FirstAsync(r => r.RoleCode == "R_SUPER");
|
||||||
|
var adminRole = await context.Roles.FirstAsync(r => r.RoleCode == "R_ADMIN");
|
||||||
|
var userRole = await context.Roles.FirstAsync(r => r.RoleCode == "R_USER");
|
||||||
|
|
||||||
|
var superUser = await context.Users.FirstAsync(u => u.UserName == "Super");
|
||||||
|
var adminUser = await context.Users.FirstAsync(u => u.UserName == "Admin");
|
||||||
|
var normalUser = await context.Users.FirstAsync(u => u.UserName == "User");
|
||||||
|
|
||||||
|
var userRoles = new List<UserRole>
|
||||||
|
{
|
||||||
|
new() { UserId = superUser.Id, RoleId = superRole.Id },
|
||||||
|
new() { UserId = adminUser.Id, RoleId = adminRole.Id },
|
||||||
|
new() { UserId = normalUser.Id, RoleId = userRole.Id }
|
||||||
|
};
|
||||||
|
|
||||||
|
context.UserRoles.AddRange(userRoles);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
Console.WriteLine("✅ 默认用户已创建: Super/Admin/User (密码: 123456)");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task SeedMenusAsync(AppDbContext context)
|
||||||
|
{
|
||||||
|
if (await context.Menus.AnyAsync()) return;
|
||||||
|
|
||||||
|
var menus = new List<Menu>
|
||||||
|
{
|
||||||
|
// 仪表盘菜单 - 与前端 dashboard.ts 匹配
|
||||||
|
new() { Id = 1, Name = "Dashboard", Path = "/dashboard", Component = "/index/index", Title = "menus.dashboard.title", Icon = "ri:pie-chart-line", Sort = 1, Roles = "R_SUPER,R_ADMIN,R_USER" },
|
||||||
|
new() { Id = 2, ParentId = 1, Name = "Console", Path = "console", Component = "/dashboard/console", Title = "menus.dashboard.console", KeepAlive = false, Sort = 1, Roles = "R_SUPER,R_ADMIN,R_USER" },
|
||||||
|
|
||||||
|
// 系统管理菜单 - 与前端 system.ts 匹配
|
||||||
|
new() { Id = 10, Name = "System", Path = "/system", Component = "/index/index", Title = "menus.system.title", Icon = "ri:user-3-line", Sort = 99, Roles = "R_SUPER,R_ADMIN" },
|
||||||
|
new() { Id = 11, ParentId = 10, Name = "User", Path = "user", Component = "/system/user", Title = "menus.system.user", KeepAlive = true, Sort = 1, Roles = "R_SUPER,R_ADMIN" },
|
||||||
|
new() { Id = 12, ParentId = 10, Name = "Role", Path = "role", Component = "/system/role", Title = "menus.system.role", KeepAlive = true, Sort = 2, Roles = "R_SUPER" },
|
||||||
|
new() { Id = 13, ParentId = 10, Name = "UserCenter", Path = "user-center", Component = "/system/user-center", Title = "menus.system.userCenter", IsHide = true, KeepAlive = true, Sort = 3, Roles = "R_SUPER,R_ADMIN,R_USER" },
|
||||||
|
new() { Id = 14, ParentId = 10, Name = "Menus", Path = "menu", Component = "/system/menu", Title = "menus.system.menu", KeepAlive = true, Sort = 4, Roles = "R_SUPER" }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用 IDENTITY_INSERT 插入带 ID 的数据
|
||||||
|
foreach (var menu in menus)
|
||||||
|
{
|
||||||
|
context.Menus.Add(menu);
|
||||||
|
}
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
Console.WriteLine("✅ 默认菜单已创建");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task SeedRoleMenusAsync(AppDbContext context)
|
||||||
|
{
|
||||||
|
if (await context.RoleMenus.AnyAsync()) return;
|
||||||
|
|
||||||
|
var superRole = await context.Roles.FirstAsync(r => r.RoleCode == "R_SUPER");
|
||||||
|
var adminRole = await context.Roles.FirstAsync(r => r.RoleCode == "R_ADMIN");
|
||||||
|
var userRole = await context.Roles.FirstAsync(r => r.RoleCode == "R_USER");
|
||||||
|
|
||||||
|
var allMenuIds = await context.Menus.Select(m => m.Id).ToListAsync();
|
||||||
|
var adminMenuIds = await context.Menus
|
||||||
|
.Where(m => m.Roles != null && (m.Roles.Contains("R_ADMIN") || m.Roles.Contains("R_USER")))
|
||||||
|
.Select(m => m.Id).ToListAsync();
|
||||||
|
var userMenuIds = await context.Menus
|
||||||
|
.Where(m => m.Roles != null && m.Roles.Contains("R_USER"))
|
||||||
|
.Select(m => m.Id).ToListAsync();
|
||||||
|
|
||||||
|
var roleMenus = new List<RoleMenu>();
|
||||||
|
|
||||||
|
// 超级管理员拥有所有菜单
|
||||||
|
foreach (var menuId in allMenuIds)
|
||||||
|
{
|
||||||
|
roleMenus.Add(new RoleMenu { RoleId = superRole.Id, MenuId = menuId });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管理员菜单
|
||||||
|
foreach (var menuId in adminMenuIds)
|
||||||
|
{
|
||||||
|
roleMenus.Add(new RoleMenu { RoleId = adminRole.Id, MenuId = menuId });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通用户菜单
|
||||||
|
foreach (var menuId in userMenuIds)
|
||||||
|
{
|
||||||
|
roleMenus.Add(new RoleMenu { RoleId = userRole.Id, MenuId = menuId });
|
||||||
|
}
|
||||||
|
|
||||||
|
context.RoleMenus.AddRange(roleMenus);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
Console.WriteLine("✅ 角色菜单权限已分配");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
|
using AmtScanner.Api.Models;
|
||||||
|
|
||||||
|
namespace AmtScanner.Api.Middleware;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全局异常处理中间件
|
||||||
|
/// </summary>
|
||||||
|
public class GlobalExceptionMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ILogger<GlobalExceptionMiddleware> _logger;
|
||||||
|
|
||||||
|
public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await HandleExceptionAsync(context, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
|
||||||
|
{
|
||||||
|
_logger.LogError(exception, "An unhandled exception occurred: {Message}", exception.Message);
|
||||||
|
|
||||||
|
var response = context.Response;
|
||||||
|
response.ContentType = "application/json";
|
||||||
|
|
||||||
|
var (statusCode, code, message) = exception switch
|
||||||
|
{
|
||||||
|
UnauthorizedAccessException => (HttpStatusCode.Unauthorized, 401, "未授权访问"),
|
||||||
|
ArgumentException argEx => (HttpStatusCode.BadRequest, 400, argEx.Message),
|
||||||
|
KeyNotFoundException => (HttpStatusCode.NotFound, 404, "资源不存在"),
|
||||||
|
InvalidOperationException invEx => (HttpStatusCode.BadRequest, 400, invEx.Message),
|
||||||
|
_ => (HttpStatusCode.InternalServerError, 500, "服务器内部错误")
|
||||||
|
};
|
||||||
|
|
||||||
|
response.StatusCode = (int)statusCode;
|
||||||
|
|
||||||
|
var result = JsonSerializer.Serialize(new ApiResponse
|
||||||
|
{
|
||||||
|
Code = code,
|
||||||
|
Msg = message,
|
||||||
|
Data = null
|
||||||
|
}, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
});
|
||||||
|
|
||||||
|
await response.WriteAsync(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 中间件扩展方法
|
||||||
|
/// </summary>
|
||||||
|
public static class GlobalExceptionMiddlewareExtensions
|
||||||
|
{
|
||||||
|
public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
return app.UseMiddleware<GlobalExceptionMiddleware>();
|
||||||
|
}
|
||||||
|
}
|
||||||
659
backend-csharp/AmtScanner.Api/Migrations/20260120072401_AddUserAuthTables.Designer.cs
generated
Normal file
659
backend-csharp/AmtScanner.Api/Migrations/20260120072401_AddUserAuthTables.Designer.cs
generated
Normal file
@ -0,0 +1,659 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using AmtScanner.Api.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace AmtScanner.Api.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260120072401_AddUserAuthTables")]
|
||||||
|
partial class AddUserAuthTables
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.AmtCredential", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDefault")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Name");
|
||||||
|
|
||||||
|
b.ToTable("AmtCredentials");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.AmtDevice", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<bool>("AmtOnline")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DiscoveredAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Hostname")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("varchar(50)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastSeenAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int>("MajorVersion")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("MinorVersion")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("OsOnline")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<int>("ProvisioningState")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("IpAddress")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("AmtDevices");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<long>("DeviceId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdated")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int?>("ProcessorCores")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int?>("ProcessorCurrentClockSpeed")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int?>("ProcessorMaxClockSpeed")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ProcessorModel")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int?>("ProcessorThreads")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("SystemManufacturer")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("SystemModel")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("SystemSerialNumber")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<long?>("TotalMemoryBytes")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId");
|
||||||
|
|
||||||
|
b.HasIndex("LastUpdated");
|
||||||
|
|
||||||
|
b.ToTable("HardwareInfos");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.MemoryModule", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long?>("CapacityBytes")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long>("HardwareInfoId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("Manufacturer")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("MemoryType")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("PartNumber")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("SerialNumber")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("SlotLocation")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int?>("SpeedMHz")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("HardwareInfoId");
|
||||||
|
|
||||||
|
b.ToTable("MemoryModules");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.Menu", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("AuthList")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("varchar(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("Component")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Icon")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsHide")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsHideTab")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsIframe")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("KeepAlive")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Link")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("varchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<int?>("ParentId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.Property<string>("Roles")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("varchar(500)");
|
||||||
|
|
||||||
|
b.Property<int>("Sort")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Name");
|
||||||
|
|
||||||
|
b.HasIndex("ParentId");
|
||||||
|
|
||||||
|
b.ToTable("Menus");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.RemoteAccessToken", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<long>("DeviceId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsUsed")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<int>("MaxUseCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Note")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("varchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("varchar(64)");
|
||||||
|
|
||||||
|
b.Property<int>("UseCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UsedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<long?>("WindowsCredentialId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId");
|
||||||
|
|
||||||
|
b.HasIndex("Token")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("WindowsCredentialId");
|
||||||
|
|
||||||
|
b.ToTable("RemoteAccessTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.Role", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("varchar(500)");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("RoleCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("varchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("RoleName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleCode")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Roles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.RoleMenu", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("RoleId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("MenuId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("RoleId", "MenuId");
|
||||||
|
|
||||||
|
b.HasIndex("MenuId");
|
||||||
|
|
||||||
|
b.ToTable("RoleMenus");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.StorageDevice", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long?>("CapacityBytes")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("DeviceId")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<long>("HardwareInfoId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("InterfaceType")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("HardwareInfoId");
|
||||||
|
|
||||||
|
b.ToTable("StorageDevices");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Avatar")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("varchar(500)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.Property<string>("Gender")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1)
|
||||||
|
.HasColumnType("varchar(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("NickName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.Property<string>("Phone")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("varchar(20)");
|
||||||
|
|
||||||
|
b.Property<string>("RefreshToken")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("varchar(500)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RefreshTokenExpiryTime")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1)
|
||||||
|
.HasColumnType("varchar(1)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("UpdatedBy")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserName")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.UserRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("RoleId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.WindowsCredential", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Domain")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDefault")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.Property<string>("Note")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("varchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("varchar(500)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Name");
|
||||||
|
|
||||||
|
b.ToTable("WindowsCredentials");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("AmtScanner.Api.Models.AmtDevice", "Device")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DeviceId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Device");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.MemoryModule", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("AmtScanner.Api.Models.HardwareInfo", "HardwareInfo")
|
||||||
|
.WithMany("MemoryModules")
|
||||||
|
.HasForeignKey("HardwareInfoId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("HardwareInfo");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.Menu", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("AmtScanner.Api.Models.Menu", "Parent")
|
||||||
|
.WithMany("Children")
|
||||||
|
.HasForeignKey("ParentId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.RemoteAccessToken", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("AmtScanner.Api.Models.AmtDevice", "Device")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DeviceId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("AmtScanner.Api.Models.WindowsCredential", "WindowsCredential")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WindowsCredentialId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("Device");
|
||||||
|
|
||||||
|
b.Navigation("WindowsCredential");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.RoleMenu", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("AmtScanner.Api.Models.Menu", "Menu")
|
||||||
|
.WithMany("RoleMenus")
|
||||||
|
.HasForeignKey("MenuId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("AmtScanner.Api.Models.Role", "Role")
|
||||||
|
.WithMany("RoleMenus")
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Menu");
|
||||||
|
|
||||||
|
b.Navigation("Role");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.StorageDevice", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("AmtScanner.Api.Models.HardwareInfo", "HardwareInfo")
|
||||||
|
.WithMany("StorageDevices")
|
||||||
|
.HasForeignKey("HardwareInfoId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("HardwareInfo");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.UserRole", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("AmtScanner.Api.Models.Role", "Role")
|
||||||
|
.WithMany("UserRoles")
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("AmtScanner.Api.Models.User", "User")
|
||||||
|
.WithMany("UserRoles")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Role");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("MemoryModules");
|
||||||
|
|
||||||
|
b.Navigation("StorageDevices");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.Menu", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Children");
|
||||||
|
|
||||||
|
b.Navigation("RoleMenus");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.Role", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("RoleMenus");
|
||||||
|
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,219 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace AmtScanner.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddUserAuthTables : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Menus",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
ParentId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
Name = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Path = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Component = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Title = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Icon = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Sort = table.Column<int>(type: "int", nullable: false),
|
||||||
|
IsHide = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
|
KeepAlive = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
|
Link = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
IsIframe = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
|
IsHideTab = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
|
Roles = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
AuthList = table.Column<string>(type: "varchar(1000)", maxLength: 1000, nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Menus", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Menus_Menus_ParentId",
|
||||||
|
column: x => x.ParentId,
|
||||||
|
principalTable: "Menus",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Roles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
RoleName = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
RoleCode = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Description = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Enabled = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Roles", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Users",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
UserName = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
PasswordHash = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
NickName = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Email = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Phone = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Avatar = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Gender = table.Column<string>(type: "varchar(1)", maxLength: 1, nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Status = table.Column<string>(type: "varchar(1)", maxLength: 1, nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||||
|
CreatedBy = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
UpdatedBy = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
IsDeleted = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
|
RefreshToken = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
RefreshTokenExpiryTime = table.Column<DateTime>(type: "datetime(6)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Users", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "RoleMenus",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
RoleId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
MenuId = table.Column<int>(type: "int", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_RoleMenus", x => new { x.RoleId, x.MenuId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_RoleMenus_Menus_MenuId",
|
||||||
|
column: x => x.MenuId,
|
||||||
|
principalTable: "Menus",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_RoleMenus_Roles_RoleId",
|
||||||
|
column: x => x.RoleId,
|
||||||
|
principalTable: "Roles",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "UserRoles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UserId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
RoleId = table.Column<int>(type: "int", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_UserRoles", x => new { x.UserId, x.RoleId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_UserRoles_Roles_RoleId",
|
||||||
|
column: x => x.RoleId,
|
||||||
|
principalTable: "Roles",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_UserRoles_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Menus_Name",
|
||||||
|
table: "Menus",
|
||||||
|
column: "Name");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Menus_ParentId",
|
||||||
|
table: "Menus",
|
||||||
|
column: "ParentId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_RoleMenus_MenuId",
|
||||||
|
table: "RoleMenus",
|
||||||
|
column: "MenuId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Roles_RoleCode",
|
||||||
|
table: "Roles",
|
||||||
|
column: "RoleCode",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UserRoles_RoleId",
|
||||||
|
table: "UserRoles",
|
||||||
|
column: "RoleId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Users_UserName",
|
||||||
|
table: "Users",
|
||||||
|
column: "UserName",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "RoleMenus");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UserRoles");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Menus");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Roles");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -194,6 +194,76 @@ namespace AmtScanner.Api.Migrations
|
|||||||
b.ToTable("MemoryModules");
|
b.ToTable("MemoryModules");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.Menu", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("AuthList")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("varchar(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("Component")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Icon")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsHide")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsHideTab")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsIframe")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("KeepAlive")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Link")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("varchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<int?>("ParentId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.Property<string>("Roles")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("varchar(500)");
|
||||||
|
|
||||||
|
b.Property<int>("Sort")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Name");
|
||||||
|
|
||||||
|
b.HasIndex("ParentId");
|
||||||
|
|
||||||
|
b.ToTable("Menus");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("AmtScanner.Api.Models.RemoteAccessToken", b =>
|
modelBuilder.Entity("AmtScanner.Api.Models.RemoteAccessToken", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@ -245,6 +315,55 @@ namespace AmtScanner.Api.Migrations
|
|||||||
b.ToTable("RemoteAccessTokens");
|
b.ToTable("RemoteAccessTokens");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.Role", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("varchar(500)");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("RoleCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("varchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("RoleName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleCode")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Roles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.RoleMenu", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("RoleId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("MenuId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("RoleId", "MenuId");
|
||||||
|
|
||||||
|
b.HasIndex("MenuId");
|
||||||
|
|
||||||
|
b.ToTable("RoleMenus");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("AmtScanner.Api.Models.StorageDevice", b =>
|
modelBuilder.Entity("AmtScanner.Api.Models.StorageDevice", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@ -273,6 +392,95 @@ namespace AmtScanner.Api.Migrations
|
|||||||
b.ToTable("StorageDevices");
|
b.ToTable("StorageDevices");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Avatar")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("varchar(500)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.Property<string>("Gender")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1)
|
||||||
|
.HasColumnType("varchar(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("NickName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.Property<string>("Phone")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("varchar(20)");
|
||||||
|
|
||||||
|
b.Property<string>("RefreshToken")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("varchar(500)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RefreshTokenExpiryTime")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1)
|
||||||
|
.HasColumnType("varchar(1)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("UpdatedBy")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserName")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.UserRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("RoleId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("AmtScanner.Api.Models.WindowsCredential", b =>
|
modelBuilder.Entity("AmtScanner.Api.Models.WindowsCredential", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@ -340,6 +548,16 @@ namespace AmtScanner.Api.Migrations
|
|||||||
b.Navigation("HardwareInfo");
|
b.Navigation("HardwareInfo");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.Menu", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("AmtScanner.Api.Models.Menu", "Parent")
|
||||||
|
.WithMany("Children")
|
||||||
|
.HasForeignKey("ParentId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("AmtScanner.Api.Models.RemoteAccessToken", b =>
|
modelBuilder.Entity("AmtScanner.Api.Models.RemoteAccessToken", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("AmtScanner.Api.Models.AmtDevice", "Device")
|
b.HasOne("AmtScanner.Api.Models.AmtDevice", "Device")
|
||||||
@ -358,6 +576,25 @@ namespace AmtScanner.Api.Migrations
|
|||||||
b.Navigation("WindowsCredential");
|
b.Navigation("WindowsCredential");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.RoleMenu", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("AmtScanner.Api.Models.Menu", "Menu")
|
||||||
|
.WithMany("RoleMenus")
|
||||||
|
.HasForeignKey("MenuId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("AmtScanner.Api.Models.Role", "Role")
|
||||||
|
.WithMany("RoleMenus")
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Menu");
|
||||||
|
|
||||||
|
b.Navigation("Role");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("AmtScanner.Api.Models.StorageDevice", b =>
|
modelBuilder.Entity("AmtScanner.Api.Models.StorageDevice", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("AmtScanner.Api.Models.HardwareInfo", "HardwareInfo")
|
b.HasOne("AmtScanner.Api.Models.HardwareInfo", "HardwareInfo")
|
||||||
@ -369,12 +606,50 @@ namespace AmtScanner.Api.Migrations
|
|||||||
b.Navigation("HardwareInfo");
|
b.Navigation("HardwareInfo");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.UserRole", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("AmtScanner.Api.Models.Role", "Role")
|
||||||
|
.WithMany("UserRoles")
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("AmtScanner.Api.Models.User", "User")
|
||||||
|
.WithMany("UserRoles")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Role");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b =>
|
modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("MemoryModules");
|
b.Navigation("MemoryModules");
|
||||||
|
|
||||||
b.Navigation("StorageDevices");
|
b.Navigation("StorageDevices");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.Menu", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Children");
|
||||||
|
|
||||||
|
b.Navigation("RoleMenus");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.Role", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("RoleMenus");
|
||||||
|
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AmtScanner.Api.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
108
backend-csharp/AmtScanner.Api/Models/ApiResponse.cs
Normal file
108
backend-csharp/AmtScanner.Api/Models/ApiResponse.cs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
namespace AmtScanner.Api.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统一 API 响应格式
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">数据类型</typeparam>
|
||||||
|
public class ApiResponse<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 状态码
|
||||||
|
/// </summary>
|
||||||
|
public int Code { get; set; } = 200;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 消息
|
||||||
|
/// </summary>
|
||||||
|
public string Msg { get; set; } = "success";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据
|
||||||
|
/// </summary>
|
||||||
|
public T? Data { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 成功响应
|
||||||
|
/// </summary>
|
||||||
|
public static ApiResponse<T> Success(T? data, string msg = "success")
|
||||||
|
{
|
||||||
|
return new ApiResponse<T>
|
||||||
|
{
|
||||||
|
Code = 200,
|
||||||
|
Msg = msg,
|
||||||
|
Data = data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 失败响应
|
||||||
|
/// </summary>
|
||||||
|
public static ApiResponse<T> Fail(int code, string msg)
|
||||||
|
{
|
||||||
|
return new ApiResponse<T>
|
||||||
|
{
|
||||||
|
Code = code,
|
||||||
|
Msg = msg,
|
||||||
|
Data = default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 无数据的统一响应
|
||||||
|
/// </summary>
|
||||||
|
public class ApiResponse : ApiResponse<object>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 成功响应(无数据)
|
||||||
|
/// </summary>
|
||||||
|
public static ApiResponse Ok(string msg = "success")
|
||||||
|
{
|
||||||
|
return new ApiResponse
|
||||||
|
{
|
||||||
|
Code = 200,
|
||||||
|
Msg = msg,
|
||||||
|
Data = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 失败响应
|
||||||
|
/// </summary>
|
||||||
|
public static new ApiResponse Fail(int code, string msg)
|
||||||
|
{
|
||||||
|
return new ApiResponse
|
||||||
|
{
|
||||||
|
Code = code,
|
||||||
|
Msg = msg,
|
||||||
|
Data = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页响应
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">数据项类型</typeparam>
|
||||||
|
public class PaginatedResponse<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 数据记录
|
||||||
|
/// </summary>
|
||||||
|
public List<T> Records { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前页码
|
||||||
|
/// </summary>
|
||||||
|
public int Current { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页大小
|
||||||
|
/// </summary>
|
||||||
|
public int Size { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总记录数
|
||||||
|
/// </summary>
|
||||||
|
public int Total { get; set; }
|
||||||
|
}
|
||||||
112
backend-csharp/AmtScanner.Api/Models/Menu.cs
Normal file
112
backend-csharp/AmtScanner.Api/Models/Menu.cs
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace AmtScanner.Api.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 系统菜单
|
||||||
|
/// </summary>
|
||||||
|
public class Menu
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 父菜单 ID(null 表示顶级菜单)
|
||||||
|
/// </summary>
|
||||||
|
public int? ParentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 路由名称
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 路由路径
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string Path { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 组件路径
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string? Component { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 菜单标题(i18n key 或直接文本)
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string? Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 图标
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string? Icon { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序(数字越小越靠前)
|
||||||
|
/// </summary>
|
||||||
|
public int Sort { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否隐藏
|
||||||
|
/// </summary>
|
||||||
|
public bool IsHide { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否缓存(keep-alive)
|
||||||
|
/// </summary>
|
||||||
|
public bool KeepAlive { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 外链地址
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(500)]
|
||||||
|
public string? Link { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否 iframe 嵌入
|
||||||
|
/// </summary>
|
||||||
|
public bool IsIframe { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否隐藏标签页
|
||||||
|
/// </summary>
|
||||||
|
public bool IsHideTab { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色限制(JSON 数组,如 ["R_SUPER", "R_ADMIN"])
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(500)]
|
||||||
|
public string? Roles { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按钮权限列表(JSON 数组)
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(1000)]
|
||||||
|
public string? AuthList { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 父菜单
|
||||||
|
/// </summary>
|
||||||
|
public Menu? Parent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 子菜单
|
||||||
|
/// </summary>
|
||||||
|
public ICollection<Menu> Children { get; set; } = new List<Menu>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色菜单关联
|
||||||
|
/// </summary>
|
||||||
|
public ICollection<RoleMenu> RoleMenus { get; set; } = new List<RoleMenu>();
|
||||||
|
}
|
||||||
52
backend-csharp/AmtScanner.Api/Models/Role.cs
Normal file
52
backend-csharp/AmtScanner.Api/Models/Role.cs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace AmtScanner.Api.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 系统角色
|
||||||
|
/// </summary>
|
||||||
|
public class Role
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色名称
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string RoleName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色编码(如 R_SUPER, R_ADMIN, R_USER)
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[MaxLength(50)]
|
||||||
|
public string RoleCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色描述
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(500)]
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否启用
|
||||||
|
/// </summary>
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户角色关联
|
||||||
|
/// </summary>
|
||||||
|
public ICollection<UserRole> UserRoles { get; set; } = new List<UserRole>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色菜单关联
|
||||||
|
/// </summary>
|
||||||
|
public ICollection<RoleMenu> RoleMenus { get; set; } = new List<RoleMenu>();
|
||||||
|
}
|
||||||
27
backend-csharp/AmtScanner.Api/Models/RoleMenu.cs
Normal file
27
backend-csharp/AmtScanner.Api/Models/RoleMenu.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
namespace AmtScanner.Api.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色-菜单关联表
|
||||||
|
/// </summary>
|
||||||
|
public class RoleMenu
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 角色 ID
|
||||||
|
/// </summary>
|
||||||
|
public int RoleId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色
|
||||||
|
/// </summary>
|
||||||
|
public Role Role { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 菜单 ID
|
||||||
|
/// </summary>
|
||||||
|
public int MenuId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 菜单
|
||||||
|
/// </summary>
|
||||||
|
public Menu Menu { get; set; } = null!;
|
||||||
|
}
|
||||||
105
backend-csharp/AmtScanner.Api/Models/User.cs
Normal file
105
backend-csharp/AmtScanner.Api/Models/User.cs
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace AmtScanner.Api.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 系统用户
|
||||||
|
/// </summary>
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户名(登录名)
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 密码哈希
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string PasswordHash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 昵称
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string? NickName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邮箱
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string? Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手机号
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(20)]
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 头像 URL
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(500)]
|
||||||
|
public string? Avatar { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 性别: 0-未知, 1-男, 2-女
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(1)]
|
||||||
|
public string Gender { get; set; } = "0";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态: 1-启用, 2-禁用
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(1)]
|
||||||
|
public string Status { get; set; } = "1";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建人
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string? CreatedBy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新人
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string? UpdatedBy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否已删除(软删除)
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDeleted { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 刷新令牌
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(500)]
|
||||||
|
public string? RefreshToken { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 刷新令牌过期时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? RefreshTokenExpiryTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户角色关联
|
||||||
|
/// </summary>
|
||||||
|
public ICollection<UserRole> UserRoles { get; set; } = new List<UserRole>();
|
||||||
|
}
|
||||||
27
backend-csharp/AmtScanner.Api/Models/UserRole.cs
Normal file
27
backend-csharp/AmtScanner.Api/Models/UserRole.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
namespace AmtScanner.Api.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户-角色关联表
|
||||||
|
/// </summary>
|
||||||
|
public class UserRole
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 用户 ID
|
||||||
|
/// </summary>
|
||||||
|
public int UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户
|
||||||
|
/// </summary>
|
||||||
|
public User User { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色 ID
|
||||||
|
/// </summary>
|
||||||
|
public int RoleId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色
|
||||||
|
/// </summary>
|
||||||
|
public Role Role { get; set; } = null!;
|
||||||
|
}
|
||||||
@ -1,11 +1,16 @@
|
|||||||
|
using AmtScanner.Api.Configuration;
|
||||||
using AmtScanner.Api.Data;
|
using AmtScanner.Api.Data;
|
||||||
|
using AmtScanner.Api.Middleware;
|
||||||
using AmtScanner.Api.Models;
|
using AmtScanner.Api.Models;
|
||||||
using AmtScanner.Api.Services;
|
using AmtScanner.Api.Services;
|
||||||
using AmtScanner.Api.Repositories;
|
using AmtScanner.Api.Repositories;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Security;
|
using System.Net.Security;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
// Disable SSL certificate validation for AMT devices (they use self-signed certs)
|
// Disable SSL certificate validation for AMT devices (they use self-signed certs)
|
||||||
ServicePointManager.ServerCertificateValidationCallback =
|
ServicePointManager.ServerCertificateValidationCallback =
|
||||||
@ -57,6 +62,46 @@ builder.Services.AddScoped<IHardwareInfoRepository, HardwareInfoRepository>();
|
|||||||
builder.Services.AddScoped<IAmtPowerService, AmtPowerService>();
|
builder.Services.AddScoped<IAmtPowerService, AmtPowerService>();
|
||||||
builder.Services.AddHttpClient<IGuacamoleService, GuacamoleService>();
|
builder.Services.AddHttpClient<IGuacamoleService, GuacamoleService>();
|
||||||
|
|
||||||
|
// Add JWT Configuration
|
||||||
|
builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection(JwtSettings.SectionName));
|
||||||
|
builder.Services.AddScoped<IJwtService, JwtService>();
|
||||||
|
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||||
|
builder.Services.AddScoped<IMenuService, MenuService>();
|
||||||
|
|
||||||
|
// Add JWT Authentication
|
||||||
|
var jwtSettings = builder.Configuration.GetSection(JwtSettings.SectionName).Get<JwtSettings>()!;
|
||||||
|
builder.Services.AddAuthentication(options =>
|
||||||
|
{
|
||||||
|
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
})
|
||||||
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidIssuer = jwtSettings.Issuer,
|
||||||
|
ValidAudience = jwtSettings.Audience,
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKey)),
|
||||||
|
ClockSkew = TimeSpan.Zero
|
||||||
|
};
|
||||||
|
|
||||||
|
options.Events = new JwtBearerEvents
|
||||||
|
{
|
||||||
|
OnAuthenticationFailed = context =>
|
||||||
|
{
|
||||||
|
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
|
||||||
|
{
|
||||||
|
context.Response.Headers.Append("Token-Expired", "true");
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline
|
// Configure the HTTP request pipeline
|
||||||
@ -68,6 +113,10 @@ if (app.Environment.IsDevelopment())
|
|||||||
|
|
||||||
app.UseCors("AllowFrontend");
|
app.UseCors("AllowFrontend");
|
||||||
|
|
||||||
|
// Add global exception handler
|
||||||
|
app.UseGlobalExceptionHandler();
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
@ -126,6 +175,9 @@ using (var scope = app.Services.CreateScope())
|
|||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine($"✅ 数据库连接成功: {databaseProvider}");
|
Console.WriteLine($"✅ 数据库连接成功: {databaseProvider}");
|
||||||
|
|
||||||
|
// 初始化种子数据
|
||||||
|
await DbSeeder.SeedAsync(db);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
161
backend-csharp/AmtScanner.Api/Services/AuthService.cs
Normal file
161
backend-csharp/AmtScanner.Api/Services/AuthService.cs
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
using AmtScanner.Api.Data;
|
||||||
|
using AmtScanner.Api.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace AmtScanner.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 认证服务实现
|
||||||
|
/// </summary>
|
||||||
|
public class AuthService : IAuthService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _context;
|
||||||
|
private readonly IJwtService _jwtService;
|
||||||
|
|
||||||
|
public AuthService(AppDbContext context, IJwtService jwtService)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_jwtService = jwtService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(User? user, string? accessToken, string? refreshToken, string? error)> LoginAsync(string userName, string password)
|
||||||
|
{
|
||||||
|
// 查找用户
|
||||||
|
var user = await _context.Users
|
||||||
|
.FirstOrDefaultAsync(u => u.UserName == userName && !u.IsDeleted);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return (null, null, null, "用户名或密码错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码
|
||||||
|
if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
|
||||||
|
{
|
||||||
|
return (null, null, null, "用户名或密码错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户状态
|
||||||
|
if (user.Status != "1")
|
||||||
|
{
|
||||||
|
return (null, null, null, "用户已被禁用");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户角色
|
||||||
|
var roles = await GetUserRolesAsync(user.Id);
|
||||||
|
|
||||||
|
// 生成 Token
|
||||||
|
var accessToken = _jwtService.GenerateAccessToken(user, roles);
|
||||||
|
var refreshToken = _jwtService.GenerateRefreshToken();
|
||||||
|
|
||||||
|
// 保存 RefreshToken 到用户
|
||||||
|
user.RefreshToken = refreshToken;
|
||||||
|
user.RefreshTokenExpiryTime = _jwtService.GetRefreshTokenExpiryTime();
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return (user, accessToken, refreshToken, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(string? accessToken, string? refreshToken, string? error)> RefreshTokenAsync(string accessToken, string refreshToken)
|
||||||
|
{
|
||||||
|
// 从过期的 Token 中获取用户信息
|
||||||
|
var principal = _jwtService.GetPrincipalFromExpiredToken(accessToken);
|
||||||
|
if (principal == null)
|
||||||
|
{
|
||||||
|
return (null, null, "无效的 Token");
|
||||||
|
}
|
||||||
|
|
||||||
|
var userIdClaim = principal.FindFirst("userId")?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userIdClaim) || !int.TryParse(userIdClaim, out var userId))
|
||||||
|
{
|
||||||
|
return (null, null, "无效的 Token");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找用户
|
||||||
|
var user = await _context.Users
|
||||||
|
.FirstOrDefaultAsync(u => u.Id == userId && !u.IsDeleted);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return (null, null, "用户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 RefreshToken
|
||||||
|
if (user.RefreshToken != refreshToken || user.RefreshTokenExpiryTime <= DateTime.UtcNow)
|
||||||
|
{
|
||||||
|
return (null, null, "RefreshToken 无效或已过期");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户角色
|
||||||
|
var roles = await GetUserRolesAsync(user.Id);
|
||||||
|
|
||||||
|
// 生成新的 Token
|
||||||
|
var newAccessToken = _jwtService.GenerateAccessToken(user, roles);
|
||||||
|
var newRefreshToken = _jwtService.GenerateRefreshToken();
|
||||||
|
|
||||||
|
// 更新 RefreshToken
|
||||||
|
user.RefreshToken = newRefreshToken;
|
||||||
|
user.RefreshTokenExpiryTime = _jwtService.GetRefreshTokenExpiryTime();
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return (newAccessToken, newRefreshToken, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> LogoutAsync(int userId)
|
||||||
|
{
|
||||||
|
var user = await _context.Users.FindAsync(userId);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除 RefreshToken
|
||||||
|
user.RefreshToken = null;
|
||||||
|
user.RefreshTokenExpiryTime = null;
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User?> GetUserByIdAsync(int userId)
|
||||||
|
{
|
||||||
|
return await _context.Users
|
||||||
|
.FirstOrDefaultAsync(u => u.Id == userId && !u.IsDeleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<string>> GetUserRolesAsync(int userId)
|
||||||
|
{
|
||||||
|
return await _context.UserRoles
|
||||||
|
.Where(ur => ur.UserId == userId)
|
||||||
|
.Join(_context.Roles, ur => ur.RoleId, r => r.Id, (ur, r) => r)
|
||||||
|
.Where(r => r.Enabled)
|
||||||
|
.Select(r => r.RoleCode)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(List<User> users, int total)> GetUsersAsync(int current, int size, string? userName = null, string? status = null)
|
||||||
|
{
|
||||||
|
var query = _context.Users.Where(u => !u.IsDeleted);
|
||||||
|
|
||||||
|
// 按用户名筛选
|
||||||
|
if (!string.IsNullOrEmpty(userName))
|
||||||
|
{
|
||||||
|
query = query.Where(u => u.UserName.Contains(userName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按状态筛选
|
||||||
|
if (!string.IsNullOrEmpty(status))
|
||||||
|
{
|
||||||
|
query = query.Where(u => u.Status == status);
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
var users = await query
|
||||||
|
.OrderByDescending(u => u.CreatedAt)
|
||||||
|
.Skip((current - 1) * size)
|
||||||
|
.Take(size)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return (users, total);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
backend-csharp/AmtScanner.Api/Services/IAuthService.cs
Normal file
39
backend-csharp/AmtScanner.Api/Services/IAuthService.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
using AmtScanner.Api.Models;
|
||||||
|
|
||||||
|
namespace AmtScanner.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 认证服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IAuthService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 用户登录
|
||||||
|
/// </summary>
|
||||||
|
Task<(User? user, string? accessToken, string? refreshToken, string? error)> LoginAsync(string userName, string password);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 刷新 Token
|
||||||
|
/// </summary>
|
||||||
|
Task<(string? accessToken, string? refreshToken, string? error)> RefreshTokenAsync(string accessToken, string refreshToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 退出登录
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> LogoutAsync(int userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取用户信息
|
||||||
|
/// </summary>
|
||||||
|
Task<User?> GetUserByIdAsync(int userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取用户角色
|
||||||
|
/// </summary>
|
||||||
|
Task<List<string>> GetUserRolesAsync(int userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取用户列表(分页)
|
||||||
|
/// </summary>
|
||||||
|
Task<(List<User> users, int total)> GetUsersAsync(int current, int size, string? userName = null, string? status = null);
|
||||||
|
}
|
||||||
35
backend-csharp/AmtScanner.Api/Services/IJwtService.cs
Normal file
35
backend-csharp/AmtScanner.Api/Services/IJwtService.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using AmtScanner.Api.Models;
|
||||||
|
|
||||||
|
namespace AmtScanner.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JWT 服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IJwtService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 生成 Access Token
|
||||||
|
/// </summary>
|
||||||
|
string GenerateAccessToken(User user, IEnumerable<string> roles);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成 Refresh Token
|
||||||
|
/// </summary>
|
||||||
|
string GenerateRefreshToken();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 Token 并返回 ClaimsPrincipal
|
||||||
|
/// </summary>
|
||||||
|
ClaimsPrincipal? ValidateToken(string token);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从过期的 Token 中获取 ClaimsPrincipal
|
||||||
|
/// </summary>
|
||||||
|
ClaimsPrincipal? GetPrincipalFromExpiredToken(string token);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 Refresh Token 过期时间
|
||||||
|
/// </summary>
|
||||||
|
DateTime GetRefreshTokenExpiryTime();
|
||||||
|
}
|
||||||
46
backend-csharp/AmtScanner.Api/Services/IMenuService.cs
Normal file
46
backend-csharp/AmtScanner.Api/Services/IMenuService.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
using AmtScanner.Api.Models;
|
||||||
|
|
||||||
|
namespace AmtScanner.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 菜单服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IMenuService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取用户菜单(根据角色过滤)
|
||||||
|
/// </summary>
|
||||||
|
Task<List<MenuDto>> GetUserMenusAsync(int userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取所有菜单
|
||||||
|
/// </summary>
|
||||||
|
Task<List<MenuDto>> GetAllMenusAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 菜单 DTO - 符合前端 AppRouteRecord 格式
|
||||||
|
/// </summary>
|
||||||
|
public class MenuDto
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Path { get; set; } = string.Empty;
|
||||||
|
public string? Component { get; set; }
|
||||||
|
public MenuMetaDto Meta { get; set; } = new();
|
||||||
|
public List<MenuDto> Children { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 菜单元数据 DTO - 符合前端 RouteMeta 格式
|
||||||
|
/// </summary>
|
||||||
|
public class MenuMetaDto
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string? Icon { get; set; }
|
||||||
|
public bool IsHide { get; set; }
|
||||||
|
public bool KeepAlive { get; set; }
|
||||||
|
public string? Link { get; set; }
|
||||||
|
public bool IsIframe { get; set; }
|
||||||
|
public List<string> Roles { get; set; } = new();
|
||||||
|
}
|
||||||
130
backend-csharp/AmtScanner.Api/Services/JwtService.cs
Normal file
130
backend-csharp/AmtScanner.Api/Services/JwtService.cs
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using AmtScanner.Api.Configuration;
|
||||||
|
using AmtScanner.Api.Models;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace AmtScanner.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JWT 服务实现
|
||||||
|
/// </summary>
|
||||||
|
public class JwtService : IJwtService
|
||||||
|
{
|
||||||
|
private readonly JwtSettings _settings;
|
||||||
|
private readonly SymmetricSecurityKey _securityKey;
|
||||||
|
|
||||||
|
public JwtService(IOptions<JwtSettings> settings)
|
||||||
|
{
|
||||||
|
_settings = settings.Value;
|
||||||
|
_securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.SecretKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GenerateAccessToken(User user, IEnumerable<string> roles)
|
||||||
|
{
|
||||||
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||||
|
new(ClaimTypes.Name, user.UserName),
|
||||||
|
new("userId", user.Id.ToString()),
|
||||||
|
new("userName", user.UserName)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加角色 claims
|
||||||
|
foreach (var role in roles)
|
||||||
|
{
|
||||||
|
claims.Add(new Claim(ClaimTypes.Role, role));
|
||||||
|
claims.Add(new Claim("roles", role));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加邮箱(如果有)
|
||||||
|
if (!string.IsNullOrEmpty(user.Email))
|
||||||
|
{
|
||||||
|
claims.Add(new Claim(ClaimTypes.Email, user.Email));
|
||||||
|
}
|
||||||
|
|
||||||
|
var credentials = new SigningCredentials(_securityKey, SecurityAlgorithms.HmacSha256);
|
||||||
|
var expires = DateTime.UtcNow.AddMinutes(_settings.AccessTokenExpirationMinutes);
|
||||||
|
|
||||||
|
var token = new JwtSecurityToken(
|
||||||
|
issuer: _settings.Issuer,
|
||||||
|
audience: _settings.Audience,
|
||||||
|
claims: claims,
|
||||||
|
expires: expires,
|
||||||
|
signingCredentials: credentials
|
||||||
|
);
|
||||||
|
|
||||||
|
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GenerateRefreshToken()
|
||||||
|
{
|
||||||
|
var randomBytes = new byte[64];
|
||||||
|
using var rng = RandomNumberGenerator.Create();
|
||||||
|
rng.GetBytes(randomBytes);
|
||||||
|
return Convert.ToBase64String(randomBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClaimsPrincipal? ValidateToken(string token)
|
||||||
|
{
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var principal = tokenHandler.ValidateToken(token, GetValidationParameters(), out _);
|
||||||
|
return principal;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClaimsPrincipal? GetPrincipalFromExpiredToken(string token)
|
||||||
|
{
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var validationParameters = GetValidationParameters();
|
||||||
|
validationParameters.ValidateLifetime = false; // 不验证过期时间
|
||||||
|
|
||||||
|
var principal = tokenHandler.ValidateToken(token, validationParameters, out var securityToken);
|
||||||
|
|
||||||
|
if (securityToken is not JwtSecurityToken jwtToken ||
|
||||||
|
!jwtToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return principal;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime GetRefreshTokenExpiryTime()
|
||||||
|
{
|
||||||
|
return DateTime.UtcNow.AddDays(_settings.RefreshTokenExpirationDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TokenValidationParameters GetValidationParameters()
|
||||||
|
{
|
||||||
|
return new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidIssuer = _settings.Issuer,
|
||||||
|
ValidAudience = _settings.Audience,
|
||||||
|
IssuerSigningKey = _securityKey,
|
||||||
|
ClockSkew = TimeSpan.Zero // 不允许时间偏差
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
88
backend-csharp/AmtScanner.Api/Services/MenuService.cs
Normal file
88
backend-csharp/AmtScanner.Api/Services/MenuService.cs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
using AmtScanner.Api.Data;
|
||||||
|
using AmtScanner.Api.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace AmtScanner.Api.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 菜单服务实现
|
||||||
|
/// </summary>
|
||||||
|
public class MenuService : IMenuService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _context;
|
||||||
|
private readonly IAuthService _authService;
|
||||||
|
|
||||||
|
public MenuService(AppDbContext context, IAuthService authService)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_authService = authService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<MenuDto>> GetUserMenusAsync(int userId)
|
||||||
|
{
|
||||||
|
// 获取用户角色
|
||||||
|
var userRoles = await _authService.GetUserRolesAsync(userId);
|
||||||
|
|
||||||
|
// 如果是超级管理员,返回所有菜单
|
||||||
|
if (userRoles.Contains("R_SUPER"))
|
||||||
|
{
|
||||||
|
return await GetAllMenusAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户角色对应的菜单
|
||||||
|
var roleIds = await _context.Roles
|
||||||
|
.Where(r => userRoles.Contains(r.RoleCode) && r.Enabled)
|
||||||
|
.Select(r => r.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var menuIds = await _context.RoleMenus
|
||||||
|
.Where(rm => roleIds.Contains(rm.RoleId))
|
||||||
|
.Select(rm => rm.MenuId)
|
||||||
|
.Distinct()
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var menus = await _context.Menus
|
||||||
|
.Where(m => menuIds.Contains(m.Id))
|
||||||
|
.OrderBy(m => m.Sort)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return BuildMenuTree(menus, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<MenuDto>> GetAllMenusAsync()
|
||||||
|
{
|
||||||
|
var menus = await _context.Menus
|
||||||
|
.OrderBy(m => m.Sort)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return BuildMenuTree(menus, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<MenuDto> BuildMenuTree(List<Menu> menus, int? parentId)
|
||||||
|
{
|
||||||
|
return menus
|
||||||
|
.Where(m => m.ParentId == parentId)
|
||||||
|
.OrderBy(m => m.Sort)
|
||||||
|
.Select(m => new MenuDto
|
||||||
|
{
|
||||||
|
Id = m.Id,
|
||||||
|
Name = m.Name,
|
||||||
|
Path = m.Path,
|
||||||
|
Component = m.Component,
|
||||||
|
Meta = new MenuMetaDto
|
||||||
|
{
|
||||||
|
Title = m.Title,
|
||||||
|
Icon = m.Icon,
|
||||||
|
IsHide = m.IsHide,
|
||||||
|
KeepAlive = m.KeepAlive,
|
||||||
|
Link = m.Link,
|
||||||
|
IsIframe = m.IsIframe,
|
||||||
|
Roles = string.IsNullOrEmpty(m.Roles)
|
||||||
|
? new List<string>()
|
||||||
|
: m.Roles.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList()
|
||||||
|
},
|
||||||
|
Children = BuildMenuTree(menus, m.Id)
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -21,5 +21,12 @@
|
|||||||
"BaseUrl": "http://localhost:8080/guacamole",
|
"BaseUrl": "http://localhost:8080/guacamole",
|
||||||
"Username": "guacadmin",
|
"Username": "guacadmin",
|
||||||
"Password": "guacadmin"
|
"Password": "guacadmin"
|
||||||
|
},
|
||||||
|
"Jwt": {
|
||||||
|
"SecretKey": "AmtScannerSecretKey2024VeryLongAndSecure!@#$%",
|
||||||
|
"Issuer": "AmtScanner",
|
||||||
|
"Audience": "AmtScannerClient",
|
||||||
|
"AccessTokenExpirationMinutes": 60,
|
||||||
|
"RefreshTokenExpirationDays": 7
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user