@nauticana/sail 1.1.9 → 1.1.11

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 (79) hide show
  1. package/README.md +4 -2
  2. package/package.json +5 -1
  3. package/src/component/abstract/base_async.ts +9 -11
  4. package/src/component/abstract/base_form.ts +6 -5
  5. package/src/component/abstract/base_table.ts +131 -58
  6. package/src/component/abstract/base_view.ts +43 -27
  7. package/src/component/account/profile_editor.ts +2 -2
  8. package/src/component/billing/payment_methods.ts +1 -2
  9. package/src/component/billing/price_selector.html +2 -1
  10. package/src/component/billing/price_selector.ts +28 -13
  11. package/src/component/billing/trial_banner.ts +7 -1
  12. package/src/component/billing/usage_meter.ts +2 -2
  13. package/src/component/dashboard/data_card.html +1 -1
  14. package/src/component/dashboard/entity_selector.ts +21 -23
  15. package/src/component/form/form_field.html +6 -5
  16. package/src/component/form/form_field.ts +39 -21
  17. package/src/component/form/form_record.html +3 -3
  18. package/src/component/form/form_record.ts +23 -8
  19. package/src/component/form/form_table.html +2 -2
  20. package/src/component/form/form_table.ts +6 -5
  21. package/src/component/login/account_deletion.ts +6 -6
  22. package/src/component/login/chpass_component.html +4 -4
  23. package/src/component/login/chpass_component.ts +0 -1
  24. package/src/component/login/confirm_chpass_component.html +4 -4
  25. package/src/component/login/confirm_chpass_component.ts +1 -4
  26. package/src/component/login/consent_gate.html +6 -6
  27. package/src/component/login/consent_gate.ts +17 -24
  28. package/src/component/login/login_component.html +7 -4
  29. package/src/component/login/login_component.ts +9 -5
  30. package/src/component/login/otp_input.html +13 -5
  31. package/src/component/login/otp_input.ts +34 -27
  32. package/src/component/login/register_component.html +31 -14
  33. package/src/component/login/register_component.ts +80 -89
  34. package/src/component/login/sms_consent.html +11 -0
  35. package/src/component/login/sms_consent.ts +38 -0
  36. package/src/component/login/social_login.html +2 -2
  37. package/src/component/login/social_login.ts +20 -13
  38. package/src/component/navigation/navigation.html +2 -2
  39. package/src/component/navigation/navigation.ts +2 -0
  40. package/src/component/oauth_connection/oauth_connection_panel.ts +24 -3
  41. package/src/component/payment/user_payment_methods.html +2 -2
  42. package/src/component/payment/user_payment_methods.ts +12 -23
  43. package/src/component/payout/bank_info_form.html +4 -4
  44. package/src/component/payout/bank_info_form.ts +22 -26
  45. package/src/component/payout/provider_onboarding.html +5 -5
  46. package/src/component/payout/provider_onboarding.ts +28 -48
  47. package/src/component/security/trusted_devices.html +1 -1
  48. package/src/component/security/trusted_devices.ts +0 -1
  49. package/src/component/security/twofactor_setup.html +8 -4
  50. package/src/component/security/twofactor_setup.ts +21 -3
  51. package/src/component/security/twofactor_verify.html +2 -1
  52. package/src/component/security/twofactor_verify.ts +2 -8
  53. package/src/component/table/table_detail.html +15 -14
  54. package/src/component/table/table_detail.ts +62 -41
  55. package/src/component/table/table_edit.html +15 -11
  56. package/src/component/table/table_edit.ts +94 -67
  57. package/src/component/table/table_list.html +45 -12
  58. package/src/component/table/table_list.ts +98 -26
  59. package/src/component/table/table_lookup.html +11 -8
  60. package/src/component/table/table_lookup.ts +45 -37
  61. package/src/component/table/table_report.html +2 -2
  62. package/src/component/table/table_report.ts +37 -31
  63. package/src/component/table/table_search.html +4 -4
  64. package/src/component/table/table_search.ts +36 -45
  65. package/src/config.ts +5 -1
  66. package/src/index.ts +2 -0
  67. package/src/model/appdata.ts +5 -9
  68. package/src/model/common.ts +27 -27
  69. package/src/model/decorator.ts +25 -21
  70. package/src/service/api-response.interceptor.ts +4 -1
  71. package/src/service/auth.interceptor.ts +27 -7
  72. package/src/service/auth.service.ts +188 -134
  73. package/src/service/label.service.ts +6 -5
  74. package/src/service/oauth_connection.service.ts +2 -2
  75. package/src/service/rest_service.ts +57 -13
  76. package/src/service/rest_url.ts +23 -7
  77. package/src/service/script_loader.ts +12 -2
  78. package/src/util/errors.ts +20 -0
  79. package/src/util/money.ts +19 -7
