@ftisindia/create-app 0.1.2 → 0.1.4
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 +65 -0
- package/package.json +1 -1
- package/template/README.md +65 -1
- package/template/_package.json +0 -2
- package/template/docs/API_REFERENCE.md +13 -0
- package/template/docs/OAUTH.md +7 -3
- package/template/scripts/gen-module.mjs +2 -0
- package/template/src/app.module.ts +16 -22
- package/template/src/common/dto/error-response.dto.ts +3 -3
- package/template/src/common/dto/membership-response.dto.ts +26 -14
- package/template/src/common/dto/mutation-response.dto.ts +1 -1
- package/template/src/common/dto/pagination-query.dto.ts +37 -0
- package/template/src/common/dto/role-summary.dto.ts +5 -5
- package/template/src/common/dto/user-summary.dto.ts +6 -6
- package/template/src/common/filters/http-exception.filter.ts +9 -19
- package/template/src/common/swagger/api-error-responses.ts +12 -12
- package/template/src/config/app.config.ts +3 -3
- package/template/src/config/auth.config.ts +3 -3
- package/template/src/config/database.config.ts +3 -3
- package/template/src/config/env.validation.ts +58 -40
- package/template/src/config/index.ts +5 -5
- package/template/src/config/rbac.config.ts +3 -3
- package/template/src/database/prisma/prisma-transaction.ts +1 -1
- package/template/src/database/prisma/prisma.module.ts +2 -2
- package/template/src/database/prisma/prisma.service.ts +3 -6
- package/template/src/main.ts +11 -11
- package/template/src/modules/access-control/access-control.module.ts +9 -9
- package/template/src/modules/access-control/application/role-permission-policy.ts +71 -0
- package/template/src/modules/access-control/application/route-registry.validator.ts +34 -63
- package/template/src/modules/access-control/application/services/ability.factory.ts +5 -9
- package/template/src/modules/access-control/application/services/access-control.service.ts +78 -85
- package/template/src/modules/access-control/application/services/permission.guard.ts +16 -21
- package/template/src/modules/access-control/application/services/rbac-cache.service.ts +7 -9
- package/template/src/modules/access-control/dto/access-control-response.dto.ts +32 -20
- package/template/src/modules/access-control/dto/create-role.dto.ts +6 -6
- package/template/src/modules/access-control/dto/update-role-permissions.dto.ts +3 -10
- package/template/src/modules/access-control/dto/update-role.dto.ts +6 -6
- package/template/src/modules/access-control/presentation/access-control.controller.ts +69 -74
- package/template/src/modules/access-control/presentation/permissions.decorator.ts +3 -3
- package/template/src/modules/access-control/presentation/public.decorator.ts +2 -2
- package/template/src/modules/access-control/types/permission-key.ts +19 -19
- package/template/src/modules/access-control/types/route-permission-registry.ts +76 -76
- package/template/src/modules/audit/application/services/audit.service.ts +7 -7
- package/template/src/modules/audit/audit.module.ts +4 -4
- package/template/src/modules/audit/dto/audit-response.dto.ts +18 -18
- package/template/src/modules/audit/dto/list-audit-logs-query.dto.ts +14 -14
- package/template/src/modules/audit/presentation/audit.controller.ts +17 -23
- package/template/src/modules/auth/application/services/auth.service.ts +147 -110
- package/template/src/modules/auth/application/services/password.service.ts +2 -2
- package/template/src/modules/auth/application/services/token.service.ts +20 -21
- package/template/src/modules/auth/auth.module.ts +20 -47
- package/template/src/modules/auth/dto/auth-response.dto.ts +9 -10
- package/template/src/modules/auth/dto/login.dto.ts +4 -4
- package/template/src/modules/auth/dto/logout.dto.ts +1 -1
- package/template/src/modules/auth/dto/oauth-exchange.dto.ts +4 -5
- package/template/src/modules/auth/dto/refresh-token.dto.ts +4 -5
- package/template/src/modules/auth/dto/signup.dto.ts +5 -11
- package/template/src/modules/auth/infrastructure/passport/google-auth.guard.ts +6 -14
- package/template/src/modules/auth/infrastructure/passport/google-oauth-state.store.ts +98 -0
- package/template/src/modules/auth/infrastructure/passport/google.strategy.ts +21 -30
- package/template/src/modules/auth/infrastructure/passport/jwt-auth.guard.ts +3 -3
- package/template/src/modules/auth/infrastructure/passport/jwt.strategy.ts +11 -11
- package/template/src/modules/auth/presentation/auth.controller.ts +45 -45
- package/template/src/modules/auth/presentation/current-user.decorator.ts +3 -5
- package/template/src/modules/auth/presentation/google-oauth-exception.filter.ts +5 -10
- package/template/src/modules/health/dto/health-response.dto.ts +5 -5
- package/template/src/modules/health/health.module.ts +2 -2
- package/template/src/modules/health/presentation/health.controller.ts +13 -13
- package/template/src/modules/invitations/application/services/invitations.service.ts +127 -176
- package/template/src/modules/invitations/dto/accept-invitation.dto.ts +6 -7
- package/template/src/modules/invitations/dto/create-invitation.dto.ts +14 -15
- package/template/src/modules/invitations/dto/invitation-response.dto.ts +37 -29
- package/template/src/modules/invitations/dto/invitation-token.dto.ts +4 -4
- package/template/src/modules/invitations/invitations.module.ts +5 -5
- package/template/src/modules/invitations/presentation/invitations.controller.ts +61 -63
- package/template/src/modules/memberships/application/services/memberships.service.ts +70 -84
- package/template/src/modules/memberships/dto/transfer-owner.dto.ts +4 -4
- package/template/src/modules/memberships/dto/update-billing-contact.dto.ts +2 -2
- package/template/src/modules/memberships/dto/update-membership-owner.dto.ts +2 -2
- package/template/src/modules/memberships/dto/update-membership-role.dto.ts +4 -4
- package/template/src/modules/memberships/dto/update-membership-status.dto.ts +3 -3
- package/template/src/modules/memberships/memberships.module.ts +4 -4
- package/template/src/modules/memberships/presentation/memberships.controller.ts +83 -99
- package/template/src/modules/organisations/application/services/organisations.service.ts +21 -23
- package/template/src/modules/organisations/dto/create-organisation.dto.ts +6 -13
- package/template/src/modules/organisations/dto/organisation-response.dto.ts +14 -14
- package/template/src/modules/organisations/infrastructure/repositories/organisations.repository.ts +4 -7
- package/template/src/modules/organisations/organisations.module.ts +5 -5
- package/template/src/modules/organisations/presentation/organisations.controller.ts +14 -23
- package/template/src/modules/organisations/types/default-organisation-data.ts +3 -9
- package/template/src/modules/request-context/application/services/request-context.service.ts +15 -7
- package/template/src/modules/request-context/presentation/org-scope.guard.ts +4 -9
- package/template/src/modules/request-context/presentation/request-context.interceptor.ts +4 -9
- package/template/src/modules/request-context/presentation/request-context.middleware.ts +7 -8
- package/template/src/modules/request-context/request-context.module.ts +7 -7
- package/template/src/modules/request-context/types/request-context.ts +2 -2
- package/template/src/modules/sample/application/services/sample.service.ts +10 -8
- package/template/src/modules/sample/dto/sample-echo.dto.ts +3 -3
- package/template/src/modules/sample/dto/sample-response.dto.ts +12 -12
- package/template/src/modules/sample/presentation/sample.controller.ts +25 -42
- package/template/src/modules/sample/sample.module.ts +4 -4
- package/template/src/modules/settings/application/services/settings.service.ts +15 -27
- package/template/src/modules/settings/dto/setting-response.dto.ts +9 -9
- package/template/src/modules/settings/dto/update-setting.dto.ts +5 -5
- package/template/src/modules/settings/presentation/settings.controller.ts +29 -35
- package/template/src/modules/settings/settings.module.ts +5 -5
- package/template/src/modules/settings/types/setting-definitions.ts +49 -33
- package/template/test/auth-refresh.spec.ts +90 -0
- package/template/test/role-permission-policy.spec.ts +94 -0
package/README.md
CHANGED
|
@@ -92,3 +92,68 @@ add a module and the architecture rules.
|
|
|
92
92
|
Released under the **PolyForm Noncommercial License 1.0.0** — free for any noncommercial
|
|
93
93
|
use. **Commercial use requires a separate commercial license from
|
|
94
94
|
[ftisindia.com](https://ftisindia.com).** See the bundled `LICENSE` file.
|
|
95
|
+
|
|
96
|
+
## Generating a module with `gen:module`
|
|
97
|
+
|
|
98
|
+
Every project this CLI generates ships a `gen:module` script. In any generated project,
|
|
99
|
+
run it to scaffold a new feature module that already follows the project's auth, RBAC,
|
|
100
|
+
request-context, and audit conventions.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npm run gen:module -- billing
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
> Note the `--` before the name — it tells npm to pass the argument through to the
|
|
107
|
+
> generator. Names are normalized automatically, so `billing`, `Billing`,
|
|
108
|
+
> `"invoice items"`, and `invoiceItems` all resolve to the same module.
|
|
109
|
+
|
|
110
|
+
### What it generates
|
|
111
|
+
|
|
112
|
+
For a name like `billing`, you get:
|
|
113
|
+
|
|
114
|
+
- `src/modules/billing/billing.module.ts` — the NestJS module (wires controller + service)
|
|
115
|
+
- `src/modules/billing/dto/billing-echo.dto.ts` — a sample DTO (class-validator + Swagger)
|
|
116
|
+
- `src/modules/billing/application/services/billing.service.ts` — a service with
|
|
117
|
+
`getStatus()` and `echo()` that use Prisma, request context, and write an audit-log row
|
|
118
|
+
- `src/modules/billing/presentation/billing.controller.ts` — an org-scoped controller
|
|
119
|
+
guarded by `JwtAuthGuard`, `OrgScopeGuard`, and `PermissionGuard`, exposing:
|
|
120
|
+
- `GET /organisations/:orgId/billing/status` — requires `billing.read`
|
|
121
|
+
- `POST /organisations/:orgId/billing/echo` — requires `billing.update`
|
|
122
|
+
- `test/billing.spec.ts` — a starter unit test
|
|
123
|
+
|
|
124
|
+
It also keeps RBAC in sync automatically by appending to:
|
|
125
|
+
|
|
126
|
+
- `src/modules/access-control/types/permission-key.ts` — adds `billing.read` and `billing.update`
|
|
127
|
+
- `src/modules/access-control/types/route-permission-registry.ts` — adds the two route
|
|
128
|
+
entries (startup and tests fail if a guarded route is missing here)
|
|
129
|
+
|
|
130
|
+
The generator never overwrites existing files — it stops if any target already exists.
|
|
131
|
+
|
|
132
|
+
### Two steps to finish
|
|
133
|
+
|
|
134
|
+
1. **Register the module** in `src/app.module.ts` (the generator prints this reminder):
|
|
135
|
+
```ts
|
|
136
|
+
import { BillingModule } from './modules/billing/billing.module';
|
|
137
|
+
// add BillingModule to the @Module({ imports: [...] }) array
|
|
138
|
+
```
|
|
139
|
+
2. **Re-seed permissions** so the new keys exist in the database and can be granted to roles:
|
|
140
|
+
```bash
|
|
141
|
+
npm run prisma:seed
|
|
142
|
+
```
|
|
143
|
+
Then assign `billing.read` / `billing.update` to the appropriate role(s).
|
|
144
|
+
|
|
145
|
+
Start the app and the new guarded endpoints are live (visible in Swagger at `/docs`).
|
|
146
|
+
|
|
147
|
+
### When not to use it
|
|
148
|
+
|
|
149
|
+
- **You need a non-org-scoped or public route.** The output is hard-wired to
|
|
150
|
+
`/organisations/:orgId/<name>` with the org guards. Build global/unauthenticated
|
|
151
|
+
endpoints by hand instead.
|
|
152
|
+
- **You're extending an existing module.** It scaffolds a brand-new module and refuses
|
|
153
|
+
to overwrite — add files to the existing module manually.
|
|
154
|
+
- **Your feature doesn't map to simple read/update permissions.** It generates exactly
|
|
155
|
+
`<name>.read` and `<name>.update`; for richer permission sets, edit the generated
|
|
156
|
+
`permission-key.ts` and registry entries afterward.
|
|
157
|
+
- **You want a different shape than the `status` + `echo` reference endpoints.** Treat
|
|
158
|
+
the output as a starting point, or copy `src/modules/sample` (see
|
|
159
|
+
`docs/SAMPLE_MODULE.md`) for a fuller reference.
|
package/package.json
CHANGED
package/template/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# **PROJECT_NAME**
|
|
2
2
|
|
|
3
3
|
NestJS modular-monolith starter with email/password auth, Google OAuth handoff, organisations, memberships, invitations, per-organisation RBAC with CASL, request context, audit logs, typed settings, Swagger, and a complete sample module.
|
|
4
4
|
|
|
@@ -144,3 +144,67 @@ Swagger is mounted at `/docs` and includes tags, request schemas, response DTOs,
|
|
|
144
144
|
common error responses, bearer authorization, route parameters, and examples for
|
|
145
145
|
all implemented API routes. See `docs/API_REFERENCE.md` before adding or changing
|
|
146
146
|
controllers.
|
|
147
|
+
|
|
148
|
+
## Generating a module with `gen:module`
|
|
149
|
+
|
|
150
|
+
Scaffold a new feature module that already follows the project's auth, RBAC,
|
|
151
|
+
request-context, and audit conventions.
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
npm run gen:module -- billing
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
> Note the `--` before the name — it tells npm to pass the argument through to the
|
|
158
|
+
> generator. Names are normalized automatically, so `billing`, `Billing`,
|
|
159
|
+
> `"invoice items"`, and `invoiceItems` all resolve to the same module.
|
|
160
|
+
|
|
161
|
+
### What it generates
|
|
162
|
+
|
|
163
|
+
For a name like `billing`, you get:
|
|
164
|
+
|
|
165
|
+
- `src/modules/billing/billing.module.ts` — the NestJS module (wires controller + service)
|
|
166
|
+
- `src/modules/billing/dto/billing-echo.dto.ts` — a sample DTO (class-validator + Swagger)
|
|
167
|
+
- `src/modules/billing/application/services/billing.service.ts` — a service with
|
|
168
|
+
`getStatus()` and `echo()` that use Prisma, request context, and write an audit-log row
|
|
169
|
+
- `src/modules/billing/presentation/billing.controller.ts` — an org-scoped controller
|
|
170
|
+
guarded by `JwtAuthGuard`, `OrgScopeGuard`, and `PermissionGuard`, exposing:
|
|
171
|
+
- `GET /organisations/:orgId/billing/status` — requires `billing.read`
|
|
172
|
+
- `POST /organisations/:orgId/billing/echo` — requires `billing.update`
|
|
173
|
+
- `test/billing.spec.ts` — a starter unit test
|
|
174
|
+
|
|
175
|
+
It also keeps RBAC in sync automatically by appending to:
|
|
176
|
+
|
|
177
|
+
- `src/modules/access-control/types/permission-key.ts` — adds `billing.read` and `billing.update`
|
|
178
|
+
- `src/modules/access-control/types/route-permission-registry.ts` — adds the two route
|
|
179
|
+
entries (startup and tests fail if a guarded route is missing here)
|
|
180
|
+
|
|
181
|
+
The generator never overwrites existing files — it stops if any target already exists.
|
|
182
|
+
|
|
183
|
+
### Two steps to finish
|
|
184
|
+
|
|
185
|
+
1. **Register the module** in `src/app.module.ts` (the generator prints this reminder):
|
|
186
|
+
```ts
|
|
187
|
+
import { BillingModule } from './modules/billing/billing.module';
|
|
188
|
+
// add BillingModule to the @Module({ imports: [...] }) array
|
|
189
|
+
```
|
|
190
|
+
2. **Re-seed permissions** so the new keys exist in the database and can be granted to roles:
|
|
191
|
+
```bash
|
|
192
|
+
npm run prisma:seed
|
|
193
|
+
```
|
|
194
|
+
Then assign `billing.read` / `billing.update` to the appropriate role(s).
|
|
195
|
+
|
|
196
|
+
Start the app and the new guarded endpoints are live (visible in Swagger at `/docs`).
|
|
197
|
+
|
|
198
|
+
### When not to use it
|
|
199
|
+
|
|
200
|
+
- **You need a non-org-scoped or public route.** The output is hard-wired to
|
|
201
|
+
`/organisations/:orgId/<name>` with the org guards. Build global/unauthenticated
|
|
202
|
+
endpoints by hand instead.
|
|
203
|
+
- **You're extending an existing module.** It scaffolds a brand-new module and refuses
|
|
204
|
+
to overwrite — add files to the existing module manually.
|
|
205
|
+
- **Your feature doesn't map to simple read/update permissions.** It generates exactly
|
|
206
|
+
`<name>.read` and `<name>.update`; for richer permission sets, edit the generated
|
|
207
|
+
`permission-key.ts` and registry entries afterward.
|
|
208
|
+
- **You want a different shape than the `status` + `echo` reference endpoints.** Treat
|
|
209
|
+
the output as a starting point, or copy `src/modules/sample` (see
|
|
210
|
+
`docs/SAMPLE_MODULE.md`) for a fuller reference.
|
package/template/_package.json
CHANGED
|
@@ -42,7 +42,6 @@
|
|
|
42
42
|
"bcryptjs": "3.0.3",
|
|
43
43
|
"class-transformer": "0.5.1",
|
|
44
44
|
"class-validator": "0.15.1",
|
|
45
|
-
"express-session": "1.19.0",
|
|
46
45
|
"passport": "0.7.0",
|
|
47
46
|
"passport-google-oauth20": "2.0.0",
|
|
48
47
|
"passport-jwt": "4.0.1",
|
|
@@ -56,7 +55,6 @@
|
|
|
56
55
|
"@nestjs/cli": "11.0.21",
|
|
57
56
|
"@nestjs/schematics": "11.1.0",
|
|
58
57
|
"@nestjs/testing": "11.1.24",
|
|
59
|
-
"@types/express-session": "1.19.0",
|
|
60
58
|
"@types/jest": "30.0.0",
|
|
61
59
|
"@types/node": "20.19.41",
|
|
62
60
|
"@types/passport": "1.0.17",
|
|
@@ -28,6 +28,19 @@ After authorization, Swagger can call protected routes directly.
|
|
|
28
28
|
Successful responses are documented per endpoint with typed response DTOs. Most
|
|
29
29
|
date values are ISO strings.
|
|
30
30
|
|
|
31
|
+
Growable list endpoints such as memberships, invitations, and roles are cursor
|
|
32
|
+
paginated:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"items": [],
|
|
37
|
+
"nextCursor": null
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Use `?limit=50` and pass `nextCursor` back as `?cursor=...` to fetch the next
|
|
42
|
+
page.
|
|
43
|
+
|
|
31
44
|
Errors use one common envelope:
|
|
32
45
|
|
|
33
46
|
```json
|
package/template/docs/OAUTH.md
CHANGED
|
@@ -18,9 +18,12 @@ Failures redirect to `AUTH_GOOGLE_ERROR_REDIRECT_URL?error=oauth_failed`.
|
|
|
18
18
|
|
|
19
19
|
## State And PKCE
|
|
20
20
|
|
|
21
|
-
Google OAuth uses state and PKCE.
|
|
21
|
+
Google OAuth uses state and PKCE. The API encrypts and authenticates the PKCE
|
|
22
|
+
code verifier inside a short-lived state value, so the callback can be handled
|
|
23
|
+
without an Express session store.
|
|
22
24
|
|
|
23
|
-
The
|
|
25
|
+
The app remains stateless JWT. There is no `passport.session()`, no persistent
|
|
26
|
+
login session, and no in-memory OAuth session store.
|
|
24
27
|
|
|
25
28
|
## Auth Endpoint Hardening
|
|
26
29
|
|
|
@@ -43,4 +46,5 @@ SESSION_SECRET=
|
|
|
43
46
|
|
|
44
47
|
`SESSION_SECRET` must be at least 32 random characters.
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
When `NODE_ENV=production`, all configured Google callback/success/error URLs
|
|
50
|
+
must use HTTPS.
|
|
@@ -78,6 +78,8 @@ export class ${names.pascal}Service {
|
|
|
78
78
|
messageLength: message.length,
|
|
79
79
|
requestId: this.requestContext.getRequestId(),
|
|
80
80
|
} satisfies Prisma.InputJsonObject,
|
|
81
|
+
ipAddress: this.requestContext.getIpAddress(),
|
|
82
|
+
userAgent: this.requestContext.getUserAgent(),
|
|
81
83
|
},
|
|
82
84
|
});
|
|
83
85
|
|
|
@@ -1,25 +1,19 @@
|
|
|
1
|
-
import { Module } from
|
|
2
|
-
import { ConfigModule } from
|
|
3
|
-
import { APP_GUARD } from
|
|
4
|
-
import { ThrottlerGuard, ThrottlerModule } from
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { InvitationsModule } from "./modules/invitations/invitations.module";
|
|
18
|
-
import { MembershipsModule } from "./modules/memberships/memberships.module";
|
|
19
|
-
import { OrganisationsModule } from "./modules/organisations/organisations.module";
|
|
20
|
-
import { RequestContextModule } from "./modules/request-context/request-context.module";
|
|
21
|
-
import { SampleModule } from "./modules/sample/sample.module";
|
|
22
|
-
import { SettingsModule } from "./modules/settings/settings.module";
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { ConfigModule } from '@nestjs/config';
|
|
3
|
+
import { APP_GUARD } from '@nestjs/core';
|
|
4
|
+
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
|
5
|
+
import { appConfig, authConfig, databaseConfig, rbacConfig, validate } from './config';
|
|
6
|
+
import { PrismaModule } from './database/prisma/prisma.module';
|
|
7
|
+
import { AccessControlModule } from './modules/access-control/access-control.module';
|
|
8
|
+
import { AuditModule } from './modules/audit/audit.module';
|
|
9
|
+
import { AuthModule } from './modules/auth/auth.module';
|
|
10
|
+
import { HealthModule } from './modules/health/health.module';
|
|
11
|
+
import { InvitationsModule } from './modules/invitations/invitations.module';
|
|
12
|
+
import { MembershipsModule } from './modules/memberships/memberships.module';
|
|
13
|
+
import { OrganisationsModule } from './modules/organisations/organisations.module';
|
|
14
|
+
import { RequestContextModule } from './modules/request-context/request-context.module';
|
|
15
|
+
import { SampleModule } from './modules/sample/sample.module';
|
|
16
|
+
import { SettingsModule } from './modules/settings/settings.module';
|
|
23
17
|
|
|
24
18
|
@Module({
|
|
25
19
|
imports: [
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { ApiProperty, ApiPropertyOptional } from
|
|
1
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
2
|
|
|
3
3
|
class ErrorBodyDto {
|
|
4
|
-
@ApiProperty({ example:
|
|
4
|
+
@ApiProperty({ example: 'BAD_REQUEST' })
|
|
5
5
|
code!: string;
|
|
6
6
|
|
|
7
|
-
@ApiProperty({ example:
|
|
7
|
+
@ApiProperty({ example: 'Validation failed.' })
|
|
8
8
|
message!: string;
|
|
9
9
|
|
|
10
10
|
@ApiPropertyOptional({ type: Object })
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import { ApiProperty, ApiPropertyOptional } from
|
|
2
|
-
import { MembershipStatus } from
|
|
3
|
-
import { RoleSummaryDto } from
|
|
4
|
-
import { ActiveUserSummaryDto } from
|
|
1
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
+
import { MembershipStatus } from '@prisma/client';
|
|
3
|
+
import { RoleSummaryDto } from './role-summary.dto';
|
|
4
|
+
import { ActiveUserSummaryDto } from './user-summary.dto';
|
|
5
5
|
|
|
6
6
|
export class MembershipResponseDto {
|
|
7
7
|
@ApiProperty({
|
|
8
|
-
example:
|
|
9
|
-
format:
|
|
8
|
+
example: '0a57fb4a-95c6-4f7e-bd5a-f96dbe0599e3',
|
|
9
|
+
format: 'uuid',
|
|
10
10
|
})
|
|
11
11
|
id!: string;
|
|
12
12
|
|
|
13
13
|
@ApiProperty({
|
|
14
|
-
example:
|
|
15
|
-
format:
|
|
14
|
+
example: '4a4f0d8a-4bd2-469f-a6a9-3e1cb6a2b456',
|
|
15
|
+
format: 'uuid',
|
|
16
16
|
})
|
|
17
17
|
userId!: string;
|
|
18
18
|
|
|
19
19
|
@ApiProperty({
|
|
20
|
-
example:
|
|
21
|
-
format:
|
|
20
|
+
example: '2c67399d-670c-4025-a5fd-1ea9a211891e',
|
|
21
|
+
format: 'uuid',
|
|
22
22
|
})
|
|
23
23
|
orgId!: string;
|
|
24
24
|
|
|
25
25
|
@ApiProperty({
|
|
26
|
-
example:
|
|
27
|
-
format:
|
|
26
|
+
example: 'f602c057-04f4-4ef8-8c84-1b7c62fbf8c5',
|
|
27
|
+
format: 'uuid',
|
|
28
28
|
})
|
|
29
29
|
roleId!: string;
|
|
30
30
|
|
|
@@ -43,9 +43,21 @@ export class MembershipResponseDto {
|
|
|
43
43
|
@ApiPropertyOptional({ type: RoleSummaryDto })
|
|
44
44
|
role?: RoleSummaryDto;
|
|
45
45
|
|
|
46
|
-
@ApiProperty({ example:
|
|
46
|
+
@ApiProperty({ example: '2026-06-01T10:30:00.000Z', format: 'date-time' })
|
|
47
47
|
createdAt!: string;
|
|
48
48
|
|
|
49
|
-
@ApiProperty({ example:
|
|
49
|
+
@ApiProperty({ example: '2026-06-01T10:30:00.000Z', format: 'date-time' })
|
|
50
50
|
updatedAt!: string;
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
export class MembershipListResponseDto {
|
|
54
|
+
@ApiProperty({ type: [MembershipResponseDto] })
|
|
55
|
+
items!: MembershipResponseDto[];
|
|
56
|
+
|
|
57
|
+
@ApiPropertyOptional({
|
|
58
|
+
example: '0a57fb4a-95c6-4f7e-bd5a-f96dbe0599e3',
|
|
59
|
+
format: 'uuid',
|
|
60
|
+
nullable: true,
|
|
61
|
+
})
|
|
62
|
+
nextCursor!: string | null;
|
|
63
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
+
import { Type } from 'class-transformer';
|
|
3
|
+
import { IsInt, IsOptional, IsUUID, Max, Min } from 'class-validator';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_PAGE_LIMIT = 50;
|
|
6
|
+
export const MAX_PAGE_LIMIT = 100;
|
|
7
|
+
|
|
8
|
+
export class PaginationQueryDto {
|
|
9
|
+
@ApiPropertyOptional({ example: 50, minimum: 1, maximum: MAX_PAGE_LIMIT })
|
|
10
|
+
@IsOptional()
|
|
11
|
+
@Type(() => Number)
|
|
12
|
+
@IsInt()
|
|
13
|
+
@Min(1)
|
|
14
|
+
@Max(MAX_PAGE_LIMIT)
|
|
15
|
+
limit?: number;
|
|
16
|
+
|
|
17
|
+
@ApiPropertyOptional({
|
|
18
|
+
example: 'df6537c4-f58b-452e-a67e-18ec528d0f0f',
|
|
19
|
+
format: 'uuid',
|
|
20
|
+
})
|
|
21
|
+
@IsOptional()
|
|
22
|
+
@IsUUID()
|
|
23
|
+
cursor?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolvePageLimit(limit: number | undefined) {
|
|
27
|
+
return Math.min(limit ?? DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function toPage<T extends { id: string }>(rows: T[], limit: number) {
|
|
31
|
+
const items = rows.slice(0, limit);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
items,
|
|
35
|
+
nextCursor: rows.length > limit ? (items[items.length - 1]?.id ?? null) : null,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { ApiProperty, ApiPropertyOptional } from
|
|
1
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
2
|
|
|
3
3
|
export class RoleSummaryDto {
|
|
4
4
|
@ApiProperty({
|
|
5
|
-
example:
|
|
6
|
-
format:
|
|
5
|
+
example: 'f602c057-04f4-4ef8-8c84-1b7c62fbf8c5',
|
|
6
|
+
format: 'uuid',
|
|
7
7
|
})
|
|
8
8
|
id!: string;
|
|
9
9
|
|
|
10
|
-
@ApiProperty({ example:
|
|
10
|
+
@ApiProperty({ example: 'Owner' })
|
|
11
11
|
name!: string;
|
|
12
12
|
|
|
13
|
-
@ApiPropertyOptional({ example:
|
|
13
|
+
@ApiPropertyOptional({ example: 'Full organisation access.', nullable: true })
|
|
14
14
|
description?: string | null;
|
|
15
15
|
|
|
16
16
|
@ApiPropertyOptional({ example: true })
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { ApiProperty, ApiPropertyOptional } from
|
|
1
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
2
|
|
|
3
3
|
export class UserSummaryDto {
|
|
4
4
|
@ApiProperty({
|
|
5
|
-
example:
|
|
6
|
-
format:
|
|
5
|
+
example: '4a4f0d8a-4bd2-469f-a6a9-3e1cb6a2b456',
|
|
6
|
+
format: 'uuid',
|
|
7
7
|
})
|
|
8
8
|
id!: string;
|
|
9
9
|
|
|
10
|
-
@ApiPropertyOptional({ example:
|
|
10
|
+
@ApiPropertyOptional({ example: 'owner@example.com', nullable: true })
|
|
11
11
|
email?: string | null;
|
|
12
12
|
|
|
13
|
-
@ApiPropertyOptional({ example:
|
|
13
|
+
@ApiPropertyOptional({ example: '+14155552671', nullable: true })
|
|
14
14
|
mobile?: string | null;
|
|
15
15
|
|
|
16
|
-
@ApiPropertyOptional({ example:
|
|
16
|
+
@ApiPropertyOptional({ example: 'Starter Owner', nullable: true })
|
|
17
17
|
displayName?: string | null;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ArgumentsHost,
|
|
3
|
-
Catch,
|
|
4
|
-
ExceptionFilter,
|
|
5
|
-
HttpException,
|
|
6
|
-
HttpStatus,
|
|
7
|
-
} from "@nestjs/common";
|
|
1
|
+
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';
|
|
8
2
|
|
|
9
3
|
type HttpResponse = {
|
|
10
4
|
status: (statusCode: number) => {
|
|
@@ -17,9 +11,7 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
|
|
17
11
|
catch(exception: unknown, host: ArgumentsHost) {
|
|
18
12
|
const response = host.switchToHttp().getResponse<HttpResponse>();
|
|
19
13
|
const status =
|
|
20
|
-
exception instanceof HttpException
|
|
21
|
-
? exception.getStatus()
|
|
22
|
-
: HttpStatus.INTERNAL_SERVER_ERROR;
|
|
14
|
+
exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
|
|
23
15
|
|
|
24
16
|
response.status(status).json({
|
|
25
17
|
error: {
|
|
@@ -32,25 +24,25 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
|
|
32
24
|
}
|
|
33
25
|
|
|
34
26
|
function errorCode(status: number) {
|
|
35
|
-
return HttpStatus[status] ??
|
|
27
|
+
return HttpStatus[status] ?? 'ERROR';
|
|
36
28
|
}
|
|
37
29
|
|
|
38
30
|
function errorMessage(exception: unknown) {
|
|
39
31
|
if (!(exception instanceof HttpException)) {
|
|
40
|
-
return
|
|
32
|
+
return 'Internal server error.';
|
|
41
33
|
}
|
|
42
34
|
|
|
43
35
|
const body = exception.getResponse();
|
|
44
|
-
if (typeof body ===
|
|
36
|
+
if (typeof body === 'string') {
|
|
45
37
|
return body;
|
|
46
38
|
}
|
|
47
39
|
|
|
48
|
-
if (isObject(body) && typeof body.message ===
|
|
40
|
+
if (isObject(body) && typeof body.message === 'string') {
|
|
49
41
|
return body.message;
|
|
50
42
|
}
|
|
51
43
|
|
|
52
44
|
if (isObject(body) && Array.isArray(body.message)) {
|
|
53
|
-
return body.message.join(
|
|
45
|
+
return body.message.join('; ');
|
|
54
46
|
}
|
|
55
47
|
|
|
56
48
|
return exception.message;
|
|
@@ -67,12 +59,10 @@ function errorDetails(exception: unknown) {
|
|
|
67
59
|
}
|
|
68
60
|
|
|
69
61
|
return Object.fromEntries(
|
|
70
|
-
Object.entries(body).filter(
|
|
71
|
-
([key]) => !["statusCode", "error", "message"].includes(key),
|
|
72
|
-
),
|
|
62
|
+
Object.entries(body).filter(([key]) => !['statusCode', 'error', 'message'].includes(key)),
|
|
73
63
|
);
|
|
74
64
|
}
|
|
75
65
|
|
|
76
66
|
function isObject(value: unknown): value is Record<string, unknown> {
|
|
77
|
-
return typeof value ===
|
|
67
|
+
return typeof value === 'object' && value !== null;
|
|
78
68
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { applyDecorators } from
|
|
1
|
+
import { applyDecorators } from '@nestjs/common';
|
|
2
2
|
import {
|
|
3
3
|
ApiBadRequestResponse,
|
|
4
4
|
ApiConflictResponse,
|
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
ApiServiceUnavailableResponse,
|
|
10
10
|
ApiTooManyRequestsResponse,
|
|
11
11
|
ApiUnauthorizedResponse,
|
|
12
|
-
} from
|
|
13
|
-
import { ErrorResponseDto } from
|
|
12
|
+
} from '@nestjs/swagger';
|
|
13
|
+
import { ErrorResponseDto } from '../dto/error-response.dto';
|
|
14
14
|
|
|
15
15
|
type ApiErrorStatus = 400 | 401 | 403 | 404 | 409 | 410 | 429 | 500 | 503;
|
|
16
16
|
|
|
@@ -27,15 +27,15 @@ const errorResponseFactories = {
|
|
|
27
27
|
} satisfies Record<ApiErrorStatus, typeof ApiBadRequestResponse>;
|
|
28
28
|
|
|
29
29
|
const descriptions: Record<ApiErrorStatus, string> = {
|
|
30
|
-
400:
|
|
31
|
-
401:
|
|
32
|
-
403:
|
|
33
|
-
404:
|
|
34
|
-
409:
|
|
35
|
-
410:
|
|
36
|
-
429:
|
|
37
|
-
500:
|
|
38
|
-
503:
|
|
30
|
+
400: 'The request body, route parameter, or query string is invalid.',
|
|
31
|
+
401: 'Authentication failed or the bearer token is missing.',
|
|
32
|
+
403: 'The authenticated user does not have access to this organisation or permission.',
|
|
33
|
+
404: 'The requested resource was not found.',
|
|
34
|
+
409: 'The request conflicts with current resource state.',
|
|
35
|
+
410: 'The resource is no longer available.',
|
|
36
|
+
429: 'Too many requests.',
|
|
37
|
+
500: 'Unexpected server error.',
|
|
38
|
+
503: 'The service is temporarily unavailable.',
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
export function ApiErrorResponses(...statuses: ApiErrorStatus[]) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { registerAs } from
|
|
2
|
-
import { getEnv } from
|
|
1
|
+
import { registerAs } from '@nestjs/config';
|
|
2
|
+
import { getEnv } from './env.validation';
|
|
3
3
|
|
|
4
|
-
export default registerAs(
|
|
4
|
+
export default registerAs('app', () => ({
|
|
5
5
|
nodeEnv: getEnv().NODE_ENV,
|
|
6
6
|
port: getEnv().PORT,
|
|
7
7
|
}));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { registerAs } from
|
|
2
|
-
import { getEnv } from
|
|
1
|
+
import { registerAs } from '@nestjs/config';
|
|
2
|
+
import { getEnv } from './env.validation';
|
|
3
3
|
|
|
4
|
-
export default registerAs(
|
|
4
|
+
export default registerAs('auth', () => {
|
|
5
5
|
const env = getEnv();
|
|
6
6
|
|
|
7
7
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { registerAs } from
|
|
2
|
-
import { getEnv } from
|
|
1
|
+
import { registerAs } from '@nestjs/config';
|
|
2
|
+
import { getEnv } from './env.validation';
|
|
3
3
|
|
|
4
|
-
export default registerAs(
|
|
4
|
+
export default registerAs('database', () => ({
|
|
5
5
|
url: getEnv().DATABASE_URL,
|
|
6
6
|
}));
|