@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.
Files changed (114) hide show
  1. package/package.json +1 -1
  2. package/template/.env.example +6 -0
  3. package/template/README.md +11 -1
  4. package/template/_package.json +0 -2
  5. package/template/docs/API_REFERENCE.md +13 -0
  6. package/template/docs/OAUTH.md +7 -3
  7. package/template/scripts/gen-module.mjs +2 -0
  8. package/template/src/app.module.ts +16 -22
  9. package/template/src/common/dto/error-response.dto.ts +3 -3
  10. package/template/src/common/dto/membership-response.dto.ts +26 -14
  11. package/template/src/common/dto/mutation-response.dto.ts +1 -1
  12. package/template/src/common/dto/pagination-query.dto.ts +37 -0
  13. package/template/src/common/dto/role-summary.dto.ts +5 -5
  14. package/template/src/common/dto/user-summary.dto.ts +6 -6
  15. package/template/src/common/filters/http-exception.filter.ts +9 -19
  16. package/template/src/common/swagger/api-error-responses.ts +12 -12
  17. package/template/src/config/app.config.ts +8 -3
  18. package/template/src/config/auth.config.ts +3 -3
  19. package/template/src/config/database.config.ts +3 -3
  20. package/template/src/config/env.validation.ts +78 -40
  21. package/template/src/config/index.ts +5 -5
  22. package/template/src/config/rbac.config.ts +3 -3
  23. package/template/src/database/prisma/prisma-transaction.ts +1 -1
  24. package/template/src/database/prisma/prisma.module.ts +2 -2
  25. package/template/src/database/prisma/prisma.service.ts +3 -6
  26. package/template/src/main.ts +24 -11
  27. package/template/src/modules/access-control/access-control.module.ts +11 -10
  28. package/template/src/modules/access-control/application/role-permission-policy.ts +71 -0
  29. package/template/src/modules/access-control/application/route-registry.validator.ts +34 -63
  30. package/template/src/modules/access-control/application/services/ability.factory.ts +5 -9
  31. package/template/src/modules/access-control/application/services/access-control.service.ts +78 -85
  32. package/template/src/modules/access-control/application/services/permission.guard.ts +16 -21
  33. package/template/src/modules/access-control/application/services/rbac-cache.service.ts +7 -9
  34. package/template/src/modules/access-control/dto/access-control-response.dto.ts +32 -20
  35. package/template/src/modules/access-control/dto/create-role.dto.ts +6 -6
  36. package/template/src/modules/access-control/dto/current-access-control-response.dto.ts +31 -0
  37. package/template/src/modules/access-control/dto/update-role-permissions.dto.ts +3 -10
  38. package/template/src/modules/access-control/dto/update-role.dto.ts +6 -6
  39. package/template/src/modules/access-control/presentation/access-control.controller.ts +69 -74
  40. package/template/src/modules/access-control/presentation/current-access-control.controller.ts +40 -0
  41. package/template/src/modules/access-control/presentation/permissions.decorator.ts +3 -3
  42. package/template/src/modules/access-control/presentation/public.decorator.ts +2 -2
  43. package/template/src/modules/access-control/types/permission-key.ts +19 -19
  44. package/template/src/modules/access-control/types/route-permission-registry.ts +76 -76
  45. package/template/src/modules/audit/application/services/audit.service.ts +7 -7
  46. package/template/src/modules/audit/audit.module.ts +4 -4
  47. package/template/src/modules/audit/dto/audit-response.dto.ts +18 -18
  48. package/template/src/modules/audit/dto/list-audit-logs-query.dto.ts +14 -14
  49. package/template/src/modules/audit/presentation/audit.controller.ts +17 -23
  50. package/template/src/modules/auth/application/services/auth.service.ts +147 -110
  51. package/template/src/modules/auth/application/services/password.service.ts +2 -2
  52. package/template/src/modules/auth/application/services/token.service.ts +20 -21
  53. package/template/src/modules/auth/auth.module.ts +20 -47
  54. package/template/src/modules/auth/dto/auth-response.dto.ts +9 -10
  55. package/template/src/modules/auth/dto/login.dto.ts +4 -4
  56. package/template/src/modules/auth/dto/logout.dto.ts +1 -1
  57. package/template/src/modules/auth/dto/oauth-exchange.dto.ts +4 -5
  58. package/template/src/modules/auth/dto/refresh-token.dto.ts +4 -5
  59. package/template/src/modules/auth/dto/signup.dto.ts +5 -11
  60. package/template/src/modules/auth/infrastructure/passport/google-auth.guard.ts +6 -14
  61. package/template/src/modules/auth/infrastructure/passport/google-oauth-state.store.ts +98 -0
  62. package/template/src/modules/auth/infrastructure/passport/google.strategy.ts +21 -30
  63. package/template/src/modules/auth/infrastructure/passport/jwt-auth.guard.ts +3 -3
  64. package/template/src/modules/auth/infrastructure/passport/jwt.strategy.ts +11 -11
  65. package/template/src/modules/auth/presentation/auth.controller.ts +45 -45
  66. package/template/src/modules/auth/presentation/current-user.decorator.ts +3 -5
  67. package/template/src/modules/auth/presentation/google-oauth-exception.filter.ts +5 -10
  68. package/template/src/modules/health/dto/health-response.dto.ts +5 -5
  69. package/template/src/modules/health/health.module.ts +2 -2
  70. package/template/src/modules/health/presentation/health.controller.ts +13 -13
  71. package/template/src/modules/invitations/application/services/invitations.service.ts +127 -176
  72. package/template/src/modules/invitations/dto/accept-invitation.dto.ts +6 -7
  73. package/template/src/modules/invitations/dto/create-invitation.dto.ts +14 -15
  74. package/template/src/modules/invitations/dto/invitation-response.dto.ts +37 -29
  75. package/template/src/modules/invitations/dto/invitation-token.dto.ts +4 -4
  76. package/template/src/modules/invitations/invitations.module.ts +5 -5
  77. package/template/src/modules/invitations/presentation/invitations.controller.ts +61 -63
  78. package/template/src/modules/memberships/application/services/memberships.service.ts +70 -84
  79. package/template/src/modules/memberships/dto/transfer-owner.dto.ts +4 -4
  80. package/template/src/modules/memberships/dto/update-billing-contact.dto.ts +2 -2
  81. package/template/src/modules/memberships/dto/update-membership-owner.dto.ts +2 -2
  82. package/template/src/modules/memberships/dto/update-membership-role.dto.ts +4 -4
  83. package/template/src/modules/memberships/dto/update-membership-status.dto.ts +3 -3
  84. package/template/src/modules/memberships/memberships.module.ts +4 -4
  85. package/template/src/modules/memberships/presentation/memberships.controller.ts +83 -99
  86. package/template/src/modules/organisations/application/services/organisations.service.ts +87 -23
  87. package/template/src/modules/organisations/dto/create-organisation.dto.ts +6 -13
  88. package/template/src/modules/organisations/dto/organisation-response.dto.ts +65 -14
  89. package/template/src/modules/organisations/infrastructure/repositories/organisations.repository.ts +4 -7
  90. package/template/src/modules/organisations/organisations.module.ts +5 -5
  91. package/template/src/modules/organisations/presentation/organisations.controller.ts +31 -18
  92. package/template/src/modules/organisations/types/default-organisation-data.ts +3 -9
  93. package/template/src/modules/request-context/application/services/request-context.service.ts +15 -7
  94. package/template/src/modules/request-context/presentation/org-scope.guard.ts +4 -9
  95. package/template/src/modules/request-context/presentation/request-context.interceptor.ts +4 -9
  96. package/template/src/modules/request-context/presentation/request-context.middleware.ts +7 -8
  97. package/template/src/modules/request-context/request-context.module.ts +7 -7
  98. package/template/src/modules/request-context/types/request-context.ts +2 -2
  99. package/template/src/modules/sample/application/services/sample.service.ts +10 -8
  100. package/template/src/modules/sample/dto/sample-echo.dto.ts +3 -3
  101. package/template/src/modules/sample/dto/sample-response.dto.ts +12 -12
  102. package/template/src/modules/sample/presentation/sample.controller.ts +25 -42
  103. package/template/src/modules/sample/sample.module.ts +4 -4
  104. package/template/src/modules/settings/application/services/settings.service.ts +15 -27
  105. package/template/src/modules/settings/dto/setting-response.dto.ts +9 -9
  106. package/template/src/modules/settings/dto/update-setting.dto.ts +5 -5
  107. package/template/src/modules/settings/presentation/settings.controller.ts +29 -35
  108. package/template/src/modules/settings/settings.module.ts +5 -5
  109. package/template/src/modules/settings/types/setting-definitions.ts +49 -33
  110. package/template/test/auth-refresh.spec.ts +90 -0
  111. package/template/test/frontend-bootstrap.spec.ts +181 -0
  112. package/template/test/role-permission-policy.spec.ts +94 -0
  113. package/template/test/route-registry.validator.spec.ts +12 -0
  114. package/template/test/security.e2e-spec.ts +134 -2
