Added tables for MPIN and OTP as well created an Login API for Mobile Application #83

Merged
vikas.nale merged 1 commits from Ashutosh_Task#469_Mobile_Login into Issue_Jun_1W_2 2025-06-05 11:46:08 +00:00
9 changed files with 3004 additions and 22 deletions

View File

@ -70,6 +70,9 @@ namespace Marco.Pms.DataAccess.Data
public DbSet<MailingList> MailingList { get; set; }
public DbSet<MailDetails> MailDetails { get; set; }
public DbSet<MailLog> MailLogs { get; set; }
public DbSet<OTPDetails> OTPDetails { get; set; }
public DbSet<MPINDetails> MPINDetails { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,84 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_OTP_And_MPIN_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "MPINDetails",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
UserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
MPIN = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
MPINToken = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
TimeStamp = table.Column<DateTime>(type: "datetime(6)", nullable: false),
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
},
constraints: table =>
{
table.PrimaryKey("PK_MPINDetails", x => x.Id);
table.ForeignKey(
name: "FK_MPINDetails_Tenants_TenantId",
column: x => x.TenantId,
principalTable: "Tenants",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "OTPDetails",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
UserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
OTP = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ExpriesInSec = table.Column<int>(type: "int", nullable: false),
TimeStamp = table.Column<DateTime>(type: "datetime(6)", nullable: false),
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
},
constraints: table =>
{
table.PrimaryKey("PK_OTPDetails", x => x.Id);
table.ForeignKey(
name: "FK_OTPDetails_Tenants_TenantId",
column: x => x.TenantId,
principalTable: "Tenants",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_MPINDetails_TenantId",
table: "MPINDetails",
column: "TenantId");
migrationBuilder.CreateIndex(
name: "IX_OTPDetails_TenantId",
table: "OTPDetails",
column: "TenantId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "MPINDetails");
migrationBuilder.DropTable(
name: "OTPDetails");
}
}
}

View File

