Compare commits

...

3 Commits

Author SHA1 Message Date
c8c7fcdd8f 删除DBContextModelSnapshot,添加MenuRequest DTO,更新JWT生成方法为同步,增加JWT过期时间配置,更新用户注册页面和模型,添加用户角色管理功能,更新用户实体类以支持菜单信息 2025-07-09 23:13:11 +08:00
17c1e08d7f 完善用户和管理员角色控制器的文档和注释
- 添加用户控制器的类和方法注释
- 添加管理员角色控制器的类和方法注释
- 优化 `SearchUserFromRoleRequest` 和 `SearchUserFromRoleResponse` 的属性注释
2025-07-09 16:08:20 +08:00
dd1c9364c1 更新用户控制器中的JWT声明名称 2025-07-09 16:05:07 +08:00
12 changed files with 186 additions and 82 deletions

View File

@ -18,15 +18,20 @@
<span asp-validation-for="Input.Email" class="text-danger"></span> <span asp-validation-for="Input.Email" class="text-danger"></span>
</div> </div>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input asp-for="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" placeholder="password" /> <input asp-for="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" placeholder="请输入密码" />
<label asp-for="Input.Password">密码</label> <label asp-for="Input.Password">密码</label>
<span asp-validation-for="Input.Password" class="text-danger"></span> <span asp-validation-for="Input.Password" class="text-danger"></span>
</div> </div>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="password" /> <input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-sex" aria-required="true" placeholder="确认密码" />
<label asp-for="Input.ConfirmPassword">确认密码</label> <label asp-for="Input.ConfirmPassword">确认密码</label>
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span> <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
</div> </div>
<div class="form-floating mb-3">
<input asp-for="Input.Sex" class="form-control" autocomplete="username" aria-required="true" placeholder="男/女" />
<label asp-for="Input.Sex">性别</label>
<span asp-validation-for="Input.Sex" class="text-danger"></span>
</div>
<button id="registerSubmit" type="submit" class="w-100 btn btn-lg btn-primary">注册</button> <button id="registerSubmit" type="submit" class="w-100 btn btn-lg btn-primary">注册</button>
</form> </form>
</div> </div>

View File

@ -104,6 +104,12 @@ namespace AGSS.Areas.Identity.Pages.Account
[Display(Name = "Confirm password")] [Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; } public string ConfirmPassword { get; set; }
[MaxLength(10)]
[Display(Name = "Confirm password")]
public string Sex { get; set; }
} }
@ -120,7 +126,8 @@ namespace AGSS.Areas.Identity.Pages.Account
if (ModelState.IsValid) if (ModelState.IsValid)
{ {
var user = CreateUser(); var user = CreateUser();
user.Id = Guid.NewGuid().ToString();
user.Sex = Input.Sex;
await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None); await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None); await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
var result = await _userManager.CreateAsync(user, Input.Password); var result = await _userManager.CreateAsync(user, Input.Password);

View File

