using AuthApi.Models; using AuthApi.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; using MongoDB.Driver; using System.IdentityModel.Tokens.Jwt; using System.Net.Mail; using System.Security.Claims; using System.Text; using System.Text.RegularExpressions; namespace AuthApi.Controllers; [ApiController] [Route("api/[controller]")] public class AuthController : ControllerBase { private readonly UserService _users; private readonly IConfiguration _cfg; private readonly BlacklistService _blacklist; public AuthController(UserService users, IConfiguration cfg, BlacklistService blacklist) { _users = users; _cfg = cfg; _blacklist = blacklist; } [HttpPost("register")] public async Task Register([FromBody] RegisterRequest req) { var username = req.Username?.Trim() ?? ""; var password = req.Password ?? ""; var email = req.Email?.Trim() ?? ""; if (!Regex.IsMatch(username, "^[A-Za-z0-9_]{3,24}$")) return BadRequest("Username must be 3-24 characters using only letters, numbers, or underscores"); if (password.Length < 8) return BadRequest("Password must be at least 8 characters"); var emailDomain = email.Split('@').LastOrDefault() ?? ""; if (string.IsNullOrWhiteSpace(email) || !MailAddress.TryCreate(email, out _) || !emailDomain.Contains('.')) return BadRequest("A valid email address is required"); if (await _users.GetByUsernameAsync(username) != null) return Conflict("Username already exists"); var hash = BCrypt.Net.BCrypt.HashPassword(password); var user = new User { Username = username, PasswordHash = hash, Role = "USER", Email = email }; try { await _users.CreateAsync(user); } catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { return Conflict("Username already exists"); } return Ok("User created"); } [HttpPost("login")] public async Task Login([FromBody] LoginRequest req) { var user = await _users.GetByUsernameAsync(req.Username); if (user == null || !BCrypt.Net.BCrypt.Verify(req.Password, user.PasswordHash)) return Unauthorized(); var (accessToken, jti, expUtc) = GenerateJwtToken(user); user.RefreshToken = Guid.NewGuid().ToString("N"); user.RefreshTokenExpiry = DateTime.UtcNow.AddDays(7); await _users.UpdateAsync(user); return Ok(new { accessToken, refreshToken = user.RefreshToken, user.Username, user.Role, jti, exp = expUtc }); } [HttpPost("refresh")] public async Task Refresh([FromBody] RefreshRequest req) { var user = await _users.GetByUsernameAsync(req.Username); if (user == null || user.RefreshToken != req.RefreshToken || user.RefreshTokenExpiry < DateTime.UtcNow) return Unauthorized("Invalid or expired refresh token"); var (accessToken, _, expUtc) = GenerateJwtToken(user); return Ok(new { accessToken, exp = expUtc }); } [HttpPost("logout")] [Authorize(Roles = "USER,SUPER")] public async Task Logout() { var token = HttpContext.Request.Headers["Authorization"].FirstOrDefault()?.Replace("Bearer ", ""); if (string.IsNullOrWhiteSpace(token)) return BadRequest("Token missing"); var jwt = new JwtSecurityTokenHandler().ReadJwtToken(token); await _blacklist.AddToBlacklistAsync(jwt.Id, jwt.ValidTo); return Ok("Logged out."); } [HttpPost("role")] [Authorize(Roles = "SUPER")] public async Task ChangeUserRole([FromBody] ChangeRoleRequest req) { if (req.NewRole is not ("USER" or "SUPER")) return BadRequest("Role must be 'USER' or 'SUPER'"); var user = await _users.GetByUsernameAsync(req.Username); if (user is null) return NotFound("User not found"); user.Role = req.NewRole; await _users.UpdateAsync(user); return Ok($"{req.Username}'s role updated to {req.NewRole}"); } [HttpGet("users")] [Authorize(Roles = "SUPER")] public async Task GetAllUsers() => Ok(await _users.GetAllAsync()); private (string token, string jti, DateTime expUtc) GenerateJwtToken(User user) { var key = Encoding.UTF8.GetBytes(_cfg["Jwt:Key"]!); var issuer = _cfg["Jwt:Issuer"] ?? "GameAuthApi"; var audience = _cfg["Jwt:Audience"] ?? issuer; var creds = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256); var jti = Guid.NewGuid().ToString("N"); var claims = new[] { new Claim(ClaimTypes.Name, user.Username), new Claim(ClaimTypes.NameIdentifier, user.Id), new Claim(ClaimTypes.Role, user.Role), new Claim(JwtRegisteredClaimNames.Jti, jti) }; var exp = DateTime.UtcNow.AddMinutes(15); var token = new JwtSecurityToken(issuer, audience, claims, expires: exp, signingCredentials: creds); return (new JwtSecurityTokenHandler().WriteToken(token), jti, exp); } }