@nauticana/sail 0.5.0

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 (70) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +741 -0
  3. package/package.json +58 -0
  4. package/src/component/abstract/base_async.ts +49 -0
  5. package/src/component/abstract/base_form.ts +11 -0
  6. package/src/component/abstract/base_table.ts +320 -0
  7. package/src/component/abstract/base_view.ts +93 -0
  8. package/src/component/billing/checkout_button.html +10 -0
  9. package/src/component/billing/checkout_button.ts +57 -0
  10. package/src/component/billing/payment_methods.html +27 -0
  11. package/src/component/billing/payment_methods.ts +36 -0
  12. package/src/component/billing/plan_selector.html +24 -0
  13. package/src/component/billing/plan_selector.ts +29 -0
  14. package/src/component/form/form_field.html +54 -0
  15. package/src/component/form/form_field.ts +156 -0
  16. package/src/component/form/form_record.html +14 -0
  17. package/src/component/form/form_record.ts +56 -0
  18. package/src/component/form/form_table.html +19 -0
  19. package/src/component/form/form_table.ts +58 -0
  20. package/src/component/login/account_deletion.html +42 -0
  21. package/src/component/login/account_deletion.ts +79 -0
  22. package/src/component/login/chpass_component.html +58 -0
  23. package/src/component/login/chpass_component.ts +96 -0
  24. package/src/component/login/confirm_chpass_component.html +47 -0
  25. package/src/component/login/confirm_chpass_component.ts +70 -0
  26. package/src/component/login/confirm_register_component.html +37 -0
  27. package/src/component/login/confirm_register_component.ts +71 -0
  28. package/src/component/login/consent_gate.html +30 -0
  29. package/src/component/login/consent_gate.ts +109 -0
  30. package/src/component/login/login_component.html +43 -0
  31. package/src/component/login/login_component.ts +73 -0
  32. package/src/component/login/otp_input.html +38 -0
  33. package/src/component/login/otp_input.ts +104 -0
  34. package/src/component/login/register_component.html +132 -0
  35. package/src/component/login/register_component.ts +184 -0
  36. package/src/component/login/social_login.html +13 -0
  37. package/src/component/login/social_login.ts +120 -0
  38. package/src/component/navigation/navigation.html +49 -0
  39. package/src/component/navigation/navigation.ts +69 -0
  40. package/src/component/security/trusted_devices.html +76 -0
  41. package/src/component/security/trusted_devices.ts +92 -0
  42. package/src/component/security/twofactor_setup.html +75 -0
  43. package/src/component/security/twofactor_setup.ts +106 -0
  44. package/src/component/security/twofactor_verify.html +45 -0
  45. package/src/component/security/twofactor_verify.ts +90 -0
  46. package/src/component/table/table_detail.html +53 -0
  47. package/src/component/table/table_detail.ts +133 -0
  48. package/src/component/table/table_edit.html +58 -0
  49. package/src/component/table/table_edit.ts +163 -0
  50. package/src/component/table/table_list.html +41 -0
  51. package/src/component/table/table_list.ts +100 -0
  52. package/src/component/table/table_lookup.html +35 -0
  53. package/src/component/table/table_lookup.ts +88 -0
  54. package/src/component/table/table_search.html +17 -0
  55. package/src/component/table/table_search.ts +80 -0
  56. package/src/config.ts +67 -0
  57. package/src/index.ts +41 -0
  58. package/src/model/appdata.ts +266 -0
  59. package/src/model/auth.ts +134 -0
  60. package/src/model/common.ts +115 -0
  61. package/src/model/decorator.ts +74 -0
  62. package/src/service/api-response.interceptor.ts +26 -0
  63. package/src/service/auth.interceptor.ts +11 -0
  64. package/src/service/auth.service.ts +576 -0
  65. package/src/service/billing.service.ts +65 -0
  66. package/src/service/label.service.ts +18 -0
  67. package/src/service/rest_service.ts +94 -0
  68. package/src/service/rest_url.ts +42 -0
  69. package/src/service/script_loader.ts +31 -0
  70. package/tsconfig.json +16 -0
