Skip to content

Commit 95b64a5

Browse files
Merge pull request #2 from microting/master
Updates from main
2 parents f5c2447 + ea54000 commit 95b64a5

File tree

16 files changed

+407
-243
lines changed

16 files changed

+407
-243
lines changed

INTEGRATION_STEPS.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Angular Material Extensions Password Strength Integration
2+
3+
## ✅ Integration Complete
4+
5+
The password strength meter has been successfully integrated with the following configuration:
6+
7+
### Configuration Applied
8+
- `enableLengthRule`: true
9+
- `enableLowerCaseLetterRule`: true
10+
- `enableUpperCaseLetterRule`: true
11+
- `enableDigitRule`: true
12+
- `enableSpecialCharRule`: false *(disabled per requirements)*
13+
- `min`: 8 *(minimum password length)*
14+
- `max`: 50
15+
16+
### Components Updated
17+
1.**User Set Password Modal** (`user-set-password.component.*`) - Admin password setting
18+
2.**Change Password** (`change-password.component.*`) - User profile password change
19+
3.**Restore Password Confirmation** (`restore-password-confirmation.component.*`) - Password reset flow
20+
21+
### Implementation Details
22+
- ✅ Package installed: `@angular-material-extensions/[email protected]`
23+
- ✅ Module imports activated in `account-management.module.ts` and `auth.module.ts`
24+
- ✅ HTML templates updated with password strength meters
25+
- ✅ TypeScript methods implemented for strength tracking
26+
- ✅ Form validation updated to require minimum 8 characters
27+
- ✅ Special character requirements disabled as requested
28+
29+
### Features Implemented
30+
- **Real-time password strength visualization**: Color-coded strength indicators
31+
- **Configurable validation rules**: Length, lowercase, uppercase, digits (special chars disabled)
32+
- **Strength scoring**: 0-100 scale with event handling
33+
- **Form validation integration**: Weak password validation (< 40 strength)
34+
- **Material Design integration**: Seamless visual integration
35+
36+
## Testing Recommendations
37+
1. Test each password field for visual feedback
38+
2. Verify strength scoring works correctly (0-100 scale)
39+
3. Test form validation integration with weak passwords
40+
4. Ensure all password requirements are enforced except special characters
41+
42+
## Example Usage
43+
```html
44+
<mat-password-strength
45+
[password]="form.get('newPassword')?.value || ''"
46+
[enableLengthRule]="true"
47+
[enableLowerCaseLetterRule]="true"
48+
[enableUpperCaseLetterRule]="true"
49+
[enableDigitRule]="true"
50+
[enableSpecialCharRule]="false"
51+
[min]="8"
52+
[max]="50"
53+
(onStrengthChanged)="onPasswordStrengthChanged($event)">
54+
</mat-password-strength>
55+
```

eFormAPI/eFormAPI.Web/Services/AdminService.cs