@@ -1,28 +1,32 @@
1
1
  import {
2
+ registerDecorator,
3
+ ValidationOptions,
2
4
  ValidatorConstraint,
3
5
  ValidatorConstraintInterface,
4
6
  ValidationArguments,
5
7
  } from 'class-validator';
6
8
 
7
- @ValidatorConstraint({ name: 'isNumeric', async: false })
8
- export class IsNumericConstraint implements ValidatorConstraintInterface {
9
- validate(value: any, args: ValidationArguments) {
10
- if (typeof value !== 'number') {
9
+ // Plain decimal only — exponential notation ('1e21') must not slip past
10
+ // the precision/scale digit counts.
11
+ const DECIMAL = /^-?\d+(\.\d+)?$/;
12
+
13
+ @ValidatorConstraint({ name: 'isSailNumeric', async: false })
14
+ export class IsSailNumericConstraint implements ValidatorConstraintInterface {
15
+ validate(value: unknown, args: ValidationArguments) {
16
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
11
17
  return false;
12
18
  }
13
19
 
14
20
  const [precision, scale] = args.constraints;
15
21
  const valueString = value.toString();
22
+ if (!DECIMAL.test(valueString)) {
23
+ return false;
24
+ }
16
25
  const parts = valueString.split('.');
17
26
  const integerPart = parts[0].startsWith('-') ? parts[0].substring(1) : parts[0];
18
27
  const decimalPart = parts[1] || '';
19
28
 
20
- // Check total digits vs precision and decimal digits vs scale
21
- if (integerPart.length + decimalPart.length > precision || decimalPart.length > scale) {
22
- return false;
23
- }
24
-
25
- return true;
29
+ return integerPart.length + decimalPart.length <= precision && decimalPart.length <= scale;
26
30
  }
27
31
 
28
32
  defaultMessage(_args: ValidationArguments) {
@@ -30,9 +34,9 @@ export class IsNumericConstraint implements ValidatorConstraintInterface {
30
34
  }
31
35
  }
32
36
 
33
- @ValidatorConstraint({ name: 'IsString', async: false })
34
- export class IsStringConstraint implements ValidatorConstraintInterface {
35
- validate(value: any, args: ValidationArguments) {
37
+ @ValidatorConstraint({ name: 'isSailString', async: false })
38
+ export class IsSailStringConstraint implements ValidatorConstraintInterface {
39
+ validate(value: unknown, args: ValidationArguments) {
36
40
  const [maxLength] = args.constraints;
37
41
 
38
42
  if (typeof value !== 'string') {
@@ -47,28 +51,28 @@ export class IsStringConstraint implements ValidatorConstraintInterface {
47
51
  }
48
52
  }
49
53
 
50
- import { registerDecorator, ValidationOptions } from 'class-validator';
51
-
52
- export function IsNumeric(precision: number, scale: number, validationOptions?: ValidationOptions) {
53
- return function (object: Record<string, any>, propertyName: string) {
54
+ // Named IsSail* so they can't be confused with class-validator's own
55
+ // IsString/IsNumber on import.
56
+ export function IsSailNumeric(precision: number, scale: number, validationOptions?: ValidationOptions) {
57
+ return function (object: object, propertyName: string) {
54
58
  registerDecorator({
55
59
  target: object.constructor,
56
60
  propertyName: propertyName,
57
61
  options: validationOptions,
58
62
  constraints: [precision, scale],
59
- validator: IsNumericConstraint,
63
+ validator: IsSailNumericConstraint,
60
64
  });
61
65
  };
62
66
  }
63
67
 
64
- export function IsString(maxLength: number, validationOptions?: ValidationOptions) {
65
- return function (object: Record<string, any>, propertyName: string) {
68
+ export function IsSailString(maxLength: number, validationOptions?: ValidationOptions) {
69
+ return function (object: object, propertyName: string) {
66
70
  registerDecorator({
67
71
  target: object.constructor,
68
72
  propertyName: propertyName,
69
73
  options: validationOptions,
70
74
  constraints: [maxLength],
71
- validator: IsStringConstraint,
75
+ validator: IsSailStringConstraint,
72
76
  });
73
77
  };
74
78
  }
@@ -34,7 +34,10 @@ export const apiResponseInterceptor: HttpInterceptorFn = (req, next) => {
34
34
  map(event => {
35
35
  if (isFinalResponse(event)) {
36
36
  const body = event.body as Record<string, unknown> | null;
37
- if (body && typeof body === 'object' && 'data' in body) {
37
+ // keel's common.APIResponse is `{data, pagination?, meta}` with `meta`
38
+ // always present. Requiring both keys keeps a legitimate body that
39
+ // merely contains a `data` field from being truncated to it.
40
+ if (body && typeof body === 'object' && 'data' in body && 'meta' in body) {
38
41
  return event.clone({ body: body['data'] });
39
42
  }
40
43
  }
@@ -1,6 +1,8 @@
1
1
  import { HttpErrorResponse, HttpEventType, HttpInterceptorFn } from '@angular/common/http';
2
+ import { inject, Injector } from '@angular/core';
2
3
  import { catchError, tap, throwError } from 'rxjs';
3
4
  import { isKeelApiUrl } from './rest_url';
5
+ import { BaseAuthService } from './auth.service';
4
6
 
5
7
  // Auth-loop breaker: repeated 401s on token-bearing API calls (a stale session) can
6
8
  // drive the app to hammer the API/CDN. After AUTH_FAIL_THRESHOLD within
@@ -13,7 +15,18 @@ const AUTH_CIRCUIT_COOLDOWN_MS = 30_000;
13
15
  const FAIL_KEY = 'keel_auth_401s';
14
16
  const OPEN_KEY = 'keel_auth_circuit_until';
15
17
 
18
+ /** Error-body code carried by the synthetic circuit-open rejection, so
19
+ * consumers can tell "session expired, request refused locally" apart from a
20
+ * real 401 and from a network failure (status 0). */
21
+ export const AUTH_CIRCUIT_OPEN_CODE = 'sail_auth_circuit_open';
22
+
16
23
  export const authInterceptor: HttpInterceptorFn = (req, next) => {
24
+ // Resolved lazily per event to avoid a DI cycle (BaseAuthService → HttpClient
25
+ // → interceptors) and so an open circuit clears the dead session even when it
26
+ // was opened by a previous page load.
27
+ const injector = inject(Injector);
28
+ const expireSession = () => injector.get(BaseAuthService, null, { optional: true })?.sessionExpired();
29
+
17
30
  const token = localStorage.getItem('jwt');
18
31
  const authedApi = !!token && isKeelApiUrl(req.url);
19
32
  if (authedApi) {
@@ -21,10 +34,15 @@ export const authInterceptor: HttpInterceptorFn = (req, next) => {
21
34
  }
22
35
 
23
36
  if (authedApi && authCircuitOpen()) {
37
+ expireSession();
24
38
  return throwError(() => new HttpErrorResponse({
25
39
  url: req.url,
26
- status: 0,
27
- statusText: 'auth circuit open: too many 401s — request stopped to prevent a loop',
40
+ status: 401,
41
+ statusText: 'Unauthorized',
42
+ error: {
43
+ code: AUTH_CIRCUIT_OPEN_CODE,
44
+ detail: 'auth circuit open: too many 401s — request stopped to prevent a loop',
45
+ },
28
46
  }));
29
47
  }
30
48
 
@@ -35,8 +53,8 @@ export const authInterceptor: HttpInterceptorFn = (req, next) => {
35
53
  }
36
54
  }),
37
55
  catchError((err: HttpErrorResponse) => {
38
- if (authedApi && err.status === 401) {
39
- recordAuthFailure();
56
+ if (authedApi && err.status === 401 && recordAuthFailure()) {
57
+ expireSession();
40
58
  }
41
59
  return throwError(() => err);
42
60
  }),
@@ -47,7 +65,8 @@ function authCircuitOpen(): boolean {
47
65
  return Date.now() < Number(sessionStorage.getItem(OPEN_KEY) ?? 0);
48
66
  }
49
67
 
50
- function recordAuthFailure(): void {
68
+ /** Returns true when this failure crossed the threshold and opened the circuit. */
69
+ function recordAuthFailure(): boolean {
51
70
  const now = Date.now();
52
71
  let times: number[];
53
72
  try {
@@ -60,9 +79,10 @@ function recordAuthFailure(): void {
60
79
  if (times.length >= AUTH_FAIL_THRESHOLD) {
61
80
  sessionStorage.setItem(OPEN_KEY, String(now + AUTH_CIRCUIT_COOLDOWN_MS));
62
81
  sessionStorage.removeItem(FAIL_KEY);
63
- } else {
64
- sessionStorage.setItem(FAIL_KEY, JSON.stringify(times));
82
+ return true;
65
83
  }
84
+ sessionStorage.setItem(FAIL_KEY, JSON.stringify(times));
85
+ return false;
66
86
  }
67
87
 
68
88
  function clearAuthFailures(): void {