@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.
Files changed (40) hide show
  1. package/.browserslistrc +16 -0
  2. package/cumulocity.config.ts +27 -0
  3. package/jest.config.js +18 -0
  4. package/package.json +23 -0
  5. package/public/favicon.ico +0 -0
  6. package/public/platform-animation.svg +2533 -0
  7. package/src/app/app.config.ts +11 -0
  8. package/src/app/bootstrap-login/bootstrap-login.component.html +3 -0
  9. package/src/app/bootstrap-login/bootstrap-login.component.ts +16 -0
  10. package/src/app/login/change-password/change-password.component.html +97 -0
  11. package/src/app/login/change-password/change-password.component.ts +101 -0
  12. package/src/app/login/credentials/credentials.component.html +141 -0
  13. package/src/app/login/credentials/credentials.component.ts +148 -0
  14. package/src/app/login/credentials-component-params.ts +4 -0
  15. package/src/app/login/credentials-from-query-params.service.ts +86 -0
  16. package/src/app/login/index.ts +9 -0
  17. package/src/app/login/login.component.html +128 -0
  18. package/src/app/login/login.component.less +136 -0
  19. package/src/app/login/login.component.ts +238 -0
  20. package/src/app/login/login.model.ts +36 -0
  21. package/src/app/login/login.service.ts +651 -0
  22. package/src/app/login/missing-application-access/missing-application-access.component.html +2 -0
  23. package/src/app/login/missing-application-access/missing-application-access.component.ts +21 -0
  24. package/src/app/login/password-strength-validator.directive.ts +26 -0
  25. package/src/app/login/provide-phone-number/provide-phone-number.component.html +39 -0
  26. package/src/app/login/provide-phone-number/provide-phone-number.component.ts +73 -0
  27. package/src/app/login/recover-password/recover-password.component.html +53 -0
  28. package/src/app/login/recover-password/recover-password.component.ts +59 -0
  29. package/src/app/login/sms-challenge/sms-challenge.component.html +50 -0
  30. package/src/app/login/sms-challenge/sms-challenge.component.ts +134 -0
  31. package/src/app/login/strength-validator-service.ts +18 -0
  32. package/src/app/login/tenant-id-setup/tenant-id-setup.component.html +28 -0
  33. package/src/app/login/tenant-id-setup/tenant-id-setup.component.ts +94 -0
  34. package/src/app/login/totp-auth/totp-auth.component.html +18 -0
  35. package/src/app/login/totp-auth/totp-auth.component.ts +72 -0
  36. package/src/bootstrap.ts +19 -0
  37. package/src/i18n.ts +18 -0
  38. package/src/main.ts +25 -0
  39. package/src/polyfills.ts +33 -0
  40. package/tsconfig.app.json +20 -0
