@c8y/login 1024.5.1 → 1024.9.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 +7 -7
- package/src/app/login/binary-download/binary-download.component.html +105 -0
- package/src/app/login/binary-download/binary-download.component.ts +96 -0
- package/src/app/login/credentials/credentials.component.html +132 -132
- package/src/app/login/credentials/credentials.component.ts +2 -3
- package/src/app/login/login.component.html +27 -12
- package/src/app/login/login.component.scss +18 -0
- package/src/app/login/login.component.ts +22 -2
- package/src/app/login/login.model.ts +2 -1
- package/src/app/login/login.service.ts +71 -1
- package/src/app/login/sms-challenge/sms-challenge.component.ts +2 -2
- package/src/app/login/totp-auth/totp-auth.component.html +3 -3
- package/src/app/login/totp-auth/totp-auth.component.ts +2 -3
package/package.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c8y/login",
|
|
3
|
-
"version": "1024.
|
|
3
|
+
"version": "1024.9.0",
|
|
4
4
|
"description": "This package is used to scaffold a login application for Cumulocity IoT.",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@angular/animations": "^21.2.0",
|
|
7
7
|
"@angular/cdk": "^21.2.0",
|
|
8
|
-
"@c8y/bootstrap": "1024.
|
|
9
|
-
"@c8y/client": "1024.
|
|
10
|
-
"@c8y/ngx-components": "1024.
|
|
11
|
-
"@c8y/style": "1024.
|
|
8
|
+
"@c8y/bootstrap": "1024.9.0",
|
|
9
|
+
"@c8y/client": "1024.9.0",
|
|
10
|
+
"@c8y/ngx-components": "1024.9.0",
|
|
11
|
+
"@c8y/style": "1024.9.0",
|
|
12
12
|
"monaco-editor": "~0.53.0",
|
|
13
13
|
"ngx-bootstrap": "21.0.1",
|
|
14
14
|
"rxjs": "7.8.2",
|
|
15
15
|
"zone.js": "~0.15.1"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@c8y/devkit": "1024.
|
|
19
|
-
"@c8y/options": "1024.
|
|
18
|
+
"@c8y/devkit": "1024.9.0",
|
|
19
|
+
"@c8y/options": "1024.9.0"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"@angular/common": ">=21 <22"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<div class="download-slot">
|
|
2
|
+
<h3 class="m-b-8">
|
|
3
|
+
@switch (state()) {
|
|
4
|
+
@case ('ready') {
|
|
5
|
+
{{ 'Your file is ready' | translate }}
|
|
6
|
+
}
|
|
7
|
+
@case ('error') {
|
|
8
|
+
<i
|
|
9
|
+
class="text-danger m-r-8"
|
|
10
|
+
c8yIcon="exclamation-circle"
|
|
11
|
+
></i
|
|
12
|
+
>{{ 'The file could not be loaded' | translate }}
|
|
13
|
+
}
|
|
14
|
+
@default {
|
|
15
|
+
{{ 'File download' | translate }}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
</h3>
|
|
19
|
+
@switch (state()) {
|
|
20
|
+
@case ('loading') {
|
|
21
|
+
<c8y-loading [attr.aria-label]="'Loading' | translate"></c8y-loading>
|
|
22
|
+
}
|
|
23
|
+
@case ('error') {
|
|
24
|
+
{{ errorMessage() }}
|
|
25
|
+
}
|
|
26
|
+
@case ('ready') {
|
|
27
|
+
@let mo = managedObject();
|
|
28
|
+
<p class="m-b-24">
|
|
29
|
+
{{ 'Click "Download" to save it.' | translate }}
|
|
30
|
+
</p>
|
|
31
|
+
<div class="d-flex a-i-start gap-16 m-b-24">
|
|
32
|
+
<i
|
|
33
|
+
class="icon-32 text-muted"
|
|
34
|
+
[c8yIcon]="{ name: mo?.name } | fileIcon"
|
|
35
|
+
aria-hidden="true"
|
|
36
|
+
></i>
|
|
37
|
+
<div class="flex-grow min-width-0">
|
|
38
|
+
<p class="text-truncate m-b-0">
|
|
39
|
+
{{ mo?.name }}
|
|
40
|
+
</p>
|
|
41
|
+
<p>
|
|
42
|
+
<small class="text-muted">
|
|
43
|
+
<em>{{ 'Size' | translate }}:</em> {{ mo?.length | bytes }} |
|
|
44
|
+
<em>{{ 'Type' | translate }}:</em> {{ mo?.contentType }}
|
|
45
|
+
</small>
|
|
46
|
+
</p>
|
|
47
|
+
<p>
|
|
48
|
+
<small class="text-muted">
|
|
49
|
+
<em>{{ 'Last modified' | translate }}:</em> {{ mo?.lastUpdated | c8yDate }}
|
|
50
|
+
</small>
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<button
|
|
55
|
+
class="btn btn-primary flex-no-shrink"
|
|
56
|
+
[title]="'Download' | translate"
|
|
57
|
+
type="button"
|
|
58
|
+
[attr.data-cy]="'c8y-login--download-button'"
|
|
59
|
+
[disabled]="isDownloading()"
|
|
60
|
+
(click)="download()"
|
|
61
|
+
>
|
|
62
|
+
@if (isDownloading()) {
|
|
63
|
+
<i
|
|
64
|
+
class="icon-16 animated rotateOut"
|
|
65
|
+
[c8yIcon]="'circle-o-notch'"
|
|
66
|
+
aria-hidden="true"
|
|
67
|
+
></i>
|
|
68
|
+
} @else {
|
|
69
|
+
<i
|
|
70
|
+
[c8yIcon]="'download'"
|
|
71
|
+
aria-hidden="true"
|
|
72
|
+
></i>
|
|
73
|
+
}
|
|
74
|
+
{{ 'Download' | translate }}
|
|
75
|
+
</button>
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
</div>
|
|
79
|
+
@if (state() !== 'loading') {
|
|
80
|
+
@if (state() === 'ready') {
|
|
81
|
+
<p class="text-muted m-t-24 m-b-8">
|
|
82
|
+
{{
|
|
83
|
+
'You can also open one of your applications below — now, or after downloading the file.'
|
|
84
|
+
| translate
|
|
85
|
+
}}
|
|
86
|
+
</p>
|
|
87
|
+
}
|
|
88
|
+
@if (state() === 'error') {
|
|
89
|
+
<p class="text-muted m-t-24 m-b-8">
|
|
90
|
+
{{ 'You can also open one of your applications below.' | translate }}
|
|
91
|
+
</p>
|
|
92
|
+
}
|
|
93
|
+
<c8y-app-switcher-inline></c8y-app-switcher-inline>
|
|
94
|
+
<button
|
|
95
|
+
class="btn btn-default btn-lg btn-block m-t-16"
|
|
96
|
+
[title]="'Logout' | translate"
|
|
97
|
+
(click)="logout()"
|
|
98
|
+
>
|
|
99
|
+
<i
|
|
100
|
+
[c8yIcon]="'logout'"
|
|
101
|
+
aria-hidden="true"
|
|
102
|
+
></i>
|
|
103
|
+
{{ 'Logout' | translate }}
|
|
104
|
+
</button>
|
|
105
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, inject, input, OnInit, signal } from '@angular/core';
|
|
2
|
+
import { DOCUMENT } from '@angular/common';
|
|
3
|
+
import { IManagedObject, InventoryBinaryService, InventoryService } from '@c8y/client';
|
|
4
|
+
import {
|
|
5
|
+
AlertService,
|
|
6
|
+
AppSwitcherInlineComponent,
|
|
7
|
+
BytesPipe,
|
|
8
|
+
C8yTranslatePipe,
|
|
9
|
+
DatePipe,
|
|
10
|
+
GenericFileIconPipe,
|
|
11
|
+
IconDirective,
|
|
12
|
+
LoadingComponent
|
|
13
|
+
} from '@c8y/ngx-components';
|
|
14
|
+
import { gettext } from '@c8y/ngx-components/gettext';
|
|
15
|
+
import { TranslateService } from '@ngx-translate/core';
|
|
16
|
+
import { LoginService } from '../login.service';
|
|
17
|
+
|
|
18
|
+
type DownloadState = 'loading' | 'ready' | 'error';
|
|
19
|
+
|
|
20
|
+
@Component({
|
|
21
|
+
selector: 'c8y-binary-download',
|
|
22
|
+
templateUrl: './binary-download.component.html',
|
|
23
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
24
|
+
imports: [
|
|
25
|
+
AppSwitcherInlineComponent,
|
|
26
|
+
BytesPipe,
|
|
27
|
+
C8yTranslatePipe,
|
|
28
|
+
DatePipe,
|
|
29
|
+
GenericFileIconPipe,
|
|
30
|
+
IconDirective,
|
|
31
|
+
LoadingComponent
|
|
32
|
+
]
|
|
33
|
+
})
|
|
34
|
+
export class BinaryDownloadComponent implements OnInit {
|
|
35
|
+
readonly binaryId = input.required<string>();
|
|
36
|
+
|
|
37
|
+
protected readonly state = signal<DownloadState>('loading');
|
|
38
|
+
protected readonly isDownloading = signal(false);
|
|
39
|
+
protected readonly managedObject = signal<IManagedObject | null>(null);
|
|
40
|
+
protected readonly errorMessage = signal<string | null>(null);
|
|
41
|
+
|
|
42
|
+
private readonly LOAD_ERROR = gettext(
|
|
43
|
+
'The link might be mistyped or outdated, or the file might have been deleted. Check the link and try again, or contact the person who shared it with you.'
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
private readonly inventory = inject(InventoryService);
|
|
47
|
+
private readonly inventoryBinary = inject(InventoryBinaryService);
|
|
48
|
+
private readonly alert = inject(AlertService);
|
|
49
|
+
private readonly translate = inject(TranslateService);
|
|
50
|
+
private readonly loginService = inject(LoginService);
|
|
51
|
+
private readonly document = inject(DOCUMENT);
|
|
52
|
+
|
|
53
|
+
async ngOnInit() {
|
|
54
|
+
try {
|
|
55
|
+
const { data } = await this.inventory.detail(this.binaryId(), { withChildren: false });
|
|
56
|
+
if (data['c8y_IsBinary'] == null) {
|
|
57
|
+
this.errorMessage.set(this.translate.instant(this.LOAD_ERROR));
|
|
58
|
+
this.state.set('error');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
this.managedObject.set(data);
|
|
62
|
+
this.state.set('ready');
|
|
63
|
+
} catch (ex) {
|
|
64
|
+
console.error('Failed to load binary for download:', ex);
|
|
65
|
+
this.errorMessage.set(this.translate.instant(this.LOAD_ERROR));
|
|
66
|
+
this.state.set('error');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
protected async download() {
|
|
71
|
+
this.alert.clearAll();
|
|
72
|
+
this.isDownloading.set(true);
|
|
73
|
+
try {
|
|
74
|
+
const mo = this.managedObject();
|
|
75
|
+
const res = await this.inventoryBinary.download(mo.id);
|
|
76
|
+
const blob = await res.blob();
|
|
77
|
+
const url = URL.createObjectURL(blob);
|
|
78
|
+
const link = this.document.createElement('a');
|
|
79
|
+
link.href = url;
|
|
80
|
+
link.download = mo['name'] ?? String(mo.id);
|
|
81
|
+
this.document.body.appendChild(link);
|
|
82
|
+
link.click();
|
|
83
|
+
this.document.body.removeChild(link);
|
|
84
|
+
URL.revokeObjectURL(url);
|
|
85
|
+
this.alert.add({ text: gettext('File downloaded.'), type: 'success', timeout: 10_000 });
|
|
86
|
+
} catch (ex) {
|
|
87
|
+
this.alert.addServerFailure(ex);
|
|
88
|
+
} finally {
|
|
89
|
+
this.isDownloading.set(false);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
protected logout() {
|
|
94
|
+
this.loginService.logout();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -1,141 +1,141 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
>
|
|
6
|
-
<button
|
|
7
|
-
class="btn btn-default btn-block btn-lg form-group m-t-8"
|
|
8
|
-
title="{{ oauthOptions.buttonName | translate }}"
|
|
9
|
-
type="button"
|
|
10
|
-
(click)="redirectToOauth()"
|
|
11
|
-
>
|
|
12
|
-
<i
|
|
13
|
-
class="pull-left"
|
|
14
|
-
[c8yIcon]="'sign-in'"
|
|
15
|
-
></i>
|
|
16
|
-
{{ oauthOptions.buttonName | translate }}
|
|
17
|
-
</button>
|
|
18
|
-
</div>
|
|
19
|
-
|
|
20
|
-
<form
|
|
21
|
-
class="loginForm"
|
|
22
|
-
(ngSubmit)="login()"
|
|
23
|
-
#loginForm="ngForm"
|
|
24
|
-
*ngIf="showLoginForm"
|
|
25
|
-
novalidate
|
|
26
|
-
>
|
|
27
|
-
<span
|
|
28
|
-
class="legend form-block center"
|
|
29
|
-
*ngIf="!(oauthOptions.initRequest && oauthOptions.visibleOnLoginPage); else orLegend"
|
|
30
|
-
translate
|
|
1
|
+
@if (oauthOptions.initRequest && oauthOptions.visibleOnLoginPage) {
|
|
2
|
+
<div
|
|
3
|
+
class="m-b-40"
|
|
4
|
+
id="oauth"
|
|
31
5
|
>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class="legend form-block center"
|
|
38
|
-
translate
|
|
6
|
+
<button
|
|
7
|
+
class="btn btn-default btn-block btn-lg form-group m-t-8"
|
|
8
|
+
title="{{ oauthOptions.buttonName | translate }}"
|
|
9
|
+
type="button"
|
|
10
|
+
(click)="redirectToOauth()"
|
|
39
11
|
>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
12
|
+
<i
|
|
13
|
+
class="pull-left"
|
|
14
|
+
[c8yIcon]="'sign-in'"
|
|
15
|
+
></i>
|
|
16
|
+
{{ oauthOptions.buttonName | translate }}
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
19
|
+
}
|
|
43
20
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
for="tenant"
|
|
51
|
-
translate
|
|
52
|
-
>
|
|
53
|
-
Tenant ID
|
|
54
|
-
</label>
|
|
55
|
-
<input
|
|
56
|
-
class="form-control"
|
|
57
|
-
id="tenant"
|
|
58
|
-
placeholder="{{ 'e.g.' | translate }} t12345"
|
|
59
|
-
name="tenant"
|
|
60
|
-
type="text"
|
|
61
|
-
required
|
|
62
|
-
[(ngModel)]="model.tenant"
|
|
63
|
-
#tenant="ngModel"
|
|
64
|
-
autocapitalize="off"
|
|
65
|
-
autocorrect="off"
|
|
66
|
-
placeholder-no-required-hint
|
|
67
|
-
[readonly]="loginViewParams.disableTenant"
|
|
68
|
-
/>
|
|
69
|
-
</c8y-form-group>
|
|
70
|
-
<c8y-form-group class="m-b-40">
|
|
71
|
-
<label
|
|
72
|
-
for="user"
|
|
73
|
-
translate
|
|
74
|
-
>
|
|
75
|
-
Username
|
|
76
|
-
</label>
|
|
77
|
-
<input
|
|
78
|
-
class="form-control"
|
|
79
|
-
id="user"
|
|
80
|
-
placeholder="{{ 'e.g. joe or joe.doe@example.com`LOCALIZE`' | translate }}"
|
|
81
|
-
name="user"
|
|
82
|
-
type="text"
|
|
83
|
-
required
|
|
84
|
-
[(ngModel)]="model.user"
|
|
85
|
-
#user="ngModel"
|
|
86
|
-
autocapitalize="off"
|
|
87
|
-
autocorrect="off"
|
|
88
|
-
placeholder-no-required-hint
|
|
89
|
-
/>
|
|
90
|
-
</c8y-form-group>
|
|
91
|
-
<c8y-form-group class="m-b-40">
|
|
92
|
-
<label
|
|
93
|
-
for="password"
|
|
94
|
-
translate
|
|
95
|
-
>
|
|
96
|
-
Password
|
|
97
|
-
</label>
|
|
98
|
-
<c8y-password-input
|
|
99
|
-
name="password"
|
|
100
|
-
required
|
|
101
|
-
[id]="'password'"
|
|
102
|
-
[(ngModel)]="model.password"
|
|
103
|
-
[autocomplete]="'off'"
|
|
104
|
-
></c8y-password-input>
|
|
105
|
-
</c8y-form-group>
|
|
106
|
-
<div
|
|
107
|
-
class="form-group "
|
|
108
|
-
*ngIf="showBasicAuth"
|
|
21
|
+
@if (showLoginForm) {
|
|
22
|
+
<form
|
|
23
|
+
class="loginForm"
|
|
24
|
+
(ngSubmit)="login()"
|
|
25
|
+
#loginForm="ngForm"
|
|
26
|
+
novalidate
|
|
109
27
|
>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
28
|
+
@if (!(oauthOptions.initRequest && oauthOptions.visibleOnLoginPage)) {
|
|
29
|
+
<span
|
|
30
|
+
class="legend form-block center"
|
|
31
|
+
translate
|
|
32
|
+
>
|
|
33
|
+
Login
|
|
34
|
+
</span>
|
|
35
|
+
} @else {
|
|
36
|
+
<div
|
|
37
|
+
class="legend form-block center"
|
|
38
|
+
translate
|
|
39
|
+
>
|
|
40
|
+
or enter your credentials`prefixed by login button`
|
|
41
|
+
</div>
|
|
42
|
+
}
|
|
43
|
+
@if (showTenant) {
|
|
44
|
+
<c8y-form-group
|
|
45
|
+
class="tenantField m-b-40"
|
|
46
|
+
id="tenantField"
|
|
47
|
+
>
|
|
48
|
+
<label
|
|
49
|
+
for="tenant"
|
|
50
|
+
translate
|
|
51
|
+
>
|
|
52
|
+
Tenant ID
|
|
53
|
+
</label>
|
|
54
|
+
<input
|
|
55
|
+
class="form-control"
|
|
56
|
+
id="tenant"
|
|
57
|
+
placeholder="{{ 'e.g.' | translate }} t12345"
|
|
58
|
+
name="tenant"
|
|
59
|
+
type="text"
|
|
60
|
+
required
|
|
61
|
+
[(ngModel)]="model.tenant"
|
|
62
|
+
#tenant="ngModel"
|
|
63
|
+
autocapitalize="off"
|
|
64
|
+
autocorrect="off"
|
|
65
|
+
placeholder-no-required-hint
|
|
66
|
+
[readonly]="loginViewParams.disableTenant"
|
|
67
|
+
/>
|
|
68
|
+
</c8y-form-group>
|
|
69
|
+
}
|
|
70
|
+
<c8y-form-group class="m-b-40">
|
|
71
|
+
<label
|
|
72
|
+
for="user"
|
|
73
|
+
translate
|
|
74
|
+
>
|
|
75
|
+
Username
|
|
76
|
+
</label>
|
|
114
77
|
<input
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
78
|
+
class="form-control"
|
|
79
|
+
id="user"
|
|
80
|
+
placeholder="{{ 'e.g. joe or joe.doe@example.com`LOCALIZE`' | translate }}"
|
|
81
|
+
name="user"
|
|
82
|
+
type="text"
|
|
83
|
+
required
|
|
84
|
+
[(ngModel)]="model.user"
|
|
85
|
+
#user="ngModel"
|
|
86
|
+
autocapitalize="off"
|
|
87
|
+
autocorrect="off"
|
|
88
|
+
placeholder-no-required-hint
|
|
118
89
|
/>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
90
|
+
</c8y-form-group>
|
|
91
|
+
<c8y-form-group class="m-b-40">
|
|
92
|
+
<label
|
|
93
|
+
for="password"
|
|
94
|
+
translate
|
|
95
|
+
>
|
|
96
|
+
Password
|
|
97
|
+
</label>
|
|
98
|
+
<c8y-password-input
|
|
99
|
+
name="password"
|
|
100
|
+
required
|
|
101
|
+
[id]="'password'"
|
|
102
|
+
[(ngModel)]="model.password"
|
|
103
|
+
[autocomplete]="'off'"
|
|
104
|
+
></c8y-password-input>
|
|
105
|
+
</c8y-form-group>
|
|
106
|
+
@if (showBasicAuth) {
|
|
107
|
+
<div class="form-group">
|
|
108
|
+
<label
|
|
109
|
+
class="c8y-checkbox"
|
|
110
|
+
title="{{ 'Remember me' | translate }}"
|
|
111
|
+
>
|
|
112
|
+
<input
|
|
113
|
+
name="remember"
|
|
114
|
+
type="checkbox"
|
|
115
|
+
[(ngModel)]="loginService.rememberMe"
|
|
116
|
+
/>
|
|
117
|
+
<span></span>
|
|
118
|
+
<span>{{ 'Remember me' | translate }}</span>
|
|
119
|
+
</label>
|
|
120
|
+
</div>
|
|
121
|
+
}
|
|
132
122
|
<button
|
|
133
|
-
class="btn btn-
|
|
134
|
-
title="{{ '
|
|
135
|
-
type="
|
|
136
|
-
|
|
123
|
+
class="btn btn-primary btn-lg btn-block form-group"
|
|
124
|
+
title="{{ 'Log in' | translate }}"
|
|
125
|
+
type="submit"
|
|
126
|
+
[disabled]="!loginForm.form.valid || isLoading"
|
|
137
127
|
>
|
|
138
|
-
{{ '
|
|
128
|
+
{{ 'Log in' | translate }}
|
|
139
129
|
</button>
|
|
140
|
-
|
|
141
|
-
|
|
130
|
+
<div class="text-center m-t-8">
|
|
131
|
+
<button
|
|
132
|
+
class="btn btn-link btn-sm"
|
|
133
|
+
title="{{ 'Forgot password?' | translate }}"
|
|
134
|
+
type="button"
|
|
135
|
+
(click)="onChangeView.emit({ view: LOGIN_VIEWS.RecoverPassword })"
|
|
136
|
+
>
|
|
137
|
+
{{ 'Forgot password?' | translate }}
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
</form>
|
|
141
|
+
}
|
|
@@ -14,7 +14,7 @@ import { gettext } from '@c8y/ngx-components/gettext';
|
|
|
14
14
|
import { LoginEvent, LoginViews } from '../login.model';
|
|
15
15
|
import { CredentialsFromQueryParamsService } from '../credentials-from-query-params.service';
|
|
16
16
|
import { CredentialsComponentParams } from '../credentials-component-params';
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
import { FormsModule } from '@angular/forms';
|
|
19
19
|
|
|
20
20
|
@Component({
|
|
@@ -23,7 +23,6 @@ import { FormsModule } from '@angular/forms';
|
|
|
23
23
|
styles: [],
|
|
24
24
|
standalone: true,
|
|
25
25
|
imports: [
|
|
26
|
-
NgIf,
|
|
27
26
|
IconDirective,
|
|
28
27
|
FormsModule,
|
|
29
28
|
C8yTranslateDirective,
|
|
@@ -85,7 +84,7 @@ export class CredentialsComponent implements OnInit {
|
|
|
85
84
|
const basicAuth = this.loginService.useBasicAuth(this.model);
|
|
86
85
|
const hasPermission = await this.loginService.login(basicAuth, this.model);
|
|
87
86
|
if (!hasPermission) {
|
|
88
|
-
this.onChangeView.emit({ view:
|
|
87
|
+
this.onChangeView.emit({ view: this.loginService.getViewForSuppressedRedirect() });
|
|
89
88
|
}
|
|
90
89
|
} catch (e) {
|
|
91
90
|
if (e.res && e.res.headers && e.res.headers.get(this.PASSWORD_RESET_HEADER_NAME)) {
|
|
@@ -16,18 +16,30 @@
|
|
|
16
16
|
@switch (currentView) {
|
|
17
17
|
@case (LOGIN_VIEWS.Credentials) {
|
|
18
18
|
<span class="{{ platformAnimationSrc ? '' : 'text-center' }}">
|
|
19
|
-
|
|
20
|
-
class="
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
@if (downloadBinaryId) {
|
|
20
|
+
<div class="download-slot">
|
|
21
|
+
<h3 class="m-b-8 d-flex a-i-center">
|
|
22
|
+
<i class="dlt-c8y-icon-info-circle text-info m-r-8"></i>
|
|
23
|
+
{{ 'File download' | translate }}
|
|
24
|
+
</h3>
|
|
25
|
+
<p class="text-16">
|
|
26
|
+
{{ 'A file has been shared with you, log in to download it.' | translate }}
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
} @else {
|
|
30
|
+
<h2
|
|
31
|
+
class="m-b-8"
|
|
32
|
+
translate
|
|
33
|
+
>
|
|
34
|
+
Welcome
|
|
35
|
+
</h2>
|
|
36
|
+
<p
|
|
37
|
+
class="text-16 m-b-40"
|
|
38
|
+
translate
|
|
39
|
+
>
|
|
40
|
+
Log in to access your IoT platform.
|
|
41
|
+
</p>
|
|
42
|
+
}
|
|
31
43
|
</span>
|
|
32
44
|
<c8y-credentials
|
|
33
45
|
(onChangeView)="handleLoginTemplate($event)"
|
|
@@ -92,6 +104,9 @@
|
|
|
92
104
|
@case (LOGIN_VIEWS.TenantIdSetup) {
|
|
93
105
|
<c8y-tenant-id-setup (onChangeView)="handleLoginTemplate($event)"></c8y-tenant-id-setup>
|
|
94
106
|
}
|
|
107
|
+
@case (LOGIN_VIEWS.Download) {
|
|
108
|
+
<c8y-binary-download [binaryId]="downloadBinaryId!"></c8y-binary-download>
|
|
109
|
+
}
|
|
95
110
|
@case (LOGIN_VIEWS.MissingApplicationAccess) {
|
|
96
111
|
<span class="{{ platformAnimationSrc ? '' : 'text-center' }}">
|
|
97
112
|
<h3
|
|
@@ -101,6 +101,7 @@ c8y-bootstrap, c8y-login, c8y-cookie-banner {
|
|
|
101
101
|
padding: 32px;
|
|
102
102
|
width: 100%;
|
|
103
103
|
margin: auto;
|
|
104
|
+
min-width: 0;
|
|
104
105
|
@media (min-width: 768px) {
|
|
105
106
|
animation-delay: .5s;
|
|
106
107
|
max-width: 550px;
|
|
@@ -153,3 +154,20 @@ c8y-bootstrap, c8y-login, c8y-cookie-banner {
|
|
|
153
154
|
background: none;
|
|
154
155
|
padding-bottom: 0;
|
|
155
156
|
}
|
|
157
|
+
|
|
158
|
+
.gap-16{
|
|
159
|
+
gap: 16px;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
.download-slot{
|
|
164
|
+
margin-bottom: 24px;
|
|
165
|
+
padding: 24px;
|
|
166
|
+
border-radius: 8px;
|
|
167
|
+
box-shadow: var(--c8y-elevation-sm);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.text-truncate{
|
|
171
|
+
text-overflow: ellipsis;
|
|
172
|
+
overflow: hidden;
|
|
173
|
+
}
|
|
@@ -29,6 +29,7 @@ import { TenantIdSetupComponent } from './tenant-id-setup/tenant-id-setup.compon
|
|
|
29
29
|
import { ProvidePhoneNumberComponent } from './provide-phone-number/provide-phone-number.component';
|
|
30
30
|
import { SmsChallengeComponent } from './sms-challenge/sms-challenge.component';
|
|
31
31
|
import { MissingApplicationAccessComponent } from './missing-application-access/missing-application-access.component';
|
|
32
|
+
import { BinaryDownloadComponent } from './binary-download/binary-download.component';
|
|
32
33
|
|
|
33
34
|
@Component({
|
|
34
35
|
selector: 'c8y-login',
|
|
@@ -47,6 +48,7 @@ import { MissingApplicationAccessComponent } from './missing-application-access/
|
|
|
47
48
|
AlertOutletComponent,
|
|
48
49
|
AsyncPipe,
|
|
49
50
|
MissingApplicationAccessComponent,
|
|
51
|
+
BinaryDownloadComponent,
|
|
50
52
|
C8yTranslateDirective,
|
|
51
53
|
C8yTranslatePipe
|
|
52
54
|
]
|
|
@@ -65,7 +67,9 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|
|
65
67
|
loginViewParams: CredentialsComponentParams | { [key: string]: any } = {};
|
|
66
68
|
recoverPasswordData: LoginEvent['recoverPasswordData'];
|
|
67
69
|
displayAlerts = false;
|
|
68
|
-
|
|
70
|
+
downloadBinaryId: string | null = null;
|
|
71
|
+
|
|
72
|
+
private readonly TOKEN_PARAM = 'token';
|
|
69
73
|
|
|
70
74
|
/**
|
|
71
75
|
* Just DI.
|
|
@@ -82,6 +86,8 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
async ngOnInit() {
|
|
89
|
+
this.downloadBinaryId = this.loginService.initDownloadBinaryId();
|
|
90
|
+
|
|
85
91
|
const token = this.getParamAndClear(this.TOKEN_PARAM);
|
|
86
92
|
const ssoData = this.getSsoData();
|
|
87
93
|
if (ssoData) {
|
|
@@ -103,6 +109,10 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
handleLoginTemplate(event: LoginEvent) {
|
|
112
|
+
if (event.view === LoginViews.Download) {
|
|
113
|
+
this.enterDownloadView();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
106
116
|
this.currentView = event.view;
|
|
107
117
|
this.credentials = event.credentials || {};
|
|
108
118
|
this.loginViewParams = event.loginViewParams || {};
|
|
@@ -117,7 +127,7 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|
|
117
127
|
|
|
118
128
|
async reset(missingPermissions: boolean) {
|
|
119
129
|
if (missingPermissions) {
|
|
120
|
-
this.handleLoginTemplate({ view:
|
|
130
|
+
this.handleLoginTemplate({ view: this.loginService.getViewForSuppressedRedirect() });
|
|
121
131
|
return;
|
|
122
132
|
}
|
|
123
133
|
this.loginService.reset();
|
|
@@ -208,6 +218,11 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|
|
208
218
|
}
|
|
209
219
|
}
|
|
210
220
|
|
|
221
|
+
private enterDownloadView() {
|
|
222
|
+
this.currentView = LoginViews.Download;
|
|
223
|
+
this.loginService.clearPendingDownload();
|
|
224
|
+
}
|
|
225
|
+
|
|
211
226
|
private getParamAndClear(paramName: string): string | undefined {
|
|
212
227
|
const paramValue = this.options.get<string>(paramName);
|
|
213
228
|
if (paramValue) {
|
|
@@ -241,6 +256,11 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|
|
241
256
|
this.loginService
|
|
242
257
|
.loginBySso(ssoData)
|
|
243
258
|
.then(() => this.loginService.login())
|
|
259
|
+
.then(hasRedirected => {
|
|
260
|
+
if (!hasRedirected && this.downloadBinaryId) {
|
|
261
|
+
this.enterDownloadView();
|
|
262
|
+
}
|
|
263
|
+
})
|
|
244
264
|
.catch(e => {
|
|
245
265
|
this.reset(false);
|
|
246
266
|
if (e.res?.status) {
|
|
@@ -27,7 +27,8 @@ export enum LoginViews {
|
|
|
27
27
|
TotpSetup = 'TOTP_SETUP',
|
|
28
28
|
ProvidePhoneNumber = 'PROVIDE_PHONE_NUMBER',
|
|
29
29
|
TenantIdSetup = 'TENANT_ID_SETUP',
|
|
30
|
-
MissingApplicationAccess = 'MISSING_APPLICATION_ACCESS'
|
|
30
|
+
MissingApplicationAccess = 'MISSING_APPLICATION_ACCESS',
|
|
31
|
+
Download = 'DOWNLOAD'
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export type SsoData = {
|
|
@@ -21,7 +21,8 @@ import { switchMap } from 'rxjs/operators';
|
|
|
21
21
|
import { BehaviorSubject, EMPTY } from 'rxjs';
|
|
22
22
|
import { isEmpty } from 'lodash-es';
|
|
23
23
|
import { TranslateService } from '@ngx-translate/core';
|
|
24
|
-
import {
|
|
24
|
+
import { Router } from '@angular/router';
|
|
25
|
+
import { LoginEvent, LoginViews, SsoData } from './login.model';
|
|
25
26
|
import { getStoredToken, getStoredTfaToken, TOKEN_KEY, TFATOKEN_KEY } from '@c8y/bootstrap';
|
|
26
27
|
|
|
27
28
|
/**
|
|
@@ -34,6 +35,11 @@ export class LoginService extends SimplifiedAuthService {
|
|
|
34
35
|
oauthOptions: ITenantLoginOption;
|
|
35
36
|
isFirstLogin = true;
|
|
36
37
|
automaticLoginInProgress$ = new BehaviorSubject(true);
|
|
38
|
+
get pendingDownloadBinaryId(): string | null {
|
|
39
|
+
return this._pendingDownloadBinaryId;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private _pendingDownloadBinaryId: string | null = null;
|
|
37
43
|
|
|
38
44
|
regexp = /\/apps\/(public\/)?([^\/]+)(\/.*)?$/;
|
|
39
45
|
|
|
@@ -49,6 +55,8 @@ export class LoginService extends SimplifiedAuthService {
|
|
|
49
55
|
private readonly IDP_HINT_QUERY_PARAM = 'idp_hint';
|
|
50
56
|
private readonly PASSWORD_MAX_LENGTH = 32;
|
|
51
57
|
private readonly PASSWORD_ALLOWED_SYMBOLS = '`~!@#$%^&*()_|+-=?;:\'",.<>{}[]\\/';
|
|
58
|
+
private readonly DOWNLOAD_PARAM = 'download';
|
|
59
|
+
private readonly DOWNLOAD_SESSION_KEY = 'c8yPendingDownloadId';
|
|
52
60
|
private translateService = inject(TranslateService);
|
|
53
61
|
ERROR_MESSAGES = {
|
|
54
62
|
minlength: gettext('Password must have at least 8 characters and no more than 32.'),
|
|
@@ -97,6 +105,7 @@ export class LoginService extends SimplifiedAuthService {
|
|
|
97
105
|
private modalService = inject(ModalService);
|
|
98
106
|
private applicationService = inject(ApplicationService);
|
|
99
107
|
private document = inject(DOCUMENT);
|
|
108
|
+
private router = inject(Router);
|
|
100
109
|
|
|
101
110
|
constructor() {
|
|
102
111
|
super();
|
|
@@ -337,7 +346,68 @@ export class LoginService extends SimplifiedAuthService {
|
|
|
337
346
|
return this.ensureUserPermissionsForRedirect(user);
|
|
338
347
|
}
|
|
339
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Reads the binary download ID from the URL (hash fragment) or from
|
|
351
|
+
* sessionStorage (SSO return path). Clears it from the URL immediately and persists it in
|
|
352
|
+
* sessionStorage so it survives the OAuth redirect. Sets pendingDownloadBinaryId so that
|
|
353
|
+
* the post-auth redirect is suppressed when a download is pending.
|
|
354
|
+
* @returns The binary ID, or null when no download param is present.
|
|
355
|
+
*/
|
|
356
|
+
initDownloadBinaryId(): string | null {
|
|
357
|
+
const stored = sessionStorage.getItem(this.DOWNLOAD_SESSION_KEY);
|
|
358
|
+
|
|
359
|
+
// Download links are always in the hash fragment (#/?download=<id>).
|
|
360
|
+
const binaryId =
|
|
361
|
+
this.router.parseUrl(this.document.location.hash.substring(1)).queryParams[
|
|
362
|
+
this.DOWNLOAD_PARAM
|
|
363
|
+
] ?? null;
|
|
364
|
+
|
|
365
|
+
// Common case: no download param in URL or session — return early
|
|
366
|
+
if (!binaryId && !stored) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (binaryId) {
|
|
371
|
+
// Remove the download param from the URL without adding a history entry
|
|
372
|
+
this.router.navigate([], {
|
|
373
|
+
queryParams: { [this.DOWNLOAD_PARAM]: null },
|
|
374
|
+
queryParamsHandling: 'merge',
|
|
375
|
+
replaceUrl: true
|
|
376
|
+
});
|
|
377
|
+
// Replace any stale entry and persist the id across a potential SSO redirect
|
|
378
|
+
sessionStorage.removeItem(this.DOWNLOAD_SESSION_KEY);
|
|
379
|
+
sessionStorage.setItem(this.DOWNLOAD_SESSION_KEY, binaryId);
|
|
380
|
+
this._pendingDownloadBinaryId = binaryId;
|
|
381
|
+
return binaryId;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// SSO return: param no longer in URL but bridged via sessionStorage
|
|
385
|
+
sessionStorage.removeItem(this.DOWNLOAD_SESSION_KEY);
|
|
386
|
+
this._pendingDownloadBinaryId = stored;
|
|
387
|
+
return stored;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
clearPendingDownload() {
|
|
391
|
+
this._pendingDownloadBinaryId = null;
|
|
392
|
+
sessionStorage.removeItem(this.DOWNLOAD_SESSION_KEY);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Resolves which view a caller should show once login()/ensureUserPermissionsForRedirect()
|
|
397
|
+
* reports that no redirect happened. A pending download always wins over the generic
|
|
398
|
+
* missing-access view, regardless of whether the user actually has application access,
|
|
399
|
+
* since the user came here to download a shared file, not to open an app.
|
|
400
|
+
*/
|
|
401
|
+
getViewForSuppressedRedirect(): LoginViews {
|
|
402
|
+
return this.pendingDownloadBinaryId ? LoginViews.Download : LoginViews.MissingApplicationAccess;
|
|
403
|
+
}
|
|
404
|
+
|
|
340
405
|
async ensureUserPermissionsForRedirect(user: IUser | ICurrentUser) {
|
|
406
|
+
// A pending download requires the user to stay on the login page.
|
|
407
|
+
// Suppress the post-auth redirect so the download view is shown instead.
|
|
408
|
+
if (this.pendingDownloadBinaryId) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
341
411
|
const redirectPath = await this.getRedirectPath();
|
|
342
412
|
if (!redirectPath) {
|
|
343
413
|
return false;
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { gettext } from '@c8y/ngx-components/gettext';
|
|
13
13
|
import { FormsModule } from '@angular/forms';
|
|
14
14
|
import { NgClass } from '@angular/common';
|
|
15
|
-
import { LoginEvent
|
|
15
|
+
import { LoginEvent } from '../login.model';
|
|
16
16
|
|
|
17
17
|
@Component({
|
|
18
18
|
selector: 'c8y-sms-challenge',
|
|
@@ -86,7 +86,7 @@ export class SmsChallengeComponent {
|
|
|
86
86
|
this.appState.currentUser.value
|
|
87
87
|
);
|
|
88
88
|
if (!result) {
|
|
89
|
-
this.onChangeView.emit({ view:
|
|
89
|
+
this.onChangeView.emit({ view: this.loginService.getViewForSuppressedRedirect() });
|
|
90
90
|
}
|
|
91
91
|
} catch (e) {
|
|
92
92
|
const resStatus = e.res && e.res.status;
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
Two-factor authentication
|
|
6
6
|
</div>
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
</c8y-totp-setup>
|
|
8
|
+
@if (isSetup) {
|
|
9
|
+
<c8y-totp-setup> </c8y-totp-setup>
|
|
10
|
+
}
|
|
10
11
|
<c8y-totp-challenge
|
|
11
12
|
[isModal]="false"
|
|
12
13
|
[loading]="loading"
|
|
@@ -15,4 +16,3 @@
|
|
|
15
16
|
(onSuccess)="onTotpSuccess($event)"
|
|
16
17
|
(totpUnconfirmedEmitter)="onCancel.emit()"
|
|
17
18
|
></c8y-totp-challenge>
|
|
18
|
-
|
|
@@ -10,13 +10,12 @@ import {
|
|
|
10
10
|
import { LoginService } from '../login.service';
|
|
11
11
|
import { LoginEvent, LoginViews } from '../login.model';
|
|
12
12
|
import { gettext } from '@c8y/ngx-components/gettext';
|
|
13
|
-
import { NgIf } from '@angular/common';
|
|
14
13
|
|
|
15
14
|
@Component({
|
|
16
15
|
selector: 'c8y-totp-auth',
|
|
17
16
|
templateUrl: './totp-auth.component.html',
|
|
18
17
|
standalone: true,
|
|
19
|
-
imports: [C8yTranslateDirective,
|
|
18
|
+
imports: [C8yTranslateDirective, TotpSetupComponent, TotpChallengeComponent]
|
|
20
19
|
})
|
|
21
20
|
export class TotpAuthComponent implements OnInit {
|
|
22
21
|
@Input() credentials: ICredentials;
|
|
@@ -55,7 +54,7 @@ export class TotpAuthComponent implements OnInit {
|
|
|
55
54
|
this.appState.currentUser.value
|
|
56
55
|
);
|
|
57
56
|
if (!result) {
|
|
58
|
-
this.onChangeView.emit({ view:
|
|
57
|
+
this.onChangeView.emit({ view: this.loginService.getViewForSuppressedRedirect() });
|
|
59
58
|
}
|
|
60
59
|
} catch (e) {
|
|
61
60
|
this.alert.removeLastDanger();
|