取消新增/Excel 导入档案时自动创建账号。
取消修改人员档案时同步登录账号。 Excel 人员模板恢复为不含“初始密码”。 登录页新增“学生首次登录?自助激活账号”入口。 公开激活地址:http://127.0.0.1:5255/activate
This commit is contained in:
@@ -1,19 +1,117 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Claims;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jiaowu.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/auth")]
|
||||
public sealed class AuthController(
|
||||
AppDbContext db,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ITokenService tokenService) : ControllerBase
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[HttpGet("activation-options")]
|
||||
public async Task<ActionResult> GetActivationOptions(CancellationToken cancellationToken)
|
||||
{
|
||||
var colleges = await db.Colleges.AsNoTracking()
|
||||
.Where(x => x.IsEnabled)
|
||||
.OrderBy(x => x.Code)
|
||||
.Select(x => new { x.Id, x.Code, x.Name })
|
||||
.ToListAsync(cancellationToken);
|
||||
var majors = await db.Majors.AsNoTracking()
|
||||
.Where(x => x.IsEnabled && x.College!.IsEnabled)
|
||||
.OrderBy(x => x.Code)
|
||||
.Select(x => new { x.Id, x.Code, x.Name, x.CollegeId })
|
||||
.ToListAsync(cancellationToken);
|
||||
var classes = await db.AdministrativeClasses.AsNoTracking()
|
||||
.Where(x => x.IsEnabled && x.Major!.IsEnabled && x.Major.College!.IsEnabled)
|
||||
.OrderByDescending(x => x.Grade)
|
||||
.ThenBy(x => x.Code)
|
||||
.Select(x => new { x.Id, x.Code, x.Name, x.Grade, x.MajorId })
|
||||
.ToListAsync(cancellationToken);
|
||||
return Ok(new
|
||||
{
|
||||
Colleges = colleges,
|
||||
Majors = majors,
|
||||
Classes = classes,
|
||||
Grades = classes.Select(x => x.Grade).Distinct().OrderByDescending(x => x)
|
||||
});
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[EnableRateLimiting("public-auth")]
|
||||
[HttpPost("activate-student")]
|
||||
public async Task<ActionResult> ActivateStudent(
|
||||
StudentActivationRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var name = request.Name.Trim();
|
||||
var studentNumber = request.StudentNumber.Trim();
|
||||
var student = await db.Students
|
||||
.Include(x => x.AdministrativeClass)
|
||||
.ThenInclude(x => x!.Major)
|
||||
.FirstOrDefaultAsync(x =>
|
||||
x.Status == StudentStatus.Active &&
|
||||
x.Name == name &&
|
||||
x.StudentNumber == studentNumber &&
|
||||
x.EnrollmentYear == request.Grade &&
|
||||
x.AdministrativeClassId == request.AdministrativeClassId &&
|
||||
x.AdministrativeClass!.Grade == request.Grade &&
|
||||
x.AdministrativeClass.MajorId == request.MajorId &&
|
||||
x.AdministrativeClass.Major!.CollegeId == request.CollegeId,
|
||||
cancellationToken);
|
||||
if (student is null)
|
||||
return ActivationProblem(
|
||||
"填写的信息与在籍学生档案不完全一致,请核对后重试。",
|
||||
StatusCodes.Status400BadRequest);
|
||||
if (student.UserId.HasValue)
|
||||
return ActivationProblem(
|
||||
"该学号已经激活,请直接登录;如忘记密码请联系管理员重置。",
|
||||
StatusCodes.Status409Conflict);
|
||||
if (await userManager.FindByNameAsync(studentNumber) is not null)
|
||||
return ActivationProblem(
|
||||
"该学号已有登录账号但未正确关联,请联系管理员处理。",
|
||||
StatusCodes.Status409Conflict);
|
||||
|
||||
await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken);
|
||||
var user = new ApplicationUser
|
||||
{
|
||||
UserName = studentNumber,
|
||||
DisplayName = student.Name,
|
||||
StaffNumber = studentNumber,
|
||||
CollegeId = request.CollegeId,
|
||||
IsEnabled = true,
|
||||
LockoutEnabled = true
|
||||
};
|
||||
var result = await userManager.CreateAsync(user, request.Password);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
await transaction.RollbackAsync(cancellationToken);
|
||||
return IdentityValidationProblem(result);
|
||||
}
|
||||
result = await userManager.AddToRoleAsync(user, SystemRoles.Student);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
await transaction.RollbackAsync(cancellationToken);
|
||||
return IdentityValidationProblem(result);
|
||||
}
|
||||
|
||||
student.UserId = user.Id;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
return Ok(new { UserName = studentNumber });
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("login")]
|
||||
public async Task<ActionResult<LoginResponse>> Login(LoginRequest request)
|
||||
@@ -77,12 +175,36 @@ public sealed class AuthController(
|
||||
user.CollegeId,
|
||||
EffectiveDataScopeResolver.Resolve(roles).ToString());
|
||||
}
|
||||
|
||||
private ActionResult IdentityValidationProblem(IdentityResult result)
|
||||
{
|
||||
foreach (var error in result.Errors)
|
||||
ModelState.AddModelError(error.Code, error.Description);
|
||||
return ValidationProblem(ModelState);
|
||||
}
|
||||
|
||||
private ActionResult ActivationProblem(string detail, int status) =>
|
||||
StatusCode(status, new ProblemDetails
|
||||
{
|
||||
Title = "账号激活失败",
|
||||
Detail = detail,
|
||||
Status = status
|
||||
});
|
||||
}
|
||||
|
||||
public sealed record LoginRequest(
|
||||
[Required, MaxLength(100)] string UserName,
|
||||
[Required, MaxLength(100)] string Password);
|
||||
|
||||
public sealed record StudentActivationRequest(
|
||||
[Required, MaxLength(50)] string Name,
|
||||
[Required, MaxLength(30)] string StudentNumber,
|
||||
Guid CollegeId,
|
||||
Guid MajorId,
|
||||
[Range(2000, 2200)] int Grade,
|
||||
Guid AdministrativeClassId,
|
||||
[Required, MinLength(8), MaxLength(100)] string Password);
|
||||
|
||||
public sealed record LoginResponse(string Token, CurrentUserResponse User);
|
||||
|
||||
public sealed record CurrentUserResponse(
|
||||
|
||||
Reference in New Issue
Block a user