package/src/index.ts ADDED
@@ -0,0 +1,41 @@
1
+ export * from './config';
2
+ export * from './model/appdata';
3
+ export * from './model/auth';
4
+ export * from './model/common';
5
+ export * from './model/decorator';
6
+ export * from './component/abstract/base_table';
7
+ export * from './component/abstract/base_form';
8
+ export * from './component/abstract/base_view';
9
+ export * from './component/abstract/base_async';
10
+ export * from './component/form/form_field';
11
+ export * from './component/form/form_record';
12
+ export * from './component/form/form_table';
13
+ export * from './component/table/table_list';
14
+ export * from './component/table/table_search';
15
+ export * from './component/table/table_edit';
16
+ export * from './component/table/table_detail';
17
+ export * from './component/table/table_lookup';
18
+ export * from './component/navigation/navigation';
19
+ export * from './component/login/login_component';
20
+ export * from './component/login/register_component';
21
+ export * from './component/login/chpass_component';
22
+ export * from './component/login/confirm_register_component';
23
+ export * from './component/login/confirm_chpass_component';
24
+ export * from './component/login/consent_gate';
25
+ export * from './component/login/otp_input';
26
+ export * from './component/login/social_login';
27
+ export * from './component/login/account_deletion';
28
+ export * from './component/security/twofactor_setup';
29
+ export * from './component/security/twofactor_verify';
30
+ export * from './component/security/trusted_devices';
31
+ export * from './component/billing/plan_selector';
32
+ export * from './component/billing/checkout_button';
33
+ export * from './component/billing/payment_methods';
34
+ export * from './service/auth.service';
35
+ export * from './service/billing.service';
36
+ export * from './service/script_loader';
37
+ export * from './service/auth.interceptor';
38
+ export * from './service/api-response.interceptor';
39
+ export * from './service/rest_service';
40
+ export * from './service/rest_url';
41
+ export * from './service/label.service';
@@ -0,0 +1,266 @@
1
+ import { AbstractControlOptions } from '@angular/forms';
2
+ import { ApplicationMenu, AuthorizationRolePermission } from './common';
3
+
4
+ export const DataType = {
5
+ STRING: 'string',
6
+ INTEGER: 'integer',
7
+ FLOAT: 'float',
8
+ BOOLEAN: 'boolean',
9
+ TIMESTAMP: 'timestamp',
10
+ } as const;
11
+
12
+ export interface LoginRequest {
13
+ Username: string;
14
+ Password: string;
15
+ }
16
+
17
+ export interface LoginResponse {
18
+ token: string;
19
+ }
20
+
21
+ export interface AuthSummary {
22
+ Obj: string;
23
+ Act: string;
24
+ Val: string;
25
+ Reg: string;
26
+ }
27
+
28
+ export interface ApplicationData {
29
+ MainMenu: ApplicationMenu[];
30
+ Permissions: AuthorizationRolePermission[];
31
+ ConstantCache: {[key: string]: {[key: string]: string}};
32
+ TableCache: {[key: string]: {[key: string]: string}};
33
+ TableDefinitions: {[key: string]: TableDefinition};
34
+ Apis: {[key: string]: DictionaryPath};
35
+ [key: string]: unknown;
36
+ }
37
+
38
+ export interface TableColumn {
39
+ ColumnName: string;
40
+ PascalName: string;
41
+ Caption: string;
42
+ DataType: string;
43
+ InputType: 'text' | 'password' | 'number' | 'date' | 'datetime' | 'datetime-local' | 'textarea' | 'select' | 'checkbox';
44
+ Size: number;
45
+ Scale: number;
46
+ Step: string;
47
+ Order: number;
48
+ Required: boolean;
49
+ IsKey: boolean;
50
+ SequenceName: string;
51
+ LookupDomain: string;
52
+ LookupTable: string;
53
+ LookupStyle: string;
54
+ HasDefault: boolean;
55
+ DefaultValue: string;
56
+ Validators?: AbstractControlOptions['validators'];
57
+ }
58
+
59
+ export interface TableAction {
60
+ action: string;
61
+ caption: string;
62
+ method: string;
63
+ enable: string;
64
+ authorityCheck: string;
65
+ recordSpecific: string;
66
+ }
67
+
68
+ export interface ForeignKey {
69
+ ParentTable: string;
70
+ ChildTable: string;
71
+ PascalName: string;
72
+ ConstraintName: string;
73
+ LookupStyle: string;
74
+ Columns: TableColumn[];
75
+ }
76
+
77
+ export interface TableDefinition {
78
+ TableName: string;
79
+ PascalName: string;
80
+ PartnerSpecific: boolean;
81
+ Keys: TableColumn[];
82
+ Columns: TableColumn[];
83
+ Actions: TableAction[];
84
+ LookupStyle: string;
85
+ LookupColumns: TableColumn[];
86
+ Details: string[];
87
+ Parents: ForeignKey[];
88
+ Children: ForeignKey[];
89
+ }
90
+
91
+ export interface DictionaryPath {
92
+ RestAPI: string;
93
+ Version: string;
94
+ Table: TableDefinition;
95
+ Caption: string;
96
+ PascalName: string;
97
+ PathType: string;
98
+ ParentKeys: TableColumn[];
99
+ ChildKeys: TableColumn[];
100
+ Children: DictionaryPath[];
101
+ }
102
+
103
+ export interface ReportParam {
104
+ Name: string;
105
+ DataType: string;
106
+ }
107
+
108
+ export interface RestReport {
109
+ Id: string;
110
+ Version: string;
111
+ QueryName: string;
112
+ Params: ReportParam[];
113
+ }
114
+
115
+ // 2FA / Trusted device types
116
+
117
+ export interface LoginResponse2FA {
118
+ token: string;
119
+ twoFactorRequired: boolean;
120
+ loginToken?: string;
121
+ userId?: number;
122
+ partnerId?: number;
123
+ // Populated for users with the single-device session policy enabled.
124
+ // UI surfaces an info banner / can enforce "signed out elsewhere" notices.
125
+ singleDeviceSession?: boolean;
126
+ previousDeviceRevoked?: boolean;
127
+ }
128
+
129
+ export interface TwoFactorSetupResponse {
130
+ secret: string;
131
+ qrUri: string;
132
+ backupCodes: string[];
133
+ }
134
+
135
+ export interface TwoFactorVerifyRequest {
136
+ code: string;
137
+ loginToken?: string;
138
+ trustDevice?: boolean;
139
+ deviceFingerprint?: string;
140
+ deviceName?: string;
141
+ }
142
+
143
+ export interface TwoFactorVerifyResponse {
144
+ valid: boolean;
145
+ token?: string;
146
+ }
147
+
148
+ export interface TrustedDevice {
149
+ id: number;
150
+ name: string;
151
+ fingerprint: string;
152
+ last_used_at: string;
153
+ created_at: string;
154
+ }
155
+
156
+ export interface PartnerRegistration {
157
+ FirstName: string;
158
+ LastName: string;
159
+ UserName: string;
160
+ Email: string;
161
+ Password: string;
162
+ PartnerCaption: string;
163
+ Address: string;
164
+ City: string;
165
+ State: string;
166
+ Zipcode: string;
167
+ Country: string;
168
+ Phone: string;
169
+ Latitude: number;
170
+ Longitude: number;
171
+ DomainURL: string;
172
+ PlanID: string;
173
+ }
174
+
175
+ // PublicPlan — shape returned by GET /public/plans. Used on the registration
176
+ // page to render the plan picker and to drive `<app-checkout-button>` flows.
177
+ export interface PublicPlan {
178
+ id: string;
179
+ caption: string;
180
+ monthlyCost: number;
181
+ annualCost: number;
182
+ // Stripe price ID (or provider-equivalent). Optional because keel only
183
+ // populates it when a plan is wired to a billing provider; FREE plans and
184
+ // not-yet-priced tiers leave it empty. Pass straight to
185
+ // `<app-checkout-button [priceId]>` / `BillingService.createCheckout({ priceId })`.
186
+ priceId?: string;
187
+ }
188
+
189
+ // ConfirmRegisterResponse — shape returned by POST /public/confirm/register.
190
+ // When paymentRequired is true the UI should redirect the user to paymentUrl
191
+ // before letting them log in.
192
+ export interface ConfirmRegisterResponse {
193
+ status: string;
194
+ partnerId: number;
195
+ planId: string;
196
+ paymentRequired: boolean;
197
+ paymentUrl?: string;
198
+ }
199
+
200
+ // ── Billing / Payment types ──
201
+ // See keel/SHARED_PAYMENT.md for the backend contract.
202
+
203
+ export interface PaymentMethod {
204
+ id: string;
205
+ provider: string; // 'stripe' | 'lemonsqueezy' | ...
206
+ methodType: string; // 'card' | 'bank' | ...
207
+ last4?: string;
208
+ isDefault: boolean;
209
+ }
210
+
211
+ // CreateCheckout is JWT-gated and validates `priceId`, `successUrl`,
212
+ // `cancelUrl` against server-side allowlists. Empty allowlists cause every
213
+ // checkout to be rejected with 400.
214
+ export type CheckoutMode = 'subscription' | 'payment' | 'setup';
215
+
216
+ export interface CheckoutRequest {
217
+ mode?: CheckoutMode; // default 'subscription'
218
+ // Required for 'subscription' / 'payment'; must be empty for 'setup'.
219
+ // Pass `PublicPlan.priceId` straight through.
220
+ priceId?: string;
221
+ quantity?: number;
222
+ email?: string;
223
+ // Hostname must match keel's `AllowedRedirectHosts`. Default port matching
224
+ // is hostname-only; include the port (e.g. `host:8443`) only if the
225
+ // operator deliberately wants port-strict matching.
226
+ successUrl: string;
227
+ cancelUrl: string;
228
+ // Provider metadata — values must be strings. keel stringifies any
229
+ // numeric / boolean values in its server-side metadata adapter, so when
230
+ // reading back metadata in webhook handlers, expect the stringified form
231
+ // (e.g. `partner_id` arrives as `"42"`, not `42`).
232
+ metadata?: { [key: string]: string };
233
+ }
234
+
235
+ export interface CheckoutResponse {
236
+ checkoutUrl: string;
237
+ }
238
+
239
+ // REST list endpoints return paginated wrappers. `BackendService.list<T>()`
240
+ // unwraps to the array; use `listPaginated<T>()` to get the metadata.
241
+ export interface PaginatedList<T> {
242
+ items: T[];
243
+ limit: number;
244
+ offset: number;
245
+ total: number;
246
+ }
247
+
248
+ export interface Subscription {
249
+ planId: string;
250
+ status: string; // 'A' active, 'P' pending-payment, 'C' cancelled, ...
251
+ begda: string;
252
+ endda?: string;
253
+ monthlyCost: number;
254
+ currency: string;
255
+ autoRenew: boolean;
256
+ }
257
+
258
+ export interface Invoice {
259
+ id: string;
260
+ number: string;
261
+ status: string; // 'paid' | 'open' | 'void' | ...
262
+ total: number;
263
+ currency: string;
264
+ issuedAt: string;
265
+ paidAt?: string;
266
+ }
@@ -0,0 +1,134 @@
1
+ // Auth / consent / OTP / social / push / re-auth types — mirror keel's port types
2
+ // (see keel/README.md and keel/handler/*).
3
+
4
+ // ── Canonical consent type labels (match keel's port.ConsentType*) ──
5
+ export const ConsentType = {
6
+ PRIVACY_POLICY: 'privacy_policy',
7
+ TERMS: 'terms',
8
+ CROSS_BORDER: 'cross_border',
9
+ VIDEO_OPT_IN: 'video_opt_in',
10
+ VIDEO_SESSION: 'video_session',
11
+ MARKETING: 'marketing',
12
+ } as const;
13
+ export type ConsentTypeValue = typeof ConsentType[keyof typeof ConsentType];
14
+
15
+ // Declarative config for an optional consent checkbox rendered by ConsentGate.
16
+ export interface ConsentOption {
17
+ id: string; // ConsentType value, e.g. 'video_opt_in'
18
+ label: string; // Checkbox label
19
+ required?: boolean; // If true, state.valid stays false until checked
20
+ hint?: string; // Small explanatory text below the label
21
+ linkUrl?: string; // Optional inline link in the label
22
+ linkText?: string;
23
+ }
24
+
25
+ // Emitted by ConsentGateComponent on every change. Mirrors keel's
26
+ // port.SignupConsent.Consents map directly.
27
+ export interface ConsentState {
28
+ consents: Record<string, boolean>;
29
+ policyVersion: string;
30
+ policyLanguage: string;
31
+ valid: boolean;
32
+ }
33
+
34
+ // Sent to the backend alongside OTP / social / register requests.
35
+ // Matches keel's user.SignupConsent.
36
+ export interface SignupConsent {
37
+ policyType?: string; // e.g. 'default' | 'custom'
38
+ policyVersion: string;
39
+ policyRegion?: string; // ISO country code
40
+ policyLanguage: string;
41
+ region?: string;
42
+ consents: Record<string, boolean>;
43
+ }
44
+
45
+ // ── Phone / email OTP ──
46
+ // `contactType` is a frontend-only convenience field used to drive UI
47
+ // navigation (phone-confirm vs email-confirm). It is NOT sent to the
48
+ // backend — keel detects phone-vs-email from the `contact` value itself.
49
+ // `BaseAuthService.sendOtp` strips this field before posting, since keel
50
+ // rejects unknown JSON fields with 400.
51
+ export type OtpContactType = 'phone' | 'email';
52
+ export type OtpPurpose = 'login' | 'register' | 'verify';
53
+
54
+ export interface OtpRequest {
55
+ contact: string;
56
+ contactType?: OtpContactType; // frontend-only, stripped before POST
57
+ purpose?: OtpPurpose;
58
+ defaultRegion?: string; // ISO country hint for phone normalization
59
+ policyType?: string;
60
+ policyVersion?: string;
61
+ policyRegion?: string;
62
+ policyLanguage?: string;
63
+ region?: string;
64
+ consents?: Record<string, boolean>;
65
+ }
66
+
67
+ // keel issues a 32-byte base64-URL opaque token bound to the user_id in
68
+ // the cache for ~5 minutes. Verify and Resend echo this back; an attacker
69
+ // guessing arbitrary user ids cannot reach Verify because the cache lookup
70
+ // fails. The login fall-through on unknown contacts also returns a token
71
+ // (resolving to user_id 0) so 200 responses don't leak which contacts are
72
+ // registered. Verify on a "fake" token returns the same generic 401 as a
73
+ // wrong code.
74
+ export interface OtpResponse {
75
+ otpToken: string;
76
+ }
77
+
78
+ export interface OtpVerifyRequest {
79
+ otpToken: string;
80
+ code: string;
81
+ }
82
+
83
+ export interface OtpVerifyResponse {
84
+ token: string;
85
+ userId?: number;
86
+ partnerId?: number;
87
+ }
88
+
89
+ export interface OtpResendRequest {
90
+ otpToken: string;
91
+ purpose?: OtpPurpose;
92
+ }
93
+
94
+ // ── Re-authentication credentials ──
95
+ // Required by Setup2FA, Disable2FA, DeleteAccount, LogoutEverywhere — server
96
+ // rejects with 401 if neither `password` nor `twoFactorCode` is supplied.
97
+ // Disable2FA additionally requires the current TOTP `code` alongside password.
98
+ export interface ReauthCredentials {
99
+ password?: string;
100
+ twoFactorCode?: string;
101
+ }
102
+
103
+ // ── Social login (ID token flow) ──
104
+ export type SocialProvider = 'google' | 'apple';
105
+
106
+ export interface SocialLoginRequest {
107
+ provider: SocialProvider;
108
+ token: string; // ID token from provider SDK
109
+ policyType?: string;
110
+ policyVersion?: string;
111
+ policyRegion?: string;
112
+ policyLanguage?: string;
113
+ region?: string;
114
+ consents?: Record<string, boolean>;
115
+ }
116
+
117
+ export interface LoginResponseSocial {
118
+ token: string;
119
+ userId?: number;
120
+ partnerId?: number;
121
+ isNewUser: boolean;
122
+ singleDeviceSession?: boolean;
123
+ previousDeviceRevoked?: boolean;
124
+ }
125
+
126
+ // ── Push notification tokens ──
127
+ export type PushPlatform = 'I' | 'A' | 'W'; // iOS / Android / Web
128
+
129
+ export interface PushRegisterRequest {
130
+ platform: PushPlatform;
131
+ token: string;
132
+ appVersion?: string;
133
+ deviceModel?: string;
134
+ }
@@ -0,0 +1,115 @@
1
+ import { IsString, IsNumeric } from './decorator';
2
+
3
+ export class SiudAction {
4
+ op_code = 'S';
5
+ }
6
+
7
+ export class UserAccountPolicy extends SiudAction {
8
+ @IsString(30)
9
+ Id?: string;
10
+
11
+ @IsNumeric(9, 0)
12
+ PolicyValue?: number;
13
+
14
+ }
15
+
16
+
17
+ export class ApplicationMenuItem extends SiudAction {
18
+ @IsString(30)
19
+ MenuId?: string;
20
+
21
+ @IsString(30)
22
+ ItemId?: string;
23
+
24
+ @IsString(80)
25
+ Caption?: string;
26
+
27
+ @IsNumeric(9, 0)
28
+ DisplayOrder?: number;
29
+
30
+ @IsString(255)
31
+ RestUri?: string;
32
+
33
+ @IsString(80)
34
+ Icon?: string;
35
+
36
+ IsActive?: boolean;
37
+
38
+ FilterOnList?: boolean;
39
+
40
+ }
41
+
42
+
43
+ export class ApplicationMenu extends SiudAction {
44
+ @IsString(30)
45
+ Id?: string;
46
+
47
+ @IsString(80)
48
+ Caption?: string;
49
+
50
+ @IsNumeric(9, 0)
51
+ DisplayOrder?: number;
52
+
53
+ @IsString(80)
54
+ Icon?: string;
55
+
56
+ IsActive?: boolean;
57
+
58
+ ApplicationMenuItems?: ApplicationMenuItem[];
59
+ }
60
+
61
+
62
+ export class AuthorizationRolePermission extends SiudAction {
63
+ @IsString(30)
64
+ RoleId?: string;
65
+
66
+ @IsString(30)
67
+ AuthorizationObjectId?: string;
68
+
69
+ @IsString(30)
70
+ Action?: string;
71
+
72
+ @IsString(80)
73
+ LowLimit?: string;
74
+
75
+ @IsString(80)
76
+ HighLimit?: string;
77
+
78
+ IsActive?: boolean;
79
+
80
+ }
81
+
82
+
83
+ export class ConstantValue extends SiudAction {
84
+ @IsString(60)
85
+ ConstantId?: string;
86
+
87
+ @IsString(80)
88
+ Value?: string;
89
+
90
+ @IsString(80)
91
+ Caption?: string;
92
+
93
+ }
94
+
95
+
96
+ export class UserAccount extends SiudAction {
97
+ @IsNumeric(9, 0)
98
+ Id?: number;
99
+
100
+ @IsString(80)
101
+ FirstName?: string;
102
+
103
+ @IsString(80)
104
+ LastName?: string;
105
+
106
+ @IsString(80)
107
+ UserName?: string;
108
+
109
+ @IsString(255)
110
+ UserEmail?: string;
111
+
112
+ @IsString(1)
113
+ Status?: string;
114
+
115
+ }
@@ -0,0 +1,74 @@
1
+ import {
2
+ ValidatorConstraint,
3
+ ValidatorConstraintInterface,
4
+ ValidationArguments,
5
+ } from 'class-validator';
6
+
7
+ @ValidatorConstraint({ name: 'isNumeric', async: false })
8
+ export class IsNumericConstraint implements ValidatorConstraintInterface {
9
+ validate(value: any, args: ValidationArguments) {
10
+ if (typeof value !== 'number') {
11
+ return false;
12
+ }
13
+
14
+ const [precision, scale] = args.constraints;
15
+ const valueString = value.toString();
16
+ const parts = valueString.split('.');
17
+ const integerPart = parts[0].startsWith('-') ? parts[0].substring(1) : parts[0];
18
+ const decimalPart = parts[1] || '';
19
+
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;
26
+ }
27
+
28
+ defaultMessage(_args: ValidationArguments) {
29
+ return `\$property must be a numeric value with a total precision of \$constraint1 digits and maximum of \$constraint2 decimal places.`;
30
+ }
31
+ }
32
+
33
+ @ValidatorConstraint({ name: 'IsString', async: false })
34
+ export class IsStringConstraint implements ValidatorConstraintInterface {
35
+ validate(value: any, args: ValidationArguments) {
36
+ const [maxLength] = args.constraints;
37
+
38
+ if (typeof value !== 'string') {
39
+ return false;
40
+ }
41
+
42
+ return value.length <= maxLength;
43
+ }
44
+
45
+ defaultMessage(_args: ValidationArguments) {
46
+ return `\$property must be shorter than or equal to \$constraint1 characters.`;
47
+ }
48
+ }
49
+
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
+ registerDecorator({
55
+ target: object.constructor,
56
+ propertyName: propertyName,
57
+ options: validationOptions,
58
+ constraints: [precision, scale],
59
+ validator: IsNumericConstraint,
60
+ });
61
+ };
62
+ }
63
+
64
+ export function IsString(maxLength: number, validationOptions?: ValidationOptions) {
65
+ return function (object: Record<string, any>, propertyName: string) {
66
+ registerDecorator({
67
+ target: object.constructor,
68
+ propertyName: propertyName,
69
+ options: validationOptions,
70
+ constraints: [maxLength],
71
+ validator: IsStringConstraint,
72
+ });
73
+ };
74
+ }
@@ -0,0 +1,26 @@
1
+ import { HttpErrorResponse, HttpInterceptorFn, HttpResponse } from '@angular/common/http';
2
+ import { catchError, map, throwError } from 'rxjs';
3
+
4
+ export const apiResponseInterceptor: HttpInterceptorFn = (req, next) => {
5
+ return next(req).pipe(
6
+ map(event => {
7
+ if (event instanceof HttpResponse) {
8
+ const body = event.body as Record<string, unknown> | null;
9
+ if (body && 'data' in body) {
10
+ return event.clone({ body: body['data'] });
11
+ }
12
+ }
13
+ return event;
14
+ }),
15
+ catchError((error: HttpErrorResponse) => {
16
+ const detail = error.error?.detail || error.error?.title || error.message;
17
+ return throwError(() => new HttpErrorResponse({
18
+ error: detail,
19
+ headers: error.headers,
20
+ status: error.status,
21
+ statusText: error.statusText,
22
+ url: error.url ?? undefined,
23
+ }));
24
+ }),
25
+ );
26
+ };
@@ -0,0 +1,11 @@
1
+ import { HttpInterceptorFn } from '@angular/common/http';
2
+
3
+ export const authInterceptor: HttpInterceptorFn = (req, next) => {
4
+ const token = localStorage.getItem('jwt');
5
+ if (token && req.url.includes('/api/')) {
6
+ req = req.clone({
7
+ setHeaders: { Authorization: `Bearer ${token}` },
8
+ });
9
+ }
10
+ return next(req);
11
+ };