@@ -1,30 +1,95 @@
1
- import { ConflictException, 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 { CreateOrganisationDto } from "../../dto/create-organisation.dto";
6
- import { OrganisationsRepository } from "../../infrastructure/repositories/organisations.repository";
1
+ import { ConflictException, Injectable } from '@nestjs/common';
2
+ import { MembershipStatus, OrganisationStatus, Prisma } from '@prisma/client';
3
+ import {
4
+ PaginationQueryDto,
5
+ resolvePageLimit,
6
+ toPage,
7
+ } from '../../../../common/dto/pagination-query.dto';
8
+ import { PrismaService } from '../../../../database/prisma/prisma.service';
9
+ import { RequestContextService } from '../../../request-context/application/services/request-context.service';
10
+ import { AuthenticatedUser } from '../../../auth/types/authenticated-user';
11
+ import { CreateOrganisationDto } from '../../dto/create-organisation.dto';
12
+ import { OrganisationsRepository } from '../../infrastructure/repositories/organisations.repository';
7
13
  import {
8
14
  defaultOrganisationRoles,
9
15
  defaultOrganisationSettings,
10
- } from "../../types/default-organisation-data";
16
+ } from '../../types/default-organisation-data';
11
17
 
12
18
  @Injectable()
13
19
  export class OrganisationsService {
14
20
  constructor(
15
21
  private readonly organisationsRepository: OrganisationsRepository,
16
22
  private readonly prisma: PrismaService,
23
+ private readonly requestContext: RequestContextService,
17
24
  ) {}
18
25
 
19
- async createOrganisation(
20
- currentUser: AuthenticatedUser,
21
- dto: CreateOrganisationDto,
22
- ) {
26
+ async listMine(currentUser: AuthenticatedUser, query: PaginationQueryDto) {
27
+ const limit = resolvePageLimit(query.limit);
28
+
29
+ const rows = await this.prisma.membership.findMany({
30
+ where: {
31
+ userId: currentUser.id,
32
+ status: MembershipStatus.ACTIVE,
33
+ organisation: {
34
+ status: OrganisationStatus.ACTIVE,
35
+ },
36
+ },
37
+ select: {
38
+ id: true,
39
+ roleId: true,
40
+ isOwner: true,
41
+ isBillingContact: true,
42
+ organisation: {
43
+ select: {
44
+ id: true,
45
+ name: true,
46
+ slug: true,
47
+ status: true,
48
+ },
49
+ },
50
+ role: {
51
+ select: {
52
+ id: true,
53
+ name: true,
54
+ description: true,
55
+ isSystemSeeded: true,
56
+ },
57
+ },
58
+ },
59
+ orderBy: [{ isOwner: 'desc' }, { createdAt: 'asc' }, { id: 'asc' }],
60
+ take: limit + 1,
61
+ ...(query.cursor
62
+ ? {
63
+ cursor: { id: query.cursor },
64
+ skip: 1,
65
+ }
66
+ : {}),
67
+ });
68
+
69
+ const page = toPage(rows, limit);
70
+
71
+ return {
72
+ items: page.items.map((membership) => ({
73
+ id: membership.organisation.id,
74
+ name: membership.organisation.name,
75
+ slug: membership.organisation.slug,
76
+ status: membership.organisation.status,
77
+ membershipId: membership.id,
78
+ roleId: membership.roleId,
79
+ role: membership.role,
80
+ isOwner: membership.isOwner,
81
+ isBillingContact: membership.isBillingContact,
82
+ })),
83
+ nextCursor: page.nextCursor,
84
+ };
85
+ }
86
+
87
+ async createOrganisation(currentUser: AuthenticatedUser, dto: CreateOrganisationDto) {
23
88
  const name = dto.name.trim();
24
89
  const slug = dto.slug?.trim() || (await this.createAvailableSlug(name));
25
90
 
26
91
  if (dto.slug && (await this.organisationsRepository.findBySlug(slug))) {
27
- throw new ConflictException("Organisation slug is already in use.");
92
+ throw new ConflictException('Organisation slug is already in use.');
28
93
  }
29
94
 
30
95
  try {
@@ -48,10 +113,10 @@ export class OrganisationsService {
48
113
  }),
49
114
  ),
50
115
  );
51
- const ownerRole = roles.find((role) => role.name === "Owner");
116
+ const ownerRole = roles.find((role) => role.name === 'Owner');
52
117
 
53
118
  if (!ownerRole) {
54
- throw new Error("Owner role was not seeded.");
119
+ throw new Error('Owner role was not seeded.');
55
120
  }
56
121
 
57
122
  const membership = await tx.membership.create({
@@ -80,8 +145,8 @@ export class OrganisationsService {
80
145
  data: {
81
146
  orgId: organisation.id,
82
147
  actorUserId: currentUser.id,
83
- action: "organisation.create",
84
- targetType: "Organisation",
148
+ action: 'organisation.create',
149
+ targetType: 'Organisation',
85
150
  targetId: organisation.id,
86
151
  metadata: {
87
152
  name: organisation.name,
@@ -89,6 +154,8 @@ export class OrganisationsService {
89
154
  defaultRoles: roles.map((role) => role.name),
90
155
  defaultSettings: settings.map((setting) => setting.key),
91
156
  },
157
+ ipAddress: this.requestContext.getIpAddress(),
158
+ userAgent: this.requestContext.getUserAgent(),
92
159
  },
93
160
  });
94
161
 
@@ -108,7 +175,7 @@ export class OrganisationsService {
108
175
  });
109
176
  } catch (error) {
110
177
  if (isPrismaUniqueError(error)) {
111
- throw new ConflictException("Organisation slug is already in use.");
178
+ throw new ConflictException('Organisation slug is already in use.');
112
179
  }
113
180
 
114
181
  throw error;
@@ -130,18 +197,15 @@ export class OrganisationsService {
130
197
  }
131
198
 
132
199
  function isPrismaUniqueError(error: unknown) {
133
- return (
134
- error instanceof Prisma.PrismaClientKnownRequestError &&
135
- error.code === "P2002"
136
- );
200
+ return error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002';
137
201
  }
138
202
 
139
203
  function slugify(value: string) {
140
204
  const slug = value
141
205
  .trim()
142
206
  .toLowerCase()
143
- .replace(/[^a-z0-9]+/g, "-")
144
- .replace(/^-+|-+$/g, "");
207
+ .replace(/[^a-z0-9]+/g, '-')
208
+ .replace(/^-+|-+$/g, '');
145
209
 
146
210
  return slug || `org-${Date.now()}`;
147
211
  }
@@ -1,23 +1,16 @@
1
- import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
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: "Acme Operations", minLength: 2, maxLength: 120 })
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
- "Optional URL-friendly slug. Generated from the name when omitted.",
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: "slug must contain lowercase letters, numbers, and hyphens only",
22
+ message: 'slug must contain lowercase letters, numbers, and hyphens only',
30
23
  })