Lines changed: 44 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -155,46 +155,13 @@ public async Task<OperationDataResult<Paged<UserInfoViewModel>>> Index(UserInfoR
155155

156156
foreach (UserInfoViewModel userInfoViewModel in userResult)
157157
{
158-
if (!userInfoViewModel.Email.Contains("microting.com") && !userInfoViewModel.Email.Contains("admin.com"))
158+
var workers = await sdkDbContext.Workers
159+
.Where(x => x.Email == userInfoViewModel.Email
160+
&& x.WorkflowState != Constants.WorkflowStates.Removed)
161+
.FirstOrDefaultAsync();
162+
if (workers != null)
159163
{
160164
userInfoViewModel.IsDeviceUser = true;
161-
162-
var fullName = userInfoViewModel.FirstName + " " + userInfoViewModel.LastName;
163-
if (sdkDbContext.Sites.Any(x =>
164-
x.Name.Replace(" ", "") == fullName.Replace(" ", "")
165-
&& x.WorkflowState != Constants.WorkflowStates.Removed))
166-
{
167-
}
168-
else
169-
{
170-
await core.SiteCreate($"{userInfoViewModel.FirstName} {userInfoViewModel.LastName}", userInfoViewModel.FirstName, userInfoViewModel.LastName,
171-
null, "da");
172-
}
173-
174-
var site = await sdkDbContext.Sites.FirstOrDefaultAsync(x =>
175-
x.Name.Replace(" ", "") == fullName.Replace(" ", "")
176-
&& x.WorkflowState != Constants.WorkflowStates.Removed);
177-
if (site != null)
178-
{
179-
site.IsLocked = true;
180-
await site.Update(sdkDbContext);
181-
var units = await sdkDbContext.Units.Where(x => x.SiteId == site.Id).ToListAsync();
182-
foreach (Unit unit in units)
183-
{
184-
unit.IsLocked = true;
185-
await unit.Update(sdkDbContext);
186-
}
187-
var siteWorker = await sdkDbContext.SiteWorkers.SingleOrDefaultAsync(x => x.SiteId == site.Id);
188-
var worker = await sdkDbContext.Workers.SingleOrDefaultAsync(x => x.Id == siteWorker.WorkerId);
189-
// var worker = await sdkDbContext.Workers.SingleOrDefaultAsync(x => x.FirstName == userInfoViewModel.FirstName
190-
// && x.LastName == userInfoViewModel.LastName
191-
// && x.WorkflowState != Constants.WorkflowStates.Removed);
192-
if (worker != null)
193-
{
194-
worker.IsLocked = true;
195-
await worker.Update(sdkDbContext);
196-
}
197-
}
198165
}
199166
}
200167

@@ -290,9 +257,20 @@ public async Task<OperationResult> Create(UserRegisterModel userRegisterModel)
290257
await dbContext.SaveChangesAsync();
291258
}
292259

293-
var site = await sdkDbContext.Sites.SingleOrDefaultAsync(x => x.Name == userRegisterModel.FirstName.Trim() + " " + userRegisterModel.LastName.Trim()
294-
&& x.WorkflowState != Constants.WorkflowStates.Removed);
260+
var worker = await sdkDbContext.Workers.FirstOrDefaultAsync(x => x.Email == user.Email && x.WorkflowState != Constants.WorkflowStates.Removed);
261+
if (worker != null)
262+
{
263+
worker.IsLocked = true;
264+
await worker.Update(sdkDbContext);
265+
}
266+
267+
var siteWorker = await sdkDbContext.SiteWorkers
268+
.Include(x => x.Site)
269+
.Where(x => x.WorkerId == worker.Id && x.WorkflowState != Constants.WorkflowStates.Removed)
270+
.FirstOrDefaultAsync();
295271

272+
var site = await sdkDbContext.Sites.SingleOrDefaultAsync(x => x.Id == siteWorker.SiteId && x.WorkflowState != Constants.WorkflowStates.Removed);
273+
// lock site and units
296274
if (site != null)
297275
{
298276
site.IsLocked = true;
@@ -303,13 +281,6 @@ public async Task<OperationResult> Create(UserRegisterModel userRegisterModel)
303281
unit.IsLocked = true;
304282
await unit.Update(sdkDbContext);
305283
}
306-
var siteWorker = await sdkDbContext.SiteWorkers.SingleOrDefaultAsync(x => x.SiteId == site.Id);
307-
var worker = await sdkDbContext.Workers.SingleOrDefaultAsync(x => x.Id == siteWorker.WorkerId);
308-
if (worker != null)
309-
{
310-
worker.IsLocked = true;
311-
await worker.Update(sdkDbContext);
312-
}
313284
}
314285

315286
return new OperationResult(true,
@@ -336,8 +307,8 @@ public async Task<OperationDataResult<UserRegisterModel>> Read(int userId)
336307
}
337308
var core = await coreHelper.GetCore();
338309
var sdkDbContext = core.DbContextHelper.GetDbContext();
339-
var site = await sdkDbContext.Sites.SingleOrDefaultAsync(x => x.Name == user.FirstName + " " + user.LastName
340-
&& x.WorkflowState != Constants.WorkflowStates.Removed);
310+
var worker = await sdkDbContext.Workers.AnyAsync(x => x.Email == user.Email
311+
&& x.WorkflowState != Constants.WorkflowStates.Removed);
341312

