@bitblit/ngx-acute-warden 6.0.145-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 CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@bitblit/ngx-acute-warden",
3
- "version": "6.0.145-alpha",
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.145-alpha",
52
- "@bitblit/ratchet-common": "6.0.145-alpha",
53
- "@bitblit/ratchet-warden-common": "6.0.145-alpha",
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.145-alpha",
71
- "@bitblit/ratchet-common": "6.0.145-alpha",
72
- "@bitblit/ratchet-warden-common": "6.0.145-alpha",
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.145-alpha"
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
+ }
@@ -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,3 @@
1
+ export interface WardenAdapterServiceConfig {
2
+ postLogoutUrl: string;
3
+ }
@@ -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
+ }