31
24
  slug?: string;
32
25
  }
@@ -1,38 +1,39 @@
1
- import { ApiProperty } from "@nestjs/swagger";
2
- import { OrganisationStatus } from "@prisma/client";
3
- import { MembershipResponseDto } from "../../../common/dto/membership-response.dto";
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { OrganisationStatus } from '@prisma/client';
3
+ import { MembershipResponseDto } from '../../../common/dto/membership-response.dto';
4
+ import { RoleSummaryDto } from '../../../common/dto/role-summary.dto';
4
5
 
5
6
  export class OrganisationResponseDto {
6
7
  @ApiProperty({
7
- example: "2c67399d-670c-4025-a5fd-1ea9a211891e",
8
- format: "uuid",
8
+ example: '2c67399d-670c-4025-a5fd-1ea9a211891e',
9
+ format: 'uuid',
9
10
  })
10
11
  id!: string;
11
12
 
12
- @ApiProperty({ example: "Acme Operations" })
13
+ @ApiProperty({ example: 'Acme Operations' })
13
14
  name!: string;
14
15
 
15
- @ApiProperty({ example: "acme-operations" })
16
+ @ApiProperty({ example: 'acme-operations' })
16
17
  slug!: string;
17
18
 
18
19
  @ApiProperty({ enum: OrganisationStatus, example: OrganisationStatus.ACTIVE })
19
20
  status!: OrganisationStatus;
20
21
 
21
- @ApiProperty({ example: "2026-06-01T10:30:00.000Z", format: "date-time" })
22
+ @ApiProperty({ example: '2026-06-01T10:30:00.000Z', format: 'date-time' })
22
23
  createdAt!: string;
23
24
 
24
- @ApiProperty({ example: "2026-06-01T10:30:00.000Z", format: "date-time" })
25
+ @ApiProperty({ example: '2026-06-01T10:30:00.000Z', format: 'date-time' })
25
26
  updatedAt!: string;
26
27
  }