342313
var result = new UserRegisterModel()
343314
{
@@ -346,7 +317,7 @@ public async Task<OperationDataResult<UserRegisterModel>> Read(int userId)
346317
FirstName = user.FirstName,
347318
LastName = user.LastName,
348319
UserName = user.UserName,
349-
IsDeviceUser = site != null
320+
IsDeviceUser = worker
350321
};
351322
// get role
352323
var roles = await userManager.GetRolesAsync(user);
@@ -412,7 +383,12 @@ public async Task<OperationResult> Update(UserRegisterModel userRegisterModel)
412383
return new OperationResult(false, localizationService.GetString("YouCantViewChangeOrDeleteAdmin"));
413384
}
414385

415-
var site = await sdkDbContext.Sites.SingleOrDefaultAsync(x => x.Name == user.FirstName + " " + user.LastName
386+
var worker = await sdkDbContext.Workers.FirstOrDefaultAsync(x => x.Email == user.Email && x.WorkflowState != Constants.WorkflowStates.Removed);
387+
var siteWorker = await sdkDbContext.SiteWorkers
388+
.Where(x => x.WorkerId == worker.Id && x.WorkflowState != Constants.WorkflowStates.Removed)
389+
.FirstOrDefaultAsync();
390+
391+
var site = await sdkDbContext.Sites.SingleOrDefaultAsync(x => x.Id == siteWorker.SiteId
416392
&& x.WorkflowState != Constants.WorkflowStates.Removed);
417393
if (site != null)
418394
{
@@ -422,11 +398,19 @@ await core.SiteUpdate((int)site.MicrotingUid!,
422398
$"{userRegisterModel.FirstName} {userRegisterModel.LastName}", userRegisterModel.FirstName,
423399
userRegisterModel.LastName, userRegisterModel.Email, language.LanguageCode);
424400
}
425-
user.Email = userRegisterModel.Email;
426401
user.EmailConfirmed = true;
402+
user.Email = userRegisterModel.Email;
427403
user.UserName = userRegisterModel.Email;
428-
user.FirstName = userRegisterModel.FirstName;
429-
user.LastName = userRegisterModel.LastName;
404+
if (worker == null)
405+
{
406+
user.FirstName = userRegisterModel.FirstName;
407+
user.LastName = userRegisterModel.LastName;
408+
}
409+
else
410+
{
411+
worker.Email = userRegisterModel.Email;
412+
await worker.Update(sdkDbContext);
413+
}
430414

431415
var result = await userManager.UpdateAsync(user);
432416
if (!result.Succeeded)
@@ -515,34 +499,16 @@ public async Task<OperationResult> Delete(int userId)
515499
return new OperationResult(false, localizationService.GetString("YouCantViewChangeOrDeleteAdmin"));
516500
}
517501

518-
var site = await sdkDbContext.Sites.SingleOrDefaultAsync(x => x.Name == user.FirstName + " " + user.LastName
519-
&& x.WorkflowState != Constants.WorkflowStates.Removed);
520-
if (site != null)
502+
var worker = await sdkDbContext.Workers.FirstOrDefaultAsync(x => x.Email == user.Email && x.WorkflowState != Constants.WorkflowStates.Removed);
503+
if (worker != null)
521504
{
522-
site.IsLocked = false;
523-
await site.Update(sdkDbContext);
524-
var units = await sdkDbContext.Units.Where(x => x.SiteId == site.Id).ToListAsync();
525-
foreach (Unit unit in units)
526-
{
527-
unit.IsLocked = false;
528-
await unit.Update(sdkDbContext);
529-
}
530-
var siteWorker = await sdkDbContext.SiteWorkers.SingleOrDefaultAsync(x => x.SiteId == site.Id);
531-
var worker = await sdkDbContext.Workers.SingleOrDefaultAsync(x => x.Id == siteWorker.WorkerId);
532-
if (worker != null)
533-
{
534-
worker.IsLocked = false;
535-
await worker.Update(sdkDbContext);
536-
}
505+
return new OperationResult(false, localizationService.GetStringWithFormat("ErrorWhileDeletingUser", userId));
537506
}
538507