@ -1,3 +1,4 @@
using AGSS.Models.DTOs;
using AGSS.Models.Entities; using AGSS.Models.Entities;
using AGSS.Models.Template; using AGSS.Models.Template;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -6,20 +7,42 @@ using Microsoft.AspNetCore.Mvc;
namespace AGSS.Controllers.Admin; namespace AGSS.Controllers.Admin;
/// <summary>
/// 控制器类,用于管理角色相关的操作,包括添加角色、分配角色给用户以及通过角色查询用户。
/// 该控制器仅限具有"Admin"角色的用户访问。
/// </summary>
[Authorize(Roles = "Admin")] [Authorize(Roles = "Admin")]
[Route("api/v1/[controller]/[action]")] [Route("api/v1/[controller]/[action]")]
public class AdminRoleControllers:ControllerBase public class AdminRoleControllers:ControllerBase
{ {
/// <summary>
/// 角色管理器,用于处理角色相关的操作,如创建、查询等。
/// 此角色管理器实例主要用于与RoleModel类型的实体进行交互
/// 支持添加新角色、为用户分配角色等功能。
/// </summary>
private readonly RoleManager<RoleModel> _roleManager; private readonly RoleManager<RoleModel> _roleManager;
/// <summary>
/// 用户管理器实例,用于处理用户相关的操作如添加角色、查询用户等。
/// 此实例通过依赖注入的方式在构造函数中初始化,并在整个控制器生命周期内可用。
/// </summary>
private readonly UserManager<UserModel> _userManager; // Assuming UserModel is the type of user private readonly UserManager<UserModel> _userManager; // Assuming UserModel is the type of user
/// <summary>
/// 管理员角色控制器,用于处理与角色相关的操作,如添加角色、分配角色给用户以及通过角色查询用户。
/// 该控制器下的所有方法都需要管理员权限才能访问。
/// </summary>
public AdminRoleControllers(RoleManager<RoleModel> roleManager, UserManager<UserModel> userManager) public AdminRoleControllers(RoleManager<RoleModel> roleManager, UserManager<UserModel> userManager)
{ {
_roleManager = roleManager; _roleManager = roleManager;
_userManager = userManager; _userManager = userManager;
} }
/// <summary>
/// 添加新角色
/// </summary>
/// <param name="role">要添加的角色信息</param>
/// <returns>返回操作结果,包含状态码、消息和数据</returns>
[HttpPost] [HttpPost]
public async Task<IActionResult> AddRole([FromBody] RoleModel role) public async Task<IActionResult> AddRole([FromBody] RoleModel role)
{ {
@ -40,6 +63,13 @@ public class AdminRoleControllers:ControllerBase
return Ok(new ReturnTemplate(StatusCodes.Status500InternalServerError,"创建失败","Failed to create role: " + string.Join(", ", result.Errors.Select(e => e.Description)))); return Ok(new ReturnTemplate(StatusCodes.Status500InternalServerError,"创建失败","Failed to create role: " + string.Join(", ", result.Errors.Select(e => e.Description))));
} }
} }
/// <summary>
/// 为指定用户分配角色
/// </summary>
/// <param name="userId">用户的唯一标识符</param>
/// <param name="roleName">要分配的角色名称</param>
/// <returns>返回一个包含操作结果的ReturnTemplate对象其中Code表示状态码Msg表示消息Data表示附加数据如果有的话</returns>
[HttpPost] [HttpPost]
public async Task<IActionResult> EndowRole(string userId, string roleName) public async Task<IActionResult> EndowRole(string userId, string roleName)
{ {
@ -67,10 +97,74 @@ public class AdminRoleControllers:ControllerBase
} }
/// <summary>
/// 删除指定用户。
/// </summary>
/// <param name="userId">要删除的用户的唯一标识符。</param>
/// <returns>返回操作结果包含状态码、消息和数据。如果删除成功则返回200状态码如果用户ID为空或未找到指定用户则分别返回400或404状态码若删除过程中出现错误则返回500状态码并附带错误信息。</returns>
[HttpPost]
public async Task<IActionResult> DelUser(string userId)
{
if (string.IsNullOrWhiteSpace(userId))
{
return Ok(new ReturnTemplate(400, "你填写的用户ID是空的~", null));
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return Ok(new ReturnTemplate(404, "未找到指定用户哦·~", null));
}
// 删除用户
var result = await _userManager.DeleteAsync(user);
if (result.Succeeded)
{
return Ok(new ReturnTemplate(200, "用户删除成功,不要留念这个用户哦~", null));
}
else
{
return StatusCode(500, new ReturnTemplate(500, "发生了一些不可预料的错误555", result.Errors));
}
}
[HttpPost]
public async Task<IActionResult> SetMenu([FromBody]MenuRequest request)
{
if (string.IsNullOrWhiteSpace(request.Id) || string.IsNullOrWhiteSpace(request.MenuName))
{
return Ok(new ReturnTemplate(400, "请求参数无效(有的参数是空的哦~", ""));
}
var user=await _userManager.FindByIdAsync(request.Id);
if (user==null)
{
return Ok(new ReturnTemplate(404, "Sorry你输入的用户我们找不到", ""));
}
user.MenuCode = request.MenuCode;
user.MenuName = request.MenuName;
var result= await _userManager.UpdateAsync(user);
if (result.Succeeded)
{
return Ok(new ReturnTemplate(200, "配置成功啦!", ""));
}
else
{
return StatusCode(500, new ReturnTemplate(500, "删除用户时发生错误", result.Errors));
}
}
/// <summary> /// <summary>
/// 通过角色查询用户,支持分页 /// 通过角色查询用户,支持分页
/// </summary> /// </summary>
/// <returns></returns> /// <param name="request">包含角色名称、页码和每页大小的请求对象</param>
/// <returns>返回包含总用户数和当前页用户的响应对象</returns>
[HttpPost] [HttpPost]
public async Task<IActionResult> SearchUserFromRole([FromBody] SearchUserFromRoleRequest request) public async Task<IActionResult> SearchUserFromRole([FromBody] SearchUserFromRoleRequest request)
{ {
@ -102,16 +196,45 @@ public class AdminRoleControllers:ControllerBase
return Ok(new ReturnTemplate(200, "查询成功", response)); return Ok(new ReturnTemplate(200, "查询成功", response));
} }
/// <summary>
/// 用于通过角色名称查询用户列表的请求模型。支持分页功能。
/// </summary>
public class SearchUserFromRoleRequest public class SearchUserFromRoleRequest
{ {
/// <summary>
/// 表示角色的名称。此属性用于指定或获取与用户管理相关的角色名称。
/// 在进行角色分配、查询等操作时,需要提供正确的角色名称以确保操作的成功执行。
/// </summary>
public string RoleName { get; set; } public string RoleName { get; set; }
/// <summary>
/// 表示当前请求的页码默认为1。用于分页查询用户时指定从哪一页开始获取数据。
/// </summary>
public int Page { get; set; } = 1; public int Page { get; set; } = 1;
/// <summary>
/// 每页显示的用户数量。默认值为10。
/// 该属性用于分页查询中指定每一页应包含的用户条目数。
/// </summary>
public int PageSize { get; set; } = 10; public int PageSize { get; set; } = 10;
} }
/// <summary>
/// 表示通过角色查询用户后返回的响应数据。
/// 该类用于封装查询结果,包括总用户数和分页后的用户列表。
/// </summary>
public class SearchUserFromRoleResponse public class SearchUserFromRoleResponse
{ {
/// <summary>
/// 表示属于特定角色的用户总数。
/// </summary>
/// <remarks>此属性用于分页查询中,返回匹配给定角色名称的所有用户的数量。</remarks>
public int TotalCount { get; set; } public int TotalCount { get; set; }
/// <summary>
/// 表示属于特定角色的用户列表。该属性用于存储和返回在给定角色下的所有用户。
/// </summary>
/// <remarks>此列表通常作为查询结果的一部分,例如通过角色名搜索用户时返回的数据。</remarks>
public List<UserModel> Users { get; set; } public List<UserModel> Users { get; set; }
} }
} }

