@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,10 +1,15 @@
|
|
|
1
1
|
import { resolvePermissions } from "@/lib/auth/core/rbac.js";
|
|
2
|
-
import { Prisma, PrismaClient } from "
|
|
3
|
-
import type { AuthzContext } from "
|
|
4
|
-
import { HttpError } from "
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import { Prisma, PrismaClient } from "../../generated/prisma/client.js";
|
|
3
|
+
import type { AuthzContext } from "../middleware/authz.middleware.js";
|
|
4
|
+
import { HttpError } from "../http-error.js";
|
|
5
|
+
import {
|
|
6
|
+
buildPageMeta,
|
|
7
|
+
normalizeLimit,
|
|
8
|
+
normalizePage,
|
|
9
|
+
type Paginated,
|
|
10
|
+
} from "../pagination.js";
|
|
11
|
+
import { PermissionCache } from "../permission-cache.js";
|
|
12
|
+
import { toId, toIdOrNull } from "../id.helper.js";
|
|
8
13
|
|
|
9
14
|
export interface MemberSummary {
|
|
10
15
|
memberId: string;
|
|
@@ -36,10 +41,15 @@ export interface MemberListFilter {
|
|
|
36
41
|
|
|
37
42
|
export type MemberListResult = Paginated<MemberSummary>;
|
|
38
43
|
|
|
39
|
-
const MEMBER_INCLUDE = {
|
|
44
|
+
const MEMBER_INCLUDE = {
|
|
45
|
+
user: true,
|
|
46
|
+
roles: { select: { role: { select: { slug: true } } } },
|
|
47
|
+
} as const;
|
|
40
48
|
|
|
41
49
|
/** A `WorkspaceMember` row as `findUnique`/`findMany` return it, joined with its user and roles. */
|
|
42
|
-
export type MemberRow = Prisma.WorkspaceMemberGetPayload<{
|
|
50
|
+
export type MemberRow = Prisma.WorkspaceMemberGetPayload<{
|
|
51
|
+
include: typeof MEMBER_INCLUDE;
|
|
52
|
+
}>;
|
|
43
53
|
|
|
44
54
|
/** Shapes a raw row into the wire DTO. Exported so `listMembers`'s collection can be shaped by the caller. */
|
|
45
55
|
export function toMemberSummary(row: MemberRow): MemberSummary {
|
|
@@ -111,7 +121,14 @@ const PERMISSION_SELECT = {
|
|
|
111
121
|
isActive: true,
|
|
112
122
|
} as const;
|
|
113
123
|
|
|
114
|
-
const ROLE_SELECT = {
|
|
124
|
+
const ROLE_SELECT = {
|
|
125
|
+
id: true,
|
|
126
|
+
slug: true,
|
|
127
|
+
name: true,
|
|
128
|
+
displayName: true,
|
|
129
|
+
isDefault: true,
|
|
130
|
+
isActive: true,
|
|
131
|
+
} as const;
|
|
115
132
|
|
|
116
133
|
/**
|
|
117
134
|
* The cache subject in this variant: authorization belongs to a membership, so the key has to be
|
|
@@ -122,7 +139,8 @@ const ROLE_SELECT = { id: true, slug: true, name: true, displayName: true, isDef
|
|
|
122
139
|
* between the two one-directional — the guard needs this repository at runtime, so the repository
|
|
123
140
|
* must not need the guard.
|
|
124
141
|
*/
|
|
125
|
-
export const memberCacheKey = (userId: string, workspaceId: string) =>
|
|
142
|
+
export const memberCacheKey = (userId: string, workspaceId: string) =>
|
|
143
|
+
`${userId}:${workspaceId}`;
|
|
126
144
|
|
|
127
145
|
export class RbacRepository {
|
|
128
146
|
constructor(
|
|
@@ -145,8 +163,15 @@ export class RbacRepository {
|
|
|
145
163
|
// Takes bigint directly: its one caller (setMemberRoles) already has the membership's id in
|
|
146
164
|
// hand from its own Prisma lookup, so there's nothing to parse here.
|
|
147
165
|
private async invalidateMemberById(memberId: bigint): Promise<void> {
|
|
148
|
-
const member = await this.prisma.workspaceMember.findUnique({
|
|
149
|
-
|
|
166
|
+
const member = await this.prisma.workspaceMember.findUnique({
|
|
167
|
+
where: { id: memberId },
|
|
168
|
+
select: { userId: true, workspaceId: true },
|
|
169
|
+
});
|
|
170
|
+
if (member)
|
|
171
|
+
await this.invalidateMember(
|
|
172
|
+
member.userId.toString(),
|
|
173
|
+
member.workspaceId.toString(),
|
|
174
|
+
);
|
|
150
175
|
}
|
|
151
176
|
|
|
152
177
|
/**
|
|
@@ -164,7 +189,10 @@ export class RbacRepository {
|
|
|
164
189
|
* `isActive: false` on a role or a permission takes it out of the answer, which is what makes
|
|
165
190
|
* deactivating either one enforcement-changing without unpicking any grant.
|
|
166
191
|
*/
|
|
167
|
-
async resolveAuthzContext(
|
|
192
|
+
async resolveAuthzContext(
|
|
193
|
+
userId: string,
|
|
194
|
+
workspaceId: string,
|
|
195
|
+
): Promise<AuthzContext | null> {
|
|
168
196
|
// workspaceId arrives straight from the untrusted `X-Workspace-Id` header — a value that
|
|
169
197
|
// isn't a valid bigint is exactly as "not a member" as one that parses but names no
|
|
170
198
|
// membership, so it must not throw and 500 the request.
|
|
@@ -178,7 +206,10 @@ export class RbacRepository {
|
|
|
178
206
|
}
|
|
179
207
|
|
|
180
208
|
const member = await this.prisma.workspaceMember.findUnique({
|
|
181
|
-
where: {
|
|
209
|
+
where: {
|
|
210
|
+
userId_workspaceId: { userId: userIdBig, workspaceId: workspaceIdBig },
|
|
211
|
+
user: { isDeleted: false },
|
|
212
|
+
},
|
|
182
213
|
select: {
|
|
183
214
|
id: true,
|
|
184
215
|
roles: {
|
|
@@ -187,18 +218,28 @@ export class RbacRepository {
|
|
|
187
218
|
role: {
|
|
188
219
|
select: {
|
|
189
220
|
slug: true,
|
|
190
|
-
permissions: {
|
|
221
|
+
permissions: {
|
|
222
|
+
where: { permission: { isActive: true, isDeleted: false } },
|
|
223
|
+
select: { permission: { select: { slug: true } } },
|
|
224
|
+
},
|
|
191
225
|
},
|
|
192
226
|
},
|
|
193
227
|
},
|
|
194
228
|
},
|
|
195
|
-
permissions: {
|
|
229
|
+
permissions: {
|
|
230
|
+
where: { permission: { isActive: true, isDeleted: false } },
|
|
231
|
+
select: { permission: { select: { slug: true } } },
|
|
232
|
+
},
|
|
196
233
|
},
|
|
197
234
|
});
|
|
198
235
|
if (!member) return null;
|
|
199
236
|
|
|
200
|
-
const rolePermissions = member.roles.flatMap((roleMember) =>
|
|
201
|
-
|
|
237
|
+
const rolePermissions = member.roles.flatMap((roleMember) =>
|
|
238
|
+
roleMember.role.permissions.map((pr) => pr.permission.slug),
|
|
239
|
+
);
|
|
240
|
+
const directPermissions = member.permissions.map(
|
|
241
|
+
(pm) => pm.permission.slug,
|
|
242
|
+
);
|
|
202
243
|
|
|
203
244
|
return {
|
|
204
245
|
workspaceId,
|
|
@@ -214,12 +255,21 @@ export class RbacRepository {
|
|
|
214
255
|
* caller feeds the id straight into another Prisma call, and the one external caller
|
|
215
256
|
* (AuthService) discards the result.
|
|
216
257
|
*/
|
|
217
|
-
async requireMember(
|
|
258
|
+
async requireMember(
|
|
259
|
+
workspaceId: string,
|
|
260
|
+
userId: string,
|
|
261
|
+
): Promise<{ id: bigint }> {
|
|
218
262
|
const member = await this.prisma.workspaceMember.findUnique({
|
|
219
|
-
where: {
|
|
263
|
+
where: {
|
|
264
|
+
userId_workspaceId: {
|
|
265
|
+
userId: toId(userId),
|
|
266
|
+
workspaceId: toId(workspaceId),
|
|
267
|
+
},
|
|
268
|
+
},
|
|
220
269
|
select: { id: true },
|
|
221
270
|
});
|
|
222
|
-
if (!member)
|
|
271
|
+
if (!member)
|
|
272
|
+
throw new HttpError(404, "user is not a member of this workspace");
|
|
223
273
|
return member;
|
|
224
274
|
}
|
|
225
275
|
|
|
@@ -227,13 +277,21 @@ export class RbacRepository {
|
|
|
227
277
|
* Newest-first, page-paginated; `search` matches an email substring. Returns raw rows —
|
|
228
278
|
* shaping the collection is the caller's job, see `toMemberSummary`.
|
|
229
279
|
*/
|
|
230
|
-
async listMembers(
|
|
280
|
+
async listMembers(
|
|
281
|
+
workspaceId: string,
|
|
282
|
+
filter: MemberListFilter = {},
|
|
283
|
+
): Promise<Paginated<MemberRow>> {
|
|
231
284
|
const page = normalizePage(filter.page);
|
|
232
285
|
const limit = normalizeLimit(filter.limit);
|
|
233
286
|
const workspaceIdBig = toId(workspaceId);
|
|
234
287
|
const where = {
|
|
235
288
|
workspaceId: workspaceIdBig,
|
|
236
|
-
user: {
|
|
289
|
+
user: {
|
|
290
|
+
isDeleted: false,
|
|
291
|
+
email: filter.search
|
|
292
|
+
? { contains: filter.search, mode: "insensitive" as const }
|
|
293
|
+
: undefined,
|
|
294
|
+
},
|
|
237
295
|
};
|
|
238
296
|
|
|
239
297
|
const [rows, total] = await this.prisma.$transaction([
|
|
@@ -268,11 +326,21 @@ export class RbacRepository {
|
|
|
268
326
|
async updateMember(
|
|
269
327
|
workspaceId: string,
|
|
270
328
|
userId: string,
|
|
271
|
-
input: {
|
|
329
|
+
input: {
|
|
330
|
+
firstName?: string | null;
|
|
331
|
+
lastName?: string | null;
|
|
332
|
+
displayName?: string | null;
|
|
333
|
+
phone?: string | null;
|
|
334
|
+
username?: string | null;
|
|
335
|
+
photo?: string | null;
|
|
336
|
+
},
|
|
272
337
|
actorUserId: string | null,
|
|
273
338
|
): Promise<MemberSummary> {
|
|
274
339
|
await this.requireMember(workspaceId, userId);
|
|
275
|
-
await this.prisma.user.update({
|
|
340
|
+
await this.prisma.user.update({
|
|
341
|
+
where: { id: toId(userId) },
|
|
342
|
+
data: { ...input, updatedBy: toIdOrNull(actorUserId) },
|
|
343
|
+
});
|
|
276
344
|
return this.getMember(workspaceId, userId);
|
|
277
345
|
}
|
|
278
346
|
|
|
@@ -280,11 +348,21 @@ export class RbacRepository {
|
|
|
280
348
|
// pattern, and `block`'s existing precedent of a workspace admin taking a global action on a
|
|
281
349
|
// member's account — this disables the account across every workspace it belongs to, not just
|
|
282
350
|
// this one, which is why it's gated the same way `block` is.
|
|
283
|
-
async deleteMember(
|
|
351
|
+
async deleteMember(
|
|
352
|
+
workspaceId: string,
|
|
353
|
+
userId: string,
|
|
354
|
+
actorUserId: string | null,
|
|
355
|
+
reason?: string,
|
|
356
|
+
): Promise<void> {
|
|
284
357
|
await this.requireMember(workspaceId, userId);
|
|
285
358
|
await this.prisma.user.update({
|
|
286
359
|
where: { id: toId(userId) },
|
|
287
|
-
data: {
|
|
360
|
+
data: {
|
|
361
|
+
isDeleted: true,
|
|
362
|
+
deletedAt: new Date(),
|
|
363
|
+
deletedBy: toIdOrNull(actorUserId),
|
|
364
|
+
deletedReason: reason ?? null,
|
|
365
|
+
},
|
|
288
366
|
});
|
|
289
367
|
await this.invalidateMember(userId, workspaceId);
|
|
290
368
|
}
|
|
@@ -292,13 +370,22 @@ export class RbacRepository {
|
|
|
292
370
|
async updateRole(
|
|
293
371
|
workspaceId: string,
|
|
294
372
|
roleId: string,
|
|
295
|
-
input: {
|
|
373
|
+
input: {
|
|
374
|
+
name?: string;
|
|
375
|
+
displayName?: string;
|
|
376
|
+
description?: string | null;
|
|
377
|
+
isActive?: boolean;
|
|
378
|
+
},
|
|
296
379
|
actorUserId: string | null,
|
|
297
380
|
): Promise<RoleSummary> {
|
|
298
381
|
const workspaceIdBig = toId(workspaceId);
|
|
299
382
|
const roleIdBig = toId(roleId);
|
|
300
|
-
const existing = await this.prisma.role.findUnique({
|
|
301
|
-
|
|
383
|
+
const existing = await this.prisma.role.findUnique({
|
|
384
|
+
where: { id: roleIdBig, isDeleted: false },
|
|
385
|
+
select: { id: true, workspaceId: true },
|
|
386
|
+
});
|
|
387
|
+
if (!existing || existing.workspaceId !== workspaceIdBig)
|
|
388
|
+
throw new HttpError(404, "role not found in this workspace");
|
|
302
389
|
const role = await this.prisma.role.update({
|
|
303
390
|
where: { id: roleIdBig },
|
|
304
391
|
data: { ...input, updatedBy: toIdOrNull(actorUserId) },
|
|
@@ -310,14 +397,28 @@ export class RbacRepository {
|
|
|
310
397
|
|
|
311
398
|
// Soft-delete. `role_member`/`permission_role` rows pointing at it are left in place — the
|
|
312
399
|
// role simply stops being resolved by `resolveAuthzContext`/`listRoles`.
|
|
313
|
-
async deleteRole(
|
|
400
|
+
async deleteRole(
|
|
401
|
+
workspaceId: string,
|
|
402
|
+
roleId: string,
|
|
403
|
+
actorUserId: string | null,
|
|
404
|
+
reason?: string,
|
|
405
|
+
): Promise<void> {
|
|
314
406
|
const workspaceIdBig = toId(workspaceId);
|
|
315
407
|
const roleIdBig = toId(roleId);
|
|
316
|
-
const existing = await this.prisma.role.findUnique({
|
|
317
|
-
|
|
408
|
+
const existing = await this.prisma.role.findUnique({
|
|
409
|
+
where: { id: roleIdBig, isDeleted: false },
|
|
410
|
+
select: { id: true, workspaceId: true },
|
|
411
|
+
});
|
|
412
|
+
if (!existing || existing.workspaceId !== workspaceIdBig)
|
|
413
|
+
throw new HttpError(404, "role not found in this workspace");
|
|
318
414
|
await this.prisma.role.update({
|
|
319
415
|
where: { id: roleIdBig },
|
|
320
|
-
data: {
|
|
416
|
+
data: {
|
|
417
|
+
isDeleted: true,
|
|
418
|
+
deletedAt: new Date(),
|
|
419
|
+
deletedBy: toIdOrNull(actorUserId),
|
|
420
|
+
deletedReason: reason ?? null,
|
|
421
|
+
},
|
|
321
422
|
});
|
|
322
423
|
await this.cache.invalidatePolicy();
|
|
323
424
|
}
|
|
@@ -333,7 +434,12 @@ export class RbacRepository {
|
|
|
333
434
|
const rows = await this.prisma.permission.findMany({
|
|
334
435
|
where: { isDeleted: false },
|
|
335
436
|
select: PERMISSION_SELECT,
|
|
336
|
-
orderBy: [
|
|
437
|
+
orderBy: [
|
|
438
|
+
{ groupOrder: "asc" },
|
|
439
|
+
{ group: "asc" },
|
|
440
|
+
{ order: "asc" },
|
|
441
|
+
{ slug: "asc" },
|
|
442
|
+
],
|
|
337
443
|
});
|
|
338
444
|
return rows.map((row) => ({ ...row, id: row.id.toString() }));
|
|
339
445
|
}
|
|
@@ -347,7 +453,10 @@ export class RbacRepository {
|
|
|
347
453
|
* Upserted on `slug`, which stays the stable identifier grants and revocations use, so renaming
|
|
348
454
|
* `name`/`displayName` never breaks a grant.
|
|
349
455
|
*/
|
|
350
|
-
async upsertPermission(
|
|
456
|
+
async upsertPermission(
|
|
457
|
+
input: PermissionInput,
|
|
458
|
+
actorUserId: string | null,
|
|
459
|
+
): Promise<PermissionSummary> {
|
|
351
460
|
const shared = {
|
|
352
461
|
name: input.name,
|
|
353
462
|
displayName: input.displayName,
|
|
@@ -389,7 +498,12 @@ export class RbacRepository {
|
|
|
389
498
|
|
|
390
499
|
async createRole(
|
|
391
500
|
workspaceId: string,
|
|
392
|
-
input: {
|
|
501
|
+
input: {
|
|
502
|
+
slug: string;
|
|
503
|
+
name?: string;
|
|
504
|
+
displayName?: string;
|
|
505
|
+
description?: string | null;
|
|
506
|
+
},
|
|
393
507
|
actorUserId: string | null,
|
|
394
508
|
): Promise<RoleSummary> {
|
|
395
509
|
const role = await this.prisma.role.create({
|
|
@@ -413,22 +527,37 @@ export class RbacRepository {
|
|
|
413
527
|
* be granted, and it opens nothing until a route names it — and no route shipped by this
|
|
414
528
|
* library can name it, because `@CheckAbility` only accepts slugs from `rbac.defaults.ts`.
|
|
415
529
|
*/
|
|
416
|
-
async attachPermissionToRole(
|
|
530
|
+
async attachPermissionToRole(
|
|
531
|
+
workspaceId: string,
|
|
532
|
+
roleId: string,
|
|
533
|
+
permissionSlug: string,
|
|
534
|
+
actorUserId: string | null,
|
|
535
|
+
): Promise<void> {
|
|
417
536
|
const workspaceIdBig = toId(workspaceId);
|
|
418
537
|
const roleIdBig = toId(roleId);
|
|
419
|
-
const role = await this.prisma.role.findUnique({
|
|
420
|
-
|
|
538
|
+
const role = await this.prisma.role.findUnique({
|
|
539
|
+
where: { id: roleIdBig, isDeleted: false },
|
|
540
|
+
select: { id: true, workspaceId: true },
|
|
541
|
+
});
|
|
542
|
+
if (!role || role.workspaceId !== workspaceIdBig)
|
|
543
|
+
throw new HttpError(404, "role not found in this workspace");
|
|
421
544
|
|
|
422
545
|
const permission = await this.ensurePermission(permissionSlug, actorUserId);
|
|
423
546
|
await this.prisma.permissionRole.upsert({
|
|
424
|
-
where: {
|
|
547
|
+
where: {
|
|
548
|
+
permissionId_roleId: { permissionId: permission.id, roleId: roleIdBig },
|
|
549
|
+
},
|
|
425
550
|
create: { permissionId: permission.id, roleId: roleIdBig },
|
|
426
551
|
update: {},
|
|
427
552
|
});
|
|
428
553
|
await this.cache.invalidatePolicy(); // every membership holding this role is affected
|
|
429
554
|
}
|
|
430
555
|
|
|
431
|
-
async assignRoleToMember(
|
|
556
|
+
async assignRoleToMember(
|
|
557
|
+
workspaceId: string,
|
|
558
|
+
userId: string,
|
|
559
|
+
roleSlug: string,
|
|
560
|
+
): Promise<void> {
|
|
432
561
|
const role = await this.requireRole(workspaceId, roleSlug);
|
|
433
562
|
const member = await this.requireMember(workspaceId, userId);
|
|
434
563
|
await this.prisma.roleMember.upsert({
|
|
@@ -439,32 +568,64 @@ export class RbacRepository {
|
|
|
439
568
|
await this.invalidateMember(userId, workspaceId);
|
|
440
569
|
}
|
|
441
570
|
|
|
442
|
-
async revokeRoleFromMember(
|
|
571
|
+
async revokeRoleFromMember(
|
|
572
|
+
workspaceId: string,
|
|
573
|
+
userId: string,
|
|
574
|
+
roleSlug: string,
|
|
575
|
+
): Promise<void> {
|
|
443
576
|
const member = await this.requireMember(workspaceId, userId);
|
|
444
|
-
const role = await this.prisma.role.findUnique({
|
|
577
|
+
const role = await this.prisma.role.findUnique({
|
|
578
|
+
where: {
|
|
579
|
+
workspaceId_slug: { workspaceId: toId(workspaceId), slug: roleSlug },
|
|
580
|
+
},
|
|
581
|
+
select: { id: true },
|
|
582
|
+
});
|
|
445
583
|
if (!role) return;
|
|
446
|
-
await this.prisma.roleMember.deleteMany({
|
|
584
|
+
await this.prisma.roleMember.deleteMany({
|
|
585
|
+
where: { memberId: member.id, roleId: role.id },
|
|
586
|
+
});
|
|
447
587
|
await this.invalidateMember(userId, workspaceId);
|
|
448
588
|
}
|
|
449
589
|
|
|
450
590
|
/** Replaces the whole set — the "set a member's roles" operation, as opposed to assign/revoke one at a time. */
|
|
451
|
-
async setMemberRoles(
|
|
591
|
+
async setMemberRoles(
|
|
592
|
+
workspaceId: string,
|
|
593
|
+
memberId: string,
|
|
594
|
+
roleSlugs: string[],
|
|
595
|
+
): Promise<{ memberId: string; roles: string[] }> {
|
|
452
596
|
const workspaceIdBig = toId(workspaceId);
|
|
453
597
|
const memberIdBig = toId(memberId);
|
|
454
|
-
const member = await this.prisma.workspaceMember.findUnique({
|
|
455
|
-
|
|
598
|
+
const member = await this.prisma.workspaceMember.findUnique({
|
|
599
|
+
where: { id: memberIdBig },
|
|
600
|
+
select: { id: true, workspaceId: true },
|
|
601
|
+
});
|
|
602
|
+
if (!member || member.workspaceId !== workspaceIdBig)
|
|
603
|
+
throw new HttpError(404, "member not found in this workspace");
|
|
456
604
|
|
|
457
605
|
const roles = await this.prisma.role.findMany({
|
|
458
|
-
where: {
|
|
606
|
+
where: {
|
|
607
|
+
workspaceId: workspaceIdBig,
|
|
608
|
+
slug: { in: roleSlugs },
|
|
609
|
+
isDeleted: false,
|
|
610
|
+
},
|
|
459
611
|
select: { id: true, slug: true },
|
|
460
612
|
});
|
|
461
|
-
const unknown = roleSlugs.filter(
|
|
462
|
-
|
|
613
|
+
const unknown = roleSlugs.filter(
|
|
614
|
+
(slug) => !roles.some((role) => role.slug === slug),
|
|
615
|
+
);
|
|
616
|
+
if (unknown.length)
|
|
617
|
+
throw new HttpError(
|
|
618
|
+
404,
|
|
619
|
+
`role(s) not defined in this workspace: ${unknown.join(", ")}`,
|
|
620
|
+
);
|
|
463
621
|
|
|
464
622
|
// Replace, in one transaction: a member must never be briefly role-less to a concurrent request.
|
|
465
623
|
await this.prisma.$transaction([
|
|
466
624
|
this.prisma.roleMember.deleteMany({ where: { memberId: memberIdBig } }),
|
|
467
|
-
this.prisma.roleMember.createMany({
|
|
625
|
+
this.prisma.roleMember.createMany({
|
|
626
|
+
data: roles.map((role) => ({ memberId: memberIdBig, roleId: role.id })),
|
|
627
|
+
skipDuplicates: true,
|
|
628
|
+
}),
|
|
468
629
|
]);
|
|
469
630
|
await this.invalidateMemberById(memberIdBig);
|
|
470
631
|
return { memberId, roles: roles.map((role) => role.slug).sort() };
|
|
@@ -477,36 +638,70 @@ export class RbacRepository {
|
|
|
477
638
|
*/
|
|
478
639
|
async defaultRoleIdsFor(workspaceId: string): Promise<bigint[]> {
|
|
479
640
|
const roles = await this.prisma.role.findMany({
|
|
480
|
-
where: {
|
|
641
|
+
where: {
|
|
642
|
+
workspaceId: toId(workspaceId),
|
|
643
|
+
isDefault: true,
|
|
644
|
+
isActive: true,
|
|
645
|
+
isDeleted: false,
|
|
646
|
+
},
|
|
481
647
|
select: { id: true },
|
|
482
648
|
});
|
|
483
649
|
return roles.map((role) => role.id);
|
|
484
650
|
}
|
|
485
651
|
|
|
486
|
-
async grantPermissionToMember(
|
|
652
|
+
async grantPermissionToMember(
|
|
653
|
+
workspaceId: string,
|
|
654
|
+
userId: string,
|
|
655
|
+
permissionSlug: string,
|
|
656
|
+
actorUserId: string | null,
|
|
657
|
+
): Promise<void> {
|
|
487
658
|
const member = await this.requireMember(workspaceId, userId);
|
|
488
659
|
const permission = await this.ensurePermission(permissionSlug, actorUserId);
|
|
489
660
|
await this.prisma.permissionMember.upsert({
|
|
490
|
-
where: {
|
|
661
|
+
where: {
|
|
662
|
+
memberId_permissionId: {
|
|
663
|
+
memberId: member.id,
|
|
664
|
+
permissionId: permission.id,
|
|
665
|
+
},
|
|
666
|
+
},
|
|
491
667
|
create: { memberId: member.id, permissionId: permission.id },
|
|
492
668
|
update: {},
|
|
493
669
|
});
|
|
494
670
|
await this.invalidateMember(userId, workspaceId);
|
|
495
671
|
}
|
|
496
672
|
|
|
497
|
-
async revokePermissionFromMember(
|
|
673
|
+
async revokePermissionFromMember(
|
|
674
|
+
workspaceId: string,
|
|
675
|
+
userId: string,
|
|
676
|
+
permissionSlug: string,
|
|
677
|
+
): Promise<void> {
|
|
498
678
|
const member = await this.requireMember(workspaceId, userId);
|
|
499
|
-
const permission = await this.prisma.permission.findUnique({
|
|
679
|
+
const permission = await this.prisma.permission.findUnique({
|
|
680
|
+
where: { slug: permissionSlug },
|
|
681
|
+
select: { id: true },
|
|
682
|
+
});
|
|
500
683
|
if (!permission) return;
|
|
501
|
-
await this.prisma.permissionMember.deleteMany({
|
|
684
|
+
await this.prisma.permissionMember.deleteMany({
|
|
685
|
+
where: { memberId: member.id, permissionId: permission.id },
|
|
686
|
+
});
|
|
502
687
|
await this.invalidateMember(userId, workspaceId);
|
|
503
688
|
}
|
|
504
689
|
|
|
505
690
|
// Returns bigint directly: its one caller (assignRoleToMember) feeds the id straight into
|
|
506
691
|
// another Prisma call.
|
|
507
|
-
private async requireRole(
|
|
508
|
-
|
|
509
|
-
|
|
692
|
+
private async requireRole(
|
|
693
|
+
workspaceId: string,
|
|
694
|
+
slug: string,
|
|
695
|
+
): Promise<{ id: bigint }> {
|
|
696
|
+
const role = await this.prisma.role.findUnique({
|
|
697
|
+
where: {
|
|
698
|
+
workspaceId_slug: { workspaceId: toId(workspaceId), slug },
|
|
699
|
+
isDeleted: false,
|
|
700
|
+
},
|
|
701
|
+
select: { id: true },
|
|
702
|
+
});
|
|
703
|
+
if (!role)
|
|
704
|
+
throw new HttpError(404, `role "${slug}" not found in this workspace`);
|
|
510
705
|
return role;
|
|
511
706
|
}
|
|
512
707
|
|
|
@@ -514,12 +709,25 @@ export class RbacRepository {
|
|
|
514
709
|
* Creates the row for a slug granted before anyone defined it; leaves an existing definition
|
|
515
710
|
* alone. Returns bigint directly: both callers feed the id straight into another Prisma call.
|
|
516
711
|
*/
|
|
517
|
-
private async ensurePermission(
|
|
518
|
-
|
|
712
|
+
private async ensurePermission(
|
|
713
|
+
slug: string,
|
|
714
|
+
actorUserId: string | null,
|
|
715
|
+
): Promise<{ id: bigint }> {
|
|
716
|
+
const existing = await this.prisma.permission.findUnique({
|
|
717
|
+
where: { slug },
|
|
718
|
+
select: { id: true },
|
|
719
|
+
});
|
|
519
720
|
return (
|
|
520
721
|
existing ??
|
|
521
722
|
(await this.prisma.permission.create({
|
|
522
|
-
data: {
|
|
723
|
+
data: {
|
|
724
|
+
slug,
|
|
725
|
+
name: slug,
|
|
726
|
+
displayName: slug,
|
|
727
|
+
group: "Custom",
|
|
728
|
+
createdBy: toIdOrNull(actorUserId),
|
|
729
|
+
updatedBy: toIdOrNull(actorUserId),
|
|
730
|
+
},
|
|
523
731
|
select: { id: true },
|
|
524
732
|
}))
|
|
525
733
|
);
|