Files
Academic-Affairs-System/src/Jiaowu.Api/Controllers/AuthController.cs
T
biss 30c15f89e5 取消新增/Excel 导入档案时自动创建账号。
取消修改人员档案时同步登录账号。
Excel 人员模板恢复为不含“初始密码”。
登录页新增“学生首次登录?自助激活账号”入口。
公开激活地址:http://127.0.0.1:5255/activate
2026-07-25 07:55:49 +08:00

217 lines
7.9 KiB
C#

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)
{
var user = await userManager.FindByNameAsync(request.UserName);
if (user is null || !user.IsEnabled)
{
return Unauthorized(new ProblemDetails
{
Title = "登录失败",
Detail = "账号或密码不正确,或账号已停用。",
Status = StatusCodes.Status401Unauthorized
});
}
if (await userManager.IsLockedOutAsync(user) ||
!await userManager.CheckPasswordAsync(user, request.Password))
{
await userManager.AccessFailedAsync(user);
return Unauthorized(new ProblemDetails
{
Title = "登录失败",
Detail = "账号或密码不正确,或账号已停用。",
Status = StatusCodes.Status401Unauthorized
});
}
await userManager.ResetAccessFailedCountAsync(user);
user.LastLoginAt = DateTime.UtcNow;
await userManager.UpdateAsync(user);
var roles = await userManager.GetRolesAsync(user);
return new LoginResponse(
tokenService.Create(user, roles),
new CurrentUserResponse(
user.Id,
user.UserName!,
user.DisplayName,
roles,
user.CollegeId,
EffectiveDataScopeResolver.Resolve(roles).ToString()));
}
[Authorize]
[HttpGet("me")]
public async Task<ActionResult<CurrentUserResponse>> Me()
{
var id = User.FindFirstValue(ClaimTypes.NameIdentifier);
var user = id is null ? null : await userManager.FindByIdAsync(id);
if (user is null || !user.IsEnabled)
{
return Unauthorized();
}
var roles = await userManager.GetRolesAsync(user);
return new CurrentUserResponse(
user.Id,
user.UserName!,
user.DisplayName,
roles,
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(
Guid Id,
string UserName,
string DisplayName,
IEnumerable<string> Roles,
Guid? CollegeId,
string EffectiveDataScope);