Compare commits

...

219 Commits

Author SHA1 Message Date
b61caa9ee7 Fixed rebase confilets 2025-06-09 20:22:36 +05:30
6df8148f40 Fixed rebase 2025-06-09 20:17:33 +05:30
Pramod Mahajan
7dfed25d8e created new DTO for projectsallocation for one specific employee 2025-06-09 20:15:15 +05:30
Pramod Mahajan
9ed615110d created two end pointe for assigne and unassign project for perticular employee 2025-06-09 20:15:15 +05:30
Pramod Mahajan
4f0515f8f4 created new dto for projectAllocation for employee 2025-06-09 20:15:15 +05:30
361a2ab5c3 Sending Is Active in Object Of Contact notes 2025-06-09 20:14:26 +05:30
0db199e74f fixed the bug of only sending ID of logged in employee in Bucket object 2025-06-09 20:14:26 +05:30
a6fc75f492 Stopping multiple entries in employee-bucket mapping table 2025-06-09 20:14:26 +05:30
af9e06cd98 Sending last updatedBy and updatedAt when sending list of notes 2025-06-09 20:14:26 +05:30
7dc80ec006 Included object consist of basic infromation of employee who created the bucket in View models 2025-06-09 20:14:26 +05:30
4abeb8cf5a Sending number of conacts within the bucket with it information in API Update bucket, Assign Bucket, and Get Bucket List 2025-06-09 20:14:26 +05:30
b88dbea6c6 Corrected Spelling mistakes 2025-06-09 20:14:25 +05:30
138eb963d0 Implemented an API to delete a Bucket 2025-06-09 20:14:25 +05:30
ae08ebeae5 Implement API to Assign Bucket to Employees 2025-06-09 20:14:20 +05:30
49988c9814 Enhancement #381: Update "Update Bucket" API to Enforce Feature 2025-06-09 20:14:00 +05:30
915ad7bdb5 Enhancement #380: Update "Create Bucket" API to Enforce Feature 2025-06-09 20:14:00 +05:30
aad79953f5 Enhancement #378: Update "Get Bucket List" API to Enforce Feature 2025-06-09 20:14:00 +05:30
404c16946b Enhancement #377: Update "Update Contact" API to Enforce Feature 2025-06-09 20:14:00 +05:30
ac837ef241 Enhancement #376: Update "Get Contact by Bucket ID" API to Enforce Feature Permissions 2025-06-09 20:13:59 +05:30
3f74646437 Enhancement #375: Update "Get Contact" API to Enforce Feature Permissions 2025-06-09 20:13:54 +05:30
6e2b0eaec0 Added an API to suspend a n existing Contact-note 2025-06-09 20:07:19 +05:30
1e0e277bbf Added an API to Update existing Contact-note 2025-06-09 20:07:19 +05:30
9a19440622 Revert "added an API to get list of contacts by bucket id and added project- contact mapping table"
This reverts commit 22f777ca87053d2c79db610a27b8d7a5169e57bc.
2025-06-09 20:01:30 +05:30
85adf50418 added an API to get list of contacts by bucket id and added project- contact mapping table 2025-06-09 19:57:20 +05:30
8ca07410b0 Added an API to suspend a n existing Contact-note 2025-06-09 19:37:04 +05:30
84767d41b8 Added an API to Update existing Contact-note 2025-06-09 19:37:04 +05:30
ec63cde59b Added functionality to stop recreating tas of same name 2025-06-09 19:35:30 +05:30
166d556b6c added an API to get list of contacts by bucket id and added project- contact mapping table 2025-06-09 19:33:11 +05:30
17398dafa6 Revert "added an API to get list of contacts by bucket id and added project- contact mapping table"
This reverts commit 22f777ca87053d2c79db610a27b8d7a5169e57bc.
2025-06-09 19:31:31 +05:30
45cdbd91c9 added an API to get list of contacts by bucket id and added project- contact mapping table 2025-06-09 19:30:13 +05:30
Pramod Mahajan
ddc6f9e393 added api to get list of contact tag 2025-06-09 19:26:03 +05:30
d294bcef44 Created an endpoint to fetch list of all contact category in that tenant 2025-06-09 19:24:31 +05:30
9ceba92447 Add Directory Management Feature with Permission Handling 2025-06-09 19:08:21 +05:30
69d27c7471 Implemented an API to update Contact Category Master 2025-06-09 19:08:21 +05:30
30cd9d3a57 Implemented an API to update Buckets for grouping contacts. 2025-06-09 19:08:21 +05:30
0427b41961 Accepting List of buckets and categories Ids rather than as payload 2025-06-09 19:08:21 +05:30
e407c96bda Implemented filtering functionality for Get Contact List API 2025-06-09 19:08:17 +05:30
8f1c1489e5 Update Marco.Pms.Services/Helpers/DirectoryHelper.cs
Added entrie to DirectoryUpdateLog Table
2025-06-09 19:07:47 +05:30
5464ee6200 Implemented an API to suspend a Contact 2025-06-09 19:07:47 +05:30
Pramod Mahajan
66c167f027 created api for contact Tag Update 2025-06-09 19:07:44 +05:30
aaacd6be91 Implemented an API to retrieve a list of organizations provided in the contacts. 2025-06-09 19:07:22 +05:30
f93caa7994 Created an API to get contact profile by its Id 2025-06-09 19:07:19 +05:30
7fd3c7b0b3 Changed tag validation 2025-06-09 19:07:01 +05:30
928886ac72 Corrected the typo of ContactTagtId to ContactTagId 2025-06-09 19:06:57 +05:30
eb096685d5 Added an API to suspend a n existing Contact-note 2025-06-09 19:05:05 +05:30
d62725cb38 Added an API to Update existing Contact-note 2025-06-09 19:05:05 +05:30
64aaf325bd Added an API to get contact category by its size 2025-06-09 19:05:05 +05:30
60acd104c1 Added functionality to stop recreating tas of same name 2025-06-09 19:05:05 +05:30
b3ceecaf9e properly mapped the updated Dto to contact table 2025-06-09 19:05:05 +05:30
1e70f2bffc added an API to get list of contacts by bucket id and added project- contact mapping table 2025-06-09 19:05:05 +05:30
eb897af87f Revert "added an API to get list of contacts by bucket id and added project- contact mapping table"
This reverts commit 22f777ca87053d2c79db610a27b8d7a5169e57bc.
2025-06-09 19:05:04 +05:30
93997c16a2 added an API to get list of contacts by bucket id and added project- contact mapping table 2025-06-09 19:05:04 +05:30
88233d9435 When checking in exsiting tag change Id from mapping Id to Tag ID 2025-06-09 19:05:04 +05:30
3b09a5f625 Added an API to create bucket 2025-06-09 19:05:04 +05:30
de8ef72cae Added an API to create a contact tag 2025-06-09 19:05:04 +05:30
9921683fa6 Added an API to Get a list of buckets Assigned to that employee 2025-06-09 19:05:04 +05:30
Pramod Mahajan
a5c472b52e added api to get list of contact tag 2025-06-09 19:05:04 +05:30
54c66daa18 Added an API to update existing contact 2025-06-09 19:05:04 +05:30
7c1171fbb0 Added logs to the 'Get List of Contacts' endpoint. 2025-06-09 19:04:25 +05:30
a0bf548e64 Added an API to create contact and populate related tables as well 2025-06-09 19:04:25 +05:30
Pramod Mahajan
94305f34ed created GetListOfContact custome function 2025-06-09 19:04:24 +05:30
25c91a64ad Models, DTOs (Data Transfer Objects), and view models have been created for the directory. 2025-06-09 19:04:24 +05:30
a7f41af44f Added an API to add a note to specific contact 2025-06-09 19:04:24 +05:30
c9073652c8 Added an API to suspend a n existing Contact-note 2025-06-09 19:04:24 +05:30
2650bdb17a Added an API to Update existing Contact-note 2025-06-09 19:04:24 +05:30
f7543a37a1 added an API to get a list of contact-notes by contact ID 2025-06-09 19:03:55 +05:30
42b5478b52 Added functionality to stop recreating tas of same name 2025-06-09 19:03:12 +05:30
eac2c31ea4 properly mapped the updated Dto to contact table 2025-06-09 19:03:11 +05:30
a00ef4313a Refetched the contact in update contact API 2025-06-09 19:03:11 +05:30
20b1f45915 added an API to get list of contacts by bucket id and added project- contact mapping table 2025-06-09 19:03:07 +05:30
3c14df4d53 Added an API to delete existing contact category 2025-06-09 19:01:53 +05:30
5f2626db88 Revert "added an API to get list of contacts by bucket id and added project- contact mapping table"
This reverts commit 22f777ca87053d2c79db610a27b8d7a5169e57bc.
2025-06-09 19:01:53 +05:30
9da45240c3 added an API to get list of contacts by bucket id and added project- contact mapping table 2025-06-09 19:01:50 +05:30
81d3e016c2 When checking in exsiting tag change Id from mapping Id to Tag ID 2025-06-09 19:01:32 +05:30
bea8793fba Added an API to create bucket 2025-06-09 19:01:32 +05:30
9c25aa1aab Added an API to create a contact tag 2025-06-09 19:01:27 +05:30
9e813c1e51 Added an API to Get a list of buckets Assigned to that employee 2025-06-09 19:00:59 +05:30
Pramod Mahajan
6e0cdbcfe3 added api to get list of contact tag 2025-06-09 19:00:34 +05:30
76671bc5dc Added an API to update existing contact 2025-06-09 19:00:07 +05:30
4f250ab46e Created an endpoint to fetch list of all contact category in that tenant 2025-06-09 19:00:07 +05:30
bab5243108 Created an API to create the Contact category 2025-06-09 19:00:02 +05:30
47d7cbba4f Added logs to the 'Get List of Contacts' endpoint. 2025-06-09 18:59:32 +05:30
3d9e22c308 Added an API to create contact and populate related tables as well 2025-06-09 18:59:32 +05:30
f2699898ed Added an API to create contact and populate related tables as well 2025-06-09 18:59:32 +05:30
Pramod Mahajan
9d67a25488 created GetListOfContact custome function 2025-06-09 18:59:27 +05:30
58b0a3fbaa Added DirectoryHelper in helper folder 2025-06-09 18:58:45 +05:30
b5ce7eb73f Added Migration for contact related tables 2025-06-09 18:57:17 +05:30
a23c8c3ff0 An API skeleton has been added. 2025-06-09 18:53:03 +05:30
52e9cb7de9 Added Directory controller file 2025-06-09 18:53:00 +05:30
8da02932e4 Models, DTOs (Data Transfer Objects), and view models have been created for the directory. 2025-06-09 18:52:22 +05:30
ccef0ba192 Models, DTOs (Data Transfer Objects), and view models have been created for the directory. 2025-06-09 18:50:19 +05:30
621b96a805 fixed the bug of only sending ID of logged in employee in Bucket object 2025-06-09 18:47:24 +05:30
f0bf5dca83 Stopping multiple entries in employee-bucket mapping table 2025-06-09 18:47:24 +05:30
d60608e544 Sending last updatedBy and updatedAt when sending list of notes 2025-06-09 18:47:24 +05:30
a48d4d308f Included object consist of basic infromation of employee who created the bucket in View models 2025-06-09 18:47:24 +05:30
197b4aea8d Sending number of conacts within the bucket with it information in API Update bucket, Assign Bucket, and Get Bucket List 2025-06-09 18:47:24 +05:30
453a64fea0 Corrected Spelling mistakes 2025-06-09 18:47:24 +05:30
a755e77397 Implemented an API to delete a Bucket 2025-06-09 18:47:24 +05:30
049189024a Implement API to Assign Bucket to Employees 2025-06-09 18:47:24 +05:30
c1cc8d5d34 Enhancement #381: Update "Update Bucket" API to Enforce Feature 2025-06-09 18:47:24 +05:30
f7d90b85e8 Enhancement #380: Update "Create Bucket" API to Enforce Feature 2025-06-09 18:47:24 +05:30
f621dbf27c Enhancement #378: Update "Get Bucket List" API to Enforce Feature 2025-06-09 18:47:24 +05:30
4650e0cbbb Enhancement #377: Update "Update Contact" API to Enforce Feature 2025-06-09 18:47:24 +05:30
fb2648ba17 Enhancement #376: Update "Get Contact by Bucket ID" API to Enforce Feature Permissions 2025-06-09 18:47:24 +05:30
54ea82b984 Enhancement #375: Update "Get Contact" API to Enforce Feature Permissions 2025-06-09 18:47:24 +05:30
3128372a78 Add Directory Management Feature with Permission Handling 2025-06-09 18:47:23 +05:30
bad2386fa4 Created an API to get contact profile by its Id 2025-06-09 18:47:23 +05:30
6a1fa9a4f8 Added an API to suspend a n existing Contact-note 2025-06-09 18:47:23 +05:30
605fd9c78d Added an API to Update existing Contact-note 2025-06-09 18:47:23 +05:30
5fde9c5e53 Added functionality to stop recreating tas of same name 2025-06-09 18:47:23 +05:30
7d704b138b properly mapped the updated Dto to contact table 2025-06-09 18:47:23 +05:30
3841dca11e added an API to get list of contacts by bucket id and added project- contact mapping table 2025-06-09 18:47:23 +05:30
5e184c770d Revert "added an API to get list of contacts by bucket id and added project- contact mapping table"
This reverts commit 22f777ca87053d2c79db610a27b8d7a5169e57bc.
2025-06-09 18:47:23 +05:30
0795eda5cc added an API to get list of contacts by bucket id and added project- contact mapping table 2025-06-09 18:47:23 +05:30
e175e58450 Added an API to Get a list of buckets Assigned to that employee 2025-06-09 18:47:23 +05:30
80da3199b9 Created an endpoint to fetch list of all contact category in that tenant 2025-06-09 18:47:23 +05:30
Pramod Mahajan
eea2b7a8b3 created GetListOfContact custome function 2025-06-09 18:47:23 +05:30
7dca88153f Added an API to suspend a n existing Contact-note 2025-06-09 18:47:23 +05:30
220b87d5c3 Added an API to Update existing Contact-note 2025-06-09 18:47:23 +05:30
ed9a2467e9 added an API to get a list of contact-notes by contact ID 2025-06-09 18:47:23 +05:30
ff13507f8f Added functionality to stop recreating tas of same name 2025-06-09 18:47:23 +05:30
0fb3690133 properly mapped the updated Dto to contact table 2025-06-09 18:47:23 +05:30
7a2ce067ab added an API to get list of contacts by bucket id and added project- contact mapping table 2025-06-09 18:47:22 +05:30
115e38b58f Revert "added an API to get list of contacts by bucket id and added project- contact mapping table"
This reverts commit 22f777ca87053d2c79db610a27b8d7a5169e57bc.
2025-06-09 18:47:22 +05:30
578b2e0d67 added an API to get list of contacts by bucket id and added project- contact mapping table 2025-06-09 18:47:22 +05:30
Pramod Mahajan
eb3bd425f6 added api to get list of contact tag 2025-06-09 18:47:22 +05:30
81f91c3b6d Created an endpoint to fetch list of all contact category in that tenant 2025-06-09 18:47:22 +05:30
9e124d0ae5 Added an API to create contact and populate related tables as well 2025-06-09 18:47:22 +05:30
d3a800fbf9 Added Migration for contact related tables 2025-06-09 18:47:22 +05:30
c51da8d1b4 Models, DTOs (Data Transfer Objects), and view models have been created for the directory. 2025-06-09 18:47:22 +05:30
10322fa432 Add Directory Management Feature with Permission Handling 2025-06-09 18:47:22 +05:30
2a0adc75c4 Implemented an API to update Contact Category Master 2025-06-09 18:47:21 +05:30
38463ccf07 Implemented an API to update Buckets for grouping contacts. 2025-06-09 18:47:21 +05:30
6dbd968676 Accepting List of buckets and categories Ids rather than as payload 2025-06-09 18:47:21 +05:30
707b948e15 Implemented filtering functionality for Get Contact List API 2025-06-09 18:47:21 +05:30
2456a9a039 Update Marco.Pms.Services/Helpers/DirectoryHelper.cs
Added entrie to DirectoryUpdateLog Table
2025-06-09 18:47:21 +05:30
d1995b820c Implemented an API to suspend a Contact 2025-06-09 18:47:21 +05:30
Pramod Mahajan
8c915d7a1d created api for contact Tag Update 2025-06-09 18:47:21 +05:30
b15022774f Implemented an API to retrieve a list of organizations provided in the contacts. 2025-06-09 18:47:21 +05:30
3633d2ce4b changed all list in contact profile view model to non-nullable and set default value to empty list 2025-06-09 18:47:21 +05:30
9922255514 Created an API to get contact profile by its Id 2025-06-09 18:47:21 +05:30
ad659c4fc9 Changed tag validation 2025-06-09 18:47:21 +05:30
d136e8b95e Addd migrations fro Typo 2025-06-09 18:47:21 +05:30
9fd01c7903 Corrected the typo of ContactTagtId to ContactTagId 2025-06-09 18:47:20 +05:30
872bcdd236 Added an API to suspend a n existing Contact-note 2025-06-09 18:47:20 +05:30
8209f06410 Added an API to Update existing Contact-note 2025-06-09 18:47:20 +05:30
a4aae7ebbe Added an API to get contact category by its size 2025-06-09 18:47:20 +05:30
9a5be28196 Added functionality to stop recreating tas of same name 2025-06-09 18:47:20 +05:30
1d6f8825c6 properly mapped the updated Dto to contact table 2025-06-09 18:47:20 +05:30
6f902178a4 added an API to get list of contacts by bucket id and added project- contact mapping table 2025-06-09 18:47:20 +05:30
282a21adc2 Added an API to delete existing contact category 2025-06-09 18:47:20 +05:30
f994ee14a2 Revert "added an API to get list of contacts by bucket id and added project- contact mapping table"
This reverts commit 22f777ca87053d2c79db610a27b8d7a5169e57bc.
2025-06-09 18:47:20 +05:30
4eeeec5dbe added an API to get list of contacts by bucket id and added project- contact mapping table 2025-06-09 18:47:20 +05:30
56e444f87f When checking in exsiting tag change Id from mapping Id to Tag ID 2025-06-09 18:47:20 +05:30
7016e38bc4 Added an API to create bucket 2025-06-09 18:47:20 +05:30
0917b6665f Added an API to create a contact tag 2025-06-09 18:47:20 +05:30
1f49057cb6 Added an API to Get a list of buckets Assigned to that employee 2025-06-09 18:47:20 +05:30
Pramod Mahajan
5a9c08fd6c added api to get list of contact tag 2025-06-09 18:47:20 +05:30
cc0ac31746 Added an API to update existing contact 2025-06-09 18:47:19 +05:30
5d71f3f2db Created an endpoint to fetch list of all contact category in that tenant 2025-06-09 18:47:19 +05:30
2985cba79b Added logs to the 'Get List of Contacts' endpoint. 2025-06-09 18:47:19 +05:30
81992f6abd Added an API to create contact and populate related tables as well 2025-06-09 18:47:19 +05:30
Pramod Mahajan
1f2cc2b2e6 created GetListOfContact custome function 2025-06-09 18:47:19 +05:30
542701f342 Models, DTOs (Data Transfer Objects), and view models have been created for the directory. 2025-06-09 18:47:19 +05:30
33e765558c Added an API to add a note to specific contact 2025-06-09 18:47:19 +05:30
ec1325dc03 Added an API to suspend a n existing Contact-note 2025-06-09 18:47:19 +05:30
fa3a7ab7f2 Added an API to Update existing Contact-note 2025-06-09 18:47:19 +05:30
10625101e1 Added an API to get contact category by its size 2025-06-09 18:47:19 +05:30
dc36cb57f4 added an API to get a list of contact-notes by contact ID 2025-06-09 18:47:19 +05:30
f820bedba3 Added an API to deleted ContactTag as well remove entries in contact-tag mapping table related to that tag 2025-06-09 18:47:19 +05:30
15c665236b Added functionality to stop recreating tas of same name 2025-06-09 18:47:19 +05:30
20eaccef04 properly mapped the updated Dto to contact table 2025-06-09 18:47:19 +05:30
3376812538 Refetched the contact in update contact API 2025-06-09 18:47:19 +05:30
e427781ad9 added an API to get list of contacts by bucket id and added project- contact mapping table 2025-06-09 18:47:19 +05:30
fe0d5e8458 Added an API to delete existing contact category 2025-06-09 18:47:19 +05:30
e89036359d Revert "added an API to get list of contacts by bucket id and added project- contact mapping table"
This reverts commit 22f777ca87053d2c79db610a27b8d7a5169e57bc.
2025-06-09 18:47:19 +05:30
999f9168c2 added an API to get list of contacts by bucket id and added project- contact mapping table 2025-06-09 18:47:19 +05:30
0a59f87b84 When checking in exsiting tag change Id from mapping Id to Tag ID 2025-06-09 18:47:18 +05:30
49bbd87a3d Added an API to create bucket 2025-06-09 18:47:18 +05:30
d52956abd9 Fixed the errorof finding tag in wrong table 2025-06-09 18:47:18 +05:30
e049b6c996 Added an API to create a contact tag 2025-06-09 18:47:18 +05:30
532e0ff16d Added an API to Get a list of buckets Assigned to that employee 2025-06-09 18:47:18 +05:30
Pramod Mahajan
967488e9c1 added api to get list of contact tag 2025-06-09 18:47:18 +05:30
ecc8c6d801 Added an API to update existing contact 2025-06-09 18:47:18 +05:30
3043a3f7ed Created an endpoint to fetch list of all contact category in that tenant 2025-06-09 18:47:18 +05:30
bb0c2acc87 Created an API to create the Contact category 2025-06-09 18:47:18 +05:30
7dffdc4c25 Added logs to the 'Get List of Contacts' endpoint. 2025-06-09 18:47:18 +05:30
d5e150d768 Added an API to create contact and populate related tables as well 2025-06-09 18:47:18 +05:30
f26d5d0021 Added an API to create contact and populate related tables as well 2025-06-09 18:47:18 +05:30
Pramod Mahajan
278a55ebe3 created GetListOfContact custome function 2025-06-09 18:47:18 +05:30
38babea9d5 Added DirectoryHelper in helper folder 2025-06-09 18:47:18 +05:30
73df47e540 Added Migration for contact related tables 2025-06-09 18:47:18 +05:30
5932a21fcc An API skeleton has been added. 2025-06-09 18:47:18 +05:30
58c73383b2 Added Directory controller file 2025-06-09 18:47:18 +05:30
004bb94d99 Models, DTOs (Data Transfer Objects), and view models have been created for the directory. 2025-06-09 18:47:18 +05:30
2c20d49609 revert c7e89630eb494454c1322bdf4cf29ab076af7b86
revert Models, DTOs (Data Transfer Objects), and view models have been created for the directory.
2025-06-09 18:47:17 +05:30
2002a79360 Models, DTOs (Data Transfer Objects), and view models have been created for the directory. 2025-06-09 18:47:17 +05:30
e760d51987 fixed typo error 2025-06-07 15:35:31 +05:30
d235bc8211 Changed varable name in VerifyOTPDto 2025-06-07 15:28:37 +05:30
9e9bb6fecd changed endpoint name of api of logging in through OTP 2025-06-07 09:53:08 +00:00
9f37c37e18 Merge pull request 'Implement API to log in through MPIN authentication.' (#87) from Ashutosh_Task#484_Login_MPIN into Issue_Jun_1W_2
Reviewed-on: #87
2025-06-07 08:01:04 +00:00
775c17531b Implement API to log in through MPIN authentication. 2025-06-07 13:20:12 +05:30
2fc44ec499 Merge pull request 'Implement API to log in through OTP verification' (#86) from Ashutosh_Task#483_Login_OTP into Issue_Jun_1W_2
Reviewed-on: #86
2025-06-07 06:32:22 +00:00
0a8c5cf587 Implement API to log in through OTP verification 2025-06-07 11:47:08 +05:30
baa168ff8f Sending list of projects of which employee has permission 2025-06-06 18:40:09 +05:30
47ad6231dd Merge pull request 'Implement API to Send OTP for Email-Based Login' (#85) from Ashutosh_Task#480_Send_OTP into Issue_Jun_1W_2
Reviewed-on: #85
2025-06-06 12:13:45 +00:00
14b0d1bfc7 Updated response sent from login-mobile API 2025-06-06 12:10:33 +00:00
5eb100c1f6 Implement API to Send OTP for Email-Based Login 2025-06-06 16:27:58 +05:30
863a154ec6 Merge pull request 'Implement an API to Generate MPIN' (#84) from Ashutosh_Task#471_Create_MPIN into Issue_Jun_1W_2
Reviewed-on: #84
2025-06-06 07:07:38 +00:00
34577c41f7 Implement an API to Generate MPIN 2025-06-06 12:17:17 +05:30
1cb7a9fea8 Merge pull request 'pramod_Task#465 : Implemented Change Password API for authenticated users' (#82) from pramod_Task#465 into Issue_Jun_1W_2
Reviewed-on: #82
2025-06-05 11:46:20 +00:00
7ef2c720cb Merge pull request 'Added tables for MPIN and OTP as well created an Login API for Mobile Application' (#83) from Ashutosh_Task#469_Mobile_Login into Issue_Jun_1W_2
Reviewed-on: #83
2025-06-05 11:46:08 +00:00
a5cf2025a1 Added tables for MPIN and OTP as well created an Login API for Mobile Application 2025-06-05 17:01:53 +05:30
Pramod Mahajan
5b08b617cf Add Change Password API for logged-in users,
- Validates old password and updates to new one using UserManager
- Secured the endpoint for authenticated users only
2025-06-05 16:33:57 +05:30
5f9784faa8 Removed Hard coded Raja removed for project report email template 2025-06-05 15:05:56 +05:30
18acfef5a0 Added Authentication folder 2025-06-05 12:59:47 +05:30
92fd335eaf Merge pull request 'Added a new feature permission for self-attendance and enforced this permission in the attendance module.' (#81) from Ashutosh_Enhancement#456_Self_Attendance into Issue_Jun_1W
Reviewed-on: #81
2025-06-04 11:32:05 +00:00
0467a825ad Added a new feature permission for self-attendance and enforced this permission in the attendance module. 2025-06-04 16:55:41 +05:30
357d615d1b Changed the project report email template 2025-06-04 15:11:04 +05:30
41 changed files with 12751 additions and 269 deletions

View File

@ -78,12 +78,14 @@ 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<DirectoryUpdateLog> DirectoryUpdateLogs { get; set; }
public DbSet<ContactProjectMapping> ContactProjectMappings { get; set; }
public DbSet<DirectoryUpdateLog> DirectoryUpdateLogs { 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; }
@ -509,8 +511,9 @@ 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 = "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("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("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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_IsUsed_FLag_In_OTPDetails_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsUsed",
table: "OTPDetails",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsUsed",
table: "OTPDetails");
}
}
}

View File

@ -229,6 +229,68 @@ 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")
@ -941,10 +1003,10 @@ namespace Marco.Pms.DataAccess.Migrations
new
{
Id = new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"),
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.",
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 = "Perform Attendance "
Name = "Team Attendance "
},
new
{
@ -955,6 +1017,14 @@ 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",
@ -1260,10 +1330,6 @@ namespace Marco.Pms.DataAccess.Migrations
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
@ -1319,6 +1385,10 @@ namespace Marco.Pms.DataAccess.Migrations
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
@ -2476,6 +2546,28 @@ namespace Marco.Pms.DataAccess.Migrations
b.Navigation("UpdatedByEmployee");
});
modelBuilder.Entity("Marco.Pms.Model.Authentication.MPINDetails", b =>
{
b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Tenant");
});
modelBuilder.Entity("Marco.Pms.Model.Authentication.OTPDetails", b =>
{
b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Tenant");
});
modelBuilder.Entity("Marco.Pms.Model.Authentication.RefreshToken", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")

View File

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

View File

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

@ -0,0 +1,17 @@
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,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace Marco.Pms.Model.Dtos
namespace Marco.Pms.Model.Dtos.Authentication
{
public class ForgotPasswordDto
{

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -4,7 +4,6 @@
{
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,6 +4,7 @@
{
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

@ -8,7 +8,6 @@ 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,6 +5,7 @@
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,11 +28,12 @@ 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)
ApplicationDbContext context, EmployeeHelper employeeHelper, ProjectsHelper projectsHelper, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permission)
{
_context = context;
_employeeHelper = employeeHelper;
@ -40,6 +41,7 @@ namespace MarcoBMS.Services.Controllers
_userHelper = userHelper;
_s3Service = s3Service;
_logger = logger;
_permission = permission;
}
private Guid GetTenantId()
@ -132,6 +134,17 @@ 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();
@ -159,42 +172,68 @@ namespace MarcoBMS.Services.Controllers
if (dateFrom == null) fromDate = DateTime.UtcNow.Date;
if (dateTo == null && dateFrom != null) toDate = fromDate.AddDays(-1);
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)
if (hasTeamAttendancePermission)
{
var result1 = new EmployeeAttendanceVM()
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)
{
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)
var result1 = new EmployeeAttendanceVM()
{
result1.FirstName = teamMember.Employee.FirstName;
result1.LastName = teamMember.Employee.LastName;
result1.JobRoleName = teamMember.Employee.JobRole != null ? teamMember.Employee.JobRole.Name : null;
}
else
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 = null;
result1.LastName = null;
result1.JobRoleName = null;
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);
}
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));
@ -211,6 +250,17 @@ 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)
@ -229,49 +279,72 @@ namespace MarcoBMS.Services.Controllers
Attendance? attendance = null;
if (date == null) forDate = DateTime.UtcNow.Date;
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 (hasTeamAttendancePermission)
{
if (teamMember.Employee != null && teamMember.Employee.JobRole != null)
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)
{
var result1 = new EmployeeAttendanceVM()
if (teamMember.Employee != null && teamMember.Employee.JobRole != null)
{
EmployeeAvatar = null,
EmployeeId = teamMember.EmployeeId,
FirstName = teamMember.Employee.FirstName,
LastName = teamMember.Employee.LastName,
JobRoleName = teamMember.Employee.JobRole.Name,
};
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);
//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;
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
};
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));
@ -282,7 +355,15 @@ 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,13 +1,17 @@
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;
using Marco.Pms.Model.Dtos.Authentication;
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;
@ -19,15 +23,17 @@ 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)
IEmailSender emailSender, IConfiguration configuration, EmployeeHelper employeeHelper, UserHelper userHelper, ILoggingService logger)
{
_userManager = userManager;
_jwtSettings = jwtSettings;
@ -36,146 +42,417 @@ namespace MarcoBMS.Services.Controllers
_configuration = configuration;
_employeeHelper = employeeHelper;
_context = context;
_userHelper = userHelper;
_logger = logger;
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginDto loginDto)
{
var user = await _context.ApplicationUsers.FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.PhoneNumber == loginDto.Username);
if (user != null)
try
{
// 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)
{
return BadRequest(ApiResponse<object>.ErrorResponse("User is In Active", "User is In Active", 400));
_logger.LogWarning("Login failed: Inactive user attempted login - UserId: {UserId}", user.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("User is inactive", "User is inactive", 400));
}
// Ensure the user's email is confirmed
if (!user.EmailConfirmed)
{
return BadRequest(ApiResponse<object>.ErrorResponse("Your email is not verified, Please verify your email", "Your email is not verified, Please verify your email", 400));
_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));
}
if (await _userManager.CheckPasswordAsync(user, loginDto.Password ?? string.Empty))
// Validate the password
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));
_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));
}
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid username or password.", "Invalid username or password.", 401));
// 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));
}
}
[HttpPost("logout")]
public async Task<IActionResult> Logout([FromBody] LogoutDto logoutDto)
{
if (string.IsNullOrEmpty(logoutDto.RefreshToken))
if (string.IsNullOrWhiteSpace(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 access token (JWT)
// Optional: Blacklist the JWT access token
string jwtToken = HttpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
if (!string.IsNullOrEmpty(jwtToken))
if (!string.IsNullOrWhiteSpace(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(ex, "Error during logout");
return BadRequest(ApiResponse<object>.ErrorResponse("Internal server error", ex.Message, 500));
_logger.LogError("Unexpected error during logout : {Error}", ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error occurred", ex.Message, 500));
}
}
[HttpPost("refresh-token")]
public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenDto refreshTokenDto)
{
var refreshToken = await _refreshTokenService.GetRefreshToken(refreshTokenDto.RefreshToken);
if (refreshToken == null || refreshToken.ExpiryDate < DateTime.UtcNow)
if (string.IsNullOrWhiteSpace(refreshTokenDto.RefreshToken))
{
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid or expired refresh token.", "Invalid or expired refresh token.", 401));
_logger.LogWarning("Refresh token is missing from the request body.");
return BadRequest(ApiResponse<object>.ErrorResponse("Refresh token is required.", "Missing refresh token.", 400));
}
// Mark token as used
await _refreshTokenService.MarkRefreshTokenAsUsed(refreshToken);
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));
}
// 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));
if (refreshToken.ExpiryDate < DateTime.UtcNow)
{
_logger.LogWarning("Refresh token expired");
return Unauthorized(ApiResponse<object>.ErrorResponse("Refresh token expired.", "Token expired.", 401));
}
Employee emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id);
// Step 2: Mark the token as used
await _refreshTokenService.MarkRefreshTokenAsUsed(refreshToken);
_logger.LogInfo("Refresh token marked as used");
if (user.UserName == null) return NotFound(ApiResponse<object>.ErrorResponse("UserName Not found", "UserName Not found", 404));
// 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));
}
var newJwtToken = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId, _jwtSettings);
var newRefreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings);
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));
}
return Ok(ApiResponse<object>.SuccessResponse(new { token = newJwtToken, refreshToken = newRefreshToken }, "User refresh token generated successfully.", 200));
// 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));
}
}
[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)
return NotFound(ApiResponse<object>.ErrorResponse("User not found.", "User not found.", 404));
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));
}
/* SEND USER REGISTRATION MAIL*/
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}";
try
{
// Generate token and build reset link
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}";
if (user.Email == null) return NotFound(ApiResponse<object>.ErrorResponse("Email Not found", "Email Not found", 404));
// Send reset email
await _emailSender.SendResetPasswordEmail(user.Email, user.UserName ?? "User", resetLink);
await _emailSender.SendResetPasswordEmail(user.Email, "", resetLink);
return Ok(ApiResponse<object>.SuccessResponse(true, "Password reset link sent.", 200));
_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));
}
}
[HttpPost("reset-password")]
public async Task<IActionResult> ResetPassword([FromBody] ResetPasswordDto model)
{
var user = await _userManager.FindByEmailAsync(model.Email ?? string.Empty);
if (user == null)
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request.", "Invalid request.", 400));
_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);
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));
}
// 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, // This is the token provider
TokenOptions.DefaultProvider,
UserManager<ApplicationUser>.ResetPasswordTokenPurpose,
model.Token
model.Token
);
if (!isDecodedTokenValid)
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request.", "Invalid request.", 400));
{
_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));
}
token = model.Token;
}
@ -184,29 +461,187 @@ namespace MarcoBMS.Services.Controllers
token = WebUtility.UrlDecode(model.Token);
}
var result = await _userManager.ResetPasswordAsync(user, token, model.NewPassword ?? string.Empty);
var result = await _userManager.ResetPasswordAsync(user, token, model.NewPassword);
if (!result.Succeeded)
{
var errors = result.Errors.Select(e => e.Description).ToList();
return BadRequest(ApiResponse<object>.ErrorResponse("Failed to Change password", errors, 400));
_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));
}
try
{
Employee emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id);
await _emailSender.SendResetPasswordSuccessEmail(user.Email ?? string.Empty, emp.FirstName + " " + emp.LastName);
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);
}
catch (Exception ex)
{
return BadRequest(ApiResponse<object>.ErrorResponse(ex.Message, ex.Message, 400));
_logger.LogError("Error while sending reset password success email to user: {Error}", ex.Message);
// Continue, do not fail because of email issue
}
return Ok(ApiResponse<object>.SuccessResponse(result.Succeeded, "Password reset successfully.", 200));
_logger.LogInfo("Password reset successful for user: {Email}", model.Email);
return Ok(ApiResponse<object>.SuccessResponse(true, "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)
@ -242,5 +677,150 @@ 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,13 +25,17 @@ 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)
public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper)
{
_context = context;
_userHelper = userHelper;
_logger = logger;
_rolesHelper = rolesHelper;
_projectsHelper = projectHelper;
}
[HttpGet("list")]
public async Task<IActionResult> GetAll()
@ -46,7 +50,22 @@ namespace MarcoBMS.Services.Controllers
}
Guid tenantId = _userHelper.GetTenantId();
List<Project> projects = await _context.Projects.Where(c => c.TenantId == tenantId).ToListAsync();
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<ProjectListVM> response = new List<ProjectListVM>();

