Compare commits

..

No commits in common. "b61caa9ee7f601d48fd8dc7f1e65a2ceeaa4befd" and "b74f70144861a16f436dd23602783c092a3bc594" have entirely different histories.

41 changed files with 275 additions and 12757 deletions

View File

@ -78,14 +78,12 @@ namespace Marco.Pms.DataAccess.Data
public DbSet<ContactTagMapping> ContactTagMappings { get; set; }
public DbSet<EmployeeBucketMapping> EmployeeBucketMappings { get; set; }
public DbSet<ContactBucketMapping> ContactBucketMappings { get; set; }
public DbSet<ContactProjectMapping> ContactProjectMappings { get; set; }
public DbSet<DirectoryUpdateLog> DirectoryUpdateLogs { get; set; }
public DbSet<ContactProjectMapping> ContactProjectMappings { get; set; }
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; }
@ -511,9 +509,8 @@ namespace Marco.Pms.DataAccess.Data
new FeaturePermission { Id = new Guid("fbd213e0-0250-46f1-9f5f-4b2a1e6e76a3"), FeatureId = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), IsEnabled = true, Name = "Assign Roles", Description = "Grants a user the authority to manage employee application roles, enabling them to assign or revoke access privileges within the system." },
new FeaturePermission { Id = new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"), FeatureId = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), IsEnabled = true, Name = "Team Attendance ", Description = "Team Attendance refers to tracking and managing the attendance of all team members collectively, often monitored by a team lead or manager." },
new FeaturePermission { Id = new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"), FeatureId = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), IsEnabled = true, Name = "Perform Attendance ", Description = "Grants a user the ability to record their own work hours or presence within the system. This typically involves checking in and checking out, logging break times, and potentially viewing their own attendance history." },
new FeaturePermission { Id = new Guid("57802c4a-00aa-4a1f-a048-fd2f70dd44b6"), FeatureId = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), IsEnabled = true, Name = "Regularize Attendance", Description = "Grants a user the authority to approve requests from employees to adjust or correct their recorded attendance. This typically involves reviewing the reason for the regularization, verifying any supporting documentation, and then officially accepting the changes to the employee's attendance records" },
new FeaturePermission { Id = new Guid("ccb0589f-712b-43de-92ed-5b6088e7dc4e"), FeatureId = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), IsEnabled = true, Name = "Self Attendance", Description = "Team Attendance refers to tracking and managing the attendance of all team members collectively, often monitored by a team lead or manager." },
new FeaturePermission { Id = new Guid("5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d"), FeatureId = new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), IsEnabled = true, Name = "View Masters", Description = "Grants a user read-only access to foundational or reference data within the system. \"Masters\" typically refer to predefined lists, categories, or templates that are used throughout the application to standardize information and maintain consistency" },
new FeaturePermission { Id = new Guid("588a8824-f924-4955-82d8-fc51956cf323"), FeatureId = new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), IsEnabled = true, Name = "Manage Masters", Description = "Grants a user the authority to create, modify, and delete foundational or reference data within the system. These \"masters\" are typically the core lists, categories, and configurations that other data and functionalities rely upon, such as departments, job titles, product categories" },

View File

@ -1,43 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_Self_Attendance_Feature_Permission : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.UpdateData(
table: "FeaturePermissions",
keyColumn: "Id",
keyValue: new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"),
columns: new[] { "Description", "Name" },
values: new object[] { "Team Attendance refers to tracking and managing the attendance of all team members collectively, often monitored by a team lead or manager.", "Team Attendance " });
migrationBuilder.InsertData(
table: "FeaturePermissions",
columns: new[] { "Id", "Description", "FeatureId", "IsEnabled", "Name" },
values: new object[] { new Guid("ccb0589f-712b-43de-92ed-5b6088e7dc4e"), "Team Attendance refers to tracking and managing the attendance of all team members collectively, often monitored by a team lead or manager.", new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), true, "Self Attendance" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "FeaturePermissions",
keyColumn: "Id",
keyValue: new Guid("ccb0589f-712b-43de-92ed-5b6088e7dc4e"));
migrationBuilder.UpdateData(
table: "FeaturePermissions",
keyColumn: "Id",
keyValue: new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"),
columns: new[] { "Description", "Name" },
values: new object[] { "Grants a user the ability to record their own work hours or presence within the system. This typically involves checking in and checking out, logging break times, and potentially viewing their own attendance history.", "Perform Attendance " });
}
}
}

View File

@ -1,84 +0,0 @@
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

@ -1,40 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_Subject_In_MailingList_And_Removed_From_MailDetails : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Subject",
table: "MailDetails");
migrationBuilder.AddColumn<string>(
name: "Subject",
table: "MailingList",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Subject",
table: "MailingList");
migrationBuilder.AddColumn<string>(
name: "Subject",
table: "MailDetails",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
}
}
}

View File

@ -1,29 +0,0 @@
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");
}
}
}

View File

@ -229,68 +229,6 @@ 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<bool>("IsUsed")
.HasColumnType("tinyint(1)");
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")
@ -1003,10 +941,10 @@ namespace Marco.Pms.DataAccess.Migrations
new
{
Id = new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"),
Description = "Team Attendance refers to tracking and managing the attendance of all team members collectively, often monitored by a team lead or manager.",
Description = "Grants a user the ability to record their own work hours or presence within the system. This typically involves checking in and checking out, logging break times, and potentially viewing their own attendance history.",
FeatureId = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"),
IsEnabled = true,
Name = "Team Attendance "
Name = "Perform Attendance "
},
new
{
@ -1017,14 +955,6 @@ namespace Marco.Pms.DataAccess.Migrations
Name = "Regularize Attendance"
},
new
{
Id = new Guid("ccb0589f-712b-43de-92ed-5b6088e7dc4e"),
Description = "Team Attendance refers to tracking and managing the attendance of all team members collectively, often monitored by a team lead or manager.",
FeatureId = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"),
IsEnabled = true,
Name = "Self Attendance"
},
new
{
Id = new Guid("5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d"),
Description = "Grants a user read-only access to foundational or reference data within the system. \"Masters\" typically refer to predefined lists, categories, or templates that are used throughout the application to standardize information and maintain consistency",
@ -1330,6 +1260,10 @@ namespace Marco.Pms.DataAccess.Migrations
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
@ -1385,10 +1319,6 @@ namespace Marco.Pms.DataAccess.Migrations
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
@ -2546,28 +2476,6 @@ 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

@ -1,13 +0,0 @@
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

@ -1,14 +0,0 @@
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 bool IsUsed { get; set; } = false;
public DateTime TimeStamp { get; set; }
}
}

View File

@ -17,4 +17,4 @@ namespace Marco.Pms.Model.Directory
[ForeignKey("ContactId")]
public Contact? Contact { get; set; }
}
}
}

View File

@ -1,17 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Marco.Pms.Model.Dtos.Authentication
{
public class ChangePasswordDto
{
public string? Email { get; set; }
public string? OldPassword { get; set; }
public string? NewPassword { get; set; }
}
}

View File

@ -1,8 +0,0 @@
namespace Marco.Pms.Model.Dtos.Authentication
{
public class GenerateMPINDto
{
public Guid EmployeeId { get; set; }
public string? MPIN { get; set; }
}
}

View File

@ -1,7 +0,0 @@
namespace Marco.Pms.Model.Dtos.Authentication
{
public class GenerateOTPDto
{
public string? Email { get; set; }
}
}

View File

@ -1,9 +0,0 @@
namespace Marco.Pms.Model.Dtos.Authentication
{
public class VerifyMPINDto
{
public Guid EmployeeId { get; set; }
public string? MPIN { get; set; }
public string? MPINToken { get; set; }
}
}

View File

@ -1,8 +0,0 @@
namespace Marco.Pms.Model.Dtos.Authentication
{
public class VerifyOTPDto
{
public string? Email { get; set; }
public string? OTP { get; set; }
}
}

