using System.IdentityModel.Tokens.Jwt; using System.Net.Http.Headers; using System.Security.Claims; using System.Text.Json; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using ProposalSystem.Domain.Entities; using ProposalSystem.Infrastructure.Data; namespace ProposalSystem.Api.Controllers; [ApiController] [Route("api/[controller]")] public class AuthController : ControllerBase { private readonly ProposalDbContext _db; private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _config; public AuthController(ProposalDbContext db, IHttpClientFactory httpClientFactory, IConfiguration config) { _db = db; _httpClientFactory = httpClientFactory; _config = config; } [HttpPost("callback")] public async Task> Callback([FromBody] AuthCallbackRequest request, CancellationToken ct) { var domain = _config["Auth:CognitoDomain"]; var clientId = _config["Auth:ClientId"]; if (string.IsNullOrEmpty(domain) || string.IsNullOrEmpty(clientId)) return StatusCode(500, new { message = "Auth not configured" }); var tokenResponse = await ExchangeCodeAsync(domain, clientId, request.Code, request.RedirectUri, ct); if (tokenResponse == null) return BadRequest(new { message = "Failed to exchange authorization code" }); var handler = new JwtSecurityTokenHandler(); var idToken = handler.ReadJwtToken(tokenResponse.IdToken); var sub = idToken.Claims.FirstOrDefault(c => c.Type == "sub")?.Value ?? throw new InvalidOperationException("No sub claim in ID token"); var email = idToken.Claims.FirstOrDefault(c => c.Type == "email")?.Value ?? ""; var name = idToken.Claims.FirstOrDefault(c => c.Type == "name")?.Value ?? idToken.Claims.FirstOrDefault(c => c.Type == "cognito:username")?.Value ?? email.Split('@')[0]; var groups = idToken.Claims.Where(c => c.Type == "cognito:groups").Select(c => c.Value).ToList(); var role = groups.Contains("sysadmins") ? UserRole.SysAdmin : groups.Contains("admins") ? UserRole.Admin : UserRole.Dispatcher; var user = await _db.Users.FirstOrDefaultAsync(u => u.CognitoSub == sub, ct); if (user == null) { user = new User { Id = Guid.NewGuid(), CognitoSub = sub, Email = email, DisplayName = name, Role = role, IsActive = true, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, }; _db.Users.Add(user); await _db.SaveChangesAsync(ct); } else if (user.Email != email || user.DisplayName != name) { user.Email = email; user.DisplayName = name; user.UpdatedAt = DateTime.UtcNow; await _db.SaveChangesAsync(ct); } return Ok(new AuthResponse( user.Id.ToString(), user.Email, user.DisplayName, user.Role.ToString(), tokenResponse.AccessToken )); } private async Task ExchangeCodeAsync( string domain, string clientId, string code, string redirectUri, CancellationToken ct) { var client = _httpClientFactory.CreateClient(); var tokenUrl = $"https://{domain}/oauth2/token"; var content = new FormUrlEncodedContent(new Dictionary { ["grant_type"] = "authorization_code", ["client_id"] = clientId, ["code"] = code, ["redirect_uri"] = redirectUri, }); var response = await client.PostAsync(tokenUrl, content, ct); if (!response.IsSuccessStatusCode) return null; var json = await response.Content.ReadAsStringAsync(ct); return JsonSerializer.Deserialize(json); } } public record AuthCallbackRequest(string Code, string RedirectUri); public record AuthResponse(string Id, string Email, string DisplayName, string Role, string Token); internal class CognitoTokenResponse { [System.Text.Json.Serialization.JsonPropertyName("access_token")] public string AccessToken { get; set; } = ""; [System.Text.Json.Serialization.JsonPropertyName("id_token")] public string IdToken { get; set; } = ""; [System.Text.Json.Serialization.JsonPropertyName("refresh_token")] public string RefreshToken { get; set; } = ""; [System.Text.Json.Serialization.JsonPropertyName("token_type")] public string TokenType { get; set; } = ""; [System.Text.Json.Serialization.JsonPropertyName("expires_in")] public int ExpiresIn { get; set; } }