File size: 7,722 Bytes
4e645a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
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<TaskTrackingSystem.Database.AppDbContextModels.User> _passwordHasher;

        public AuthService(AppDbContext db, IConfiguration configuration, IPasswordHasher<TaskTrackingSystem.Database.AppDbContextModels.User> passwordHasher)
        {
            _db = db;
            _configuration = configuration;
            _passwordHasher = passwordHasher;
        }

        public async Task<AuthResponseDto?> 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<AuthResponseDto> 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<Result> 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);
        }
    }
}