View File

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace Marco.Pms.Model.Dtos.Authentication
namespace Marco.Pms.Model.Dtos
{
public class ForgotPasswordDto
{

View File

@ -1,4 +1,4 @@
namespace Marco.Pms.Model.Dtos.Authentication
namespace Marco.Pms.Model.Dtos
{
public class LoginDto
{

View File

@ -1,4 +1,5 @@
namespace Marco.Pms.Model.Dtos.Authentication

namespace Marco.Pms.Model.Dtos
{
public class LogoutDto
{ public string? RefreshToken { get; set; }

View File

@ -4,6 +4,7 @@
{
public Guid ProjectId { get; set; }
public string Recipient { get; set; } = string.Empty; // Eamil Address of recipient
public string Subject { get; set; } = string.Empty;
public string Schedule { get; set; } = string.Empty; // json object which includes when to send mail and at what interval
public Guid MailListId { get; set; }
}

View File

@ -4,7 +4,6 @@
{
public string Title { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty;
public string Subject { get; set; } = string.Empty;
public string Keywords { get; set; } = string.Empty;
}
}

View File

@ -1,4 +1,4 @@
namespace Marco.Pms.Model.Dtos.Authentication
namespace Marco.Pms.Model.Dtos
{
public class RefreshTokenDto
{

View File

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace Marco.Pms.Model.Dtos.Authentication
namespace Marco.Pms.Model.Dtos
{
public class RegisterDto
{

View File

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace Marco.Pms.Model.Dtos.Authentication
namespace Marco.Pms.Model.Dtos
{
public class ResetPasswordDto
{

View File

@ -8,6 +8,7 @@ namespace Marco.Pms.Model.Mail
public Guid Id { get; set; }
public Guid ProjectId { get; set; }
public string Recipient { get; set; } = string.Empty; // Eamil Address of recipient
public string Subject { get; set; } = string.Empty; // Eamil Address of recipient
public string Schedule { get; set; } = string.Empty; // json object which includes when to send mail and at what interval
public Guid MailListId { get; set; }
[ValidateNever]

View File

@ -5,7 +5,6 @@
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty;
public string Subject { get; set; } = string.Empty; // Eamil Subject of recipient
public string Keywords { get; set; } = string.Empty; // Comma seprated list of variables in mail body
public Guid TenantId { get; set; }
}

View File

@ -28,12 +28,11 @@ namespace MarcoBMS.Services.Controllers
private readonly ProjectsHelper _projectsHelper;
private readonly UserHelper _userHelper;
private readonly S3UploadService _s3Service;
private readonly PermissionServices _permission;
private readonly ILoggingService _logger;
public AttendanceController(
ApplicationDbContext context, EmployeeHelper employeeHelper, ProjectsHelper projectsHelper, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permission)
ApplicationDbContext context, EmployeeHelper employeeHelper, ProjectsHelper projectsHelper, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger)
{
_context = context;
_employeeHelper = employeeHelper;
@ -41,7 +40,6 @@ namespace MarcoBMS.Services.Controllers
_userHelper = userHelper;
_s3Service = s3Service;
_logger = logger;
_permission = permission;
}
private Guid GetTenantId()
@ -134,17 +132,6 @@ namespace MarcoBMS.Services.Controllers
public async Task<IActionResult> EmployeeAttendanceByDateRange([FromQuery] Guid projectId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null)
{
Guid TenantId = GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var hasTeamAttendancePermission = await _permission.HasPermission(new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"), LoggedInEmployee.Id);
var hasSelfAttendancePermission = await _permission.HasPermission(new Guid("ccb0589f-712b-43de-92ed-5b6088e7dc4e"), LoggedInEmployee.Id);
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId.ToString());
if (!hasProjectPermission)
{
_logger.LogWarning("Employee {EmployeeId} tries to access attendance of project {ProjectId}, but don't have access", LoggedInEmployee.Id, projectId);
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized access", "Unauthorized access", 404));
}
DateTime fromDate = new DateTime();
DateTime toDate = new DateTime();
@ -172,68 +159,42 @@ namespace MarcoBMS.Services.Controllers
if (dateFrom == null) fromDate = DateTime.UtcNow.Date;
if (dateTo == null && dateFrom != null) toDate = fromDate.AddDays(-1);
if (hasTeamAttendancePermission)
List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date >= fromDate && c.AttendanceDate.Date <= toDate && c.TenantId == TenantId).ToListAsync();
List<ProjectAllocation> projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, true);
var jobRole = await _context.JobRoles.ToListAsync();
foreach (Attendance? attendance in lstAttendance)
{
List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == TenantId).ToListAsync();
List<ProjectAllocation> projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, true);
var jobRole = await _context.JobRoles.ToListAsync();
foreach (Attendance? attendance in lstAttendance)
var result1 = new EmployeeAttendanceVM()
{
var result1 = new EmployeeAttendanceVM()
Id = attendance.Id,
CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime,
Activity = attendance.Activity
};
teamMember = projectteam.Find(x => x.EmployeeId == attendance.EmployeeID);
if (teamMember != null)
{
result1.EmployeeAvatar = null;
result1.EmployeeId = teamMember.EmployeeId;
if (teamMember.Employee != null)
{
Id = attendance.Id,
CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime,
Activity = attendance.Activity
};
teamMember = projectteam.Find(x => x.EmployeeId == attendance.EmployeeID);
if (teamMember != null)
result1.FirstName = teamMember.Employee.FirstName;
result1.LastName = teamMember.Employee.LastName;
result1.JobRoleName = teamMember.Employee.JobRole != null ? teamMember.Employee.JobRole.Name : null;
}
else
{
result1.EmployeeAvatar = null;
result1.EmployeeId = teamMember.EmployeeId;
if (teamMember.Employee != null)
{
result1.FirstName = teamMember.Employee.FirstName;
result1.LastName = teamMember.Employee.LastName;
result1.JobRoleName = teamMember.Employee.JobRole != null ? teamMember.Employee.JobRole.Name : null;
}
else
{
result1.FirstName = null;
result1.LastName = null;
result1.JobRoleName = null;
}
result.Add(result1);
result1.FirstName = null;
result1.LastName = null;
result1.JobRoleName = null;
}
result.Add(result1);
}
}
else if (hasSelfAttendancePermission)
{
List<Attendance> lstAttendances = await _context.Attendes.Where(c => c.ProjectID == projectId && c.EmployeeID == LoggedInEmployee.Id && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == TenantId).ToListAsync();
ProjectAllocation? projectAllocation = await _context.ProjectAllocations.Include(pa => pa.Employee).FirstOrDefaultAsync(pa => pa.ProjectId == projectId && pa.EmployeeId == LoggedInEmployee.Id && pa.TenantId == TenantId && pa.IsActive);
foreach (var attendance in lstAttendances)
{
if (projectAllocation != null)
{
EmployeeAttendanceVM result1 = new EmployeeAttendanceVM
{
Id = attendance.Id,
EmployeeAvatar = null,
EmployeeId = projectAllocation.EmployeeId,
FirstName = projectAllocation.Employee?.FirstName,
LastName = projectAllocation.Employee?.LastName,
JobRoleName = projectAllocation.Employee?.JobRole?.Name,
CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime,
Activity = attendance.Activity
};
result.Add(result1);
}
}
}
_logger.LogInfo("{count} Attendance records fetched successfully", result.Count);
return Ok(ApiResponse<object>.SuccessResponse(result, System.String.Format("{0} Attendance records fetched successfully", result.Count), 200));
@ -250,17 +211,6 @@ namespace MarcoBMS.Services.Controllers
public async Task<IActionResult> EmployeeAttendanceByProject([FromQuery] Guid projectId, [FromQuery] bool IncludeInActive, [FromQuery] string? date = null)
{
Guid TenantId = GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var hasTeamAttendancePermission = await _permission.HasPermission(new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"), LoggedInEmployee.Id);
var hasSelfAttendancePermission = await _permission.HasPermission(new Guid("ccb0589f-712b-43de-92ed-5b6088e7dc4e"), LoggedInEmployee.Id);
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId.ToString());
if (!hasProjectPermission)
{
_logger.LogWarning("Employee {EmployeeId} tries to access attendance of project {ProjectId}, but don't have access", LoggedInEmployee.Id, projectId);
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized access", "Unauthorized access", 404));
}
DateTime forDate = new DateTime();
if (date != null && DateTime.TryParse(date, out forDate) == false)
@ -279,72 +229,49 @@ namespace MarcoBMS.Services.Controllers
Attendance? attendance = null;
if (date == null) forDate = DateTime.UtcNow.Date;
if (hasTeamAttendancePermission)
List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date == forDate && c.TenantId == TenantId).ToListAsync();
List<ProjectAllocation> projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, IncludeInActive);
var idList = projectteam.Select(p => p.EmployeeId).ToList();
//var emp = await _context.Employees.Where(e => idList.Contains(e.Id)).Include(e => e.JobRole).ToListAsync();
var jobRole = await _context.JobRoles.ToListAsync();
foreach (ProjectAllocation teamMember in projectteam)
{
List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date == forDate && c.TenantId == TenantId).ToListAsync();
List<ProjectAllocation> projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, IncludeInActive);
var idList = projectteam.Select(p => p.EmployeeId).ToList();
//var emp = await _context.Employees.Where(e => idList.Contains(e.Id)).Include(e => e.JobRole).ToListAsync();
var jobRole = await _context.JobRoles.ToListAsync();
foreach (ProjectAllocation teamMember in projectteam)
if (teamMember.Employee != null && teamMember.Employee.JobRole != null)
{
if (teamMember.Employee != null && teamMember.Employee.JobRole != null)
var result1 = new EmployeeAttendanceVM()
{
var result1 = new EmployeeAttendanceVM()
{
EmployeeAvatar = null,
EmployeeId = teamMember.EmployeeId,
FirstName = teamMember.Employee.FirstName,
LastName = teamMember.Employee.LastName,
JobRoleName = teamMember.Employee.JobRole.Name,
};
//var member = emp.Where(e => e.Id == teamMember.EmployeeId);
attendance = lstAttendance.Find(x => x.EmployeeID == teamMember.EmployeeId) ?? new Attendance();
if (attendance != null)
{
result1.Id = attendance.Id;
result1.CheckInTime = attendance.InTime;
result1.CheckOutTime = attendance.OutTime;
result1.Activity = attendance.Activity;
}
result.Add(result1);
}
}
result.Sort(delegate (EmployeeAttendanceVM x, EmployeeAttendanceVM y)
{
//return x.FirstName.CompareTo(y.FirstName);
return string.Compare(x.FirstName, y.FirstName, StringComparison.Ordinal);
});
}
else if (hasSelfAttendancePermission)
{
Attendance lstAttendance = await _context.Attendes.FirstOrDefaultAsync(c => c.ProjectID == projectId && c.EmployeeID == LoggedInEmployee.Id && c.AttendanceDate.Date == forDate && c.TenantId == TenantId) ?? new Attendance();
ProjectAllocation? projectAllocation = await _context.ProjectAllocations.Include(pa => pa.Employee).FirstOrDefaultAsync(pa => pa.ProjectId == projectId && pa.EmployeeId == LoggedInEmployee.Id && pa.TenantId == TenantId && pa.IsActive);
if (projectAllocation != null)
{
EmployeeAttendanceVM result1 = new EmployeeAttendanceVM
{
Id = lstAttendance.Id,
EmployeeAvatar = null,
EmployeeId = projectAllocation.EmployeeId,
FirstName = projectAllocation.Employee?.FirstName,
LastName = projectAllocation.Employee?.LastName,
JobRoleName = projectAllocation.Employee?.JobRole?.Name,
CheckInTime = lstAttendance.InTime,
CheckOutTime = lstAttendance.OutTime,
Activity = lstAttendance.Activity
EmployeeId = teamMember.EmployeeId,
FirstName = teamMember.Employee.FirstName,
LastName = teamMember.Employee.LastName,
JobRoleName = teamMember.Employee.JobRole.Name,
};
//var member = emp.Where(e => e.Id == teamMember.EmployeeId);
attendance = lstAttendance.Find(x => x.EmployeeID == teamMember.EmployeeId) ?? new Attendance();
if (attendance != null)
{
result1.Id = attendance.Id;
result1.CheckInTime = attendance.InTime;
result1.CheckOutTime = attendance.OutTime;
result1.Activity = attendance.Activity;
}
result.Add(result1);
}
}
result.Sort(delegate (EmployeeAttendanceVM x, EmployeeAttendanceVM y)
{
//return x.FirstName.CompareTo(y.FirstName);
return string.Compare(x.FirstName, y.FirstName, StringComparison.Ordinal);
});
_logger.LogInfo("{count} Attendance records fetched successfully", result.Count);
return Ok(ApiResponse<object>.SuccessResponse(result, System.String.Format("{0} Attendance records fetched successfully", result.Count), 200));
@ -355,15 +282,7 @@ namespace MarcoBMS.Services.Controllers
public async Task<IActionResult> GetRequestRegularizeAttendance([FromQuery] Guid projectId, [FromQuery] bool IncludeInActive)
{
Guid TenantId = GetTenantId();
Employee LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var result = new List<EmployeeAttendanceVM>();
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId.ToString());
if (!hasProjectPermission)
{
_logger.LogWarning("Employee {EmployeeId} tries to access attendance of project {ProjectId}, but don't have access", LoggedInEmployee.Id, projectId);
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized access", "Unauthorized access", 404));
}
List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.TenantId == TenantId).ToListAsync();

View File

@ -1,17 +1,13 @@
using System.Net;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Authentication;
using Marco.Pms.Model.Dtos.Authentication;
using Marco.Pms.Model.Dtos;
using Marco.Pms.Model.Dtos.Util;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Utilities;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@ -23,17 +19,15 @@ namespace MarcoBMS.Services.Controllers
public class AuthController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly UserHelper _userHelper;
private readonly ApplicationDbContext _context;
private readonly JwtSettings _jwtSettings;
private readonly RefreshTokenService _refreshTokenService;
private readonly IEmailSender _emailSender;
private readonly IConfiguration _configuration;
private readonly EmployeeHelper _employeeHelper;
private readonly ILoggingService _logger;
//string tenentId = "1";
public AuthController(UserManager<ApplicationUser> userManager, ApplicationDbContext context, JwtSettings jwtSettings, RefreshTokenService refreshTokenService,
IEmailSender emailSender, IConfiguration configuration, EmployeeHelper employeeHelper, UserHelper userHelper, ILoggingService logger)
IEmailSender emailSender, IConfiguration configuration, EmployeeHelper employeeHelper)
{
_userManager = userManager;
_jwtSettings = jwtSettings;
@ -42,417 +36,146 @@ namespace MarcoBMS.Services.Controllers
_configuration = configuration;
_employeeHelper = employeeHelper;
_context = context;
_userHelper = userHelper;
_logger = logger;
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginDto loginDto)
{
try
var user = await _context.ApplicationUsers.FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.PhoneNumber == loginDto.Username);
if (user != null)
{
// Find user by email or phone number
var user = await _context.ApplicationUsers
.FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.PhoneNumber == loginDto.Username);
if (user == null)
{
_logger.LogWarning("Login failed: User not found for input {Username}", loginDto.Username ?? string.Empty);
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid username or password.", "Invalid username or password.", 401));
}
// Check if the user is active
if (!user.IsActive)
{
_logger.LogWarning("Login failed: Inactive user attempted login - UserId: {UserId}", user.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("User is inactive", "User is inactive", 400));
return BadRequest(ApiResponse<object>.ErrorResponse("User is In Active", "User is In Active", 400));
}
// Ensure the user's email is confirmed
if (!user.EmailConfirmed)
{
_logger.LogWarning("Login failed: Email not confirmed for UserId: {UserId}", user.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Email not verified", "Your email is not verified, please verify your email", 400));
return BadRequest(ApiResponse<object>.ErrorResponse("Your email is not verified, Please verify your email", "Your email is not verified, Please verify your email", 400));
}
if (await _userManager.CheckPasswordAsync(user, loginDto.Password ?? string.Empty))
{
Employee emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id);
//var refreshToken = GenerateRefreshToken();
if (user.UserName == null) return NotFound(ApiResponse<object>.ErrorResponse("UserName Not found", "UserName Not found", 404)); ;
var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId, _jwtSettings);
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings);
return Ok(ApiResponse<object>.SuccessResponse(new { token = token, refreshToken = refreshToken }, "User logged in successfully.", 200));
}
// Validate the password
if (!await _userManager.CheckPasswordAsync(user, loginDto.Password ?? string.Empty))
{
_logger.LogWarning("Login failed: Incorrect password for UserId: {UserId}", user.Id);
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid username or password.", "Invalid username or password.", 401));
}
// Retrieve employee details
var emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id);
if (emp == null)
{
_logger.LogWarning("Login failed: No employee record found for UserId: {UserId}", user.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Employee record not found", "Employee not found", 404));
}
// Ensure UserName exists for JWT
if (string.IsNullOrWhiteSpace(user.UserName))
{
_logger.LogWarning("Login failed: Username not found for UserId: {UserId}", user.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Username not found", "Username not found", 404));
}
// Generate tokens
var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId, _jwtSettings);
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings);
_logger.LogInfo("User login successful - UserId: {UserId}", user.Id);
return Ok(ApiResponse<object>.SuccessResponse(new
{
token,
refreshToken
}, "User logged in successfully.", 200));
}
catch (Exception ex)
{
_logger.LogError("Unexpected error during login : {Error}", ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error", ex.Message, 500));
}
}
[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);
// Fetch MPIN Token
var mpinToken = await _context.MPINDetails.FirstOrDefaultAsync(p => p.UserId == Guid.Parse(user.Id) && p.TenantId == emp.TenantId);
// Combine all tokens in response
var responseData = new
{
token,
refreshToken,
mpinToken = mpinToken?.MPINToken
};
// Return success response
return Ok(ApiResponse<object>.SuccessResponse(responseData, "User logged in successfully.", 200));
}
[HttpPost("login-mpin")]
public async Task<IActionResult> VerifyMPIN([FromBody] VerifyMPINDto verifyMPIN)
{
try
{
// Validate the MPIN token and extract claims
var claimsPrincipal = _refreshTokenService.ValidateToken(verifyMPIN.MPINToken, _jwtSettings);
if (claimsPrincipal?.Identity == null || !claimsPrincipal.Identity.IsAuthenticated)
{
_logger.LogWarning("Invalid or unauthenticated MPIN token");
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid MPIN token", "Unauthorized", 401));
}
string? tokenType = claimsPrincipal.FindFirst("token_type")?.Value;
string? tokenTenantId = claimsPrincipal.FindFirst("TenantId")?.Value;
string? tokenUserId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
// Validate essential claims
if (string.IsNullOrWhiteSpace(tokenType) || string.IsNullOrWhiteSpace(tokenTenantId) || string.IsNullOrWhiteSpace(tokenUserId))
{
_logger.LogWarning("MPIN token claims are incomplete");
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid token claims", "MPIN token does not match your identity", 401));
}
Guid tenantId = Guid.Parse(tokenTenantId);
// Fetch employee by ID and tenant
var requestEmployee = await _context.Employees
.Include(e => e.ApplicationUser)
.FirstOrDefaultAsync(e => e.Id == verifyMPIN.EmployeeId && e.TenantId == tenantId && e.ApplicationUserId == tokenUserId && e.IsActive);
if (requestEmployee == null || string.IsNullOrWhiteSpace(requestEmployee.ApplicationUserId))
{
_logger.LogWarning("Employee not found or invalid for verification - EmployeeId: {EmployeeId}", verifyMPIN.EmployeeId);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request", "Provided invalid employee information", 400));
}
// Validate that the token belongs to the same employee making the request
if (requestEmployee.ApplicationUserId != tokenUserId || tokenType != "mpin")
{
_logger.LogWarning("Token identity does not match employee info - EmployeeId: {EmployeeId}", requestEmployee.Id);
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "MPIN token does not match your identity", 401));
}
// Ensure MPIN input is valid
if (string.IsNullOrWhiteSpace(verifyMPIN.MPIN))
{
_logger.LogWarning("MPIN not provided for EmployeeId: {EmployeeId}", requestEmployee.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request", "MPIN not provided", 400));
}
// Retrieve MPIN details
var mpinDetails = await _context.MPINDetails
.FirstOrDefaultAsync(p => p.UserId == Guid.Parse(requestEmployee.ApplicationUserId) && p.TenantId == tenantId);
if (mpinDetails == null)
{
_logger.LogWarning("MPIN not set for EmployeeId: {EmployeeId}", requestEmployee.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("MPIN not set", "You have not set an MPIN", 400));
}
// Compare hashed MPIN
var providedMPINHash = ComputeSha256Hash(verifyMPIN.MPIN);
if (providedMPINHash != mpinDetails.MPIN)
{
_logger.LogWarning("MPIN mismatch for EmployeeId: {EmployeeId}", requestEmployee.Id);
return Unauthorized(ApiResponse<object>.ErrorResponse("MPIN mismatch", "MPIN did not match", 401));
}
// Generate new tokens
var jwtToken = _refreshTokenService.GenerateJwtToken(requestEmployee.Email, tenantId, _jwtSettings);
var refreshToken = await _refreshTokenService.CreateRefreshToken(requestEmployee.ApplicationUserId, tenantId.ToString(), _jwtSettings);
_logger.LogInfo("MPIN verification successful - EmployeeId: {EmployeeId}", requestEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(new
{
token = jwtToken,
refreshToken
}, "User logged in successfully.", 200));
}
catch (Exception ex)
{
_logger.LogError("Unexpected error occurred while verifying MPIN : {Error}", ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error", ex.Message, 500));
}
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid username or password.", "Invalid username or password.", 401));
}
[HttpPost("logout")]
public async Task<IActionResult> Logout([FromBody] LogoutDto logoutDto)
{
if (string.IsNullOrWhiteSpace(logoutDto.RefreshToken))
if (string.IsNullOrEmpty(logoutDto.RefreshToken))
{
_logger.LogWarning("Logout failed: Refresh token is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("Refresh token is required", "Refresh token is required", 400));
}
try
{
// Revoke the refresh token
bool isRevoked = await _refreshTokenService.RevokeRefreshTokenAsync(logoutDto.RefreshToken);
if (!isRevoked)
{
_logger.LogWarning("Logout failed: Invalid or expired refresh token");
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid or expired refresh token", "Invalid or expired refresh token", 401));
}
// Optional: Blacklist the JWT access token
if (!isRevoked)
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid or expired refresh token", "Invalid or expired refresh token", 401));
// Optional: Blacklist the access token (JWT)
string jwtToken = HttpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
if (!string.IsNullOrWhiteSpace(jwtToken))
if (!string.IsNullOrEmpty(jwtToken))
{
await _refreshTokenService.BlacklistJwtTokenAsync(jwtToken);
_logger.LogInfo("JWT access token blacklisted successfully");
}
_logger.LogInfo("User logged out successfully");
return Ok(ApiResponse<object>.SuccessResponse(new { }, "Logged out successfully", 200));
}
catch (Exception ex)
{
_logger.LogError("Unexpected error during logout : {Error}", ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error occurred", ex.Message, 500));
// _logger.LogError(ex, "Error during logout");
return BadRequest(ApiResponse<object>.ErrorResponse("Internal server error", ex.Message, 500));
}
}
[HttpPost("refresh-token")]
public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenDto refreshTokenDto)
{
if (string.IsNullOrWhiteSpace(refreshTokenDto.RefreshToken))
var refreshToken = await _refreshTokenService.GetRefreshToken(refreshTokenDto.RefreshToken);
if (refreshToken == null || refreshToken.ExpiryDate < DateTime.UtcNow)
{
_logger.LogWarning("Refresh token is missing from the request body.");
return BadRequest(ApiResponse<object>.ErrorResponse("Refresh token is required.", "Missing refresh token.", 400));
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid or expired refresh token.", "Invalid or expired refresh token.", 401));
}
try
{
// Step 1: Fetch and validate the refresh token
var refreshToken = await _refreshTokenService.GetRefreshToken(refreshTokenDto.RefreshToken);
if (refreshToken == null)
{
_logger.LogWarning("Refresh token not found in the database");
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid or expired refresh token.", "Token not found.", 401));
}
// Mark token as used
await _refreshTokenService.MarkRefreshTokenAsUsed(refreshToken);
if (refreshToken.ExpiryDate < DateTime.UtcNow)
{
_logger.LogWarning("Refresh token expired");
return Unauthorized(ApiResponse<object>.ErrorResponse("Refresh token expired.", "Token expired.", 401));
}
// Generate new JWT token and refresh token
var user = await _userManager.FindByIdAsync(refreshToken.UserId ?? string.Empty);
if (user == null)
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request.", "Invalid request.", 400));
// Step 2: Mark the token as used
await _refreshTokenService.MarkRefreshTokenAsUsed(refreshToken);
_logger.LogInfo("Refresh token marked as used");
Employee emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id);
// Step 3: Validate and retrieve user
var user = await _userManager.FindByIdAsync(refreshToken.UserId ?? string.Empty);
if (user == null)
{
_logger.LogWarning("User not found for RefreshToken: {Token}", refreshTokenDto.RefreshToken);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request.", "User not found.", 400));
}
if (user.UserName == null) return NotFound(ApiResponse<object>.ErrorResponse("UserName Not found", "UserName Not found", 404));
if (string.IsNullOrWhiteSpace(user.UserName))
{
_logger.LogError("Username missing for user ID: {UserId}", user.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Username not found.", "Username not found.", 404));
}
var newJwtToken = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId, _jwtSettings);
var newRefreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings);
// Step 4: Fetch employee and generate new tokens
var emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id);
var newJwtToken = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId, _jwtSettings);
var newRefreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings);
_logger.LogInfo("New access and refresh token issued for user: {UserId}", user.Id);
return Ok(ApiResponse<object>.SuccessResponse(
new { token = newJwtToken, refreshToken = newRefreshToken },
"User refresh token generated successfully.",
200));
}
catch (Exception ex)
{
_logger.LogError("An unexpected error occurred during token refresh. : {Error}", ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error occurred.", ex.Message, 500));
}
return Ok(ApiResponse<object>.SuccessResponse(new { token = newJwtToken, refreshToken = newRefreshToken }, "User refresh token generated successfully.", 200));
}
[HttpPost("forgot-password")]
public async Task<IActionResult> ForgotPassword([FromBody] ForgotPasswordDto forgotPasswordDto)
{
if (string.IsNullOrWhiteSpace(forgotPasswordDto.Email))
{
_logger.LogWarning("ForgotPassword request received without email.");
return BadRequest(ApiResponse<object>.ErrorResponse("Email is required.", "Email is required.", 400));
}
var user = await _userManager.FindByEmailAsync(forgotPasswordDto.Email);
if (user == null || user.Email == null)
{
_logger.LogWarning("ForgotPassword requested for non-existent or null-email user: {Email}", forgotPasswordDto.Email);
// Do not disclose whether the email exists (security best practice)
return Ok(ApiResponse<object>.SuccessResponse(true, "Password reset link sent if the account exists.", 200));
}
if (user == null)
return NotFound(ApiResponse<object>.ErrorResponse("User not found.", "User not found.", 404));
try
{
// Generate token and build reset link
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}";
/* SEND USER REGISTRATION MAIL*/
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}";
// Send reset email
await _emailSender.SendResetPasswordEmail(user.Email, user.UserName ?? "User", resetLink);
if (user.Email == null) return NotFound(ApiResponse<object>.ErrorResponse("Email Not found", "Email Not found", 404));
_logger.LogInfo("Password reset link sent to user: {Email}", user.Email);
return Ok(ApiResponse<object>.SuccessResponse(true, "Password reset link sent if the account exists.", 200));
}
catch (Exception ex)
{
_logger.LogError("Error while sending password reset email to: {Error}", ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Error sending password reset email.", ex.Message, 500));
}
await _emailSender.SendResetPasswordEmail(user.Email, "", resetLink);
return Ok(ApiResponse<object>.SuccessResponse(true, "Password reset link sent.", 200));
}
[HttpPost("reset-password")]
public async Task<IActionResult> ResetPassword([FromBody] ResetPasswordDto model)
{
_logger.LogInfo("Password reset request received for email: {Email}", model.Email ?? string.Empty);
if (string.IsNullOrWhiteSpace(model.Email) || string.IsNullOrWhiteSpace(model.Token) || string.IsNullOrWhiteSpace(model.NewPassword))
{
_logger.LogWarning("Reset password failed due to missing input fields for email: {Email}", model.Email ?? string.Empty);
return BadRequest(ApiResponse<object>.ErrorResponse("All fields are required.", "Invalid input.", 400));
}
var user = await _userManager.FindByEmailAsync(model.Email);
var user = await _userManager.FindByEmailAsync(model.Email ?? string.Empty);
if (user == null)
{
_logger.LogWarning("Reset password failed - user not found for email: {Email}", model.Email);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request.", "Invalid user.", 400));
}
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request.", "Invalid request.", 400));
// var isTokenValid = await _userManager.VerifyUserTokenAsync(user,UserManager<ApplicationUser>.ResetPasswordTokenPurpose, model.ResetCode);
var isTokenValid = await _userManager.VerifyUserTokenAsync(
user,
TokenOptions.DefaultProvider, // This is the token provider
UserManager<ApplicationUser>.ResetPasswordTokenPurpose,
WebUtility.UrlDecode(model.Token)
);
user,
TokenOptions.DefaultProvider, // This is the token provider
UserManager<ApplicationUser>.ResetPasswordTokenPurpose,
WebUtility.UrlDecode(model.Token)
);
string token = "";
if (!isTokenValid)
{
_logger.LogWarning("Decoded token failed, retrying with raw token for email: {Email}", model.Email);
var isDecodedTokenValid = await _userManager.VerifyUserTokenAsync(
user,
TokenOptions.DefaultProvider,
TokenOptions.DefaultProvider, // This is the token provider
UserManager<ApplicationUser>.ResetPasswordTokenPurpose,
model.Token
model.Token
);
if (!isDecodedTokenValid)
{
_logger.LogWarning("Both decoded and raw token failed for email: {Email}", model.Email);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request.", "Invalid or expired reset token.", 400));
}
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request.", "Invalid request.", 400));
token = model.Token;
}
@ -461,187 +184,29 @@ namespace MarcoBMS.Services.Controllers
token = WebUtility.UrlDecode(model.Token);
}
var result = await _userManager.ResetPasswordAsync(user, token, model.NewPassword);
var result = await _userManager.ResetPasswordAsync(user, token, model.NewPassword ?? string.Empty);
if (!result.Succeeded)
{
var errors = result.Errors.Select(e => e.Description).ToList();
_logger.LogWarning("Reset password failed for user: {Email}. Errors: {Errors}", model.Email, string.Join(", ", errors));
return BadRequest(ApiResponse<object>.ErrorResponse("Failed to reset password.", errors, 400));
return BadRequest(ApiResponse<object>.ErrorResponse("Failed to Change password", errors, 400));
}
try
{
var emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id);
string fullName = $"{emp.FirstName} {emp.LastName}".Trim();
await _emailSender.SendResetPasswordSuccessEmail(user.Email!, fullName);
_logger.LogInfo("Reset password success email sent to user: {Email}", model.Email);
Employee emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id);
await _emailSender.SendResetPasswordSuccessEmail(user.Email ?? string.Empty, emp.FirstName + " " + emp.LastName);
}
catch (Exception ex)
{
_logger.LogError("Error while sending reset password success email to user: {Error}", ex.Message);
// Continue, do not fail because of email issue
return BadRequest(ApiResponse<object>.ErrorResponse(ex.Message, ex.Message, 400));
}
_logger.LogInfo("Password reset successful for user: {Email}", model.Email);
return Ok(ApiResponse<object>.SuccessResponse(true, "Password reset successfully.", 200));
return Ok(ApiResponse<object>.SuccessResponse(result.Succeeded, "Password reset successfully.", 200));
}
[HttpPost("send-otp")]
public async Task<IActionResult> SendOtpEmail([FromBody] GenerateOTPDto generateOTP)
{
try
{
// Validate input email
if (string.IsNullOrWhiteSpace(generateOTP.Email))
{
_logger.LogWarning("Send OTP failed - Email is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("Email is required", "Invalid email", 400));
}
// Fetch user by email
var requestedUser = await _userManager.FindByEmailAsync(generateOTP.Email);
string title = "Send OTP";
if (requestedUser != null && requestedUser.IsActive)
{
// Fetch employee details
var requestedEmployee = await _context.Employees
.FirstOrDefaultAsync(e => e.ApplicationUserId == requestedUser.Id);
// Generate a random 4-digit OTP
string otp = new Random().Next(1000, 9999).ToString();
// Store OTP in database
var otpDetails = new OTPDetails
{
UserId = Guid.Parse(requestedUser.Id),
OTP = otp,
ExpriesInSec = 600, // 10 minutes
TimeStamp = DateTime.UtcNow,
TenantId = requestedUser.TenantId
};
_context.OTPDetails.Add(otpDetails);
await _context.SaveChangesAsync();
// Prepare email
List<string> toEmails = [generateOTP.Email];
string name = $"{requestedEmployee?.FirstName} {requestedEmployee?.LastName}".Trim();
var mailTemplate = await _context.MailingList
.FirstOrDefaultAsync(t => t.Title.ToLower() == title.ToLower());
string subject = mailTemplate?.Subject ?? string.Empty;
string emailBody = mailTemplate?.Body ?? string.Empty;
// Send OTP via email
await _emailSender.SendOTP(toEmails, emailBody, name, otp, subject);
_logger.LogInfo("OTP sent successfully to {Email}", generateOTP.Email);
return Ok(ApiResponse<object>.SuccessResponse("Success", "OTP generated successfully", 200));
}
_logger.LogWarning("Send OTP failed - Invalid or inactive user: {Email}", generateOTP.Email);
return BadRequest(ApiResponse<object>.ErrorResponse("Provided invalid information", "User not found or inactive", 400));
}
catch (Exception ex)
{
_logger.LogError("An unexpected error occurred while sending OTP to {Email} : {Error}", generateOTP.Email ?? "", ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An unexpected error occurred.", ex.Message, 500));
}
}
[HttpPost("login-otp")]
public async Task<IActionResult> LoginWithOTP([FromBody] VerifyOTPDto verifyOTP)
{
try
{
// Validate input
if (string.IsNullOrWhiteSpace(verifyOTP.Email) ||
string.IsNullOrWhiteSpace(verifyOTP.OTP) ||
verifyOTP.OTP.Length != 4 ||
!verifyOTP.OTP.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.OTP)
{
_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")]
public async Task<IActionResult> SendEmail([FromBody] EmailDot emailDot)
@ -677,150 +242,5 @@ namespace MarcoBMS.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(new { }, "Password reset link sent.", 200));
}
[Authorize]
[HttpPost("change-password")]
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordDto changePassword)
{
try
{
// Get the currently logged-in user
var loggedUser = await _userHelper.GetCurrentUserAsync();
// Validate email
if (string.IsNullOrWhiteSpace(changePassword.Email))
{
_logger.LogWarning("Change password attempt failed - Email is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("Email is missing", "Email is missing", 400));
}
// Find the user by email
var requestedUser = await _userManager.FindByEmailAsync(changePassword.Email);
if (requestedUser == null)
{
_logger.LogWarning("Change password attempt failed - Email not found: {Email}", changePassword.Email);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid email", "User not found.", 400));
}
// Validate the old password
bool isOldPasswordCorrect = await _userManager.CheckPasswordAsync(requestedUser, changePassword.OldPassword ?? string.Empty);
// Ensure user identity and old password match
if (loggedUser?.Email != requestedUser.Email || !isOldPasswordCorrect)
{
_logger.LogWarning("Change password denied - User {Email} provided incorrect credentials", changePassword.Email);
return BadRequest(ApiResponse<object>.ErrorResponse("Incorrect credentials", "Invalid request.", 400));
}
// Generate reset token and change password
var resetToken = await _userManager.GeneratePasswordResetTokenAsync(requestedUser);
var result = await _userManager.ResetPasswordAsync(requestedUser, resetToken, changePassword.NewPassword ?? string.Empty);
if (!result.Succeeded)
{
var errors = result.Errors.Select(e => e.Description).ToList();
_logger.LogError("Password reset failed for user {Email}. Errors: {Errors}", changePassword.Email, string.Join("; ", errors));
return BadRequest(ApiResponse<object>.ErrorResponse("Failed to change password", errors, 400));
}
// Send confirmation email
var emp = await _employeeHelper.GetEmployeeByApplicationUserID(requestedUser.Id);
await _emailSender.SendResetPasswordSuccessEmail(requestedUser.Email ?? string.Empty, $"{emp.FirstName} {emp.LastName}");
_logger.LogInfo("Password changed successfully for user {Email}", requestedUser.Email ?? string.Empty);
return Ok(ApiResponse<object>.SuccessResponse(true, "Password changed successfully.", 200));
}
catch (Exception exp)
{
_logger.LogError("An unexpected error occurred while changing password : {Error}", exp.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An unexpected error occurred.", exp.Message, 500));
}
}
[Authorize]
[HttpPost("generate-mpin")]
public async Task<IActionResult> GenerateMPIN([FromBody] GenerateMPINDto generateMPINDto)
{
Guid tenantId = _userHelper.GetTenantId();
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Get the employee for whom MPIN is being generated
var requestEmployee = await _context.Employees
.Include(e => e.ApplicationUser)
.FirstOrDefaultAsync(e => e.Id == generateMPINDto.EmployeeId && e.TenantId == tenantId);
// Validate employee and MPIN input
if (requestEmployee == null || string.IsNullOrWhiteSpace(generateMPINDto.MPIN) || generateMPINDto.MPIN.Length != 6 || !generateMPINDto.MPIN.All(char.IsDigit))
{
_logger.LogError("Employee {EmployeeId} provided invalid information to generate MPIN", loggedInEmployee.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Provided invalid information", "Provided invalid information", 400));
}
// Ensure the logged-in user is only generating their own MPIN
if (requestEmployee.Id != loggedInEmployee.Id)
{
_logger.LogWarning("Employee {EmployeeId} tried to set MPIN for a different employee", loggedInEmployee.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("You can't create MPIN for another employee", "Unauthorized MPIN creation", 400));
}
// Generate hash and token
string mpinHash = ComputeSha256Hash(generateMPINDto.MPIN);
string mpinToken = _refreshTokenService.CreateMPINToken(
requestEmployee.ApplicationUserId,
requestEmployee.TenantId.ToString(),
_jwtSettings
);
// Prepare MPIN entity
Guid userId = Guid.Parse(requestEmployee.ApplicationUserId ?? string.Empty);
var existingMPIN = await _context.MPINDetails.FirstOrDefaultAsync(p => p.UserId == userId && p.TenantId == tenantId);
if (existingMPIN == null)
{
// Add new MPIN record
var mPINDetails = new MPINDetails
{
UserId = userId,
MPIN = mpinHash,
MPINToken = mpinToken,
TimeStamp = DateTime.UtcNow,
TenantId = tenantId
};
_context.MPINDetails.Add(mPINDetails);
await _context.SaveChangesAsync();
_logger.LogInfo("MPIN generated successfully for employee {EmployeeId}", requestEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(mpinToken, "MPIN generated successfully", 200));
}
else
{
// Update existing MPIN record
existingMPIN.MPIN = mpinHash;
existingMPIN.MPINToken = mpinToken;
existingMPIN.TimeStamp = DateTime.UtcNow;
await _context.SaveChangesAsync();
_logger.LogInfo("MPIN updated successfully for employee {EmployeeId}", requestEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(mpinToken, "MPIN updated successfully", 200));
}
}
private static string ComputeSha256Hash(string rawData)
{
using (SHA256 sha256 = SHA256.Create())
{
// Convert the input string to bytes and compute the hash
byte[] bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(rawData));
// Convert byte array to a readable hex string
StringBuilder builder = new StringBuilder();
foreach (var b in bytes)
builder.Append(b.ToString("x2"));
return builder.ToString();
}
}
}
}

View File

@ -25,17 +25,13 @@ namespace MarcoBMS.Services.Controllers
private readonly ApplicationDbContext _context;
private readonly UserHelper _userHelper;
private readonly ILoggingService _logger;
private readonly RolesHelper _rolesHelper;
private readonly ProjectsHelper _projectsHelper;
public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper)
public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger)
{
_context = context;
_userHelper = userHelper;
_logger = logger;
_rolesHelper = rolesHelper;
_projectsHelper = projectHelper;
}
[HttpGet("list")]
public async Task<IActionResult> GetAll()
@ -50,22 +46,7 @@ namespace MarcoBMS.Services.Controllers
}
Guid tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(LoggedInEmployee.Id);
string[] projectsId = [];
List<Project> projects = new List<Project>();
/* User with permission manage project can see all projects */
if (featurePermission != null && featurePermission.Exists(c => c.Id.ToString() == "172fc9b6-755b-4f62-ab26-55c34a330614"))
{
projects = await _projectsHelper.GetAllProjectByTanentID(LoggedInEmployee.TenantId);
}
else
{
List<ProjectAllocation> allocation = await _projectsHelper.GetProjectByEmployeeID(LoggedInEmployee.Id);
projectsId = allocation.Select(c => c.ProjectId.ToString()).ToArray();
projects = await _context.Projects.Where(c => projectsId.Contains(c.Id.ToString()) && c.TenantId == tenantId).ToListAsync();
}
List<Project> projects = await _context.Projects.Where(c => c.TenantId == tenantId).ToListAsync();
List<ProjectListVM> response = new List<ProjectListVM>();

