- 为全部 5 个项目(Avalonia-API、Avalonia-Common、Avalonia-EFCore、 Avalonia-PC、Avalonia-Services)中缺失注释的类、方法、属性、字段、 接口成员等补全中文 XML 文档注释 - 共修改约 37 个文件,补全约 220+ 处注释 - 修复 ServiceEndpointCollection.cs 中 MapDelete<TService> 语法错误 - 修复 PcAuthService.cs 中 const prefix 位置错乱导致编译失败的问题 - 扫描结果:缺失项 0 - 构建结果:4/4 项目编译通过
161 lines
6.5 KiB
C#
161 lines
6.5 KiB
C#
using Avalonia_Common.Core;
|
||
using Avalonia_EFCore.Database;
|
||
using Avalonia_EFCore.Models;
|
||
using Avalonia_Services.Core;
|
||
using Avalonia_Services.Services.AuthService;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using System.Text.Json;
|
||
|
||
namespace Avalonia_API.Authentication
|
||
{
|
||
/// <summary>
|
||
/// API 鉴权端点服务,实现 <see cref="IApiAuthEndpointService"/>,
|
||
/// 处理登录、刷新 Token 和登出操作,使用 JWT 与 Refresh Token 机制。
|
||
/// </summary>
|
||
public sealed class ApiAuthEndpointService(
|
||
AppDataContext db,
|
||
JwtTokenService jwtTokenService,
|
||
RefreshTokenService refreshTokenService) : IApiAuthEndpointService
|
||
{
|
||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||
{
|
||
PropertyNameCaseInsensitive = true,
|
||
};
|
||
|
||
/// <summary>
|
||
/// 处理用户登录请求。根据账号(邮箱或用户名)查找或创建用户,
|
||
/// 生成 JWT Access Token 和 Refresh Token 并返回。
|
||
/// </summary>
|
||
/// <param name="ctx">服务端点上下文,包含请求体、请求头等信息。</param>
|
||
/// <returns>包含 AccessToken、RefreshToken 及过期时间的认证响应。</returns>
|
||
public async Task<object?> LoginAsync(ServiceEndpointContext ctx)
|
||
{
|
||
var request = Deserialize<ApiLoginRequest>(ctx.Body);
|
||
if (string.IsNullOrWhiteSpace(request?.Account))
|
||
{
|
||
ctx.StatusCode = 400;
|
||
return ResponseHelper.Failure(400, "账号不能为空");
|
||
}
|
||
|
||
var user = await db.Users.FirstOrDefaultAsync(
|
||
x => x.Email == request.Account || x.Name == request.Account);
|
||
|
||
if (user is null)
|
||
{
|
||
user = new UserEntity
|
||
{
|
||
Name = request.Account,
|
||
Email = request.Account.Contains('@') ? request.Account : null,
|
||
};
|
||
db.Users.Add(user);
|
||
await db.SaveChangesAsync();
|
||
}
|
||
|
||
var roles = NormalizeRoles(request.Roles);
|
||
var accessToken = jwtTokenService.CreateAccessToken(user, roles);
|
||
var refreshToken = await refreshTokenService.CreateAsync(
|
||
user.Id,
|
||
ctx.GetHeader("User-Agent"),
|
||
GetRemoteIpAddress(ctx));
|
||
|
||
return ResponseHelper.Ok(new AuthTokenResponse(
|
||
accessToken.Token,
|
||
refreshToken.Token,
|
||
accessToken.ExpiresAt,
|
||
refreshToken.Entity.ExpiresAt,
|
||
roles), "登录成功");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 使用 Refresh Token 轮换新的 Access Token 和 Refresh Token。
|
||
/// 旧的 Refresh Token 会被撤销并替换。
|
||
/// </summary>
|
||
/// <param name="ctx">服务端点上下文,包含请求体中的 RefreshToken。</param>
|
||
/// <returns>新的 Token 对;若 Refresh Token 无效则返回 401 错误。</returns>
|
||
public async Task<object?> RefreshAsync(ServiceEndpointContext ctx)
|
||
{
|
||
var request = Deserialize<ApiRefreshTokenRequest>(ctx.Body);
|
||
var rotated = await refreshTokenService.RotateAsync(
|
||
request?.RefreshToken,
|
||
ctx.GetHeader("User-Agent"),
|
||
GetRemoteIpAddress(ctx));
|
||
|
||
if (rotated is null)
|
||
{
|
||
ctx.StatusCode = 401;
|
||
return ResponseHelper.Failure(401, "刷新 token 无效或已过期");
|
||
}
|
||
|
||
var user = await db.Users.FindAsync(rotated.Value.Entity.UserId);
|
||
if (user is null)
|
||
{
|
||
ctx.StatusCode = 401;
|
||
return ResponseHelper.Failure(401, "用户不存在");
|
||
}
|
||
|
||
var roles = new[] { "Admin" };
|
||
var accessToken = jwtTokenService.CreateAccessToken(user, roles);
|
||
|
||
return ResponseHelper.Ok(new AuthTokenResponse(
|
||
accessToken.Token,
|
||
rotated.Value.Token,
|
||
accessToken.ExpiresAt,
|
||
rotated.Value.Entity.ExpiresAt,
|
||
roles), "刷新成功");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理用户登出请求,撤销指定的 Refresh Token。
|
||
/// </summary>
|
||
/// <param name="ctx">服务端点上下文,包含请求体中的 RefreshToken。</param>
|
||
/// <returns>登出成功的响应。</returns>
|
||
public async Task<object?> LogoutAsync(ServiceEndpointContext ctx)
|
||
{
|
||
var request = Deserialize<ApiLogoutRequest>(ctx.Body);
|
||
await refreshTokenService.RevokeAsync(request?.RefreshToken);
|
||
return ResponseHelper.Succeed("退出成功");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将 JSON 请求体反序列化为指定类型。
|
||
/// </summary>
|
||
/// <typeparam name="T">目标类型。</typeparam>
|
||
/// <param name="body">JSON 请求体字符串,可为空。</param>
|
||
/// <returns>反序列化后的对象;若 body 为空则返回默认值。</returns>
|
||
private static T? Deserialize<T>(string? body)
|
||
{
|
||
return string.IsNullOrWhiteSpace(body)
|
||
? default
|
||
: JsonSerializer.Deserialize<T>(body, JsonOptions);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从上下文的 Items 中提取 ASP.NET Core HttpContext,并获取客户端远程 IP 地址。
|
||
/// </summary>
|
||
/// <param name="ctx">服务端点上下文。</param>
|
||
/// <returns>客户端 IP 地址字符串;若无法获取则返回 null。</returns>
|
||
private static string? GetRemoteIpAddress(ServiceEndpointContext ctx)
|
||
{
|
||
return ctx.Items.TryGetValue("HttpContext", out var value) && value is HttpContext httpContext
|
||
? httpContext.Connection.RemoteIpAddress?.ToString()
|
||
: null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 规范化角色数组:去空白、去重(忽略大小写),为空时默认返回 Admin 角色。
|
||
/// </summary>
|
||
/// <param name="roles">原始角色数组,可为 null。</param>
|
||
/// <returns>规范化后的角色数组。</returns>
|
||
private static string[] NormalizeRoles(string[]? roles)
|
||
{
|
||
var normalized = roles?
|
||
.Where(role => !string.IsNullOrWhiteSpace(role))
|
||
.Select(role => role.Trim())
|
||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||
.ToArray();
|
||
|
||
return normalized is { Length: > 0 } ? normalized : ["Admin"];
|
||
}
|
||
}
|
||
}
|