@c8y/login 1023.53.0 → 1023.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@c8y/login",
3
- "version": "1023.53.0",
3
+ "version": "1023.57.0",
4
4
  "description": "This package is used to scaffold a login application for Cumulocity IoT.",
5
5
  "dependencies": {
6
- "@c8y/style": "1023.53.0",
7
- "@c8y/ngx-components": "1023.53.0",
8
- "@c8y/client": "1023.53.0",
9
- "@c8y/bootstrap": "1023.53.0",
6
+ "@c8y/style": "1023.57.0",
7
+ "@c8y/ngx-components": "1023.57.0",
8
+ "@c8y/client": "1023.57.0",
9
+ "@c8y/bootstrap": "1023.57.0",
10
10
  "@angular/cdk": "^20.2.14",
11
11
  "monaco-editor": "~0.53.0",
12
12
  "ngx-bootstrap": "20.0.2",
13
13
  "rxjs": "7.8.2"
14
14
  },
15
15
  "devDependencies": {
16
- "@c8y/options": "1023.53.0",
17
- "@c8y/devkit": "1023.53.0"
16
+ "@c8y/options": "1023.57.0",
17
+ "@c8y/devkit": "1023.57.0"
18
18
  },
19
19
  "peerDependencies": {
20
20
  "@angular/common": ">=20 <21"
@@ -1,34 +1,49 @@
1
- <form class="loginForm" (ngSubmit)="changePassword()" #changePasswordForm="ngForm" novalidate>
2
- <div class="legend form-block center" translate>Change password</div>
1
+ <form
2
+ class="loginForm"
3
+ (ngSubmit)="changePassword()"
4
+ #changePasswordForm="ngForm"
5
+ novalidate
6
+ >
7
+ <div
8
+ class="legend form-block center"
9
+ translate
10
+ >
11
+ Change password
12
+ </div>
3
13
 
4
- <c8y-form-group class="tenantField" id="tenantField" *ngIf="loginService.showTenant()">
5
- <label translate>Tenant ID</label>
6
- <input
7
- [(ngModel)]="model.tenantId"
8
- #tenantId="ngModel"
9
- type="text"
10
- name="tenantId"
11
- autocapitalize="off"
12
- autocorrect="off"
13
- class="form-control"
14
- placeholder="{{ 'Tenant ID' | translate }}"
15
- required
16
- />
17
- </c8y-form-group>
14
+ @if (loginService.showTenant()) {
15
+ <c8y-form-group
16
+ class="tenantField"
17
+ id="tenantField"
18
+ >
19
+ <label translate>Tenant ID</label>
20
+ <input
21
+ class="form-control"
22
+ placeholder="{{ 'Tenant ID' | translate }}"
23
+ name="tenantId"
24
+ type="text"
25
+ required
26
+ [(ngModel)]="model.tenantId"
27
+ #tenantId="ngModel"
28
+ autocapitalize="off"
29
+ autocorrect="off"
30
+ />
31
+ </c8y-form-group>
32
+ }
18
33
 
19
34
  <c8y-form-group>
20
35
  <label translate>Email address</label>
21
36
  <input
37
+ class="form-control"
38
+ placeholder="{{ 'Email address' | translate }}"
39
+ name="email"
40
+ type="text"
41
+ required
22
42
  [(ngModel)]="model.email"
23
43
  #email="ngModel"
24
- type="text"
25
- name="email"
26
44
  autocapitalize="off"
27
45
  autocorrect="off"
28
- class="form-control"
29
- placeholder="{{ 'Email address' | translate }}"
30
46
  email
31
- required
32
47
  [readonly]="emailReadOnly"
33
48
  />
34
49
  </c8y-form-group>
@@ -38,21 +53,23 @@
38
53
  <c8y-form-group>
39
54
  <label translate>New password</label>
40
55
  <input
41
- [(ngModel)]="model.newPassword"
42
- #newPassword="ngModel"
43
- type="password"
44
- name="newPassword"
45
56
  class="form-control"
46
57
  placeholder="{{ 'New password' | translate }}"
47
- [pattern]="passwordPattern"
58
+ name="newPassword"
59
+ type="password"
48
60
  autocomplete="new-password"
49
- [passwordStrengthEnforced]="passwordStrengthEnforced"
50
61
  required
62
+ [(ngModel)]="model.newPassword"
63
+ #newPassword="ngModel"
64
+ c8yPasswordValidation
65
+ [passwordStrengthEnforced]="passwordStrengthEnforced"
66
+ [minLength]="effectiveMinLength"
67
+ (input)="newPasswordConfirm.control.updateValueAndValidity()"
51
68
  />
52
69
  <c8y-messages>
53
70
  <c8y-message
54
- name="pattern"
55
- [text]="loginService.ERROR_MESSAGES.pattern_newPassword"
71
+ name="passwordStrengthChecklist"
72
+ [text]="'Password is not strong enough, check the requirements below.' | translate"
56
73
  ></c8y-message>
57
74
  </c8y-messages>
58
75
  </c8y-form-group>
@@ -60,15 +77,15 @@
60
77
  <c8y-form-group>
61
78
  <label translate>Confirm password</label>
62
79
  <input
63
- [(ngModel)]="model.newPasswordConfirm"
64
- #newPasswordConfirm="ngModel"
65
- type="password"
66
- name="newPasswordConfirm"
67
80
  class="form-control"
68
81
  placeholder="{{ 'Confirm password' | translate }}"
69
- passwordConfirm="newPassword"
82
+ name="newPasswordConfirm"
83
+ type="password"
70
84
  autocomplete="new-password"
71
85
  required
86
+ [(ngModel)]="model.newPasswordConfirm"
87
+ #newPasswordConfirm="ngModel"
88
+ passwordConfirm="newPassword"
72
89
  />
73
90
  <c8y-messages>
74
91
  <c8y-message
@@ -82,15 +99,16 @@
82
99
  <c8y-password-check-list
83
100
  [password]="model.newPassword"
84
101
  [strengthEnforced]="passwordStrengthEnforced"
102
+ (onRequirementsFulfilled)="updateValidity($event)"
85
103
  ></c8y-password-check-list>
86
104
  </div>
87
105
  </div>
88
106
 
89
107
  <button
108
+ class="btn btn-primary btn-lg btn-block form-group"
90
109
  title="{{ 'Set password' | translate }}"
91
- [disabled]="!changePasswordForm.form.valid || isLoading"
92
110
  type="submit"
93
- class="btn btn-primary btn-lg btn-block form-group"
111
+ [disabled]="!changePasswordForm.form.valid || isLoading"
94
112
  >
95
113
  {{ 'Set password' | translate }}
96
114
  </button>
@@ -1,23 +1,23 @@
1
- import { Component, OnInit, Output, Input, EventEmitter } from '@angular/core';
2
- import { LoginService } from '../login.service';
3
- import { IResetPassword, ICredentials, UserService, PasswordStrength } from '@c8y/client';
4
- import { LoginEvent, LoginViews } from '../login.model';
5
- import { FormsModule } from '@angular/forms';
6
- import { NgIf } from '@angular/common';
7
- import { PasswordStrengthValidatorDirective } from '../password-strength-validator.directive';
1
+ import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
2
+ import { FormsModule, NgModel, ValidatorFn } from '@angular/forms';
3
+ import { ICredentials, IResetPassword, PasswordStrength, UserService } from '@c8y/client';
8
4
  import {
5
+ AlertService,
6
+ C8yTranslateDirective,
9
7
  C8yTranslatePipe,
10
- PasswordCheckListComponent,
11
- PasswordConfirm,
8
+ FormGroupComponent,
12
9
  MessageDirective,
13
10
  MessagesComponent,
14
- RequiredInputPlaceholderDirective,
15
- FormGroupComponent,
16
- C8yTranslateDirective,
17
- AlertService,
18
- OptionsService
11
+ OptionsService,
12
+ PasswordCheckListComponent,
13
+ PasswordConfirm,
14
+ PasswordStrengthService,
15
+ PasswordValidationDirective,
16
+ PasswordValidationService,
17
+ RequiredInputPlaceholderDirective
19
18
  } from '@c8y/ngx-components';
20
- import { PasswordStrengthService } from '@c8y/ngx-components';
19
+ import { LoginEvent, LoginViews } from '../login.model';
20
+ import { LoginService } from '../login.service';
21
21
 
22
22
  @Component({
23
23
  selector: 'c8y-change-password',
@@ -27,10 +27,9 @@ import { PasswordStrengthService } from '@c8y/ngx-components';
27
27
  imports: [
28
28
  FormsModule,
29
29
  C8yTranslateDirective,
30
- NgIf,
31
30
  FormGroupComponent,
32
31
  RequiredInputPlaceholderDirective,
33
- PasswordStrengthValidatorDirective,
32
+ PasswordValidationDirective,
34
33
  MessagesComponent,
35
34
  MessageDirective,
36
35
  PasswordConfirm,
@@ -42,8 +41,8 @@ export class ChangePasswordComponent implements OnInit {
42
41
  @Input() credentials: ICredentials;
43
42
  @Output() onChangeView = new EventEmitter<LoginEvent>();
44
43
 
45
- passwordPattern = /^[a-zA-Z0-9`~!@#$%^&*()_|+\-=?;:'",.<>{}[\]\\/]{8,32}$/;
46
44
  isLoading = false;
45
+ requirementsFulfilled = false;
47
46
  model = {
48
47
  tenantId: '',
49
48
  email: '',
@@ -53,22 +52,54 @@ export class ChangePasswordComponent implements OnInit {
53
52
  emailReadOnly = false;
54
53
  passwordStrengthEnforced = false;
55
54
 
55
+ private readonly DEFAULT_MIN_LENGTH = 8;
56
+ private minLength: number;
56
57
  private TOKEN_PARAM = 'token';
57
58
  private EMAIL_PARAM = 'email';
58
59
 
60
+ get effectiveMinLength(): number {
61
+ return this.passwordStrengthEnforced
62
+ ? this.minLength || this.DEFAULT_MIN_LENGTH
63
+ : this.DEFAULT_MIN_LENGTH;
64
+ }
65
+
66
+ newPasswordModel: NgModel;
67
+
68
+ @ViewChild('newPassword')
69
+ set _newPasswordModel(ngModel: NgModel) {
70
+ if (ngModel) {
71
+ this.newPasswordModel = ngModel;
72
+ ngModel.control.addValidators(this.passwordChecklistValidator);
73
+ }
74
+ }
75
+
59
76
  constructor(
60
77
  public loginService: LoginService,
61
78
  private passwordStrength: PasswordStrengthService,
62
79
  private users: UserService,
63
80
  private options: OptionsService,
64
- private alert: AlertService
81
+ private alert: AlertService,
82
+ private passwordValidation: PasswordValidationService
65
83
  ) {}
66
84
 
85
+ // Keep form invalid when strength is enforced and checklist requirements aren't met.
86
+ passwordChecklistValidator: ValidatorFn = control =>
87
+ !this.passwordStrengthEnforced || this.requirementsFulfilled || !control.value
88
+ ? null
89
+ : { passwordStrengthChecklist: true };
90
+
67
91
  async ngOnInit() {
68
92
  this.model.tenantId = this.loginService.getTenant();
69
93
  this.model.email = this.options.get(this.EMAIL_PARAM, '');
70
94
  this.emailReadOnly = !!this.model.email;
71
- this.passwordStrengthEnforced = await this.passwordStrength.getPasswordStrengthEnforced();
95
+
96
+ const [passwordStrengthEnforced, greenMinLength] = await Promise.all([
97
+ this.passwordStrength.getPasswordStrengthEnforced(),
98
+ this.passwordStrength.getGreenMinLength()
99
+ ]);
100
+
101
+ this.passwordStrengthEnforced = passwordStrengthEnforced;
102
+ this.minLength = greenMinLength;
72
103
  }
73
104
 
74
105
  async changePassword() {
@@ -98,4 +129,35 @@ export class ChangePasswordComponent implements OnInit {
98
129
  this.isLoading = false;
99
130
  }
100
131
  }
132
+
133
+ updateValidity(requirementsFulfilled: boolean) {
134
+ this.requirementsFulfilled = requirementsFulfilled;
135
+
136
+ if (!this.newPasswordModel) {
137
+ return;
138
+ }
139
+
140
+ this.newPasswordModel.control.updateValueAndValidity();
141
+
142
+ const errors = this.newPasswordModel.control.errors;
143
+ if (!errors || !this.passwordStrengthEnforced) {
144
+ return;
145
+ }
146
+
147
+ const password = this.model.newPassword || '';
148
+ const hasInvalidChars = password && !this.passwordValidation.hasValidCharsOnly(password);
149
+
150
+ const filteredErrors = { ...errors };
151
+ if (!this.requirementsFulfilled && !hasInvalidChars) {
152
+ // Checklist not fulfilled AND no invalid chars → show checklist error, hide pattern errors
153
+ delete filteredErrors['password'];
154
+ delete filteredErrors['passwordSimple'];
155
+ } else if (filteredErrors['password'] || filteredErrors['passwordSimple']) {
156
+ // Pattern error (invalid chars or checklist fulfilled) → show pattern error, hide checklist
157
+ delete filteredErrors['passwordStrengthChecklist'];
158
+ }
159
+
160
+ const remaining = Object.keys(filteredErrors).length ? filteredErrors : null;
161
+ this.newPasswordModel.control.setErrors(remaining);
162
+ }
101
163
  }
@@ -53,6 +53,8 @@ export class LoginService extends SimplifiedAuthService {
53
53
  ] as const;
54
54
 
55
55
  private readonly IDP_HINT_QUERY_PARAM = 'idp_hint';
56
+ private readonly PASSWORD_MAX_LENGTH = 32;
57
+ private readonly PASSWORD_ALLOWED_SYMBOLS = '`~!@#$%^&*()_|+-=?;:\'",.<>{}[]\\/';
56
58
  private translateService = inject(TranslateService);
57
59
  ERROR_MESSAGES = {
58
60
  minlength: gettext('Password must have at least 8 characters and no more than 32.'),
@@ -68,11 +70,8 @@ export class LoginService extends SimplifiedAuthService {
68
70
  'Password reset link expired. Please enter your email address to receive a new one.'
69
71
  ),
70
72
  tfa_pin_invalid: gettext('The code you entered is invalid. Please try again.'),
71
- pattern_newPassword: this.translateService.instant(
72
- gettext(
73
- 'Password must have at least 8 characters and no more than 32 and can only contain letters, numbers and following symbols: {{ symbols }}'
74
- ),
75
- { symbols: '`~!@#$%^&*()_|+-=?;:\'",.<>{}[]\\/' }
73
+ pattern_newPassword: gettext(
74
+ 'Password must have at least {{ minLength }} characters and no more than {{ maxLength }} and can only contain letters, numbers and following symbols: {{ symbols }}'
76
75
  ),
77
76
  internationalPhoneNumber: gettext(
78
77
  'Must be a valid phone number (only digits, spaces, slashes ("/"), dashes ("-"), and plus ("+") allowed, for example: +49 9 876 543 210).'
@@ -125,6 +124,29 @@ export class LoginService extends SimplifiedAuthService {
125
124
  this.tenantUiService.getOauth2Option(loginOptions) || ({} as ITenantLoginOption);
126
125
  }
127
126
 
127
+ /**
128
+ * Returns the password pattern error message with the correct min length.
129
+ * @param minLength The minimum password length from tenant configuration.
130
+ * @returns The translated error message.
131
+ */
132
+ getPasswordPatternErrorMessage(minLength: number): string {
133
+ return this.translateService.instant(this.ERROR_MESSAGES.pattern_newPassword, {
134
+ minLength,
135
+ maxLength: this.PASSWORD_MAX_LENGTH,
136
+ symbols: this.PASSWORD_ALLOWED_SYMBOLS
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Builds a regex pattern for password validation with the given min length.
142
+ * @param minLength The minimum password length from tenant configuration.
143
+ * @returns A RegExp for password validation.
144
+ */
145
+ buildPasswordPattern(minLength: number): RegExp {
146
+ const allowedChars = 'a-zA-Z0-9`~!@#$%^&*()_|+\\-=?;:\'",.<>{}[\\]\\\\/';
147
+ return new RegExp(`^[${allowedChars}]{${minLength},${this.PASSWORD_MAX_LENGTH}}$`);
148
+ }
149
+
128
150
  redirectToOauth() {
129
151
  const idpHint = this.getIdpHintFromQueryParams();
130
152
  const { initRequest, flowControlledByUI } = this.oauthOptions;