@simple-auth-kit/cli 1.4.2 → 1.5.0
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/registry/combos/express-drizzle/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
- package/registry/combos/express-drizzle/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +23 -7
- package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/middleware}/response-envelope.middleware.ts +15 -3
- package/registry/combos/express-drizzle/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +32 -9
- package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
- package/registry/combos/express-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
- package/registry/combos/express-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +20 -6
- package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
- package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +395 -0
- package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +52 -19
- package/registry/combos/express-drizzle/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
- package/registry/combos/express-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +28 -10
- package/registry/combos/express-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +239 -48
- package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +497 -0
- package/registry/combos/express-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +480 -109
- package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +69 -23
- package/registry/combos/express-drizzle/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
- package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +147 -28
- package/registry/combos/express-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +30 -11
- package/registry/combos/express-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +319 -70
- package/registry/combos/express-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +127 -30
- package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +534 -0
- package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +194 -0
- package/registry/combos/express-drizzle/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +544 -122
- package/registry/combos/express-prisma/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
- package/registry/combos/express-prisma/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +19 -6
- package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/middleware}/response-envelope.middleware.ts +15 -3
- package/registry/combos/express-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +27 -9
- package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
- package/registry/combos/express-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
- package/registry/combos/express-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
- package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
- package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +398 -0
- package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +57 -22
- package/registry/combos/express-prisma/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
- package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +67 -19
- package/registry/combos/express-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/express-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +199 -48
- package/registry/combos/express-prisma/variants/base/src/routers/admin.router.ts +508 -0
- package/registry/combos/express-prisma/variants/base/src/seed.ts +78 -20
- package/registry/combos/express-prisma/variants/base/src/{auth.service.ts → services/auth.service.ts} +454 -105
- package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +81 -26
- package/registry/combos/express-prisma/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
- package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +147 -28
- package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +73 -21
- package/registry/combos/express-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/express-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +272 -64
- package/registry/combos/express-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +86 -23
- package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +545 -0
- package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +194 -0
- package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +109 -29
- package/registry/combos/express-prisma/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +518 -119
- package/registry/combos/nestjs-drizzle/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
- package/registry/combos/nestjs-drizzle/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
- package/registry/combos/nestjs-drizzle/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
- package/registry/combos/nestjs-drizzle/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
- package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/interceptors}/response.interceptor.ts +10 -2
- package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +63 -0
- package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
- package/registry/combos/nestjs-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
- package/registry/combos/nestjs-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +21 -7
- package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
- package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1097 -307
- package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +44 -20
- package/registry/combos/nestjs-drizzle/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +228 -62
- package/registry/combos/nestjs-drizzle/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
- package/registry/combos/nestjs-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +27 -10
- package/registry/combos/nestjs-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +249 -51
- package/registry/combos/nestjs-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +499 -116
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +48 -22
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +269 -70
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +29 -11
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +323 -71
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +131 -30
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +988 -0
- package/registry/combos/nestjs-prisma/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
- package/registry/combos/nestjs-prisma/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
- package/registry/combos/nestjs-prisma/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
- package/registry/combos/nestjs-prisma/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
- package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/interceptors}/response.interceptor.ts +10 -2
- package/registry/combos/nestjs-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +25 -8
- package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
- package/registry/combos/nestjs-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
- package/registry/combos/nestjs-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
- package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
- package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1097 -307
- package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +59 -28
- package/registry/combos/nestjs-prisma/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +469 -110
- package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.gateway.ts → gateways/audit-log.gateway.ts} +22 -6
- package/registry/combos/nestjs-prisma/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
- package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +98 -27
- package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/nestjs-prisma/variants/base/src/{country.repository.ts → repositories/country.repository.ts} +98 -25
- package/registry/combos/nestjs-prisma/variants/base/src/{customer.repository.ts → repositories/customer.repository.ts} +106 -27
- package/registry/combos/nestjs-prisma/variants/base/src/{language.repository.ts → repositories/language.repository.ts} +93 -25
- package/registry/combos/nestjs-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +229 -55
- package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +78 -20
- package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +1064 -0
- package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +55 -27
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +544 -128
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
- package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +104 -29
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{country.repository.ts → repositories/country.repository.ts} +127 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{customer.repository.ts → repositories/customer.repository.ts} +137 -28
- package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +243 -0
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +314 -74
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +94 -24
- package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +109 -29
- package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
- package/registry.json +6 -4
- package/simple-auth-kit.ts +582 -124
- package/registry/combos/express-drizzle/shared/src/auth.router.ts +0 -266
- package/registry/combos/express-drizzle/shared/src/password-reset.repository.ts +0 -35
- package/registry/combos/express-drizzle/variants/base/src/admin.router.ts +0 -307
- package/registry/combos/express-drizzle/variants/workspaces/src/admin.router.ts +0 -323
- package/registry/combos/express-drizzle/variants/workspaces/src/workspace.router.ts +0 -123
- package/registry/combos/express-prisma/shared/src/auth.router.ts +0 -269
- package/registry/combos/express-prisma/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/express-prisma/variants/base/src/admin.router.ts +0 -314
- package/registry/combos/express-prisma/variants/workspaces/src/admin.router.ts +0 -330
- package/registry/combos/express-prisma/variants/workspaces/src/workspace.router.ts +0 -123
- package/registry/combos/nestjs-drizzle/shared/src/oauth.repository.ts +0 -36
- package/registry/combos/nestjs-drizzle/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.service.ts +0 -553
- package/registry/combos/nestjs-prisma/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/nestjs-prisma/variants/base/src/auth.service.ts +0 -618
- package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.service.ts +0 -509
- package/registry/combos/nestjs-prisma/variants/workspaces/src/language.repository.ts +0 -143
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { Inject, Injectable, NotFoundException } from "@nestjs/common";
|
|
2
2
|
import { resolvePermissions } from "@/lib/auth/core/rbac.js";
|
|
3
|
-
import { Prisma, PrismaClient } from "
|
|
4
|
-
import type { AuthzContext } from "
|
|
5
|
-
import { PermissionCache } from "
|
|
6
|
-
import { toId, toIdOrNull } from "
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
import { Prisma, PrismaClient } from "../../generated/prisma/client.js";
|
|
4
|
+
import type { AuthzContext } from "../guards/authz.guard.js";
|
|
5
|
+
import { PermissionCache } from "../permission-cache.js";
|
|
6
|
+
import { toId, toIdOrNull } from "../id.helper.js";
|
|
7
|
+
import {
|
|
8
|
+
buildPageMeta,
|
|
9
|
+
normalizeLimit,
|
|
10
|
+
normalizePage,
|
|
11
|
+
type Paginated,
|
|
12
|
+
} from "../pagination.js";
|
|
13
|
+
|
|
14
|
+
const MEMBER_INCLUDE = {
|
|
15
|
+
user: true,
|
|
16
|
+
roles: { select: { role: { select: { slug: true } } } },
|
|
17
|
+
} as const;
|
|
10
18
|
|
|
11
19
|
/** A `WorkspaceMember` row as `findUnique`/`findMany` return it, joined with its user and roles. */
|
|
12
|
-
export type MemberRow = Prisma.WorkspaceMemberGetPayload<{
|
|
20
|
+
export type MemberRow = Prisma.WorkspaceMemberGetPayload<{
|
|
21
|
+
include: typeof MEMBER_INCLUDE;
|
|
22
|
+
}>;
|
|
13
23
|
|
|
14
24
|
/** Shapes a raw row into the wire DTO. Exported so `listMembers`'s collection can be shaped by the caller. */
|
|
15
25
|
/** Every column `User`/`WorkspaceMember` has, except the two secrets (`passwordHash`, `twoFactorSecret`) — see the note on `MemberSummary`. */
|
|
@@ -107,7 +117,9 @@ export interface RoleSummary {
|
|
|
107
117
|
permissions: string[];
|
|
108
118
|
}
|
|
109
119
|
|
|
110
|
-
const ROLE_PERMISSIONS_INCLUDE = {
|
|
120
|
+
const ROLE_PERMISSIONS_INCLUDE = {
|
|
121
|
+
permissions: { select: { permission: { select: { slug: true } } } },
|
|
122
|
+
} as const;
|
|
111
123
|
|
|
112
124
|
const PERMISSION_SELECT = {
|
|
113
125
|
id: true,
|
|
@@ -121,11 +133,19 @@ const PERMISSION_SELECT = {
|
|
|
121
133
|
isActive: true,
|
|
122
134
|
} as const;
|
|
123
135
|
|
|
124
|
-
const ROLE_SELECT = {
|
|
136
|
+
const ROLE_SELECT = {
|
|
137
|
+
id: true,
|
|
138
|
+
slug: true,
|
|
139
|
+
name: true,
|
|
140
|
+
displayName: true,
|
|
141
|
+
isDefault: true,
|
|
142
|
+
isActive: true,
|
|
143
|
+
} as const;
|
|
125
144
|
|
|
126
145
|
// The cache subject in this variant: authorization belongs to a membership, so the key is the
|
|
127
146
|
// `[userId, workspaceId]` pair — keeps invalidation in one workspace from flushing the others.
|
|
128
|
-
export const memberCacheKey = (userId: string, workspaceId: string) =>
|
|
147
|
+
export const memberCacheKey = (userId: string, workspaceId: string) =>
|
|
148
|
+
`${userId}:${workspaceId}`;
|
|
129
149
|
|
|
130
150
|
@Injectable()
|
|
131
151
|
export class RbacRepository {
|
|
@@ -147,8 +167,15 @@ export class RbacRepository {
|
|
|
147
167
|
// Takes bigint directly: its one caller (setMemberRoles) already has the membership's id in
|
|
148
168
|
// hand from its own Prisma lookup, so there's nothing to parse here.
|
|
149
169
|
private async invalidateMemberById(memberId: bigint): Promise<void> {
|
|
150
|
-
const member = await this.prisma.workspaceMember.findUnique({
|
|
151
|
-
|
|
170
|
+
const member = await this.prisma.workspaceMember.findUnique({
|
|
171
|
+
where: { id: memberId },
|
|
172
|
+
select: { userId: true, workspaceId: true },
|
|
173
|
+
});
|
|
174
|
+
if (member)
|
|
175
|
+
await this.invalidateMember(
|
|
176
|
+
member.userId.toString(),
|
|
177
|
+
member.workspaceId.toString(),
|
|
178
|
+
);
|
|
152
179
|
}
|
|
153
180
|
|
|
154
181
|
// The hot path: every workspace-scoped request runs this once. Anchored on the
|
|
@@ -156,7 +183,10 @@ export class RbacRepository {
|
|
|
156
183
|
// member → role_member → roles → permission_role → permissions, and member →
|
|
157
184
|
// permission_member → permissions. Workspace-scoped by construction: the same login in
|
|
158
185
|
// another workspace resolves a different membership.
|
|
159
|
-
async resolveAuthzContext(
|
|
186
|
+
async resolveAuthzContext(
|
|
187
|
+
userId: string,
|
|
188
|
+
workspaceId: string,
|
|
189
|
+
): Promise<AuthzContext | null> {
|
|
160
190
|
// workspaceId arrives straight from the untrusted `X-Workspace-Id` header — a value that
|
|
161
191
|
// isn't a valid bigint is exactly as "not a member" as one that parses but names no
|
|
162
192
|
// membership, so it must not throw and 500 the request.
|
|
@@ -170,7 +200,10 @@ export class RbacRepository {
|
|
|
170
200
|
}
|
|
171
201
|
|
|
172
202
|
const member = await this.prisma.workspaceMember.findUnique({
|
|
173
|
-
where: {
|
|
203
|
+
where: {
|
|
204
|
+
userId_workspaceId: { userId: userIdBig, workspaceId: workspaceIdBig },
|
|
205
|
+
user: { isDeleted: false },
|
|
206
|
+
},
|
|
174
207
|
select: {
|
|
175
208
|
id: true,
|
|
176
209
|
roles: {
|
|
@@ -179,18 +212,28 @@ export class RbacRepository {
|
|
|
179
212
|
role: {
|
|
180
213
|
select: {
|
|
181
214
|
slug: true,
|
|
182
|
-
permissions: {
|
|
215
|
+
permissions: {
|
|
216
|
+
where: { permission: { isActive: true, isDeleted: false } },
|
|
217
|
+
select: { permission: { select: { slug: true } } },
|
|
218
|
+
},
|
|
183
219
|
},
|
|
184
220
|
},
|
|
185
221
|
},
|
|
186
222
|
},
|
|
187
|
-
permissions: {
|
|
223
|
+
permissions: {
|
|
224
|
+
where: { permission: { isActive: true, isDeleted: false } },
|
|
225
|
+
select: { permission: { select: { slug: true } } },
|
|
226
|
+
},
|
|
188
227
|
},
|
|
189
228
|
});
|
|
190
229
|
if (!member) return null;
|
|
191
230
|
|
|
192
|
-
const rolePermissions = member.roles.flatMap((roleMember) =>
|
|
193
|
-
|
|
231
|
+
const rolePermissions = member.roles.flatMap((roleMember) =>
|
|
232
|
+
roleMember.role.permissions.map((pr) => pr.permission.slug),
|
|
233
|
+
);
|
|
234
|
+
const directPermissions = member.permissions.map(
|
|
235
|
+
(pm) => pm.permission.slug,
|
|
236
|
+
);
|
|
194
237
|
|
|
195
238
|
return {
|
|
196
239
|
workspaceId,
|
|
@@ -203,12 +246,21 @@ export class RbacRepository {
|
|
|
203
246
|
// Never crosses a workspace boundary: a caller can only name a target inside their own scope.
|
|
204
247
|
// Returns bigint directly: every internal caller feeds the id straight into another Prisma
|
|
205
248
|
// call, and the one external caller (auth.service.ts) discards the result.
|
|
206
|
-
async requireMember(
|
|
249
|
+
async requireMember(
|
|
250
|
+
workspaceId: string,
|
|
251
|
+
userId: string,
|
|
252
|
+
): Promise<{ id: bigint }> {
|
|
207
253
|
const member = await this.prisma.workspaceMember.findUnique({
|
|
208
|
-
where: {
|
|
254
|
+
where: {
|
|
255
|
+
userId_workspaceId: {
|
|
256
|
+
userId: toId(userId),
|
|
257
|
+
workspaceId: toId(workspaceId),
|
|
258
|
+
},
|
|
259
|
+
},
|
|
209
260
|
select: { id: true },
|
|
210
261
|
});
|
|
211
|
-
if (!member)
|
|
262
|
+
if (!member)
|
|
263
|
+
throw new NotFoundException("user is not a member of this workspace");
|
|
212
264
|
return member;
|
|
213
265
|
}
|
|
214
266
|
|
|
@@ -216,12 +268,20 @@ export class RbacRepository {
|
|
|
216
268
|
* Newest-first, page-paginated; `search` matches an email substring. Returns raw rows —
|
|
217
269
|
* shaping the collection is the caller's job, see `toMemberSummary`.
|
|
218
270
|
*/
|
|
219
|
-
async listMembers(
|
|
271
|
+
async listMembers(
|
|
272
|
+
workspaceId: string,
|
|
273
|
+
filter: MemberListFilter = {},
|
|
274
|
+
): Promise<Paginated<MemberRow>> {
|
|
220
275
|
const page = normalizePage(filter.page);
|
|
221
276
|
const limit = normalizeLimit(filter.limit);
|
|
222
277
|
const where = {
|
|
223
278
|
workspaceId: toId(workspaceId),
|
|
224
|
-
user: {
|
|
279
|
+
user: {
|
|
280
|
+
isDeleted: false,
|
|
281
|
+
email: filter.search
|
|
282
|
+
? { contains: filter.search, mode: "insensitive" as const }
|
|
283
|
+
: undefined,
|
|
284
|
+
},
|
|
225
285
|
};
|
|
226
286
|
|
|
227
287
|
const [rows, total] = await this.prisma.$transaction([
|
|
@@ -256,11 +316,21 @@ export class RbacRepository {
|
|
|
256
316
|
async updateMember(
|
|
257
317
|
workspaceId: string,
|
|
258
318
|
userId: string,
|
|
259
|
-
input: {
|
|
319
|
+
input: {
|
|
320
|
+
firstName?: string | null;
|
|
321
|
+
lastName?: string | null;
|
|
322
|
+
displayName?: string | null;
|
|
323
|
+
phone?: string | null;
|
|
324
|
+
username?: string | null;
|
|
325
|
+
photo?: string | null;
|
|
326
|
+
},
|
|
260
327
|
actorUserId: string | null,
|
|
261
328
|
): Promise<MemberSummary> {
|
|
262
329
|
await this.requireMember(workspaceId, userId);
|
|
263
|
-
await this.prisma.user.update({
|
|
330
|
+
await this.prisma.user.update({
|
|
331
|
+
where: { id: toId(userId) },
|
|
332
|
+
data: { ...input, updatedBy: toIdOrNull(actorUserId) },
|
|
333
|
+
});
|
|
264
334
|
return this.getMember(workspaceId, userId);
|
|
265
335
|
}
|
|
266
336
|
|
|
@@ -268,11 +338,21 @@ export class RbacRepository {
|
|
|
268
338
|
// pattern, and `block`'s existing precedent of a workspace admin taking a global action on a
|
|
269
339
|
// member's account — this disables the account across every workspace it belongs to, not just
|
|
270
340
|
// this one, which is why it's gated the same way `block` is.
|
|
271
|
-
async deleteMember(
|
|
341
|
+
async deleteMember(
|
|
342
|
+
workspaceId: string,
|
|
343
|
+
userId: string,
|
|
344
|
+
actorUserId: string | null,
|
|
345
|
+
reason?: string,
|
|
346
|
+
): Promise<void> {
|
|
272
347
|
await this.requireMember(workspaceId, userId);
|
|
273
348
|
await this.prisma.user.update({
|
|
274
349
|
where: { id: toId(userId) },
|
|
275
|
-
data: {
|
|
350
|
+
data: {
|
|
351
|
+
isDeleted: true,
|
|
352
|
+
deletedAt: new Date(),
|
|
353
|
+
deletedBy: toIdOrNull(actorUserId),
|
|
354
|
+
deletedReason: reason ?? null,
|
|
355
|
+
},
|
|
276
356
|
});
|
|
277
357
|
await this.invalidateMember(userId, workspaceId);
|
|
278
358
|
}
|
|
@@ -280,32 +360,59 @@ export class RbacRepository {
|
|
|
280
360
|
async updateRole(
|
|
281
361
|
workspaceId: string,
|
|
282
362
|
roleId: string,
|
|
283
|
-
input: {
|
|
363
|
+
input: {
|
|
364
|
+
name?: string;
|
|
365
|
+
displayName?: string;
|
|
366
|
+
description?: string | null;
|
|
367
|
+
isActive?: boolean;
|
|
368
|
+
},
|
|
284
369
|
actorUserId: string | null,
|
|
285
370
|
): Promise<RoleSummary> {
|
|
286
371
|
const workspaceIdBig = toId(workspaceId);
|
|
287
372
|
const roleIdBig = toId(roleId);
|
|
288
|
-
const existing = await this.prisma.role.findUnique({
|
|
289
|
-
|
|
373
|
+
const existing = await this.prisma.role.findUnique({
|
|
374
|
+
where: { id: roleIdBig, isDeleted: false },
|
|
375
|
+
select: { id: true, workspaceId: true },
|
|
376
|
+
});
|
|
377
|
+
if (!existing || existing.workspaceId !== workspaceIdBig)
|
|
378
|
+
throw new NotFoundException("role not found in this workspace");
|
|
290
379
|
const role = await this.prisma.role.update({
|
|
291
380
|
where: { id: roleIdBig },
|
|
292
381
|
data: { ...input, updatedBy: toIdOrNull(actorUserId) },
|
|
293
382
|
select: { ...ROLE_SELECT, ...ROLE_PERMISSIONS_INCLUDE },
|
|
294
383
|
});
|
|
295
384
|
await this.cache.invalidatePolicy();
|
|
296
|
-
return {
|
|
385
|
+
return {
|
|
386
|
+
...role,
|
|
387
|
+
id: role.id.toString(),
|
|
388
|
+
permissions: role.permissions.map((p) => p.permission.slug).sort(),
|
|
389
|
+
};
|
|
297
390
|
}
|
|
298
391
|
|
|
299
392
|
// Soft-delete. `role_member`/`permission_role` rows pointing at it are left in place — the
|
|
300
393
|
// role simply stops being resolved by `resolveAuthzContext`/`listRoles`.
|
|
301
|
-
async deleteRole(
|
|
394
|
+
async deleteRole(
|
|
395
|
+
workspaceId: string,
|
|
396
|
+
roleId: string,
|
|
397
|
+
actorUserId: string | null,
|
|
398
|
+
reason?: string,
|
|
399
|
+
): Promise<void> {
|
|
302
400
|
const workspaceIdBig = toId(workspaceId);
|
|
303
401
|
const roleIdBig = toId(roleId);
|
|
304
|
-
const existing = await this.prisma.role.findUnique({
|
|
305
|
-
|
|
402
|
+
const existing = await this.prisma.role.findUnique({
|
|
403
|
+
where: { id: roleIdBig, isDeleted: false },
|
|
404
|
+
select: { id: true, workspaceId: true },
|
|
405
|
+
});
|
|
406
|
+
if (!existing || existing.workspaceId !== workspaceIdBig)
|
|
407
|
+
throw new NotFoundException("role not found in this workspace");
|
|
306
408
|
await this.prisma.role.update({
|
|
307
409
|
where: { id: roleIdBig },
|
|
308
|
-
data: {
|
|
410
|
+
data: {
|
|
411
|
+
isDeleted: true,
|
|
412
|
+
deletedAt: new Date(),
|
|
413
|
+
deletedBy: toIdOrNull(actorUserId),
|
|
414
|
+
deletedReason: reason ?? null,
|
|
415
|
+
},
|
|
309
416
|
});
|
|
310
417
|
await this.cache.invalidatePolicy();
|
|
311
418
|
}
|
|
@@ -318,14 +425,22 @@ export class RbacRepository {
|
|
|
318
425
|
const rows = await this.prisma.permission.findMany({
|
|
319
426
|
where: { isDeleted: false, isActive: activeOnly ? true : undefined },
|
|
320
427
|
select: PERMISSION_SELECT,
|
|
321
|
-
orderBy: [
|
|
428
|
+
orderBy: [
|
|
429
|
+
{ groupOrder: "asc" },
|
|
430
|
+
{ group: "asc" },
|
|
431
|
+
{ order: "asc" },
|
|
432
|
+
{ slug: "asc" },
|
|
433
|
+
],
|
|
322
434
|
});
|
|
323
435
|
return rows.map((row) => ({ ...row, id: row.id.toString() }));
|
|
324
436
|
}
|
|
325
437
|
|
|
326
438
|
// Upserted on `slug`, the stable identifier grants/revocations use, so renaming the display
|
|
327
439
|
// name never breaks a grant. Effective in every workspace on the next request.
|
|
328
|
-
async upsertPermission(
|
|
440
|
+
async upsertPermission(
|
|
441
|
+
input: PermissionInput,
|
|
442
|
+
actorUserId: string | null,
|
|
443
|
+
): Promise<PermissionSummary> {
|
|
329
444
|
const shared = {
|
|
330
445
|
name: input.name,
|
|
331
446
|
displayName: input.displayName,
|
|
@@ -355,18 +470,34 @@ export class RbacRepository {
|
|
|
355
470
|
}
|
|
356
471
|
|
|
357
472
|
/** `activeOnly` is what a role picker (Add member) passes — the admin management list itself wants everything, active or not. */
|
|
358
|
-
async listRoles(
|
|
473
|
+
async listRoles(
|
|
474
|
+
workspaceId: string,
|
|
475
|
+
activeOnly?: boolean,
|
|
476
|
+
): Promise<RoleSummary[]> {
|
|
359
477
|
const rows = await this.prisma.role.findMany({
|
|
360
|
-
where: {
|
|
478
|
+
where: {
|
|
479
|
+
workspaceId: toId(workspaceId),
|
|
480
|
+
isDeleted: false,
|
|
481
|
+
isActive: activeOnly ? true : undefined,
|
|
482
|
+
},
|
|
361
483
|
select: { ...ROLE_SELECT, ...ROLE_PERMISSIONS_INCLUDE },
|
|
362
484
|
orderBy: [{ order: "asc" }, { slug: "asc" }],
|
|
363
485
|
});
|
|
364
|
-
return rows.map((row) => ({
|
|
486
|
+
return rows.map((row) => ({
|
|
487
|
+
...row,
|
|
488
|
+
id: row.id.toString(),
|
|
489
|
+
permissions: row.permissions.map((p) => p.permission.slug).sort(),
|
|
490
|
+
}));
|
|
365
491
|
}
|
|
366
492
|
|
|
367
493
|
async createRole(
|
|
368
494
|
workspaceId: string,
|
|
369
|
-
input: {
|
|
495
|
+
input: {
|
|
496
|
+
slug: string;
|
|
497
|
+
name?: string;
|
|
498
|
+
displayName?: string;
|
|
499
|
+
description?: string | null;
|
|
500
|
+
},
|
|
370
501
|
actorUserId: string | null,
|
|
371
502
|
): Promise<RoleSummary> {
|
|
372
503
|
const role = await this.prisma.role.create({
|
|
@@ -385,15 +516,26 @@ export class RbacRepository {
|
|
|
385
516
|
}
|
|
386
517
|
|
|
387
518
|
// Scoped by workspace, so an admin can't reach a role id belonging to a workspace they aren't in.
|
|
388
|
-
async attachPermissionToRole(
|
|
519
|
+
async attachPermissionToRole(
|
|
520
|
+
workspaceId: string,
|
|
521
|
+
roleId: string,
|
|
522
|
+
permissionSlug: string,
|
|
523
|
+
actorUserId: string | null,
|
|
524
|
+
): Promise<void> {
|
|
389
525
|
const workspaceIdBig = toId(workspaceId);
|
|
390
526
|
const roleIdBig = toId(roleId);
|
|
391
|
-
const role = await this.prisma.role.findUnique({
|
|
392
|
-
|
|
527
|
+
const role = await this.prisma.role.findUnique({
|
|
528
|
+
where: { id: roleIdBig, isDeleted: false },
|
|
529
|
+
select: { id: true, workspaceId: true },
|
|
530
|
+
});
|
|
531
|
+
if (!role || role.workspaceId !== workspaceIdBig)
|
|
532
|
+
throw new NotFoundException("role not found in this workspace");
|
|
393
533
|
|
|
394
534
|
const permission = await this.ensurePermission(permissionSlug, actorUserId);
|
|
395
535
|
await this.prisma.permissionRole.upsert({
|
|
396
|
-
where: {
|
|
536
|
+
where: {
|
|
537
|
+
permissionId_roleId: { permissionId: permission.id, roleId: roleIdBig },
|
|
538
|
+
},
|
|
397
539
|
create: { permissionId: permission.id, roleId: roleIdBig },
|
|
398
540
|
update: {},
|
|
399
541
|
});
|
|
@@ -401,20 +543,37 @@ export class RbacRepository {
|
|
|
401
543
|
}
|
|
402
544
|
|
|
403
545
|
// Scoped by workspace, so an admin can't reach a role id belonging to a workspace they aren't in.
|
|
404
|
-
async detachPermissionFromRole(
|
|
546
|
+
async detachPermissionFromRole(
|
|
547
|
+
workspaceId: string,
|
|
548
|
+
roleId: string,
|
|
549
|
+
permissionSlug: string,
|
|
550
|
+
): Promise<void> {
|
|
405
551
|
const workspaceIdBig = toId(workspaceId);
|
|
406
552
|
const roleIdBig = toId(roleId);
|
|
407
|
-
const role = await this.prisma.role.findUnique({
|
|
408
|
-
|
|
553
|
+
const role = await this.prisma.role.findUnique({
|
|
554
|
+
where: { id: roleIdBig, isDeleted: false },
|
|
555
|
+
select: { id: true, workspaceId: true },
|
|
556
|
+
});
|
|
557
|
+
if (!role || role.workspaceId !== workspaceIdBig)
|
|
558
|
+
throw new NotFoundException("role not found in this workspace");
|
|
409
559
|
|
|
410
|
-
const permission = await this.prisma.permission.findUnique({
|
|
560
|
+
const permission = await this.prisma.permission.findUnique({
|
|
561
|
+
where: { slug: permissionSlug },
|
|
562
|
+
select: { id: true },
|
|
563
|
+
});
|
|
411
564
|
if (!permission) throw new NotFoundException("permission not found");
|
|
412
565
|
|
|
413
|
-
await this.prisma.permissionRole.deleteMany({
|
|
566
|
+
await this.prisma.permissionRole.deleteMany({
|
|
567
|
+
where: { roleId: roleIdBig, permissionId: permission.id },
|
|
568
|
+
});
|
|
414
569
|
await this.cache.invalidatePolicy();
|
|
415
570
|
}
|
|
416
571
|
|
|
417
|
-
async assignRoleToMember(
|
|
572
|
+
async assignRoleToMember(
|
|
573
|
+
workspaceId: string,
|
|
574
|
+
userId: string,
|
|
575
|
+
roleSlug: string,
|
|
576
|
+
): Promise<void> {
|
|
418
577
|
const role = await this.requireRole(workspaceId, roleSlug);
|
|
419
578
|
const member = await this.requireMember(workspaceId, userId);
|
|
420
579
|
await this.prisma.roleMember.upsert({
|
|
@@ -425,32 +584,63 @@ export class RbacRepository {
|
|
|
425
584
|
await this.invalidateMember(userId, workspaceId);
|
|
426
585
|
}
|
|
427
586
|
|
|
428
|
-
async revokeRoleFromMember(
|
|
587
|
+
async revokeRoleFromMember(
|
|
588
|
+
workspaceId: string,
|
|
589
|
+
userId: string,
|
|
590
|
+
roleSlug: string,
|
|
591
|
+
): Promise<void> {
|
|
429
592
|
const member = await this.requireMember(workspaceId, userId);
|
|
430
|
-
const role = await this.prisma.role.findUnique({
|
|
593
|
+
const role = await this.prisma.role.findUnique({
|
|
594
|
+
where: {
|
|
595
|
+
workspaceId_slug: { workspaceId: toId(workspaceId), slug: roleSlug },
|
|
596
|
+
},
|
|
597
|
+
select: { id: true },
|
|
598
|
+
});
|
|
431
599
|
if (!role) return;
|
|
432
|
-
await this.prisma.roleMember.deleteMany({
|
|
600
|
+
await this.prisma.roleMember.deleteMany({
|
|
601
|
+
where: { memberId: member.id, roleId: role.id },
|
|
602
|
+
});
|
|
433
603
|
await this.invalidateMember(userId, workspaceId);
|
|
434
604
|
}
|
|
435
605
|
|
|
436
606
|
// Replaces the whole set, as opposed to assign/revoke one at a time.
|
|
437
|
-
async setMemberRoles(
|
|
607
|
+
async setMemberRoles(
|
|
608
|
+
workspaceId: string,
|
|
609
|
+
memberId: string,
|
|
610
|
+
roleSlugs: string[],
|
|
611
|
+
): Promise<{ memberId: string; roles: string[] }> {
|
|
438
612
|
const workspaceIdBig = toId(workspaceId);
|
|
439
613
|
const memberIdBig = toId(memberId);
|
|
440
|
-
const member = await this.prisma.workspaceMember.findUnique({
|
|
441
|
-
|
|
614
|
+
const member = await this.prisma.workspaceMember.findUnique({
|
|
615
|
+
where: { id: memberIdBig },
|
|
616
|
+
select: { id: true, workspaceId: true },
|
|
617
|
+
});
|
|
618
|
+
if (!member || member.workspaceId !== workspaceIdBig)
|
|
619
|
+
throw new NotFoundException("member not found in this workspace");
|
|
442
620
|
|
|
443
621
|
const roles = await this.prisma.role.findMany({
|
|
444
|
-
where: {
|
|
622
|
+
where: {
|
|
623
|
+
workspaceId: workspaceIdBig,
|
|
624
|
+
slug: { in: roleSlugs },
|
|
625
|
+
isDeleted: false,
|
|
626
|
+
},
|
|
445
627
|
select: { id: true, slug: true },
|
|
446
628
|
});
|
|
447
|
-
const unknown = roleSlugs.filter(
|
|
448
|
-
|
|
629
|
+
const unknown = roleSlugs.filter(
|
|
630
|
+
(slug) => !roles.some((role) => role.slug === slug),
|
|
631
|
+
);
|
|
632
|
+
if (unknown.length)
|
|
633
|
+
throw new NotFoundException(
|
|
634
|
+
`role(s) not defined in this workspace: ${unknown.join(", ")}`,
|
|
635
|
+
);
|
|
449
636
|
|
|
450
637
|
// One transaction: a member must never be briefly role-less to a concurrent request.
|
|
451
638
|
await this.prisma.$transaction([
|
|
452
639
|
this.prisma.roleMember.deleteMany({ where: { memberId: memberIdBig } }),
|
|
453
|
-
this.prisma.roleMember.createMany({
|
|
640
|
+
this.prisma.roleMember.createMany({
|
|
641
|
+
data: roles.map((role) => ({ memberId: memberIdBig, roleId: role.id })),
|
|
642
|
+
skipDuplicates: true,
|
|
643
|
+
}),
|
|
454
644
|
]);
|
|
455
645
|
await this.invalidateMemberById(memberIdBig);
|
|
456
646
|
return { memberId, roles: roles.map((role) => role.slug).sort() };
|
|
@@ -459,45 +649,95 @@ export class RbacRepository {
|
|
|
459
649
|
// The roles a brand-new membership starts with — the rows flagged `isDefault` in this workspace.
|
|
460
650
|
// Unused by any caller today; kept bigint-native rather than round-tripped through string.
|
|
461
651
|
async defaultRoleIdsFor(workspaceId: string): Promise<bigint[]> {
|
|
462
|
-
const roles = await this.prisma.role.findMany({
|
|
652
|
+
const roles = await this.prisma.role.findMany({
|
|
653
|
+
where: {
|
|
654
|
+
workspaceId: toId(workspaceId),
|
|
655
|
+
isDefault: true,
|
|
656
|
+
isActive: true,
|
|
657
|
+
isDeleted: false,
|
|
658
|
+
},
|
|
659
|
+
select: { id: true },
|
|
660
|
+
});
|
|
463
661
|
return roles.map((role) => role.id);
|
|
464
662
|
}
|
|
465
663
|
|
|
466
|
-
async grantPermissionToMember(
|
|
664
|
+
async grantPermissionToMember(
|
|
665
|
+
workspaceId: string,
|
|
666
|
+
userId: string,
|
|
667
|
+
permissionSlug: string,
|
|
668
|
+
actorUserId: string | null,
|
|
669
|
+
): Promise<void> {
|
|
467
670
|
const member = await this.requireMember(workspaceId, userId);
|
|
468
671
|
const permission = await this.ensurePermission(permissionSlug, actorUserId);
|
|
469
672
|
await this.prisma.permissionMember.upsert({
|
|
470
|
-
where: {
|
|
673
|
+
where: {
|
|
674
|
+
memberId_permissionId: {
|
|
675
|
+
memberId: member.id,
|
|
676
|
+
permissionId: permission.id,
|
|
677
|
+
},
|
|
678
|
+
},
|
|
471
679
|
create: { memberId: member.id, permissionId: permission.id },
|
|
472
680
|
update: {},
|
|
473
681
|
});
|
|
474
682
|
await this.invalidateMember(userId, workspaceId);
|
|
475
683
|
}
|
|
476
684
|
|
|
477
|
-
async revokePermissionFromMember(
|
|
685
|
+
async revokePermissionFromMember(
|
|
686
|
+
workspaceId: string,
|
|
687
|
+
userId: string,
|
|
688
|
+
permissionSlug: string,
|
|
689
|
+
): Promise<void> {
|
|
478
690
|
const member = await this.requireMember(workspaceId, userId);
|
|
479
|
-
const permission = await this.prisma.permission.findUnique({
|
|
691
|
+
const permission = await this.prisma.permission.findUnique({
|
|
692
|
+
where: { slug: permissionSlug },
|
|
693
|
+
select: { id: true },
|
|
694
|
+
});
|
|
480
695
|
if (!permission) return;
|
|
481
|
-
await this.prisma.permissionMember.deleteMany({
|
|
696
|
+
await this.prisma.permissionMember.deleteMany({
|
|
697
|
+
where: { memberId: member.id, permissionId: permission.id },
|
|
698
|
+
});
|
|
482
699
|
await this.invalidateMember(userId, workspaceId);
|
|
483
700
|
}
|
|
484
701
|
|
|
485
702
|
// Returns bigint directly: its one caller (assignRoleToMember) feeds the id straight into
|
|
486
703
|
// another Prisma call.
|
|
487
|
-
private async requireRole(
|
|
488
|
-
|
|
489
|
-
|
|
704
|
+
private async requireRole(
|
|
705
|
+
workspaceId: string,
|
|
706
|
+
slug: string,
|
|
707
|
+
): Promise<{ id: bigint }> {
|
|
708
|
+
const role = await this.prisma.role.findUnique({
|
|
709
|
+
where: {
|
|
710
|
+
workspaceId_slug: { workspaceId: toId(workspaceId), slug },
|
|
711
|
+
isDeleted: false,
|
|
712
|
+
},
|
|
713
|
+
select: { id: true },
|
|
714
|
+
});
|
|
715
|
+
if (!role)
|
|
716
|
+
throw new NotFoundException(`role "${slug}" not found in this workspace`);
|
|
490
717
|
return role;
|
|
491
718
|
}
|
|
492
719
|
|
|
493
720
|
// Creates the row for a slug granted before anyone defined it; leaves an existing definition alone.
|
|
494
721
|
// Returns bigint directly: both callers feed the id straight into another Prisma call.
|
|
495
|
-
private async ensurePermission(
|
|
496
|
-
|
|
722
|
+
private async ensurePermission(
|
|
723
|
+
slug: string,
|
|
724
|
+
actorUserId: string | null,
|
|
725
|
+
): Promise<{ id: bigint }> {
|
|
726
|
+
const existing = await this.prisma.permission.findUnique({
|
|
727
|
+
where: { slug },
|
|
728
|
+
select: { id: true },
|
|
729
|
+
});
|
|
497
730
|
return (
|
|
498
731
|
existing ??
|
|
499
732
|
(await this.prisma.permission.create({
|
|
500
|
-
data: {
|
|
733
|
+
data: {
|
|
734
|
+
slug,
|
|
735
|
+
name: slug,
|
|
736
|
+
displayName: slug,
|
|
737
|
+
group: "Custom",
|
|
738
|
+
createdBy: toIdOrNull(actorUserId),
|
|
739
|
+
updatedBy: toIdOrNull(actorUserId),
|
|
740
|
+
},
|
|
501
741
|
select: { id: true },
|
|
502
742
|
}))
|
|
503
743
|
);
|