@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.
Files changed (109) hide show
  1. package/README.md +65 -0
  2. package/package.json +1 -1
  3. package/template/README.md +65 -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 +3 -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 +58 -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 +11 -11
  27. package/template/src/modules/access-control/access-control.module.ts +9 -9
  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/update-role-permissions.dto.ts +3 -10
  37. package/template/src/modules/access-control/dto/update-role.dto.ts +6 -6
  38. package/template/src/modules/access-control/presentation/access-control.controller.ts +69 -74
  39. package/template/src/modules/access-control/presentation/permissions.decorator.ts +3 -3
  40. package/template/src/modules/access-control/presentation/public.decorator.ts +2 -2
  41. package/template/src/modules/access-control/types/permission-key.ts +19 -19
  42. package/template/src/modules/access-control/types/route-permission-registry.ts +76 -76
  43. package/template/src/modules/audit/application/services/audit.service.ts +7 -7
  44. package/template/src/modules/audit/audit.module.ts +4 -4
  45. package/template/src/modules/audit/dto/audit-response.dto.ts +18 -18
  46. package/template/src/modules/audit/dto/list-audit-logs-query.dto.ts +14 -14
  47. package/template/src/modules/audit/presentation/audit.controller.ts +17 -23
  48. package/template/src/modules/auth/application/services/auth.service.ts +147 -110
  49. package/template/src/modules/auth/application/services/password.service.ts +2 -2
  50. package/template/src/modules/auth/application/services/token.service.ts +20 -21
  51. package/template/src/modules/auth/auth.module.ts +20 -47
  52. package/template/src/modules/auth/dto/auth-response.dto.ts +9 -10
  53. package/template/src/modules/auth/dto/login.dto.ts +4 -4
  54. package/template/src/modules/auth/dto/logout.dto.ts +1 -1
  55. package/template/src/modules/auth/dto/oauth-exchange.dto.ts +4 -5
  56. package/template/src/modules/auth/dto/refresh-token.dto.ts +4 -5
  57. package/template/src/modules/auth/dto/signup.dto.ts +5 -11
  58. package/template/src/modules/auth/infrastructure/passport/google-auth.guard.ts +6 -14
  59. package/template/src/modules/auth/infrastructure/passport/google-oauth-state.store.ts +98 -0
  60. package/template/src/modules/auth/infrastructure/passport/google.strategy.ts +21 -30
  61. package/template/src/modules/auth/infrastructure/passport/jwt-auth.guard.ts +3 -3
  62. package/template/src/modules/auth/infrastructure/passport/jwt.strategy.ts +11 -11
  63. package/template/src/modules/auth/presentation/auth.controller.ts +45 -45
  64. package/template/src/modules/auth/presentation/current-user.decorator.ts +3 -5
  65. package/template/src/modules/auth/presentation/google-oauth-exception.filter.ts +5 -10
  66. package/template/src/modules/health/dto/health-response.dto.ts +5 -5
  67. package/template/src/modules/health/health.module.ts +2 -2
  68. package/template/src/modules/health/presentation/health.controller.ts +13 -13
  69. package/template/src/modules/invitations/application/services/invitations.service.ts +127 -176
  70. package/template/src/modules/invitations/dto/accept-invitation.dto.ts +6 -7
  71. package/template/src/modules/invitations/dto/create-invitation.dto.ts +14 -15
  72. package/template/src/modules/invitations/dto/invitation-response.dto.ts +37 -29
  73. package/template/src/modules/invitations/dto/invitation-token.dto.ts +4 -4
  74. package/template/src/modules/invitations/invitations.module.ts +5 -5
  75. package/template/src/modules/invitations/presentation/invitations.controller.ts +61 -63
  76. package/template/src/modules/memberships/application/services/memberships.service.ts +70 -84
  77. package/template/src/modules/memberships/dto/transfer-owner.dto.ts +4 -4
  78. package/template/src/modules/memberships/dto/update-billing-contact.dto.ts +2 -2
  79. package/template/src/modules/memberships/dto/update-membership-owner.dto.ts +2 -2
  80. package/template/src/modules/memberships/dto/update-membership-role.dto.ts +4 -4
  81. package/template/src/modules/memberships/dto/update-membership-status.dto.ts +3 -3
  82. package/template/src/modules/memberships/memberships.module.ts +4 -4
  83. package/template/src/modules/memberships/presentation/memberships.controller.ts +83 -99
  84. package/template/src/modules/organisations/application/services/organisations.service.ts +21 -23
  85. package/template/src/modules/organisations/dto/create-organisation.dto.ts +6 -13
  86. package/template/src/modules/organisations/dto/organisation-response.dto.ts +14 -14
  87. package/template/src/modules/organisations/infrastructure/repositories/organisations.repository.ts +4 -7
  88. package/template/src/modules/organisations/organisations.module.ts +5 -5
  89. package/template/src/modules/organisations/presentation/organisations.controller.ts +14 -23
  90. package/template/src/modules/organisations/types/default-organisation-data.ts +3 -9
  91. package/template/src/modules/request-context/application/services/request-context.service.ts +15 -7
  92. package/template/src/modules/request-context/presentation/org-scope.guard.ts +4 -9
  93. package/template/src/modules/request-context/presentation/request-context.interceptor.ts +4 -9
  94. package/template/src/modules/request-context/presentation/request-context.middleware.ts +7 -8
  95. package/template/src/modules/request-context/request-context.module.ts +7 -7
  96. package/template/src/modules/request-context/types/request-context.ts +2 -2
  97. package/template/src/modules/sample/application/services/sample.service.ts +10 -8
  98. package/template/src/modules/sample/dto/sample-echo.dto.ts +3 -3
  99. package/template/src/modules/sample/dto/sample-response.dto.ts +12 -12
  100. package/template/src/modules/sample/presentation/sample.controller.ts +25 -42
  101. package/template/src/modules/sample/sample.module.ts +4 -4
  102. package/template/src/modules/settings/application/services/settings.service.ts +15 -27
  103. package/template/src/modules/settings/dto/setting-response.dto.ts +9 -9
  104. package/template/src/modules/settings/dto/update-setting.dto.ts +5 -5
  105. package/template/src/modules/settings/presentation/settings.controller.ts +29 -35
  106. package/template/src/modules/settings/settings.module.ts +5 -5
  107. package/template/src/modules/settings/types/setting-definitions.ts +49 -33
  108. package/template/test/auth-refresh.spec.ts +90 -0
  109. package/template/test/role-permission-policy.spec.ts +94 -0