27
28
 
28
29
  class SeededOrganisationRoleDto {
29
30
  @ApiProperty({
30
- example: "f602c057-04f4-4ef8-8c84-1b7c62fbf8c5",
31
- format: "uuid",
31
+ example: 'f602c057-04f4-4ef8-8c84-1b7c62fbf8c5',
32
+ format: 'uuid',
32
33
  })
33
34
  id!: string;
34
35
 
35
- @ApiProperty({ example: "Owner" })
36
+ @ApiProperty({ example: 'Owner' })
36
37
  name!: string;
37
38
 
38
39
  @ApiProperty({ example: true })
@@ -40,10 +41,10 @@ class SeededOrganisationRoleDto {
40
41
  }
41
42
 
42
43
  class SeededOrganisationSettingDto {
43
- @ApiProperty({ example: "timezone" })
44
+ @ApiProperty({ example: 'timezone' })
44
45
  key!: string;
45
46
 
46
- @ApiProperty({ example: "UTC" })
47
+ @ApiProperty({ example: 'UTC' })
47
48
  value!: unknown;
48
49
  }
49
50
 
@@ -60,3 +61,53 @@ export class CreateOrganisationResponseDto {
60
61
  @ApiProperty({ type: [SeededOrganisationSettingDto] })
61
62
  settings!: SeededOrganisationSettingDto[];
62
63
  }
64
+
65
+ export class MyOrganisationResponseDto {
66
+ @ApiProperty({
67
+ example: '2c67399d-670c-4025-a5fd-1ea9a211891e',
68
+ format: 'uuid',
69
+ })
70
+ id!: string;
71
+
72
+ @ApiProperty({ example: 'Acme Operations' })
73
+ name!: string;
74
+
75
+ @ApiProperty({ example: 'acme-operations' })
76
+ slug!: string;
77
+
78
+ @ApiProperty({ enum: OrganisationStatus, example: OrganisationStatus.ACTIVE })
79
+ status!: OrganisationStatus;
80
+
81
+ @ApiProperty({
82
+ example: '0a57fb4a-95c6-4f7e-bd5a-f96dbe0599e3',
83
+ format: 'uuid',
84
+ })
85
+ membershipId!: string;
86
+
87
+ @ApiProperty({
88
+ example: 'f602c057-04f4-4ef8-8c84-1b7c62fbf8c5',
89
+ format: 'uuid',
90
+ })
91
+ roleId!: string;
92
+
93
+ @ApiProperty({ type: RoleSummaryDto })
94
+ role!: RoleSummaryDto;
95
+
96
+ @ApiProperty({ example: true })
97
+ isOwner!: boolean;
98
+
99
+ @ApiProperty({ example: true })
100
+ isBillingContact!: boolean;
101
+ }
102
+
103
+ export class MyOrganisationListResponseDto {
104
+ @ApiProperty({ type: [MyOrganisationResponseDto] })
105
+ items!: MyOrganisationResponseDto[];
106
+
107
+ @ApiProperty({
108
+ example: '2c67399d-670c-4025-a5fd-1ea9a211891e',
109
+ format: 'uuid',
110
+ nullable: true,
111
+ })
112
+ nextCursor!: string | null;
113
+ }
@@ -1,6 +1,6 @@
1
- import { Injectable } from "@nestjs/common";
2
- import { Prisma } from "@prisma/client";
3
- import { PrismaService } from "../../../../database/prisma/prisma.service";
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 "@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";
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,50 @@
1
- import { Body, Controller, Post, UseGuards } from "@nestjs/common";
1
+ import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common';
2
2
  import {
3
3
  ApiBearerAuth,
4
4
  ApiCreatedResponse,
5
+ ApiOkResponse,
5
6
  ApiOperation,
6
7
  ApiTags,
7
- } from "@nestjs/swagger";
8
- import { ApiErrorResponses } from "../../../common/swagger/api-error-responses";
9
- import { JwtAuthGuard } from "../../auth/infrastructure/passport/jwt-auth.guard";
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";
8
+ } from '@nestjs/swagger';
9
+ import { PaginationQueryDto } from '../../../common/dto/pagination-query.dto';
10
+ import { ApiErrorResponses } from '../../../common/swagger/api-error-responses';
11
+ import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
12
+ import { CurrentUser } from '../../auth/presentation/current-user.decorator';
13
+ import { AuthenticatedUser } from '../../auth/types/authenticated-user';
14
+ import { OrganisationsService } from '../application/services/organisations.service';
15
+ import { CreateOrganisationDto } from '../dto/create-organisation.dto';
16
+ import {
17
+ CreateOrganisationResponseDto,
18
+ MyOrganisationListResponseDto,
19
+ } from '../dto/organisation-response.dto';
15
20
 
