using System.Collections.Concurrent; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using Fantasy; using Fantasy.Async; using Fantasy.Network.HTTP; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; namespace NBF; [ApiController] [Route("api/[controller]")] [ServiceFilter(typeof(SceneContextFilter))] public class AuthController : NBControllerBase { private static readonly ConcurrentDictionary CaptchaCache = new(); private static readonly TimeSpan CaptchaLifetime = TimeSpan.FromMinutes(10); private readonly Scene _scene; public AuthController(Scene scene) { _scene = scene; } [HttpGet("login")] [AllowAnonymous] public async FTask Login([FromQuery] string account, [FromQuery] string code) { if (string.IsNullOrWhiteSpace(account) || string.IsNullOrWhiteSpace(code)) { await FTask.CompletedTask; return Error(ErrorCode.ArgsError, "account or code is empty"); } var key = account.Trim(); if (!CaptchaCache.TryGetValue(key, out var captchaInfo)) { await FTask.CompletedTask; return Error(ErrorCode.ArgsError, "captcha not found"); } if (captchaInfo.ExpireAtUtc <= DateTime.UtcNow) { CaptchaCache.TryRemove(key, out _); await FTask.CompletedTask; return Error(ErrorCode.ArgsError, "captcha expired"); } if (!string.Equals(captchaInfo.Code, code.Trim(), StringComparison.Ordinal)) { await FTask.CompletedTask; return Error(ErrorCode.ArgsError, "captcha invalid"); } CaptchaCache.TryRemove(key, out _); var (token, expireAtUtc) = GenerateJwtToken(key); await FTask.CompletedTask; return Success(new { token, expireAtUtc }); } [HttpGet("code")] [AllowAnonymous] public async FTask SendCode([FromQuery] string account) { if (string.IsNullOrWhiteSpace(account)) { await FTask.CompletedTask; return Error(ErrorCode.ArgsError, "account is empty"); } var key = account.Trim(); var captchaCode = RandomNumberGenerator.GetInt32(0, 1_000_000).ToString("D6"); var expireAtUtc = DateTime.UtcNow.Add(CaptchaLifetime); CaptchaCache[key] = new CaptchaInfo { Code = captchaCode, ExpireAtUtc = expireAtUtc }; await DingTalkHelper.SendCAPTCHA(account, captchaCode); return Success(); } [HttpGet("me")] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] public IActionResult Me() { var account = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? string.Empty; return Success(new { account }); } private static (string Token, DateTime ExpireAtUtc) GenerateJwtToken(string account) { var now = DateTime.UtcNow; var expireAtUtc = now.Add(ApiJwtHelper.TokenLifetime); var tokenHandler = new JwtSecurityTokenHandler(); var claims = new[] { new Claim(ClaimTypes.NameIdentifier, account), new Claim(ClaimTypes.Name, account), new Claim(ClaimTypes.Role, "Player") }; var token = tokenHandler.CreateToken(new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims), Expires = expireAtUtc, NotBefore = now, Issuer = ApiJwtHelper.Issuer, Audience = ApiJwtHelper.Audience, SigningCredentials = new SigningCredentials( ApiJwtHelper.CreateSigningKey(), SecurityAlgorithms.HmacSha256) }); return (tokenHandler.WriteToken(token), expireAtUtc); } private sealed class CaptchaInfo { public string Code { get; init; } = string.Empty; public DateTime ExpireAtUtc { get; init; } } }