80 lines
3.0 KiB
C#
80 lines
3.0 KiB
C#
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; } = "";
|
|
/// <summary>服务端要求的 UTF-8 密码的 32 位大写 MD5,由调用方计算。</summary>
|
|
[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<LoginResult> LoginAsync(LoginRequest request, CancellationToken cancellationToken = default);
|
|
Task LogoutAsync(CancellationToken cancellationToken = default);
|
|
Task<CurrentUser> MeAsync(CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
internal sealed class AuthApi(ApiTransport transport, TokenSession session) : IAuthApi
|
|
{
|
|
// 登录和退出串行执行,避免迟到的登录响应覆盖退出操作。
|
|
private readonly SemaphoreSlim _gate = new(1, 1);
|
|
|
|
public async Task<LoginResult> LoginAsync(LoginRequest request, CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
LoginResult result = await transport.GetRequiredAsync<LoginResult>("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<object>("Auth/Logout", Method.Post, cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
session.ClearIfCurrent(token);
|
|
_gate.Release();
|
|
}
|
|
}
|
|
|
|
public Task<CurrentUser> MeAsync(CancellationToken cancellationToken = default)
|
|
=> transport.GetRequiredAsync<CurrentUser>("Auth/Me", Method.Get, cancellationToken: cancellationToken);
|
|
}
|