539508
var result = await userManager.DeleteAsync(user);
540-
if (!result.Succeeded)
541-
{
542-
return new OperationResult(false, string.Join(" ", result.Errors.Select(x => x.Description).ToArray()));
543-
}
544-
545-
return new OperationResult(true, localizationService.GetStringWithFormat("UserParamWasDeleted", userId));
509+
return !result.Succeeded
510+
? new OperationResult(false, string.Join(" ", result.Errors.Select(x => x.Description).ToArray()))
511+
: new OperationResult(true, localizationService.GetStringWithFormat("UserParamWasDeleted", userId));
546512
}
547513
catch (Exception e)
548514
{

eFormAPI/eFormAPI.Web/Startup.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
395395
app.UseEFormPlugins(Program.EnabledPlugins);
396396
// Route all unknown requests to app root
397397
app.UseAngularMiddleware(env);
398+
FixUserToWorkerLinks();
398399
}
399400

400401

@@ -471,4 +472,34 @@ private ICollection<PluginPermissionModel> GetPluginsPermissions()
471472

472473
return permissions;
473474
}
475+
476+
// TODO remove, when we are sure that all users have been fixed.
477+
// This method is only needed for a short period of time to fix the users that already exist.
478+
// It matches users to workers based on First and Last name, and updates the email on the worker.
479+
// It is not a perfect solution, but it is the best we can do without having a direct link between the two.
480+
// It is assumed that users have unique First and Last names.
481+
// If there are multiple workers with the same name, only the first one found will be updated.
482+
// If a worker already has an email, it will be overwritten.
483+
// After a while this method should be removed again.
484+
private void FixUserToWorkerLinks()
485+
{
486+
if (Configuration.MyConnectionString() == "...") return;
487+
var contextFactory = new BaseDbContextFactory();
488+
using var dbContext = contextFactory.CreateDbContext([Configuration.MyConnectionString()]);
489+
490+
var sdkDbContextFactory = new MicrotingDbContextFactory();
491+
using var sdkDbContext =
492+
sdkDbContextFactory.CreateDbContext([Configuration.MyConnectionString().Replace("Angular", "SDK")]);
493+
494+
var users = dbContext.Users.AsNoTracking().ToList();
495+
foreach (var user in users)
496+
{
497+
var fullName = (user.FirstName + user.LastName).Trim().ToLower().Replace(" ", "");
498+
var worker = sdkDbContext.Workers.FirstOrDefault(x =>
499+
(x.FirstName + x.LastName).Trim().ToLower().Replace(" ", "") == fullName && x.WorkflowState != "removed");
500+
if (worker == null) continue;
501+
worker.Email = user.Email;
502+
worker.Update(sdkDbContext).GetAwaiter().GetResult();
503+
}
504+
}
474505
}

eFormAPI/eFormAPI.Web/eFormAPI.Web.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,11 @@
5555
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.9" />
5656
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.9" />
5757
<PackageReference Include="Microsoft.Extensions.PlatformAbstractions" Version="1.1.0" />
58-
<PackageReference Include="Microting.eForm" Version="9.0.52" />
59-
<PackageReference Include="Microting.EformAngularFrontendBase" Version="9.0.44" />
60-
<PackageReference Include="Microting.eFormApi.BasePn" Version="9.0.49" />
58+
<PackageReference Include="Microting.eForm" Version="9.0.56" />
59+
<PackageReference Include="Microting.EformAngularFrontendBase" Version="9.0.46" />
60+
<PackageReference Include="Microting.eFormApi.BasePn" Version="9.0.51" />
6161
<PackageReference Include="PureOtp" Version="1.0.0.1" />
62-
<PackageReference Include="Sentry" Version="5.14.1" />
62+
<PackageReference Include="Sentry" Version="5.15.1" />
6363
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
6464
<PackageReference Include="McMaster.NETCore.Plugins" Version="2.0.0" />
6565
<PackageReference Include="sendgrid" Version="9.29.3" />

