@bitblit/ngx-acute-warden 6.0.146-alpha → 6.0.147-alpha
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 +9 -8
- package/src/build/ngx-acute-warden-info.ts +19 -0
- package/src/components/acute-create-user/acute-create-user.component.html +48 -0
- package/src/components/acute-create-user/acute-create-user.component.ts +93 -0
- package/src/components/acute-login/acute-login.component.html +126 -0
- package/src/components/acute-login/acute-login.component.ts +265 -0
- package/src/components/acute-magic-lander/acute-magic-lander.component.html +10 -0
- package/src/components/acute-magic-lander/acute-magic-lander.component.ts +78 -0
- package/src/components/acute-user-profile/acute-user-profile.component.html +40 -0
- package/src/components/acute-user-profile/acute-user-profile.component.scss +32 -0
- package/src/components/acute-user-profile/acute-user-profile.component.ts +122 -0
- package/src/constants.ts +5 -0
- package/src/guards/logged-in-guard.ts +30 -0
- package/src/guards/not-logged-in-guard.ts +41 -0
- package/src/index.ts +10 -0
- package/src/services/warden-adapter-service-config.ts +3 -0
- package/src/services/warden-adapter.service.spec.ts +17 -0
- package/src/services/warden-adapter.service.ts +67 -0
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitblit/ngx-acute-warden",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.147-alpha",
|
|
4
4
|
"description": "Library for using angular with ratchet-warden",
|
|
5
5
|
"module": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
8
|
+
"src/**",
|
|
8
9
|
"lib/**",
|
|
9
10
|
"bin/**"
|
|
10
11
|
],
|
|
@@ -48,9 +49,9 @@
|
|
|
48
49
|
"@angular/platform-browser": "20.3.9",
|
|
49
50
|
"@angular/platform-browser-dynamic": "20.3.9",
|
|
50
51
|
"@angular/router": "20.3.9",
|
|
51
|
-
"@bitblit/ngx-acute-common": "6.0.
|
|
52
|
-
"@bitblit/ratchet-common": "6.0.
|
|
53
|
-
"@bitblit/ratchet-warden-common": "6.0.
|
|
52
|
+
"@bitblit/ngx-acute-common": "6.0.147-alpha",
|
|
53
|
+
"@bitblit/ratchet-common": "6.0.147-alpha",
|
|
54
|
+
"@bitblit/ratchet-warden-common": "6.0.147-alpha",
|
|
54
55
|
"primeflex": "4.0.0",
|
|
55
56
|
"primeicons": "7.0.0",
|
|
56
57
|
"primeng": "20.3.0",
|
|
@@ -67,9 +68,9 @@
|
|
|
67
68
|
"@angular/platform-browser": "^20.3.9",
|
|
68
69
|
"@angular/platform-browser-dynamic": "^20.3.9",
|
|
69
70
|
"@angular/router": "^20.3.9",
|
|
70
|
-
"@bitblit/ngx-acute-common": "6.0.
|
|
71
|
-
"@bitblit/ratchet-common": "6.0.
|
|
72
|
-
"@bitblit/ratchet-warden-common": "6.0.
|
|
71
|
+
"@bitblit/ngx-acute-common": "6.0.147-alpha",
|
|
72
|
+
"@bitblit/ratchet-common": "6.0.147-alpha",
|
|
73
|
+
"@bitblit/ratchet-warden-common": "6.0.147-alpha",
|
|
73
74
|
"primeflex": "4.0.0",
|
|
74
75
|
"primeicons": "7.0.0",
|
|
75
76
|
"primeng": "20.3.0",
|
|
@@ -78,6 +79,6 @@
|
|
|
78
79
|
},
|
|
79
80
|
"devDependencies": {
|
|
80
81
|
"@angular/compiler-cli": "20.3.9",
|
|
81
|
-
"@bitblit/ratchet-node-only": "6.0.
|
|
82
|
+
"@bitblit/ratchet-node-only": "6.0.147-alpha"
|
|
82
83
|
}
|
|
83
84
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BuildInformation } from '@bitblit/ratchet-common/build/build-information';
|
|
2
|
+
|
|
3
|
+
export class NgxAcuteWardenInfo {
|
|
4
|
+
// Empty constructor prevents instantiation
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
6
|
+
private constructor() {}
|
|
7
|
+
|
|
8
|
+
public static buildInformation(): BuildInformation {
|
|
9
|
+
const val: BuildInformation = {
|
|
10
|
+
version: 'LOCAL-SNAPSHOT',
|
|
11
|
+
hash: 'LOCAL-HASH',
|
|
12
|
+
branch: 'LOCAL-BRANCH',
|
|
13
|
+
tag: 'LOCAL-TAG',
|
|
14
|
+
timeBuiltISO: 'LOCAL-TIME-ISO',
|
|
15
|
+
notes: 'LOCAL-NOTES',
|
|
16
|
+
};
|
|
17
|
+
return val;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<div class="row">
|
|
2
|
+
<p-card header="Create User">
|
|
3
|
+
<form [formGroup]="form">
|
|
4
|
+
<div style="display: flex; flex-direction: column">
|
|
5
|
+
<div class="p-inputgroup">
|
|
6
|
+
<label class="p-inputgroup-addon">Contact Type</label>
|
|
7
|
+
<p-select
|
|
8
|
+
[options]="userContactTypes"
|
|
9
|
+
formControlName="userContactType"
|
|
10
|
+
id="userContactType"
|
|
11
|
+
[style]="{ 'min-width': '50%' }"
|
|
12
|
+
></p-select>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="p-inputgroup">
|
|
15
|
+
<span class="p-inputgroup-addon">Contact</span>
|
|
16
|
+
<input pInputText formControlName="userContactValue" placeholder="e.g., test@test.com" id="userContactValue" />
|
|
17
|
+
@if (f('userContactValue').dirty) {
|
|
18
|
+
@if (f('userContactValue').errors?.['required']) {
|
|
19
|
+
<span style="color: red">Contact is Required</span>
|
|
20
|
+
}
|
|
21
|
+
@if(f('userContactValue').errors?.['email']) {
|
|
22
|
+
<span style="color: red">Please enter a valid email address</span>
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
</div>
|
|
26
|
+
<div class="p-inputgroup">
|
|
27
|
+
<span class="p-inputgroup-addon">Full name</span>
|
|
28
|
+
<input pInputText formControlName="userFullName" id="userFullName" placeholder="e.g., Tom Smith" />
|
|
29
|
+
@if(f('userFullName').dirty){
|
|
30
|
+
@if(f('userFullName').errors?.['required']) {
|
|
31
|
+
<span style="color: red" >Full name is Required</span>
|
|
32
|
+
}
|
|
33
|
+
@if(f('userFullName').errors?.['minlength']) {
|
|
34
|
+
<span style="color: red" >Please enter a longer name</span>
|
|
35
|
+
}
|
|
36
|
+
@if(f('userFullName').errors?.['maxlength']) {
|
|
37
|
+
<span style="color: red" >Please enter a shorter name</span>
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div>
|
|
43
|
+
<p-button pRipple styleClass="p-button-raised" (click)="createUser()">Create user</p-button>
|
|
44
|
+
<p-button pRipple styleClass="p-button-raised" routerLink="/public/login">Cancel</p-button>
|
|
45
|
+
</div>
|
|
46
|
+
</form>
|
|
47
|
+
</p-card>
|
|
48
|
+
</div>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Component, OnInit } from '@angular/core';
|
|
2
|
+
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
|
3
|
+
|
|
4
|
+
import { Logger } from '@bitblit/ratchet-common/logger/logger';
|
|
5
|
+
import { WardenClient } from '@bitblit/ratchet-warden-common/client/warden-client';
|
|
6
|
+
import { No } from '@bitblit/ratchet-common/lang/no';
|
|
7
|
+
import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
|
|
8
|
+
import { AbstractControl, FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
9
|
+
import { WardenContactType } from '@bitblit/ratchet-warden-common/common/model/warden-contact-type';
|
|
10
|
+
import { EnumRatchet } from '@bitblit/ratchet-common/lang/enum-ratchet';
|
|
11
|
+
import { CommonModule } from '@angular/common';
|
|
12
|
+
import { CardModule } from 'primeng/card';
|
|
13
|
+
import { ButtonModule } from 'primeng/button';
|
|
14
|
+
import { AlertComponent } from '@bitblit/ngx-acute-common';
|
|
15
|
+
import { DialogService } from 'primeng/dynamicdialog';
|
|
16
|
+
import { Ripple } from 'primeng/ripple';
|
|
17
|
+
import { InputTextModule } from 'primeng/inputtext';
|
|
18
|
+
import { Select } from "primeng/select";
|
|
19
|
+
|
|
20
|
+
@Component({
|
|
21
|
+
selector: 'ngx-acute-warden-create-user',
|
|
22
|
+
templateUrl: './acute-create-user.component.html',
|
|
23
|
+
standalone: true,
|
|
24
|
+
imports: [
|
|
25
|
+
RouterModule,
|
|
26
|
+
FormsModule,
|
|
27
|
+
ReactiveFormsModule,
|
|
28
|
+
CommonModule,
|
|
29
|
+
CardModule,
|
|
30
|
+
ButtonModule,
|
|
31
|
+
Ripple,
|
|
32
|
+
InputTextModule,
|
|
33
|
+
Select
|
|
34
|
+
]
|
|
35
|
+
})
|
|
36
|
+
export class AcuteCreateUserComponent implements OnInit {
|
|
37
|
+
public form: FormGroup;
|
|
38
|
+
|
|
39
|
+
public userContactTypes: string[] = EnumRatchet.listEnumKeys(WardenContactType);
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
private route: ActivatedRoute,
|
|
43
|
+
private router: Router,
|
|
44
|
+
private wardenClient: WardenClient,
|
|
45
|
+
private fb: FormBuilder,
|
|
46
|
+
private dlgService: DialogService,
|
|
47
|
+
) {
|
|
48
|
+
this.form = this.fb.group(
|
|
49
|
+
{
|
|
50
|
+
userFullName: [null, Validators.compose([Validators.required, Validators.minLength(2), Validators.maxLength(35)])],
|
|
51
|
+
userContactValue: [null, Validators.compose([Validators.required, Validators.email])],
|
|
52
|
+
userContactType: [StringRatchet.safeString(WardenContactType.EmailAddress), Validators.compose([Validators.required])],
|
|
53
|
+
},
|
|
54
|
+
{ validators: [] },
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public f(name: string): AbstractControl {
|
|
59
|
+
return this.form.controls[name];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
ngOnInit(): void {
|
|
63
|
+
Logger.info('ngi: %j', this.route.queryParamMap);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public async createUser(): Promise<void> {
|
|
67
|
+
try {
|
|
68
|
+
const type: WardenContactType = EnumRatchet.keyToEnum<WardenContactType>(
|
|
69
|
+
WardenContactType,
|
|
70
|
+
this.form.controls['userContactType'].value,
|
|
71
|
+
false,
|
|
72
|
+
);
|
|
73
|
+
if (
|
|
74
|
+
StringRatchet.trimToNull(this.form.controls['userContactValue'].value) &&
|
|
75
|
+
StringRatchet.trimToNull(this.form.controls['userFullName'].value) &&
|
|
76
|
+
type
|
|
77
|
+
) {
|
|
78
|
+
const userId: string = await this.wardenClient.createAccount(
|
|
79
|
+
{ type: type, value: this.form.controls['userContactValue'].value },
|
|
80
|
+
true,
|
|
81
|
+
this.form.controls['userFullName'].value,
|
|
82
|
+
[],
|
|
83
|
+
);
|
|
84
|
+
Logger.info('Got user id %s', userId);
|
|
85
|
+
this.router.navigate(['/public/login']).then(No.op);
|
|
86
|
+
} else {
|
|
87
|
+
AlertComponent.showAlert(this.dlgService, 'Missing data');
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
AlertComponent.showAlert(this.dlgService, 'Failed: ' + err);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<div style="display: flex; flex-direction: column; margin: 10px; padding: 2px; align-content: center; justify-content: center">
|
|
2
|
+
<div style="display: flex; flex-direction: column; align-content: center; justify-content: center; margin: 1em; gap: 2px">
|
|
3
|
+
@if(applicationName) {
|
|
4
|
+
<div class="text-3xl font-bold text-center text-primary">{{ applicationName }}</div>
|
|
5
|
+
}
|
|
6
|
+
@if(helperText) {
|
|
7
|
+
<div class="text-base font-italic text-center text-secondary">{{ helperText }}</div>
|
|
8
|
+
}
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
@if(!showCodePage) {
|
|
12
|
+
<div>
|
|
13
|
+
<!--<button pButton icon="pi pi-ban" label="Clear Saved Logins" (click)="clearSavedLogins()" style="margin-top: 10px"></button>-->
|
|
14
|
+
|
|
15
|
+
<div style="display: flex; flex-direction: column">
|
|
16
|
+
<div style="display: flex; flex-direction: row; justify-content: space-between; gap: 5px">
|
|
17
|
+
<input
|
|
18
|
+
class="p-inputtext rounded-input"
|
|
19
|
+
[(ngModel)]="newContactValue"
|
|
20
|
+
id="newContactValue"
|
|
21
|
+
pInputText
|
|
22
|
+
style="width: 100%"
|
|
23
|
+
placeholder="Your Email or Text-Capable Phone"
|
|
24
|
+
type="text"
|
|
25
|
+
/>
|
|
26
|
+
<p-button
|
|
27
|
+
icon="pi pi-envelope"
|
|
28
|
+
(click)="sendCodeToNewContact(newContactValue)"
|
|
29
|
+
pTooltip="Send code"
|
|
30
|
+
[disabled]="contactValueInvalidAndDirty()"
|
|
31
|
+
>
|
|
32
|
+
</p-button>
|
|
33
|
+
</div>
|
|
34
|
+
@if(contactValueInvalidAndDirty()) {
|
|
35
|
+
<span style="color: red">Valid email or phone number required</span>
|
|
36
|
+
}
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
@if(hasRecentLogins){
|
|
40
|
+
<div style="display: flex; flex-direction: row; justify-content: space-around; margin: 2em">
|
|
41
|
+
<div>-----</div>
|
|
42
|
+
<div class="text-lg font-italic">Or select a previously used method</div>
|
|
43
|
+
<div>-----</div>
|
|
44
|
+
</div>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@for(lg of recentLogins; track $index) { <!-- lg.userById -->
|
|
48
|
+
<div>
|
|
49
|
+
<div style="display: flex; flex-direction: row; justify-content: space-between">
|
|
50
|
+
<h3 class="p-toolbar-start">{{ lg.user.userLabel }}</h3>
|
|
51
|
+
<div class="p-toolbar-center" style="display: flex; flex-direction: row; gap: 1em">
|
|
52
|
+
@if(lg?.user?.webAuthnAuthenticatorSummaries?.length) {
|
|
53
|
+
<p-button
|
|
54
|
+
icon="pi pi-lock-open"
|
|
55
|
+
(click)="processWebAuthnLogin(lg.user.userId)"
|
|
56
|
+
pTooltip="Authenticate by device (WebAuthn)"
|
|
57
|
+
>
|
|
58
|
+
</p-button>
|
|
59
|
+
}
|
|
60
|
+
@for(e of lg.user.contactMethods; track $index) { <!-- e.contactByName -->
|
|
61
|
+
<div style="display: flex; flex-direction: row; gap: 5px">
|
|
62
|
+
@if(e.type === 'EmailAddress'){
|
|
63
|
+
<p-button
|
|
64
|
+
icon="pi pi-envelope"
|
|
65
|
+
(click)="sendCodeToContact(e)"
|
|
66
|
+
pTooltip="{{ e.value }}"
|
|
67
|
+
></p-button>
|
|
68
|
+
}
|
|
69
|
+
@if(e.type === 'TextCapablePhoneNumber'){
|
|
70
|
+
<p-button
|
|
71
|
+
icon="pi pi-mobile"
|
|
72
|
+
(click)="sendCodeToContact(e)"
|
|
73
|
+
pTooltip="{{ e.value }}"
|
|
74
|
+
></p-button>
|
|
75
|
+
}
|
|
76
|
+
</div>
|
|
77
|
+
}
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div class="p-toolbar-end">
|
|
81
|
+
<p-button
|
|
82
|
+
icon="pi pi-delete-left"
|
|
83
|
+
severity="danger"
|
|
84
|
+
(click)="removeSingleLogin(lg.user.userId)"
|
|
85
|
+
pTooltip="Forget {{ lg.user.userLabel }}"
|
|
86
|
+
></p-button>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
}
|
|
91
|
+
</div>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@if(showCodePage) {
|
|
95
|
+
<div>
|
|
96
|
+
<p-fieldset legend="One-Time Code">
|
|
97
|
+
<div style="display: flex; flex-direction: column; min-width: 100%; justify-content: center; align-items: center; gap: 10px">
|
|
98
|
+
<div>
|
|
99
|
+
Enter the code sent to <span style="font-weight: bolder">{{ waitingContact.value }}</span>
|
|
100
|
+
</div>
|
|
101
|
+
<input
|
|
102
|
+
type="hidden"
|
|
103
|
+
class="p-inputtext rounded-input"
|
|
104
|
+
[(ngModel)]="waitingContact.value"
|
|
105
|
+
id="waitingContactValue"
|
|
106
|
+
readonly="true"
|
|
107
|
+
/>
|
|
108
|
+
<!-- <p-inputOtp [(ngModel)]="verificationCode" id="verificationCode" [length]="6" size="large"
|
|
109
|
+
[integerOnly]="integerOnly" (ngModelChange)="otpChanged()">
|
|
110
|
+
<ng-template #input let-token let-events="events">
|
|
111
|
+
<input class="custom-otp-input" (input)="events.input($event)" (keydown)="events.keydown($event)" type="text" [attr.value]="token" [maxLength]="1" />
|
|
112
|
+
</ng-template>
|
|
113
|
+
</p-inputOtp> -->
|
|
114
|
+
<p-inputOtp [(ngModel)]="verificationCode" id="verificationCode" [length]="6" size="large"
|
|
115
|
+
[integerOnly]="integerOnly" (ngModelChange)="otpChanged()"
|
|
116
|
+
[attr.inputmode]="otpInputMode" [attr.pattern]="otpInputPattern"
|
|
117
|
+
/>
|
|
118
|
+
<div style="display: flex; flex-direction: row; justify-content: space-between; gap: 5px">
|
|
119
|
+
<p-button (click)="submitVerificationCode(waitingContact.value, verificationCode)" label="Submit code" > </p-button>
|
|
120
|
+
<p-button (click)="verificationCode = null; showCodePage = false" label="Cancel"></p-button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</p-fieldset>
|
|
124
|
+
</div>
|
|
125
|
+
}
|
|
126
|
+
</div>
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
|
|
2
|
+
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
|
3
|
+
|
|
4
|
+
import { Logger } from '@bitblit/ratchet-common/logger/logger';
|
|
5
|
+
import { WardenClient } from '@bitblit/ratchet-warden-common/client/warden-client';
|
|
6
|
+
import { WardenUserService } from '@bitblit/ratchet-warden-common/client/warden-user-service';
|
|
7
|
+
import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
|
|
8
|
+
import { WardenContact } from '@bitblit/ratchet-warden-common/common/model/warden-contact';
|
|
9
|
+
import { WardenRecentLoginDescriptor } from '@bitblit/ratchet-warden-common/client/provider/warden-recent-login-descriptor';
|
|
10
|
+
import { WardenUtils } from '@bitblit/ratchet-warden-common/common/util/warden-utils';
|
|
11
|
+
import { WardenContactType } from '@bitblit/ratchet-warden-common/common/model/warden-contact-type';
|
|
12
|
+
import { WardenLoggedInUserWrapper } from '@bitblit/ratchet-warden-common/client/provider/warden-logged-in-user-wrapper';
|
|
13
|
+
import { ButtonModule } from 'primeng/button';
|
|
14
|
+
import { FormsModule } from '@angular/forms';
|
|
15
|
+
import { CommonModule } from '@angular/common';
|
|
16
|
+
import { CardModule } from 'primeng/card';
|
|
17
|
+
import { TooltipModule } from 'primeng/tooltip';
|
|
18
|
+
import { FieldsetModule } from 'primeng/fieldset';
|
|
19
|
+
import { AlertComponent } from '@bitblit/ngx-acute-common';
|
|
20
|
+
import { DialogService } from 'primeng/dynamicdialog';
|
|
21
|
+
import { InputTextModule } from 'primeng/inputtext';
|
|
22
|
+
import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet';
|
|
23
|
+
import { InputOtpModule } from 'primeng/inputotp';
|
|
24
|
+
import { AccordionModule } from 'primeng/accordion';
|
|
25
|
+
import { SplitButtonModule } from 'primeng/splitbutton';
|
|
26
|
+
import { InputGroupModule } from 'primeng/inputgroup';
|
|
27
|
+
import { ToolbarModule } from 'primeng/toolbar';
|
|
28
|
+
import { Base64Ratchet } from "@bitblit/ratchet-common/lang/base64-ratchet";
|
|
29
|
+
import { No } from "@bitblit/ratchet-common/lang/no";
|
|
30
|
+
import { Subscription, timer } from "rxjs";
|
|
31
|
+
|
|
32
|
+
@Component({
|
|
33
|
+
selector: 'ngx-acute-warden-login',
|
|
34
|
+
templateUrl: './acute-login.component.html',
|
|
35
|
+
standalone: true,
|
|
36
|
+
imports: [
|
|
37
|
+
ButtonModule,
|
|
38
|
+
RouterModule,
|
|
39
|
+
FormsModule,
|
|
40
|
+
CommonModule,
|
|
41
|
+
CardModule,
|
|
42
|
+
TooltipModule,
|
|
43
|
+
ButtonModule,
|
|
44
|
+
FieldsetModule,
|
|
45
|
+
InputTextModule,
|
|
46
|
+
InputOtpModule,
|
|
47
|
+
AccordionModule,
|
|
48
|
+
SplitButtonModule,
|
|
49
|
+
InputGroupModule,
|
|
50
|
+
ToolbarModule,
|
|
51
|
+
],
|
|
52
|
+
styles: [
|
|
53
|
+
`
|
|
54
|
+
.custom-otp-input {
|
|
55
|
+
width: 40px;
|
|
56
|
+
font-size: 36px;
|
|
57
|
+
border: 0 none;
|
|
58
|
+
appearance: none;
|
|
59
|
+
text-align: center;
|
|
60
|
+
transition: all 0.2s;
|
|
61
|
+
background: transparent;
|
|
62
|
+
border-bottom: 2px solid grey;
|
|
63
|
+
border-radius: 0px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.custom-otp-input:focus {
|
|
67
|
+
outline: 0 none;
|
|
68
|
+
border-bottom-color: var(--p-primary-color);
|
|
69
|
+
}
|
|
70
|
+
`
|
|
71
|
+
],
|
|
72
|
+
})
|
|
73
|
+
export class AcuteLoginComponent implements OnDestroy, OnInit {
|
|
74
|
+
@Input() public applicationName: string;
|
|
75
|
+
@Input() public helperText: string;
|
|
76
|
+
|
|
77
|
+
@Input() public postLoginUrl: string;
|
|
78
|
+
@Input() public integerOnly: boolean;
|
|
79
|
+
@Input() public createUserIfMissing: boolean = false;
|
|
80
|
+
|
|
81
|
+
@Input() public caseSensitiveContactValues: boolean = false;
|
|
82
|
+
|
|
83
|
+
public verificationCode: string;
|
|
84
|
+
public waitingContact: WardenContact;
|
|
85
|
+
public showCodePage: boolean = false;
|
|
86
|
+
|
|
87
|
+
public newContactValue: string;
|
|
88
|
+
|
|
89
|
+
private checkForBackgroundLogInTimerSubscription: Subscription;
|
|
90
|
+
|
|
91
|
+
constructor(
|
|
92
|
+
private route: ActivatedRoute,
|
|
93
|
+
private router: Router,
|
|
94
|
+
public userService: WardenUserService<any>,
|
|
95
|
+
public wardenClient: WardenClient,
|
|
96
|
+
private dlgService: DialogService,
|
|
97
|
+
) {
|
|
98
|
+
Logger.info('Found %s recent logins', this.recentLogins.length);
|
|
99
|
+
this.checkForBackgroundLogInTimerSubscription = timer(0, 2_500).subscribe(async (_t) => {
|
|
100
|
+
if (this.userService.isLoggedIn()) {
|
|
101
|
+
Logger.info('Detected background login - redirecting');
|
|
102
|
+
await this.router.navigate([this.postLoginUrl]);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
ngOnInit(): void {
|
|
109
|
+
Logger.info('Login init');
|
|
110
|
+
RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(this.postLoginUrl, 'postLoginUrl may not be empty');
|
|
111
|
+
// Check if auto-login was requested
|
|
112
|
+
const mode: string = StringRatchet.trimToNull(this.route.snapshot.queryParamMap.get('mode'));
|
|
113
|
+
const code: string = StringRatchet.trimToNull(this.route.snapshot.queryParamMap.get('code'));
|
|
114
|
+
const contactClear: string = StringRatchet.trimToNull(this.route.snapshot.queryParamMap.get('contact'));
|
|
115
|
+
const contactB64: string = StringRatchet.trimToNull(this.route.snapshot.queryParamMap.get('contactEncoded'));
|
|
116
|
+
const contact: string = contactB64 ? Base64Ratchet.base64StringToString(contactB64) : contactClear;
|
|
117
|
+
|
|
118
|
+
if (mode==='auto' && code && contact) {
|
|
119
|
+
this.attemptAutoLogin(contact, code).then(No.op);
|
|
120
|
+
} else {
|
|
121
|
+
Logger.info('No auto-login detected');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
ngOnDestroy(): void {
|
|
126
|
+
if (this.checkForBackgroundLogInTimerSubscription) {
|
|
127
|
+
this.checkForBackgroundLogInTimerSubscription.unsubscribe();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public get otpInputMode(): string {
|
|
132
|
+
return this.integerOnly ? 'numeric' : 'text';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public get otpInputPattern(): string {
|
|
136
|
+
return this.integerOnly ? '[0-9]*' : '[0-9A-Za-z]*';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public userById(index: number, ld: WardenRecentLoginDescriptor) {
|
|
140
|
+
return ld.user.userId;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public contactByName(index: number, contact: WardenContact) {
|
|
144
|
+
return contact.value;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public get hasRecentLogins(): boolean {
|
|
148
|
+
return this.recentLogins.length > 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public get recentLogins(): WardenRecentLoginDescriptor[] {
|
|
152
|
+
let rval: WardenRecentLoginDescriptor[] = this.userService?.serviceOptions?.recentLoginProvider?.fetchAllLogins() || [];
|
|
153
|
+
|
|
154
|
+
// Filter out third party logins for now
|
|
155
|
+
rval = rval.filter((ld: WardenRecentLoginDescriptor) => {return ld?.user?.contactMethods?.length>0 || ld?.user?.webAuthnAuthenticatorSummaries?.length>0})
|
|
156
|
+
|
|
157
|
+
return rval;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public clearSavedLogins(): void {
|
|
161
|
+
if (confirm('Are you sure you want to clear saved logins?')) {
|
|
162
|
+
this.userService.serviceOptions.recentLoginProvider.clearAllLogins();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
public async attemptAutoLogin(contactValue: string, code: string): Promise<void> {
|
|
168
|
+
Logger.info('Attempting auto-login');
|
|
169
|
+
const contact: WardenContact = WardenUtils.stringToWardenContact(contactValue);
|
|
170
|
+
const wrapper: WardenLoggedInUserWrapper<any> = await this.userService.executeValidationTokenBasedLogin(
|
|
171
|
+
contact,
|
|
172
|
+
code,
|
|
173
|
+
this.createUserIfMissing,
|
|
174
|
+
);
|
|
175
|
+
if (wrapper) {
|
|
176
|
+
await this.router.navigate([this.postLoginUrl]);
|
|
177
|
+
} else {
|
|
178
|
+
Logger.info('Auto-login failed');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
public async sendCodeToNewContact(inValue: string): Promise<void> {
|
|
184
|
+
const value: string = this.caseSensitiveContactValues ? inValue : inValue.toLowerCase();
|
|
185
|
+
Logger.info('Trying to send code to %s %s %s', value, WardenUtils.stringIsEmailAddress(value), WardenUtils.stringIsPhoneNumber(value));
|
|
186
|
+
if (StringRatchet.trimToNull(value)) {
|
|
187
|
+
let ct: WardenContactType = null;
|
|
188
|
+
if (WardenUtils.stringIsEmailAddress(value)) {
|
|
189
|
+
ct = WardenContactType.EmailAddress;
|
|
190
|
+
} else if (WardenUtils.stringIsPhoneNumber(value)) {
|
|
191
|
+
ct = WardenContactType.TextCapablePhoneNumber;
|
|
192
|
+
}
|
|
193
|
+
if (!ct) {
|
|
194
|
+
AlertComponent.showAlert(this.dlgService, 'Cannot treat this as an email or a phone number - ignoring');
|
|
195
|
+
} else {
|
|
196
|
+
this.waitingContact = { type: ct, value: value };
|
|
197
|
+
const val: boolean = await this.userService.sendExpiringCode(this.waitingContact);
|
|
198
|
+
if (val) {
|
|
199
|
+
this.showCodePage = true;
|
|
200
|
+
} else {
|
|
201
|
+
AlertComponent.showAlert(this.dlgService, 'Failed to send code');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
public async sendCodeToContact(contact: WardenContact): Promise<void> {
|
|
208
|
+
Logger.info('Send code to contact %j', contact);
|
|
209
|
+
if (contact && StringRatchet.trimToNull(contact.value) && contact.type) {
|
|
210
|
+
this.waitingContact = contact;
|
|
211
|
+
const val: boolean = await this.userService.sendExpiringCode(contact);
|
|
212
|
+
if (val) {
|
|
213
|
+
this.showCodePage = true;
|
|
214
|
+
} else {
|
|
215
|
+
AlertComponent.showAlert(this.dlgService, 'Failed to send code');
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
Logger.info('Failed to send code to contact : %j', contact);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
public async submitVerificationCode(input: string, verificationCode: string): Promise<void> {
|
|
223
|
+
Logger.info('Submit: %s, %s', input, verificationCode);
|
|
224
|
+
const val: WardenLoggedInUserWrapper<any> = await this.userService.executeValidationTokenBasedLogin(
|
|
225
|
+
{ type: WardenUtils.stringToContactType(input), value: input },
|
|
226
|
+
verificationCode,
|
|
227
|
+
this.createUserIfMissing
|
|
228
|
+
);
|
|
229
|
+
if (val) {
|
|
230
|
+
await this.router.navigate([this.postLoginUrl]);
|
|
231
|
+
} else {
|
|
232
|
+
AlertComponent.showAlert(this.dlgService, 'Code submission failed');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
public async processWebAuthnLogin(userId: string): Promise<void> {
|
|
237
|
+
Logger.info('processWebAuthnLogin: %s', userId);
|
|
238
|
+
const val: WardenLoggedInUserWrapper<any> = await this.userService.executeWebAuthnBasedLogin(userId);
|
|
239
|
+
if (val) {
|
|
240
|
+
await this.router.navigate([this.postLoginUrl]);
|
|
241
|
+
} else {
|
|
242
|
+
AlertComponent.showAlert(this.dlgService, 'Web authentication method failed');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
public removeSingleLogin(userId: string): void {
|
|
247
|
+
this.userService.serviceOptions.recentLoginProvider.removeUser(userId);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
public contactValueInvalidAndDirty(): boolean {
|
|
251
|
+
return (
|
|
252
|
+
StringRatchet.trimToNull(this.newContactValue) &&
|
|
253
|
+
!WardenUtils.stringIsPhoneNumber(this.newContactValue) &&
|
|
254
|
+
!WardenUtils.stringIsEmailAddress(this.newContactValue)
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
public async otpChanged(): Promise<void> {
|
|
259
|
+
if (this.waitingContact?.value && this.verificationCode?.length===6) {
|
|
260
|
+
Logger.debug('Code length is 6, submitting');
|
|
261
|
+
await this.submitVerificationCode(this.waitingContact.value, this.verificationCode)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<div class="row">
|
|
2
|
+
<p-card header="Magic Lander">
|
|
3
|
+
<div style="display: flex; flex-direction: column">
|
|
4
|
+
<p>{{ currentStatus }}</p>
|
|
5
|
+
@if (showLoginButton) {
|
|
6
|
+
<a [routerLink]="[directLoginUrl]" pButton>Back to login page</a>
|
|
7
|
+
}
|
|
8
|
+
</div>
|
|
9
|
+
</p-card>
|
|
10
|
+
</div>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Component, Input, OnInit } from '@angular/core';
|
|
2
|
+
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
|
3
|
+
|
|
4
|
+
import { Logger } from '@bitblit/ratchet-common/logger/logger';
|
|
5
|
+
import { WardenUserService } from '@bitblit/ratchet-warden-common/client/warden-user-service';
|
|
6
|
+
import { No } from '@bitblit/ratchet-common/lang/no';
|
|
7
|
+
import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
|
|
8
|
+
import { Base64Ratchet } from '@bitblit/ratchet-common/lang/base64-ratchet';
|
|
9
|
+
import { WardenContact } from '@bitblit/ratchet-warden-common/common/model/warden-contact';
|
|
10
|
+
import { WardenLoggedInUserWrapper } from '@bitblit/ratchet-warden-common/client/provider/warden-logged-in-user-wrapper';
|
|
11
|
+
import { CardModule } from 'primeng/card';
|
|
12
|
+
import { FormsModule } from '@angular/forms';
|
|
13
|
+
import { CommonModule } from '@angular/common';
|
|
14
|
+
import { ButtonDirective } from 'primeng/button';
|
|
15
|
+
import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet';
|
|
16
|
+
|
|
17
|
+
@Component({
|
|
18
|
+
selector: 'ngx-acute-warden-magic-lander',
|
|
19
|
+
templateUrl: './acute-magic-lander.component.html',
|
|
20
|
+
standalone: true,
|
|
21
|
+
imports: [CardModule, RouterModule, FormsModule, CommonModule, CardModule, ButtonDirective],
|
|
22
|
+
})
|
|
23
|
+
export class AcuteMagicLanderComponent implements OnInit {
|
|
24
|
+
@Input() public directLoginUrl: string;
|
|
25
|
+
|
|
26
|
+
public currentStatus: string = 'Starting up...';
|
|
27
|
+
public showLoginButton: boolean = false;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
private route: ActivatedRoute,
|
|
31
|
+
private router: Router,
|
|
32
|
+
public userService: WardenUserService<any>,
|
|
33
|
+
) {}
|
|
34
|
+
|
|
35
|
+
ngOnInit(): void {
|
|
36
|
+
RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(this.directLoginUrl, 'directLoginUrl may not be empty');
|
|
37
|
+
this.currentStatus = 'Parsing parameters...';
|
|
38
|
+
const codeString: string = StringRatchet.trimToNull(this.route.snapshot.queryParamMap.get('code'));
|
|
39
|
+
const metaString: string = this.route.snapshot.queryParamMap.get('meta');
|
|
40
|
+
const t2: string = Base64Ratchet.base64StringToString(metaString);
|
|
41
|
+
const meta: Record<string, string> = Base64Ratchet.safeBase64JSONParse(metaString);
|
|
42
|
+
Logger.info('ngi: %j : Code %s : Meta %s :: %s :: MetaP: %j', this.route.queryParamMap, codeString, metaString, t2, meta);
|
|
43
|
+
|
|
44
|
+
this.processParameters(codeString, meta).then(No.op);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public async processParameters(verificationCode: string, meta: any): Promise<void> {
|
|
48
|
+
const targetContact: WardenContact = (meta || {}).contact;
|
|
49
|
+
if (verificationCode && targetContact?.value && targetContact?.type) {
|
|
50
|
+
this.currentStatus = 'Logging in...';
|
|
51
|
+
Logger.info('Logging in with code');
|
|
52
|
+
try {
|
|
53
|
+
const val: WardenLoggedInUserWrapper<any> = await this.userService.executeValidationTokenBasedLogin(
|
|
54
|
+
targetContact,
|
|
55
|
+
verificationCode,
|
|
56
|
+
);
|
|
57
|
+
Logger.info('Rval from login was : %j', val);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
Logger.error('Failed to login : %s', err, err);
|
|
60
|
+
this.currentStatus = 'Failed to login : ' + err;
|
|
61
|
+
this.showLoginButton = true;
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
Logger.info('Could not login, missing code or contact : %s : %j', verificationCode, targetContact);
|
|
65
|
+
}
|
|
66
|
+
if (this.userService.isLoggedIn()) {
|
|
67
|
+
if (meta?.redirect) {
|
|
68
|
+
Logger.info('Meta redirect to %s requested', meta.redirect);
|
|
69
|
+
this.currentStatus = 'Redirecting to ' + meta.redirect;
|
|
70
|
+
await this.router.navigate([meta.redirect]);
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
Logger.info('Ignoring meta - not logged in.');
|
|
74
|
+
this.currentStatus = 'Login failed - sending to login page';
|
|
75
|
+
this.showLoginButton = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<p-card header="User profile : {{ user.userObject.wardenData.userLabel }}">
|
|
2
|
+
<div>
|
|
3
|
+
ID: {{ user.userObject.wardenData.userId }} <br />
|
|
4
|
+
Full Name: {{ user.userObject.wardenData.userLabel }} <br />
|
|
5
|
+
<h3>Contacts</h3>
|
|
6
|
+
<ul>
|
|
7
|
+
@for(cm of user.userObject.wardenData.contactMethods; track cm) {
|
|
8
|
+
<li>
|
|
9
|
+
<p-button icon="pi pi-trash" pTooltip="Remove contact method {{ cm.value }}" (click)="removeContact(cm)"></p-button>
|
|
10
|
+
{{ cm.type }} : {{ cm.value }}
|
|
11
|
+
</li>
|
|
12
|
+
}
|
|
13
|
+
</ul>
|
|
14
|
+
@if (user?.userObject?.wardenData?.webAuthnAuthenticatorSummaries?.length) {
|
|
15
|
+
<h3>Web Authenticators</h3>
|
|
16
|
+
<ul>
|
|
17
|
+
@for(waa of user.userObject.wardenData.webAuthnAuthenticatorSummaries; track waa) {
|
|
18
|
+
<li>
|
|
19
|
+
<p-button icon="pi pi-trash" pTooltip="Remove web auth {{ webAuthLabel(waa) }}" (click)="removeWebAuthn(waa)"></p-button>
|
|
20
|
+
<p-button icon="pi pi-file-export" pTooltip="Export web auth {{ webAuthLabel(waa) }}" (click)="exportWebAuthn(waa.origin)"></p-button>
|
|
21
|
+
{{ webAuthLabel(waa) }}
|
|
22
|
+
</li>
|
|
23
|
+
}
|
|
24
|
+
</ul>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
<div style="display: flex; flex-direction: row">
|
|
28
|
+
<p-button icon="pi pi-user" pTooltip="Add a new contact method" (click)="addContact()" icon="pi pi-user-plus"></p-button>
|
|
29
|
+
<p-button icon="pi pi-lock" pTooltip="Add this device as an authenticator" (click)="addWebAuthnDevice()"></p-button>
|
|
30
|
+
<p-button icon="pi pi-file-import" pTooltip="Import web authn" (click)="importWebAuthn()"></p-button>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<br />
|
|
34
|
+
Global Roles: {{ user.userObject.globalRoleIds | json}} <br />
|
|
35
|
+
<br />
|
|
36
|
+
Roles: {{ user.userObject.teamRoleMappings | json}} <br />
|
|
37
|
+
<hr />
|
|
38
|
+
Time (MS) left in token : {{ timeLeftMS }} <button (click)="refreshToken()">Refresh</button>
|
|
39
|
+
</div>
|
|
40
|
+
</p-card>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#warden-user-profile-main-title {
|
|
2
|
+
font-weight: bolder;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
#warden-user-profile-content {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
#warden-user-profile-user-id-section {
|
|
11
|
+
font-weight: bolder;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#warden-user-profile-full-name-section {
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#warden-user-profile-contacts-section {
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#warden-user-profile-web-authn-section {
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#warden-user-profile-add-new-section {
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: row;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#warden-user-profile-roles-section {
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#warden-user-profile-time-remaining-section {
|
|
32
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
2
|
+
import { Router } from '@angular/router';
|
|
3
|
+
import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
|
|
4
|
+
import { Logger } from '@bitblit/ratchet-common/logger/logger';
|
|
5
|
+
import { WardenContact } from '@bitblit/ratchet-warden-common/common/model/warden-contact';
|
|
6
|
+
import { WardenUtils } from '@bitblit/ratchet-warden-common/common/util/warden-utils';
|
|
7
|
+
import { WardenDelegatingCurrentUserProvidingUserServiceEventProcessingProvider } from '@bitblit/ratchet-warden-common/client/warden-delegating-current-user-providing-user-service-event-processing-provider';
|
|
8
|
+
import { WardenUserService } from '@bitblit/ratchet-warden-common/client/warden-user-service';
|
|
9
|
+
import { No } from '@bitblit/ratchet-common/lang/no';
|
|
10
|
+
import { DurationRatchet } from '@bitblit/ratchet-common/lang/duration-ratchet';
|
|
11
|
+
import { WardenLoggedInUserWrapper } from '@bitblit/ratchet-warden-common/client/provider/warden-logged-in-user-wrapper';
|
|
12
|
+
import { WardenWebAuthnEntrySummary } from '@bitblit/ratchet-warden-common/common/model/warden-web-authn-entry-summary';
|
|
13
|
+
import { ButtonModule } from 'primeng/button';
|
|
14
|
+
import { CardModule } from 'primeng/card';
|
|
15
|
+
import { CommonModule } from '@angular/common';
|
|
16
|
+
import { TooltipModule } from 'primeng/tooltip';
|
|
17
|
+
|
|
18
|
+
@Component({
|
|
19
|
+
selector: 'ngx-acute-warden-user-profile',
|
|
20
|
+
templateUrl: './acute-user-profile.component.html',
|
|
21
|
+
standalone: true,
|
|
22
|
+
imports: [ButtonModule, CardModule, CommonModule, TooltipModule],
|
|
23
|
+
})
|
|
24
|
+
export class AcuteUserProfileComponent {
|
|
25
|
+
public user: WardenLoggedInUserWrapper<any>;
|
|
26
|
+
public timeLeftMS: string;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
private router: Router,
|
|
30
|
+
public userService: WardenUserService<any>,
|
|
31
|
+
private userProvider: WardenDelegatingCurrentUserProvidingUserServiceEventProcessingProvider<any>,
|
|
32
|
+
) {
|
|
33
|
+
Logger.info('Construct AcuteUserProfileComponent');
|
|
34
|
+
this.updateData();
|
|
35
|
+
this.userProvider.currentUserSubject.subscribe((_val) => {
|
|
36
|
+
this.updateData();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
public toggleStayLoggedIn(): void {
|
|
40
|
+
Logger.info('Toggling stay logged in (currently %s before toggle)', this.userService.autoRefreshEnabled);
|
|
41
|
+
this.userService.autoRefreshEnabled = !this.userService.autoRefreshEnabled;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private updateData(): void {
|
|
45
|
+
Logger.info('Called updateData');
|
|
46
|
+
const tok: WardenLoggedInUserWrapper<any> = this.userService.fetchLoggedInUserWrapper();
|
|
47
|
+
this.user = tok;
|
|
48
|
+
this.timeLeftMS = this?.user?.userObject?.exp ? DurationRatchet.formatMsDuration(this.user.userObject.exp * 1000 - Date.now()) : '0';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async refreshToken(): Promise<void> {
|
|
52
|
+
await this.userService.refreshToken();
|
|
53
|
+
this.updateData();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public async addContact(): Promise<void> {
|
|
57
|
+
const value: string = prompt('Please enter a phone number or email address to add');
|
|
58
|
+
const newContact: WardenContact = WardenUtils.stringToWardenContact(value);
|
|
59
|
+
if (newContact) {
|
|
60
|
+
const rval: boolean = await this.userService.addContactToLoggedInUser(newContact);
|
|
61
|
+
if (rval) {
|
|
62
|
+
await this.userService.refreshToken();
|
|
63
|
+
} else {
|
|
64
|
+
Logger.info('Add contact failed : %s', value);
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
Logger.info('No contact found for %s', value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public async addWebAuthnDevice(): Promise<void> {
|
|
72
|
+
await this.userService.saveCurrentDeviceAsWebAuthnForCurrentUser();
|
|
73
|
+
await this.userService.refreshToken();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public async removeContact(ct: WardenContact): Promise<void> {
|
|
77
|
+
Logger.info('Remove %j', ct);
|
|
78
|
+
await this.userService.removeContactFromLoggedInUser(ct);
|
|
79
|
+
await this.userService.refreshToken();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public async removeWebAuthn(webId: WardenWebAuthnEntrySummary): Promise<void> {
|
|
83
|
+
Logger.info('Remove webauthn: %s', webId);
|
|
84
|
+
await this.userService.removeWebAuthnRegistrationFromLoggedInUser(webId.credentialIdBase64);
|
|
85
|
+
await this.userService.refreshToken();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public async exportWebAuthn(origin: string): Promise<string> {
|
|
89
|
+
Logger.info('exportWebAuthn: %s', origin);
|
|
90
|
+
const newValue: string = await this.userService.exportWebAuthnRegistrationEntryForLoggedInUser(origin);
|
|
91
|
+
alert(newValue);
|
|
92
|
+
Logger.info('Export token is %s', newValue);
|
|
93
|
+
return newValue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public async importWebAuthn(): Promise<boolean> {
|
|
97
|
+
let rval: boolean = false;
|
|
98
|
+
const value = prompt('Input the web authn token to import');
|
|
99
|
+
if (StringRatchet.trimToNull(value)) {
|
|
100
|
+
Logger.info('importWebAuthn: %s', origin);
|
|
101
|
+
rval = await this.userService.importWebAuthnRegistrationEntryForLoggedInUser(value);
|
|
102
|
+
}
|
|
103
|
+
Logger.info('Import returned is %s', rval);
|
|
104
|
+
return rval;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public webAuthLabel(webId: WardenWebAuthnEntrySummary): string {
|
|
108
|
+
let rval: string = 'Error - missing';
|
|
109
|
+
if (webId) {
|
|
110
|
+
rval = webId.credentialIdBase64.substring(0, 4).toUpperCase() + ' : ';
|
|
111
|
+
rval += StringRatchet.trimToNull(webId.applicationName) ? webId.applicationName + ' ' : '';
|
|
112
|
+
rval += StringRatchet.trimToNull(webId.deviceLabel) ? webId.deviceLabel + ' ' : '';
|
|
113
|
+
rval += StringRatchet.trimToNull(webId.origin) ? webId.origin + ' ' : '';
|
|
114
|
+
}
|
|
115
|
+
return rval;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
performLogout(): void {
|
|
119
|
+
this.userService.logout();
|
|
120
|
+
this.router.navigate(['/public/login']).then(No.op);
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { InjectionToken } from "@angular/core";
|
|
2
|
+
|
|
3
|
+
export const ACUTE_WARDEN_LOGIN_PATH: InjectionToken<string> = new InjectionToken<string>('ACUTE_WARDEN_LOGIN_PATH');
|
|
4
|
+
export const ACUTE_WARDEN_DEFAULT_POST_LOGIN_PATH: InjectionToken<string> = new InjectionToken<string>('ACUTE_WARDEN_DEFAULT_POST_LOGIN_PATH');
|
|
5
|
+
export const ACUTE_WARDEN_DEFAULT_IF_LOGGED_IN_PATH: InjectionToken<string> = new InjectionToken<string>('ACUTE_WARDEN_DEFAULT_IF_LOGGED_IN_PATH');
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Inject, Injectable } from "@angular/core";
|
|
2
|
+
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
|
|
3
|
+
import { Observable } from "rxjs";
|
|
4
|
+
import { WardenUserService } from "@bitblit/ratchet-warden-common/client/warden-user-service";
|
|
5
|
+
import { No } from "@bitblit/ratchet-common/lang/no";
|
|
6
|
+
import { ACUTE_WARDEN_DEFAULT_POST_LOGIN_PATH, ACUTE_WARDEN_LOGIN_PATH } from "../constants.ts";
|
|
7
|
+
import { RequireRatchet } from "@bitblit/ratchet-common/lang/require-ratchet";
|
|
8
|
+
|
|
9
|
+
@Injectable()
|
|
10
|
+
export class LoggedInGuard implements CanActivate {
|
|
11
|
+
constructor(
|
|
12
|
+
private userService: WardenUserService<any>,
|
|
13
|
+
private router: Router,
|
|
14
|
+
@Inject(ACUTE_WARDEN_LOGIN_PATH) private loginPath: string,
|
|
15
|
+
@Inject(ACUTE_WARDEN_DEFAULT_POST_LOGIN_PATH) private defaultPostLoginPath: string
|
|
16
|
+
) {
|
|
17
|
+
RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(this.loginPath, 'ACUTE_WARDEN_LOGIN_PATH');
|
|
18
|
+
RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(this.defaultPostLoginPath, 'ACUTE_WARDEN_DEFAULT_POST_LOGIN_PATH');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
|
|
22
|
+
if (!this.userService.isLoggedIn()) {
|
|
23
|
+
// Storing post-login redirect is handling by the login provider
|
|
24
|
+
const target: string = state.url === this.loginPath ? this.defaultPostLoginPath : state.url;
|
|
25
|
+
this.router.navigate([this.loginPath], { queryParams: { returnUrl: target } }).then(No.op);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Inject, Injectable } from "@angular/core";
|
|
2
|
+
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
|
|
3
|
+
import { Observable } from "rxjs";
|
|
4
|
+
|
|
5
|
+
import { Logger } from "@bitblit/ratchet-common/logger/logger";
|
|
6
|
+
import { WardenUserService } from "@bitblit/ratchet-warden-common/client/warden-user-service";
|
|
7
|
+
import { No } from "@bitblit/ratchet-common/lang/no";
|
|
8
|
+
import { ACUTE_WARDEN_DEFAULT_IF_LOGGED_IN_PATH, ACUTE_WARDEN_LOGIN_PATH } from "../constants.ts";
|
|
9
|
+
import { RequireRatchet } from "@bitblit/ratchet-common/lang/require-ratchet";
|
|
10
|
+
|
|
11
|
+
@Injectable()
|
|
12
|
+
export class NotLoggedInGuard implements CanActivate {
|
|
13
|
+
constructor(
|
|
14
|
+
private userService: WardenUserService<any>,
|
|
15
|
+
private router: Router,
|
|
16
|
+
@Inject(ACUTE_WARDEN_DEFAULT_IF_LOGGED_IN_PATH) private defaultIfLoggedInPath: string,
|
|
17
|
+
@Inject(ACUTE_WARDEN_LOGIN_PATH) private loginPath: string
|
|
18
|
+
) {
|
|
19
|
+
RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(this.defaultIfLoggedInPath, 'ACUTE_WARDEN_DEFAULT_IF_LOGGED_IN_PATH');
|
|
20
|
+
RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(this.loginPath, 'ACUTE_WARDEN_LOGIN_PATH');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
canActivate(route: ActivatedRouteSnapshot, _state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
|
|
24
|
+
Logger.info('--- %j', route.queryParamMap);
|
|
25
|
+
if (route.queryParamMap.get('logout') === 'true') {
|
|
26
|
+
Logger.info('Logging out...');
|
|
27
|
+
this.userService.logout();
|
|
28
|
+
this.router.navigate([this.loginPath]).then(No.op);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (this.userService.isLoggedIn()) {
|
|
32
|
+
// Redirect to the default or saved
|
|
33
|
+
const target: string = this.defaultIfLoggedInPath;
|
|
34
|
+
|
|
35
|
+
Logger.info('NotLoggedInGuard fail(logged in) : Redirecting to logged in %s', target);
|
|
36
|
+
this.router.navigate([target]).then(No.op);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './constants';
|
|
2
|
+
|
|
3
|
+
export * from './build/ngx-acute-warden-info';
|
|
4
|
+
|
|
5
|
+
export * from './components/acute-create-user/acute-create-user.component';
|
|
6
|
+
export * from './components/acute-login/acute-login.component';
|
|
7
|
+
export * from './components/acute-magic-lander/acute-magic-lander.component';
|
|
8
|
+
export * from './components/acute-user-profile/acute-user-profile.component';
|
|
9
|
+
|
|
10
|
+
export * from './services/warden-adapter.service';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { beforeEach, describe, expect, test } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { WardenAdapterService } from './warden-adapter.service';
|
|
5
|
+
|
|
6
|
+
describe('WardenAdapterService', () => {
|
|
7
|
+
let service: WardenAdapterService;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
TestBed.configureTestingModule({});
|
|
11
|
+
service = TestBed.inject(WardenAdapterService);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test.skip('should be created', () => {
|
|
15
|
+
expect(service).toBeTruthy();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { Logger } from '@bitblit/ratchet-common/logger/logger';
|
|
3
|
+
import { No } from '@bitblit/ratchet-common/lang/no';
|
|
4
|
+
import { Router } from '@angular/router';
|
|
5
|
+
import { WardenUserServiceEventProcessingProvider } from '@bitblit/ratchet-warden-common/client/provider/warden-user-service-event-processing-provider';
|
|
6
|
+
import { WardenLoggedInUserWrapper } from '@bitblit/ratchet-warden-common/client/provider/warden-logged-in-user-wrapper';
|
|
7
|
+
import { WardenContact } from '@bitblit/ratchet-warden-common/common/model/warden-contact';
|
|
8
|
+
import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet';
|
|
9
|
+
import { WardenClient } from '@bitblit/ratchet-warden-common/client/warden-client';
|
|
10
|
+
|
|
11
|
+
@Injectable({ providedIn: 'root' })
|
|
12
|
+
export class WardenAdapterService implements WardenUserServiceEventProcessingProvider<any> {
|
|
13
|
+
constructor(
|
|
14
|
+
public router: Router,
|
|
15
|
+
public wardenClient: WardenClient,
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
public onAutomaticLogout(): void {
|
|
19
|
+
// Make sure we leave
|
|
20
|
+
this.router.navigate(['/public/login']).then(No.op);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public onAutomaticTokenRefresh(_refreshUser: WardenLoggedInUserWrapper<any>): void {
|
|
24
|
+
Logger.info('User token refreshed');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public onLoginFailure(reason: string): void {
|
|
28
|
+
Logger.info('Failed to login: %s', reason);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public onLogout(): void {
|
|
32
|
+
// Make sure we leave
|
|
33
|
+
this.router.navigate(['/public/login']).then(No.op);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public onSuccessfulLogin(newUser: WardenLoggedInUserWrapper<any>): void {
|
|
37
|
+
Logger.info('Logged in as %s', newUser?.userObject?.wardenData?.userLabel);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public async sendMagicLink(contact: WardenContact, magicLanderUrl: string, postLoginUrl?: string): Promise<void> {
|
|
41
|
+
RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(magicLanderUrl, 'magicLanderUrl may not be empty');
|
|
42
|
+
|
|
43
|
+
const mUrl: URL = new URL(magicLanderUrl);
|
|
44
|
+
const param: URLSearchParams = new URLSearchParams(mUrl.search);
|
|
45
|
+
if (!param.has('code')) {
|
|
46
|
+
param.set('code', '{CODE}');
|
|
47
|
+
}
|
|
48
|
+
if (!param.has('meta')) {
|
|
49
|
+
param.set('meta', '{META}');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
mUrl.search = param.toString();
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
let curUrl: string = loc.toString();
|
|
56
|
+
curUrl = curUrl.indexOf('?') > -1 ? curUrl.substring(0, curUrl.indexOf('?')) : curUrl;
|
|
57
|
+
let landingUrl: string = curUrl.split('public/login').join('public/magic-lander');
|
|
58
|
+
landingUrl += '?code={CODE}&meta={META}';
|
|
59
|
+
*/
|
|
60
|
+
const meta: Record<string, string> = {
|
|
61
|
+
redirect: postLoginUrl,
|
|
62
|
+
};
|
|
63
|
+
const sent: boolean = await this.wardenClient.sendMagicLink(contact, mUrl.toString(), meta);
|
|
64
|
+
|
|
65
|
+
Logger.info('Sent was %s : %s', sent, mUrl.toString());
|
|
66
|
+
}
|
|
67
|
+
}
|