@@ -0,0 +1,128 @@
1
+ <div class="login-panel {{ platformAnimationSrc ? 'isc8y' : '' }}">
2
+ <div
3
+ class="square animated fadeIn"
4
+ *ngIf="platformAnimationSrc"
5
+ >
6
+ <img [src]="platformAnimationSrc" />
7
+ </div>
8
+
9
+ <div
10
+ class="login-form animated fadeIn"
11
+ *ngIf="currentView !== LOGIN_VIEWS.None"
12
+ [ngSwitch]="currentView"
13
+ >
14
+ <main class="card-block p-b-0 form-group-lg">
15
+ <span class="mainlogo {{ !isBrandLogoSet ? 'c8y-logo' : '' }}"></span>
16
+ <ng-container *ngSwitchCase="LOGIN_VIEWS.Credentials">
17
+ <span class="{{ platformAnimationSrc ? '' : 'text-center' }}">
18
+ <h2
19
+ class="m-b-8"
20
+ translate
21
+ >
22
+ Welcome
23
+ </h2>
24
+ <p
25
+ class="text-16 m-b-40"
26
+ translate
27
+ >
28
+ Log in to access your IoT platform.
29
+ </p>
30
+ </span>
31
+ </ng-container>
32
+ <ng-container *ngSwitchCase="LOGIN_VIEWS.RecoverPassword">
33
+ <span class="{{ platformAnimationSrc ? '' : 'text-center' }}">
34
+ <h2
35
+ class="m-b-8"
36
+ translate
37
+ >
38
+ Reset password
39
+ </h2>
40
+ <p
41
+ class="text-16 m-b-40"
42
+ translate
43
+ >
44
+ Enter your email address and we'll send you a secure link to reset your password.
45
+ </p>
46
+ </span>
47
+ </ng-container>
48
+ <c8y-alert-outlet
49
+ class="m-b-24 d-block"
50
+ position="static"
51
+ ></c8y-alert-outlet>
52
+
53
+ <c8y-credentials
54
+ *ngSwitchCase="LOGIN_VIEWS.Credentials"
55
+ (onChangeView)="handleLoginTemplate($event)"
56
+ [loginViewParams]="loginViewParams"
57
+ ></c8y-credentials>
58
+ <c8y-recover-password
59
+ *ngSwitchCase="LOGIN_VIEWS.RecoverPassword"
60
+ (onChangeView)="handleLoginTemplate($event)"
61
+ ></c8y-recover-password>
62
+ <c8y-change-password
63
+ *ngSwitchCase="LOGIN_VIEWS.ChangePassword"
64
+ (onChangeView)="handleLoginTemplate($event)"
65
+ [credentials]="credentials"
66
+ ></c8y-change-password>
67
+ <c8y-totp-auth
68
+ *ngSwitchCase="LOGIN_VIEWS.TotpChallenge"
69
+ (onCancel)="reset(false)"
70
+ [view]="currentView"
71
+ [credentials]="credentials"
72
+ ></c8y-totp-auth>
73
+ <c8y-totp-auth
74
+ *ngSwitchCase="LOGIN_VIEWS.TotpSetup"
75
+ (onCancel)="reset(false)"
76
+ [view]="currentView"
77
+ [credentials]="credentials"
78
+ ></c8y-totp-auth>
79
+ <c8y-sms-challenge
80
+ *ngSwitchCase="LOGIN_VIEWS.SmsChallenge"
81
+ (onCancel)="reset(false)"
82
+ [credentials]="credentials"
83
+ ></c8y-sms-challenge>
84
+
85
+ <c8y-provide-phone-number
86
+ *ngSwitchCase="LOGIN_VIEWS.ProvidePhoneNumber"
87
+ (onCancel)="reset(false)"
88
+ (onChangeView)="handleLoginTemplate($event)"
89
+ [credentials]="credentials"
90
+ ></c8y-provide-phone-number>
91
+ <c8y-tenant-id-setup
92
+ *ngSwitchCase="LOGIN_VIEWS.TenantIdSetup"
93
+ (onChangeView)="handleLoginTemplate($event)"
94
+ ></c8y-tenant-id-setup>
95
+
96
+ <c8y-missing-application-access
97
+ *ngSwitchCase="LOGIN_VIEWS.MissingApplicationAccess"
98
+ ></c8y-missing-application-access>
99
+
100
+ <div
101
+ class="text-center m-t-8"
102
+ *ngIf="!!(ui.state$ | async).loginExtraLink"
103
+ >
104
+ <div *ngIf="!!(ui.state$ | async).loginExtraLink.length; else singleExtraLink">
105
+ <a
106
+ class="small d-block m-t-8"
107
+ title="{{ link.label }}"
108
+ role="button"
109
+ *ngFor="let link of (ui.state$ | async).loginExtraLink"
110
+ [href]="link.url"
111
+ >
112
+ {{ link.label }}
113
+ </a>
114
+ </div>
115
+ <ng-template #singleExtraLink>
116
+ <a
117
+ class="small"
118
+ title="{{ (ui.state$ | async).loginExtraLink.label }}"
119
+ role="button"
120
+ [href]="(ui.state$ | async).loginExtraLink.url"
121
+ >
122
+ {{ (ui.state$ | async).loginExtraLink.label }}
123
+ </a>
124
+ </ng-template>
125
+ </div>
126
+ </main>
127
+ </div>
128
+ </div>
@@ -0,0 +1,136 @@
1
+ .login-panel {
2
+ display: grid;
3
+ min-height: 100vh;
4
+ &.isc8y{
5
+ @media( min-width: 768px) {
6
+ grid-template-columns: repeat(2, minmax(0, 1fr));
7
+ }
8
+ }
9
+ }
10
+
11
+ c8y-bootstrap, c8y-login, c8y-cookie-banner {
12
+ display: contents;
13
+ }
14
+
15
+ .square{
16
+ background-color: var(--c8y-palette-gray-10);
17
+ color: var(--c8y-palette-gray-90);
18
+ display: none;
19
+ img{
20
+ max-width: 100%;
21
+ }
22
+ .c8y-dark-theme & {
23
+ background-color: var(--c8y-palette-gray-100);
24
+ color: var(--c8y-palette-gray-20);
25
+ }
26
+ @media( min-width: 768px) {
27
+ display: flex;
28
+ align-items: center;
29
+ position: sticky;
30
+ top: 0;
31
+ max-height: 100vh;
32
+ }
33
+ }
34
+
35
+ .password-strength {
36
+ width: 180px;
37
+ margin-bottom: 20px;
38
+
39
+ .table & {
40
+ width: 100px;
41
+ margin-left: auto;
42
+ margin-right: auto;
43
+ }
44
+ }
45
+
46
+ .password-strength>div {
47
+ position: relative;
48
+ width: 100%;
49
+ height: 4px;
50
+ overflow: hidden;
51
+ background-color: var(--c8y-palette-gray-90);
52
+ }
53
+
54
+ .password-strength>.password-strength-label {
55
+ float: left;
56
+ color: var(--c8y-component-form-label-color, var(--c8y-root-component-form-label-color));
57
+ }
58
+
59
+ .password-green .password-bar,
60
+ .password-yellow .password-bar,
61
+ .password-red .password-bar {
62
+ position: absolute;
63
+ top: 0;
64
+ left: 0;
65
+ height: 100%;
66
+ }
67
+
68
+ .password-green {
69
+ .password-bar {
70
+ width: 100%;
71
+ background-color: var(--palette-status-success, var(--c8y-palette-status-success));
72
+ }
73
+ }
74
+
75
+ .password-yellow {
76
+ .password-bar {
77
+ width: 50%;
78
+ background-color: var(--palette-status-warning, var(--c8y-palette-status-warning));
79
+ }
80
+ }
81
+
82
+ .password-red {
83
+ .password-bar {
84
+ width: 25%;
85
+ background-color: var(--palette-status-danger, var(--c8y-palette-status-danger));;
86
+ }
87
+ }
88
+
89
+ .login-form {
90
+ height: auto;
91
+ padding: 32px;
92
+ width: 100%;
93
+ margin: auto;
94
+ @media (min-width: 768px) {
95
+ animation-delay: .5s;
96
+ max-width: 550px;
97
+ .isc8y &{
98
+ padding-left: 56px;
99
+ margin-left: 0;
100
+ }
101
+ }
102
+ }
103
+
104
+ .init-load {
105
+ height: 100vh;
106
+ margin: 0 auto;
107
+ display: flex;
108
+ flex-direction: column;
109
+ justify-content: center;
110
+ max-width: 320px;
111
+ }
112
+
113
+ .mainlogo {
114
+ background-image: var(--brand-logo-img, var(--c8y-brand-logo-img));
115
+ width: 100%;
116
+ max-width: 350px;
117
+ padding-bottom: var(--brand-logo-height, var(--c8y-brand-logo-height));
118
+ background-position: top center;
119
+ background-repeat: no-repeat;
120
+ background-size: contain;
121
+ display: block;
122
+ margin: 0 auto 32px;
123
+ &.c8y-logo{
124
+ margin: 0 0 32px;
125
+ background-position: top left;
126
+ filter: brightness(.4);
127
+ .c8y-dark-theme &{
128
+ filter: grayscale(1) contrast(0.5) brightness(1.4);
129
+ }
130
+ }
131
+ }
132
+
133
+ .mainlogo[src] {
134
+ background: none;
135
+ padding-bottom: 0;
136
+ }
@@ -0,0 +1,238 @@
1
+ import {
2
+ Component,
3
+ Input,
4
+ OnInit,
5
+ HostListener,
6
+ OnDestroy,
7
+ ViewEncapsulation
8
+ } from '@angular/core';
9
+ import { ICredentials, TenantLoginOptionType } from '@c8y/client';
10
+ import { LoginService } from './login.service';
11
+ import {
12
+ OptionsService,
13
+ AlertService,
14
+ AppStateService,
15
+ AlertOutletComponent,
16
+ C8yTranslateDirective,
17
+ C8yTranslatePipe
18
+ } from '@c8y/ngx-components';
19
+ import { gettext } from '@c8y/ngx-components/gettext';
20
+ import { LoginEvent, LoginViews, SsoData, SsoError } from './login.model';
21
+ import { CredentialsFromQueryParamsService } from './credentials-from-query-params.service';
22
+ import { CredentialsComponentParams } from './credentials-component-params';
23
+ import { NgIf, NgSwitch, NgSwitchCase, NgFor, AsyncPipe } from '@angular/common';
24
+ import { CredentialsComponent } from './credentials/credentials.component';
25
+ import { RecoverPasswordComponent } from './recover-password/recover-password.component';
26
+ import { ChangePasswordComponent } from './change-password/change-password.component';
27
+ import { TotpAuthComponent } from './totp-auth/totp-auth.component';
28
+ import { TenantIdSetupComponent } from './tenant-id-setup/tenant-id-setup.component';
29
+ import { ProvidePhoneNumberComponent } from './provide-phone-number/provide-phone-number.component';
30
+ import { SmsChallengeComponent } from './sms-challenge/sms-challenge.component';
31
+ import { MissingApplicationAccessComponent } from './missing-application-access/missing-application-access.component';
32
+
33
+ @Component({
34
+ selector: 'c8y-login',
35
+ templateUrl: './login.component.html',
36
+ styleUrls: ['./login.component.less'],
37
+ encapsulation: ViewEncapsulation.None,
38
+ standalone: true,
39
+ imports: [
40
+ NgIf,
41
+ NgSwitch,
42
+ NgSwitchCase,
43
+ CredentialsComponent,
44
+ RecoverPasswordComponent,
45
+ ChangePasswordComponent,
46
+ TotpAuthComponent,
47
+ SmsChallengeComponent,
48
+ ProvidePhoneNumberComponent,
49
+ TenantIdSetupComponent,
50
+ NgFor,
51
+ AlertOutletComponent,
52
+ AsyncPipe,
53
+ MissingApplicationAccessComponent,
54
+ C8yTranslateDirective,
55
+ C8yTranslatePipe
56
+ ]
57
+ })
58
+ export class LoginComponent implements OnInit, OnDestroy {
59
+ currentView: LoginViews = LoginViews.None;
60
+ LOGIN_VIEWS = LoginViews;
61
+ platformAnimationSrc: string | false = false;
62
+ isBrandLogoSet = false;
63
+
64
+ disabled = false;
65
+
66
+ @Input() name: string;
67
+
68
+ credentials: ICredentials = {};
69
+ loginViewParams: CredentialsComponentParams | { [key: string]: any } = {};
70
+ displayAlerts = false;
71
+ private TOKEN_PARAM = 'token';
72
+
73
+ /**
74
+ * Just DI.
75
+ */
76
+ constructor(
77
+ public loginService: LoginService,
78
+ private options: OptionsService,
79
+ private alert: AlertService,
80
+ private credentialsFromQueryParamsService: CredentialsFromQueryParamsService,
81
+ public ui: AppStateService
82
+ ) {
83
+ this.isBrandLogoSet = !!this.getValueForCSSVariable('--brand-logo-img');
84
+ this.platformAnimationSrc = this.getPlatformAnimationPath();
85
+ }
86
+
87
+ ngOnInit() {
88
+ const token = this.getParamAndClear(this.TOKEN_PARAM);
89
+ const ssoData = this.getSsoData();
90
+ if (ssoData) {
91
+ this.handleSso(ssoData);
92
+ } else if (this.loginService.isFirstLogin) {
93
+ if (!token) {
94
+ this.loginAutomatically();
95
+ } else {
96
+ this.credentials.token = token;
97
+ this.reset(false);
98
+ }
99
+ }
100
+ this.loginService.isFirstLogin = false;
101
+ }
102
+
103
+ ngOnDestroy(): void {
104
+ // make sure that we do not have any queryParameters related to credentials after logging in or even if we were already logged in.
105
+ this.credentialsFromQueryParamsService.removeCredentialsFromQueryParams();
106
+ }
107
+
108
+ handleLoginTemplate(event: LoginEvent) {
109
+ this.currentView = event.view;
110
+ this.credentials = event.credentials || {};
111
+ this.loginViewParams = event.loginViewParams || {};
112
+ }
113
+
114
+ @HostListener('keyup', ['$event']) onkeyup(event: KeyboardEvent) {
115
+ if (event.key !== 'Enter') {
116
+ this.loginService.cleanMessages();
117
+ }
118
+ }
119
+
120
+ reset(missingPermissions: boolean) {
121
+ if (missingPermissions) {
122
+ this.handleLoginTemplate({ view: LoginViews.MissingApplicationAccess });
123
+ return;
124
+ }
125
+ this.loginService.reset();
126
+ this.setView();
127
+ this.loginService.cleanMessages();
128
+ }
129
+
130
+ private getPlatformAnimationPath() {
131
+ const defaultPath = './platform-animation.svg';
132
+
133
+ const platformAnimationImagePath = this.getValueForCSSVariable(
134
+ '--login-platform-animation-img'
135
+ );
136
+ if (platformAnimationImagePath) {
137
+ return platformAnimationImagePath;
138
+ }
139
+
140
+ // in case we have a brand logo image, we don't want to show the platform animation
141
+ if (this.isBrandLogoSet) {
142
+ return false;
143
+ }
144
+
145
+ return defaultPath;
146
+ }
147
+
148
+ private getValueForCSSVariable(variableName: string): string {
149
+ const rootStyles = getComputedStyle(document.body);
150
+
151
+ // getPropertyValue might not be available in e.g. unit tests
152
+ if (rootStyles && typeof rootStyles.getPropertyValue === 'function') {
153
+ const brandLogo = rootStyles?.getPropertyValue(variableName).trim();
154
+ return brandLogo;
155
+ }
156
+ return '';
157
+ }
158
+
159
+ private async loginAutomatically() {
160
+ this.loginService.automaticLoginInProgress$.next(true);
161
+ try {
162
+ const result = await this.loginService.login();
163
+ if (result) {
164
+ return;
165
+ }
166
+ this.reset(true);
167
+ } catch (e) {
168
+ await this.loginService.clearCookies();
169
+ const preferredLoginOptionType = this.loginService.loginMode.type;
170
+ if (preferredLoginOptionType === TenantLoginOptionType.OAUTH2) {
171
+ this.loginService.redirectToOauth();
172
+ } else {
173
+ this.reset(false);
174
+ if (
175
+ preferredLoginOptionType === TenantLoginOptionType.OAUTH2_INTERNAL &&
176
+ window.location.protocol !== 'https:'
177
+ ) {
178
+ this.alert.danger(gettext('Current login mode only supports HTTPS.'));
179
+ } else if (e.res && e.res.status === 403) {
180
+ this.alert.addServerFailure(e);
181
+ }
182
+ }
183
+ }
184
+ this.loginService.automaticLoginInProgress$.next(false);
185
+ }
186
+
187
+ private setView() {
188
+ if (this.credentials && this.credentials.token) {
189
+ this.handleLoginTemplate({ view: LoginViews.ChangePassword, credentials: this.credentials });
190
+ } else if (this.loginService.showTenantSetup()) {
191
+ this.handleLoginTemplate({ view: LoginViews.TenantIdSetup });
192
+ } else {
193
+ this.handleLoginTemplate({ view: LoginViews.Credentials });
194
+ }
195
+ }
196
+
197
+ private getParamAndClear(paramName: string): string | undefined {
198
+ const paramValue = this.options.get<string>(paramName);
199
+ if (paramValue) {
200
+ this.options.set(paramName, undefined); // only use once
201
+ }
202
+ return paramValue;
203
+ }
204
+
205
+ private getSsoData(): SsoData | SsoError | false {
206
+ const code = this.getParamAndClear('code');
207
+ const sessionState = this.getParamAndClear('session_state');
208
+ if (code) {
209
+ return { sessionState, code };
210
+ }
211
+
212
+ const ssoError = this.getParamAndClear('error');
213
+ const ssoErrorDescription = this.getParamAndClear('error_description');
214
+ if (ssoError && ssoErrorDescription) {
215
+ return { ssoError, ssoErrorDescription };
216
+ }
217
+ return false;
218
+ }
219
+
220
+ private handleSso(ssoData: SsoData | SsoError) {
221
+ if ('ssoError' in ssoData) {
222
+ this.loginService.showSsoError(
223
+ decodeURIComponent(ssoData.ssoErrorDescription).replace(/\+/g, '%20')
224
+ );
225
+ this.reset(false);
226
+ } else {
227
+ this.loginService
228
+ .loginBySso(ssoData)
229
+ .then(() => this.loginService.login())
230
+ .catch(e => {
231
+ this.reset(false);
232
+ if (e.res?.status) {
233
+ this.alert.addServerFailure(e);
234
+ }
235
+ });
236
+ }
237
+ }
238
+ }
@@ -0,0 +1,36 @@
1
+ import { ICredentials } from '@c8y/client';
2
+ import { CredentialsComponentParams } from './credentials-component-params';
3
+
4
+ export interface LoginMessage {
5
+ message: string;
6
+ type: string;
7
+ }
8
+
9
+ export interface LoginEvent {
10
+ view: LoginViews;
11
+ credentials?: ICredentials;
12
+ loginViewParams?: CredentialsComponentParams | { [key: string]: any };
13
+ }
14
+
15
+ export enum LoginViews {
16
+ None = 'NONE',
17
+ Credentials = 'CREDENTIALS',
18
+ RecoverPassword = 'RECOVER_PASSWORD',
19
+ SmsChallenge = 'SMS_CHALLENGE',
20
+ ChangePassword = 'CHANGE_PASSWORD',
21
+ TotpChallenge = 'TOTP_CHALLENGE',
22
+ TotpSetup = 'TOTP_SETUP',
23
+ ProvidePhoneNumber = 'PROVIDE_PHONE_NUMBER',
24
+ TenantIdSetup = 'TENANT_ID_SETUP',
25
+ MissingApplicationAccess = 'MISSING_APPLICATION_ACCESS'
26
+ }
27
+
28
+ export type SsoData = {
29
+ code: string;
30
+ sessionState?: string;
31
+ };
32
+
33
+ export type SsoError = {
34
+ ssoError: string;
35
+ ssoErrorDescription: string;
36
+ };