View File

@ -45,7 +45,6 @@ namespace Marco.Pms.Services.Controllers
Recipient = mailDetailsDto.Recipient,
Schedule = mailDetailsDto.Schedule,
MailListId = mailDetailsDto.MailListId,
Subject = mailDetailsDto.Subject,
TenantId = tenantId
};
_context.MailDetails.Add(mailDetails);
@ -57,10 +56,22 @@ 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
};
@ -92,7 +103,8 @@ 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 ?? ""
MailBody = g.FirstOrDefault()?.MailBody?.Body ?? "",
Subject = g.FirstOrDefault()?.MailBody?.Subject ?? string.Empty,
})
.ToList();
@ -104,7 +116,7 @@ namespace Marco.Pms.Services.Controllers
await semaphore.WaitAsync();
try
{
var response = await GetProjectStatistics(mailDetail.ProjectId, mailDetail.Recipients, mailDetail.MailBody, tenantId);
var response = await GetProjectStatistics(mailDetail.ProjectId, mailDetail.Recipients, mailDetail.MailBody, mailDetail.Subject, tenantId);
if (response.StatusCode == 200)
Interlocked.Increment(ref successCount);
else if (response.StatusCode == 404)
@ -137,7 +149,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, Guid tenantId)
private async Task<ApiResponse<object>> GetProjectStatistics(Guid projectId, List<string> recipientEmails, string body, string subject, Guid tenantId)
{
DateTime reportDate = DateTime.UtcNow.AddDays(-1).Date;
@ -303,7 +315,7 @@ namespace Marco.Pms.Services.Controllers
statisticReport.PerformedAttendance = performedAttendance;
// Send Email
var emailBody = await _emailSender.SendProjectStatisticsEmail(recipientEmails, body, statisticReport);
var emailBody = await _emailSender.SendProjectStatisticsEmail(recipientEmails, body, subject, 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">
<div class="u-row-container" style="padding: 0px; background-color: #f9f9f9; page-break-before: 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: #f9f9f9;">
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
@ -338,7 +338,6 @@
<!--<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>
@ -363,7 +362,7 @@
<!--<div>Mail Body</div>-->
<div class="u-row-container" style="padding: 0px;background-color: transparent">
<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;">
@ -384,9 +383,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: center; word-wrap: break-word;">
<span style="font-size: 18px; line-height: 25.2px; color: #666666;">
Project Status Reported - Generated at {{TIMESTAMP}}
<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
</span>
<table cellpadding="1" cellspacing="0" width="100%"
border="0" style="margin-top: 15px">
@ -394,7 +393,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 Attendane
Todays Attendance
</div>
<div style="font-size: 25px; color: #bc3803;margin:20px 20px 0px!important; font-weight:bold; ">
{{TODAYS_ATTENDANCES}} /
@ -411,7 +410,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;">
@ -478,13 +477,47 @@
</td>
</tr>
<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;">
<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 Today
Available On Site {{DATE}}
</span>
<table cellpadding="1" cellspacing="0" width="100%"
border="0">
@ -493,63 +526,137 @@
</div>
</td>
</tr>
<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;">
<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 Today
(Tasks) Performed {{DATE}}
</span> <br />
<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>
<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>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<tr>
<!--[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;">
<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 Today
Performed {{DATE}}
</span> <br />
<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>
<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>
</div>
</td>
</tr>
@ -572,7 +679,7 @@
<!--<div>Contact</div>-->
<div class="u-row-container" style="padding: 0px;background-color: transparent">
<div class="u-row-container" style="padding: 0px; background-color: transparent; page-break-before: 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: #e93f32;">
<div style="border-collapse: collapse;display: table;width: 100%;height: 100%;background-color: transparent;">
@ -725,18 +832,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>
@ -785,14 +892,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

@ -0,0 +1,538 @@

<!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,6 +126,7 @@ 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,6 +38,17 @@ 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)
{
@ -74,7 +85,6 @@ 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>
@ -111,8 +121,7 @@ namespace MarcoBMS.Services.Service
await SendEmailAsync(toEmails, "User Requested a Demo", emailBody);
}
public async Task<string> SendProjectStatisticsEmail(List<string> toEmails, string emailBody, ProjectStatisticReport report)
public async Task<string> SendProjectStatisticsEmail(List<string> toEmails, string emailBody, string subject, ProjectStatisticReport report)
{
var date = report.Date.ToString("dd-MMM-yyyy", CultureInfo.InvariantCulture);
var replacements = new Dictionary<string, string>
@ -141,11 +150,68 @@ 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));
string subject = $"DPR - {date} - {report.ProjectName}";
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);
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 "";
@ -234,26 +300,6 @@ 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,11 +5,13 @@ 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<string> SendProjectStatisticsEmail(List<string> toEmails, string emailBody, ProjectStatisticReport report);
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);
}
}

