@tbs-fitness/contract 0.0.0-stage → 0.1.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.
- package/README.md +123 -2
- package/dist/cjs/audit/audit-log.dto.d.ts +26 -0
- package/dist/cjs/audit/audit-log.dto.js +24 -0
- package/dist/cjs/audit/index.d.ts +1 -0
- package/dist/cjs/audit/index.js +17 -0
- package/dist/cjs/auth/auth.dto.d.ts +121 -0
- package/dist/cjs/auth/auth.dto.js +91 -0
- package/dist/cjs/auth/index.d.ts +1 -0
- package/dist/cjs/auth/index.js +17 -0
- package/dist/cjs/common/api-response.schema.d.ts +37 -0
- package/dist/cjs/common/api-response.schema.js +26 -0
- package/dist/cjs/common/contact.schema.d.ts +5 -0
- package/dist/cjs/common/contact.schema.js +20 -0
- package/dist/cjs/common/index.d.ts +4 -0
- package/dist/cjs/common/index.js +20 -0
- package/dist/cjs/common/pagination.schema.d.ts +14 -0
- package/dist/cjs/common/pagination.schema.js +15 -0
- package/dist/cjs/common/safe-text.schema.d.ts +7 -0
- package/dist/cjs/common/safe-text.schema.js +21 -0
- package/dist/cjs/enums/index.d.ts +12 -0
- package/dist/cjs/enums/index.js +20 -0
- package/dist/cjs/feature-flags/feature-flag.dto.d.ts +142 -0
- package/dist/cjs/feature-flags/feature-flag.dto.js +135 -0
- package/dist/cjs/feature-flags/index.d.ts +1 -0
- package/dist/cjs/feature-flags/index.js +17 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/audit/audit-log.dto.d.ts +26 -0
- package/dist/esm/audit/audit-log.dto.js +21 -0
- package/dist/esm/audit/index.d.ts +1 -0
- package/dist/esm/audit/index.js +1 -0
- package/dist/esm/auth/auth.dto.d.ts +121 -0
- package/dist/esm/auth/auth.dto.js +88 -0
- package/dist/esm/auth/index.d.ts +1 -0
- package/dist/esm/auth/index.js +1 -0
- package/dist/esm/common/api-response.schema.d.ts +37 -0
- package/dist/esm/common/api-response.schema.js +21 -0
- package/dist/esm/common/contact.schema.d.ts +5 -0
- package/dist/esm/common/contact.schema.js +17 -0
- package/dist/esm/common/index.d.ts +4 -0
- package/dist/esm/common/index.js +4 -0
- package/dist/esm/common/pagination.schema.d.ts +14 -0
- package/dist/esm/common/pagination.schema.js +12 -0
- package/dist/esm/common/safe-text.schema.d.ts +7 -0
- package/dist/esm/common/safe-text.schema.js +17 -0
- package/dist/esm/enums/index.d.ts +12 -0
- package/dist/esm/enums/index.js +17 -0
- package/dist/esm/feature-flags/feature-flag.dto.d.ts +142 -0
- package/dist/esm/feature-flags/feature-flag.dto.js +132 -0
- package/dist/esm/feature-flags/index.d.ts +1 -0
- package/dist/esm/feature-flags/index.js +1 -0
- package/package.json +108 -4
package/README.md
CHANGED
|
@@ -1,3 +1,124 @@
|
|
|
1
|
-
#
|
|
1
|
+
# TBS_contract
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The contract between the TBS platforms. The backend, web and mobile apps all import their API schemas, DTO types and enums from this package, so a request or response has one definition and a breaking change shows up as a compile error.
|
|
4
|
+
|
|
5
|
+
It is published as `@tbs-fitness/contract`. The package holds definitions only: no database code, no framework code and no secrets, because it ends up in client bundles.
|
|
6
|
+
|
|
7
|
+
## What is in it
|
|
8
|
+
|
|
9
|
+
| Import | Contents |
|
|
10
|
+
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| `@tbs-fitness/contract/enums` | `ROLES`, `ACCOUNT_STATUSES`, `TOKEN_TRANSPORTS`, `AUDIT_OUTCOMES`, `APP_ENVIRONMENTS`, `FEATURE_FLAG_RULE_TYPES` and their types |
|
|
12
|
+
| `@tbs-fitness/contract/common` | Pagination, response envelopes (`{ data }`, `{ data, meta }`, `{ error }`), `emailSchema`, `phoneSchema`, `safeTextSchema` |
|
|
13
|
+
| `@tbs-fitness/contract/auth` | Login, sessions, password changes and resets, account administration |
|
|
14
|
+
| `@tbs-fitness/contract/audit` | The audit log query and response |
|
|
15
|
+
| `@tbs-fitness/contract/feature-flags` | Feature flag requests and responses |
|
|
16
|
+
|
|
17
|
+
Each domain has its own entry point, so names only need to be unique within a domain. `auditLogResponseSchema`, for example, exists in both `/audit` and `/feature-flags`.
|
|
18
|
+
|
|
19
|
+
## Using it
|
|
20
|
+
|
|
21
|
+
The package needs `zod` 4 from the app, so that every app has a single copy of it.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import type { PaginatedResponse } from '@tbs-fitness/contract/common';
|
|
25
|
+
import {
|
|
26
|
+
type CreateFlagInput,
|
|
27
|
+
createFlagSchema,
|
|
28
|
+
type FlagResponse,
|
|
29
|
+
flagResponseSchema,
|
|
30
|
+
} from '@tbs-fitness/contract/feature-flags';
|
|
31
|
+
|
|
32
|
+
const input: CreateFlagInput = {
|
|
33
|
+
key: 'new-checkout',
|
|
34
|
+
name: 'New checkout',
|
|
35
|
+
environment: 'staging',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const result = createFlagSchema.safeParse(input); // The same rules as the API.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requests have two types. Clients use `*Input`, where fields with a default are optional. The backend uses `*Body`, which is the result after validation. List queries come from the URL as strings, so they have no `*Input`: use `Partial<PaginationQuery>` or `Partial<AuditLogQuery>`.
|
|
42
|
+
|
|
43
|
+
## Installing it
|
|
44
|
+
|
|
45
|
+
The package is public on npm, so installing it needs no token:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install @tbs-fitness/contract
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Adding to it
|
|
52
|
+
|
|
53
|
+
1. Put a domain's schemas in a `.dto.ts` file in `src/<domain>`, and export its types with `z.input` and `z.output`.
|
|
54
|
+
2. Put values shared by several domains in `src/enums` or `src/common`.
|
|
55
|
+
3. For a new domain, add `src/<domain>/index.ts` and an entry to `exports` in `package.json`.
|
|
56
|
+
4. Add tests next to the schema.
|
|
57
|
+
|
|
58
|
+
Keep validation that needs a server, such as a uniqueness check, in the backend.
|
|
59
|
+
|
|
60
|
+
The package is public, so anyone can read everything in it, permanently. Add only what clients can already see in API traffic: never secrets, internal hostnames or customer data.
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
|
|
64
|
+
You need Node 24.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
nvm install
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Command | What it does |
|
|
75
|
+
| ---------------- | --------------------------------------------- |
|
|
76
|
+
| `npm run build` | Builds ESM and CommonJS into `dist` |
|
|
77
|
+
| `npm test` | Runs the unit tests |
|
|
78
|
+
| `npm run check` | Runs the typecheck, lint, format check, tests |
|
|
79
|
+
| `npm run format` | Formats the code |
|
|
80
|
+
|
|
81
|
+
### Trying a change in the backend
|
|
82
|
+
|
|
83
|
+
The backend installs this package from a sibling checkout until the first version is published. After changing it, run this in `TBS_backend`, which rebuilds the package and installs it as a published copy would be. `npm install ../TBS_contract` alone keeps the old copy while the version is unchanged.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
rm -rf node_modules/@tbs-fitness/contract && npm install
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Versioning
|
|
90
|
+
|
|
91
|
+
Versions follow semver, and every app pins the version it was tested with.
|
|
92
|
+
|
|
93
|
+
| Change | Version |
|
|
94
|
+
| ------------------------------------------------------------------- | ------- |
|
|
95
|
+
| Removing or renaming anything exported, or a field in a response | Major |
|
|
96
|
+
| A stricter rule, a new required request field, a removed enum value | Major |
|
|
97
|
+
| A new schema, a new optional request field, a new response field | Minor |
|
|
98
|
+
| A fix that accepts and returns the same data | Patch |
|
|
99
|
+
|
|
100
|
+
A new enum value can break a client with an exhaustive `switch`, so treat it as a major version unless the clients are known to handle it.
|
|
101
|
+
|
|
102
|
+
## Publishing
|
|
103
|
+
|
|
104
|
+
Log in to npm as a user who can publish to the `tbs-fitness` organisation:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm login
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Then, from an up-to-date `main`, set the new version and publish it. The first release is already `0.1.0`, so skip `npm version` for that one.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npm version minor
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npm publish
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
git push --follow-tags
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`npm publish` runs all the checks and a fresh build first. `publishConfig` in `package.json` sets `access` to `public`, because npm otherwise treats a scoped package as private, which needs a paid plan.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const auditLogQuerySchema: z.ZodObject<{
|
|
3
|
+
page: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
4
|
+
pageSize: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
5
|
+
actorId: z.ZodOptional<z.ZodUUID>;
|
|
6
|
+
action: z.ZodOptional<z.ZodString>;
|
|
7
|
+
resourceType: z.ZodOptional<z.ZodString>;
|
|
8
|
+
resourceId: z.ZodOptional<z.ZodString>;
|
|
9
|
+
}, z.core.$strip>;
|
|
10
|
+
export type AuditLogQuery = z.output<typeof auditLogQuerySchema>;
|
|
11
|
+
export declare const auditLogResponseSchema: z.ZodObject<{
|
|
12
|
+
id: z.ZodUUID;
|
|
13
|
+
actorId: z.ZodNullable<z.ZodUUID>;
|
|
14
|
+
action: z.ZodString;
|
|
15
|
+
outcome: z.ZodEnum<{
|
|
16
|
+
success: "success";
|
|
17
|
+
failure: "failure";
|
|
18
|
+
}>;
|
|
19
|
+
resourceType: z.ZodString;
|
|
20
|
+
resourceId: z.ZodNullable<z.ZodString>;
|
|
21
|
+
requestId: z.ZodNullable<z.ZodString>;
|
|
22
|
+
correlationId: z.ZodNullable<z.ZodString>;
|
|
23
|
+
metadata: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
24
|
+
createdAt: z.ZodISODateTime;
|
|
25
|
+
}, z.core.$strip>;
|
|
26
|
+
export type AuditLogResponse = z.output<typeof auditLogResponseSchema>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.auditLogResponseSchema = exports.auditLogQuerySchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const pagination_schema_js_1 = require("../common/pagination.schema.js");
|
|
6
|
+
const index_js_1 = require("../enums/index.js");
|
|
7
|
+
exports.auditLogQuerySchema = pagination_schema_js_1.paginationQuerySchema.extend({
|
|
8
|
+
actorId: zod_1.z.uuid().optional(),
|
|
9
|
+
action: zod_1.z.string().max(100).optional(),
|
|
10
|
+
resourceType: zod_1.z.string().max(100).optional(),
|
|
11
|
+
resourceId: zod_1.z.string().max(100).optional(),
|
|
12
|
+
});
|
|
13
|
+
exports.auditLogResponseSchema = zod_1.z.object({
|
|
14
|
+
id: zod_1.z.uuid(),
|
|
15
|
+
actorId: zod_1.z.uuid().nullable(),
|
|
16
|
+
action: zod_1.z.string(),
|
|
17
|
+
outcome: zod_1.z.enum(index_js_1.AUDIT_OUTCOMES),
|
|
18
|
+
resourceType: zod_1.z.string(),
|
|
19
|
+
resourceId: zod_1.z.string().nullable(),
|
|
20
|
+
requestId: zod_1.z.string().nullable(),
|
|
21
|
+
correlationId: zod_1.z.string().nullable(),
|
|
22
|
+
metadata: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).nullable(),
|
|
23
|
+
createdAt: zod_1.z.iso.datetime(),
|
|
24
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './audit-log.dto.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./audit-log.dto.js"), exports);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const MIN_PASSWORD_LENGTH = 12;
|
|
3
|
+
export declare const MAX_PASSWORD_LENGTH = 128;
|
|
4
|
+
export declare const newPasswordSchema: z.ZodString;
|
|
5
|
+
export declare const loginSchema: z.ZodObject<{
|
|
6
|
+
email: z.ZodPipe<z.ZodString, z.ZodEmail>;
|
|
7
|
+
password: z.ZodString;
|
|
8
|
+
deviceName: z.ZodOptional<z.ZodString>;
|
|
9
|
+
tokenTransport: z.ZodDefault<z.ZodEnum<{
|
|
10
|
+
body: "body";
|
|
11
|
+
cookie: "cookie";
|
|
12
|
+
}>>;
|
|
13
|
+
}, z.core.$strict>;
|
|
14
|
+
export type LoginInput = z.input<typeof loginSchema>;
|
|
15
|
+
export type LoginBody = z.output<typeof loginSchema>;
|
|
16
|
+
export declare const refreshSchema: z.ZodDefault<z.ZodObject<{
|
|
17
|
+
refreshToken: z.ZodOptional<z.ZodString>;
|
|
18
|
+
}, z.core.$strict>>;
|
|
19
|
+
export type RefreshInput = z.input<typeof refreshSchema>;
|
|
20
|
+
export type RefreshBody = z.output<typeof refreshSchema>;
|
|
21
|
+
export declare const changePasswordSchema: z.ZodObject<{
|
|
22
|
+
currentPassword: z.ZodString;
|
|
23
|
+
newPassword: z.ZodString;
|
|
24
|
+
}, z.core.$strict>;
|
|
25
|
+
export type ChangePasswordInput = z.input<typeof changePasswordSchema>;
|
|
26
|
+
export type ChangePasswordBody = z.output<typeof changePasswordSchema>;
|
|
27
|
+
export declare const passwordResetRequestSchema: z.ZodObject<{
|
|
28
|
+
email: z.ZodPipe<z.ZodString, z.ZodEmail>;
|
|
29
|
+
}, z.core.$strict>;
|
|
30
|
+
export type PasswordResetRequestInput = z.input<typeof passwordResetRequestSchema>;
|
|
31
|
+
export type PasswordResetRequestBody = z.output<typeof passwordResetRequestSchema>;
|
|
32
|
+
export declare const passwordResetConfirmSchema: z.ZodObject<{
|
|
33
|
+
token: z.ZodString;
|
|
34
|
+
newPassword: z.ZodString;
|
|
35
|
+
}, z.core.$strict>;
|
|
36
|
+
export type PasswordResetConfirmInput = z.input<typeof passwordResetConfirmSchema>;
|
|
37
|
+
export type PasswordResetConfirmBody = z.output<typeof passwordResetConfirmSchema>;
|
|
38
|
+
export declare const setRolesSchema: z.ZodObject<{
|
|
39
|
+
roles: z.ZodArray<z.ZodEnum<{
|
|
40
|
+
admin: "admin";
|
|
41
|
+
trainer: "trainer";
|
|
42
|
+
customer: "customer";
|
|
43
|
+
}>>;
|
|
44
|
+
}, z.core.$strict>;
|
|
45
|
+
export type SetRolesInput = z.input<typeof setRolesSchema>;
|
|
46
|
+
export type SetRolesBody = z.output<typeof setRolesSchema>;
|
|
47
|
+
export declare const setStatusSchema: z.ZodObject<{
|
|
48
|
+
status: z.ZodEnum<{
|
|
49
|
+
active: "active";
|
|
50
|
+
disabled: "disabled";
|
|
51
|
+
}>;
|
|
52
|
+
}, z.core.$strict>;
|
|
53
|
+
export type SetStatusInput = z.input<typeof setStatusSchema>;
|
|
54
|
+
export type SetStatusBody = z.output<typeof setStatusSchema>;
|
|
55
|
+
export declare const idParamSchema: z.ZodUUID;
|
|
56
|
+
export declare const tokenResponseSchema: z.ZodObject<{
|
|
57
|
+
accessToken: z.ZodString;
|
|
58
|
+
tokenType: z.ZodLiteral<"Bearer">;
|
|
59
|
+
expiresInSeconds: z.ZodNumber;
|
|
60
|
+
refreshToken: z.ZodOptional<z.ZodString>;
|
|
61
|
+
csrfToken: z.ZodOptional<z.ZodString>;
|
|
62
|
+
sessionId: z.ZodUUID;
|
|
63
|
+
}, z.core.$strip>;
|
|
64
|
+
export type TokenResponse = z.output<typeof tokenResponseSchema>;
|
|
65
|
+
export declare const accountResponseSchema: z.ZodObject<{
|
|
66
|
+
id: z.ZodUUID;
|
|
67
|
+
email: z.ZodString;
|
|
68
|
+
roles: z.ZodArray<z.ZodString>;
|
|
69
|
+
permissions: z.ZodArray<z.ZodString>;
|
|
70
|
+
lastLoginAt: z.ZodNullable<z.ZodISODateTime>;
|
|
71
|
+
}, z.core.$strip>;
|
|
72
|
+
export type AccountResponse = z.output<typeof accountResponseSchema>;
|
|
73
|
+
export declare const loginResponseSchema: z.ZodObject<{
|
|
74
|
+
accessToken: z.ZodString;
|
|
75
|
+
tokenType: z.ZodLiteral<"Bearer">;
|
|
76
|
+
expiresInSeconds: z.ZodNumber;
|
|
77
|
+
refreshToken: z.ZodOptional<z.ZodString>;
|
|
78
|
+
csrfToken: z.ZodOptional<z.ZodString>;
|
|
79
|
+
sessionId: z.ZodUUID;
|
|
80
|
+
account: z.ZodObject<{
|
|
81
|
+
id: z.ZodUUID;
|
|
82
|
+
email: z.ZodString;
|
|
83
|
+
roles: z.ZodArray<z.ZodString>;
|
|
84
|
+
permissions: z.ZodArray<z.ZodString>;
|
|
85
|
+
lastLoginAt: z.ZodNullable<z.ZodISODateTime>;
|
|
86
|
+
}, z.core.$strip>;
|
|
87
|
+
}, z.core.$strip>;
|
|
88
|
+
export type LoginResponse = z.output<typeof loginResponseSchema>;
|
|
89
|
+
export declare const sessionResponseSchema: z.ZodObject<{
|
|
90
|
+
id: z.ZodUUID;
|
|
91
|
+
deviceName: z.ZodNullable<z.ZodString>;
|
|
92
|
+
userAgent: z.ZodNullable<z.ZodString>;
|
|
93
|
+
createdAt: z.ZodISODateTime;
|
|
94
|
+
lastUsedAt: z.ZodISODateTime;
|
|
95
|
+
expiresAt: z.ZodISODateTime;
|
|
96
|
+
current: z.ZodBoolean;
|
|
97
|
+
}, z.core.$strip>;
|
|
98
|
+
export type SessionResponse = z.output<typeof sessionResponseSchema>;
|
|
99
|
+
export declare const logoutAllResponseSchema: z.ZodObject<{
|
|
100
|
+
revokedSessions: z.ZodNumber;
|
|
101
|
+
}, z.core.$strip>;
|
|
102
|
+
export type LogoutAllResponse = z.output<typeof logoutAllResponseSchema>;
|
|
103
|
+
export declare const passwordResetRequestedResponseSchema: z.ZodObject<{
|
|
104
|
+
message: z.ZodString;
|
|
105
|
+
}, z.core.$strip>;
|
|
106
|
+
export type PasswordResetRequestedResponse = z.output<typeof passwordResetRequestedResponseSchema>;
|
|
107
|
+
export declare const adminAccountResponseSchema: z.ZodObject<{
|
|
108
|
+
id: z.ZodUUID;
|
|
109
|
+
email: z.ZodString;
|
|
110
|
+
roles: z.ZodArray<z.ZodString>;
|
|
111
|
+
status: z.ZodEnum<{
|
|
112
|
+
active: "active";
|
|
113
|
+
disabled: "disabled";
|
|
114
|
+
}>;
|
|
115
|
+
lastLoginAt: z.ZodNullable<z.ZodISODateTime>;
|
|
116
|
+
}, z.core.$strip>;
|
|
117
|
+
export type AdminAccountResponse = z.output<typeof adminAccountResponseSchema>;
|
|
118
|
+
export declare const revokedSessionsResponseSchema: z.ZodObject<{
|
|
119
|
+
revokedSessions: z.ZodNumber;
|
|
120
|
+
}, z.core.$strip>;
|
|
121
|
+
export type RevokedSessionsResponse = z.output<typeof revokedSessionsResponseSchema>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.revokedSessionsResponseSchema = exports.adminAccountResponseSchema = exports.passwordResetRequestedResponseSchema = exports.logoutAllResponseSchema = exports.sessionResponseSchema = exports.loginResponseSchema = exports.accountResponseSchema = exports.tokenResponseSchema = exports.idParamSchema = exports.setStatusSchema = exports.setRolesSchema = exports.passwordResetConfirmSchema = exports.passwordResetRequestSchema = exports.changePasswordSchema = exports.refreshSchema = exports.loginSchema = exports.newPasswordSchema = exports.MAX_PASSWORD_LENGTH = exports.MIN_PASSWORD_LENGTH = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const contact_schema_js_1 = require("../common/contact.schema.js");
|
|
6
|
+
const safe_text_schema_js_1 = require("../common/safe-text.schema.js");
|
|
7
|
+
const index_js_1 = require("../enums/index.js");
|
|
8
|
+
// Per OWASP: length is what matters, with no composition rules. The upper limit
|
|
9
|
+
// stops very long inputs from being used to exhaust the hashing function.
|
|
10
|
+
exports.MIN_PASSWORD_LENGTH = 12;
|
|
11
|
+
exports.MAX_PASSWORD_LENGTH = 128;
|
|
12
|
+
const MAX_OPAQUE_TOKEN_LENGTH = 512;
|
|
13
|
+
exports.newPasswordSchema = zod_1.z
|
|
14
|
+
.string()
|
|
15
|
+
.min(exports.MIN_PASSWORD_LENGTH)
|
|
16
|
+
.max(exports.MAX_PASSWORD_LENGTH);
|
|
17
|
+
/** No minimum at login, so an old short password fails like any wrong password. */
|
|
18
|
+
const submittedPasswordSchema = zod_1.z.string().min(1).max(exports.MAX_PASSWORD_LENGTH);
|
|
19
|
+
const opaqueTokenSchema = zod_1.z.string().min(1).max(MAX_OPAQUE_TOKEN_LENGTH);
|
|
20
|
+
// Strict objects reject unknown fields, so an attempt to send `roles` or
|
|
21
|
+
// `status` fails loudly rather than being silently ignored.
|
|
22
|
+
exports.loginSchema = zod_1.z.strictObject({
|
|
23
|
+
email: contact_schema_js_1.emailSchema,
|
|
24
|
+
password: submittedPasswordSchema,
|
|
25
|
+
deviceName: (0, safe_text_schema_js_1.safeTextSchema)(100).optional(),
|
|
26
|
+
tokenTransport: zod_1.z.enum(index_js_1.TOKEN_TRANSPORTS).default('body'),
|
|
27
|
+
});
|
|
28
|
+
exports.refreshSchema = zod_1.z
|
|
29
|
+
.strictObject({ refreshToken: opaqueTokenSchema.optional() })
|
|
30
|
+
.default({});
|
|
31
|
+
exports.changePasswordSchema = zod_1.z.strictObject({
|
|
32
|
+
currentPassword: submittedPasswordSchema,
|
|
33
|
+
newPassword: exports.newPasswordSchema,
|
|
34
|
+
});
|
|
35
|
+
exports.passwordResetRequestSchema = zod_1.z.strictObject({
|
|
36
|
+
email: contact_schema_js_1.emailSchema,
|
|
37
|
+
});
|
|
38
|
+
exports.passwordResetConfirmSchema = zod_1.z.strictObject({
|
|
39
|
+
token: opaqueTokenSchema,
|
|
40
|
+
newPassword: exports.newPasswordSchema,
|
|
41
|
+
});
|
|
42
|
+
exports.setRolesSchema = zod_1.z.strictObject({
|
|
43
|
+
roles: zod_1.z.array(zod_1.z.enum(index_js_1.ROLES)).max(index_js_1.ROLES.length),
|
|
44
|
+
});
|
|
45
|
+
exports.setStatusSchema = zod_1.z.strictObject({
|
|
46
|
+
status: zod_1.z.enum(index_js_1.ACCOUNT_STATUSES),
|
|
47
|
+
});
|
|
48
|
+
exports.idParamSchema = zod_1.z.uuid();
|
|
49
|
+
exports.tokenResponseSchema = zod_1.z.object({
|
|
50
|
+
accessToken: zod_1.z.string(),
|
|
51
|
+
tokenType: zod_1.z.literal('Bearer'),
|
|
52
|
+
expiresInSeconds: zod_1.z.number().int(),
|
|
53
|
+
refreshToken: zod_1.z.string().optional(),
|
|
54
|
+
csrfToken: zod_1.z.string().optional(),
|
|
55
|
+
sessionId: zod_1.z.uuid(),
|
|
56
|
+
});
|
|
57
|
+
exports.accountResponseSchema = zod_1.z.object({
|
|
58
|
+
id: zod_1.z.uuid(),
|
|
59
|
+
email: zod_1.z.string(),
|
|
60
|
+
roles: zod_1.z.array(zod_1.z.string()),
|
|
61
|
+
permissions: zod_1.z.array(zod_1.z.string()),
|
|
62
|
+
lastLoginAt: zod_1.z.iso.datetime().nullable(),
|
|
63
|
+
});
|
|
64
|
+
exports.loginResponseSchema = exports.tokenResponseSchema.extend({
|
|
65
|
+
account: exports.accountResponseSchema,
|
|
66
|
+
});
|
|
67
|
+
exports.sessionResponseSchema = zod_1.z.object({
|
|
68
|
+
id: zod_1.z.uuid(),
|
|
69
|
+
deviceName: zod_1.z.string().nullable(),
|
|
70
|
+
userAgent: zod_1.z.string().nullable(),
|
|
71
|
+
createdAt: zod_1.z.iso.datetime(),
|
|
72
|
+
lastUsedAt: zod_1.z.iso.datetime(),
|
|
73
|
+
expiresAt: zod_1.z.iso.datetime(),
|
|
74
|
+
current: zod_1.z.boolean(),
|
|
75
|
+
});
|
|
76
|
+
exports.logoutAllResponseSchema = zod_1.z.object({
|
|
77
|
+
revokedSessions: zod_1.z.number().int(),
|
|
78
|
+
});
|
|
79
|
+
exports.passwordResetRequestedResponseSchema = zod_1.z.object({
|
|
80
|
+
message: zod_1.z.string(),
|
|
81
|
+
});
|
|
82
|
+
exports.adminAccountResponseSchema = zod_1.z.object({
|
|
83
|
+
id: zod_1.z.uuid(),
|
|
84
|
+
email: zod_1.z.string(),
|
|
85
|
+
roles: zod_1.z.array(zod_1.z.string()),
|
|
86
|
+
status: zod_1.z.enum(index_js_1.ACCOUNT_STATUSES),
|
|
87
|
+
lastLoginAt: zod_1.z.iso.datetime().nullable(),
|
|
88
|
+
});
|
|
89
|
+
exports.revokedSessionsResponseSchema = zod_1.z.object({
|
|
90
|
+
revokedSessions: zod_1.z.number().int(),
|
|
91
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './auth.dto.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./auth.dto.js"), exports);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type PaginationMeta } from './pagination.schema.js';
|
|
3
|
+
export declare const apiErrorResponseSchema: z.ZodObject<{
|
|
4
|
+
error: z.ZodObject<{
|
|
5
|
+
statusCode: z.ZodNumber;
|
|
6
|
+
code: z.ZodString;
|
|
7
|
+
message: z.ZodString;
|
|
8
|
+
details: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
9
|
+
location: z.ZodString;
|
|
10
|
+
path: z.ZodString;
|
|
11
|
+
message: z.ZodString;
|
|
12
|
+
}, z.core.$strip>>>;
|
|
13
|
+
requestId: z.ZodString;
|
|
14
|
+
timestamp: z.ZodISODateTime;
|
|
15
|
+
path: z.ZodString;
|
|
16
|
+
}, z.core.$strip>;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
export type ApiErrorResponse = z.output<typeof apiErrorResponseSchema>;
|
|
19
|
+
export declare const dataResponseSchema: <T extends z.ZodType>(data: T) => z.ZodObject<{
|
|
20
|
+
data: T;
|
|
21
|
+
}, z.core.$strip>;
|
|
22
|
+
export interface DataResponse<T> {
|
|
23
|
+
data: T;
|
|
24
|
+
}
|
|
25
|
+
export declare const paginatedResponseSchema: <T extends z.ZodType>(item: T) => z.ZodObject<{
|
|
26
|
+
data: z.ZodArray<T>;
|
|
27
|
+
meta: z.ZodObject<{
|
|
28
|
+
page: z.ZodNumber;
|
|
29
|
+
pageSize: z.ZodNumber;
|
|
30
|
+
totalItems: z.ZodNumber;
|
|
31
|
+
totalPages: z.ZodNumber;
|
|
32
|
+
}, z.core.$strip>;
|
|
33
|
+
}, z.core.$strip>;
|
|
34
|
+
export interface PaginatedResponse<T> {
|
|
35
|
+
data: T[];
|
|
36
|
+
meta: PaginationMeta;
|
|
37
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.paginatedResponseSchema = exports.dataResponseSchema = exports.apiErrorResponseSchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const pagination_schema_js_1 = require("./pagination.schema.js");
|
|
6
|
+
exports.apiErrorResponseSchema = zod_1.z.object({
|
|
7
|
+
error: zod_1.z.object({
|
|
8
|
+
statusCode: zod_1.z.number().int(),
|
|
9
|
+
code: zod_1.z.string(),
|
|
10
|
+
message: zod_1.z.string(),
|
|
11
|
+
details: zod_1.z
|
|
12
|
+
.array(zod_1.z.object({
|
|
13
|
+
location: zod_1.z.string(),
|
|
14
|
+
path: zod_1.z.string(),
|
|
15
|
+
message: zod_1.z.string(),
|
|
16
|
+
}))
|
|
17
|
+
.optional(),
|
|
18
|
+
requestId: zod_1.z.string(),
|
|
19
|
+
timestamp: zod_1.z.iso.datetime(),
|
|
20
|
+
path: zod_1.z.string(),
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
const dataResponseSchema = (data) => zod_1.z.object({ data });
|
|
24
|
+
exports.dataResponseSchema = dataResponseSchema;
|
|
25
|
+
const paginatedResponseSchema = (item) => zod_1.z.object({ data: zod_1.z.array(item), meta: pagination_schema_js_1.paginationMetaSchema });
|
|
26
|
+
exports.paginatedResponseSchema = paginatedResponseSchema;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Trimmed and lower-cased, so that one address cannot map to two accounts. */
|
|
3
|
+
export declare const emailSchema: z.ZodPipe<z.ZodString, z.ZodEmail>;
|
|
4
|
+
/** Normalized to E.164 by dropping common formatting characters. */
|
|
5
|
+
export declare const phoneSchema: z.ZodString;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.phoneSchema = exports.emailSchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
// RFC 5321 limits a usable address to 254 characters.
|
|
6
|
+
const MAX_EMAIL_LENGTH = 254;
|
|
7
|
+
/** Trimmed and lower-cased, so that one address cannot map to two accounts. */
|
|
8
|
+
exports.emailSchema = zod_1.z
|
|
9
|
+
.string()
|
|
10
|
+
.trim()
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.max(MAX_EMAIL_LENGTH)
|
|
13
|
+
.pipe(zod_1.z.email());
|
|
14
|
+
/** Normalized to E.164 by dropping common formatting characters. */
|
|
15
|
+
exports.phoneSchema = zod_1.z
|
|
16
|
+
.string()
|
|
17
|
+
.overwrite((value) => value.replace(/[\s().-]/g, ''))
|
|
18
|
+
.regex(/^\+[1-9]\d{7,14}$/, {
|
|
19
|
+
error: 'Must be an international number such as +919876543210',
|
|
20
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./api-response.schema.js"), exports);
|
|
18
|
+
__exportStar(require("./contact.schema.js"), exports);
|
|
19
|
+
__exportStar(require("./pagination.schema.js"), exports);
|
|
20
|
+
__exportStar(require("./safe-text.schema.js"), exports);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const MAX_PAGE_SIZE = 100;
|
|
3
|
+
export declare const paginationQuerySchema: z.ZodObject<{
|
|
4
|
+
page: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
5
|
+
pageSize: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export type PaginationQuery = z.output<typeof paginationQuerySchema>;
|
|
8
|
+
export declare const paginationMetaSchema: z.ZodObject<{
|
|
9
|
+
page: z.ZodNumber;
|
|
10
|
+
pageSize: z.ZodNumber;
|
|
11
|
+
totalItems: z.ZodNumber;
|
|
12
|
+
totalPages: z.ZodNumber;
|
|
13
|
+
}, z.core.$strip>;
|
|
14
|
+
export type PaginationMeta = z.output<typeof paginationMetaSchema>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.paginationMetaSchema = exports.paginationQuerySchema = exports.MAX_PAGE_SIZE = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
exports.MAX_PAGE_SIZE = 100;
|
|
6
|
+
exports.paginationQuerySchema = zod_1.z.object({
|
|
7
|
+
page: zod_1.z.coerce.number().int().min(1).default(1),
|
|
8
|
+
pageSize: zod_1.z.coerce.number().int().min(1).max(exports.MAX_PAGE_SIZE).default(20),
|
|
9
|
+
});
|
|
10
|
+
exports.paginationMetaSchema = zod_1.z.object({
|
|
11
|
+
page: zod_1.z.number().int(),
|
|
12
|
+
pageSize: zod_1.z.number().int(),
|
|
13
|
+
totalItems: zod_1.z.number().int(),
|
|
14
|
+
totalPages: zod_1.z.number().int(),
|
|
15
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Short plain-text input such as a device name. Output encoding remains the
|
|
4
|
+
* renderer's job; rejecting markup here keeps stored values harmless even if a
|
|
5
|
+
* future client forgets. Use a real sanitizer for fields meant to hold HTML.
|
|
6
|
+
*/
|
|
7
|
+
export declare const safeTextSchema: (maxLength: number) => z.ZodString;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.safeTextSchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
// eslint-disable-next-line no-control-regex
|
|
6
|
+
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/g;
|
|
7
|
+
const HTML_LIKE = /<[a-zA-Z!/?]/;
|
|
8
|
+
/**
|
|
9
|
+
* Short plain-text input such as a device name. Output encoding remains the
|
|
10
|
+
* renderer's job; rejecting markup here keeps stored values harmless even if a
|
|
11
|
+
* future client forgets. Use a real sanitizer for fields meant to hold HTML.
|
|
12
|
+
*/
|
|
13
|
+
const safeTextSchema = (maxLength) => zod_1.z
|
|
14
|
+
.string()
|
|
15
|
+
.overwrite((value) => value.replace(CONTROL_CHARACTERS, '').trim())
|
|
16
|
+
.min(1)
|
|
17
|
+
.max(maxLength)
|
|
18
|
+
.refine((value) => !HTML_LIKE.test(value), {
|
|
19
|
+
error: 'Must not contain markup',
|
|
20
|
+
});
|
|
21
|
+
exports.safeTextSchema = safeTextSchema;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const APP_ENVIRONMENTS: readonly ["development", "staging", "production"];
|
|
2
|
+
export type AppEnvironment = (typeof APP_ENVIRONMENTS)[number];
|
|
3
|
+
export declare const ROLES: readonly ["admin", "trainer", "customer"];
|
|
4
|
+
export type Role = (typeof ROLES)[number];
|
|
5
|
+
export declare const ACCOUNT_STATUSES: readonly ["active", "disabled"];
|
|
6
|
+
export type AccountStatus = (typeof ACCOUNT_STATUSES)[number];
|
|
7
|
+
export declare const TOKEN_TRANSPORTS: readonly ["body", "cookie"];
|
|
8
|
+
export type TokenTransport = (typeof TOKEN_TRANSPORTS)[number];
|
|
9
|
+
export declare const AUDIT_OUTCOMES: readonly ["success", "failure"];
|
|
10
|
+
export type AuditOutcome = (typeof AUDIT_OUTCOMES)[number];
|
|
11
|
+
export declare const FEATURE_FLAG_RULE_TYPES: readonly ["user", "customer", "trainer", "role"];
|
|
12
|
+
export type FeatureFlagRuleType = (typeof FEATURE_FLAG_RULE_TYPES)[number];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// These values are stored in the database and sent over the API. Renaming or
|
|
3
|
+
// removing one is a breaking change, so it needs a major version.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.FEATURE_FLAG_RULE_TYPES = exports.AUDIT_OUTCOMES = exports.TOKEN_TRANSPORTS = exports.ACCOUNT_STATUSES = exports.ROLES = exports.APP_ENVIRONMENTS = void 0;
|
|
6
|
+
exports.APP_ENVIRONMENTS = [
|
|
7
|
+
'development',
|
|
8
|
+
'staging',
|
|
9
|
+
'production',
|
|
10
|
+
];
|
|
11
|
+
exports.ROLES = ['admin', 'trainer', 'customer'];
|
|
12
|
+
exports.ACCOUNT_STATUSES = ['active', 'disabled'];
|
|
13
|
+
exports.TOKEN_TRANSPORTS = ['body', 'cookie'];
|
|
14
|
+
exports.AUDIT_OUTCOMES = ['success', 'failure'];
|
|
15
|
+
exports.FEATURE_FLAG_RULE_TYPES = [
|
|
16
|
+
'user',
|
|
17
|
+
'customer',
|
|
18
|
+
'trainer',
|
|
19
|
+
'role',
|
|
20
|
+
];
|