@ftisindia/create-app 0.1.3 → 0.1.5
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/package.json +1 -1
- package/template/.env.example +6 -0
- package/template/README.md +11 -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 +8 -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 +78 -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 +24 -11
- package/template/src/modules/access-control/access-control.module.ts +11 -10
- 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/current-access-control-response.dto.ts +31 -0
- 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/current-access-control.controller.ts +40 -0
- 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 +87 -23
- package/template/src/modules/organisations/dto/create-organisation.dto.ts +6 -13
- package/template/src/modules/organisations/dto/organisation-response.dto.ts +65 -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 +31 -18
- 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/frontend-bootstrap.spec.ts +181 -0
- package/template/test/role-permission-policy.spec.ts +94 -0
- package/template/test/route-registry.validator.spec.ts +12 -0
- package/template/test/security.e2e-spec.ts +134 -2
package/package.json
CHANGED
package/template/.env.example
CHANGED
|
@@ -21,6 +21,12 @@ AUTH_FACEBOOK_ENABLED=false
|
|
|
21
21
|
AUTH_MOBILE_OTP_ENABLED=false
|
|
22
22
|
AUTH_MAGIC_LINK_ENABLED=false
|
|
23
23
|
|
|
24
|
+
CORS_ENABLED=false
|
|
25
|
+
# Comma-separated exact browser origins, for example:
|
|
26
|
+
# CORS_ORIGINS=http://localhost:5173,https://app.example.com
|
|
27
|
+
CORS_ORIGINS=
|
|
28
|
+
CORS_CREDENTIALS=false
|
|
29
|
+
|
|
24
30
|
RBAC_CACHE_TTL_SECONDS=60
|
|
25
31
|
# Path is the only supported mode in this starter phase.
|
|
26
32
|
ORG_CONTEXT_MODE=path
|
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
|
|
|
@@ -78,6 +78,7 @@ GET /auth/google
|
|
|
78
78
|
GET /auth/google/callback
|
|
79
79
|
GET /auth/me
|
|
80
80
|
POST /organisations
|
|
81
|
+
GET /organisations/mine
|
|
81
82
|
GET /organisations/:orgId/memberships/me
|
|
82
83
|
GET /organisations/:orgId/memberships
|
|
83
84
|
PATCH /organisations/:orgId/memberships/:membershipId/status
|
|
@@ -91,6 +92,7 @@ POST /organisations/:orgId/invitations/:invitationId/revoke
|
|
|
91
92
|
POST /organisations/:orgId/invitations/:invitationId/resend
|
|
92
93
|
POST /invitations/accept
|
|
93
94
|
POST /invitations/decline
|
|
95
|
+
GET /organisations/:orgId/access-control/me
|
|
94
96
|
GET /organisations/:orgId/access-control/permissions
|
|
95
97
|
GET /organisations/:orgId/access-control/route-permissions
|
|
96
98
|
GET /organisations/:orgId/access-control/roles
|
|
@@ -107,6 +109,14 @@ GET /organisations/:orgId/sample/status
|
|
|
107
109
|
POST /organisations/:orgId/sample/echo
|
|
108
110
|
```
|
|
109
111
|
|
|
112
|
+
## Frontend Bootstrap
|
|
113
|
+
|
|
114
|
+
After login, call `GET /organisations/mine` to populate the organisation picker,
|
|
115
|
+
store the selected `orgId`, then call
|
|
116
|
+
`GET /organisations/:orgId/access-control/me`. Use the returned
|
|
117
|
+
`permissionKeys` for route, menu, and button visibility in the frontend. Backend
|
|
118
|
+
guards remain the source of truth for access control.
|
|
119
|
+
|
|
110
120
|
## Architecture Rules
|
|
111
121
|
|
|
112
122
|
- JWTs contain identity only. Organisation membership, roles, and permissions are read per request.
|
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,12 @@
|
|
|
1
|
-
import { registerAs } from
|
|
2
|
-
import { getEnv } from
|
|
1
|
+
import { registerAs } from '@nestjs/config';
|
|
2
|
+
import { getEnv, parseCorsOrigins } 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
|
+
cors: {
|
|
8
|
+
enabled: getEnv().CORS_ENABLED,
|
|
9
|
+
origins: parseCorsOrigins(getEnv().CORS_ORIGINS),
|
|
10
|
+
credentials: getEnv().CORS_CREDENTIALS,
|
|
11
|
+
},
|
|
7
12
|
}));
|
|
@@ -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
|
}));
|