View File

@ -45,6 +45,7 @@ namespace Marco.Pms.Services.Controllers
Recipient = mailDetailsDto.Recipient,
Schedule = mailDetailsDto.Schedule,
MailListId = mailDetailsDto.MailListId,
Subject = mailDetailsDto.Subject,
TenantId = tenantId
};
_context.MailDetails.Add(mailDetails);
@ -56,22 +57,10 @@ namespace Marco.Pms.Services.Controllers
public async Task<IActionResult> AddMailTemplate([FromBody] MailTemeplateDto mailTemeplateDto)
{
Guid tenantId = _userHelper.GetTenantId();
if (string.IsNullOrWhiteSpace(mailTemeplateDto.Body) && string.IsNullOrWhiteSpace(mailTemeplateDto.Title))
{
_logger.LogWarning("User tries to set email template but send invalid data");
return BadRequest(ApiResponse<object>.ErrorResponse("Provided Invalid data", "Provided Invalid data", 400));
}
var existngTemalate = await _context.MailingList.FirstOrDefaultAsync(t => t.Title.ToLower() == mailTemeplateDto.Title.ToLower());
if (existngTemalate != null)
{
_logger.LogWarning("User tries to set email template, but title already existed in database");
return BadRequest(ApiResponse<object>.ErrorResponse("Email title is already existed", "Email title is already existed", 400));
}
MailingList mailingList = new MailingList
{
Title = mailTemeplateDto.Title,
Body = mailTemeplateDto.Body,
Subject = mailTemeplateDto.Subject,
Keywords = mailTemeplateDto.Keywords,
TenantId = tenantId
};
@ -103,8 +92,7 @@ namespace Marco.Pms.Services.Controllers
ProjectId = g.Key.ProjectId,
MailListId = g.Key.MailListId,
Recipients = g.Select(m => m.Recipient).Distinct().ToList(),
MailBody = g.FirstOrDefault()?.MailBody?.Body ?? "",
Subject = g.FirstOrDefault()?.MailBody?.Subject ?? string.Empty,
MailBody = g.FirstOrDefault()?.MailBody?.Body ?? ""
})
.ToList();
@ -116,7 +104,7 @@ namespace Marco.Pms.Services.Controllers
await semaphore.WaitAsync();
try
{
var response = await GetProjectStatistics(mailDetail.ProjectId, mailDetail.Recipients, mailDetail.MailBody, mailDetail.Subject, tenantId);
var response = await GetProjectStatistics(mailDetail.ProjectId, mailDetail.Recipients, mailDetail.MailBody, tenantId);
if (response.StatusCode == 200)
Interlocked.Increment(ref successCount);
else if (response.StatusCode == 404)
@ -149,7 +137,7 @@ namespace Marco.Pms.Services.Controllers
/// <param name="projectId">The ID of the project.</param>
/// <param name="recipientEmail">The email address of the recipient.</param>
/// <returns>An ApiResponse indicating the success or failure of retrieving statistics and sending the email.</returns>
private async Task<ApiResponse<object>> GetProjectStatistics(Guid projectId, List<string> recipientEmails, string body, string subject, Guid tenantId)
private async Task<ApiResponse<object>> GetProjectStatistics(Guid projectId, List<string> recipientEmails, string body, Guid tenantId)
{
DateTime reportDate = DateTime.UtcNow.AddDays(-1).Date;
@ -315,7 +303,7 @@ namespace Marco.Pms.Services.Controllers
statisticReport.PerformedAttendance = performedAttendance;
// Send Email
var emailBody = await _emailSender.SendProjectStatisticsEmail(recipientEmails, body, subject, statisticReport);
var emailBody = await _emailSender.SendProjectStatisticsEmail(recipientEmails, body, statisticReport);
var employee = await _context.Employees.FirstOrDefaultAsync(e => e.Email != null && recipientEmails.Contains(e.Email)) ?? new Employee();
List<MailLog> mailLogs = new List<MailLog>();

View File

@ -173,7 +173,7 @@
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color: #f9f9f9;"><![endif]-->
<!--<div>Top Spacing</div>-->
<div class="u-row-container" style="padding: 0px; background-color: #f9f9f9; page-break-before: always;">
<div class="u-row-container" style="padding: 0px;background-color: #f9f9f9">
<div class="u-row"
style="margin: 0 auto;min-width: 320px;max-width: 95%;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: #f9f9f9;">
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
@ -338,6 +338,7 @@
<!--<p style="font-size: 14px; line-height: 140%; text-align: center;"><span style="font-size: 28px; line-height: 39.2px; color: #ffffff; font-family: Lato, sans-serif;">{{PROJECT_NAME}} </span></p>.-->
<p style="font-size: 14px; line-height: 140%; text-align: center;">
<span style="font-size: 28px; line-height: 39.2px; color: #ffffff; font-family: Lato, sans-serif;">
Raja
{{PROJECT_NAME}}
</span>
</p>
@ -362,7 +363,7 @@
<!--<div>Mail Body</div>-->
<div class="u-row-container" style="padding: 0px; background-color: transparent; page-break-before: always; page-break-after: always;">
<div class="u-row-container" style="padding: 0px;background-color: transparent">
<div class="u-row"
style="margin: 0 auto;min-width: 320px;max-width: 95%;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: #ffffff;">
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
@ -383,9 +384,9 @@
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:40px 10px 30px;font-family:'Lato',sans-serif;"
align="left">
<div style="font-size: 14px; line-height: 140%; text-align: start; word-wrap: break-word;">
<span style="font-size: 10px; line-height: 25.2px; color: #666666;">
* Project Status Reported - Generated at {{TIMESTAMP}} UTC
<div style="font-size: 14px; line-height: 140%; text-align: center; word-wrap: break-word;">
<span style="font-size: 18px; line-height: 25.2px; color: #666666;">
Project Status Reported - Generated at {{TIMESTAMP}}
</span>
<table cellpadding="1" cellspacing="0" width="100%"
border="0" style="margin-top: 15px">
@ -393,7 +394,7 @@
<td class="column" style="text-align:center">
<div style="border: 1px solid #d5d5d5; border-radius: 10px; margin: 10px 10px; padding: 10px; height: 135px !important">
<div style="font-size: 18px; color: #525b75 ">
Todays Attendance
Todays Attendane
</div>
<div style="font-size: 25px; color: #bc3803;margin:20px 20px 0px!important; font-weight:bold; ">
{{TODAYS_ATTENDANCES}} /
@ -410,7 +411,7 @@
Daily Tasks Completed
</div>
<div style="font-size: 25px; color: #bc3803; margin: 20px 20px 0px !important; font-weight: bold;">
{{TODAYS_COMPLETED}} /
{{TODAYS_COMPLETED}} /
{{TODAYS_PLANNED}}
</div>
<span style="font-size: 10px; color: #0984e3; font-weight: bold;">
@ -477,47 +478,13 @@
</td>
</tr>
</tbody>
</table>
</div>
<!--[if (!mso)&(!IE)]><!-->
</div><!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
</div>
</div>
<div class="u-row-container" style="padding: 0px; background-color: transparent; page-break-before: always; page-break-after: always;">
<div class="u-row"
style="margin: 0 auto;min-width: 320px;max-width: 95%;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: #ffffff;">
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:600px;"><tr style="background-color: #ffffff;"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="600" style="width: 600px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><![endif]-->
<div class="u-col u-col-100"
style="max-width: 320px;min-width: 600px;display: table-cell;vertical-align: top;">
<div style="height: 100%;width: 100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div style="box-sizing: border-box; height: 100%; padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;">
<!--<![endif]-->
<table style="font-family:'Lato',sans-serif;" role="presentation"
cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr style="page-break-before: always;">
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:0px 1px 30px;font-family:'Lato',sans-serif;"
align="left">
<div style="font-size: 14px; line-height: 140%; text-align: center; word-wrap: break-word;">
<span style="font-size: 18px; line-height: 25.2px; color: #666666;">
Team
Available On Site {{DATE}}
Available On Site Today
</span>
<table cellpadding="1" cellspacing="0" width="100%"
border="0">
@ -526,137 +493,63 @@
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if (!mso)&(!IE)]><!-->
</div><!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
</div>
</div>
<div class="u-row-container" style="padding: 0px; background-color: transparent; page-break-before: always; page-break-after: always;">
<div class="u-row"
style="margin: 0 auto;min-width: 320px;max-width: 95%;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: #ffffff;">
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:600px;"><tr style="background-color: #ffffff;"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="600" style="width: 600px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><![endif]-->
<div class="u-col u-col-100"
style="max-width: 320px;min-width: 600px;display: table-cell;vertical-align: top;">
<div style="height: 100%;width: 100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div style="box-sizing: border-box; height: 100%; padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;">
<!--<![endif]-->
<table style="font-family:'Lato',sans-serif;" role="presentation"
cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr style="page-break-before: always;">
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:0px 10px 30px;font-family:'Lato',sans-serif;"
align="left">
<div style="font-size: 14px; line-height: 140%; text-align: center; word-wrap: break-word;">
<span style="font-size: 18px; line-height: 25.2px; color: #666666;margin:10px">
Activities
(Tasks) Performed {{DATE}}
(Tasks) Performed Today
</span> <br />
<table cellpadding="1" cellspacing="0" width="100%"
border="1">
<thead>
<tr style="vertical-align:middle">
<th style="text-align:center">
Activity/ <br />
Location
</th>
<th style="text-align:center">
Assigned
Today/<br /> Pending
</th>
<th style="text-align:center">
Completed Today
</th>
<th style="text-align:center">Date </th>
<th style="text-align:center">Team Members</th>
<th style="text-align:center">Comment</th>
</tr>
</thead>
<tr></tr>
<tbody>
{{PERFORMED_TASK}}
</tbody>
</table>
<table cellpadding="1" cellspacing="0" width="100%"
border="1">
<tr style="vertical-align:middle">
<th style="text-align:center">
Activity/ <br />
Location
</th>
<th style="text-align:center">
Assigned
Today/<br /> Pending
</th>
<th style="text-align:center">
Completed Today
</th>
<th style="text-align:center">Date </th>
<th style="text-align:center">Team Members</th>
<th style="text-align:center">Comment</th>
</tr>
<tr></tr>
{{PERFORMED_TASK}}
</table>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if (!mso)&(!IE)]><!-->
</div><!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
</div>
</div>
<div class="u-row-container" style="padding: 0px; background-color: transparent; page-break-before: always; page-break-after: always;">
<div class="u-row"
style="margin: 0 auto;min-width: 320px;max-width: 95%;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: #ffffff;">
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:600px;"><tr style="background-color: #ffffff;"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="600" style="width: 600px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><![endif]-->
<div class="u-col u-col-100"
style="max-width: 320px;min-width: 600px;display: table-cell;vertical-align: top;">
<div style="height: 100%;width: 100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div style="box-sizing: border-box; height: 100%; padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;">
<!--<![endif]-->
<table style="font-family:'Lato',sans-serif;" role="presentation"
cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr style="page-break-before: always;">
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:0px 10px 30px;font-family:'Lato',sans-serif;"
align="left">
<div style="font-size: 14px; line-height: 140%; text-align: center; word-wrap: break-word;">
<span style="font-size: 18px; line-height: 25.2px; color: #666666;margin:10px">
Attendance
Performed {{DATE}}
Performed Today
</span> <br />
<table cellpadding="1" cellspacing="0" width="100%"
border="1">
<thead>
<tr style="vertical-align:middle">
<th style="text-align:center">Name</th>
<th style="text-align:center">Job Role</th>
<th style="text-align:center">Check In</th>
<th style="text-align:center">Check Out </th>
<th style="text-align:center">Comment</th>
</tr>
</thead>
<tr></tr>
<tbody>
{{PERFORMED_ATTENDANCE}}
</tbody>
</table>
<table cellpadding="1" cellspacing="0" width="100%"
border="1">
<tr style="vertical-align:middle">
<th style="text-align:center">Name</th>
<th style="text-align:center">Job Role</th>
<th style="text-align:center">Check In</th>
<th style="text-align:center">Check Out </th>
<th style="text-align:center">Comment</th>
</tr>
<tr></tr>
{{PERFORMED_ATTENDANCE}}
</table>
</div>
</td>
</tr>
@ -679,7 +572,7 @@
<!--<div>Contact</div>-->
<div class="u-row-container" style="padding: 0px; background-color: transparent; page-break-before: always;">
<div class="u-row-container" style="padding: 0px;background-color: transparent">
<div class="u-row"
style="margin: 0 auto;min-width: 320px;max-width: 95%;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: #e93f32;">
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
@ -832,18 +725,18 @@
</table>
<!--<table style="font-family:'Lato',sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:5px 10px 10px;font-family:'Lato',sans-serif;" align="left">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:5px 10px 10px;font-family:'Lato',sans-serif;" align="left">
<div style="font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;">
<p style="line-height: 140%; font-size: 14px;"><span style="font-size: 14px; line-height: 19.6px;"><span style="color: #ecf0f1; font-size: 14px; line-height: 19.6px;"><span style="line-height: 19.6px; font-size: 14px;">Marco AIoT Technologies Pvt. Ltd. &copy;&nbsp; All Rights Reserved</span></span></span></p>
</div>
<div style="font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;">
<p style="line-height: 140%; font-size: 14px;"><span style="font-size: 14px; line-height: 19.6px;"><span style="color: #ecf0f1; font-size: 14px; line-height: 19.6px;"><span style="line-height: 19.6px; font-size: 14px;">Marco AIoT Technologies Pvt. Ltd. &copy;&nbsp; All Rights Reserved</span></span></span></p>
</div>
</td>
</tr>
</tbody>
</table>-->
</td>
</tr>
</tbody>
</table>-->
<!--[if (!mso)&(!IE)]><!-->
</div><!--<![endif]-->
</div>
@ -892,14 +785,14 @@
</p>
<!--<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;border-top: 1px solid #e93f32;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%">
<tbody>
<tr style="vertical-align: top">
<td style="word-break: break-word;border-collapse: collapse !important;vertical-align: top;mso-line-height-rule: exactly;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%">
<tbody>
<tr style="vertical-align: top">
<td style="word-break: break-word;border-collapse: collapse !important;vertical-align: top;mso-line-height-rule: exactly;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%">
</td>
</tr>
</tbody>
</table>-->
</td>
</tr>
</tbody>
</table>-->
</td>
</tr>

