using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using UniVerse.Application.DTOs.Auth; using UniVerse.Application.DTOs.Users; using UniVerse.Application.Interfaces; using UniVerse.Application.Mappings; using UniVerse.Domain.Entities; using UniVerse.Domain.Enums; using UniVerse.Domain.Exceptions; using UniVerse.Infrastructure.Data; namespace UniVerse.Infrastructure.Services; public class AuthService : IAuthService { private readonly AppDbContext _db; private readonly IConfiguration _config; private readonly IGamificationService _gamification; public AuthService(AppDbContext db, IConfiguration config, IGamificationService gamification) { _db = db; _config = config; _gamification = gamification; } public async Task LoginWithMicrosoftAsync(string authorizationCode) { // Stub: in production, exchange authorizationCode with Microsoft Entra ID // For now, create/find a demo user throw new NotImplementedException( "Microsoft Entra ID integration not yet configured. Use /api/v1/auth/login/dev in Development mode."); } public async Task DevLoginAsync(string email, string? displayName, UserRole role) { var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == email); if (user == null) { user = new User { Email = email, DisplayName = displayName ?? email.Split('@')[0], Role = role, IsActive = true }; _db.Users.Add(user); await _db.SaveChangesAsync(); // Create profile based on role if (role == UserRole.Student) { _db.StudentProfiles.Add(new StudentProfile { UserId = user.Id }); await _db.SaveChangesAsync(); } else if (role == UserRole.Teacher) { _db.TeacherProfiles.Add(new TeacherProfile { UserId = user.Id }); await _db.SaveChangesAsync(); } } if (!user.IsActive) throw new ForbiddenException("Account is deactivated."); var accessToken = GenerateAccessToken(user); var refreshToken = await GenerateRefreshTokenAsync(user.Id); return new AuthResponse( accessToken, DateTime.UtcNow.AddMinutes(GetAccessTokenExpiration()), user.ToAuthDto() ); } public async Task RefreshTokenAsync(string refreshToken) { var token = await _db.RefreshTokens .Include(rt => rt.User) .FirstOrDefaultAsync(rt => rt.Token == refreshToken); if (token == null || !token.IsActive) throw new ForbiddenException("Invalid or expired refresh token."); // Revoke old token token.RevokedAt = DateTime.UtcNow; // Generate new tokens var accessToken = GenerateAccessToken(token.User); var newRefreshToken = await GenerateRefreshTokenAsync(token.UserId); await _db.SaveChangesAsync(); return new AuthResponse( accessToken, DateTime.UtcNow.AddMinutes(GetAccessTokenExpiration()), token.User.ToAuthDto() ); } public async Task RevokeRefreshTokenAsync(string refreshToken) { var token = await _db.RefreshTokens.FirstOrDefaultAsync(rt => rt.Token == refreshToken); if (token != null && token.IsActive) { token.RevokedAt = DateTime.UtcNow; await _db.SaveChangesAsync(); } } public async Task GetCurrentUserAsync(int userId) { var user = await _db.Users.FindAsync(userId) ?? throw new NotFoundException("User", userId); return user.ToDto(_gamification.CalculateLevel(user.Xp)); } private string GenerateAccessToken(User user) { var key = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(_config["Jwt:Secret"]!)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var claims = new[] { new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), new Claim(JwtRegisteredClaimNames.Email, user.Email), new Claim(ClaimTypes.Role, user.Role.ToString()), new Claim("display_name", user.DisplayName ?? "") }; var token = new JwtSecurityToken( issuer: _config["Jwt:Issuer"], audience: _config["Jwt:Audience"], claims: claims, expires: DateTime.UtcNow.AddMinutes(GetAccessTokenExpiration()), signingCredentials: creds ); return new JwtSecurityTokenHandler().WriteToken(token); } private async Task GenerateRefreshTokenAsync(int userId) { var randomBytes = RandomNumberGenerator.GetBytes(64); var tokenString = Convert.ToBase64String(randomBytes); var refreshToken = new RefreshToken { UserId = userId, Token = tokenString, ExpiresAt = DateTime.UtcNow.AddDays(GetRefreshTokenExpiration()), CreatedAt = DateTime.UtcNow }; _db.RefreshTokens.Add(refreshToken); await _db.SaveChangesAsync(); return tokenString; } private int GetAccessTokenExpiration() => int.Parse(_config["Jwt:AccessTokenExpirationMinutes"] ?? "30"); private int GetRefreshTokenExpiration() => int.Parse(_config["Jwt:RefreshTokenExpirationDays"] ?? "30"); }