@ -229,6 +229,65 @@ namespace Marco.Pms.DataAccess.Migrations
b.ToTable("AttendanceLogs");
});
modelBuilder.Entity("Marco.Pms.Model.Authentication.MPINDetails", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<string>("MPIN")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("MPINToken")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
b.Property<DateTime>("TimeStamp")
.HasColumnType("datetime(6)");
b.Property<Guid>("UserId")
.HasColumnType("char(36)");
b.HasKey("Id");
b.HasIndex("TenantId");
b.ToTable("MPINDetails");
});
modelBuilder.Entity("Marco.Pms.Model.Authentication.OTPDetails", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<int>("ExpriesInSec")
.HasColumnType("int");
b.Property<string>("OTP")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
b.Property<DateTime>("TimeStamp")
.HasColumnType("datetime(6)");
b.Property<Guid>("UserId")
.HasColumnType("char(36)");
b.HasKey("Id");
b.HasIndex("TenantId");
b.ToTable("OTPDetails");
});
modelBuilder.Entity("Marco.Pms.Model.Authentication.RefreshToken", b =>
{
b.Property<Guid>("Id")
@ -2118,6 +2177,28 @@ namespace Marco.Pms.DataAccess.Migrations
b.Navigation("UpdatedByEmployee");
});
modelBuilder.Entity("Marco.Pms.Model.Authentication.MPINDetails", b =>
{
b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Tenant");
});
modelBuilder.Entity("Marco.Pms.Model.Authentication.OTPDetails", b =>
{
b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Tenant");
});
modelBuilder.Entity("Marco.Pms.Model.Authentication.RefreshToken", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")

View File

@ -0,0 +1,13 @@
using Marco.Pms.Model.Utilities;
namespace Marco.Pms.Model.Authentication
{
public class MPINDetails : TenantRelation
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string MPIN { get; set; } = string.Empty;
public string MPINToken { get; set; } = string.Empty;
public DateTime TimeStamp { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using Marco.Pms.Model.Utilities;
namespace Marco.Pms.Model.Authentication
{
public class OTPDetails : TenantRelation
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string OTP { get; set; } = string.Empty;
public int ExpriesInSec { get; set; }
public DateTime TimeStamp { get; set; }
}
}

View File

@ -71,6 +71,79 @@ namespace MarcoBMS.Services.Controllers
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid username or password.", "Invalid username or password.", 401));
}
[HttpPost("login-mobile")]
public async Task<IActionResult> LoginMobile([FromBody] LoginDto loginDto)
{
// Validate input DTO
if (loginDto == null || string.IsNullOrWhiteSpace(loginDto.Username) || string.IsNullOrWhiteSpace(loginDto.Password))
{
return BadRequest(ApiResponse<object>.ErrorResponse("Username or password is missing.", "Invalid request", 400));
}
// Find user by email or phone number
var user = await _context.ApplicationUsers
.FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.PhoneNumber == loginDto.Username);
// If user not found, return unauthorized
if (user == null)
{
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid username or password.", "Invalid username or password.", 401));
}
// Check if user is inactive
if (!user.IsActive)
{
return BadRequest(ApiResponse<object>.ErrorResponse("User is inactive", "User is inactive", 400));
}
// Check if user email is not confirmed
if (!user.EmailConfirmed)
{
return BadRequest(ApiResponse<object>.ErrorResponse("Your email is not verified. Please verify your email.", "Email not verified", 400));
}
// Validate password using ASP.NET Identity
var isPasswordValid = await _userManager.CheckPasswordAsync(user, loginDto.Password);
if (!isPasswordValid)
{
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid username or password.", "Invalid credentials", 401));
}
// Check if username is missing
if (string.IsNullOrWhiteSpace(user.UserName))
{
return NotFound(ApiResponse<object>.ErrorResponse("UserName not found", "Username is missing", 404));
}
// Get employee information for tenant context
var emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id);
if (emp == null)
{
return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Employee details missing", 404));
}
// Generate JWT token
var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId, _jwtSettings);
// Generate Refresh Token and store in DB
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings);
// Generate MPIN Token (custom short-term token)
var mpinToken = await _refreshTokenService.CreateMPINToken(user.Id, emp.TenantId.ToString(), _jwtSettings);
// Combine all tokens in response
var responseData = new
{
token,
refreshToken,
mpinToken
};
// Return success response
return Ok(ApiResponse<object>.SuccessResponse(responseData, "User logged in successfully.", 200));
}
[HttpPost("logout")]
public async Task<IActionResult> Logout([FromBody] LogoutDto logoutDto)
{
@ -206,8 +279,6 @@ namespace MarcoBMS.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(result.Succeeded, "Password reset successfully.", 200));
}
[HttpPost("sendmail")]
public async Task<IActionResult> SendEmail([FromBody] EmailDot emailDot)
{

View File

@ -48,36 +48,37 @@ namespace MarcoBMS.Services.Service
return new JwtSecurityTokenHandler().WriteToken(token);
}
public async Task<string> CreateRefreshToken(string userId, string tenantId, JwtSettings _jwtSettings)
public async Task<string> CreateRefreshToken(string userId, string tenantId, JwtSettings jwtSettings)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_jwtSettings.Key);
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, userId),
new Claim("TenantId", tenantId),
new Claim("token_type", "refresh")
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, userId),
new Claim("TenantId", tenantId), // Add TenantId claim
new Claim("token_type", "refresh") // Custom claim to differentiate refresh tokens
}),
Expires = DateTime.UtcNow.AddDays(7), // Refresh token valid for 7 days
Issuer = _jwtSettings.Issuer,
Audience = _jwtSettings.Audience,
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddDays(jwtSettings.RefreshTokenExpiresInDays),
Issuer = jwtSettings.Issuer,
Audience = jwtSettings.Audience,
SigningCredentials = credentials
};
var token = tokenHandler.CreateToken(tokenDescriptor);
string strToken = tokenHandler.WriteToken(token);
var tokenHandler = new JwtSecurityTokenHandler();
var refreshTokenString = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));
var refreshToken = new RefreshToken
{
Token = strToken,
Token = refreshTokenString,
UserId = userId,
ExpiryDate = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpiresInDays),
ExpiryDate = DateTime.UtcNow.AddDays(jwtSettings.RefreshTokenExpiresInDays),
IsRevoked = false
};
@ -89,7 +90,7 @@ namespace MarcoBMS.Services.Service
_context.RefreshTokens.Add(refreshToken);
}
await _context.SaveChangesAsync();
return strToken;
return refreshTokenString;
}
catch (Exception ex)
{
@ -97,6 +98,52 @@ namespace MarcoBMS.Services.Service
throw;
}
}
public async Task<string> CreateMPINToken(string userId, string tenantId, JwtSettings jwtSettings)
{
try
{
var existingMPIN = await _context.MPINDetails.FirstOrDefaultAsync(p => p.UserId == Guid.Parse(userId) && p.TenantId == Guid.Parse(tenantId));
if (existingMPIN != null)
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, userId),
new Claim("TenantId", tenantId),
new Claim("token_type", "mpin")
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Issuer = jwtSettings.Issuer,
Audience = jwtSettings.Audience,
SigningCredentials = creds
// No 'Expires' means the token won't expire
};
var tokenHandler = new JwtSecurityTokenHandler();
var MPINToken = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));
existingMPIN.MPINToken = MPINToken;
await _context.SaveChangesAsync();
return MPINToken;
}
return null;
}
catch (Exception ex)
{
_logger.LogError("Error creating MPIN token for userId: {UserId}, tenantId: {TenantId}, error : {Error}", userId, tenantId, ex.Message);
throw;
}
}
public async Task<RefreshToken> GetRefreshToken(string token)
{

View File

@ -34,7 +34,7 @@
"RefreshTokenExpiresInDays": 7
},
"MailingList": {
"RequestDemoReceivers": "ashutosh.nehete@marcoaiot.com;vikas@marcoaiot.com;umesh@marcoait.com",
"RequestDemoReceivers": "ashutosh.nehete@marcoaiot.com;vikas@marcoaiot.com;umesh@marcoait.com"
//"ProjectStatisticsReceivers": "ashutosh.nehete@marcoaiot.com;vikas@marcoaiot.com;umesh@marcoait.com"
},
"AWS": {