View File

@ -1,538 +0,0 @@

<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="x-apple-disable-message-reformatting">
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]-->
<title></title>
<style type="text/css">
@media only screen and (min-width: 620px) {
.u-row {
width: 600px !important;
}
.u-row .u-col {
vertical-align: top;
}
.u-row .u-col-50 {
width: 300px !important;
}
.u-row .u-col-100 {
width: 600px !important;
}
}
@media only screen and (max-width: 620px) {
.u-row-container {
max-width: 100% !important;
padding-left: 0px !important;
padding-right: 0px !important;
}
.u-row {
width: 100% !important;
}
.u-row .u-col {
display: block !important;
width: 100% !important;
min-width: 320px !important;
max-width: 100% !important;
}
.u-row .u-col > div {
margin: 0 auto;
}
.u-row .u-col img {
max-width: 100% !important;
}
}
body {
margin: 0;
padding: 0
}
table, td, tr {
border-collapse: collapse;
vertical-align: top
}
p {
margin: 0
}
.ie-container table, .mso-container table {
table-layout: fixed
}
* {
line-height: inherit
}
a[x-apple-data-detectors=true] {
color: inherit !important;
text-decoration: none !important
}
table, td {
color: #000000;
}
#u_body a {
color: #e93f32;
text-decoration: underline;
}
</style>
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Lato:400,700" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Lato:400,700" rel="stylesheet" type="text/css"><!--<![endif]-->
</head>
<body class="clean-body u_body" style="margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #f9f9f9;color: #000000">
<!--[if IE]><div class="ie-container"><![endif]-->
<!--[if mso]><div class="mso-container"><![endif]-->
<table id="u_body" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;min-width: 320px;Margin: 0 auto;background-color: #f9f9f9;width:100%" cellpadding="0" cellspacing="0">
<tbody>
<tr style="vertical-align: top">
<td style="word-break: break-word;border-collapse: collapse !important;vertical-align: top">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color: #f9f9f9;"><![endif]-->
<!--<div>Top Spacing</div>-->
<div class="u-row-container" style="padding: 0px;background-color: #f9f9f9">
<div class="u-row" style="margin: 0 auto;min-width: 320px;max-width: 600px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: #f9f9f9;">
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: #f9f9f9;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:600px;"><tr style="background-color: #f9f9f9;"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="600" style="width: 600px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><![endif]-->
<div class="u-col u-col-100" style="max-width: 320px;min-width: 600px;display: table-cell;vertical-align: top;">
<div style="height: 100%;width: 100% !important;">
<!--[if (!mso)&(!IE)]><!--><div style="box-sizing: border-box; height: 100%; padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;">
<!--<![endif]-->
<table style="font-family:'Lato',sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:15px;font-family:'Lato',sans-serif;" align="left">
<table height="0px" align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;border-top: 1px solid #f9f9f9;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%">
<tbody>
<tr style="vertical-align: top">
<td style="word-break: break-word;border-collapse: collapse !important;vertical-align: top;font-size: 0px;line-height: 0px;mso-line-height-rule: exactly;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%">
Sita
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if (!mso)&(!IE)]><!-->
</div><!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--<div>Logo Block</div>-->
<div class="u-row-container" style="padding: 0px;background-color: transparent">
<div class="u-row" style="margin: 0 auto;min-width: 320px;max-width: 600px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: #ffffff;">
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:600px;"><tr style="background-color: #ffffff;"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="600" style="width: 600px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><![endif]-->
<div class="u-col u-col-100" style="max-width: 320px;min-width: 600px;display: table-cell;vertical-align: top;">
<div style="height: 100%;width: 100% !important;">
<!--[if (!mso)&(!IE)]><!--><div style="box-sizing: border-box; height: 100%; padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;">
<!--<![endif]-->
<table style="font-family:'Lato',sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:25px 10px;font-family:'Lato',sans-serif;" align="left">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="padding-right: 0px;padding-left: 0px;" align="center">
<img border="0" src="https://stageapi.marcoaiot.com/logos/marco-aiot-tech-logo.jpg" alt="Image" title="Image" style="outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;clear: both;display: inline-block !important;border: none;height: auto;float: none;width: 29%;max-width: 168.2px;" width="168" />
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if (!mso)&(!IE)]><!-->
</div><!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--<div>Title Block</div>-->
<div class="u-row-container" style="padding: 0px;background-color: transparent">
<div class="u-row" style="margin: 0 auto; min-width: 320px; max-width: 600px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: #f46b61;">
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:600px;"><tr style="background-color: #e93f32;"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="600" style="width: 600px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><![endif]-->
<div class="u-col u-col-100" style="max-width: 320px;min-width: 600px;display: table-cell;vertical-align: top;">
<div style="height: 100%;width: 100% !important;">
<!--[if (!mso)&(!IE)]><!--><div style="box-sizing: border-box; height: 100%; padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;">
<!--<![endif]-->
<table style="font-family:'Lato',sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:35px 10px 10px;font-family:'Lato',sans-serif;" align="left">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="padding-right: 0px;padding-left: 0px;" align="center">
<img border="0" src="https://cdn.templates.unlayer.com/assets/1593141680866-reset.png" alt="Image" title="Image" style="outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;clear: both;display: inline-block !important;border: none;height: auto;float: none;width: 10%;max-width: 58px;" width="58" />
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
<table style="font-family:'Lato',sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:0px 10px 30px;font-family:'Lato',sans-serif;" align="left">
<div style="font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;">
<p style="font-size: 14px; line-height: 140%; text-align: center;"><span style="font-size: 28px; line-height: 39.2px; color: #ffffff; font-family: Lato, sans-serif;">Your OTP Code for Verification</span></p>
</div>
</td>
</tr>
</tbody>
</table>
<!--[if (!mso)&(!IE)]><!-->
</div><!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--<div>Mail Body</div>-->
<div class="u-row-container" style="padding: 0px;background-color: transparent">
<div class="u-row" style="margin: 0 auto;min-width: 320px;max-width: 600px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: #ffffff;">
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:600px;"><tr style="background-color: #ffffff;"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="600" style="width: 600px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><![endif]-->
<div class="u-col u-col-100" style="max-width: 320px;min-width: 600px;display: table-cell;vertical-align: top;">
<div style="height: 100%;width: 100% !important;">
<!--[if (!mso)&(!IE)]><!--><div style="box-sizing: border-box; height: 100%; padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;">
<!--<![endif]-->
<table style="font-family:'Lato',sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:40px 40px 30px;font-family:'Lato',sans-serif;" align="left">
<p>Dear {{NAME}},</p>
<br/>
<p>Your One-Time Password (OTP) for verification is: <strong style="color:#2e6c80;">{{OTP}}</strong></p>
<br/>
<p>This OTP is valid for the next <strong>10 minutes</strong>. Please do not share this code with anyone.</p>
<br/>
<p>If you did not request this code, please ignore this email or contact our support team immediately.</p>
<br>
<p>
Thank you,<br>
Marco AIoT Team
</p>
</td>
</tr>
</tbody>
</table>
<!--[if (!mso)&(!IE)]><!-->
</div><!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--<div>Contact</div>-->
<div class="u-row-container" style="padding: 0px;background-color: transparent">
<div class="u-row" style="margin: 0 auto;min-width: 320px;max-width: 600px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: #e93f32;">
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:600px;"><tr style="background-color: #e93f32;"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="300" style="width: 300px;padding: 20px 20px 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><![endif]-->
<div class="u-col u-col-50" style="max-width: 320px;min-width: 300px;display: table-cell;vertical-align: top;">
<div style="height: 100%;width: 100% !important;">
<!--[if (!mso)&(!IE)]><!--><div style="box-sizing: border-box; height: 100%; padding: 20px 0px 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;">
<!--<![endif]-->
<table style="font-family:'Lato',sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:'Lato',sans-serif;" align="left">
<div style="font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;">
<!--<p style="font-size: 14px; line-height: 140%;"><span style="font-size: 16px; line-height: 22.4px; color: #ecf0f1;">Contact</span></p>-->
<!--<p style="font-size: 14px; line-height: 140%;"><span style="font-size: 14px; line-height: 19.6px; color: #ecf0f1;">2nd Floor, Fullora Building, Tejas CHS, Dahanukar Colony, Kothrud, Pune (INDIA) - 411038</span></p>-->
<p style="font-size: 14px; line-height: 140%;"><span style="font-size: 14px; line-height: 19.6px; color: #ecf0f1;">Contact Us: <a href="mailto:info@marcoaiot.com" style="color:#ffff" target="_blank">info@marcoaiot.com</a></span></p>
</div>
</td>
</tr>
</tbody>
</table>
<!--[if (!mso)&(!IE)]><!-->
</div><!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="300" style="width: 300px;padding: 0px 0px 0px 20px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><![endif]-->
<div class="u-col u-col-50" style="max-width: 320px;min-width: 300px;display: table-cell;vertical-align: top;">
<div style="height: 100%;width: 100% !important;">
<!--[if (!mso)&(!IE)]><!--><div style="box-sizing: border-box; height: 100%; padding: 0px 0px 0px 20px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;">
<!--<![endif]-->
<table style="font-family:'Lato',sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:25px 10px 10px;font-family:'Lato',sans-serif;" align="left">
<div align="right" style="direction: ltr;">
<div style="display: table; max-width:187px;">
<!--[if (mso)|(IE)]><table width="187" cellpadding="0" cellspacing="0" border="0"><tr><td style="border-collapse:collapse;" align="left"><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse; mso-table-lspace: 0pt;mso-table-rspace: 0pt; width:187px;"><tr><![endif]-->
<!--[if (mso)|(IE)]><td width="32" style="width:32px; padding-right: 15px;" valign="top"><![endif]-->
<table border="0" cellspacing="0" cellpadding="0" width="32" height="32" style="width: 32px !important;height: 32px !important;display: inline-block;border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;margin-right: 15px">
<tbody>
<tr style="vertical-align: top">
<td valign="middle" style="word-break: break-word;border-collapse: collapse !important;vertical-align: top">
<a href=" " title="Facebook" target="_blank">
<img src="https://cdn.tools.unlayer.com/social/icons/circle-white/facebook.png" alt="Facebook" title="Facebook" width="32" style="outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;clear: both;display: block !important;border: none;height: auto;float: none;max-width: 32px !important">
</a>
</td>
</tr>
</tbody>
</table>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]><td width="32" style="width:32px; padding-right: 15px;" valign="top"><![endif]-->
<table border="0" cellspacing="0" cellpadding="0" width="32" height="32" style="width: 32px !important;height: 32px !important;display: inline-block;border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;margin-right: 15px">
<tbody>
<tr style="vertical-align: top">
<td valign="middle" style="word-break: break-word;border-collapse: collapse !important;vertical-align: top">
<a href="https://x.com/marcoaiot" title="X" target="_blank">
<img src="https://cdn.tools.unlayer.com/social/icons/circle-white/x.png" alt="Twitter" title="Twitter" width="32" style="color:#000;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;clear: both;display: block !important;border: none;height: auto;float: none;max-width: 32px !important">
</a>
</td>
</tr>
</tbody>
</table>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]><td width="32" style="width:32px; padding-right: 15px;" valign="top"><![endif]-->
<table border="0" cellspacing="0" cellpadding="0" width="32" height="32" style="width: 32px !important;height: 32px !important;display: inline-block;border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;margin-right: 15px">
<tbody>
<tr style="vertical-align: top">
<td valign="middle" style="word-break: break-word;border-collapse: collapse !important;vertical-align: top">
<a href=" " title="Instagram" target="_blank">
<img src="https://cdn.tools.unlayer.com/social/icons/circle-white/instagram.png" alt="Instagram" title="Instagram" width="32" style="outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;clear: both;display: block !important;border: none;height: auto;float: none;max-width: 32px !important">
</a>
</td>
</tr>
</tbody>
</table>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]><td width="32" style="width:32px; padding-right: 0px;" valign="top"><![endif]-->
<table border="0" cellspacing="0" cellpadding="0" width="32" height="32" style="width: 32px !important;height: 32px !important;display: inline-block;border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;margin-right: 0px">
<tbody>
<tr style="vertical-align: top">
<td valign="middle" style="word-break: break-word;border-collapse: collapse !important;vertical-align: top">
<a href=" " title="LinkedIn" target="_blank">
<img src="https://cdn.tools.unlayer.com/social/icons/circle-white/linkedin.png" alt="LinkedIn" title="LinkedIn" width="32" style="outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;clear: both;display: block !important;border: none;height: auto;float: none;max-width: 32px !important">
</a>
</td>
</tr>
</tbody>
</table>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
</div>
</div>
</td>
</tr>
</tbody>
</table>
<!--<table style="font-family:'Lato',sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:5px 10px 10px;font-family:'Lato',sans-serif;" align="left">
<div style="font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;">
<p style="line-height: 140%; font-size: 14px;"><span style="font-size: 14px; line-height: 19.6px;"><span style="color: #ecf0f1; font-size: 14px; line-height: 19.6px;"><span style="line-height: 19.6px; font-size: 14px;">Marco AIoT Technologies Pvt. Ltd. &copy;&nbsp; All Rights Reserved</span></span></span></p>
</div>
</td>
</tr>
</tbody>
</table>-->
<!--[if (!mso)&(!IE)]><!-->
</div><!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--<div>Bottom Spacing - light red</div>-->
<div class="u-row-container" style="padding: 0px;background-color: #f9f9f9">
<div class="u-row" style="margin: 0 auto; min-width: 320px; max-width: 600px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: #f46b61;">
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: #f9f9f9;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:600px;"><tr style="background-color: #e93f32;"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="600" style="width: 600px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><![endif]-->
<div class="u-col u-col-100" style="max-width: 320px;min-width: 600px;display: table-cell;vertical-align: top;">
<div style="height: 100%;width: 100% !important;">
<!--[if (!mso)&(!IE)]><!--><div style="box-sizing: border-box; height: 100%; padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;">
<!--<![endif]-->
<table style="font-family:'Lato',sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:15px;font-family:'Lato',sans-serif;" align="center">
<p style="line-height: 140%; font-size: 14px;"><span style="font-size: 14px; line-height: 19.6px;"><span style="color: #ecf0f1; font-size: 14px; line-height: 19.6px;"><span style="line-height: 19.6px; font-size: 14px;">Marco AIoT Technologies Pvt. Ltd. &copy;&nbsp; All Rights Reserved</span></span></span></p>
<!--<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;border-top: 1px solid #e93f32;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%">
<tbody>
<tr style="vertical-align: top">
<td style="word-break: break-word;border-collapse: collapse !important;vertical-align: top;mso-line-height-rule: exactly;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%">
</td>
</tr>
</tbody>
</table>-->
</td>
</tr>
</tbody>
</table>
<!--[if (!mso)&(!IE)]><!-->
</div><!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<div class="u-row-container" style="padding: 0px;background-color: transparent">
<div class="u-row" style="margin: 0 auto;min-width: 320px;max-width: 600px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: #f9f9f9;">
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:600px;"><tr style="background-color: #f9f9f9;"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="600" style="width: 600px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><![endif]-->
<div class="u-col u-col-100" style="max-width: 320px;min-width: 600px;display: table-cell;vertical-align: top;">
<div style="height: 100%;width: 100% !important;">
<!--[if (!mso)&(!IE)]><!--><div style="box-sizing: border-box; height: 100%; padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;">
<!--<![endif]-->
<table style="font-family:'Lato',sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
<tbody>
<tr>
<td style="overflow-wrap:break-word;word-break:break-word;padding:20px 40px 30px 20px;font-family:'Lato',sans-serif;" align="left">
<div style="font-size: 14px; line-height: 140%; text-align: left; word-wrap: break-word;">
<small style="color: #a5a3a3;"> You're receiving this email because you have a MarcoPMS account. This email is not a marketing or promotional email. That is why this email does not contain an unsubscribe link. You will receive this email even if you have unsubscribed from MarcoPMS's marketing emails</small>
</div>
</td>
</tr>
</tbody>
</table>
<!--[if (!mso)&(!IE)]><!-->
</div><!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><![endif]-->
<!--[if (mso)|(IE)]></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<!--[if mso]></div><![endif]-->
<!--[if IE]></div><![endif]-->
</body>
</html>

