using Api.Models; using Api.Models.DTOs; using Api.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace Api.Controllers { [ApiController] [Route("api/[controller]")] public class AuthController : ControllerBase { private readonly AppDbContext _context; private readonly IOAuthValidationService _oauthValidationService; private readonly IJwtService _jwtService; private readonly ILogger _logger; public AuthController( AppDbContext context, IOAuthValidationService oauthValidationService, IJwtService jwtService, ILogger logger) { _context = context; _oauthValidationService = oauthValidationService; _jwtService = jwtService; _logger = logger; } /// /// Authenticates a user with an OAuth ID token and returns a custom access token /// /// Authentication request containing ID token and provider /// Custom access token and user information [HttpPost("authenticate")] public async Task> AuthenticateAsync([FromBody] AuthenticateRequest request) { if (!ModelState.IsValid) { return BadRequest(ModelState); } try { // Validate the ID token with the specified provider var (isValid, principal, errorMessage) = await _oauthValidationService .ValidateIdTokenAsync(request.IdToken, request.Provider); if (!isValid || principal == null) { _logger.LogWarning("Invalid ID token for provider {Provider}: {Error}", request.Provider, errorMessage); return BadRequest(new { error = "invalid_token", message = errorMessage ?? "Invalid ID token" }); } // Extract user information from the validated token var userInfo = request.Provider.ToLowerInvariant() == "microsoft" ? await _oauthValidationService.ExtractUserInfoAsync(principal, request.Provider, request.IdToken, request.AccessToken) : _oauthValidationService.ExtractUserInfo(principal, request.Provider); if (string.IsNullOrEmpty(userInfo.Email)) { _logger.LogWarning("No email found in {Provider} token", request.Provider); return BadRequest(new { error = "invalid_token", message = "Email claim is required" }); } // Parse the provider enum if (!Enum.TryParse(request.Provider, true, out var providerEnum)) { return BadRequest(new { error = "invalid_provider", message = $"Unsupported provider: {request.Provider}" }); } // Find or create user var (user, isNewUser) = await FindOrCreateUserAsync(userInfo, providerEnum); // Update last login time user.LastLoginAt = DateTime.UtcNow; // Update or create OAuth provider record await UpdateUserOAuthProviderAsync(user, userInfo, providerEnum); await _context.SaveChangesAsync(); // Generate custom access token var accessToken = _jwtService.GenerateToken(user); var expiresAt = _jwtService.GetTokenExpiration(accessToken); // Prepare response var userProfile = new UserProfile { Id = user.Id, Email = user.Email, FirstName = user.FirstName, LastName = user.LastName, ProfilePictureUrl = user.ProfilePictureUrl, CreatedAt = user.CreatedAt, LastLoginAt = user.LastLoginAt, Providers = user.OAuthProviders.Select(p => p.Provider.ToString()).ToList() }; var response = new AuthenticateResponse { AccessToken = accessToken, ExpiresAt = expiresAt, User = userProfile, IsNewUser = isNewUser }; _logger.LogInformation("User {Email} authenticated successfully with {Provider} (New: {IsNew})", user.Email, request.Provider, isNewUser); return Ok(response); } catch (Exception ex) { _logger.LogError(ex, "Authentication failed for provider {Provider}", request.Provider); return StatusCode(500, new { error = "internal_error", message = "Authentication failed" }); } } /// /// Gets the current user's profile information /// /// User profile information [HttpGet("me")] [Authorize] public async Task> GetCurrentUserAsync() { try { var userIdClaim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier); if (userIdClaim == null || !int.TryParse(userIdClaim.Value, out var userId)) { return Unauthorized(new { error = "invalid_token", message = "User ID not found in token" }); } var user = await _context.Users .Include(u => u.OAuthProviders) .FirstOrDefaultAsync(u => u.Id == userId && u.IsActive); if (user == null) { return NotFound(new { error = "user_not_found", message = "User not found or inactive" }); } var userProfile = new UserProfile { Id = user.Id, Email = user.Email, FirstName = user.FirstName, LastName = user.LastName, ProfilePictureUrl = user.ProfilePictureUrl, CreatedAt = user.CreatedAt, LastLoginAt = user.LastLoginAt, Providers = user.OAuthProviders.Select(p => p.Provider.ToString()).ToList() }; return Ok(userProfile); } catch (Exception ex) { _logger.LogError(ex, "Error retrieving current user profile"); return StatusCode(500, new { error = "internal_error", message = "Failed to retrieve user profile" }); } } /// /// Revokes the current access token (logout) /// /// Success message [HttpPost("logout")] [Authorize] public IActionResult Logout() { // In a real application, you might want to maintain a blacklist of revoked tokens // For now, we'll just return success as JWT tokens are stateless _logger.LogInformation("User logged out"); return Ok(new { message = "Logged out successfully" }); } private async Task<(User User, bool IsNewUser)> FindOrCreateUserAsync( (string Email, string? FirstName, string? LastName, string? ProfilePictureUrl, string ProviderId) userInfo, OAuthProvider provider) { var existingUser = await _context.Users .Include(u => u.OAuthProviders) .FirstOrDefaultAsync(u => u.Email == userInfo.Email && u.IsActive); if (existingUser != null) { // Update user information if it has changed var hasChanges = false; if (!string.IsNullOrEmpty(userInfo.FirstName) && existingUser.FirstName != userInfo.FirstName) { existingUser.FirstName = userInfo.FirstName; hasChanges = true; } if (!string.IsNullOrEmpty(userInfo.LastName) && existingUser.LastName != userInfo.LastName) { existingUser.LastName = userInfo.LastName; hasChanges = true; } if (!string.IsNullOrEmpty(userInfo.ProfilePictureUrl) && existingUser.ProfilePictureUrl != userInfo.ProfilePictureUrl) { existingUser.ProfilePictureUrl = userInfo.ProfilePictureUrl; hasChanges = true; } if (hasChanges) { _logger.LogInformation("Updated user information for {Email}", userInfo.Email); } return (existingUser, false); } // Create new user var newUser = new User { Email = userInfo.Email, FirstName = userInfo.FirstName, LastName = userInfo.LastName, ProfilePictureUrl = userInfo.ProfilePictureUrl, CreatedAt = DateTime.UtcNow, IsActive = true }; _context.Users.Add(newUser); _logger.LogInformation("Created new user for {Email}", userInfo.Email); return (newUser, true); } private async Task UpdateUserOAuthProviderAsync( User user, (string Email, string? FirstName, string? LastName, string? ProfilePictureUrl, string ProviderId) userInfo, OAuthProvider provider) { var existingProvider = user.OAuthProviders .FirstOrDefault(p => p.Provider == provider); if (existingProvider != null) { // Update existing provider record existingProvider.ProviderEmail = userInfo.Email; existingProvider.ProviderName = $"{userInfo.FirstName} {userInfo.LastName}".Trim(); existingProvider.LastUsedAt = DateTime.UtcNow; } else { // Create new provider record var newProvider = new UserOAuthProvider { UserId = user.Id, Provider = provider, ProviderId = userInfo.ProviderId, ProviderEmail = userInfo.Email, ProviderName = $"{userInfo.FirstName} {userInfo.LastName}".Trim(), CreatedAt = DateTime.UtcNow, LastUsedAt = DateTime.UtcNow }; user.OAuthProviders.Add(newProvider); _logger.LogInformation("Added {Provider} OAuth provider for user {Email}", provider, user.Email); } await Task.CompletedTask; // Make the method actually async } } }