@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
|
@@ -1,30 +1,29 @@
|
|
|
1
|
-
import { ConflictException, Injectable } from
|
|
2
|
-
import { Prisma } from
|
|
3
|
-
import { PrismaService } from
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import { ConflictException, Injectable } from '@nestjs/common';
|
|
2
|
+
import { Prisma } from '@prisma/client';
|
|
3
|
+
import { PrismaService } from '../../../../database/prisma/prisma.service';
|
|
4
|
+
import { RequestContextService } from '../../../request-context/application/services/request-context.service';
|
|
5
|
+
import { AuthenticatedUser } from '../../../auth/types/authenticated-user';
|
|
6
|
+
import { CreateOrganisationDto } from '../../dto/create-organisation.dto';
|
|
7
|
+
import { OrganisationsRepository } from '../../infrastructure/repositories/organisations.repository';
|
|
7
8
|
import {
|
|
8
9
|
defaultOrganisationRoles,
|
|
9
10
|
defaultOrganisationSettings,
|
|
10
|
-
} from
|
|
11
|
+
} from '../../types/default-organisation-data';
|
|
11
12
|
|
|
12
13
|
@Injectable()
|
|
13
14
|
export class OrganisationsService {
|
|
14
15
|
constructor(
|
|
15
16
|
private readonly organisationsRepository: OrganisationsRepository,
|
|
16
17
|
private readonly prisma: PrismaService,
|
|
18
|
+
private readonly requestContext: RequestContextService,
|
|
17
19
|
) {}
|
|
18
20
|
|
|
19
|
-
async createOrganisation(
|
|
20
|
-
currentUser: AuthenticatedUser,
|
|
21
|
-
dto: CreateOrganisationDto,
|
|
22
|
-
) {
|
|
21
|
+
async createOrganisation(currentUser: AuthenticatedUser, dto: CreateOrganisationDto) {
|
|
23
22
|
const name = dto.name.trim();
|
|
24
23
|
const slug = dto.slug?.trim() || (await this.createAvailableSlug(name));
|
|
25
24
|
|
|
26
25
|
if (dto.slug && (await this.organisationsRepository.findBySlug(slug))) {
|
|
27
|
-
throw new ConflictException(
|
|
26
|
+
throw new ConflictException('Organisation slug is already in use.');
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
try {
|
|
@@ -48,10 +47,10 @@ export class OrganisationsService {
|
|
|
48
47
|
}),
|
|
49
48
|
),
|
|
50
49
|
);
|
|
51
|
-
const ownerRole = roles.find((role) => role.name ===
|
|
50
|
+
const ownerRole = roles.find((role) => role.name === 'Owner');
|
|
52
51
|
|
|
53
52
|
if (!ownerRole) {
|
|
54
|
-
throw new Error(
|
|
53
|
+
throw new Error('Owner role was not seeded.');
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
const membership = await tx.membership.create({
|
|
@@ -80,8 +79,8 @@ export class OrganisationsService {
|
|
|
80
79
|
data: {
|
|
81
80
|
orgId: organisation.id,
|
|
82
81
|
actorUserId: currentUser.id,
|
|
83
|
-
action:
|
|
84
|
-
targetType:
|
|
82
|
+
action: 'organisation.create',
|
|
83
|
+
targetType: 'Organisation',
|
|
85
84
|
targetId: organisation.id,
|
|
86
85
|
metadata: {
|
|
87
86
|
name: organisation.name,
|
|
@@ -89,6 +88,8 @@ export class OrganisationsService {
|
|
|
89
88
|
defaultRoles: roles.map((role) => role.name),
|
|
90
89
|
defaultSettings: settings.map((setting) => setting.key),
|
|
91
90
|
},
|
|
91
|
+
ipAddress: this.requestContext.getIpAddress(),
|
|
92
|
+
userAgent: this.requestContext.getUserAgent(),
|
|
92
93
|
},
|
|
93
94
|
});
|
|
94
95
|
|
|
@@ -108,7 +109,7 @@ export class OrganisationsService {
|
|
|
108
109
|
});
|
|
109
110
|
} catch (error) {
|
|
110
111
|
if (isPrismaUniqueError(error)) {
|
|
111
|
-
throw new ConflictException(
|
|
112
|
+
throw new ConflictException('Organisation slug is already in use.');
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
throw error;
|
|
@@ -130,18 +131,15 @@ export class OrganisationsService {
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
function isPrismaUniqueError(error: unknown) {
|
|
133
|
-
return
|
|
134
|
-
error instanceof Prisma.PrismaClientKnownRequestError &&
|
|
135
|
-
error.code === "P2002"
|
|
136
|
-
);
|
|
134
|
+
return error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002';
|
|
137
135
|
}
|
|
138
136
|
|
|
139
137
|
function slugify(value: string) {
|
|
140
138
|
const slug = value
|
|
141
139
|
.trim()
|
|
142
140
|
.toLowerCase()
|
|
143
|
-
.replace(/[^a-z0-9]+/g,
|
|
144
|
-
.replace(/^-+|-+$/g,
|
|
141
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
142
|
+
.replace(/^-+|-+$/g, '');
|
|
145
143
|
|
|
146
144
|
return slug || `org-${Date.now()}`;
|
|
147
145
|
}
|
|
@@ -1,23 +1,16 @@
|
|
|
1
|
-
import { ApiProperty, ApiPropertyOptional } from
|
|
2
|
-
import {
|
|
3
|
-
IsOptional,
|
|
4
|
-
IsString,
|
|
5
|
-
Matches,
|
|
6
|
-
MaxLength,
|
|
7
|
-
MinLength,
|
|
8
|
-
} from "class-validator";
|
|
1
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
+
import { IsOptional, IsString, Matches, MaxLength, MinLength } from 'class-validator';
|
|
9
3
|
|
|
10
4
|
export class CreateOrganisationDto {
|
|
11
|
-
@ApiProperty({ example:
|
|
5
|
+
@ApiProperty({ example: 'Acme Operations', minLength: 2, maxLength: 120 })
|
|
12
6
|
@IsString()
|
|
13
7
|
@MinLength(2)
|
|
14
8
|
@MaxLength(120)
|
|
15
9
|
name!: string;
|
|
16
10
|
|
|
17
11
|
@ApiPropertyOptional({
|
|
18
|
-
description:
|
|
19
|
-
|
|
20
|
-
example: "acme-operations",
|
|
12
|
+
description: 'Optional URL-friendly slug. Generated from the name when omitted.',
|
|
13
|
+
example: 'acme-operations',
|
|
21
14
|
minLength: 2,
|
|
22
15
|
maxLength: 80,
|
|
23
16
|
})
|
|
@@ -26,7 +19,7 @@ export class CreateOrganisationDto {
|
|
|
26
19
|
@MinLength(2)
|
|
27
20
|
@MaxLength(80)
|
|
28
21
|
@Matches(/^[a-z0-9][a-z0-9-]*$/, {
|
|
29
|
-
message:
|
|
22
|
+
message: 'slug must contain lowercase letters, numbers, and hyphens only',
|
|
30
23
|
})
|
|
31
24
|
slug?: string;
|
|
32
25
|
}
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import { ApiProperty } from
|
|
2
|
-
import { OrganisationStatus } from
|
|
3
|
-
import { MembershipResponseDto } from
|
|
1
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
2
|
+
import { OrganisationStatus } from '@prisma/client';
|
|
3
|
+
import { MembershipResponseDto } from '../../../common/dto/membership-response.dto';
|
|
4
4
|
|
|
5
5
|
export class OrganisationResponseDto {
|
|
6
6
|
@ApiProperty({
|
|
7
|
-
example:
|
|
8
|
-
format:
|
|
7
|
+
example: '2c67399d-670c-4025-a5fd-1ea9a211891e',
|
|
8
|
+
format: 'uuid',
|
|
9
9
|
})
|
|
10
10
|
id!: string;
|
|
11
11
|
|
|
12
|
-
@ApiProperty({ example:
|
|
12
|
+
@ApiProperty({ example: 'Acme Operations' })
|
|
13
13
|
name!: string;
|
|
14
14
|
|
|
15
|
-
@ApiProperty({ example:
|
|
15
|
+
@ApiProperty({ example: 'acme-operations' })
|
|
16
16
|
slug!: string;
|
|
17
17
|
|
|
18
18
|
@ApiProperty({ enum: OrganisationStatus, example: OrganisationStatus.ACTIVE })
|
|
19
19
|
status!: OrganisationStatus;
|
|
20
20
|
|
|
21
|
-
@ApiProperty({ example:
|
|
21
|
+
@ApiProperty({ example: '2026-06-01T10:30:00.000Z', format: 'date-time' })
|
|
22
22
|
createdAt!: string;
|
|
23
23
|
|
|
24
|
-
@ApiProperty({ example:
|
|
24
|
+
@ApiProperty({ example: '2026-06-01T10:30:00.000Z', format: 'date-time' })
|
|
25
25
|
updatedAt!: string;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
class SeededOrganisationRoleDto {
|
|
29
29
|
@ApiProperty({
|
|
30
|
-
example:
|
|
31
|
-
format:
|
|
30
|
+
example: 'f602c057-04f4-4ef8-8c84-1b7c62fbf8c5',
|
|
31
|
+
format: 'uuid',
|
|
32
32
|
})
|
|
33
33
|
id!: string;
|
|
34
34
|
|
|
35
|
-
@ApiProperty({ example:
|
|
35
|
+
@ApiProperty({ example: 'Owner' })
|
|
36
36
|
name!: string;
|
|
37
37
|
|
|
38
38
|
@ApiProperty({ example: true })
|
|
@@ -40,10 +40,10 @@ class SeededOrganisationRoleDto {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
class SeededOrganisationSettingDto {
|
|
43
|
-
@ApiProperty({ example:
|
|
43
|
+
@ApiProperty({ example: 'timezone' })
|
|
44
44
|
key!: string;
|
|
45
45
|
|
|
46
|
-
@ApiProperty({ example:
|
|
46
|
+
@ApiProperty({ example: 'UTC' })
|
|
47
47
|
value!: unknown;
|
|
48
48
|
}
|
|
49
49
|
|
package/template/src/modules/organisations/infrastructure/repositories/organisations.repository.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Injectable } from
|
|
2
|
-
import { Prisma } from
|
|
3
|
-
import { PrismaService } from
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { Prisma } from '@prisma/client';
|
|
3
|
+
import { PrismaService } from '../../../../database/prisma/prisma.service';
|
|
4
4
|
|
|
5
5
|
type PrismaClientLike = Prisma.TransactionClient | PrismaService;
|
|
6
6
|
|
|
@@ -15,10 +15,7 @@ export class OrganisationsRepository {
|
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
create(
|
|
19
|
-
data: Prisma.OrganisationCreateInput,
|
|
20
|
-
client: PrismaClientLike = this.prisma,
|
|
21
|
-
) {
|
|
18
|
+
create(data: Prisma.OrganisationCreateInput, client: PrismaClientLike = this.prisma) {
|
|
22
19
|
return client.organisation.create({ data });
|
|
23
20
|
}
|
|
24
21
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Module } from
|
|
2
|
-
import { AuthModule } from
|
|
3
|
-
import { OrganisationsService } from
|
|
4
|
-
import { OrganisationsRepository } from
|
|
5
|
-
import { OrganisationsController } from
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { AuthModule } from '../auth/auth.module';
|
|
3
|
+
import { OrganisationsService } from './application/services/organisations.service';
|
|
4
|
+
import { OrganisationsRepository } from './infrastructure/repositories/organisations.repository';
|
|
5
|
+
import { OrganisationsController } from './presentation/organisations.controller';
|
|
6
6
|
|
|
7
7
|
@Module({
|
|
8
8
|
imports: [AuthModule],
|
|
@@ -1,37 +1,28 @@
|
|
|
1
|
-
import { Body, Controller, Post, UseGuards } from
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { CurrentUser } from "../../auth/presentation/current-user.decorator";
|
|
11
|
-
import { AuthenticatedUser } from "../../auth/types/authenticated-user";
|
|
12
|
-
import { OrganisationsService } from "../application/services/organisations.service";
|
|
13
|
-
import { CreateOrganisationDto } from "../dto/create-organisation.dto";
|
|
14
|
-
import { CreateOrganisationResponseDto } from "../dto/organisation-response.dto";
|
|
1
|
+
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
|
|
2
|
+
import { ApiBearerAuth, ApiCreatedResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
|
3
|
+
import { ApiErrorResponses } from '../../../common/swagger/api-error-responses';
|
|
4
|
+
import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
|
|
5
|
+
import { CurrentUser } from '../../auth/presentation/current-user.decorator';
|
|
6
|
+
import { AuthenticatedUser } from '../../auth/types/authenticated-user';
|
|
7
|
+
import { OrganisationsService } from '../application/services/organisations.service';
|
|
8
|
+
import { CreateOrganisationDto } from '../dto/create-organisation.dto';
|
|
9
|
+
import { CreateOrganisationResponseDto } from '../dto/organisation-response.dto';
|
|
15
10
|
|
|
16
|
-
@ApiTags(
|
|
11
|
+
@ApiTags('Organisations')
|
|
17
12
|
@ApiBearerAuth()
|
|
18
|
-
@Controller(
|
|
13
|
+
@Controller('organisations')
|
|
19
14
|
export class OrganisationsController {
|
|
20
15
|
constructor(private readonly organisationsService: OrganisationsService) {}
|
|
21
16
|
|
|
22
17
|
@Post()
|
|
23
18
|
@UseGuards(JwtAuthGuard)
|
|
24
|
-
@ApiOperation({ summary:
|
|
19
|
+
@ApiOperation({ summary: 'Create an organisation for the current user.' })
|
|
25
20
|
@ApiCreatedResponse({
|
|
26
|
-
description:
|
|
27
|
-
"Organisation created with default roles, settings, and owner membership.",
|
|
21
|
+
description: 'Organisation created with default roles, settings, and owner membership.',
|
|
28
22
|
type: CreateOrganisationResponseDto,
|
|
29
23
|
})
|
|
30
24
|
@ApiErrorResponses(400, 401, 409)
|
|
31
|
-
create(
|
|
32
|
-
@CurrentUser() user: AuthenticatedUser,
|
|
33
|
-
@Body() dto: CreateOrganisationDto,
|
|
34
|
-
) {
|
|
25
|
+
create(@CurrentUser() user: AuthenticatedUser, @Body() dto: CreateOrganisationDto) {
|
|
35
26
|
return this.organisationsService.createOrganisation(user, dto);
|
|
36
27
|
}
|
|
37
28
|
}
|
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import { Prisma } from
|
|
2
|
-
import { settingDefinitions } from
|
|
1
|
+
import { Prisma } from '@prisma/client';
|
|
2
|
+
import { settingDefinitions } from '../../settings/types/setting-definitions';
|
|
3
3
|
|
|
4
|
-
export const defaultOrganisationRoles = [
|
|
5
|
-
"Owner",
|
|
6
|
-
"Admin",
|
|
7
|
-
"Manager",
|
|
8
|
-
"Staff",
|
|
9
|
-
"Viewer",
|
|
10
|
-
] as const;
|
|
4
|
+
export const defaultOrganisationRoles = ['Owner', 'Admin', 'Manager', 'Staff', 'Viewer'] as const;
|
|
11
5
|
|
|
12
6
|
export const defaultOrganisationSettings: Array<{
|
|
13
7
|
key: string;
|
package/template/src/modules/request-context/application/services/request-context.service.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from
|
|
2
|
-
import { randomUUID } from
|
|
3
|
-
import { ForbiddenException, Injectable } from
|
|
4
|
-
import { RequestContext } from
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { ForbiddenException, Injectable } from '@nestjs/common';
|
|
4
|
+
import { RequestContext } from '../../types/request-context';
|
|
5
5
|
|
|
6
6
|
@Injectable()
|
|
7
7
|
export class RequestContextService {
|
|
@@ -11,7 +11,7 @@ export class RequestContextService {
|
|
|
11
11
|
return this.storage.run(
|
|
12
12
|
{
|
|
13
13
|
requestId: context.requestId ?? randomUUID(),
|
|
14
|
-
source: context.source ??
|
|
14
|
+
source: context.source ?? 'worker',
|
|
15
15
|
startedAt: context.startedAt ?? new Date(),
|
|
16
16
|
...context,
|
|
17
17
|
},
|
|
@@ -51,6 +51,14 @@ export class RequestContextService {
|
|
|
51
51
|
return this.get()?.rbac?.roleId;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
getIpAddress() {
|
|
55
|
+
return this.get()?.ipAddress;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getUserAgent() {
|
|
59
|
+
return this.get()?.userAgent;
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
getPermissions() {
|
|
55
63
|
return this.get()?.rbac?.permissionKeys ?? [];
|
|
56
64
|
}
|
|
@@ -67,12 +75,12 @@ export class RequestContextService {
|
|
|
67
75
|
assertOrgScope(orgId: string) {
|
|
68
76
|
const activeOrgId = this.getOrgId();
|
|
69
77
|
if (!activeOrgId) {
|
|
70
|
-
throw new ForbiddenException(
|
|
78
|
+
throw new ForbiddenException('Active organisation context is required.');
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
if (activeOrgId !== orgId) {
|
|
74
82
|
throw new ForbiddenException(
|
|
75
|
-
|
|
83
|
+
'Request organisation context does not match the requested resource.',
|
|
76
84
|
);
|
|
77
85
|
}
|
|
78
86
|
}
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
ForbiddenException,
|
|
5
|
-
Injectable,
|
|
6
|
-
} from "@nestjs/common";
|
|
7
|
-
import { RequestContextService } from "../application/services/request-context.service";
|
|
8
|
-
import { RequestWithContext } from "../types/request-context";
|
|
1
|
+
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
|
|
2
|
+
import { RequestContextService } from '../application/services/request-context.service';
|
|
3
|
+
import { RequestWithContext } from '../types/request-context';
|
|
9
4
|
|
|
10
5
|
@Injectable()
|
|
11
6
|
export class OrgScopeGuard implements CanActivate {
|
|
@@ -16,7 +11,7 @@ export class OrgScopeGuard implements CanActivate {
|
|
|
16
11
|
const orgId = request.params?.orgId;
|
|
17
12
|
|
|
18
13
|
if (!orgId) {
|
|
19
|
-
throw new ForbiddenException(
|
|
14
|
+
throw new ForbiddenException('Organisation context is required.');
|
|
20
15
|
}
|
|
21
16
|
|
|
22
17
|
request.orgId = orgId;
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
NestInterceptor,
|
|
6
|
-
} from "@nestjs/common";
|
|
7
|
-
import { Observable } from "rxjs";
|
|
8
|
-
import { RequestContextService } from "../application/services/request-context.service";
|
|
9
|
-
import { RequestWithContext } from "../types/request-context";
|
|
1
|
+
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { RequestContextService } from '../application/services/request-context.service';
|
|
4
|
+
import { RequestWithContext } from '../types/request-context';
|
|
10
5
|
|
|
11
6
|
@Injectable()
|
|
12
7
|
export class RequestContextInterceptor implements NestInterceptor {
|
|
@@ -1,25 +1,24 @@
|
|
|
1
|
-
import { Injectable, NestMiddleware } from
|
|
2
|
-
import { randomUUID } from
|
|
3
|
-
import { RequestContextService } from
|
|
4
|
-
import { RequestWithContext } from
|
|
1
|
+
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { RequestContextService } from '../application/services/request-context.service';
|
|
4
|
+
import { RequestWithContext } from '../types/request-context';
|
|
5
5
|
|
|
6
6
|
@Injectable()
|
|
7
7
|
export class RequestContextMiddleware implements NestMiddleware {
|
|
8
8
|
constructor(private readonly requestContext: RequestContextService) {}
|
|
9
9
|
|
|
10
10
|
use(request: RequestWithContext, _response: unknown, next: () => void) {
|
|
11
|
-
const requestId =
|
|
12
|
-
headerAsString(request.headers?.["x-request-id"]) || randomUUID();
|
|
11
|
+
const requestId = headerAsString(request.headers?.['x-request-id']) || randomUUID();
|
|
13
12
|
|
|
14
13
|
this.requestContext.run(
|
|
15
14
|
{
|
|
16
15
|
requestId,
|
|
17
|
-
source:
|
|
16
|
+
source: 'http',
|
|
18
17
|
startedAt: new Date(),
|
|
19
18
|
method: request.method,
|
|
20
19
|
path: request.originalUrl ?? request.url,
|
|
21
20
|
ipAddress: request.ip,
|
|
22
|
-
userAgent: headerAsString(request.headers?.[
|
|
21
|
+
userAgent: headerAsString(request.headers?.['user-agent']),
|
|
23
22
|
},
|
|
24
23
|
next,
|
|
25
24
|
);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Global, MiddlewareConsumer, Module, NestModule } from
|
|
2
|
-
import { APP_INTERCEPTOR } from
|
|
3
|
-
import { RequestContextService } from
|
|
4
|
-
import { OrgScopeGuard } from
|
|
5
|
-
import { RequestContextInterceptor } from
|
|
6
|
-
import { RequestContextMiddleware } from
|
|
1
|
+
import { Global, MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
|
2
|
+
import { APP_INTERCEPTOR } from '@nestjs/core';
|
|
3
|
+
import { RequestContextService } from './application/services/request-context.service';
|
|
4
|
+
import { OrgScopeGuard } from './presentation/org-scope.guard';
|
|
5
|
+
import { RequestContextInterceptor } from './presentation/request-context.interceptor';
|
|
6
|
+
import { RequestContextMiddleware } from './presentation/request-context.middleware';
|
|
7
7
|
|
|
8
8
|
@Global()
|
|
9
9
|
@Module({
|
|
@@ -20,6 +20,6 @@ import { RequestContextMiddleware } from "./presentation/request-context.middlew
|
|
|
20
20
|
})
|
|
21
21
|
export class RequestContextModule implements NestModule {
|
|
22
22
|
configure(consumer: MiddlewareConsumer) {
|
|
23
|
-
consumer.apply(RequestContextMiddleware).forRoutes(
|
|
23
|
+
consumer.apply(RequestContextMiddleware).forRoutes('*');
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { RbacContext } from
|
|
1
|
+
import { RbacContext } from '../../access-control/types/rbac-context';
|
|
2
2
|
|
|
3
|
-
export type RequestSource =
|
|
3
|
+
export type RequestSource = 'http' | 'worker';
|
|
4
4
|
|
|
5
5
|
export type RequestContext = {
|
|
6
6
|
requestId: string;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Injectable } from
|
|
2
|
-
import { Prisma } from
|
|
3
|
-
import { PrismaService } from
|
|
4
|
-
import { AuthenticatedUser } from
|
|
5
|
-
import { RequestContextService } from
|
|
6
|
-
import { SampleEchoDto } from
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { Prisma } from '@prisma/client';
|
|
3
|
+
import { PrismaService } from '../../../../database/prisma/prisma.service';
|
|
4
|
+
import { AuthenticatedUser } from '../../../auth/types/authenticated-user';
|
|
5
|
+
import { RequestContextService } from '../../../request-context/application/services/request-context.service';
|
|
6
|
+
import { SampleEchoDto } from '../../dto/sample-echo.dto';
|
|
7
7
|
|
|
8
8
|
@Injectable()
|
|
9
9
|
export class SampleService {
|
|
@@ -30,7 +30,7 @@ export class SampleService {
|
|
|
30
30
|
await this.writeAudit({
|
|
31
31
|
orgId,
|
|
32
32
|
actorUserId: user.id,
|
|
33
|
-
action:
|
|
33
|
+
action: 'sample.echo',
|
|
34
34
|
targetId: orgId,
|
|
35
35
|
metadata: {
|
|
36
36
|
messageLength: message.length,
|
|
@@ -58,9 +58,11 @@ export class SampleService {
|
|
|
58
58
|
orgId: data.orgId,
|
|
59
59
|
actorUserId: data.actorUserId,
|
|
60
60
|
action: data.action,
|
|
61
|
-
targetType:
|
|
61
|
+
targetType: 'Sample',
|
|
62
62
|
targetId: data.targetId,
|
|
63
63
|
metadata: data.metadata,
|
|
64
|
+
ipAddress: this.requestContext.getIpAddress(),
|
|
65
|
+
userAgent: this.requestContext.getUserAgent(),
|
|
64
66
|
},
|
|
65
67
|
});
|
|
66
68
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ApiProperty } from
|
|
2
|
-
import { IsString, MaxLength, MinLength } from
|
|
1
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
2
|
+
import { IsString, MaxLength, MinLength } from 'class-validator';
|
|
3
3
|
|
|
4
4
|
export class SampleEchoDto {
|
|
5
|
-
@ApiProperty({ example:
|
|
5
|
+
@ApiProperty({ example: 'Hello from the sample module.' })
|
|
6
6
|
@IsString()
|
|
7
7
|
@MinLength(1)
|
|
8
8
|
@MaxLength(200)
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import { ApiProperty } from
|
|
1
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
2
2
|
|
|
3
3
|
export class SampleStatusResponseDto {
|
|
4
4
|
@ApiProperty({ example: true })
|
|
5
5
|
ok!: boolean;
|
|
6
6
|
|
|
7
7
|
@ApiProperty({
|
|
8
|
-
example:
|
|
9
|
-
format:
|
|
8
|
+
example: '2c67399d-670c-4025-a5fd-1ea9a211891e',
|
|
9
|
+
format: 'uuid',
|
|
10
10
|
})
|
|
11
11
|
orgId!: string;
|
|
12
12
|
|
|
13
13
|
@ApiProperty({
|
|
14
|
-
example:
|
|
15
|
-
format:
|
|
14
|
+
example: '2c67399d-670c-4025-a5fd-1ea9a211891e',
|
|
15
|
+
format: 'uuid',
|
|
16
16
|
})
|
|
17
17
|
contextOrgId!: string;
|
|
18
18
|
|
|
19
|
-
@ApiProperty({ example:
|
|
19
|
+
@ApiProperty({ example: 'req_01HZX3J8TBMJAEK42S7XK4V7C8' })
|
|
20
20
|
requestId!: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export class SampleEchoResponseDto {
|
|
24
24
|
@ApiProperty({
|
|
25
|
-
example:
|
|
26
|
-
format:
|
|
25
|
+
example: '2c67399d-670c-4025-a5fd-1ea9a211891e',
|
|
26
|
+
format: 'uuid',
|
|
27
27
|
})
|
|
28
28
|
orgId!: string;
|
|
29
29
|
|
|
30
30
|
@ApiProperty({
|
|
31
|
-
example:
|
|
32
|
-
format:
|
|
31
|
+
example: '2c67399d-670c-4025-a5fd-1ea9a211891e',
|
|
32
|
+
format: 'uuid',
|
|
33
33
|
})
|
|
34
34
|
contextOrgId!: string;
|
|
35
35
|
|
|
36
|
-
@ApiProperty({ example:
|
|
36
|
+
@ApiProperty({ example: 'Hello from the sample module.' })
|
|
37
37
|
message!: string;
|
|
38
38
|
|
|
39
|
-
@ApiProperty({ example:
|
|
39
|
+
@ApiProperty({ example: 'req_01HZX3J8TBMJAEK42S7XK4V7C8' })
|
|
40
40
|
requestId!: string;
|
|
41
41
|
}
|