View File

@ -126,7 +126,6 @@ builder.Services.AddMemoryCache();
//builder.Services.AddScoped<IProjectAllocationRepository, ProjectAllocationRepository>();
builder.Services.AddScoped<RefreshTokenService>();
builder.Services.AddScoped<PermissionServices>();
builder.Services.AddScoped<UserHelper>();
builder.Services.AddScoped<RolesHelper>();

View File

@ -38,17 +38,6 @@ namespace MarcoBMS.Services.Service
return content;
}
//public async Task<string> GetEmailTemplate1()
//{
// string path = Path.Combine(_env.ContentRootPath, "EmailTemplates", $"send-otp.html");
// if (!File.Exists(path))
// throw new FileNotFoundException("Template file not found");
// string content = await File.ReadAllTextAsync(path);
// return content;
//}
public async Task SendResetPasswordEmailOnRegister(string toEmail, string toName, string resetLink)
{
@ -85,6 +74,7 @@ namespace MarcoBMS.Services.Service
await SendEmailAsync(toEmails, "Reset Your Password", emailBody);
}
public async Task SendResetPasswordSuccessEmail(string toEmail, string toName)
{
var replacements = new Dictionary<string, string>
@ -121,7 +111,8 @@ namespace MarcoBMS.Services.Service
await SendEmailAsync(toEmails, "User Requested a Demo", emailBody);
}
public async Task<string> SendProjectStatisticsEmail(List<string> toEmails, string emailBody, string subject, ProjectStatisticReport report)
public async Task<string> SendProjectStatisticsEmail(List<string> toEmails, string emailBody, ProjectStatisticReport report)
{
var date = report.Date.ToString("dd-MMM-yyyy", CultureInfo.InvariantCulture);
var replacements = new Dictionary<string, string>
@ -150,68 +141,11 @@ namespace MarcoBMS.Services.Service
emailBody = emailBody.Replace("{{TEAM_ON_SITE}}", BuildTeamOnSiteHtml(report.TeamOnSite));
emailBody = emailBody.Replace("{{PERFORMED_TASK}}", BuildPerformedTaskHtml(report.PerformedTasks, report.Date));
emailBody = emailBody.Replace("{{PERFORMED_ATTENDANCE}}", BuildPerformedAttendanceHtml(report.PerformedAttendance));
var subjectReplacements = new Dictionary<string, string>
{
{"DATE", date },
{"PROJECT_NAME", report.ProjectName}
};
foreach (var item in subjectReplacements)
{
subject = subject.Replace($"{{{{{item.Key}}}}}", item.Value);
}
string env = _configuration["environment:Title"] ?? string.Empty;
subject = CheckSubject(subject);
string subject = $"DPR - {date} - {report.ProjectName}";
await SendEmailAsync(toEmails, subject, emailBody);
return emailBody;
}
public async Task SendOTP(List<string> toEmails, string emailBody, string name, string otp, string subject)
{
var replacements = new Dictionary<string, string>
{
{ "NAME", name },
{ "OTP", otp }
};
foreach (var item in replacements)
{
emailBody = emailBody.Replace($"{{{{{item.Key}}}}}", item.Value);
}
subject = CheckSubject(subject);
await SendEmailAsync(toEmails, subject, emailBody);
}
public async Task SendEmailAsync(List<string> toEmails, string subject, string body)
{
var email = new MimeMessage();
email.From.Add(new MailboxAddress(_smtpSettings.SenderName, _smtpSettings.SenderEmail));
foreach (var toEmail in toEmails)
{
email.To.Add(MailboxAddress.Parse(toEmail));
}
email.Subject = subject;
var bodyBuilder = new BodyBuilder { HtmlBody = body };
email.Body = bodyBuilder.ToMessageBody();
using var smtp = new SmtpClient();
await smtp.ConnectAsync(_smtpSettings.SmtpServer, _smtpSettings.Port, MailKit.Security.SecureSocketOptions.StartTls);
await smtp.AuthenticateAsync(_smtpSettings.SenderEmail, _smtpSettings.Password);
await smtp.SendAsync(email);
await smtp.DisconnectAsync(true);
}
private string CheckSubject(string subject)
{
string env = _configuration["Environment:Title"] ?? string.Empty;
if (string.IsNullOrWhiteSpace(env))
{
return subject = $"{subject}";
}
else
{
return subject = $"({env}) {subject}";
}
}
private string BuildTeamOnSiteHtml(List<TeamOnSite> team)
{
if (team == null || !team.Any()) return "";
@ -300,6 +234,26 @@ namespace MarcoBMS.Services.Service
return sb.ToString();
}
public async Task SendEmailAsync(List<string> toEmails, string subject, string body)
{
var email = new MimeMessage();
email.From.Add(new MailboxAddress(_smtpSettings.SenderName, _smtpSettings.SenderEmail));
foreach (var toEmail in toEmails)
{
email.To.Add(MailboxAddress.Parse(toEmail));
}
email.Subject = subject;
var bodyBuilder = new BodyBuilder { HtmlBody = body };
email.Body = bodyBuilder.ToMessageBody();
using var smtp = new SmtpClient();
await smtp.ConnectAsync(_smtpSettings.SmtpServer, _smtpSettings.Port, MailKit.Security.SecureSocketOptions.StartTls);
await smtp.AuthenticateAsync(_smtpSettings.SenderEmail, _smtpSettings.Password);
await smtp.SendAsync(email);
await smtp.DisconnectAsync(true);
}
}
}