View File

@ -1,3 +1,4 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
using AGSS.Models.Entities; using AGSS.Models.Entities;
using AGSS.Models.Template; using AGSS.Models.Template;
@ -7,25 +8,38 @@ using Microsoft.AspNetCore.Mvc;
namespace AGSS.Controllers.User; namespace AGSS.Controllers.User;
/// <summary>
/// 用户控制器提供与用户相关的API接口。
/// </summary>
/// <remarks>此控制器需要授权才能访问其方法。</remarks>
[Authorize] [Authorize]
[Route("api/v1/[controller]/[action]")] [Route("api/v1/[controller]/[action]")]
public class UserControllers:ControllerBase public class UserControllers:ControllerBase
{ {
/// <summary>
/// 用户服务实例,用于执行与用户相关的操作。
/// 该服务提供了一系列方法来处理用户的查询和更新等操作,
/// 包括但不限于获取用户详细信息、修改用户资料等功能。
/// </summary>
private readonly UserService _userService; private readonly UserService _userService;
/// <summary>
/// 用户控制器提供用户相关操作的API接口。
/// </summary>
public UserControllers(UserService userService, UserManager<UserModel> userManager) public UserControllers(UserService userService, UserManager<UserModel> userManager)
{ {
_userService = userService; _userService = userService;
} }
/// <summary>
/// 获取当前登录用户的个人信息。
/// </summary>
/// <returns>返回一个包含状态码、消息和用户信息的ReturnTemplate对象。如果成功状态码为200如果失败状态码为500。</returns>
[HttpGet] [HttpGet]
public async Task<IActionResult> My() public async Task<IActionResult> My()
{ {
string userId = this.User.FindFirst(ClaimTypes.NameIdentifier)!.Value; string userId = this.User.FindFirst(JwtRegisteredClaimNames.Sub)!.Value;
if (string.IsNullOrEmpty(userId)) if (string.IsNullOrEmpty(userId))
{ {
return Ok(new ReturnTemplate(500,"获取用户失败JWT解析错误",null)); return Ok(new ReturnTemplate(500,"获取用户失败JWT解析错误",null));

View File

@ -93,6 +93,14 @@ namespace AGSS.Migrations
b.Property<DateTimeOffset?>("LockoutEnd") b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<string>("MenuCode")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("MenuName")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("NormalizedEmail") b.Property<string>("NormalizedEmail")
.HasMaxLength(256) .HasMaxLength(256)
.HasColumnType("character varying(256)"); .HasColumnType("character varying(256)");

View File

@ -1,67 +0,0 @@
// <auto-generated />
using System;
using AGSS.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace AGSS.Migrations
{
[DbContext(typeof(DBContext))]
partial class DBContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.6")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("AGSS.Models.Entities.UserModel", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AuthId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Birthday")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("Config")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Description")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("JobCode")
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<string>("JobName")
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<string>("Sex")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("Id");
b.ToTable("UserModels");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,8 @@
namespace AGSS.Models.DTOs;
public struct MenuRequest
{
public string Id { get; set; }
public string MenuName { get; set; }
public string? MenuCode { get; set; }
}

View File

@ -17,6 +17,10 @@ public class UserModel:IdentityUser<string>
public string? JobName { get; set; } public string? JobName { get; set; }
[MaxLength(20)] [MaxLength(20)]
public string? Birthday { get; set; } public string? Birthday { get; set; }
[MaxLength(500)]
public string? MenuCode { get; set; }
[MaxLength(500)]
public string? MenuName { get; set; }
} }

View File

@ -33,7 +33,7 @@ builder.Services.AddDbContext<ApplicationDbContext>(opt =>
opt.UseNpgsql(builder.Configuration.GetConnectionString("DBContext"))); opt.UseNpgsql(builder.Configuration.GetConnectionString("DBContext")));
// Identity 配置 // Identity 配置
builder.Services.AddIdentity<UserModel, IdentityRole>() builder.Services.AddIdentity<UserModel, RoleModel>()
.AddEntityFrameworkStores<ApplicationDbContext>() .AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders() .AddDefaultTokenProviders()
.AddDefaultUI(); .AddDefaultUI();

View File

@ -28,7 +28,7 @@ public class Jwt
return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor); return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
} }
public async Task<string> GenerateJwtToken(UserModel user,IList<string> roles) public string GenerateJwtToken(UserModel user,IList<string> roles)
{ {
var claims = new List<Claim>(); var claims = new List<Claim>();
claims.Add(new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString())); claims.Add(new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()));

