@ftisindia/create-app 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/.env.example +6 -0
- package/template/README.md +11 -1
- package/template/_package.json +0 -2
- package/template/docs/API_REFERENCE.md +13 -0
- package/template/docs/OAUTH.md +7 -3
- package/template/scripts/gen-module.mjs +2 -0
- package/template/src/app.module.ts +16 -22
- package/template/src/common/dto/error-response.dto.ts +3 -3
- package/template/src/common/dto/membership-response.dto.ts +26 -14
- package/template/src/common/dto/mutation-response.dto.ts +1 -1
- package/template/src/common/dto/pagination-query.dto.ts +37 -0
- package/template/src/common/dto/role-summary.dto.ts +5 -5
- package/template/src/common/dto/user-summary.dto.ts +6 -6
- package/template/src/common/filters/http-exception.filter.ts +9 -19
- package/template/src/common/swagger/api-error-responses.ts +12 -12
- package/template/src/config/app.config.ts +8 -3
- package/template/src/config/auth.config.ts +3 -3
- package/template/src/config/database.config.ts +3 -3
- package/template/src/config/env.validation.ts +78 -40
- package/template/src/config/index.ts +5 -5
- package/template/src/config/rbac.config.ts +3 -3
- package/template/src/database/prisma/prisma-transaction.ts +1 -1
- package/template/src/database/prisma/prisma.module.ts +2 -2
- package/template/src/database/prisma/prisma.service.ts +3 -6
- package/template/src/main.ts +24 -11
- package/template/src/modules/access-control/access-control.module.ts +11 -10
- package/template/src/modules/access-control/application/role-permission-policy.ts +71 -0
- package/template/src/modules/access-control/application/route-registry.validator.ts +34 -63
- package/template/src/modules/access-control/application/services/ability.factory.ts +5 -9
- package/template/src/modules/access-control/application/services/access-control.service.ts +78 -85
- package/template/src/modules/access-control/application/services/permission.guard.ts +16 -21
- package/template/src/modules/access-control/application/services/rbac-cache.service.ts +7 -9
- package/template/src/modules/access-control/dto/access-control-response.dto.ts +32 -20
- package/template/src/modules/access-control/dto/create-role.dto.ts +6 -6
- package/template/src/modules/access-control/dto/current-access-control-response.dto.ts +31 -0
- package/template/src/modules/access-control/dto/update-role-permissions.dto.ts +3 -10
- package/template/src/modules/access-control/dto/update-role.dto.ts +6 -6
- package/template/src/modules/access-control/presentation/access-control.controller.ts +69 -74
- package/template/src/modules/access-control/presentation/current-access-control.controller.ts +40 -0
- package/template/src/modules/access-control/presentation/permissions.decorator.ts +3 -3
- package/template/src/modules/access-control/presentation/public.decorator.ts +2 -2
- package/template/src/modules/access-control/types/permission-key.ts +19 -19
- package/template/src/modules/access-control/types/route-permission-registry.ts +76 -76
- package/template/src/modules/audit/application/services/audit.service.ts +7 -7
- package/template/src/modules/audit/audit.module.ts +4 -4
- package/template/src/modules/audit/dto/audit-response.dto.ts +18 -18
- package/template/src/modules/audit/dto/list-audit-logs-query.dto.ts +14 -14
- package/template/src/modules/audit/presentation/audit.controller.ts +17 -23
- package/template/src/modules/auth/application/services/auth.service.ts +147 -110
- package/template/src/modules/auth/application/services/password.service.ts +2 -2
- package/template/src/modules/auth/application/services/token.service.ts +20 -21
- package/template/src/modules/auth/auth.module.ts +20 -47
- package/template/src/modules/auth/dto/auth-response.dto.ts +9 -10
- package/template/src/modules/auth/dto/login.dto.ts +4 -4
- package/template/src/modules/auth/dto/logout.dto.ts +1 -1
- package/template/src/modules/auth/dto/oauth-exchange.dto.ts +4 -5
- package/template/src/modules/auth/dto/refresh-token.dto.ts +4 -5
- package/template/src/modules/auth/dto/signup.dto.ts +5 -11
- package/template/src/modules/auth/infrastructure/passport/google-auth.guard.ts +6 -14
- package/template/src/modules/auth/infrastructure/passport/google-oauth-state.store.ts +98 -0
- package/template/src/modules/auth/infrastructure/passport/google.strategy.ts +21 -30
- package/template/src/modules/auth/infrastructure/passport/jwt-auth.guard.ts +3 -3
- package/template/src/modules/auth/infrastructure/passport/jwt.strategy.ts +11 -11
- package/template/src/modules/auth/presentation/auth.controller.ts +45 -45
- package/template/src/modules/auth/presentation/current-user.decorator.ts +3 -5
- package/template/src/modules/auth/presentation/google-oauth-exception.filter.ts +5 -10
- package/template/src/modules/health/dto/health-response.dto.ts +5 -5
- package/template/src/modules/health/health.module.ts +2 -2
- package/template/src/modules/health/presentation/health.controller.ts +13 -13
- package/template/src/modules/invitations/application/services/invitations.service.ts +127 -176
- package/template/src/modules/invitations/dto/accept-invitation.dto.ts +6 -7
- package/template/src/modules/invitations/dto/create-invitation.dto.ts +14 -15
- package/template/src/modules/invitations/dto/invitation-response.dto.ts +37 -29
- package/template/src/modules/invitations/dto/invitation-token.dto.ts +4 -4
- package/template/src/modules/invitations/invitations.module.ts +5 -5
- package/template/src/modules/invitations/presentation/invitations.controller.ts +61 -63
- package/template/src/modules/memberships/application/services/memberships.service.ts +70 -84
- package/template/src/modules/memberships/dto/transfer-owner.dto.ts +4 -4
- package/template/src/modules/memberships/dto/update-billing-contact.dto.ts +2 -2
- package/template/src/modules/memberships/dto/update-membership-owner.dto.ts +2 -2
- package/template/src/modules/memberships/dto/update-membership-role.dto.ts +4 -4
- package/template/src/modules/memberships/dto/update-membership-status.dto.ts +3 -3
- package/template/src/modules/memberships/memberships.module.ts +4 -4
- package/template/src/modules/memberships/presentation/memberships.controller.ts +83 -99
- package/template/src/modules/organisations/application/services/organisations.service.ts +87 -23
- package/template/src/modules/organisations/dto/create-organisation.dto.ts +6 -13
- package/template/src/modules/organisations/dto/organisation-response.dto.ts +65 -14
- package/template/src/modules/organisations/infrastructure/repositories/organisations.repository.ts +4 -7
- package/template/src/modules/organisations/organisations.module.ts +5 -5
- package/template/src/modules/organisations/presentation/organisations.controller.ts +31 -18
- package/template/src/modules/organisations/types/default-organisation-data.ts +3 -9
- package/template/src/modules/request-context/application/services/request-context.service.ts +15 -7
- package/template/src/modules/request-context/presentation/org-scope.guard.ts +4 -9
- package/template/src/modules/request-context/presentation/request-context.interceptor.ts +4 -9
- package/template/src/modules/request-context/presentation/request-context.middleware.ts +7 -8
- package/template/src/modules/request-context/request-context.module.ts +7 -7
- package/template/src/modules/request-context/types/request-context.ts +2 -2
- package/template/src/modules/sample/application/services/sample.service.ts +10 -8
- package/template/src/modules/sample/dto/sample-echo.dto.ts +3 -3
- package/template/src/modules/sample/dto/sample-response.dto.ts +12 -12
- package/template/src/modules/sample/presentation/sample.controller.ts +25 -42
- package/template/src/modules/sample/sample.module.ts +4 -4
- package/template/src/modules/settings/application/services/settings.service.ts +15 -27
- package/template/src/modules/settings/dto/setting-response.dto.ts +9 -9
- package/template/src/modules/settings/dto/update-setting.dto.ts +5 -5
- package/template/src/modules/settings/presentation/settings.controller.ts +29 -35
- package/template/src/modules/settings/settings.module.ts +5 -5
- package/template/src/modules/settings/types/setting-definitions.ts +49 -33
- package/template/test/auth-refresh.spec.ts +90 -0
- package/template/test/frontend-bootstrap.spec.ts +181 -0
- package/template/test/role-permission-policy.spec.ts +94 -0
- package/template/test/route-registry.validator.spec.ts +12 -0
- package/template/test/security.e2e-spec.ts +134 -2
|
@@ -3,12 +3,18 @@ import {
|
|
|
3
3
|
ForbiddenException,
|
|
4
4
|
Injectable,
|
|
5
5
|
NotFoundException,
|
|
6
|
-
} from
|
|
7
|
-
import { MembershipStatus, Prisma } from
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
60
|
+
const rows = await this.prisma.membership.findMany({
|
|
54
61
|
where: { orgId },
|
|
55
62
|
include: membershipDetailInclude,
|
|
56
|
-
orderBy: [{ isOwner:
|
|
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:
|
|
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: {
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
|
2
|
-
import { IsUUID } from
|
|
1
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
2
|
+
import { IsUUID } from 'class-validator';
|
|
3
3
|
|
|
4
4
|
export class TransferOwnerDto {
|
|
5
5
|
@ApiProperty({
|
|
6
|
-
example:
|
|
7
|
-
format:
|
|
6
|
+
example: '0a57fb4a-95c6-4f7e-bd5a-f96dbe0599e3',
|
|
7
|
+
format: 'uuid',
|
|
8
8
|
})
|
|
9
9
|
@IsUUID()
|
|
10
10
|
toMembershipId!: string;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { ApiProperty } from
|
|
2
|
-
import { IsUUID } from
|
|
1
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
2
|
+
import { IsUUID } from 'class-validator';
|
|
3
3
|
|
|
4
4
|
export class UpdateMembershipRoleDto {
|
|
5
5
|
@ApiProperty({
|
|
6
|
-
example:
|
|
7
|
-
format:
|
|
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
|
|
2
|
-
import { MembershipStatus } from
|
|
3
|
-
import { IsEnum } from
|
|
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
|
|
2
|
-
import { AuthModule } from
|
|
3
|
-
import { MembershipsService } from
|
|
4
|
-
import { MembershipsController } from
|
|
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
|
|
11
|
+
} from '@nestjs/common';
|
|
12
|
+
import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
|
|
11
13
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
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(
|
|
32
|
+
@ApiTags('Memberships')
|
|
34
33
|
@ApiBearerAuth()
|
|
35
|
-
@ApiParam({ name:
|
|
34
|
+
@ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
|
|
36
35
|
@ApiProtectedErrorResponses(404, 409)
|
|
37
|
-
@Controller(
|
|
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(
|
|
43
|
-
@RequirePermissions(
|
|
41
|
+
@Get('me')
|
|
42
|
+
@RequirePermissions('memberships.read')
|
|
44
43
|
@ApiOperation({
|
|
45
|
-
summary:
|
|
44
|
+
summary: 'Return the current user membership in the organisation.',
|
|
46
45
|
})
|
|
47
46
|
@ApiOkResponse({
|
|
48
|
-
description:
|
|
47
|
+
description: 'Current membership.',
|
|
49
48
|
type: MembershipResponseDto,
|
|
50
49
|
})
|
|
51
|
-
me(@CurrentUser() user: AuthenticatedUser, @Param(
|
|
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(
|
|
57
|
-
@ApiOperation({ summary:
|
|
55
|
+
@RequirePermissions('memberships.read')
|
|
56
|
+
@ApiOperation({ summary: 'List organisation memberships.' })
|
|
58
57
|
@ApiOkResponse({
|
|
59
|
-
description:
|
|
60
|
-
type:
|
|
58
|
+
description: 'Organisation memberships.',
|
|
59
|
+
type: MembershipListResponseDto,
|
|
61
60
|
})
|
|
62
|
-
list(
|
|
63
|
-
|
|
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(
|
|
67
|
-
@RequirePermissions(
|
|
69
|
+
@Patch(':membershipId/status')
|
|
70
|
+
@RequirePermissions('memberships.revoke')
|
|
68
71
|
@ApiParam({
|
|
69
|
-
name:
|
|
70
|
-
description:
|
|
71
|
-
format:
|
|
72
|
+
name: 'membershipId',
|
|
73
|
+
description: 'Membership ID.',
|
|
74
|
+
format: 'uuid',
|
|
72
75
|
})
|
|
73
|
-
@ApiOperation({ summary:
|
|
76
|
+
@ApiOperation({ summary: 'Update membership status.' })
|
|
74
77
|
@ApiOkResponse({
|
|
75
|
-
description:
|
|
78
|
+
description: 'Updated membership.',
|
|
76
79
|
type: MembershipResponseDto,
|
|
77
80
|
})
|
|
78
81
|
updateStatus(
|
|
79
82
|
@CurrentUser() user: AuthenticatedUser,
|
|
80
|
-
@Param(
|
|
81
|
-
@Param(
|
|
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(
|
|
93
|
-
@RequirePermissions(
|
|
90
|
+
@Patch(':membershipId/role')
|
|
91
|
+
@RequirePermissions('roles.manage')
|
|
94
92
|
@ApiParam({
|
|
95
|
-
name:
|
|
96
|
-
description:
|
|
97
|
-
format:
|
|
93
|
+
name: 'membershipId',
|
|
94
|
+
description: 'Membership ID.',
|
|
95
|
+
format: 'uuid',
|
|
98
96
|
})
|
|
99
|
-
@ApiOperation({ summary:
|
|
97
|
+
@ApiOperation({ summary: 'Assign a role to a membership.' })
|
|
100
98
|
@ApiOkResponse({
|
|
101
|
-
description:
|
|
99
|
+
description: 'Updated membership.',
|
|
102
100
|
type: MembershipResponseDto,
|
|
103
101
|
})
|
|
104
102
|
updateRole(
|
|
105
103
|
@CurrentUser() user: AuthenticatedUser,
|
|
106
|
-
@Param(
|
|
107
|
-
@Param(
|
|
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(
|
|
119
|
-
@RequirePermissions(
|
|
111
|
+
@Patch(':membershipId/owner')
|
|
112
|
+
@RequirePermissions('roles.manage')
|
|
120
113
|
@ApiParam({
|
|
121
|
-
name:
|
|
122
|
-
description:
|
|
123
|
-
format:
|
|
114
|
+
name: 'membershipId',
|
|
115
|
+
description: 'Membership ID.',
|
|
116
|
+
format: 'uuid',
|
|
124
117
|
})
|
|
125
|
-
@ApiOperation({ summary:
|
|
118
|
+
@ApiOperation({ summary: 'Grant or remove owner status on a membership.' })
|
|
126
119
|
@ApiOkResponse({
|
|
127
|
-
description:
|
|
120
|
+
description: 'Updated membership.',
|
|
128
121
|
type: MembershipResponseDto,
|
|
129
122
|
})
|
|
130
123
|
updateOwner(
|
|
131
124
|
@CurrentUser() user: AuthenticatedUser,
|
|
132
|
-
@Param(
|
|
133
|
-
@Param(
|
|
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(
|
|
145
|
-
@RequirePermissions(
|
|
132
|
+
@Patch(':membershipId/billing-contact')
|
|
133
|
+
@RequirePermissions('billing.manage')
|
|
146
134
|
@ApiParam({
|
|
147
|
-
name:
|
|
148
|
-
description:
|
|
149
|
-
format:
|
|
135
|
+
name: 'membershipId',
|
|
136
|
+
description: 'Membership ID.',
|
|
137
|
+
format: 'uuid',
|
|
150
138
|
})
|
|
151
139
|
@ApiOperation({
|
|
152
|
-
summary:
|
|
140
|
+
summary: 'Grant or remove billing contact status on a membership.',
|
|
153
141
|
})
|
|
154
142
|
@ApiOkResponse({
|
|
155
|
-
description:
|
|
143
|
+
description: 'Updated membership.',
|
|
156
144
|
type: MembershipResponseDto,
|
|
157
145
|
})
|
|
158
146
|
updateBillingContact(
|
|
159
147
|
@CurrentUser() user: AuthenticatedUser,
|
|
160
|
-
@Param(
|
|
161
|
-
@Param(
|
|
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(
|
|
160
|
+
@Post('transfer-owner')
|
|
173
161
|
@HttpCode(200)
|
|
174
|
-
@RequirePermissions(
|
|
162
|
+
@RequirePermissions('roles.manage')
|
|
175
163
|
@ApiOperation({
|
|
176
|
-
summary:
|
|
164
|
+
summary: 'Transfer organisation ownership to another active membership.',
|
|
177
165
|
})
|
|
178
166
|
@ApiOkResponse({
|
|
179
|
-
description:
|
|
167
|
+
description: 'New owner membership.',
|
|
180
168
|
type: MembershipResponseDto,
|
|
181
169
|
})
|
|
182
170
|
transferOwner(
|
|
183
171
|
@CurrentUser() user: AuthenticatedUser,
|
|
184
|
-
@Param(
|
|
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
|
}
|