Implement API to log in through OTP verification #86
2673
Marco.Pms.DataAccess/Migrations/20250607061133_Added_IsUsed_FLag_In_OTPDetails_Table.Designer.cs
generated
Normal file
2673
Marco.Pms.DataAccess/Migrations/20250607061133_Added_IsUsed_FLag_In_OTPDetails_Table.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Marco.Pms.DataAccess.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Added_IsUsed_FLag_In_OTPDetails_Table : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsUsed",
|
||||||
|
table: "OTPDetails",
|
||||||
|
type: "tinyint(1)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsUsed",
|
||||||
|
table: "OTPDetails");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -268,6 +268,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
b.Property<int>("ExpriesInSec")
|
b.Property<int>("ExpriesInSec")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("IsUsed")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
b.Property<string>("OTP")
|
b.Property<string>("OTP")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext");
|
.HasColumnType("longtext");
|
||||||
|
@ -8,6 +8,7 @@ namespace Marco.Pms.Model.Authentication
|
|||||||
public Guid UserId { get; set; }
|
public Guid UserId { get; set; }
|
||||||
public string OTP { get; set; } = string.Empty;
|
public string OTP { get; set; } = string.Empty;
|
||||||
public int ExpriesInSec { get; set; }
|
public int ExpriesInSec { get; set; }
|
||||||
|
public bool IsUsed { get; set; } = false;
|
||||||
public DateTime TimeStamp { get; set; }
|
public DateTime TimeStamp { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
8
Marco.Pms.Model/Dtos/Authentication/VerifyOTPDto.cs
Normal file
8
Marco.Pms.Model/Dtos/Authentication/VerifyOTPDto.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace Marco.Pms.Model.Dtos.Authentication
|
||||||
|
{
|
||||||
|
public class VerifyOTPDto
|
||||||
|
{
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? OPT { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -349,6 +349,97 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("login-opt")]
|
||||||
|
public async Task<IActionResult> LoginWithOTP([FromBody] VerifyOTPDto verifyOTP)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Validate input
|
||||||
|
if (string.IsNullOrWhiteSpace(verifyOTP.Email) ||
|
||||||
|
string.IsNullOrWhiteSpace(verifyOTP.OPT) ||
|
||||||
|
verifyOTP.OPT.Length != 4 ||
|
||||||
|
!verifyOTP.OPT.All(char.IsDigit))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("OTP login failed - invalid input provided");
|
||||||
|
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid input", "Please provide a valid 4-digit OTP and Email", 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch employee by email
|
||||||
|
var requestEmployee = await _context.Employees
|
||||||
|
.Include(e => e.ApplicationUser)
|
||||||
|
.FirstOrDefaultAsync(e => e.Email == verifyOTP.Email && e.IsActive);
|
||||||
|
|
||||||
|
if (requestEmployee == null || string.IsNullOrWhiteSpace(requestEmployee.ApplicationUserId))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("OTP login failed - user not found for email {Email}", verifyOTP.Email);
|
||||||
|
return NotFound(ApiResponse<object>.ErrorResponse("User not found", "User not found", 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
Guid userId = Guid.Parse(requestEmployee.ApplicationUserId);
|
||||||
|
|
||||||
|
// Fetch most recent OTP
|
||||||
|
var otpDetails = await _context.OTPDetails
|
||||||
|
.Where(o => o.UserId == userId && o.TenantId == requestEmployee.TenantId)
|
||||||
|
.OrderByDescending(o => o.TimeStamp)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (otpDetails == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("OTP login failed - no OTP found for user {UserId}", userId);
|
||||||
|
return NotFound(ApiResponse<object>.ErrorResponse("OTP not found", "No OTP was generated for this user", 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate OTP expiration
|
||||||
|
var validUntil = otpDetails.TimeStamp.AddSeconds(otpDetails.ExpriesInSec);
|
||||||
|
if (DateTime.UtcNow > validUntil || otpDetails.IsUsed)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("OTP login failed - OTP expired for user {UserId}", userId);
|
||||||
|
return BadRequest(ApiResponse<object>.ErrorResponse("OTP expired", "The OTP has expired, please request a new one", 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match OTP
|
||||||
|
if (otpDetails.OTP != verifyOTP.OPT)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("OTP login failed - incorrect OTP entered for user {UserId}", userId);
|
||||||
|
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid OTP", "OTP did not match", 401));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate access and refresh tokens
|
||||||
|
var accessToken = _refreshTokenService.GenerateJwtToken(
|
||||||
|
requestEmployee.ApplicationUser?.UserName,
|
||||||
|
requestEmployee.TenantId,
|
||||||
|
_jwtSettings
|
||||||
|
);
|
||||||
|
|
||||||
|
var refreshToken = await _refreshTokenService.CreateRefreshToken(
|
||||||
|
requestEmployee.ApplicationUserId,
|
||||||
|
requestEmployee.TenantId.ToString(),
|
||||||
|
_jwtSettings
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch MPIN token if exists
|
||||||
|
var mpinDetails = await _context.MPINDetails
|
||||||
|
.FirstOrDefaultAsync(p => p.UserId == userId && p.TenantId == requestEmployee.TenantId);
|
||||||
|
|
||||||
|
// Build and return response
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
token = accessToken,
|
||||||
|
refreshToken,
|
||||||
|
mpinToken = mpinDetails?.MPINToken
|
||||||
|
};
|
||||||
|
otpDetails.IsUsed = true;
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
_logger.LogInfo("OTP login successful for employee {EmployeeId}", requestEmployee.Id);
|
||||||
|
return Ok(ApiResponse<object>.SuccessResponse(response, "User logged in successfully.", 200));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError("An unexpected error occurred during OTP login for email {Email} : {Error}", verifyOTP.Email ?? string.Empty, ex.Message);
|
||||||
|
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error", ex.Message, 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("sendmail")]
|
[HttpPost("sendmail")]
|
||||||
public async Task<IActionResult> SendEmail([FromBody] EmailDot emailDot)
|
public async Task<IActionResult> SendEmail([FromBody] EmailDot emailDot)
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user