View File

@ -5,13 +5,11 @@ namespace MarcoBMS.Services.Service
{
public interface IEmailSender
{
//Task<string> GetEmailTemplate1();
Task SendResetPasswordEmail(string toEmail, string userName, string resetLink);
Task SendResetPasswordEmailOnRegister(string toEmail, string toName, string resetLink);
Task SendResetPasswordSuccessEmail(string toEmail, string userName);
Task SendRequestDemoEmail(List<string> toEmails, InquiryEmailObject demoEmailObject);
Task SendEmailAsync(List<string> toEmails, string subject, string body);
Task SendOTP(List<string> toEmails, string emailBody, string name, string otp, string subject);
Task<string> SendProjectStatisticsEmail(List<string> toEmails, string emailBody, string subject, ProjectStatisticReport report);
Task<string> SendProjectStatisticsEmail(List<string> toEmails, string emailBody, ProjectStatisticReport report);
}
}

View File

@ -1,52 +0,0 @@
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Projects;
using MarcoBMS.Services.Helpers;
using Microsoft.EntityFrameworkCore;
namespace Marco.Pms.Services.Service
{
public class PermissionServices
{
private readonly ApplicationDbContext _context;
private readonly RolesHelper _rolesHelper;
private readonly ProjectsHelper _projectsHelper;
public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, ProjectsHelper projectsHelper)
{
_context = context;
_rolesHelper = rolesHelper;
_projectsHelper = projectsHelper;
}
public async Task<bool> HasPermission(Guid featurePermissionId, Guid employeeId)
{
var hasPermission = await _context.EmployeeRoleMappings
.Where(er => er.EmployeeId == employeeId)
.Select(er => er.RoleId)
.Distinct()
.AnyAsync(roleId => _context.RolePermissionMappings
.Any(rp => rp.FeaturePermissionId == featurePermissionId && rp.ApplicationRoleId == roleId));
return hasPermission;
}
public async Task<bool> HasProjectPermission(Employee emp, string projectId)
{
List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(emp.Id);
string[] projectsId = [];
/* User with permission manage project can see all projects */
if (featurePermission != null && featurePermission.Exists(c => c.Id.ToString() == "172fc9b6-755b-4f62-ab26-55c34a330614"))
{
List<Project> projects = await _projectsHelper.GetAllProjectByTanentID(emp.TenantId);
projectsId = projects.Select(c => c.Id.ToString()).ToArray();
}
else
{
List<ProjectAllocation> allocation = await _projectsHelper.GetProjectByEmployeeID(emp.Id);
projectsId = allocation.Select(c => c.ProjectId.ToString()).ToArray();
}
bool response = projectsId.Contains(projectId);
return response;
}
}
}

