< Summary

Information
Class: Backend.Controllers.AuthController
Assembly: Backend
File(s): D:\a\smart-meal-planner\smart-meal-planner\backend\Backend\Controllers\AuthController.cs
Line coverage
92%
Covered lines: 163
Uncovered lines: 14
Coverable lines: 177
Total lines: 380
Line coverage: 92%
Branch coverage
86%
Covered branches: 69
Total branches: 80
Branch coverage: 86.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity Line coverage
.ctor(...)100%1100%
Register()87.5%16100%
Login()87.5%16100%
Refresh()87.5%1690%
Logout()66.66%661.53%
ChangePassword()100%10100%
ForgotPassword()100%4100%
ResetPassword()100%4100%
GenerateTokens()62.5%877.77%

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
 4020        public AuthController(ITokenService tokenService, IUserService userService, IEmailService emailService, ILogger<
 21        {
 4022            _tokenService = tokenService;
 4023            _userService = userService;
 4024            _emailService = emailService;
 4025            _logger = logger;
 4026        }
 27
 28        // POST: api/auth/register
 29        [HttpPost("register")]
 30        public async Task<IActionResult> Register(RegisterRequest request)
 31        {
 732            _logger.LogDebug("Request: {Request}", request);
 733            if (request.Email == null || request.Password == null)
 34            {
 235                _logger.LogWarning("Registration failed: Email or password is null.");
 236                return BadRequest("Email and password are required.");
 37            }
 38
 539            if (!ModelState.IsValid)
 40            {
 141                _logger.LogWarning("Registration failed: Model state is invalid.");
 142                _logger.LogDebug("ModelState errors: {Errors}", ModelState.Values.SelectMany(v => v.Errors).Select(e => 
 143                return BadRequest(ModelState);
 44            }
 45
 46            // Check if user already exists
 447            if (await _userService.GetByEmailAsync(request.Email) != null)
 48            {
 149                _logger.LogWarning("Registration failed: User with email {Email} already exists.", request.Email);
 150                return BadRequest("User already exists.");
 51            }
 52
 353            var user = await _userService.CreateUserAsync(request.Email, request.Password);
 354            if (user == null)
 55            {
 156                _logger.LogError("Registration failed: User creation returned null.");
 157                _logger.LogDebug("Failed to create user with email {Email}.", request.Email);
 158                return StatusCode(500, "Failed to create user.");
 59            }
 60
 261            var result = await GenerateTokens(user);
 262            if (result.Error != null)
 63            {
 164                _logger.LogError("Registration failed: {Error}", result.Error);
 165                _logger.LogDebug("Failed to generate tokens for user with email {Email}.", request.Email);
 166                return result.Error ?? StatusCode(500, "Failed to generate tokens.");
 67            }
 68
 169            _logger.LogInformation("User registered successfully with email {Email}.", request.Email);
 170            _logger.LogDebug("Generated tokens for user with email {Email}.", request.Email);
 71
 72            // Return the tokens
 173            return Ok(new TokenResponse
 174            {
 175                AccessToken = result.AccessToken,
 176                RefreshToken = result.RefreshToken?.Token!,
 177            });
 778        }
 79
 80        // POST: api/auth/login
 81        [HttpPost("login")]
 82        public async Task<IActionResult> Login(LoginRequest request)
 83        {
 784            _logger.LogDebug("Login request: {Request}", request);
 785            if (request.Email == null || request.Password == null)
 86            {
 287                _logger.LogWarning("Login failed: Email or password is null.");
 288                return BadRequest("Email and password are required.");
 89            }
 90
 591            if (!ModelState.IsValid)
 92            {
 193                _logger.LogWarning("Login failed: Model state is invalid.");
 194                _logger.LogDebug("ModelState errors: {Errors}", ModelState.Values.SelectMany(v => v.Errors).Select(e => 
 195                return BadRequest(ModelState);
 96            }
 97
 498            var user = await _userService.GetByEmailAsync(request.Email);
 499            if (user == null)
 100            {
 1101                _logger.LogWarning("Login failed: User with email {Email} not found.", request.Email);
 1102                return Unauthorized("Invalid email or password.");
 103            }
 104
 3105            if (!_userService.VerifyPasswordHash(request.Password, user))
 106            {
 1107                _logger.LogWarning("Login failed: Invalid password for user with email {Email}.", request.Email);
 1108                return Unauthorized("Invalid email or password.");
 109            }
 110
 2111            var result = await GenerateTokens(user);
 2112            if (result.Error != null)
 113            {
 1114                _logger.LogError("Login failed: {Error}", result.Error);
 1115                _logger.LogDebug("Failed to generate tokens for user with email {Email}.", request.Email);
 1116                return result.Error ?? StatusCode(500, "Failed to generate tokens.");
 117            }
 118
 1119            _logger.LogInformation("User logged in successfully with email {Email}.", request.Email);
 120
 1121            return Ok(new TokenResponse
 1122            {
 1123                AccessToken = result.AccessToken,
 1124                RefreshToken = result.RefreshToken?.Token!,
 1125            });
 7126        }
 127
 128        // POST: api/auth/refresh
 129        [Authorize]
 130        [HttpPost("refresh")]
 131        public async Task<IActionResult> Refresh([FromBody] string refreshToken)
 132        {
 11133            if (string.IsNullOrEmpty(refreshToken))
 134            {
 2135                _logger.LogWarning("Null refresh token provided.");
 2136                return BadRequest("Refresh token is required.");
 137            }
 138
 9139            if (!ModelState.IsValid)
 140            {
 0141                _logger.LogWarning("Model state is invalid for refresh token request.");
 0142                _logger.LogDebug("ModelState errors: {Errors}", ModelState.Values.SelectMany(v => v.Errors).Select(e => 
 0143                _logger.LogDebug("Refresh token: {RefreshToken}", refreshToken);
 0144                return BadRequest(ModelState);
 145            }
 146
 9147            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
 9151            if (oldRefreshToken == null || oldRefreshToken.Expires < DateTime.UtcNow
 9152                || oldRefreshToken.IsRevoked)
 153            {
 3154                _logger.LogWarning("Invalid or expired refresh token provided: {RefreshToken}", refreshToken);
 3155                return Unauthorized("Invalid refresh token.");
 156            }
 157
 6158            var user = await _userService.GetByIdAsync(oldRefreshToken.UserId);
 6159            if (user == null)
 160            {
 1161                _logger.LogWarning("User not found for refresh token: {RefreshToken}", refreshToken);
 1162                return Unauthorized("Invalid user.");
 163            }
 164
 165            // Mark the old refresh token as revoked
 5166            oldRefreshToken.IsRevoked = true;
 5167            _logger.LogDebug("Revoking old refresh token: {RefreshToken}", refreshToken);
 168
 169            // Generate new tokens
 5170            var result = await GenerateTokens(user);
 5171            if (result.Error != null)
 172            {
 4173                _logger.LogError("Failed to generate new tokens: {Error}", result.Error);
 4174                _logger.LogDebug("Failed to generate new tokens for user with email {Email}.", user.Email);
 4175                return result.Error ?? StatusCode(500, "Failed to generate tokens.");
 176            }
 177
 1178            _logger.LogInformation("Tokens refreshed successfully for user with email {Email}.", user.Email);
 179
 1180            return Ok(new TokenResponse
 1181            {
 1182                AccessToken = result.AccessToken,
 1183                RefreshToken = result.RefreshToken.Token!
 1184            });
 11185        }
 186
 187        // POST: api/auth/logout
 188        [Authorize]
 189        [HttpPost("logout")]
 190        public async Task<IActionResult> Logout([FromBody] string refreshToken)
 191        {
 2192            if (string.IsNullOrEmpty(refreshToken))
 193            {
 0194                _logger.LogWarning("Logout failed: Null refresh token provided.");
 0195                return BadRequest("Refresh token is required.");
 196            }
 197
 2198            if (!ModelState.IsValid)
 199            {
 0200                _logger.LogWarning("Logout failed: Model state is invalid.");
 0201                _logger.LogDebug("ModelState errors: {Errors}", ModelState.Values.SelectMany(v => v.Errors).Select(e => 
 0202                _logger.LogDebug("Refresh token: {RefreshToken}", refreshToken);
 0203                return BadRequest(ModelState);
 204            }
 205
 2206            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
 2211            if (refreshTokenObj == null)
 1212                return Ok();
 213
 1214            await _tokenService.RevokeRefreshToken(refreshTokenObj);
 1215            return Ok();
 2216        }
 217
 218        // PUT: api/auth/change-password
 219        [Authorize]
 220        [HttpPut("change-password")]
 221        public async Task<IActionResult> ChangePassword(ChangePasswordRequest request)
 222        {
 6223            if (!ModelState.IsValid)
 224            {
 1225                _logger.LogWarning("Change password failed: Model state is invalid.");
 1226                _logger.LogDebug("ModelState errors: {Errors}", ModelState.Values.SelectMany(v => v.Errors).Select(e => 
 1227                return BadRequest(ModelState);
 228            }
 229
 5230            if (string.IsNullOrEmpty(request.OldPassword) || string.IsNullOrEmpty(request.NewPassword))
 231            {
 1232                _logger.LogWarning("Change password failed: Old password or new password is null or empty.");
 1233                return BadRequest("Old password and new password are required.");
 234            }
 235
 236
 4237            var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
 4238            if (string.IsNullOrEmpty(userId))
 239            {
 1240                _logger.LogWarning("Change password failed: User ID not found in claims.");
 1241                return Unauthorized();
 242            }
 243
 244            try
 245            {
 3246                await _userService.ChangePasswordAsync(userId, request.OldPassword, request.NewPassword);
 1247            }
 1248            catch (UnauthorizedAccessException ex)
 249            {
 1250                _logger.LogWarning(ex, "Change password unauthorized for user ID {UserId}: {Message}", userId, ex.Messag
 1251                return Unauthorized("Old password is incorrect.");
 252            }
 1253            catch (Exception ex)
 254            {
 1255                _logger.LogError(ex, "Change password failed for user ID {UserId}: {Message}", userId, ex.Message);
 1256                return StatusCode(500, "An error occurred while changing the password. " + ex.Message);
 257            }
 258
 1259            _logger.LogInformation("Password changed successfully for user ID {UserId}.", userId);
 260
 1261            return Ok(new { message = "Password updated successfully" });
 6262        }
 263
 264        [HttpPost("forgot-password")]
 265        public async Task<IActionResult> ForgotPassword(ForgotPasswordRequest request)
 266        {
 4267            var user = await _userService.GetByEmailAsync(request.Email);
 4268            if (user == null)
 269            {
 1270                _logger.LogInformation("Forgot password request for non-existing email: {Email}", request.Email);
 1271                return Ok("If that email exists, a reset link has been sent."); // don’t reveal if email exists
 272            }
 273
 3274            var token = _tokenService.GenerateResetToken(user);
 3275            if (string.IsNullOrEmpty(token))
 276            {
 1277                _logger.LogError("Failed to generate reset token for user with email {Email}.", request.Email);
 1278                return StatusCode(500, "Failed to generate reset token.");
 279            }
 280
 2281            _logger.LogInformation("Reset token generated for user with email {Email}: {Token}", request.Email, token);
 282
 283            try
 284            {
 2285                await _emailService.SendPasswordResetEmailAsync(user.Email, token);
 1286                _logger.LogInformation("Reset password email sent to {Email}.", user.Email);
 1287            }
 1288            catch (Exception ex)
 289            {
 1290                _logger.LogError(ex, "Failed to send reset password email to {Email}: {Message}", user.Email, ex.Message
 1291                return StatusCode(500, "Failed to send reset email.");
 292            }
 293
 1294            return Ok("If that email exists, a reset link has been sent.");
 4295        }
 296
 297        [HttpPost("reset-password")]
 298        public async Task<IActionResult> ResetPassword(ResetPasswordRequest request)
 299        {
 3300            var userId = _tokenService.ValidateResetToken(request.ResetCode);
 3301            if (userId == null)
 302            {
 1303                _logger.LogWarning("Reset password failed: Invalid or expired token.");
 1304                _logger.LogDebug("Reset token: {Token}", request.ResetCode);
 1305                return BadRequest("Invalid or expired token");
 306            }
 307
 2308            var success = await _userService.UpdatePasswordAsync(userId.Value, request.NewPassword);
 2309            if (!success)
 310            {
 1311                _logger.LogError("Failed to reset password for user ID {UserId}.", userId);
 1312                _logger.LogDebug("Reset token: {Token}", request.ResetCode);
 1313                return StatusCode(500, "Could not reset password");
 314            }
 315
 1316            _logger.LogInformation("Password reset successfully for user ID {UserId}.", userId);
 1317            return Ok("Password has been reset successfully.");
 3318        }
 319
 320        private async Task<TokenGenerateResult> GenerateTokens(User user)
 321        {
 9322            var result = new TokenGenerateResult();
 323            try
 324            {
 9325                var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
 3326                result.AccessToken = _tokenService.GenerateAccessToken(user);
 3327                result.RefreshToken = await _tokenService.GenerateRefreshToken(user, ip);
 3328            }
 6329            catch (Exception ex)
 330            {
 6331                _logger.LogError(ex, "Error generating token for user with email {Email}: {message}", user.Email, ex.Mes
 6332                result.Error = StatusCode(500, $"Internal server error: Failed to generate token.");
 6333                return result;
 334            }
 335
 3336            if (result.AccessToken == null || result.RefreshToken == null)
 337            {
 0338                _logger.LogError("Failed to generate access token or refresh token for user with email {Email}.", user.E
 0339                _logger.LogDebug("AccessToken: {AccessToken}, RefreshToken: {RefreshToken}", result.AccessToken, result.
 0340                result.Error = StatusCode(500, "Failed to generate token.");
 0341                return result;
 342            }
 343
 3344            _logger.LogDebug("Generated access token and refresh token for user with email {Email}.", user.Email);
 3345            _logger.LogDebug("AccessToken: {AccessToken}, RefreshToken: {RefreshToken}", result.AccessToken, result.Refr
 3346            return result;
 9347        }
 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    {
 371        public required string OldPassword { get; set; }
 372        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}