View File

@ -0,0 +1,52 @@
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,36 +48,37 @@ namespace MarcoBMS.Services.Service
return new JwtSecurityTokenHandler().WriteToken(token);
}
public async Task<string> CreateRefreshToken(string userId, string tenantId, JwtSettings _jwtSettings)
public async Task<string> CreateRefreshToken(string userId, string tenantId, JwtSettings jwtSettings)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_jwtSettings.Key);
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, userId),
new Claim("TenantId", tenantId),
new Claim("token_type", "refresh")
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, userId),
new Claim("TenantId", tenantId), // Add TenantId claim
new Claim("token_type", "refresh") // Custom claim to differentiate refresh tokens
}),
Expires = DateTime.UtcNow.AddDays(7), // Refresh token valid for 7 days
Issuer = _jwtSettings.Issuer,
Audience = _jwtSettings.Audience,
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddDays(jwtSettings.RefreshTokenExpiresInDays),
Issuer = jwtSettings.Issuer,
Audience = jwtSettings.Audience,
SigningCredentials = credentials
};
var token = tokenHandler.CreateToken(tokenDescriptor);
string strToken = tokenHandler.WriteToken(token);
var tokenHandler = new JwtSecurityTokenHandler();
var refreshTokenString = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));
var refreshToken = new RefreshToken
{
Token = strToken,
Token = refreshTokenString,
UserId = userId,
ExpiryDate = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpiresInDays),
ExpiryDate = DateTime.UtcNow.AddDays(jwtSettings.RefreshTokenExpiresInDays),
IsRevoked = false
};
@ -89,7 +90,7 @@ namespace MarcoBMS.Services.Service
_context.RefreshTokens.Add(refreshToken);
}
await _context.SaveChangesAsync();
return strToken;
return refreshTokenString;
}
catch (Exception ex)
{
@ -97,6 +98,44 @@ 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)
{
@ -154,5 +193,35 @@ 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,7 +4,10 @@
"AllowedMethods": "*",
"AllowedHeaders": "*"
},
"Environment": {
"Name": "Development",
"Title": "Dev"
},
"ConnectionStrings": {
"DefaultConnectionString": "Server=147.93.98.152;User ID=devuser;Password=AppUser@123$;Database=MarcoBMSGuid"
},
@ -34,7 +37,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,6 +4,10 @@
"AllowedMethods": "*",
"AllowedHeaders": "*"
},
"Environment": {
"Name": "Production",
"Title": ""
},
"ConnectionStrings": {
"DefaultConnectionString": "Server=147.93.98.152;User ID=devuser;Password=AppUser@123$;Database=MarcoBMS1"
},