16
- @ApiTags("Organisations")
21
+ @ApiTags('Organisations')
17
22
  @ApiBearerAuth()
18
- @Controller("organisations")
23
+ @Controller('organisations')
19
24
  export class OrganisationsController {
20
25
  constructor(private readonly organisationsService: OrganisationsService) {}
21
26
 
27
+ @Get('mine')
28
+ @UseGuards(JwtAuthGuard)
29
+ @ApiOperation({ summary: 'List active organisations for the current user.' })
30
+ @ApiOkResponse({
31
+ description: 'Current user organisations.',
32
+ type: MyOrganisationListResponseDto,
33
+ })
34
+ @ApiErrorResponses(400, 401)
35
+ mine(@CurrentUser() user: AuthenticatedUser, @Query() query: PaginationQueryDto) {
36
+ return this.organisationsService.listMine(user, query);
37
+ }
38
+
22
39
  @Post()
23
40
  @UseGuards(JwtAuthGuard)
24
- @ApiOperation({ summary: "Create an organisation for the current user." })
41
+ @ApiOperation({ summary: 'Create an organisation for the current user.' })
25
42
  @ApiCreatedResponse({
26
- description:
27
- "Organisation created with default roles, settings, and owner membership.",
43
+ description: 'Organisation created with default roles, settings, and owner membership.',
28
44
  type: CreateOrganisationResponseDto,
29
45
  })
30
46
  @ApiErrorResponses(400, 401, 409)
31
- create(
32
- @CurrentUser() user: AuthenticatedUser,
33
- @Body() dto: CreateOrganisationDto,
34
- ) {
47
+ create(@CurrentUser() user: AuthenticatedUser, @Body() dto: CreateOrganisationDto) {
35
48
  return this.organisationsService.createOrganisation(user, dto);
36
49
  }
37
50
  }
@@ -1,13 +1,7 @@
1
- import { Prisma } from "@prisma/client";
2
- import { settingDefinitions } from "../../settings/types/setting-definitions";
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;
@@ -1,7 +1,7 @@
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";
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 ?? "worker",
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("Active organisation context is required.");
78
+ throw new ForbiddenException('Active organisation context is required.');
71
79
  }
72
80
 
73
81
  if (activeOrgId !== orgId) {
74
82
  throw new ForbiddenException(
75
- "Request organisation context does not match the requested resource.",
83
+ 'Request organisation context does not match the requested resource.',
76
84
  );
77
85
  }
78
86
  }
@@ -1,11 +1,6 @@
1
- import {
2
- CanActivate,
3
- ExecutionContext,
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("Organisation context is required.");
14
+ throw new ForbiddenException('Organisation context is required.');
20
15
  }
21
16
 
22
17
  request.orgId = orgId;
@@ -1,12 +1,7 @@
1
- import {
2
- CallHandler,
3
- ExecutionContext,
4
- Injectable,
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 "@nestjs/common";
2
- import { randomUUID } from "crypto";
3
- import { RequestContextService } from "../application/services/request-context.service";
4
- import { RequestWithContext } from "../types/request-context";
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: "http",
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?.["user-agent"]),
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 "@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";
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 "../../access-control/types/rbac-context";
1
+ import { RbacContext } from '../../access-control/types/rbac-context';
2
2
 
3
- export type RequestSource = "http" | "worker";
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 "@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";
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: "sample.echo",
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: "Sample",
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
  }