@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.
Files changed (47) hide show
  1. package/README.md +120 -15
  2. package/package.json +2 -2
  3. package/src/component/abstract/base_async.ts +8 -2
  4. package/src/component/abstract/base_table.ts +94 -35
  5. package/src/component/abstract/base_view.ts +28 -41
  6. package/src/component/billing/checkout_button.ts +1 -1
  7. package/src/component/billing/payment_methods.ts +1 -1
  8. package/src/component/billing/plan_selector.ts +1 -1
  9. package/src/component/form/form_field.ts +8 -11
  10. package/src/component/form/form_record.html +2 -2
  11. package/src/component/form/form_record.ts +1 -1
  12. package/src/component/form/form_table.html +1 -1
  13. package/src/component/form/form_table.ts +1 -1
  14. package/src/component/login/account_deletion.ts +1 -1
  15. package/src/component/login/chpass_component.ts +11 -16
  16. package/src/component/login/confirm_chpass_component.ts +9 -15
  17. package/src/component/login/confirm_register_component.ts +9 -15
  18. package/src/component/login/consent_gate.ts +1 -1
  19. package/src/component/login/login_component.html +1 -1
  20. package/src/component/login/login_component.ts +1 -1
  21. package/src/component/login/otp_input.ts +1 -1
  22. package/src/component/login/register_component.ts +1 -1
  23. package/src/component/login/social_login.ts +1 -1
  24. package/src/component/navigation/navigation.ts +10 -11
  25. package/src/component/security/trusted_devices.ts +1 -1
  26. package/src/component/security/twofactor_setup.ts +18 -26
  27. package/src/component/security/twofactor_verify.ts +10 -12
  28. package/src/component/table/table_detail.html +4 -4
  29. package/src/component/table/table_detail.ts +22 -34
  30. package/src/component/table/table_edit.html +13 -3
  31. package/src/component/table/table_edit.ts +17 -17
  32. package/src/component/table/table_list.ts +18 -52
  33. package/src/component/table/table_lookup.html +1 -1
  34. package/src/component/table/table_lookup.ts +1 -3
  35. package/src/component/table/table_report.ts +11 -14
  36. package/src/component/table/table_search.html +1 -1
  37. package/src/component/table/table_search.ts +14 -38
  38. package/src/index.ts +2 -0
  39. package/src/model/appdata.ts +2 -2
  40. package/src/model/common.ts +39 -1
  41. package/src/service/auth.service.ts +26 -26
  42. package/src/service/base_rest.service.ts +20 -0
  43. package/src/service/billing.service.ts +3 -9
  44. package/src/service/payout.service.ts +3 -9
  45. package/src/service/rest_service.ts +5 -5
  46. package/src/service/user_payment_method.service.ts +4 -8
  47. 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>
@@ -14,7 +14,7 @@ interface TableFormDialogData {
14
14
  }
15
15
 
16
16
  @Component({
17
- selector: 'table-form',
17
+ selector: 'sail-table-form',
18
18
  changeDetection: ChangeDetectionStrategy.OnPush,
19
19
  encapsulation: ViewEncapsulation.None,
20
20
  templateUrl: './form_table.html',
@@ -24,7 +24,7 @@ import { SAIL_GUI_CONFIG, SailGuiConfig, DEFAULT_CONFIG } from '../../config';
24
24
  * `/login/local`).
25
25
  */
26
26
  @Component({
27
- selector: 'app-account-deletion',
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: 'app-chpass',
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 request = this.forgotPassword()
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
- request.subscribe({
82
- next: () => {
83
- if (this.forgotPassword()) {
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
- error: (err) => {
91
- const msg = typeof err.error === 'string' ? err.error : err.error?.message;
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, signal, OnInit, ViewEncapsulation } from '@angular/core';
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: 'app-confirm-chpass',
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.auth.confirmChpass(username!, code!, new_password!).subscribe({
61
- next: () => {
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
- error: (err) => {
66
- this.errorMessage.set(err.error?.message ?? 'Confirmation failed. Please try again.');
67
- },
68
- });
61
+ 'Confirmation failed. Please try again.',
62
+ );
69
63
  }
70
64
  }
@@ -1,4 +1,4 @@
1
- import { ChangeDetectionStrategy, Component, inject, signal, OnInit, ViewEncapsulation } from '@angular/core';
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: 'app-confirm-register',
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.auth.confirmRegister(email!, code!).subscribe({
54
- next: (resp) => {
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
- error: (err) => {
67
- this.errorMessage.set(err.error?.message ?? 'Confirmation failed. Please try again.');
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: 'app-consent-gate',
25
+ selector: 'sail-consent-gate',
26
26
  templateUrl: './consent_gate.html',
27
27
  changeDetection: ChangeDetectionStrategy.OnPush,
28
28
  encapsulation: ViewEncapsulation.None,
@@ -24,7 +24,7 @@
24
24
  </form>
25
25
  @if (showSocialLogin()) {
26
26
  <hr class="auth-divider">
27
- <app-social-login
27
+ <sail-social-login
28
28
  [providers]="enabledSocialProviders()"
29
29
  (loginSuccess)="onSocialSuccess($event)"
30
30
  (loginError)="onSocialError($event)" />
@@ -10,7 +10,7 @@ import { LoginResponseSocial, SocialProvider } from '../../model/auth';
10
10
  import { SocialLoginComponent } from './social_login';
11
11
 
12
12
  @Component({
13
- selector: 'app-login',
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: 'app-otp-input',
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: 'app-register',
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: 'app-social-login',
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, OnInit, signal, ViewEncapsulation } from "@angular/core";
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: 'app-navigation',
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 implements OnInit {
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
- private readonly allMenuItems = signal<ApplicationMenu[]>([]);
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
- const menus = this.allMenuItems();
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: 'app-trusted-devices',
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: 'app-twofactor-setup',
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.auth.setup2FA({ password }).subscribe({
54
- next: (res) => {
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
- error: (err) => {
62
- this.errorMessage.set(err.error?.message ?? '2FA setup failed.');
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.auth.verify2FA({ code: code! }).subscribe({
77
- next: (res) => {
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
- error: (err) => {
86
- this.errorMessage.set(err.error?.message ?? 'Verification failed.');
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.auth.disable2FA(password!, code!).subscribe({
96
- next: () => {
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
- error: (err) => {
102
- this.errorMessage.set(err.error?.message ?? 'Failed to disable 2FA.');
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: 'app-twofactor-verify',
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.errorMessage.set('');
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.auth.verifyBackupCode(code!).subscribe({
52
- next: (res) => {
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
- error: (err) => {
65
- this.errorMessage.set(err.error?.message ?? 'Backup code verification failed.');
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[config.opField] === 'D'" [class.updated-record]="record[config.opField] === 'U'" [class.new-record]="record[config.opField] === 'I'">
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[config.opField] === 'D') {
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[config.opField] !== 'I') {
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"