eform-client/package.json

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"@angular/platform-browser": "20.1.2",
7272
"@angular/platform-browser-dynamic": "20.1.2",
7373
"@angular/router": "20.1.2",
74+
"@angular-material-extensions/password-strength": "^16.0.0",
7475
"@ng-matero/extensions": "20.2.1",
7576
"@ngrx/effects": "19.2.1",
7677
"@ngrx/entity": "19.2.1",
@@ -80,10 +81,10 @@
8081
"@sentry/angular": "^10.8.0",
8182
"@sentry/cli": "^2.53.0",
8283
"@swimlane/ngx-charts": "23.0.0",
83-
"@types/ramda": "^0.31.0",
84+
"@types/ramda": "^0.31.1",
8485
"@userback/widget": "^0.3.11",
8586
"build": "^0.1.4",
86-
"core-js": "3.45.0",
87+
"core-js": "3.45.1",
8788
"d3": "7.9.0",
8889
"date-fns": "4.1.0",
8990
"date-fns-tz": "3.2.0",
@@ -110,20 +111,20 @@
110111
"run": "^1.4.0",
111112
"rxjs": "7.8.2",
112113
"tslib": "^2.8.1",
113-
"uuid": "11.1.0",
114+
"uuid": "13.0.0",
114115
"zone.js": "^0.15.1",
115116
"ngx-material-timepicker": "^13.1.1",
116-
"luxon": "^3.6.0",
117+
"luxon": "^3.7.2",
117118
"validator": "^13.15.15"
118119
},
119120
"devDependencies": {
120-
"@angular-devkit/build-angular": "20.2.2",
121+
"@angular-devkit/build-angular": "20.3.2",
121122
"@angular-devkit/core": "20.2.1",
122-
"@angular-devkit/schematics": "20.1.4",
123-
"@angular-eslint/builder": "20.1.1",
124-
"@angular-eslint/eslint-plugin": "20.1.1",
123+
"@angular-devkit/schematics": "20.3.3",
124+
"@angular-eslint/builder": "20.3.0",
125+
"@angular-eslint/eslint-plugin": "20.3.0",
125126
"@angular-eslint/eslint-plugin-template": "20.1.1",
126-
"@angular-eslint/schematics": "20.1.1",
127+
"@angular-eslint/schematics": "20.3.0",
127128
"@angular-eslint/template-parser": "20.1.1",
128129
"@angular/cli": "20.1.6",
129130
"@angular/compiler-cli": "20.1.2",
@@ -133,14 +134,14 @@
133134
"@types/dragula": "^2.1.34",
134135
"@types/file-saver": "^2.0.7",
135136
"@types/mocha": "^10.0.10",
136-
"@types/node": "^24.3.1",
137+
"@types/node": "^24.5.2",
137138
"@types/uuid": "^10.0.0",
138139
"@typescript-eslint/eslint-plugin": "^7.18.0",
139140
"@typescript-eslint/parser": "^7.18.0",
140141
"@typescript-eslint/utils": "^8.39.1",
141142
"@wdio/cli": "9.19.2",
142143
"@wdio/local-runner": "9.19.2",
143-
"@wdio/mocha-framework": "9.18.0",
144+
"@wdio/mocha-framework": "9.19.2",
144145
"@wdio/spec-reporter": "9.19.2",
145146
"angular-mocks": "^1.8.3",
146147
"chai": "^6.0.1",

eform-client/src/app/modules/account-management/account-management.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {MatDialogModule} from '@angular/material/dialog';
2323
import {MatInputModule} from '@angular/material/input';
2424
import {MatIconModule} from '@angular/material/icon';
2525
import {FileUploadModule} from "ng2-file-upload";
26+
import {MatPasswordStrengthModule} from '@angular-material-extensions/password-strength';
2627

2728
@NgModule({
2829
imports: [
@@ -44,6 +45,7 @@ import {FileUploadModule} from "ng2-file-upload";
4445
MatInputModule,
4546
MatIconModule,
4647
FileUploadModule,
48+
MatPasswordStrengthModule,
4749
],
4850
declarations: [
4951
ChangePasswordComponent,

0 commit comments

Comments
 (0)