< Summary

Information
Class: Backend.Controllers.ChangePasswordRequest
Assembly: Backend
File(s): D:\a\smart-meal-planner\smart-meal-planner\backend\Backend\Controllers\AuthController.cs
Line coverage
100%
Covered lines: 2
Uncovered lines: 0
Coverable lines: 2
Total lines: 380
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity Line coverage
get_OldPassword()100%1100%
get_NewPassword()100%1100%

File(s)

D:\a\smart-meal-planner\smart-meal-planner\backend\Backend\Controllers\AuthController.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Identity.Data;
 2using Microsoft.AspNetCore.Mvc;
 3using Backend.Model;
 4using Backend.Services;
 5using Serilog;
 6using Microsoft.AspNetCore.Authorization;
 7using System.Security.Claims;
 8
 9namespace Backend.Controllers
 10{
 11    [ApiController]
 12    [Route("api/[controller]")]
 13    public class AuthController : ControllerBase
 14    {
 15        private readonly ITokenService _tokenService;
 16        private readonly IUserService _userService;
 17        private readonly IEmailService _emailService;
 18        private readonly ILogger<AuthController> _logger;
 19
 20        public AuthController(ITokenService tokenService, IUserService userService, IEmailService emailService, ILogger<
 21        {
 22            _tokenService = tokenService;
 23            _userService = userService;
 24            _emailService = emailService;
 25            _logger = logger;
 26        }
 27
 28        // POST: api/auth/register
 29        [HttpPost("register")]
 30        public async Task<IActionResult> Register(RegisterRequest request)
 31        {
 32            _logger.LogDebug("Request: {Request}", request);
 33            if (request.Email == null || request.Password == null)
 34            {
 35                _logger.LogWarning("Registration failed: Email or password is null.");
 36                return BadRequest("Email and password are required.");
 37            }
 38
 39            if (!ModelState.IsValid)
 40            {
 41                _logger.LogWarning("Registration failed: Model state is invalid.");
 42                _logger.LogDebug("ModelState errors: {Errors}", ModelState.Values.SelectMany(v => v.Errors).Select(e => 
 43                return BadRequest(ModelState);
 44            }
 45
 46            // Check if user already exists
 47            if (await _userService.GetByEmailAsync(request.Email) != null)
 48            {
 49                _logger.LogWarning("Registration failed: User with email {Email} already exists.", request.Email);
 50                return BadRequest("User already exists.");
 51            }
 52
 53            var user = await _userService.CreateUserAsync(request.Email, request.Password);
 54            if (user == null)
 55            {
 56                _logger.LogError("Registration failed: User creation returned null.");
 57                _logger.LogDebug("Failed to create user with email {Email}.", request.Email);
 58                return StatusCode(500, "Failed to create user.");
 59            }
 60
 61            var result = await GenerateTokens(user);
 62            if (result.Error != null)
 63            {
 64                _logger.LogError("Registration failed: {Error}", result.Error);
 65                _logger.LogDebug("Failed to generate tokens for user with email {Email}.", request.Email);
 66                return result.Error ?? StatusCode(500, "Failed to generate tokens.");
 67            }
 68
 69            _logger.LogInformation("User registered successfully with email {Email}.", request.Email);
 70            _logger.LogDebug("Generated tokens for user with email {Email}.", request.Email);
 71
 72            // Return the tokens
 73            return Ok(new TokenResponse
 74            {
 75                AccessToken = result.AccessToken,
 76                RefreshToken = result.RefreshToken?.Token!,
 77            });
 78        }
 79
 80        // POST: api/auth/login
 81        [HttpPost("login")]
 82        public async Task<IActionResult> Login(LoginRequest request)
 83        {
 84            _logger.LogDebug("Login request: {Request}", request);
 85            if (request.Email == null || request.Password == null)
 86            {
 87                _logger.LogWarning("Login failed: Email or password is null.");
 88                return BadRequest("Email and password are required.");
 89            }
 90
 91            if (!ModelState.IsValid)
 92            {
 93                _logger.LogWarning("Login failed: Model state is invalid.");
 94                _logger.LogDebug("ModelState errors: {Errors}", ModelState.Values.SelectMany(v => v.Errors).Select(e => 
 95                return BadRequest(ModelState);
 96            }
 97
 98            var user = await _userService.GetByEmailAsync(request.Email);
 99            if (user == null)
 100            {
 101                _logger.LogWarning("Login failed: User with email {Email} not found.", request.Email);
 102                return Unauthorized("Invalid email or password.");
 103            }
 104
 105            if (!_userService.VerifyPasswordHash(request.Password, user))
 106            {
 107                _logger.LogWarning("Login failed: Invalid password for user with email {Email}.", request.Email);
 108                return Unauthorized("Invalid email or password.");
 109            }
 110
 111            var result = await GenerateTokens(user);
 112            if (result.Error != null)
 113            {
 114                _logger.LogError("Login failed: {Error}", result.Error);
 115                _logger.LogDebug("Failed to generate tokens for user with email {Email}.", request.Email);
 116                return result.Error ?? StatusCode(500, "Failed to generate tokens.");
 117            }
 118
 119            _logger.LogInformation("User logged in successfully with email {Email}.", request.Email);
 120
 121            return Ok(new TokenResponse
 122            {
 123                AccessToken = result.AccessToken,
 124                RefreshToken = result.RefreshToken?.Token!,
 125            });
 126        }
 127
 128        // POST: api/auth/refresh
 129        [Authorize]
 130        [HttpPost("refresh")]
 131        public async Task<IActionResult> Refresh([FromBody] string refreshToken)
 132        {
 133            if (string.IsNullOrEmpty(refreshToken))
 134            {
 135                _logger.LogWarning("Null refresh token provided.");
 136                return BadRequest("Refresh token is required.");
 137            }
 138
 139            if (!ModelState.IsValid)
 140            {
 141                _logger.LogWarning("Model state is invalid for refresh token request.");
 142                _logger.LogDebug("ModelState errors: {Errors}", ModelState.Values.SelectMany(v => v.Errors).Select(e => 
 143                _logger.LogDebug("Refresh token: {RefreshToken}", refreshToken);
 144                return BadRequest(ModelState);
 145            }
 146
 147            var oldRefreshToken = await _tokenService.FindRefreshToken(refreshToken);
 148
 149            // if the refresh token is not found or is expired/revoked, return Unauthorized
 150            // this is a security measure to prevent token reuse
 151            if (oldRefreshToken == null || oldRefreshToken.Expires < DateTime.UtcNow
 152                || oldRefreshToken.IsRevoked)
 153            {
 154                _logger.LogWarning("Invalid or expired refresh token provided: {RefreshToken}", refreshToken);
 155                return Unauthorized("Invalid refresh token.");
 156            }
 157
 158            var user = await _userService.GetByIdAsync(oldRefreshToken.UserId);
 159            if (user == null)
 160            {
 161                _logger.LogWarning("User not found for refresh token: {RefreshToken}", refreshToken);
 162                return Unauthorized("Invalid user.");
 163            }
 164
 165            // Mark the old refresh token as revoked
 166            oldRefreshToken.IsRevoked = true;
 167            _logger.LogDebug("Revoking old refresh token: {RefreshToken}", refreshToken);
 168
 169            // Generate new tokens
 170            var result = await GenerateTokens(user);
 171            if (result.Error != null)
 172            {
 173                _logger.LogError("Failed to generate new tokens: {Error}", result.Error);
 174                _logger.LogDebug("Failed to generate new tokens for user with email {Email}.", user.Email);
 175                return result.Error ?? StatusCode(500, "Failed to generate tokens.");
 176            }
 177
 178            _logger.LogInformation("Tokens refreshed successfully for user with email {Email}.", user.Email);
 179
 180            return Ok(new TokenResponse
 181            {
 182                AccessToken = result.AccessToken,
 183                RefreshToken = result.RefreshToken.Token!
 184            });
 185        }
 186
 187        // POST: api/auth/logout
 188        [Authorize]
 189        [HttpPost("logout")]
 190        public async Task<IActionResult> Logout([FromBody] string refreshToken)
 191        {
 192            if (string.IsNullOrEmpty(refreshToken))
 193            {
 194                _logger.LogWarning("Logout failed: Null refresh token provided.");
 195                return BadRequest("Refresh token is required.");
 196            }
 197
 198            if (!ModelState.IsValid)
 199            {
 200                _logger.LogWarning("Logout failed: Model state is invalid.");
 201                _logger.LogDebug("ModelState errors: {Errors}", ModelState.Values.SelectMany(v => v.Errors).Select(e => 
 202                _logger.LogDebug("Refresh token: {RefreshToken}", refreshToken);
 203                return BadRequest(ModelState);
 204            }
 205
 206            var refreshTokenObj = await _tokenService.FindRefreshToken(refreshToken);
 207
 208            // If the refresh token is not found, we can still return OK
 209            // This is to ensure that the client can safely call logout without worrying about the token's existence
 210            // This is a common practice to avoid leaking information about token validity
 211            if (refreshTokenObj == null)
 212                return Ok();
 213
 214            await _tokenService.RevokeRefreshToken(refreshTokenObj);
 215            return Ok();
 216        }
 217
 218        // PUT: api/auth/change-password
 219        [Authorize]
 220        [HttpPut("change-password")]
 221        public async Task<IActionResult> ChangePassword(ChangePasswordRequest request)
 222        {
 223            if (!ModelState.IsValid)
 224            {
 225                _logger.LogWarning("Change password failed: Model state is invalid.");
 226                _logger.LogDebug("ModelState errors: {Errors}", ModelState.Values.SelectMany(v => v.Errors).Select(e => 
 227                return BadRequest(ModelState);
 228            }
 229
 230            if (string.IsNullOrEmpty(request.OldPassword) || string.IsNullOrEmpty(request.NewPassword))
 231            {
 232                _logger.LogWarning("Change password failed: Old password or new password is null or empty.");
 233                return BadRequest("Old password and new password are required.");
 234            }
 235
 236
 237            var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
 238            if (string.IsNullOrEmpty(userId))
 239            {
 240                _logger.LogWarning("Change password failed: User ID not found in claims.");
 241                return Unauthorized();
 242            }
 243
 244            try
 245            {
 246                await _userService.ChangePasswordAsync(userId, request.OldPassword, request.NewPassword);
 247            }
 248            catch (UnauthorizedAccessException ex)
 249            {
 250                _logger.LogWarning(ex, "Change password unauthorized for user ID {UserId}: {Message}", userId, ex.Messag
 251                return Unauthorized("Old password is incorrect.");
 252            }
 253            catch (Exception ex)
 254            {
 255                _logger.LogError(ex, "Change password failed for user ID {UserId}: {Message}", userId, ex.Message);
 256                return StatusCode(500, "An error occurred while changing the password. " + ex.Message);
 257            }
 258
 259            _logger.LogInformation("Password changed successfully for user ID {UserId}.", userId);
 260
 261            return Ok(new { message = "Password updated successfully" });
 262        }
 263
 264        [HttpPost("forgot-password")]
 265        public async Task<IActionResult> ForgotPassword(ForgotPasswordRequest request)
 266        {
 267            var user = await _userService.GetByEmailAsync(request.Email);
 268            if (user == null)
 269            {
 270                _logger.LogInformation("Forgot password request for non-existing email: {Email}", request.Email);
 271                return Ok("If that email exists, a reset link has been sent."); // don’t reveal if email exists
 272            }
 273
 274            var token = _tokenService.GenerateResetToken(user);
 275            if (string.IsNullOrEmpty(token))
 276            {
 277                _logger.LogError("Failed to generate reset token for user with email {Email}.", request.Email);
 278                return StatusCode(500, "Failed to generate reset token.");
 279            }
 280
 281            _logger.LogInformation("Reset token generated for user with email {Email}: {Token}", request.Email, token);
 282
 283            try
 284            {
 285                await _emailService.SendPasswordResetEmailAsync(user.Email, token);
 286                _logger.LogInformation("Reset password email sent to {Email}.", user.Email);
 287            }
 288            catch (Exception ex)
 289            {
 290                _logger.LogError(ex, "Failed to send reset password email to {Email}: {Message}", user.Email, ex.Message
 291                return StatusCode(500, "Failed to send reset email.");
 292            }
 293
 294            return Ok("If that email exists, a reset link has been sent.");
 295        }
 296
 297        [HttpPost("reset-password")]
 298        public async Task<IActionResult> ResetPassword(ResetPasswordRequest request)
 299        {
 300            var userId = _tokenService.ValidateResetToken(request.ResetCode);
 301            if (userId == null)
 302            {
 303                _logger.LogWarning("Reset password failed: Invalid or expired token.");
 304                _logger.LogDebug("Reset token: {Token}", request.ResetCode);
 305                return BadRequest("Invalid or expired token");
 306            }
 307
 308            var success = await _userService.UpdatePasswordAsync(userId.Value, request.NewPassword);
 309            if (!success)
 310            {
 311                _logger.LogError("Failed to reset password for user ID {UserId}.", userId);
 312                _logger.LogDebug("Reset token: {Token}", request.ResetCode);
 313                return StatusCode(500, "Could not reset password");
 314            }
 315
 316            _logger.LogInformation("Password reset successfully for user ID {UserId}.", userId);
 317            return Ok("Password has been reset successfully.");
 318        }
 319
 320        private async Task<TokenGenerateResult> GenerateTokens(User user)
 321        {
 322            var result = new TokenGenerateResult();
 323            try
 324            {
 325                var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
 326                result.AccessToken = _tokenService.GenerateAccessToken(user);
 327                result.RefreshToken = await _tokenService.GenerateRefreshToken(user, ip);
 328            }
 329            catch (Exception ex)
 330            {
 331                _logger.LogError(ex, "Error generating token for user with email {Email}: {message}", user.Email, ex.Mes
 332                result.Error = StatusCode(500, $"Internal server error: Failed to generate token.");
 333                return result;
 334            }
 335
 336            if (result.AccessToken == null || result.RefreshToken == null)
 337            {
 338                _logger.LogError("Failed to generate access token or refresh token for user with email {Email}.", user.E
 339                _logger.LogDebug("AccessToken: {AccessToken}, RefreshToken: {RefreshToken}", result.AccessToken, result.
 340                result.Error = StatusCode(500, "Failed to generate token.");
 341                return result;
 342            }
 343
 344            _logger.LogDebug("Generated access token and refresh token for user with email {Email}.", user.Email);
 345            _logger.LogDebug("AccessToken: {AccessToken}, RefreshToken: {RefreshToken}", result.AccessToken, result.Refr
 346            return result;
 347        }
 348    }
 349
 350    public class TokenGenerateResult
 351    {
 352        public IActionResult? Error { get; set; }
 353        public string AccessToken { get; set; } = null!;
 354        public RefreshToken RefreshToken { get; set; } = null!;
 355    }
 356
 357    public class LoginRequest
 358    {
 359        public required string Email { get; set; }
 360        public required string Password { get; set; }
 361    }
 362
 363    public class TokenResponse
 364    {
 365        public required string AccessToken { get; set; }
 366        public required string RefreshToken { get; set; }
 367    }
 368
 369    public class ChangePasswordRequest
 370    {
 14371        public required string OldPassword { get; set; }
 13372        public required string NewPassword { get; set; }
 373    }
 374
 375    public class ResetPasswordRequest
 376    {
 377        public required string ResetCode { get; set; }
 378        public required string NewPassword { get; set; }
 379    }
 380}