using Api.SeaHavenIndustries.DTOs; using Api.SeaHavenIndustries.Helper; using Api.SeaHavenIndustries.Infrastructure; using Data.SeaHavenIndustries; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.RateLimiting; using Microsoft.Extensions.Logging; using SeaHaven.Services.DTOs; using SeaHaven.Services.Interfaces; using System.Runtime.CompilerServices; using System.Security.Claims; namespace Api.SeaHavenIndustries.Controllers { [ApiController] [Route("api/Authentication")] public class AuthenticationController : Controller { private readonly IAuthenticationService _authenticationService; private readonly ILogger _logger; public AuthenticationController(IAuthenticationService authenticationService, ILogger logger) { _authenticationService = authenticationService; _logger = logger; } [AllowAnonymous] [HttpPost] [Route("login")] public async Task Login([FromBody] LoginModel model, CancellationToken cancellationToken) { try { var result = await _authenticationService.LoginAsync(model.Username, model.Password, cancellationToken); if (result != null) { return Ok(LoginPayload.From(result)); } return Unauthorized(); } catch (Exception ex) { return StatusCode(500, _logger.Sanitize(ex)); } } [Authorize] [Route("ChangePassword")] [HttpPost] public async Task ChangePassword(ChangePasswords usermodel, CancellationToken cancellationToken) { // [Compare] already rejects this during model validation; the check here keeps the // unconfirmed password from ever being set if that validation is bypassed. if (!string.Equals(usermodel.Newpassword, usermodel.Confirmpassword, StringComparison.Ordinal)) return BadRequest(new Response { Status = "Password confirmation does not match", Message = "Passwords don't match" }); var userid = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? ""; var result = await _authenticationService.ChangePasswordAsync(userid, usermodel.Currentpassword, usermodel.Newpassword, cancellationToken); return result.Status switch { ChangePasswordStatus.Succeeded => Ok(new Response { Status = "Success ", Message = "Password successfully changed" }), ChangePasswordStatus.PasswordRejected => BadRequest(new Response { Status = "Password does not meet requirements", Message = PasswordRequirementsMessage }), ChangePasswordStatus.Failed => BadRequest(new Response { Status = "Password not changed", Message = "Your password could not be changed. Try again." }), _ => BadRequest(new Response { Status = "Old Password is incorrect", Message = "Current password is incorrect" }) }; } private const string PasswordRequirementsMessage = "Password must be at least 6 characters and include one uppercase letter, one number, and one special character."; [HttpPost] [Route("UpdateProfile")] public async Task UserProfileUpdate([FromForm] User_DTO model, CancellationToken cancellationToken) { try { var userid = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? ""; var dto = new UpdateProfileRequestDTO { Name = model.Name, Email = model.Email, Contact = model.Contact }; var data = await _authenticationService.UpdateProfileAsync(userid, dto, cancellationToken); return Ok(new DataResponse { Message = "Introduction Updated Successfully", Status = "200", Data = data == null ? null : new { data.FirstName, data.Email, data.Contact } }); } catch (Exception ex) { return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex) }); } } #region Forget Password Area public const string ForgetPasswordMessage = "If that email belongs to an account, a reset code has been sent to it."; // Email and code are read only from the JSON body, so they never appear in URLs // or in proxy and load balancer access logs. [AllowAnonymous] [HttpPost()] [Route("ForgetPassword")] [EnableRateLimiting(PasswordResetRateLimiting.ForgetPasswordPolicy)] public async Task ForgetPassword( [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] ForgetPasswordRequest_Dto? body, CancellationToken cancellationToken) { try { await _authenticationService.ForgetPasswordAsync(body?.Email, cancellationToken); } catch (Exception ex) { // Same answer as success: a failure only a registered address can hit // must not reveal that the address is registered. LogResetFailure(ex); } return Ok(new Response { Status = "Success ", Message = ForgetPasswordMessage }); } [AllowAnonymous] [HttpPost()] [Route("VerificationCode")] [EnableRateLimiting(PasswordResetRateLimiting.VerificationCodePolicy)] public async Task VerificationCode( [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] VerificationCode_Dto? body, CancellationToken cancellationToken) { try { if (await _authenticationService.VerifyCodeAsync(body?.Email, body?.Code, cancellationToken)) { return Ok(new Response { Status = "Success ", Message = "Code Matched" }); } else { return BadRequest(new Response { Status = "Error", Message = "Code Not Matched" }); } } catch (Exception ex) { LogResetFailure(ex); return BadRequest(new Response { Status = "Error", Message = "Code Not Matched" }); } } [AllowAnonymous] [HttpPost()] [Route("ResetPassword")] [EnableRateLimiting(PasswordResetRateLimiting.ResetPasswordPolicy)] public async Task ResetPassword(ForgetPassword_Dto fpdto, CancellationToken cancellationToken) { try { if (await _authenticationService.ResetPasswordAsync(fpdto.Email, fpdto.Code, fpdto.Password, cancellationToken)) { return Ok(new Response { Status = "Success ", Message = "password changed" }); } else { return BadRequest(new Response { Status = "Error", Message = "Your email or code not found please check" }); } } catch (Exception ex) { LogResetFailure(ex); return BadRequest(new Response { Status = "Error", Message = "Your email or code not found please check" }); } } // These endpoints answer failures exactly like a wrong code or an unknown email, so // an error only a registered account can trigger reveals nothing. Exception // messages here can echo the email or code, so only the type is logged. private void LogResetFailure(Exception exception, [CallerMemberName] string operation = "") { _logger.LogError( "Password reset {Operation} failed with {ExceptionType}.", operation, exception.GetType().FullName); } #endregion } }