mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 04:53:11 +00:00
Forgot Password answers every address the same way and emails a code only to an active account. Codes are stored as salted SHA-256 hashes, expire 15 minutes after issue, are replaced by a newer request, and are checked only against the email they were issued to. Five failed checks delete the code; attempts are reserved with one conditional UPDATE so concurrent guesses cannot exceed the budget. VerificationCode requires the email, and email and code are accepted in the JSON body so they stay out of URLs. The three anonymous endpoints are rate limited to 10 requests per 15 minutes per client IP. Forwarded headers are trusted only through loopback and private hops, since the API sits behind the EB load balancer and nginx. The migration adds hash, salt, expiry and attempt columns and deletes the old plaintext rows.
204 lines
7.8 KiB
C#
204 lines
7.8 KiB
C#
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<AuthenticationController> _logger;
|
|
|
|
public AuthenticationController(IAuthenticationService authenticationService, ILogger<AuthenticationController> logger)
|
|
{
|
|
_authenticationService = authenticationService;
|
|
_logger = logger;
|
|
}
|
|
|
|
[AllowAnonymous]
|
|
[HttpPost]
|
|
[Route("login")]
|
|
public async Task<IActionResult> Login([FromBody] LoginModel model, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
var result = await _authenticationService.LoginAsync(model.Username, model.Password, cancellationToken);
|
|
if (result != null)
|
|
{
|
|
return Ok(new
|
|
{
|
|
token = result.Token,
|
|
expiration = result.Expiration,
|
|
email = result.Email,
|
|
userRoles = result.UserRole,
|
|
phoneNumber = result.PhoneNumber,
|
|
fullname = result.Fullname,
|
|
id = result.Id
|
|
});
|
|
}
|
|
|
|
return Unauthorized();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
|
|
return StatusCode(500, _logger.Sanitize(ex));
|
|
}
|
|
|
|
}
|
|
|
|
[Route("ChangePassword")]
|
|
[HttpPost]
|
|
public async Task<IActionResult> ChangePassword(ChangePasswords usermodel, CancellationToken cancellationToken)
|
|
{
|
|
var userid = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "";
|
|
var succeeded = await _authenticationService.ChangePasswordAsync(userid, usermodel.Currentpassword, usermodel.Confirmpassword, cancellationToken);
|
|
if (succeeded)
|
|
{
|
|
return Ok(new Response { Status = "Success ", Message = "Password successfully changed" });
|
|
}
|
|
else
|
|
return BadRequest(new Response { Status = "Old Password is incorrect" });
|
|
}
|
|
|
|
[HttpPost]
|
|
[Route("UpdateProfile")]
|
|
public async Task<IActionResult> 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 from the JSON body so they stay out of URLs and proxy
|
|
// access logs; the query-string form is still accepted for older clients.
|
|
[AllowAnonymous]
|
|
[HttpPost()]
|
|
[Route("ForgetPassword")]
|
|
[EnableRateLimiting(PasswordResetRateLimiting.ForgetPasswordPolicy)]
|
|
public async Task<IActionResult> ForgetPassword(
|
|
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] ForgetPasswordRequest_Dto? body,
|
|
[FromQuery(Name = "Email")] string? email,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await _authenticationService.ForgetPasswordAsync(body?.Email ?? 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<IActionResult> VerificationCode(
|
|
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] VerificationCode_Dto? body,
|
|
[FromQuery] string? email,
|
|
[FromQuery] string? code,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
if (await _authenticationService.VerifyCodeAsync(body?.Email ?? email, body?.Code ?? 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<IActionResult> 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
|
|
}
|
|
}
|