@c8y/login 1023.52.0 → 1023.55.5
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.
|
|
3
|
+
"version": "1023.55.5",
|
|
4
4
|
"description": "This package is used to scaffold a login application for Cumulocity IoT.",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@c8y/style": "1023.
|
|
7
|
-
"@c8y/ngx-components": "1023.
|
|
8
|
-
"@c8y/client": "1023.
|
|
9
|
-
"@c8y/bootstrap": "1023.
|
|
6
|
+
"@c8y/style": "1023.55.5",
|
|
7
|
+
"@c8y/ngx-components": "1023.55.5",
|
|
8
|
+
"@c8y/client": "1023.55.5",
|
|
9
|
+
"@c8y/bootstrap": "1023.55.5",
|
|
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.
|
|
17
|
-
"@c8y/devkit": "1023.
|
|
16
|
+
"@c8y/options": "1023.55.5",
|
|
17
|
+
"@c8y/devkit": "1023.55.5"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
20
|
"@angular/common": ">=20 <21"
|
|
@@ -1,34 +1,49 @@
|
|
|
1
|
-
<form
|
|
2
|
-
|
|
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
|
-
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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="
|
|
55
|
-
[text]="
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
2
|
-
import {
|
|
3
|
-
import { IResetPassword,
|
|
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
|
-
|
|
11
|
-
PasswordConfirm,
|
|
8
|
+
FormGroupComponent,
|
|
12
9
|
MessageDirective,
|
|
13
10
|
MessagesComponent,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
OptionsService,
|
|
12
|
+
PasswordCheckListComponent,
|
|
13
|
+
PasswordConfirm,
|
|
14
|
+
PasswordStrengthService,
|
|
15
|
+
PasswordValidationDirective,
|
|
16
|
+
PasswordValidationService,
|
|
17
|
+
RequiredInputPlaceholderDirective
|
|
19
18
|
} from '@c8y/ngx-components';
|
|
20
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
72
|
-
|
|
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;
|