@@ -3,12 +3,18 @@ import {
3
3
  ForbiddenException,
4
4
  Injectable,
5
5
  NotFoundException,
6
- } from "@nestjs/common";
7
- import { MembershipStatus, Prisma } from "@prisma/client";
8
- import { PrismaService } from "../../../../database/prisma/prisma.service";
9
- import { RbacCacheService } from "../../../access-control/application/services/rbac-cache.service";
10
- import { AuthenticatedUser } from "../../../auth/types/authenticated-user";
11
- import { RequestContextService } from "../../../request-context/application/services/request-context.service";
6
+ } from '@nestjs/common';
7
+ import { MembershipStatus, Prisma } from '@prisma/client';
8
+ import {
9
+ PaginationQueryDto,
10
+ resolvePageLimit,
11
+ toPage,
12
+ } from '../../../../common/dto/pagination-query.dto';
13
+ import { PrismaService } from '../../../../database/prisma/prisma.service';
14
+ import { assertRoleWithinActorPermissions } from '../../../access-control/application/role-permission-policy';
15
+ import { RbacCacheService } from '../../../access-control/application/services/rbac-cache.service';
16
+ import { AuthenticatedUser } from '../../../auth/types/authenticated-user';
17
+ import { RequestContextService } from '../../../request-context/application/services/request-context.service';
12
18
 