View File

@ -48,37 +48,36 @@ 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 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 tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_jwtSettings.Key);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddDays(jwtSettings.RefreshTokenExpiresInDays),
Issuer = jwtSettings.Issuer,
Audience = jwtSettings.Audience,
SigningCredentials = credentials
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)
};
var tokenHandler = new JwtSecurityTokenHandler();
var refreshTokenString = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));
var token = tokenHandler.CreateToken(tokenDescriptor);
string strToken = tokenHandler.WriteToken(token);
var refreshToken = new RefreshToken
{
Token = refreshTokenString,
Token = strToken,
UserId = userId,
ExpiryDate = DateTime.UtcNow.AddDays(jwtSettings.RefreshTokenExpiresInDays),
ExpiryDate = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpiresInDays),
IsRevoked = false
};
@ -90,7 +89,7 @@ namespace MarcoBMS.Services.Service
_context.RefreshTokens.Add(refreshToken);
}
await _context.SaveChangesAsync();
return refreshTokenString;
return strToken;
}
catch (Exception ex)
{
@ -98,44 +97,6 @@ namespace MarcoBMS.Services.Service
throw;
}
}
public string CreateMPINToken(string userId, string tenantId, JwtSettings jwtSettings)
{
try
{
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));
return MPINToken;
}
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)
{
@ -193,35 +154,5 @@ namespace MarcoBMS.Services.Service
return jwtToken?.ValidTo;
}
public ClaimsPrincipal ValidateToken(string token, JwtSettings jwtSettings)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = System.Text.Encoding.ASCII.GetBytes(jwtSettings.Key);
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidIssuer = jwtSettings.Issuer,
ValidateAudience = true,
ValidAudience = jwtSettings.Audience,
ValidateLifetime = false, // Disable lifetime validation (ignores expiration)
ClockSkew = TimeSpan.Zero // Optional: Remove time skew buffer
};
try
{
var principal = tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
return principal;
}
catch (Exception ex)
{
// Token is invalid
Console.WriteLine($"Token validation failed: {ex.Message}");
return null;
}
}
}
}

View File

@ -4,10 +4,7 @@
"AllowedMethods": "*",
"AllowedHeaders": "*"
},
"Environment": {
"Name": "Development",
"Title": "Dev"
},
"ConnectionStrings": {
"DefaultConnectionString": "Server=147.93.98.152;User ID=devuser;Password=AppUser@123$;Database=MarcoBMSGuid"
},
@ -37,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": {

View File

@ -4,10 +4,6 @@
"AllowedMethods": "*",
"AllowedHeaders": "*"
},
"Environment": {
"Name": "Production",
"Title": ""
},
"ConnectionStrings": {
"DefaultConnectionString": "Server=147.93.98.152;User ID=devuser;Password=AppUser@123$;Database=MarcoBMS1"
},