@c8y/login 1022.3.2
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/.browserslistrc +16 -0
- package/cumulocity.config.ts +27 -0
- package/jest.config.js +18 -0
- package/package.json +23 -0
- package/public/favicon.ico +0 -0
- package/public/platform-animation.svg +2533 -0
- package/src/app/app.config.ts +11 -0
- package/src/app/bootstrap-login/bootstrap-login.component.html +3 -0
- package/src/app/bootstrap-login/bootstrap-login.component.ts +16 -0
- package/src/app/login/change-password/change-password.component.html +97 -0
- package/src/app/login/change-password/change-password.component.ts +101 -0
- package/src/app/login/credentials/credentials.component.html +141 -0
- package/src/app/login/credentials/credentials.component.ts +148 -0
- package/src/app/login/credentials-component-params.ts +4 -0
- package/src/app/login/credentials-from-query-params.service.ts +86 -0
- package/src/app/login/index.ts +9 -0
- package/src/app/login/login.component.html +128 -0
- package/src/app/login/login.component.less +136 -0
- package/src/app/login/login.component.ts +238 -0
- package/src/app/login/login.model.ts +36 -0
- package/src/app/login/login.service.ts +651 -0
- package/src/app/login/missing-application-access/missing-application-access.component.html +2 -0
- package/src/app/login/missing-application-access/missing-application-access.component.ts +21 -0
- package/src/app/login/password-strength-validator.directive.ts +26 -0
- package/src/app/login/provide-phone-number/provide-phone-number.component.html +39 -0
- package/src/app/login/provide-phone-number/provide-phone-number.component.ts +73 -0
- package/src/app/login/recover-password/recover-password.component.html +53 -0
- package/src/app/login/recover-password/recover-password.component.ts +59 -0
- package/src/app/login/sms-challenge/sms-challenge.component.html +50 -0
- package/src/app/login/sms-challenge/sms-challenge.component.ts +134 -0
- package/src/app/login/strength-validator-service.ts +18 -0
- package/src/app/login/tenant-id-setup/tenant-id-setup.component.html +28 -0
- package/src/app/login/tenant-id-setup/tenant-id-setup.component.ts +94 -0
- package/src/app/login/totp-auth/totp-auth.component.html +18 -0
- package/src/app/login/totp-auth/totp-auth.component.ts +72 -0
- package/src/bootstrap.ts +19 -0
- package/src/i18n.ts +18 -0
- package/src/main.ts +25 -0
- package/src/polyfills.ts +33 -0
- package/tsconfig.app.json +20 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<form #twoFactorForm="ngForm" class="loginForm" (ngSubmit)="save()" novalidate>
|
|
2
|
+
<div class="legend form-block center" translate>Two-factor authentication</div>
|
|
3
|
+
|
|
4
|
+
<c8y-form-group [ngClass]="requestInProgress || twoFactorForm.invalid ? 'p-b-8' : ''">
|
|
5
|
+
<label translate>Provide your phone number</label>
|
|
6
|
+
|
|
7
|
+
<input
|
|
8
|
+
class="form-control"
|
|
9
|
+
[(ngModel)]="phoneNumber"
|
|
10
|
+
#contactPhone="ngModel"
|
|
11
|
+
type="text"
|
|
12
|
+
name="phone"
|
|
13
|
+
autocomplete="off"
|
|
14
|
+
placeholder="{{ 'e.g. +49 9 876 543 210`LOCALIZE`' | translate }}"
|
|
15
|
+
c8yPhoneValidation
|
|
16
|
+
required
|
|
17
|
+
/>
|
|
18
|
+
</c8y-form-group>
|
|
19
|
+
|
|
20
|
+
<button
|
|
21
|
+
title="{{ 'Save and continue' | translate }}"
|
|
22
|
+
type="submit"
|
|
23
|
+
class="btn btn-primary btn-lg btn-block form-group"
|
|
24
|
+
[disabled]="requestInProgress || twoFactorForm.invalid"
|
|
25
|
+
>
|
|
26
|
+
{{ 'Save and continue' | translate }}
|
|
27
|
+
</button>
|
|
28
|
+
|
|
29
|
+
<div class="d-flex m-t-8">
|
|
30
|
+
<a
|
|
31
|
+
title="{{ 'Login' | translate }}"
|
|
32
|
+
class="small pointer m-auto"
|
|
33
|
+
href="#"
|
|
34
|
+
(click)="onCancel.emit()"
|
|
35
|
+
>
|
|
36
|
+
{{ 'Login' | translate }}
|
|
37
|
+
</a>
|
|
38
|
+
</div>
|
|
39
|
+
</form>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Component, Output, EventEmitter, Input } from '@angular/core';
|
|
2
|
+
import { LoginService } from '../login.service';
|
|
3
|
+
import { LoginEvent, LoginViews } from '../login.model';
|
|
4
|
+
import { ICredentials, UserService } from '@c8y/client';
|
|
5
|
+
import { FormsModule } from '@angular/forms';
|
|
6
|
+
import { NgClass } from '@angular/common';
|
|
7
|
+
import {
|
|
8
|
+
RequiredInputPlaceholderDirective,
|
|
9
|
+
PhoneValidationDirective,
|
|
10
|
+
C8yTranslatePipe,
|
|
11
|
+
FormGroupComponent,
|
|
12
|
+
C8yTranslateDirective,
|
|
13
|
+
AlertService
|
|
14
|
+
} from '@c8y/ngx-components';
|
|
15
|
+
|
|
16
|
+
@Component({
|
|
17
|
+
selector: 'c8y-provide-phone-number',
|
|
18
|
+
templateUrl: './provide-phone-number.component.html',
|
|
19
|
+
standalone: true,
|
|
20
|
+
imports: [
|
|
21
|
+
FormsModule,
|
|
22
|
+
C8yTranslateDirective,
|
|
23
|
+
FormGroupComponent,
|
|
24
|
+
NgClass,
|
|
25
|
+
RequiredInputPlaceholderDirective,
|
|
26
|
+
PhoneValidationDirective,
|
|
27
|
+
C8yTranslatePipe
|
|
28
|
+
]
|
|
29
|
+
})
|
|
30
|
+
export class ProvidePhoneNumberComponent {
|
|
31
|
+
@Input() credentials: ICredentials;
|
|
32
|
+
@Output() onCancel = new EventEmitter();
|
|
33
|
+
@Output() onChangeView = new EventEmitter<LoginEvent>();
|
|
34
|
+
|
|
35
|
+
phoneNumber: string;
|
|
36
|
+
requestInProgress = false;
|
|
37
|
+
private readonly sendTfa: string = '0';
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
public loginService: LoginService,
|
|
41
|
+
public alert: AlertService,
|
|
42
|
+
private userService: UserService
|
|
43
|
+
) {}
|
|
44
|
+
|
|
45
|
+
async save() {
|
|
46
|
+
try {
|
|
47
|
+
this.requestInProgress = true;
|
|
48
|
+
await this.userService.savePhoneNumber(this.phoneNumber);
|
|
49
|
+
await this.sendTFASms();
|
|
50
|
+
this.onChangeView.emit({
|
|
51
|
+
view: LoginViews.SmsChallenge,
|
|
52
|
+
credentials: this.credentials
|
|
53
|
+
});
|
|
54
|
+
} catch (e) {
|
|
55
|
+
this.alert.addServerFailure(e);
|
|
56
|
+
} finally {
|
|
57
|
+
this.requestInProgress = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private async sendTFASms() {
|
|
62
|
+
try {
|
|
63
|
+
await this.userService.verifyTFACode(this.sendTfa);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
if (e.res.status === 403) {
|
|
66
|
+
this.loginService.cleanMessages();
|
|
67
|
+
this.loginService.addSuccessMessage('send_sms');
|
|
68
|
+
} else {
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<form #resetForm="ngForm" class="loginForm" (ngSubmit)="resetPassword()" novalidate>
|
|
2
|
+
<c8y-form-group class="tenantField" id="tenantField" *ngIf="loginService.showTenant()">
|
|
3
|
+
<label translate>Tenant ID</label>
|
|
4
|
+
<input
|
|
5
|
+
[(ngModel)]="model.tenantId"
|
|
6
|
+
#tenantId="ngModel"
|
|
7
|
+
type="text"
|
|
8
|
+
name="tenantId"
|
|
9
|
+
autocapitalize="off"
|
|
10
|
+
autocorrect="off"
|
|
11
|
+
class="form-control"
|
|
12
|
+
placeholder="{{ 'Tenant ID' | translate }}"
|
|
13
|
+
required
|
|
14
|
+
/>
|
|
15
|
+
</c8y-form-group>
|
|
16
|
+
|
|
17
|
+
<c8y-form-group>
|
|
18
|
+
<label translate>Email address</label>
|
|
19
|
+
<input
|
|
20
|
+
[(ngModel)]="model.email"
|
|
21
|
+
#email="ngModel"
|
|
22
|
+
type="text"
|
|
23
|
+
name="email"
|
|
24
|
+
autocapitalize="off"
|
|
25
|
+
autocorrect="off"
|
|
26
|
+
class="form-control"
|
|
27
|
+
placeholder="{{ 'Email address' | translate }}"
|
|
28
|
+
email
|
|
29
|
+
required
|
|
30
|
+
/>
|
|
31
|
+
</c8y-form-group>
|
|
32
|
+
|
|
33
|
+
<div class="m-t-32">
|
|
34
|
+
<button
|
|
35
|
+
title="{{ 'Reset password' | translate }}"
|
|
36
|
+
[disabled]="!resetForm.form.valid || isLoading"
|
|
37
|
+
type="submit"
|
|
38
|
+
class="btn btn-primary btn-lg btn-block form-group"
|
|
39
|
+
>
|
|
40
|
+
{{ 'Reset password' | translate }}
|
|
41
|
+
</button>
|
|
42
|
+
<div class="text-center m-t-8">
|
|
43
|
+
<button
|
|
44
|
+
type="submit"
|
|
45
|
+
title="{{ 'Login' | translate }}"
|
|
46
|
+
class="btn btn-link btn-sm"
|
|
47
|
+
(click)="onChangeView.emit({ view: LOGIN_VIEWS.Credentials })"
|
|
48
|
+
>
|
|
49
|
+
{{ 'Login' | translate }}
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</form>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
|
|
2
|
+
import { UserService } from '@c8y/client';
|
|
3
|
+
import { LoginService } from '../login.service';
|
|
4
|
+
import { LoginEvent, LoginViews } from '../login.model';
|
|
5
|
+
import { FormsModule } from '@angular/forms';
|
|
6
|
+
import {
|
|
7
|
+
C8yTranslateDirective,
|
|
8
|
+
FormGroupComponent,
|
|
9
|
+
RequiredInputPlaceholderDirective,
|
|
10
|
+
C8yTranslatePipe
|
|
11
|
+
} from '@c8y/ngx-components';
|
|
12
|
+
|
|
13
|
+
import { NgIf } from '@angular/common';
|
|
14
|
+
|
|
15
|
+
@Component({
|
|
16
|
+
selector: 'c8y-recover-password',
|
|
17
|
+
templateUrl: './recover-password.component.html',
|
|
18
|
+
styles: [],
|
|
19
|
+
standalone: true,
|
|
20
|
+
imports: [
|
|
21
|
+
FormsModule,
|
|
22
|
+
C8yTranslateDirective,
|
|
23
|
+
NgIf,
|
|
24
|
+
FormGroupComponent,
|
|
25
|
+
RequiredInputPlaceholderDirective,
|
|
26
|
+
C8yTranslatePipe
|
|
27
|
+
]
|
|
28
|
+
})
|
|
29
|
+
export class RecoverPasswordComponent implements OnInit {
|
|
30
|
+
@Output() onChangeView = new EventEmitter<LoginEvent>();
|
|
31
|
+
LOGIN_VIEWS = LoginViews;
|
|
32
|
+
isLoading = false;
|
|
33
|
+
model = {
|
|
34
|
+
email: '',
|
|
35
|
+
tenantId: ''
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
private users: UserService,
|
|
40
|
+
public loginService: LoginService
|
|
41
|
+
) {}
|
|
42
|
+
|
|
43
|
+
ngOnInit() {
|
|
44
|
+
this.model.tenantId = this.loginService.getTenant();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async resetPassword() {
|
|
48
|
+
try {
|
|
49
|
+
this.isLoading = true;
|
|
50
|
+
const { res } = await this.users.sendPasswordResetMail(this.model.email, this.model.tenantId);
|
|
51
|
+
if (res.status === 200) {
|
|
52
|
+
this.loginService.addSuccessMessage('password_reset_requested');
|
|
53
|
+
}
|
|
54
|
+
} finally {
|
|
55
|
+
this.loginService.reset();
|
|
56
|
+
this.isLoading = false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<form #twoFactorForm="ngForm" class="loginForm" (ngSubmit)="verifyTFACode()" novalidate>
|
|
2
|
+
<div class="legend form-block center" translate>Two-factor authentication</div>
|
|
3
|
+
|
|
4
|
+
<c8y-form-group>
|
|
5
|
+
<label translate>Verification code</label>
|
|
6
|
+
<input
|
|
7
|
+
[(ngModel)]="model.smsToken"
|
|
8
|
+
#sms_token="ngModel"
|
|
9
|
+
type="text"
|
|
10
|
+
name="sms_token"
|
|
11
|
+
autofocus
|
|
12
|
+
autocapitalize="off"
|
|
13
|
+
autocorrect="off"
|
|
14
|
+
class="form-control"
|
|
15
|
+
placeholder="{{ 'e.g.' | translate }} 624327"
|
|
16
|
+
required
|
|
17
|
+
/>
|
|
18
|
+
<p *ngIf="!twoFactorForm.form.valid || isLoading" class="help-block" translate>
|
|
19
|
+
Insert the code received via SMS.
|
|
20
|
+
</p>
|
|
21
|
+
</c8y-form-group>
|
|
22
|
+
|
|
23
|
+
<button
|
|
24
|
+
title="{{ 'Verify' | translate }}"
|
|
25
|
+
[disabled]="!twoFactorForm.form.valid || isLoading"
|
|
26
|
+
class="btn btn-primary btn-lg btn-block form-group"
|
|
27
|
+
>
|
|
28
|
+
{{ 'Verify' | translate }}
|
|
29
|
+
</button>
|
|
30
|
+
|
|
31
|
+
<div class="d-flex m-t-8 j-c-center">
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
title="{{ 'Send new code' | translate }}"
|
|
35
|
+
[ngClass]="{ disabled: isLoading }"
|
|
36
|
+
class="btn btn-link btn-sm"
|
|
37
|
+
(click)="resendTFASms()"
|
|
38
|
+
>
|
|
39
|
+
{{ 'Send new code' | translate }}
|
|
40
|
+
</button>
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
title="{{ 'Log in' | translate }}"
|
|
44
|
+
class="btn btn-link btn-sm"
|
|
45
|
+
(click)="onCancel.emit()"
|
|
46
|
+
>
|
|
47
|
+
{{ 'Log in' | translate }}
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
</form>
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Component, Output, EventEmitter, Input } from '@angular/core';
|
|
2
|
+
import { UserService, ICredentials } from '@c8y/client';
|
|
3
|
+
import { LoginService } from '../login.service';
|
|
4
|
+
import {
|
|
5
|
+
AlertService,
|
|
6
|
+
C8yTranslateDirective,
|
|
7
|
+
FormGroupComponent,
|
|
8
|
+
RequiredInputPlaceholderDirective,
|
|
9
|
+
C8yTranslatePipe,
|
|
10
|
+
AppStateService
|
|
11
|
+
} from '@c8y/ngx-components';
|
|
12
|
+
import { gettext } from '@c8y/ngx-components/gettext';
|
|
13
|
+
import { FormsModule } from '@angular/forms';
|
|
14
|
+
import { NgIf, NgClass } from '@angular/common';
|
|
15
|
+
import { LoginEvent, LoginViews } from '../login.model';
|
|
16
|
+
|
|
17
|
+
@Component({
|
|
18
|
+
selector: 'c8y-sms-challenge',
|
|
19
|
+
templateUrl: './sms-challenge.component.html',
|
|
20
|
+
styles: [],
|
|
21
|
+
standalone: true,
|
|
22
|
+
imports: [
|
|
23
|
+
FormsModule,
|
|
24
|
+
C8yTranslateDirective,
|
|
25
|
+
FormGroupComponent,
|
|
26
|
+
RequiredInputPlaceholderDirective,
|
|
27
|
+
NgIf,
|
|
28
|
+
NgClass,
|
|
29
|
+
C8yTranslatePipe
|
|
30
|
+
]
|
|
31
|
+
})
|
|
32
|
+
export class SmsChallengeComponent {
|
|
33
|
+
@Input() credentials: ICredentials;
|
|
34
|
+
@Output() onCancel = new EventEmitter();
|
|
35
|
+
@Output() onChangeView = new EventEmitter<LoginEvent>();
|
|
36
|
+
|
|
37
|
+
model = {
|
|
38
|
+
smsToken: ''
|
|
39
|
+
};
|
|
40
|
+
isLoading = false;
|
|
41
|
+
|
|
42
|
+
private resendTfa = '0';
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
public loginService: LoginService,
|
|
46
|
+
private users: UserService,
|
|
47
|
+
private alert: AlertService,
|
|
48
|
+
private appState: AppStateService
|
|
49
|
+
) {}
|
|
50
|
+
|
|
51
|
+
async verifyTFACode() {
|
|
52
|
+
this.isLoading = true;
|
|
53
|
+
if (await this.usesOAuthInternal()) {
|
|
54
|
+
await this.verifyCodeWithOauth();
|
|
55
|
+
} else {
|
|
56
|
+
await this.verifyCodeWithBasicAuth();
|
|
57
|
+
}
|
|
58
|
+
this.isLoading = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async resendTFASms() {
|
|
62
|
+
try {
|
|
63
|
+
this.isLoading = true;
|
|
64
|
+
await this.users.verifyTFACode(this.resendTfa);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
if (e.res.status === 403) {
|
|
67
|
+
this.loginService.cleanMessages();
|
|
68
|
+
this.loginService.addSuccessMessage('resend_sms');
|
|
69
|
+
} else {
|
|
70
|
+
this.alert.addServerFailure(e);
|
|
71
|
+
}
|
|
72
|
+
} finally {
|
|
73
|
+
this.isLoading = false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async usesOAuthInternal() {
|
|
78
|
+
return this.loginService.isPasswordGrantLogin(this.credentials);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async verifyCodeWithOauth() {
|
|
82
|
+
try {
|
|
83
|
+
const { credentials } = this;
|
|
84
|
+
await this.loginService.switchLoginMode({ ...credentials, tfa: this.model.smsToken });
|
|
85
|
+
await this.loginService.authFulfilled();
|
|
86
|
+
const result = await this.loginService.ensureUserPermissionsForRedirect(
|
|
87
|
+
this.appState.currentUser.value
|
|
88
|
+
);
|
|
89
|
+
if (!result) {
|
|
90
|
+
this.onChangeView.emit({ view: LoginViews.MissingApplicationAccess });
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
const resStatus = e.res && e.res.status;
|
|
94
|
+
if (resStatus === 401) {
|
|
95
|
+
// it is assumed that the user and password are correct so it must be the tfa code
|
|
96
|
+
this.alert.danger(gettext('Invalid code'));
|
|
97
|
+
} else {
|
|
98
|
+
this.alert.addServerFailure(e);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async verifyCodeWithBasicAuth() {
|
|
104
|
+
try {
|
|
105
|
+
const { res } = await this.users.verifyTFACode(this.model.smsToken);
|
|
106
|
+
const tfaToken = res.headers.get('tfatoken');
|
|
107
|
+
this.credentials.tfa = tfaToken;
|
|
108
|
+
await this.loginWithTFA(tfaToken);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
const resStatus = e.res && e.res.status;
|
|
111
|
+
// BE returns 403 in case of invalid tfa code
|
|
112
|
+
if (resStatus === 403) {
|
|
113
|
+
this.alert.danger(gettext('Invalid code'));
|
|
114
|
+
} else {
|
|
115
|
+
this.alert.addServerFailure(e);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async loginWithTFA(tfaToken) {
|
|
121
|
+
try {
|
|
122
|
+
await this.loginService.login(
|
|
123
|
+
this.loginService.useBasicAuth({ tfa: tfaToken }),
|
|
124
|
+
this.credentials
|
|
125
|
+
);
|
|
126
|
+
this.loginService.saveTFAToken(tfaToken, sessionStorage);
|
|
127
|
+
if (this.loginService.rememberMe) {
|
|
128
|
+
this.loginService.saveTFAToken(tfaToken, localStorage);
|
|
129
|
+
}
|
|
130
|
+
} catch (e) {
|
|
131
|
+
this.alert.addServerFailure(e);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { PasswordService } from '@c8y/ngx-components';
|
|
3
|
+
import { PasswordStrength } from '@c8y/client';
|
|
4
|
+
|
|
5
|
+
@Injectable({
|
|
6
|
+
providedIn: 'root'
|
|
7
|
+
})
|
|
8
|
+
export class StrengthValidatorService {
|
|
9
|
+
constructor(private passwordService: PasswordService) {}
|
|
10
|
+
|
|
11
|
+
isStrong(password: string): boolean {
|
|
12
|
+
return this.isPasswordGreen(this.passwordService.getStrengthColor(password).passwordStrength);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private isPasswordGreen(strength: PasswordStrength) {
|
|
16
|
+
return (strength as PasswordStrength) === (PasswordStrength.GREEN as PasswordStrength);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<form #tenantIdSetupForm="ngForm" class="loginForm" (ngSubmit)="setupLoginMode()" novalidate>
|
|
2
|
+
<div class="legend form-block center" translate>Tenant setup</div>
|
|
3
|
+
<c8y-form-group class="tenantField" id="tenantField">
|
|
4
|
+
<label for="tenant" translate>Tenant ID</label>
|
|
5
|
+
<input
|
|
6
|
+
[(ngModel)]="model.tenant"
|
|
7
|
+
#tenant="ngModel"
|
|
8
|
+
type="text"
|
|
9
|
+
name="tenant"
|
|
10
|
+
id="tenant"
|
|
11
|
+
autocapitalize="off"
|
|
12
|
+
autocorrect="off"
|
|
13
|
+
class="form-control"
|
|
14
|
+
placeholder="{{ 'e.g.' | translate }} t12345"
|
|
15
|
+
placeholder-no-required-hint
|
|
16
|
+
required
|
|
17
|
+
/>
|
|
18
|
+
</c8y-form-group>
|
|
19
|
+
|
|
20
|
+
<button
|
|
21
|
+
title="{{ 'Apply' | translate }}"
|
|
22
|
+
type="submit"
|
|
23
|
+
class="btn btn-primary btn-lg btn-block form-group"
|
|
24
|
+
[disabled]="!tenantIdSetupForm.form.valid"
|
|
25
|
+
>
|
|
26
|
+
{{ 'Apply' | translate }}
|
|
27
|
+
</button>
|
|
28
|
+
</form>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Component, Output, EventEmitter } from '@angular/core';
|
|
2
|
+
import { LoginEvent, LoginViews } from '../login.model';
|
|
3
|
+
import { FetchClient } from '@c8y/client';
|
|
4
|
+
import {
|
|
5
|
+
AppStateService,
|
|
6
|
+
AlertService,
|
|
7
|
+
C8yTranslateDirective,
|
|
8
|
+
FormGroupComponent,
|
|
9
|
+
RequiredInputPlaceholderDirective,
|
|
10
|
+
C8yTranslatePipe
|
|
11
|
+
} from '@c8y/ngx-components';
|
|
12
|
+
import { LoginService } from '../login.service';
|
|
13
|
+
import { TranslateService } from '@ngx-translate/core';
|
|
14
|
+
import { gettext } from '@c8y/ngx-components/gettext';
|
|
15
|
+
import { FormsModule } from '@angular/forms';
|
|
16
|
+
|
|
17
|
+
@Component({
|
|
18
|
+
selector: 'c8y-tenant-id-setup',
|
|
19
|
+
templateUrl: './tenant-id-setup.component.html',
|
|
20
|
+
styles: [],
|
|
21
|
+
standalone: true,
|
|
22
|
+
imports: [
|
|
23
|
+
FormsModule,
|
|
24
|
+
C8yTranslateDirective,
|
|
25
|
+
FormGroupComponent,
|
|
26
|
+
RequiredInputPlaceholderDirective,
|
|
27
|
+
C8yTranslatePipe
|
|
28
|
+
]
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `TenantIdSetupComponent` is intended to be shown when tenant's id cannot be determined based on the current URL.
|
|
33
|
+
* It asks the user to provide target tenant's id and then it fetches login options for this tenant.
|
|
34
|
+
* In case of OAI-Secure login mode, login options will contain `domain` property set by backend.
|
|
35
|
+
* The component will redirect user to this domain, preserving URL path and params.
|
|
36
|
+
*/
|
|
37
|
+
export class TenantIdSetupComponent {
|
|
38
|
+
@Output() onChangeView = new EventEmitter<LoginEvent>();
|
|
39
|
+
LOGIN_VIEWS = LoginViews;
|
|
40
|
+
model = {
|
|
41
|
+
tenant: ''
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
private client: FetchClient,
|
|
46
|
+
private ui: AppStateService,
|
|
47
|
+
private loginService: LoginService,
|
|
48
|
+
private alert: AlertService,
|
|
49
|
+
private translateService: TranslateService
|
|
50
|
+
) {}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Sets up login mode for particular tenant. In case of OAI-Secure will redirect user to tenant domain.
|
|
54
|
+
*/
|
|
55
|
+
async setupLoginMode() {
|
|
56
|
+
this.client.tenant = this.model.tenant;
|
|
57
|
+
try {
|
|
58
|
+
await this.ui.refreshLoginOptions();
|
|
59
|
+
this.loginService.initLoginOptions();
|
|
60
|
+
this.redirectToCorrectDomain();
|
|
61
|
+
} catch (e) {
|
|
62
|
+
if (e.res && e.res.status === 401) {
|
|
63
|
+
this.alert.danger(
|
|
64
|
+
this.translateService.instant(
|
|
65
|
+
gettext('Could not find tenant with ID "{{ tenantId }}".'),
|
|
66
|
+
{ tenantId: this.model.tenant }
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
} else {
|
|
70
|
+
this.alert.addServerFailure(e);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Redirects to tenant domain when login mode contains domain.
|
|
77
|
+
*/
|
|
78
|
+
redirectToCorrectDomain() {
|
|
79
|
+
const loginRedirectDomain = this.loginService.loginMode.loginRedirectDomain;
|
|
80
|
+
if (loginRedirectDomain) {
|
|
81
|
+
const alreadyOnCorrectDomain = window.location.href.includes(loginRedirectDomain);
|
|
82
|
+
if (!alreadyOnCorrectDomain) {
|
|
83
|
+
this.loginService.redirectToDomain(loginRedirectDomain);
|
|
84
|
+
} else {
|
|
85
|
+
this.onChangeView.emit({
|
|
86
|
+
view: LoginViews.Credentials,
|
|
87
|
+
loginViewParams: { showTenant: true, disableTenant: true }
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
this.onChangeView.emit({ view: LoginViews.Credentials });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<div
|
|
2
|
+
class="legend form-block center"
|
|
3
|
+
translate
|
|
4
|
+
>
|
|
5
|
+
Two-factor authentication
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<c8y-totp-setup *ngIf="isSetup">
|
|
9
|
+
</c8y-totp-setup>
|
|
10
|
+
<c8y-totp-challenge
|
|
11
|
+
[isModal]="false"
|
|
12
|
+
[loading]="loading"
|
|
13
|
+
[hasError]="hasError"
|
|
14
|
+
[verify]="view === LOGIN_VIEWS.TotpSetup"
|
|
15
|
+
(onSuccess)="onTotpSuccess($event)"
|
|
16
|
+
(totpUnconfirmedEmitter)="onCancel.emit()"
|
|
17
|
+
></c8y-totp-challenge>
|
|
18
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
|
|
2
|
+
import { ICredentials, UserService } from '@c8y/client';
|
|
3
|
+
import {
|
|
4
|
+
AlertService,
|
|
5
|
+
C8yTranslateDirective,
|
|
6
|
+
TotpSetupComponent,
|
|
7
|
+
TotpChallengeComponent,
|
|
8
|
+
AppStateService
|
|
9
|
+
} from '@c8y/ngx-components';
|
|
10
|
+
import { LoginService } from '../login.service';
|
|
11
|
+
import { LoginEvent, LoginViews } from '../login.model';
|
|
12
|
+
import { gettext } from '@c8y/ngx-components/gettext';
|
|
13
|
+
import { NgIf } from '@angular/common';
|
|
14
|
+
|
|
15
|
+
@Component({
|
|
16
|
+
selector: 'c8y-totp-auth',
|
|
17
|
+
templateUrl: './totp-auth.component.html',
|
|
18
|
+
standalone: true,
|
|
19
|
+
imports: [C8yTranslateDirective, NgIf, TotpSetupComponent, TotpChallengeComponent]
|
|
20
|
+
})
|
|
21
|
+
export class TotpAuthComponent implements OnInit {
|
|
22
|
+
@Input() credentials: ICredentials;
|
|
23
|
+
@Input() view: LoginViews;
|
|
24
|
+
@Output() onCancel = new EventEmitter();
|
|
25
|
+
@Output() onChangeView = new EventEmitter<LoginEvent>();
|
|
26
|
+
LOGIN_VIEWS = LoginViews;
|
|
27
|
+
loading = false;
|
|
28
|
+
hasError = false;
|
|
29
|
+
isSetup = false;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
public loginService: LoginService,
|
|
33
|
+
private userService: UserService,
|
|
34
|
+
private alert: AlertService,
|
|
35
|
+
private appState: AppStateService
|
|
36
|
+
) {}
|
|
37
|
+
|
|
38
|
+
ngOnInit() {
|
|
39
|
+
if (this.view === this.LOGIN_VIEWS.TotpSetup) {
|
|
40
|
+
this.isSetup = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async onTotpSuccess(code: string) {
|
|
45
|
+
try {
|
|
46
|
+
this.loading = true;
|
|
47
|
+
this.hasError = false;
|
|
48
|
+
this.credentials.tfa = code;
|
|
49
|
+
if (this.isSetup) {
|
|
50
|
+
await this.userService.activateTotp();
|
|
51
|
+
}
|
|
52
|
+
await this.loginService.switchLoginMode(this.credentials);
|
|
53
|
+
await this.loginService.authFulfilled();
|
|
54
|
+
const result = await this.loginService.ensureUserPermissionsForRedirect(
|
|
55
|
+
this.appState.currentUser.value
|
|
56
|
+
);
|
|
57
|
+
if (!result) {
|
|
58
|
+
this.onChangeView.emit({ view: LoginViews.MissingApplicationAccess });
|
|
59
|
+
}
|
|
60
|
+
} catch (e) {
|
|
61
|
+
this.alert.removeLastDanger();
|
|
62
|
+
if (e.data && e.data.message === 'Authentication failed! : User account is locked') {
|
|
63
|
+
this.alert.warning(gettext('Authentication failed due to: user account is locked.'));
|
|
64
|
+
} else {
|
|
65
|
+
this.alert.addServerFailure(e);
|
|
66
|
+
this.hasError = true;
|
|
67
|
+
}
|
|
68
|
+
} finally {
|
|
69
|
+
this.loading = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/bootstrap.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import './polyfills';
|
|
2
|
+
import '@angular/compiler';
|
|
3
|
+
|
|
4
|
+
import { enableProdMode } from '@angular/core';
|
|
5
|
+
import { bootstrapApplication } from '@angular/platform-browser';
|
|
6
|
+
import { appConfig } from './app/app.config';
|
|
7
|
+
import { BootstrapLoginComponent } from './app/bootstrap-login/bootstrap-login.component';
|
|
8
|
+
import { provideBootstrapMetadata } from '@c8y/ngx-components';
|
|
9
|
+
import { BootstrapMetaData } from '@c8y/bootstrap';
|
|
10
|
+
|
|
11
|
+
declare const __MODE__: string;
|
|
12
|
+
if (__MODE__ === 'production') {
|
|
13
|
+
enableProdMode();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function bootstrap(metadata: BootstrapMetaData) {
|
|
17
|
+
appConfig.providers.push(...provideBootstrapMetadata(metadata));
|
|
18
|
+
return bootstrapApplication(BootstrapLoginComponent, appConfig).catch(err => console.log(err));
|
|
19
|
+
}
|
package/src/i18n.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internationalizing files in po format (https://en.wikipedia.org/wiki/Gettext#Translating)
|
|
3
|
+
* You can always add additional strings by adding your own po file. All po files are
|
|
4
|
+
* combined to one JSON file per language and are loaded if the specific language is needed.
|
|
5
|
+
*/
|
|
6
|
+
import '@c8y/ngx-components/locales/de.po';
|
|
7
|
+
import '@c8y/ngx-components/locales/en.po';
|
|
8
|
+
import '@c8y/ngx-components/locales/en_US.po';
|
|
9
|
+
import '@c8y/ngx-components/locales/es.po';
|
|
10
|
+
import '@c8y/ngx-components/locales/fr.po';
|
|
11
|
+
import '@c8y/ngx-components/locales/ja_JP.po';
|
|
12
|
+
import '@c8y/ngx-components/locales/ko.po';
|
|
13
|
+
import '@c8y/ngx-components/locales/nl.po';
|
|
14
|
+
import '@c8y/ngx-components/locales/pl.po';
|
|
15
|
+
import '@c8y/ngx-components/locales/pt_BR.po';
|
|
16
|
+
import '@c8y/ngx-components/locales/zh_CN.po';
|
|
17
|
+
import '@c8y/ngx-components/locales/zh_TW.po';
|
|
18
|
+
// import './locales/de.po'; // <- adding additional strings to the german translation.
|