13
19
  const membershipDetailInclude = {
14
20
  user: {
@@ -47,14 +53,24 @@ export class MembershipsService {
47
53
  return this.assertActiveMembership(currentUser.id, orgId);
48
54
  }
49
55
 
50
- async listMemberships(currentUser: AuthenticatedUser, orgId: string) {
56
+ async listMemberships(currentUser: AuthenticatedUser, orgId: string, query: PaginationQueryDto) {
51
57
  await this.assertActiveMembership(currentUser.id, orgId);
58
+ const limit = resolvePageLimit(query.limit);
52
59
 
53
- return this.prisma.membership.findMany({
60
+ const rows = await this.prisma.membership.findMany({
54
61
  where: { orgId },
55
62
  include: membershipDetailInclude,
56
- orderBy: [{ isOwner: "desc" }, { createdAt: "asc" }],
63
+ orderBy: [{ isOwner: 'desc' }, { createdAt: 'asc' }],
64
+ take: limit + 1,
65
+ ...(query.cursor
66
+ ? {
67
+ cursor: { id: query.cursor },
68
+ skip: 1,
69
+ }
70
+ : {}),
57
71
  });
72
+
73
+ return toPage(rows, limit);
58
74
  }
59
75
 
60
76
  async updateStatus(
@@ -87,7 +103,7 @@ export class MembershipsService {
87
103
  await this.writeAudit(tx, {
88
104
  orgId,
89
105
  actorUserId: currentUser.id,
90
- action: "membership.status.update",
106
+ action: 'membership.status.update',
91
107
  targetId: target.id,
92
108
  metadata: {
93
109
  previousStatus: target.status,
@@ -102,9 +118,7 @@ export class MembershipsService {
102
118
  };
103
119
  });
104
120
 
105
- this.rbacCache.invalidateMany(
106
- result.affectedUserIds.map((userId) => ({ userId, orgId })),
107
- );
121
+ this.rbacCache.invalidateMany(result.affectedUserIds.map((userId) => ({ userId, orgId })));
108
122
  return result.updated;
109
123
  }
110
124
 
@@ -127,13 +141,28 @@ export class MembershipsService {
127
141
  orgId,
128
142
  },
129
143
  },
130
- select: { id: true, name: true },
144
+ select: {
145
+ id: true,
146
+ name: true,
147
+ permissions: {
148
+ select: { permission: { select: { key: true } } },
149
+ },
150
+ },
131
151
  });
132
152
 
133
153
  if (!role) {
134
- throw new NotFoundException("Role was not found in this organisation.");
154
+ throw new NotFoundException('Role was not found in this organisation.');
135
155
  }
136
156
 
157
+ // Prevent privilege self-elevation: a non-owner actor may only assign a
158
+ // role whose permissions are a subset of their own effective permissions.
159
+ const actorContext = await this.rbacCache.getContext(currentUser.id, orgId);
160
+ assertRoleWithinActorPermissions({
161
+ actorIsOwner: actorContext.isOwner,
162
+ actorPermissionKeys: actorContext.permissionKeys,
163
+ rolePermissionKeys: role.permissions.map((rolePermission) => rolePermission.permission.key),
164
+ });
165
+
137
166
  const updated = await tx.membership.update({
138
167
  where: { id: target.id },
139
168
  data: { roleId },
@@ -143,7 +172,7 @@ export class MembershipsService {
143
172
  await this.writeAudit(tx, {
144
173
  orgId,
145
174
  actorUserId: currentUser.id,
146
- action: "membership.role.update",
175
+ action: 'membership.role.update',
147
176
  targetId: target.id,
148
177
  metadata: {
149
178
  previousRoleId: target.roleId,
@@ -159,9 +188,7 @@ export class MembershipsService {
159
188
  };
160
189
  });
161
190
 
162
- this.rbacCache.invalidateMany(
163
- result.affectedUserIds.map((userId) => ({ userId, orgId })),
164
- );
191
+ this.rbacCache.invalidateMany(result.affectedUserIds.map((userId) => ({ userId, orgId })));
165
192
  return result.updated;
166
193
  }
167
194
 
@@ -178,16 +205,10 @@ export class MembershipsService {
178
205
  this.assertMembershipCanBeModified(target);
179
206
 
180
207
  if (isOwner && target.status !== MembershipStatus.ACTIVE) {
181
- throw new ConflictException(
182
- "Only active memberships can be made owners.",
183
- );
208
+ throw new ConflictException('Only active memberships can be made owners.');
184
209
  }
185
210
 
186
- if (
187
- !isOwner &&
188
- target.isOwner &&
189
- target.status === MembershipStatus.ACTIVE
190
- ) {
211
+ if (!isOwner && target.isOwner && target.status === MembershipStatus.ACTIVE) {
191
212
  await this.ensureOtherActiveOwner(tx, orgId, target.id);
192
213
  }
193
214
 
@@ -200,7 +221,7 @@ export class MembershipsService {
200
221
  await this.writeAudit(tx, {
201
222
  orgId,
202
223
  actorUserId: currentUser.id,
203
- action: "membership.owner.update",
224
+ action: 'membership.owner.update',
204
225
  targetId: target.id,
205
226
  metadata: {
206
227
  previousIsOwner: target.isOwner,
@@ -215,9 +236,7 @@ export class MembershipsService {
215
236
  };
216
237
  });
217
238
 
218
- this.rbacCache.invalidateMany(
219
- result.affectedUserIds.map((userId) => ({ userId, orgId })),
220
- );
239
+ this.rbacCache.invalidateMany(result.affectedUserIds.map((userId) => ({ userId, orgId })));
221
240
  return result.updated;
222
241
  }
223
242
 
@@ -242,7 +261,7 @@ export class MembershipsService {
242
261
  await this.writeAudit(tx, {
243
262
  orgId,
244
263
  actorUserId: currentUser.id,
245
- action: "membership.billing_contact.update",
264
+ action: 'membership.billing_contact.update',
246
265
  targetId: target.id,
247
266
  metadata: {
248
267
  previousIsBillingContact: target.isBillingContact,
@@ -257,29 +276,17 @@ export class MembershipsService {
257
276
  };
258
277
  });
259
278
 
260
- this.rbacCache.invalidateMany(
261
- result.affectedUserIds.map((userId) => ({ userId, orgId })),
262
- );
279
+ this.rbacCache.invalidateMany(result.affectedUserIds.map((userId) => ({ userId, orgId })));
263
280
  return result.updated;
264
281
  }
265
282
 
266
- async transferOwner(
267
- currentUser: AuthenticatedUser,
268
- orgId: string,
269
- toMembershipId: string,
270
- ) {
283
+ async transferOwner(currentUser: AuthenticatedUser, orgId: string, toMembershipId: string) {
271
284
  const result = await this.prisma.$transaction(async (tx) => {
272
- const actorMembership = await this.assertActiveOwner(
273
- currentUser.id,
274
- orgId,
275
- tx,
276
- );
285
+ const actorMembership = await this.assertActiveOwner(currentUser.id, orgId, tx);
277
286
  const target = await this.findMembershipInOrg(tx, orgId, toMembershipId);
278
287
 
279
288
  if (target.status !== MembershipStatus.ACTIVE) {
280
- throw new ConflictException(
281
- "Ownership can only be transferred to an active membership.",
282
- );
289
+ throw new ConflictException('Ownership can only be transferred to an active membership.');
283
290
  }
284
291
 
285
292
  if (actorMembership.id === target.id) {
@@ -302,7 +309,7 @@ export class MembershipsService {
302
309
  await this.writeAudit(tx, {
303
310
  orgId,
304
311
  actorUserId: currentUser.id,
305
- action: "membership.owner.transfer",
312
+ action: 'membership.owner.transfer',
306
313
  targetId: target.id,
307
314
  metadata: {
308
315
  fromMembershipId: actorMembership.id,
@@ -322,17 +329,11 @@ export class MembershipsService {
322
329
  };
323
330
  });
324
331
 
325
- this.rbacCache.invalidateMany(
326
- result.affectedUserIds.map((userId) => ({ userId, orgId })),
327
- );
332
+ this.rbacCache.invalidateMany(result.affectedUserIds.map((userId) => ({ userId, orgId })));
328
333
  return result.updated;
329
334
  }
330
335
 
331
- async assertActiveMembership(
332
- userId: string,
333
- orgId: string,
334
- client: PrismaClient = this.prisma,
335
- ) {
336
+ async assertActiveMembership(userId: string, orgId: string, client: PrismaClient = this.prisma) {
336
337
  this.requestContext.assertOrgScope(orgId);
337
338
 
338
339
  const membership = await client.membership.findUnique({
@@ -346,33 +347,23 @@ export class MembershipsService {
346
347
  });
347
348
 
348
349
  if (!membership || membership.status !== MembershipStatus.ACTIVE) {
349
- throw new ForbiddenException(
350
- "Active organisation membership is required.",
351
- );
350
+ throw new ForbiddenException('Active organisation membership is required.');
352
351
  }
353
352
 
354
353
  return membership;
355
354
  }
356
355
 
357
- async assertActiveOwner(
358
- userId: string,
359
- orgId: string,
360
- client: PrismaClient = this.prisma,
361
- ) {
356
+ async assertActiveOwner(userId: string, orgId: string, client: PrismaClient = this.prisma) {
362
357
  const membership = await this.assertActiveMembership(userId, orgId, client);
363
358
 
364
359
  if (!membership.isOwner) {
365
- throw new ForbiddenException("Organisation owner access is required.");
360
+ throw new ForbiddenException('Organisation owner access is required.');
366
361
  }
367
362
 
368
363
  return membership;
369
364
  }
370
365
 
371
- private async findMembershipInOrg(
372
- client: PrismaClient,
373
- orgId: string,
374
- membershipId: string,
375
- ) {
366
+ private async findMembershipInOrg(client: PrismaClient, orgId: string, membershipId: string) {
376
367
  const membership = await client.membership.findFirst({
377
368
  where: {
378
369
  id: membershipId,
@@ -382,9 +373,7 @@ export class MembershipsService {
382
373
  });
383
374
 
384
375
  if (!membership) {
385
- throw new NotFoundException(
386
- "Membership was not found in this organisation.",
387
- );
376
+ throw new NotFoundException('Membership was not found in this organisation.');
388
377
  }
389
378
 
390
379
  return membership;
@@ -394,19 +383,14 @@ export class MembershipsService {
394
383
  membership: MembershipDetail,
395
384
  nextStatus: MembershipStatus,
396
385
  ) {
397
- if (
398
- membership.status === MembershipStatus.REVOKED &&
399
- nextStatus !== MembershipStatus.REVOKED
400
- ) {
401
- throw new ConflictException(
402
- "Revoked memberships cannot be reactivated or reused.",
403
- );
386
+ if (membership.status === MembershipStatus.REVOKED && nextStatus !== MembershipStatus.REVOKED) {
387
+ throw new ConflictException('Revoked memberships cannot be reactivated or reused.');
404
388
  }
405
389
  }
406
390
 
407
391
  private assertMembershipCanBeModified(membership: MembershipDetail) {
408
392
  if (membership.status === MembershipStatus.REVOKED) {
409
- throw new ConflictException("Revoked memberships cannot be modified.");
393
+ throw new ConflictException('Revoked memberships cannot be modified.');
410
394
  }
411
395
  }
412
396
 
@@ -426,7 +410,7 @@ export class MembershipsService {
426
410
 
427
411
  if (!activeOwners.some((owner) => owner.id !== targetMembershipId)) {
428
412
  throw new ConflictException(
429
- "The last active owner cannot be removed, suspended, or demoted.",
413
+ 'The last active owner cannot be removed, suspended, or demoted.',
430
414
  );
431
415
  }
432
416
  }
@@ -446,9 +430,11 @@ export class MembershipsService {
446
430
  orgId: data.orgId,
447
431
  actorUserId: data.actorUserId,
448
432
  action: data.action,
449
- targetType: "Membership",
433
+ targetType: 'Membership',
450
434
  targetId: data.targetId,
451
435
  metadata: data.metadata,
436
+ ipAddress: this.requestContext.getIpAddress(),
437
+ userAgent: this.requestContext.getUserAgent(),
452
438
  },
453
439
  });
454
440
  }
@@ -1,10 +1,10 @@
1
- import { ApiProperty } from "@nestjs/swagger";
2
- import { IsUUID } from "class-validator";
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { IsUUID } from 'class-validator';
3
3
 
4
4
  export class TransferOwnerDto {
5
5
  @ApiProperty({
6
- example: "0a57fb4a-95c6-4f7e-bd5a-f96dbe0599e3",
7
- format: "uuid",
6
+ example: '0a57fb4a-95c6-4f7e-bd5a-f96dbe0599e3',
7
+ format: 'uuid',
8
8
  })
9
9
  @IsUUID()
10
10
  toMembershipId!: string;
@@ -1,5 +1,5 @@
1
- import { ApiProperty } from "@nestjs/swagger";
2
- import { IsBoolean } from "class-validator";
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { IsBoolean } from 'class-validator';
3
3
 
4
4
  export class UpdateBillingContactDto {
5
5
  @ApiProperty({ example: true })
@@ -1,5 +1,5 @@
1
- import { ApiProperty } from "@nestjs/swagger";
2
- import { IsBoolean } from "class-validator";
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { IsBoolean } from 'class-validator';
3
3
 
4
4
  export class UpdateMembershipOwnerDto {
5
5
  @ApiProperty({ example: true })
@@ -1,10 +1,10 @@
1
- import { ApiProperty } from "@nestjs/swagger";
2
- import { IsUUID } from "class-validator";
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { IsUUID } from 'class-validator';
3
3
 
4
4
  export class UpdateMembershipRoleDto {
5
5
  @ApiProperty({
6
- example: "f602c057-04f4-4ef8-8c84-1b7c62fbf8c5",
7
- format: "uuid",
6
+ example: 'f602c057-04f4-4ef8-8c84-1b7c62fbf8c5',
7
+ format: 'uuid',
8
8
  })
9
9
  @IsUUID()
10
10
  roleId!: string;
@@ -1,6 +1,6 @@
1
- import { ApiProperty } from "@nestjs/swagger";
2
- import { MembershipStatus } from "@prisma/client";
3
- import { IsEnum } from "class-validator";
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { MembershipStatus } from '@prisma/client';
3
+ import { IsEnum } from 'class-validator';
4
4
 
5
5
  export class UpdateMembershipStatusDto {
6
6
  @ApiProperty({ enum: MembershipStatus, example: MembershipStatus.SUSPENDED })
@@ -1,7 +1,7 @@
1
- import { Module } from "@nestjs/common";
2
- import { AuthModule } from "../auth/auth.module";
3
- import { MembershipsService } from "./application/services/memberships.service";
4
- import { MembershipsController } from "./presentation/memberships.controller";
1
+ import { Module } from '@nestjs/common';
2
+ import { AuthModule } from '../auth/auth.module';
3
+ import { MembershipsService } from './application/services/memberships.service';
4
+ import { MembershipsController } from './presentation/memberships.controller';
5
5
 
6
6
  @Module({
7
7
  imports: [AuthModule],
@@ -6,159 +6,147 @@ import {
6
6
  Param,
7
7
  Patch,
8
8
  Post,
9
+ Query,
9
10
  UseGuards,
10
- } from "@nestjs/common";
11
+ } from '@nestjs/common';
12
+ import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
11
13
  import {
12
- ApiBearerAuth,
13
- ApiOkResponse,
14
- ApiOperation,
15
- ApiParam,
16
- ApiTags,
17
- } from "@nestjs/swagger";
18
- import { MembershipResponseDto } from "../../../common/dto/membership-response.dto";
19
- import { ApiProtectedErrorResponses } from "../../../common/swagger/api-error-responses";
20
- import { PermissionGuard } from "../../access-control/application/services/permission.guard";
21
- import { RequirePermissions } from "../../access-control/presentation/permissions.decorator";
22
- import { JwtAuthGuard } from "../../auth/infrastructure/passport/jwt-auth.guard";
23
- import { CurrentUser } from "../../auth/presentation/current-user.decorator";
24
- import { AuthenticatedUser } from "../../auth/types/authenticated-user";
25
- import { OrgScopeGuard } from "../../request-context/presentation/org-scope.guard";
26
- import { MembershipsService } from "../application/services/memberships.service";
27
- import { TransferOwnerDto } from "../dto/transfer-owner.dto";
28
- import { UpdateBillingContactDto } from "../dto/update-billing-contact.dto";
29
- import { UpdateMembershipOwnerDto } from "../dto/update-membership-owner.dto";
30
- import { UpdateMembershipRoleDto } from "../dto/update-membership-role.dto";
31
- import { UpdateMembershipStatusDto } from "../dto/update-membership-status.dto";
14
+ MembershipListResponseDto,
15
+ MembershipResponseDto,
16
+ } from '../../../common/dto/membership-response.dto';
17
+ import { PaginationQueryDto } from '../../../common/dto/pagination-query.dto';
18
+ import { ApiProtectedErrorResponses } from '../../../common/swagger/api-error-responses';
19
+ import { PermissionGuard } from '../../access-control/application/services/permission.guard';
20
+ import { RequirePermissions } from '../../access-control/presentation/permissions.decorator';
21
+ import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
22
+ import { CurrentUser } from '../../auth/presentation/current-user.decorator';
23
+ import { AuthenticatedUser } from '../../auth/types/authenticated-user';
24
+ import { OrgScopeGuard } from '../../request-context/presentation/org-scope.guard';
25
+ import { MembershipsService } from '../application/services/memberships.service';
26
+ import { TransferOwnerDto } from '../dto/transfer-owner.dto';
27
+ import { UpdateBillingContactDto } from '../dto/update-billing-contact.dto';
28
+ import { UpdateMembershipOwnerDto } from '../dto/update-membership-owner.dto';
29
+ import { UpdateMembershipRoleDto } from '../dto/update-membership-role.dto';
30
+ import { UpdateMembershipStatusDto } from '../dto/update-membership-status.dto';
32
31
 
33
- @ApiTags("Memberships")
32
+ @ApiTags('Memberships')
34
33
  @ApiBearerAuth()
35
- @ApiParam({ name: "orgId", description: "Organisation ID.", format: "uuid" })
34
+ @ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
36
35
  @ApiProtectedErrorResponses(404, 409)
37
- @Controller("organisations/:orgId/memberships")
36
+ @Controller('organisations/:orgId/memberships')
38
37
  @UseGuards(JwtAuthGuard, OrgScopeGuard, PermissionGuard)
39
38
  export class MembershipsController {
40
39
  constructor(private readonly membershipsService: MembershipsService) {}
41
40
 
42
- @Get("me")
43
- @RequirePermissions("memberships.read")
41
+ @Get('me')
42
+ @RequirePermissions('memberships.read')
44
43
  @ApiOperation({
45
- summary: "Return the current user membership in the organisation.",
44
+ summary: 'Return the current user membership in the organisation.',
46
45
  })
47
46
  @ApiOkResponse({
48
- description: "Current membership.",
47
+ description: 'Current membership.',
49
48
  type: MembershipResponseDto,
50
49
  })
51
- me(@CurrentUser() user: AuthenticatedUser, @Param("orgId") orgId: string) {
50
+ me(@CurrentUser() user: AuthenticatedUser, @Param('orgId') orgId: string) {
52
51
  return this.membershipsService.getCurrentMembership(user, orgId);
53
52
  }
54
53
 
55
54
  @Get()
56
- @RequirePermissions("memberships.read")
57
- @ApiOperation({ summary: "List organisation memberships." })
55
+ @RequirePermissions('memberships.read')
56
+ @ApiOperation({ summary: 'List organisation memberships.' })
58
57
  @ApiOkResponse({
59
- description: "Organisation memberships.",
60
- type: [MembershipResponseDto],
58
+ description: 'Organisation memberships.',
59
+ type: MembershipListResponseDto,
61
60
  })
62
- list(@CurrentUser() user: AuthenticatedUser, @Param("orgId") orgId: string) {
63
- return this.membershipsService.listMemberships(user, orgId);
61
+ list(
62
+ @CurrentUser() user: AuthenticatedUser,
63
+ @Param('orgId') orgId: string,
64
+ @Query() query: PaginationQueryDto,
65
+ ) {
66
+ return this.membershipsService.listMemberships(user, orgId, query);
64
67
  }
65
68
 
66
- @Patch(":membershipId/status")
67
- @RequirePermissions("memberships.revoke")
69
+ @Patch(':membershipId/status')
70
+ @RequirePermissions('memberships.revoke')
68
71
  @ApiParam({
69
- name: "membershipId",
70
- description: "Membership ID.",
71
- format: "uuid",
72
+ name: 'membershipId',
73
+ description: 'Membership ID.',
74
+ format: 'uuid',
72
75
  })
73
- @ApiOperation({ summary: "Update membership status." })
76
+ @ApiOperation({ summary: 'Update membership status.' })
74
77
  @ApiOkResponse({
75
- description: "Updated membership.",
78
+ description: 'Updated membership.',
76
79
  type: MembershipResponseDto,
77
80
  })
78
81
  updateStatus(
79
82
  @CurrentUser() user: AuthenticatedUser,
80
- @Param("orgId") orgId: string,
81
- @Param("membershipId") membershipId: string,
83
+ @Param('orgId') orgId: string,
84
+ @Param('membershipId') membershipId: string,
82
85
  @Body() dto: UpdateMembershipStatusDto,
83
86
  ) {
84
- return this.membershipsService.updateStatus(
85
- user,
86
- orgId,
87
- membershipId,
88
- dto.status,
89
- );
87
+ return this.membershipsService.updateStatus(user, orgId, membershipId, dto.status);
90
88
  }
91
89
 
92
- @Patch(":membershipId/role")
93
- @RequirePermissions("roles.manage")
90
+ @Patch(':membershipId/role')
91
+ @RequirePermissions('roles.manage')
94
92
  @ApiParam({
95
- name: "membershipId",
96
- description: "Membership ID.",
97
- format: "uuid",
93
+ name: 'membershipId',
94
+ description: 'Membership ID.',
95
+ format: 'uuid',
98
96
  })
99
- @ApiOperation({ summary: "Assign a role to a membership." })
97
+ @ApiOperation({ summary: 'Assign a role to a membership.' })
100
98
  @ApiOkResponse({
101
- description: "Updated membership.",
99
+ description: 'Updated membership.',
102
100
  type: MembershipResponseDto,
103
101
  })
104
102
  updateRole(
105
103
  @CurrentUser() user: AuthenticatedUser,
106
- @Param("orgId") orgId: string,
107
- @Param("membershipId") membershipId: string,
104
+ @Param('orgId') orgId: string,
105
+ @Param('membershipId') membershipId: string,
108
106
  @Body() dto: UpdateMembershipRoleDto,
109
107
  ) {
110
- return this.membershipsService.assignRole(
111
- user,
112
- orgId,
113
- membershipId,
114
- dto.roleId,
115
- );
108
+ return this.membershipsService.assignRole(user, orgId, membershipId, dto.roleId);
116
109
  }
117
110
 
118
- @Patch(":membershipId/owner")
119
- @RequirePermissions("roles.manage")
111
+ @Patch(':membershipId/owner')
112
+ @RequirePermissions('roles.manage')
120
113
  @ApiParam({
121
- name: "membershipId",
122
- description: "Membership ID.",
123
- format: "uuid",
114
+ name: 'membershipId',
115
+ description: 'Membership ID.',
116
+ format: 'uuid',
124
117
  })
125
- @ApiOperation({ summary: "Grant or remove owner status on a membership." })
118
+ @ApiOperation({ summary: 'Grant or remove owner status on a membership.' })
126
119
  @ApiOkResponse({
127
- description: "Updated membership.",
120
+ description: 'Updated membership.',
128
121
  type: MembershipResponseDto,
129
122
  })
130
123
  updateOwner(
131
124
  @CurrentUser() user: AuthenticatedUser,
132
- @Param("orgId") orgId: string,
133
- @Param("membershipId") membershipId: string,
125
+ @Param('orgId') orgId: string,
126
+ @Param('membershipId') membershipId: string,
134
127
  @Body() dto: UpdateMembershipOwnerDto,
135
128
  ) {
136
- return this.membershipsService.updateOwner(
137
- user,
138
- orgId,
139
- membershipId,
140
- dto.isOwner,
141
- );
129
+ return this.membershipsService.updateOwner(user, orgId, membershipId, dto.isOwner);
142
130
  }
143
131
 
144
- @Patch(":membershipId/billing-contact")
145
- @RequirePermissions("billing.manage")
132
+ @Patch(':membershipId/billing-contact')
133
+ @RequirePermissions('billing.manage')
146
134
  @ApiParam({
147
- name: "membershipId",
148
- description: "Membership ID.",
149
- format: "uuid",
135
+ name: 'membershipId',
136
+ description: 'Membership ID.',
137
+ format: 'uuid',
150
138
  })
151
139
  @ApiOperation({
152
- summary: "Grant or remove billing contact status on a membership.",
140
+ summary: 'Grant or remove billing contact status on a membership.',
153
141
  })
154
142
  @ApiOkResponse({
155
- description: "Updated membership.",
143
+ description: 'Updated membership.',
156
144
  type: MembershipResponseDto,
157
145
  })
158
146
  updateBillingContact(
159
147
  @CurrentUser() user: AuthenticatedUser,
160
- @Param("orgId") orgId: string,
161
- @Param("membershipId") membershipId: string,
148
+ @Param('orgId') orgId: string,
149
+ @Param('membershipId') membershipId: string,
162
150
  @Body() dto: UpdateBillingContactDto,
163
151
  ) {
164
152
  return this.membershipsService.updateBillingContact(
@@ -169,25 +157,21 @@ export class MembershipsController {
169
157
  );
170
158
  }
171
159
 
172
- @Post("transfer-owner")
160
+ @Post('transfer-owner')
173
161
  @HttpCode(200)
174
- @RequirePermissions("roles.manage")
162
+ @RequirePermissions('roles.manage')
175
163
  @ApiOperation({
176
- summary: "Transfer organisation ownership to another active membership.",
164
+ summary: 'Transfer organisation ownership to another active membership.',
177
165
  })
178
166
  @ApiOkResponse({
179
- description: "New owner membership.",
167
+ description: 'New owner membership.',
180
168
  type: MembershipResponseDto,
181
169
  })
182
170
  transferOwner(
183
171
  @CurrentUser() user: AuthenticatedUser,
184
- @Param("orgId") orgId: string,
172
+ @Param('orgId') orgId: string,
185
173
  @Body() dto: TransferOwnerDto,
186
174
  ) {
187
- return this.membershipsService.transferOwner(
188
- user,
189
- orgId,
190
- dto.toMembershipId,
191
- );
175
+ return this.membershipsService.transferOwner(user, orgId, dto.toMembershipId);
192
176
  }
193
177
  }