View File

@ -15,6 +15,7 @@
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"Jwt": { "Jwt": {
"ExpireMinutes": "4",
"Issuer": "https://api.zeronode.cn/api", "Issuer": "https://api.zeronode.cn/api",
"Audience": "https://api.zeronode.cn/api", "Audience": "https://api.zeronode.cn/api",
"Key": "7wU9bdVfBsX3jITh0w4bgE6fkvLk8pIcZRSUw6r8HQUnXfslYxlx4c4E0ZAIw4Ak" "Key": "7wU9bdVfBsX3jITh0w4bgE6fkvLk8pIcZRSUw6r8HQUnXfslYxlx4c4E0ZAIw4Ak"

View File

@ -15,6 +15,7 @@
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"Jwt": { "Jwt": {
"ExpireMinutes": "4",
"Issuer": "https://api.zeronode.cn/api", "Issuer": "https://api.zeronode.cn/api",
"Audience": "https://api.zeronode.cn/api", "Audience": "https://api.zeronode.cn/api",
"Key": "7wU9bdVfBsX3jITh0w4bgE6fkvLk8pIcZRSUw6r8HQUnXfslYxlx4c4E0ZAIw4Ak" "Key": "7wU9bdVfBsX3jITh0w4bgE6fkvLk8pIcZRSUw6r8HQUnXfslYxlx4c4E0ZAIw4Ak"