@nauticana/sail 0.8.0 → 0.8.1
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/README.md +120 -15
- package/package.json +2 -2
- package/src/component/abstract/base_async.ts +8 -2
- package/src/component/abstract/base_table.ts +94 -35
- package/src/component/abstract/base_view.ts +28 -41
- package/src/component/billing/checkout_button.ts +1 -1
- package/src/component/billing/payment_methods.ts +1 -1
- package/src/component/billing/plan_selector.ts +1 -1
- package/src/component/form/form_field.ts +8 -11
- package/src/component/form/form_record.html +2 -2
- package/src/component/form/form_record.ts +1 -1
- package/src/component/form/form_table.html +1 -1
- package/src/component/form/form_table.ts +1 -1
- package/src/component/login/account_deletion.ts +1 -1
- package/src/component/login/chpass_component.ts +11 -16
- package/src/component/login/confirm_chpass_component.ts +9 -15
- package/src/component/login/confirm_register_component.ts +9 -15
- package/src/component/login/consent_gate.ts +1 -1
- package/src/component/login/login_component.html +1 -1
- package/src/component/login/login_component.ts +1 -1
- package/src/component/login/otp_input.ts +1 -1
- package/src/component/login/register_component.ts +1 -1
- package/src/component/login/social_login.ts +1 -1
- package/src/component/navigation/navigation.ts +10 -11
- package/src/component/security/trusted_devices.ts +1 -1
- package/src/component/security/twofactor_setup.ts +18 -26
- package/src/component/security/twofactor_verify.ts +10 -12
- package/src/component/table/table_detail.html +4 -4
- package/src/component/table/table_detail.ts +22 -34
- package/src/component/table/table_edit.html +13 -3
- package/src/component/table/table_edit.ts +17 -17
- package/src/component/table/table_list.ts +18 -52
- package/src/component/table/table_lookup.html +1 -1
- package/src/component/table/table_lookup.ts +1 -3
- package/src/component/table/table_report.ts +11 -14
- package/src/component/table/table_search.html +1 -1
- package/src/component/table/table_search.ts +14 -38
- package/src/index.ts +2 -0
- package/src/model/appdata.ts +2 -2
- package/src/model/common.ts +39 -1
- package/src/service/auth.service.ts +26 -26
- package/src/service/base_rest.service.ts +20 -0
- package/src/service/billing.service.ts +3 -9
- package/src/service/payout.service.ts +3 -9
- package/src/service/rest_service.ts +5 -5
- package/src/service/user_payment_method.service.ts +4 -8
- package/src/util/text.ts +19 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
<div class="form-container">
|
|
2
2
|
@for (key of computedFields(); track key) {
|
|
3
3
|
@if (!isArray(record()[key])) {
|
|
4
|
-
<dynamic-field
|
|
4
|
+
<sail-dynamic-field
|
|
5
5
|
[tableName]="tableName()"
|
|
6
6
|
[value]="record()[key]"
|
|
7
7
|
(valueChange)="record()[key]=$event"
|
|
8
8
|
(recordUpdate)="onRecordUpdate($event)"
|
|
9
9
|
[field]="key"
|
|
10
10
|
[readonly]="isReadOnly()(key)">
|
|
11
|
-
</dynamic-field>
|
|
11
|
+
</sail-dynamic-field>
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
</div>
|
|
@@ -4,7 +4,7 @@ import { BaseAuthService } from "../../service/auth.service";
|
|
|
4
4
|
import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from "../../config";
|
|
5
5
|
|
|
6
6
|
@Component({
|
|
7
|
-
selector: 'record-form',
|
|
7
|
+
selector: 'sail-record-form',
|
|
8
8
|
templateUrl: './form_record.html',
|
|
9
9
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
10
10
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
</button>
|
|
12
12
|
</h1>
|
|
13
13
|
<div mat-dialog-content>
|
|
14
|
-
<record-form [tableName]="tableName()" [record]="editableRecord" [isReadOnly]="isReadOnlyBound"></record-form>
|
|
14
|
+
<sail-record-form [tableName]="tableName()" [record]="editableRecord" [isReadOnly]="isReadOnlyBound"></sail-record-form>
|
|
15
15
|
</div>
|
|
16
16
|
<div mat-dialog-actions align="end">
|
|
17
17
|
<button matButton="elevated" class="primary" type="submit" (click)="onSave()">Save</button>
|
|
@@ -24,7 +24,7 @@ import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
|
|
|
24
24
|
* `/login/local`).
|
|
25
25
|
*/
|
|
26
26
|
@Component({
|
|
27
|
-
selector: '
|
|
27
|
+
selector: 'sail-account-deletion',
|
|
28
28
|
templateUrl: './account_deletion.html',
|
|
29
29
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
30
30
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -7,9 +7,10 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
|
|
7
7
|
import { MatInputModule } from '@angular/material/input';
|
|
8
8
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
|
9
9
|
import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
|
|
10
|
+
import { BaseAsync } from '../abstract/base_async';
|
|
10
11
|
|
|
11
12
|
@Component({
|
|
12
|
-
selector: '
|
|
13
|
+
selector: 'sail-chpass',
|
|
13
14
|
templateUrl: './chpass_component.html',
|
|
14
15
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
15
16
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -22,15 +23,13 @@ import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
|
|
|
22
23
|
RouterLink,
|
|
23
24
|
],
|
|
24
25
|
})
|
|
25
|
-
export class ChpassComponent {
|
|
26
|
+
export class ChpassComponent extends BaseAsync {
|
|
26
27
|
private auth = inject(BaseAuthService);
|
|
27
28
|
private fb = inject(FormBuilder);
|
|
28
29
|
private router = inject(Router);
|
|
29
30
|
protected readonly guiConfig: SailGuiConfig = inject(SAIL_GUI_CONFIG, {optional: true}) ?? DEFAULT_CONFIG;
|
|
30
31
|
|
|
31
32
|
forgotPassword = signal(false);
|
|
32
|
-
errorMessage = signal('');
|
|
33
|
-
successMessage = signal('');
|
|
34
33
|
|
|
35
34
|
chpassForm = this.fb.group({
|
|
36
35
|
username: ['', Validators.required],
|
|
@@ -62,9 +61,6 @@ export class ChpassComponent {
|
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
changePassword(): void {
|
|
65
|
-
this.errorMessage.set('');
|
|
66
|
-
this.successMessage.set('');
|
|
67
|
-
|
|
68
64
|
if (this.chpassForm.invalid) return;
|
|
69
65
|
|
|
70
66
|
const { username, old_password, new_password, confirm_password } = this.chpassForm.value;
|
|
@@ -74,23 +70,22 @@ export class ChpassComponent {
|
|
|
74
70
|
return;
|
|
75
71
|
}
|
|
76
72
|
|
|
77
|
-
const
|
|
73
|
+
const isForgot = this.forgotPassword();
|
|
74
|
+
const request = isForgot
|
|
78
75
|
? this.auth.forgotPassword(username!)
|
|
79
76
|
: this.auth.chpass(username!, new_password!, old_password!);
|
|
80
77
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
this.run(
|
|
79
|
+
request,
|
|
80
|
+
() => {
|
|
81
|
+
if (isForgot) {
|
|
84
82
|
this.successMessage.set('A confirmation code has been sent to your email.');
|
|
85
83
|
this.router.navigate(['/confirm/password'], { queryParams: { username } });
|
|
86
84
|
} else {
|
|
87
85
|
this.successMessage.set('Password changed successfully.');
|
|
88
86
|
}
|
|
89
87
|
},
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
this.errorMessage.set(msg || 'Password change failed. Please try again.');
|
|
93
|
-
},
|
|
94
|
-
});
|
|
88
|
+
'Password change failed. Please try again.',
|
|
89
|
+
);
|
|
95
90
|
}
|
|
96
91
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy, Component, inject,
|
|
1
|
+
import { ChangeDetectionStrategy, Component, inject, OnInit, ViewEncapsulation } from '@angular/core';
|
|
2
2
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
3
3
|
import { BaseAuthService } from '../../service/auth.service';
|
|
4
4
|
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
|
@@ -6,9 +6,10 @@ import { MatButtonModule } from '@angular/material/button';
|
|
|
6
6
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
7
7
|
import { MatInputModule } from '@angular/material/input';
|
|
8
8
|
import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
|
|
9
|
+
import { BaseAsync } from '../abstract/base_async';
|
|
9
10
|
|
|
10
11
|
@Component({
|
|
11
|
-
selector: '
|
|
12
|
+
selector: 'sail-confirm-chpass',
|
|
12
13
|
templateUrl: './confirm_chpass_component.html',
|
|
13
14
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
14
15
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -20,16 +21,13 @@ import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
|
|
|
20
21
|
RouterLink,
|
|
21
22
|
],
|
|
22
23
|
})
|
|
23
|
-
export class ConfirmChpassComponent implements OnInit {
|
|
24
|
+
export class ConfirmChpassComponent extends BaseAsync implements OnInit {
|
|
24
25
|
private auth = inject(BaseAuthService);
|
|
25
26
|
private fb = inject(FormBuilder);
|
|
26
27
|
private router = inject(Router);
|
|
27
28
|
private route = inject(ActivatedRoute);
|
|
28
29
|
protected readonly guiConfig: SailGuiConfig = inject(SAIL_GUI_CONFIG, {optional: true}) ?? DEFAULT_CONFIG;
|
|
29
30
|
|
|
30
|
-
errorMessage = signal('');
|
|
31
|
-
successMessage = signal('');
|
|
32
|
-
|
|
33
31
|
confirmForm = this.fb.group({
|
|
34
32
|
username: ['', Validators.required],
|
|
35
33
|
code: ['', Validators.required],
|
|
@@ -45,9 +43,6 @@ export class ConfirmChpassComponent implements OnInit {
|
|
|
45
43
|
}
|
|
46
44
|
|
|
47
45
|
confirm(): void {
|
|
48
|
-
this.errorMessage.set('');
|
|
49
|
-
this.successMessage.set('');
|
|
50
|
-
|
|
51
46
|
if (this.confirmForm.invalid) return;
|
|
52
47
|
|
|
53
48
|
const { username, code, new_password, confirm_password } = this.confirmForm.value;
|
|
@@ -57,14 +52,13 @@ export class ConfirmChpassComponent implements OnInit {
|
|
|
57
52
|
return;
|
|
58
53
|
}
|
|
59
54
|
|
|
60
|
-
this.
|
|
61
|
-
|
|
55
|
+
this.run(
|
|
56
|
+
this.auth.confirmChpass(username!, code!, new_password!),
|
|
57
|
+
() => {
|
|
62
58
|
this.successMessage.set('Password changed successfully. Redirecting to login...');
|
|
63
59
|
this.router.navigate(['/login/local']);
|
|
64
60
|
},
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
},
|
|
68
|
-
});
|
|
61
|
+
'Confirmation failed. Please try again.',
|
|
62
|
+
);
|
|
69
63
|
}
|
|
70
64
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy, Component, inject,
|
|
1
|
+
import { ChangeDetectionStrategy, Component, inject, OnInit, ViewEncapsulation } from '@angular/core';
|
|
2
2
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
3
3
|
import { BaseAuthService } from '../../service/auth.service';
|
|
4
4
|
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
|
@@ -6,9 +6,10 @@ import { MatButtonModule } from '@angular/material/button';
|
|
|
6
6
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
7
7
|
import { MatInputModule } from '@angular/material/input';
|
|
8
8
|
import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
|
|
9
|
+
import { BaseAsync } from '../abstract/base_async';
|
|
9
10
|
|
|
10
11
|
@Component({
|
|
11
|
-
selector: '
|
|
12
|
+
selector: 'sail-confirm-register',
|
|
12
13
|
templateUrl: './confirm_register_component.html',
|
|
13
14
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
14
15
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -20,16 +21,13 @@ import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
|
|
|
20
21
|
RouterLink,
|
|
21
22
|
],
|
|
22
23
|
})
|
|
23
|
-
export class ConfirmRegisterComponent implements OnInit {
|
|
24
|
+
export class ConfirmRegisterComponent extends BaseAsync implements OnInit {
|
|
24
25
|
private auth = inject(BaseAuthService);
|
|
25
26
|
private fb = inject(FormBuilder);
|
|
26
27
|
private router = inject(Router);
|
|
27
28
|
private route = inject(ActivatedRoute);
|
|
28
29
|
protected readonly guiConfig: SailGuiConfig = inject(SAIL_GUI_CONFIG, {optional: true}) ?? DEFAULT_CONFIG;
|
|
29
30
|
|
|
30
|
-
errorMessage = signal('');
|
|
31
|
-
successMessage = signal('');
|
|
32
|
-
|
|
33
31
|
confirmForm = this.fb.group({
|
|
34
32
|
email: ['', [Validators.required, Validators.email]],
|
|
35
33
|
code: ['', Validators.required],
|
|
@@ -43,15 +41,13 @@ export class ConfirmRegisterComponent implements OnInit {
|
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
confirm(): void {
|
|
46
|
-
this.errorMessage.set('');
|
|
47
|
-
this.successMessage.set('');
|
|
48
|
-
|
|
49
44
|
if (this.confirmForm.invalid) return;
|
|
50
45
|
|
|
51
46
|
const { email, code } = this.confirmForm.value;
|
|
52
47
|
|
|
53
|
-
this.
|
|
54
|
-
|
|
48
|
+
this.run(
|
|
49
|
+
this.auth.confirmRegister(email!, code!),
|
|
50
|
+
(resp) => {
|
|
55
51
|
this.successMessage.set('Registration confirmed.');
|
|
56
52
|
if (resp?.paymentRequired && resp.paymentUrl) {
|
|
57
53
|
// Paid plan: go straight to payment. Subscription is 'P' until paid.
|
|
@@ -63,9 +59,7 @@ export class ConfirmRegisterComponent implements OnInit {
|
|
|
63
59
|
// Free plan: land on login.
|
|
64
60
|
this.router.navigate(['/login/local']);
|
|
65
61
|
},
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
},
|
|
69
|
-
});
|
|
62
|
+
'Confirmation failed. Please try again.',
|
|
63
|
+
);
|
|
70
64
|
}
|
|
71
65
|
}
|
|
@@ -22,7 +22,7 @@ import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
|
|
|
22
22
|
* See keel/README.md → "Consent Capture" for the backend-side contract.
|
|
23
23
|
*/
|
|
24
24
|
@Component({
|
|
25
|
-
selector: '
|
|
25
|
+
selector: 'sail-consent-gate',
|
|
26
26
|
templateUrl: './consent_gate.html',
|
|
27
27
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
28
28
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -10,7 +10,7 @@ import { LoginResponseSocial, SocialProvider } from '../../model/auth';
|
|
|
10
10
|
import { SocialLoginComponent } from './social_login';
|
|
11
11
|
|
|
12
12
|
@Component({
|
|
13
|
-
selector: '
|
|
13
|
+
selector: 'sail-login',
|
|
14
14
|
templateUrl: './login_component.html',
|
|
15
15
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
16
16
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -13,7 +13,7 @@ import { MatIconModule } from '@angular/material/icon';
|
|
|
13
13
|
* call verifyOtp / resendOtp. See README "Phone / email OTP login".
|
|
14
14
|
*/
|
|
15
15
|
@Component({
|
|
16
|
-
selector: '
|
|
16
|
+
selector: 'sail-otp-input',
|
|
17
17
|
templateUrl: './otp_input.html',
|
|
18
18
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
19
19
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -15,7 +15,7 @@ import { PartnerRegistration, PublicPlan } from '../../model/appdata';
|
|
|
15
15
|
import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
|
|
16
16
|
|
|
17
17
|
@Component({
|
|
18
|
-
selector: '
|
|
18
|
+
selector: 'sail-register',
|
|
19
19
|
templateUrl: './register_component.html',
|
|
20
20
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
21
21
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -27,7 +27,7 @@ const APPLE_URL = 'https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/
|
|
|
27
27
|
* without config are hidden (with a console.warn for diagnostics).
|
|
28
28
|
*/
|
|
29
29
|
@Component({
|
|
30
|
-
selector: '
|
|
30
|
+
selector: 'sail-social-login',
|
|
31
31
|
templateUrl: './social_login.html',
|
|
32
32
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
33
33
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy, Component, computed, inject,
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, inject, ViewEncapsulation } from "@angular/core";
|
|
2
2
|
import { toSignal } from "@angular/core/rxjs-interop";
|
|
3
3
|
import { RouterLink, RouterOutlet } from "@angular/router";
|
|
4
4
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
|
@@ -14,7 +14,7 @@ import { BaseAuthService } from "../../service/auth.service";
|
|
|
14
14
|
import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from "../../config";
|
|
15
15
|
|
|
16
16
|
@Component({
|
|
17
|
-
selector: '
|
|
17
|
+
selector: 'sail-navigation',
|
|
18
18
|
templateUrl: './navigation.html',
|
|
19
19
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
20
20
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -29,16 +29,20 @@ import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from "../../config";
|
|
|
29
29
|
MatExpansionModule,
|
|
30
30
|
],
|
|
31
31
|
})
|
|
32
|
-
export class Navigation
|
|
32
|
+
export class Navigation {
|
|
33
33
|
private readonly authService = inject(BaseAuthService);
|
|
34
34
|
private readonly breakpointObserver = inject(BreakpointObserver);
|
|
35
35
|
protected readonly guiConfig: SailGuiConfig = inject(SAIL_GUI_CONFIG, {optional: true}) ?? DEFAULT_CONFIG;
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
// Restore the stored JWT before the menu stream fires so `canAccess()` checks
|
|
38
|
+
// resolve against the right user. Done at field-init time (runs in injection
|
|
39
|
+
// context) rather than ngOnInit, so the toSignal below can read from it.
|
|
40
|
+
private readonly _sessionLoaded = (this.authService.loadStoredSession(), true);
|
|
41
|
+
|
|
42
|
+
private readonly allMenuItems = toSignal(this.authService.getMenus(), { initialValue: [] as ApplicationMenu[] });
|
|
38
43
|
|
|
39
44
|
readonly menuItems = computed(() => {
|
|
40
|
-
|
|
41
|
-
return menus
|
|
45
|
+
return this.allMenuItems()
|
|
42
46
|
.map(menu => ({
|
|
43
47
|
...menu,
|
|
44
48
|
ApplicationMenuItems: (menu.ApplicationMenuItems ?? [])
|
|
@@ -54,11 +58,6 @@ export class Navigation implements OnInit {
|
|
|
54
58
|
|
|
55
59
|
readonly isLoggedIn = this.authService.isLoggedIn;
|
|
56
60
|
|
|
57
|
-
ngOnInit() {
|
|
58
|
-
this.authService.loadStoredSession();
|
|
59
|
-
this.authService.getMenus().subscribe((menuItems: ApplicationMenu[]) => { this.allMenuItems.set(menuItems); });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
61
|
logout() {
|
|
63
62
|
this.authService.logout();
|
|
64
63
|
}
|
|
@@ -10,7 +10,7 @@ import { MatIconModule } from '@angular/material/icon';
|
|
|
10
10
|
import { RouterLink } from '@angular/router';
|
|
11
11
|
|
|
12
12
|
@Component({
|
|
13
|
-
selector: '
|
|
13
|
+
selector: 'sail-trusted-devices',
|
|
14
14
|
templateUrl: './trusted_devices.html',
|
|
15
15
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
16
16
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -5,9 +5,10 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
|
|
5
5
|
import { MatInputModule } from '@angular/material/input';
|
|
6
6
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
7
7
|
import { RouterLink } from '@angular/router';
|
|
8
|
+
import { BaseAsync } from '../abstract/base_async';
|
|
8
9
|
|
|
9
10
|
@Component({
|
|
10
|
-
selector: '
|
|
11
|
+
selector: 'sail-twofactor-setup',
|
|
11
12
|
templateUrl: './twofactor_setup.html',
|
|
12
13
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
13
14
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -19,7 +20,7 @@ import { RouterLink } from '@angular/router';
|
|
|
19
20
|
RouterLink,
|
|
20
21
|
],
|
|
21
22
|
})
|
|
22
|
-
export class TwoFactorSetupComponent {
|
|
23
|
+
export class TwoFactorSetupComponent extends BaseAsync {
|
|
23
24
|
private readonly auth = inject(BaseAuthService);
|
|
24
25
|
private readonly fb = inject(FormBuilder);
|
|
25
26
|
|
|
@@ -27,8 +28,6 @@ export class TwoFactorSetupComponent {
|
|
|
27
28
|
readonly qrUri = signal('');
|
|
28
29
|
readonly secret = signal('');
|
|
29
30
|
readonly backupCodes = signal<string[]>([]);
|
|
30
|
-
readonly errorMessage = signal('');
|
|
31
|
-
readonly successMessage = signal('');
|
|
32
31
|
|
|
33
32
|
// setup2FA requires the user's current password (or current TOTP code
|
|
34
33
|
// if 2FA already enabled — pass twoFactorCode instead). Disable additionally
|
|
@@ -48,20 +47,18 @@ export class TwoFactorSetupComponent {
|
|
|
48
47
|
|
|
49
48
|
startSetup() {
|
|
50
49
|
if (this.initForm.invalid) return;
|
|
51
|
-
this.errorMessage.set('');
|
|
52
50
|
const password = this.initForm.value.password!;
|
|
53
|
-
this.
|
|
54
|
-
|
|
51
|
+
this.run(
|
|
52
|
+
this.auth.setup2FA({ password }),
|
|
53
|
+
(res) => {
|
|
55
54
|
this.qrUri.set(res.qrUri);
|
|
56
55
|
this.secret.set(res.secret);
|
|
57
56
|
this.backupCodes.set(res.backupCodes);
|
|
58
57
|
this.step.set('qr');
|
|
59
58
|
this.initForm.reset();
|
|
60
59
|
},
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
},
|
|
64
|
-
});
|
|
60
|
+
'2FA setup failed.',
|
|
61
|
+
);
|
|
65
62
|
}
|
|
66
63
|
|
|
67
64
|
proceedToVerify() {
|
|
@@ -70,11 +67,10 @@ export class TwoFactorSetupComponent {
|
|
|
70
67
|
|
|
71
68
|
confirmSetup() {
|
|
72
69
|
if (this.verifyForm.invalid) return;
|
|
73
|
-
this.errorMessage.set('');
|
|
74
|
-
|
|
75
70
|
const { code } = this.verifyForm.value;
|
|
76
|
-
this.
|
|
77
|
-
|
|
71
|
+
this.run(
|
|
72
|
+
this.auth.verify2FA({ code: code! }),
|
|
73
|
+
(res) => {
|
|
78
74
|
if (res.valid) {
|
|
79
75
|
this.successMessage.set('Two-factor authentication enabled.');
|
|
80
76
|
this.step.set('done');
|
|
@@ -82,25 +78,21 @@ export class TwoFactorSetupComponent {
|
|
|
82
78
|
this.errorMessage.set('Invalid code. Please try again.');
|
|
83
79
|
}
|
|
84
80
|
},
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
},
|
|
88
|
-
});
|
|
81
|
+
'Verification failed.',
|
|
82
|
+
);
|
|
89
83
|
}
|
|
90
84
|
|
|
91
85
|
disable2FA() {
|
|
92
86
|
if (this.disableForm.invalid) return;
|
|
93
|
-
this.errorMessage.set('');
|
|
94
87
|
const { password, code } = this.disableForm.value;
|
|
95
|
-
this.
|
|
96
|
-
|
|
88
|
+
this.run(
|
|
89
|
+
this.auth.disable2FA(password!, code!),
|
|
90
|
+
() => {
|
|
97
91
|
this.successMessage.set('Two-factor authentication disabled.');
|
|
98
92
|
this.step.set('init');
|
|
99
93
|
this.disableForm.reset();
|
|
100
94
|
},
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
},
|
|
104
|
-
});
|
|
95
|
+
'Failed to disable 2FA.',
|
|
96
|
+
);
|
|
105
97
|
}
|
|
106
98
|
}
|
|
@@ -7,9 +7,10 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
|
|
|
7
7
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
8
8
|
import { RouterLink } from '@angular/router';
|
|
9
9
|
import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
|
|
10
|
+
import { BaseAsync } from '../abstract/base_async';
|
|
10
11
|
|
|
11
12
|
@Component({
|
|
12
|
-
selector: '
|
|
13
|
+
selector: 'sail-twofactor-verify',
|
|
13
14
|
templateUrl: './twofactor_verify.html',
|
|
14
15
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
15
16
|
encapsulation: ViewEncapsulation.None,
|
|
@@ -22,13 +23,12 @@ import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
|
|
|
22
23
|
RouterLink,
|
|
23
24
|
],
|
|
24
25
|
})
|
|
25
|
-
export class TwoFactorVerifyComponent {
|
|
26
|
+
export class TwoFactorVerifyComponent extends BaseAsync {
|
|
26
27
|
private readonly auth = inject(BaseAuthService);
|
|
27
28
|
private readonly fb = inject(FormBuilder);
|
|
28
29
|
protected readonly guiConfig: SailGuiConfig = inject(SAIL_GUI_CONFIG, {optional: true}) ?? DEFAULT_CONFIG;
|
|
29
30
|
|
|
30
31
|
readonly useBackupCode = signal(false);
|
|
31
|
-
readonly errorMessage = signal('');
|
|
32
32
|
|
|
33
33
|
readonly verifyForm = this.fb.group({
|
|
34
34
|
code: ['', [Validators.required, Validators.minLength(6)]],
|
|
@@ -38,18 +38,17 @@ export class TwoFactorVerifyComponent {
|
|
|
38
38
|
toggleBackupCode() {
|
|
39
39
|
this.useBackupCode.update(v => !v);
|
|
40
40
|
this.verifyForm.controls.code.reset();
|
|
41
|
-
this.
|
|
41
|
+
this.clearMessages();
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
verify() {
|
|
45
45
|
if (this.verifyForm.invalid) return;
|
|
46
|
-
this.errorMessage.set('');
|
|
47
|
-
|
|
48
46
|
const { code, trustDevice } = this.verifyForm.value;
|
|
49
47
|
|
|
50
48
|
if (this.useBackupCode()) {
|
|
51
|
-
this.
|
|
52
|
-
|
|
49
|
+
this.run(
|
|
50
|
+
this.auth.verifyBackupCode(code!),
|
|
51
|
+
(res) => {
|
|
53
52
|
if (res.valid && res.token) {
|
|
54
53
|
localStorage.removeItem('loginToken');
|
|
55
54
|
this.auth.token = res.token;
|
|
@@ -61,11 +60,10 @@ export class TwoFactorVerifyComponent {
|
|
|
61
60
|
this.errorMessage.set('Invalid backup code.');
|
|
62
61
|
}
|
|
63
62
|
},
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
},
|
|
67
|
-
});
|
|
63
|
+
'Backup code verification failed.',
|
|
64
|
+
);
|
|
68
65
|
} else {
|
|
66
|
+
this.clearMessages();
|
|
69
67
|
this.auth.verify2FALogin(
|
|
70
68
|
code!,
|
|
71
69
|
trustDevice ?? false,
|
|
@@ -26,14 +26,14 @@
|
|
|
26
26
|
</thead>
|
|
27
27
|
<tbody>
|
|
28
28
|
@for (record of records; track $index) {
|
|
29
|
-
<tr [class.deleted-record]="record
|
|
29
|
+
<tr [class.deleted-record]="isDeleted(record)" [class.updated-record]="isUpdated(record)" [class.new-record]="isInserted(record)">
|
|
30
30
|
@if (record == editingRecord) {
|
|
31
31
|
@for (col of displayedColumns; track col) {
|
|
32
32
|
<td>
|
|
33
33
|
@if (isReadOnly(col)) {
|
|
34
34
|
{{ record[col] }}
|
|
35
35
|
} @else {
|
|
36
|
-
<dynamic-field [tableName]="tableName()" [value]="record[col]" [field]="col" (valueChange)="record[col]=$event" subscriptSizing="dynamic"></dynamic-field>
|
|
36
|
+
<sail-dynamic-field [tableName]="tableName()" [value]="record[col]" [field]="col" (valueChange)="record[col]=$event" subscriptSizing="dynamic"></sail-dynamic-field>
|
|
37
37
|
}
|
|
38
38
|
</td>
|
|
39
39
|
}
|
|
@@ -49,12 +49,12 @@
|
|
|
49
49
|
@if (canUpdate()) {
|
|
50
50
|
<button matIconButton (click)="editRow(record)" title="Edit"><mat-icon>edit</mat-icon></button>
|
|
51
51
|
}
|
|
52
|
-
@if (record
|
|
52
|
+
@if (isDeleted(record)) {
|
|
53
53
|
<button matIconButton (click)="unDeleteRow(record)" title="Undelete"><mat-icon>restore_from_trash</mat-icon></button>
|
|
54
54
|
} @else if (canDelete()) {
|
|
55
55
|
<button matIconButton (click)="deleteRow(record)" title="Delete"><mat-icon>delete</mat-icon></button>
|
|
56
56
|
}
|
|
57
|
-
@if (record
|
|
57
|
+
@if (!isInserted(record)) {
|
|
58
58
|
@for (action of getActions(true); track action.action) {
|
|
59
59
|
@if (canExecuteAction(action)) {
|
|
60
60
|
<button matIconButton type="button"
|