using System.ComponentModel.DataAnnotations; using RestSharp; using Store.Management.Api.Transport; namespace Store.Management.Api.Authentication; public sealed class LoginRequest { [Required] public string UserName { get; set; } = ""; /// 服务端要求的 UTF-8 密码的 32 位大写 MD5,由调用方计算。 [Required] public string Password { get; set; } = ""; [StringLength(100)] public string? DeviceId { get; set; } [StringLength(100)] public string? DeviceName { get; set; } } public sealed class LoginResult { public string TokenType { get; init; } = "Bearer"; public string Token { get; init; } = ""; public uint ExpiresInMinutes { get; init; } } public sealed record CurrentUser(string UserName, string? NickName); public interface IAuthApi { Task LoginAsync(LoginRequest request, CancellationToken cancellationToken = default); Task LogoutAsync(CancellationToken cancellationToken = default); Task MeAsync(CancellationToken cancellationToken = default); } internal sealed class AuthApi(ApiTransport transport, TokenSession session) : IAuthApi { // 登录和退出串行执行,避免迟到的登录响应覆盖退出操作。 private readonly SemaphoreSlim _gate = new(1, 1); public async Task LoginAsync(LoginRequest request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); try { LoginResult result = await transport.GetRequiredAsync("Auth/Login", Method.Post, request, authenticated: false, cancellationToken: cancellationToken).ConfigureAwait(false); if (string.IsNullOrWhiteSpace(result.Token) || result.Token.Contains('\r') || result.Token.Contains('\n') || !string.Equals(result.TokenType, "Bearer", StringComparison.OrdinalIgnoreCase)) { throw new ApiException(ApiErrorKind.InvalidResponse, "登录响应缺少有效的 Bearer Token。"); } session.Set(result.Token); return result; } finally { _gate.Release(); } } public async Task LogoutAsync(CancellationToken cancellationToken = default) { await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); string? token = session.Token; try { if (token is not null) { await transport.SendAsync("Auth/Logout", Method.Post, cancellationToken: cancellationToken).ConfigureAwait(false); } } finally { session.ClearIfCurrent(token); _gate.Release(); } } public Task MeAsync(CancellationToken cancellationToken = default) => transport.GetRequiredAsync("Auth/Me", Method.Get, cancellationToken: cancellationToken); }