using System; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using TaskTrackingSystem.Shared; using TaskTrackingSystem.Shared.Models.Auth; using TaskTrackingSystem.Database; using TaskTrackingSystem.Database.AppDbContextModels; namespace TaskTrackingSystem.WebApi.Features.Auth { public class AuthService { private readonly AppDbContext _db; private readonly IConfiguration _configuration; private readonly IPasswordHasher _passwordHasher; public AuthService(AppDbContext db, IConfiguration configuration, IPasswordHasher passwordHasher) { _db = db; _configuration = configuration; _passwordHasher = passwordHasher; } public async Task LoginAsync(LoginDto loginDto) { var user = await _db.Users .Include(u => u.Role) .FirstOrDefaultAsync(u => (u.Username == loginDto.UsernameOrEmail || u.Email == loginDto.UsernameOrEmail) && !u.IsDeleted); if (user == null) return null; if (!user.IsActive) { throw new InvalidOperationException("This account has been deactivated."); } var passwordCheck = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, loginDto.Password); if (passwordCheck == PasswordVerificationResult.Failed) { if (!IsLegacyPasswordMatch(user.PasswordHash, loginDto.Password)) { return null; } user.PasswordHash = _passwordHasher.HashPassword(user, loginDto.Password); await _db.SaveChangesAsync(); } var roleId = user.RoleId; if (roleId <= 0 && user.Role != null) { roleId = user.Role.Id; } var roleName = user.Role?.Name ?? string.Empty; var token = GenerateJwtToken(user.Id.ToString(), user.Username, user.Email, roleId, roleName); return new AuthResponseDto { Token = token, Username = user.Username, Email = user.Email, RoleId = roleId, RoleName = roleName }; } public async Task RegisterAsync(RegisterDto dto) { var usernameExists = await _db.Users.AnyAsync(u => u.Username == dto.Username && !u.IsDeleted); if (usernameExists) { throw new InvalidOperationException("Username is already taken."); } var emailExists = await _db.Users.AnyAsync(u => u.Email == dto.Email && !u.IsDeleted); if (emailExists) { throw new InvalidOperationException("Email is already registered."); } // Find default Role (Employee role, or fallback to lowest ID) var defaultRole = await _db.Roles.FirstOrDefaultAsync(r => r.Name == "Employee" && r.IsDeleted != true) ?? await _db.Roles.FirstOrDefaultAsync(r => r.IsDeleted != true); if (defaultRole == null) { throw new InvalidOperationException("No system roles configured. Registration failed."); } var user = new TaskTrackingSystem.Database.AppDbContextModels.User { Username = dto.Username, FirstName = dto.FirstName, LastName = dto.LastName, Email = dto.Email, PasswordHash = string.Empty, Phone = dto.Phone, RoleId = defaultRole.Id, IsActive = true, CreatedAt = DateTime.UtcNow }; user.PasswordHash = _passwordHasher.HashPassword(user, dto.Password); _db.Users.Add(user); await _db.SaveChangesAsync(); var token = GenerateJwtToken(user.Id.ToString(), user.Username, user.Email, defaultRole.Id, defaultRole.Name); return new AuthResponseDto { Token = token, Username = user.Username, Email = user.Email, RoleId = defaultRole.Id, RoleName = defaultRole.Name }; } public async Task ResetPasswordAsync(ResetPasswordDto dto) { if (string.IsNullOrWhiteSpace(dto.UsernameOrEmail) || string.IsNullOrWhiteSpace(dto.RecoveryCode) || string.IsNullOrWhiteSpace(dto.NewPassword)) { return Result.Failure(ResultMessages.FillAllFields, 400); } var expectedCode = _configuration["PasswordReset:RecoveryCode"] ?? throw new InvalidOperationException("PasswordReset:RecoveryCode must be configured."); if (!string.Equals(dto.RecoveryCode.Trim(), expectedCode, StringComparison.Ordinal)) { return Result.Failure("Invalid recovery code.", 400); } var user = await _db.Users .FirstOrDefaultAsync(u => (u.Username == dto.UsernameOrEmail || u.Email == dto.UsernameOrEmail) && !u.IsDeleted); if (user == null) { return Result.Failure("Account not found.", 404); } user.PasswordHash = _passwordHasher.HashPassword(user, dto.NewPassword); user.UpdatedAt = DateTime.UtcNow; _db.Users.Update(user); await _db.SaveChangesAsync(); return Result.Success(200); } private string GenerateJwtToken(string userId, string username, string email, long roleId, string roleName) { var jwtSettings = _configuration.GetSection("JwtSettings"); var keyStr = jwtSettings["Key"] ?? throw new InvalidOperationException("JwtSettings:Key must be configured."); var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(keyStr)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var claims = new[] { new Claim(ClaimTypes.NameIdentifier, userId), new Claim(ClaimTypes.Name, username), new Claim(ClaimTypes.Email, email), new Claim("role_id", roleId.ToString()), new Claim(ClaimTypes.Role, roleName) }; var token = new JwtSecurityToken( issuer: jwtSettings["Issuer"] ?? throw new InvalidOperationException("JwtSettings:Issuer must be configured."), audience: jwtSettings["Audience"] ?? throw new InvalidOperationException("JwtSettings:Audience must be configured."), claims: claims, expires: DateTime.UtcNow.AddDays(7), signingCredentials: creds ); return new JwtSecurityTokenHandler().WriteToken(token); } private static bool IsLegacyPasswordMatch(string passwordHash, string password) { return string.Equals(passwordHash, "HASHED_" + password, StringComparison.Ordinal); } } }