@donkeylabs/cli 2.0.15 → 2.0.16
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/src/commands/config.ts +610 -0
- package/src/commands/deploy-enhanced.ts +354 -0
- package/src/commands/deploy.ts +204 -0
- package/src/commands/init-enhanced.ts +1994 -0
- package/src/deployment/manager.ts +356 -0
- package/src/index.ts +47 -19
- package/templates/starter/.env.example +0 -44
- package/templates/starter/.gitignore.template +0 -4
- package/templates/starter/donkeylabs.config.ts +0 -6
- package/templates/starter/package.json +0 -21
- package/templates/starter/src/index.ts +0 -54
- package/templates/starter/src/plugins/stats/index.ts +0 -105
- package/templates/starter/src/routes/health/handlers/ping.ts +0 -22
- package/templates/starter/src/routes/health/index.ts +0 -19
- package/templates/starter/tsconfig.json +0 -27
- package/templates/sveltekit-app/.env.example +0 -59
- package/templates/sveltekit-app/README.md +0 -103
- package/templates/sveltekit-app/bun.lock +0 -683
- package/templates/sveltekit-app/donkeylabs.config.ts +0 -12
- package/templates/sveltekit-app/package.json +0 -38
- package/templates/sveltekit-app/src/app.css +0 -40
- package/templates/sveltekit-app/src/app.html +0 -12
- package/templates/sveltekit-app/src/hooks.server.ts +0 -4
- package/templates/sveltekit-app/src/lib/components/ui/badge/badge.svelte +0 -30
- package/templates/sveltekit-app/src/lib/components/ui/badge/index.ts +0 -3
- package/templates/sveltekit-app/src/lib/components/ui/button/button.svelte +0 -48
- package/templates/sveltekit-app/src/lib/components/ui/button/index.ts +0 -9
- package/templates/sveltekit-app/src/lib/components/ui/card/card-content.svelte +0 -18
- package/templates/sveltekit-app/src/lib/components/ui/card/card-description.svelte +0 -18
- package/templates/sveltekit-app/src/lib/components/ui/card/card-footer.svelte +0 -18
- package/templates/sveltekit-app/src/lib/components/ui/card/card-header.svelte +0 -18
- package/templates/sveltekit-app/src/lib/components/ui/card/card-title.svelte +0 -18
- package/templates/sveltekit-app/src/lib/components/ui/card/card.svelte +0 -21
- package/templates/sveltekit-app/src/lib/components/ui/card/index.ts +0 -21
- package/templates/sveltekit-app/src/lib/components/ui/index.ts +0 -4
- package/templates/sveltekit-app/src/lib/components/ui/input/index.ts +0 -2
- package/templates/sveltekit-app/src/lib/components/ui/input/input.svelte +0 -20
- package/templates/sveltekit-app/src/lib/permissions.ts +0 -213
- package/templates/sveltekit-app/src/lib/utils/index.ts +0 -6
- package/templates/sveltekit-app/src/routes/+layout.svelte +0 -8
- package/templates/sveltekit-app/src/routes/+page.server.ts +0 -25
- package/templates/sveltekit-app/src/routes/+page.svelte +0 -680
- package/templates/sveltekit-app/src/routes/workflows/+page.server.ts +0 -23
- package/templates/sveltekit-app/src/routes/workflows/+page.svelte +0 -522
- package/templates/sveltekit-app/src/server/events.ts +0 -28
- package/templates/sveltekit-app/src/server/index.ts +0 -124
- package/templates/sveltekit-app/src/server/plugins/auth/auth.test.ts +0 -377
- package/templates/sveltekit-app/src/server/plugins/auth/index.ts +0 -815
- package/templates/sveltekit-app/src/server/plugins/auth/migrations/001_create_users.ts +0 -25
- package/templates/sveltekit-app/src/server/plugins/auth/migrations/002_create_sessions.ts +0 -32
- package/templates/sveltekit-app/src/server/plugins/auth/migrations/003_create_refresh_tokens.ts +0 -33
- package/templates/sveltekit-app/src/server/plugins/auth/migrations/004_create_passkeys.ts +0 -60
- package/templates/sveltekit-app/src/server/plugins/auth/schema.ts +0 -65
- package/templates/sveltekit-app/src/server/plugins/demo/index.ts +0 -262
- package/templates/sveltekit-app/src/server/plugins/email/email.test.ts +0 -369
- package/templates/sveltekit-app/src/server/plugins/email/index.ts +0 -411
- package/templates/sveltekit-app/src/server/plugins/email/migrations/001_create_email_tokens.ts +0 -33
- package/templates/sveltekit-app/src/server/plugins/email/schema.ts +0 -24
- package/templates/sveltekit-app/src/server/plugins/permissions/index.ts +0 -1048
- package/templates/sveltekit-app/src/server/plugins/permissions/migrations/001_create_tenants.ts +0 -63
- package/templates/sveltekit-app/src/server/plugins/permissions/migrations/002_create_roles.ts +0 -90
- package/templates/sveltekit-app/src/server/plugins/permissions/migrations/003_create_resource_grants.ts +0 -50
- package/templates/sveltekit-app/src/server/plugins/permissions/permissions.test.ts +0 -566
- package/templates/sveltekit-app/src/server/plugins/permissions/schema.ts +0 -67
- package/templates/sveltekit-app/src/server/plugins/workflow-demo/index.ts +0 -198
- package/templates/sveltekit-app/src/server/routes/auth/auth.schemas.ts +0 -66
- package/templates/sveltekit-app/src/server/routes/auth/handlers/login.handler.ts +0 -18
- package/templates/sveltekit-app/src/server/routes/auth/handlers/logout.handler.ts +0 -16
- package/templates/sveltekit-app/src/server/routes/auth/handlers/me.handler.ts +0 -20
- package/templates/sveltekit-app/src/server/routes/auth/handlers/refresh.handler.ts +0 -17
- package/templates/sveltekit-app/src/server/routes/auth/handlers/register.handler.ts +0 -19
- package/templates/sveltekit-app/src/server/routes/auth/handlers/update-profile.handler.ts +0 -21
- package/templates/sveltekit-app/src/server/routes/auth/index.ts +0 -73
- package/templates/sveltekit-app/src/server/routes/demo.ts +0 -464
- package/templates/sveltekit-app/src/server/routes/example/example.schemas.ts +0 -22
- package/templates/sveltekit-app/src/server/routes/example/handlers/greet.handler.ts +0 -21
- package/templates/sveltekit-app/src/server/routes/example/index.ts +0 -28
- package/templates/sveltekit-app/src/server/routes/permissions/index.ts +0 -248
- package/templates/sveltekit-app/src/server/routes/tenants/index.ts +0 -339
- package/templates/sveltekit-app/static/robots.txt +0 -3
- package/templates/sveltekit-app/svelte.config.ts +0 -17
- package/templates/sveltekit-app/tsconfig.json +0 -20
- package/templates/sveltekit-app/vite.config.ts +0 -12
|
@@ -1,1048 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Permissions Plugin - Multi-tenant RBAC with resource-level grants
|
|
3
|
-
*
|
|
4
|
-
* Features:
|
|
5
|
-
* - Multi-tenant isolation
|
|
6
|
-
* - Role-based access control (RBAC) with inheritance
|
|
7
|
-
* - Resource-level grants (user or role can access specific resources)
|
|
8
|
-
* - Owner-based access (resource creators have full access)
|
|
9
|
-
* - Middleware for route protection
|
|
10
|
-
* - Client-side permission checking for UI
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { createPlugin, createMiddleware } from "@donkeylabs/server";
|
|
14
|
-
import type { ColumnType } from "kysely";
|
|
15
|
-
|
|
16
|
-
// =============================================================================
|
|
17
|
-
// CONFIGURATION TYPES
|
|
18
|
-
// =============================================================================
|
|
19
|
-
|
|
20
|
-
export type ResourceAction = "create" | "read" | "write" | "delete" | "admin";
|
|
21
|
-
|
|
22
|
-
export interface PermissionsConfig<
|
|
23
|
-
TPermissions extends Record<string, readonly string[]> = Record<string, readonly string[]>
|
|
24
|
-
> {
|
|
25
|
-
/**
|
|
26
|
-
* Define available permissions for autocomplete
|
|
27
|
-
* @example { documents: ["create", "read", "write"], users: ["invite", "manage"] }
|
|
28
|
-
*/
|
|
29
|
-
permissions: TPermissions;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Default roles created for new tenants
|
|
33
|
-
*/
|
|
34
|
-
defaultRoles?: Array<{
|
|
35
|
-
name: string;
|
|
36
|
-
permissions: string[];
|
|
37
|
-
isDefault?: boolean; // Auto-assign to new members
|
|
38
|
-
}>;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* How to resolve tenant context
|
|
42
|
-
* @default "header"
|
|
43
|
-
*/
|
|
44
|
-
tenantResolver?: "header" | "subdomain" | "path";
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Header name for tenant ID
|
|
48
|
-
* @default "x-tenant-id"
|
|
49
|
-
*/
|
|
50
|
-
tenantHeader?: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// =============================================================================
|
|
54
|
-
// DATABASE SCHEMA
|
|
55
|
-
// =============================================================================
|
|
56
|
-
|
|
57
|
-
interface TenantsTable {
|
|
58
|
-
id: string;
|
|
59
|
-
name: string;
|
|
60
|
-
slug: string;
|
|
61
|
-
settings: string | null; // JSON
|
|
62
|
-
created_at: ColumnType<string, string | undefined, never>;
|
|
63
|
-
updated_at: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface TenantMembersTable {
|
|
67
|
-
id: string;
|
|
68
|
-
tenant_id: string;
|
|
69
|
-
user_id: string;
|
|
70
|
-
created_at: ColumnType<string, string | undefined, never>;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface RolesTable {
|
|
74
|
-
id: string;
|
|
75
|
-
tenant_id: string | null; // null = global role
|
|
76
|
-
name: string;
|
|
77
|
-
description: string | null;
|
|
78
|
-
permissions: string; // JSON array
|
|
79
|
-
inherits_from: string | null; // role_id
|
|
80
|
-
is_default: number; // 0 or 1 - auto-assign to new members
|
|
81
|
-
created_at: ColumnType<string, string | undefined, never>;
|
|
82
|
-
updated_at: string;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
interface UserRolesTable {
|
|
86
|
-
id: string;
|
|
87
|
-
user_id: string;
|
|
88
|
-
role_id: string;
|
|
89
|
-
tenant_id: string;
|
|
90
|
-
assigned_by: string | null;
|
|
91
|
-
created_at: ColumnType<string, string | undefined, never>;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
interface ResourceGrantsTable {
|
|
95
|
-
id: string;
|
|
96
|
-
tenant_id: string;
|
|
97
|
-
resource_type: string;
|
|
98
|
-
resource_id: string;
|
|
99
|
-
grantee_type: "user" | "role";
|
|
100
|
-
grantee_id: string;
|
|
101
|
-
permissions: string; // JSON array of actions
|
|
102
|
-
granted_by: string | null;
|
|
103
|
-
created_at: ColumnType<string, string | undefined, never>;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
interface PermissionsSchema {
|
|
107
|
-
tenants: TenantsTable;
|
|
108
|
-
tenant_members: TenantMembersTable;
|
|
109
|
-
roles: RolesTable;
|
|
110
|
-
user_roles: UserRolesTable;
|
|
111
|
-
resource_grants: ResourceGrantsTable;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// =============================================================================
|
|
115
|
-
// TYPES
|
|
116
|
-
// =============================================================================
|
|
117
|
-
|
|
118
|
-
export interface Tenant {
|
|
119
|
-
id: string;
|
|
120
|
-
name: string;
|
|
121
|
-
slug: string;
|
|
122
|
-
settings: Record<string, unknown> | null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export interface Role {
|
|
126
|
-
id: string;
|
|
127
|
-
tenantId: string | null;
|
|
128
|
-
name: string;
|
|
129
|
-
description: string | null;
|
|
130
|
-
permissions: string[];
|
|
131
|
-
inheritsFrom: string | null;
|
|
132
|
-
isDefault: boolean;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export interface ResourceGrant {
|
|
136
|
-
resourceType: string;
|
|
137
|
-
resourceId: string;
|
|
138
|
-
granteeType: "user" | "role";
|
|
139
|
-
granteeId: string;
|
|
140
|
-
permissions: ResourceAction[];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export interface PermissionContext {
|
|
144
|
-
userId: string;
|
|
145
|
-
tenantId: string;
|
|
146
|
-
roles: Role[];
|
|
147
|
-
permissions: Set<string>;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// =============================================================================
|
|
151
|
-
// PLUGIN DEFINITION
|
|
152
|
-
// =============================================================================
|
|
153
|
-
|
|
154
|
-
export const permissionsPlugin = <
|
|
155
|
-
TPermissions extends Record<string, readonly string[]>
|
|
156
|
-
>(config: PermissionsConfig<TPermissions>) => {
|
|
157
|
-
// Generate permission type union for validation
|
|
158
|
-
type PermissionKey = {
|
|
159
|
-
[K in keyof TPermissions]: `${K & string}.${TPermissions[K][number]}`;
|
|
160
|
-
}[keyof TPermissions];
|
|
161
|
-
|
|
162
|
-
const factory = createPlugin
|
|
163
|
-
.withSchema<PermissionsSchema>()
|
|
164
|
-
.withConfig<PermissionsConfig<TPermissions>>()
|
|
165
|
-
.define({
|
|
166
|
-
name: "permissions",
|
|
167
|
-
|
|
168
|
-
customErrors: {
|
|
169
|
-
TenantNotFound: {
|
|
170
|
-
status: 404,
|
|
171
|
-
code: "TENANT_NOT_FOUND",
|
|
172
|
-
message: "Tenant not found",
|
|
173
|
-
},
|
|
174
|
-
TenantRequired: {
|
|
175
|
-
status: 400,
|
|
176
|
-
code: "TENANT_REQUIRED",
|
|
177
|
-
message: "Tenant context is required",
|
|
178
|
-
},
|
|
179
|
-
NotTenantMember: {
|
|
180
|
-
status: 403,
|
|
181
|
-
code: "NOT_TENANT_MEMBER",
|
|
182
|
-
message: "User is not a member of this tenant",
|
|
183
|
-
},
|
|
184
|
-
PermissionDenied: {
|
|
185
|
-
status: 403,
|
|
186
|
-
code: "PERMISSION_DENIED",
|
|
187
|
-
message: "Permission denied",
|
|
188
|
-
},
|
|
189
|
-
RoleNotFound: {
|
|
190
|
-
status: 404,
|
|
191
|
-
code: "ROLE_NOT_FOUND",
|
|
192
|
-
message: "Role not found",
|
|
193
|
-
},
|
|
194
|
-
ResourceAccessDenied: {
|
|
195
|
-
status: 403,
|
|
196
|
-
code: "RESOURCE_ACCESS_DENIED",
|
|
197
|
-
message: "Access to this resource is denied",
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
|
|
201
|
-
service: async (ctx) => {
|
|
202
|
-
const tenantHeader = config.tenantHeader || "x-tenant-id";
|
|
203
|
-
|
|
204
|
-
// =====================================================================
|
|
205
|
-
// TENANT METHODS
|
|
206
|
-
// =====================================================================
|
|
207
|
-
|
|
208
|
-
async function createTenant(data: {
|
|
209
|
-
name: string;
|
|
210
|
-
slug: string;
|
|
211
|
-
ownerId: string;
|
|
212
|
-
settings?: Record<string, unknown>;
|
|
213
|
-
}): Promise<Tenant> {
|
|
214
|
-
const id = crypto.randomUUID();
|
|
215
|
-
const now = new Date().toISOString();
|
|
216
|
-
|
|
217
|
-
await ctx.db
|
|
218
|
-
.insertInto("tenants")
|
|
219
|
-
.values({
|
|
220
|
-
id,
|
|
221
|
-
name: data.name,
|
|
222
|
-
slug: data.slug.toLowerCase(),
|
|
223
|
-
settings: data.settings ? JSON.stringify(data.settings) : null,
|
|
224
|
-
created_at: now,
|
|
225
|
-
updated_at: now,
|
|
226
|
-
})
|
|
227
|
-
.execute();
|
|
228
|
-
|
|
229
|
-
// Add owner as member
|
|
230
|
-
await ctx.db
|
|
231
|
-
.insertInto("tenant_members")
|
|
232
|
-
.values({
|
|
233
|
-
id: crypto.randomUUID(),
|
|
234
|
-
tenant_id: id,
|
|
235
|
-
user_id: data.ownerId,
|
|
236
|
-
created_at: now,
|
|
237
|
-
})
|
|
238
|
-
.execute();
|
|
239
|
-
|
|
240
|
-
// Create default roles if configured
|
|
241
|
-
if (config.defaultRoles) {
|
|
242
|
-
for (const roleDef of config.defaultRoles) {
|
|
243
|
-
const roleId = crypto.randomUUID();
|
|
244
|
-
await ctx.db
|
|
245
|
-
.insertInto("roles")
|
|
246
|
-
.values({
|
|
247
|
-
id: roleId,
|
|
248
|
-
tenant_id: id,
|
|
249
|
-
name: roleDef.name,
|
|
250
|
-
description: null,
|
|
251
|
-
permissions: JSON.stringify(roleDef.permissions),
|
|
252
|
-
inherits_from: null,
|
|
253
|
-
is_default: roleDef.isDefault ? 1 : 0,
|
|
254
|
-
created_at: now,
|
|
255
|
-
updated_at: now,
|
|
256
|
-
})
|
|
257
|
-
.execute();
|
|
258
|
-
|
|
259
|
-
// Assign default role to owner
|
|
260
|
-
if (roleDef.isDefault || roleDef.name.toLowerCase() === "admin") {
|
|
261
|
-
await ctx.db
|
|
262
|
-
.insertInto("user_roles")
|
|
263
|
-
.values({
|
|
264
|
-
id: crypto.randomUUID(),
|
|
265
|
-
user_id: data.ownerId,
|
|
266
|
-
role_id: roleId,
|
|
267
|
-
tenant_id: id,
|
|
268
|
-
assigned_by: data.ownerId,
|
|
269
|
-
created_at: now,
|
|
270
|
-
})
|
|
271
|
-
.execute();
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return {
|
|
277
|
-
id,
|
|
278
|
-
name: data.name,
|
|
279
|
-
slug: data.slug,
|
|
280
|
-
settings: data.settings || null,
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
async function getTenant(tenantId: string): Promise<Tenant | null> {
|
|
285
|
-
const row = await ctx.db
|
|
286
|
-
.selectFrom("tenants")
|
|
287
|
-
.where("id", "=", tenantId)
|
|
288
|
-
.selectAll()
|
|
289
|
-
.executeTakeFirst();
|
|
290
|
-
|
|
291
|
-
if (!row) return null;
|
|
292
|
-
|
|
293
|
-
return {
|
|
294
|
-
id: row.id,
|
|
295
|
-
name: row.name,
|
|
296
|
-
slug: row.slug,
|
|
297
|
-
settings: row.settings ? JSON.parse(row.settings) : null,
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
async function getTenantBySlug(slug: string): Promise<Tenant | null> {
|
|
302
|
-
const row = await ctx.db
|
|
303
|
-
.selectFrom("tenants")
|
|
304
|
-
.where("slug", "=", slug.toLowerCase())
|
|
305
|
-
.selectAll()
|
|
306
|
-
.executeTakeFirst();
|
|
307
|
-
|
|
308
|
-
if (!row) return null;
|
|
309
|
-
|
|
310
|
-
return {
|
|
311
|
-
id: row.id,
|
|
312
|
-
name: row.name,
|
|
313
|
-
slug: row.slug,
|
|
314
|
-
settings: row.settings ? JSON.parse(row.settings) : null,
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
async function isTenantMember(userId: string, tenantId: string): Promise<boolean> {
|
|
319
|
-
const member = await ctx.db
|
|
320
|
-
.selectFrom("tenant_members")
|
|
321
|
-
.where("user_id", "=", userId)
|
|
322
|
-
.where("tenant_id", "=", tenantId)
|
|
323
|
-
.selectAll()
|
|
324
|
-
.executeTakeFirst();
|
|
325
|
-
|
|
326
|
-
return !!member;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
async function addTenantMember(
|
|
330
|
-
tenantId: string,
|
|
331
|
-
userId: string,
|
|
332
|
-
addedBy?: string
|
|
333
|
-
): Promise<void> {
|
|
334
|
-
const now = new Date().toISOString();
|
|
335
|
-
|
|
336
|
-
await ctx.db
|
|
337
|
-
.insertInto("tenant_members")
|
|
338
|
-
.values({
|
|
339
|
-
id: crypto.randomUUID(),
|
|
340
|
-
tenant_id: tenantId,
|
|
341
|
-
user_id: userId,
|
|
342
|
-
created_at: now,
|
|
343
|
-
})
|
|
344
|
-
.execute();
|
|
345
|
-
|
|
346
|
-
// Assign default roles
|
|
347
|
-
const defaultRoles = await ctx.db
|
|
348
|
-
.selectFrom("roles")
|
|
349
|
-
.where("tenant_id", "=", tenantId)
|
|
350
|
-
.where("is_default", "=", 1)
|
|
351
|
-
.selectAll()
|
|
352
|
-
.execute();
|
|
353
|
-
|
|
354
|
-
for (const role of defaultRoles) {
|
|
355
|
-
await ctx.db
|
|
356
|
-
.insertInto("user_roles")
|
|
357
|
-
.values({
|
|
358
|
-
id: crypto.randomUUID(),
|
|
359
|
-
user_id: userId,
|
|
360
|
-
role_id: role.id,
|
|
361
|
-
tenant_id: tenantId,
|
|
362
|
-
assigned_by: addedBy || null,
|
|
363
|
-
created_at: now,
|
|
364
|
-
})
|
|
365
|
-
.execute();
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
async function removeTenantMember(tenantId: string, userId: string): Promise<void> {
|
|
370
|
-
await ctx.db
|
|
371
|
-
.deleteFrom("tenant_members")
|
|
372
|
-
.where("tenant_id", "=", tenantId)
|
|
373
|
-
.where("user_id", "=", userId)
|
|
374
|
-
.execute();
|
|
375
|
-
|
|
376
|
-
await ctx.db
|
|
377
|
-
.deleteFrom("user_roles")
|
|
378
|
-
.where("tenant_id", "=", tenantId)
|
|
379
|
-
.where("user_id", "=", userId)
|
|
380
|
-
.execute();
|
|
381
|
-
|
|
382
|
-
await ctx.db
|
|
383
|
-
.deleteFrom("resource_grants")
|
|
384
|
-
.where("tenant_id", "=", tenantId)
|
|
385
|
-
.where("grantee_type", "=", "user")
|
|
386
|
-
.where("grantee_id", "=", userId)
|
|
387
|
-
.execute();
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
async function getUserTenants(userId: string): Promise<Tenant[]> {
|
|
391
|
-
const rows = await ctx.db
|
|
392
|
-
.selectFrom("tenant_members")
|
|
393
|
-
.innerJoin("tenants", "tenants.id", "tenant_members.tenant_id")
|
|
394
|
-
.where("tenant_members.user_id", "=", userId)
|
|
395
|
-
.select([
|
|
396
|
-
"tenants.id",
|
|
397
|
-
"tenants.name",
|
|
398
|
-
"tenants.slug",
|
|
399
|
-
"tenants.settings",
|
|
400
|
-
])
|
|
401
|
-
.execute();
|
|
402
|
-
|
|
403
|
-
return rows.map((row) => ({
|
|
404
|
-
id: row.id,
|
|
405
|
-
name: row.name,
|
|
406
|
-
slug: row.slug,
|
|
407
|
-
settings: row.settings ? JSON.parse(row.settings) : null,
|
|
408
|
-
}));
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// =====================================================================
|
|
412
|
-
// ROLE METHODS
|
|
413
|
-
// =====================================================================
|
|
414
|
-
|
|
415
|
-
async function createRole(data: {
|
|
416
|
-
tenantId: string;
|
|
417
|
-
name: string;
|
|
418
|
-
description?: string;
|
|
419
|
-
permissions: string[];
|
|
420
|
-
inheritsFrom?: string;
|
|
421
|
-
isDefault?: boolean;
|
|
422
|
-
}): Promise<Role> {
|
|
423
|
-
const id = crypto.randomUUID();
|
|
424
|
-
const now = new Date().toISOString();
|
|
425
|
-
|
|
426
|
-
await ctx.db
|
|
427
|
-
.insertInto("roles")
|
|
428
|
-
.values({
|
|
429
|
-
id,
|
|
430
|
-
tenant_id: data.tenantId,
|
|
431
|
-
name: data.name,
|
|
432
|
-
description: data.description || null,
|
|
433
|
-
permissions: JSON.stringify(data.permissions),
|
|
434
|
-
inherits_from: data.inheritsFrom || null,
|
|
435
|
-
is_default: data.isDefault ? 1 : 0,
|
|
436
|
-
created_at: now,
|
|
437
|
-
updated_at: now,
|
|
438
|
-
})
|
|
439
|
-
.execute();
|
|
440
|
-
|
|
441
|
-
return {
|
|
442
|
-
id,
|
|
443
|
-
tenantId: data.tenantId,
|
|
444
|
-
name: data.name,
|
|
445
|
-
description: data.description || null,
|
|
446
|
-
permissions: data.permissions,
|
|
447
|
-
inheritsFrom: data.inheritsFrom || null,
|
|
448
|
-
isDefault: data.isDefault || false,
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
async function getRole(roleId: string): Promise<Role | null> {
|
|
453
|
-
const row = await ctx.db
|
|
454
|
-
.selectFrom("roles")
|
|
455
|
-
.where("id", "=", roleId)
|
|
456
|
-
.selectAll()
|
|
457
|
-
.executeTakeFirst();
|
|
458
|
-
|
|
459
|
-
if (!row) return null;
|
|
460
|
-
|
|
461
|
-
return {
|
|
462
|
-
id: row.id,
|
|
463
|
-
tenantId: row.tenant_id,
|
|
464
|
-
name: row.name,
|
|
465
|
-
description: row.description,
|
|
466
|
-
permissions: JSON.parse(row.permissions),
|
|
467
|
-
inheritsFrom: row.inherits_from,
|
|
468
|
-
isDefault: row.is_default === 1,
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
async function getTenantRoles(tenantId: string): Promise<Role[]> {
|
|
473
|
-
const rows = await ctx.db
|
|
474
|
-
.selectFrom("roles")
|
|
475
|
-
.where((eb) =>
|
|
476
|
-
eb.or([
|
|
477
|
-
eb("tenant_id", "=", tenantId),
|
|
478
|
-
eb("tenant_id", "is", null), // Global roles
|
|
479
|
-
])
|
|
480
|
-
)
|
|
481
|
-
.selectAll()
|
|
482
|
-
.execute();
|
|
483
|
-
|
|
484
|
-
return rows.map((row) => ({
|
|
485
|
-
id: row.id,
|
|
486
|
-
tenantId: row.tenant_id,
|
|
487
|
-
name: row.name,
|
|
488
|
-
description: row.description,
|
|
489
|
-
permissions: JSON.parse(row.permissions),
|
|
490
|
-
inheritsFrom: row.inherits_from,
|
|
491
|
-
isDefault: row.is_default === 1,
|
|
492
|
-
}));
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
async function assignRole(
|
|
496
|
-
userId: string,
|
|
497
|
-
roleId: string,
|
|
498
|
-
tenantId: string,
|
|
499
|
-
assignedBy?: string
|
|
500
|
-
): Promise<void> {
|
|
501
|
-
const existing = await ctx.db
|
|
502
|
-
.selectFrom("user_roles")
|
|
503
|
-
.where("user_id", "=", userId)
|
|
504
|
-
.where("role_id", "=", roleId)
|
|
505
|
-
.where("tenant_id", "=", tenantId)
|
|
506
|
-
.selectAll()
|
|
507
|
-
.executeTakeFirst();
|
|
508
|
-
|
|
509
|
-
if (existing) return;
|
|
510
|
-
|
|
511
|
-
await ctx.db
|
|
512
|
-
.insertInto("user_roles")
|
|
513
|
-
.values({
|
|
514
|
-
id: crypto.randomUUID(),
|
|
515
|
-
user_id: userId,
|
|
516
|
-
role_id: roleId,
|
|
517
|
-
tenant_id: tenantId,
|
|
518
|
-
assigned_by: assignedBy || null,
|
|
519
|
-
created_at: new Date().toISOString(),
|
|
520
|
-
})
|
|
521
|
-
.execute();
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
async function revokeRole(
|
|
525
|
-
userId: string,
|
|
526
|
-
roleId: string,
|
|
527
|
-
tenantId: string
|
|
528
|
-
): Promise<void> {
|
|
529
|
-
await ctx.db
|
|
530
|
-
.deleteFrom("user_roles")
|
|
531
|
-
.where("user_id", "=", userId)
|
|
532
|
-
.where("role_id", "=", roleId)
|
|
533
|
-
.where("tenant_id", "=", tenantId)
|
|
534
|
-
.execute();
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
async function getUserRoles(userId: string, tenantId: string): Promise<Role[]> {
|
|
538
|
-
const rows = await ctx.db
|
|
539
|
-
.selectFrom("user_roles")
|
|
540
|
-
.innerJoin("roles", "roles.id", "user_roles.role_id")
|
|
541
|
-
.where("user_roles.user_id", "=", userId)
|
|
542
|
-
.where("user_roles.tenant_id", "=", tenantId)
|
|
543
|
-
.select([
|
|
544
|
-
"roles.id",
|
|
545
|
-
"roles.tenant_id",
|
|
546
|
-
"roles.name",
|
|
547
|
-
"roles.description",
|
|
548
|
-
"roles.permissions",
|
|
549
|
-
"roles.inherits_from",
|
|
550
|
-
"roles.is_default",
|
|
551
|
-
])
|
|
552
|
-
.execute();
|
|
553
|
-
|
|
554
|
-
return rows.map((row) => ({
|
|
555
|
-
id: row.id,
|
|
556
|
-
tenantId: row.tenant_id,
|
|
557
|
-
name: row.name,
|
|
558
|
-
description: row.description,
|
|
559
|
-
permissions: JSON.parse(row.permissions),
|
|
560
|
-
inheritsFrom: row.inherits_from,
|
|
561
|
-
isDefault: row.is_default === 1,
|
|
562
|
-
}));
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// =====================================================================
|
|
566
|
-
// PERMISSION RESOLUTION
|
|
567
|
-
// =====================================================================
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* Get all permissions for a role, including inherited ones
|
|
571
|
-
*/
|
|
572
|
-
async function resolveRolePermissions(
|
|
573
|
-
roleId: string,
|
|
574
|
-
visited: Set<string> = new Set()
|
|
575
|
-
): Promise<Set<string>> {
|
|
576
|
-
if (visited.has(roleId)) return new Set(); // Prevent cycles
|
|
577
|
-
visited.add(roleId);
|
|
578
|
-
|
|
579
|
-
const role = await getRole(roleId);
|
|
580
|
-
if (!role) return new Set();
|
|
581
|
-
|
|
582
|
-
const permissions = new Set(role.permissions);
|
|
583
|
-
|
|
584
|
-
// Add inherited permissions
|
|
585
|
-
if (role.inheritsFrom) {
|
|
586
|
-
const inherited = await resolveRolePermissions(role.inheritsFrom, visited);
|
|
587
|
-
for (const perm of inherited) {
|
|
588
|
-
permissions.add(perm);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
return permissions;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
/**
|
|
596
|
-
* Get all static permissions for a user in a tenant
|
|
597
|
-
*/
|
|
598
|
-
async function getUserPermissions(
|
|
599
|
-
userId: string,
|
|
600
|
-
tenantId: string
|
|
601
|
-
): Promise<Set<string>> {
|
|
602
|
-
const roles = await getUserRoles(userId, tenantId);
|
|
603
|
-
const permissions = new Set<string>();
|
|
604
|
-
|
|
605
|
-
for (const role of roles) {
|
|
606
|
-
const rolePerms = await resolveRolePermissions(role.id);
|
|
607
|
-
for (const perm of rolePerms) {
|
|
608
|
-
permissions.add(perm);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
return permissions;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
/**
|
|
616
|
-
* Check if user has a static permission
|
|
617
|
-
*/
|
|
618
|
-
async function hasPermission(
|
|
619
|
-
userId: string,
|
|
620
|
-
tenantId: string,
|
|
621
|
-
permission: string
|
|
622
|
-
): Promise<boolean> {
|
|
623
|
-
const permissions = await getUserPermissions(userId, tenantId);
|
|
624
|
-
|
|
625
|
-
// Check exact match
|
|
626
|
-
if (permissions.has(permission)) return true;
|
|
627
|
-
|
|
628
|
-
// Check wildcard (e.g., "documents.*" or "*")
|
|
629
|
-
if (permissions.has("*")) return true;
|
|
630
|
-
|
|
631
|
-
const [resource] = permission.split(".");
|
|
632
|
-
if (permissions.has(`${resource}.*`)) return true;
|
|
633
|
-
|
|
634
|
-
return false;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
/**
|
|
638
|
-
* Require a static permission, throw if denied
|
|
639
|
-
*/
|
|
640
|
-
async function requirePermission(
|
|
641
|
-
userId: string,
|
|
642
|
-
tenantId: string,
|
|
643
|
-
permission: string
|
|
644
|
-
): Promise<void> {
|
|
645
|
-
const has = await hasPermission(userId, tenantId, permission);
|
|
646
|
-
if (!has) {
|
|
647
|
-
throw ctx.core.errors.Forbidden(`Missing permission: ${permission}`);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// =====================================================================
|
|
652
|
-
// RESOURCE GRANTS
|
|
653
|
-
// =====================================================================
|
|
654
|
-
|
|
655
|
-
/**
|
|
656
|
-
* Grant access to a resource
|
|
657
|
-
*/
|
|
658
|
-
async function grantAccess(data: {
|
|
659
|
-
tenantId: string;
|
|
660
|
-
resourceType: string;
|
|
661
|
-
resourceId: string;
|
|
662
|
-
granteeType: "user" | "role";
|
|
663
|
-
granteeId: string;
|
|
664
|
-
permissions: ResourceAction[];
|
|
665
|
-
grantedBy?: string;
|
|
666
|
-
}): Promise<void> {
|
|
667
|
-
// Check if grant already exists
|
|
668
|
-
const existing = await ctx.db
|
|
669
|
-
.selectFrom("resource_grants")
|
|
670
|
-
.where("tenant_id", "=", data.tenantId)
|
|
671
|
-
.where("resource_type", "=", data.resourceType)
|
|
672
|
-
.where("resource_id", "=", data.resourceId)
|
|
673
|
-
.where("grantee_type", "=", data.granteeType)
|
|
674
|
-
.where("grantee_id", "=", data.granteeId)
|
|
675
|
-
.selectAll()
|
|
676
|
-
.executeTakeFirst();
|
|
677
|
-
|
|
678
|
-
if (existing) {
|
|
679
|
-
// Update permissions
|
|
680
|
-
const existingPerms: ResourceAction[] = JSON.parse(existing.permissions);
|
|
681
|
-
const merged = [...new Set([...existingPerms, ...data.permissions])];
|
|
682
|
-
|
|
683
|
-
await ctx.db
|
|
684
|
-
.updateTable("resource_grants")
|
|
685
|
-
.set({ permissions: JSON.stringify(merged) })
|
|
686
|
-
.where("id", "=", existing.id)
|
|
687
|
-
.execute();
|
|
688
|
-
} else {
|
|
689
|
-
await ctx.db
|
|
690
|
-
.insertInto("resource_grants")
|
|
691
|
-
.values({
|
|
692
|
-
id: crypto.randomUUID(),
|
|
693
|
-
tenant_id: data.tenantId,
|
|
694
|
-
resource_type: data.resourceType,
|
|
695
|
-
resource_id: data.resourceId,
|
|
696
|
-
grantee_type: data.granteeType,
|
|
697
|
-
grantee_id: data.granteeId,
|
|
698
|
-
permissions: JSON.stringify(data.permissions),
|
|
699
|
-
granted_by: data.grantedBy || null,
|
|
700
|
-
created_at: new Date().toISOString(),
|
|
701
|
-
})
|
|
702
|
-
.execute();
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
/**
|
|
707
|
-
* Revoke access to a resource
|
|
708
|
-
*/
|
|
709
|
-
async function revokeAccess(data: {
|
|
710
|
-
tenantId: string;
|
|
711
|
-
resourceType: string;
|
|
712
|
-
resourceId: string;
|
|
713
|
-
granteeType: "user" | "role";
|
|
714
|
-
granteeId: string;
|
|
715
|
-
permissions?: ResourceAction[]; // If not provided, revokes all
|
|
716
|
-
}): Promise<void> {
|
|
717
|
-
if (!data.permissions) {
|
|
718
|
-
await ctx.db
|
|
719
|
-
.deleteFrom("resource_grants")
|
|
720
|
-
.where("tenant_id", "=", data.tenantId)
|
|
721
|
-
.where("resource_type", "=", data.resourceType)
|
|
722
|
-
.where("resource_id", "=", data.resourceId)
|
|
723
|
-
.where("grantee_type", "=", data.granteeType)
|
|
724
|
-
.where("grantee_id", "=", data.granteeId)
|
|
725
|
-
.execute();
|
|
726
|
-
} else {
|
|
727
|
-
const existing = await ctx.db
|
|
728
|
-
.selectFrom("resource_grants")
|
|
729
|
-
.where("tenant_id", "=", data.tenantId)
|
|
730
|
-
.where("resource_type", "=", data.resourceType)
|
|
731
|
-
.where("resource_id", "=", data.resourceId)
|
|
732
|
-
.where("grantee_type", "=", data.granteeType)
|
|
733
|
-
.where("grantee_id", "=", data.granteeId)
|
|
734
|
-
.selectAll()
|
|
735
|
-
.executeTakeFirst();
|
|
736
|
-
|
|
737
|
-
if (existing) {
|
|
738
|
-
const existingPerms: ResourceAction[] = JSON.parse(existing.permissions);
|
|
739
|
-
const remaining = existingPerms.filter((p) => !data.permissions!.includes(p));
|
|
740
|
-
|
|
741
|
-
if (remaining.length === 0) {
|
|
742
|
-
await ctx.db
|
|
743
|
-
.deleteFrom("resource_grants")
|
|
744
|
-
.where("id", "=", existing.id)
|
|
745
|
-
.execute();
|
|
746
|
-
} else {
|
|
747
|
-
await ctx.db
|
|
748
|
-
.updateTable("resource_grants")
|
|
749
|
-
.set({ permissions: JSON.stringify(remaining) })
|
|
750
|
-
.where("id", "=", existing.id)
|
|
751
|
-
.execute();
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
/**
|
|
758
|
-
* Check if user can access a resource
|
|
759
|
-
*/
|
|
760
|
-
async function canAccess(
|
|
761
|
-
userId: string,
|
|
762
|
-
tenantId: string,
|
|
763
|
-
resourceType: string,
|
|
764
|
-
resourceId: string,
|
|
765
|
-
action: ResourceAction,
|
|
766
|
-
ownerId?: string // If provided, owner check is performed
|
|
767
|
-
): Promise<boolean> {
|
|
768
|
-
// 1. Owner check - owners have full access
|
|
769
|
-
if (ownerId && ownerId === userId) {
|
|
770
|
-
return true;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// 2. Admin permission check
|
|
774
|
-
const hasAdmin = await hasPermission(userId, tenantId, `${resourceType}.admin`);
|
|
775
|
-
if (hasAdmin) return true;
|
|
776
|
-
|
|
777
|
-
// 3. Direct user grant
|
|
778
|
-
const userGrant = await ctx.db
|
|
779
|
-
.selectFrom("resource_grants")
|
|
780
|
-
.where("tenant_id", "=", tenantId)
|
|
781
|
-
.where("resource_type", "=", resourceType)
|
|
782
|
-
.where("resource_id", "=", resourceId)
|
|
783
|
-
.where("grantee_type", "=", "user")
|
|
784
|
-
.where("grantee_id", "=", userId)
|
|
785
|
-
.selectAll()
|
|
786
|
-
.executeTakeFirst();
|
|
787
|
-
|
|
788
|
-
if (userGrant) {
|
|
789
|
-
const perms: ResourceAction[] = JSON.parse(userGrant.permissions);
|
|
790
|
-
if (perms.includes(action) || perms.includes("admin")) {
|
|
791
|
-
return true;
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// 4. Role grant
|
|
796
|
-
const userRoles = await getUserRoles(userId, tenantId);
|
|
797
|
-
for (const role of userRoles) {
|
|
798
|
-
const roleGrant = await ctx.db
|
|
799
|
-
.selectFrom("resource_grants")
|
|
800
|
-
.where("tenant_id", "=", tenantId)
|
|
801
|
-
.where("resource_type", "=", resourceType)
|
|
802
|
-
.where("resource_id", "=", resourceId)
|
|
803
|
-
.where("grantee_type", "=", "role")
|
|
804
|
-
.where("grantee_id", "=", role.id)
|
|
805
|
-
.selectAll()
|
|
806
|
-
.executeTakeFirst();
|
|
807
|
-
|
|
808
|
-
if (roleGrant) {
|
|
809
|
-
const perms: ResourceAction[] = JSON.parse(roleGrant.permissions);
|
|
810
|
-
if (perms.includes(action) || perms.includes("admin")) {
|
|
811
|
-
return true;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
return false;
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
/**
|
|
820
|
-
* Require access to a resource, throw if denied
|
|
821
|
-
*/
|
|
822
|
-
async function requireAccess(
|
|
823
|
-
userId: string,
|
|
824
|
-
tenantId: string,
|
|
825
|
-
resourceType: string,
|
|
826
|
-
resourceId: string,
|
|
827
|
-
action: ResourceAction,
|
|
828
|
-
ownerId?: string
|
|
829
|
-
): Promise<void> {
|
|
830
|
-
const can = await canAccess(userId, tenantId, resourceType, resourceId, action, ownerId);
|
|
831
|
-
if (!can) {
|
|
832
|
-
throw ctx.core.errors.Forbidden(
|
|
833
|
-
`Cannot ${action} ${resourceType}:${resourceId}`
|
|
834
|
-
);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
/**
|
|
839
|
-
* Get all grants for a resource
|
|
840
|
-
*/
|
|
841
|
-
async function getResourceGrants(
|
|
842
|
-
tenantId: string,
|
|
843
|
-
resourceType: string,
|
|
844
|
-
resourceId: string
|
|
845
|
-
): Promise<ResourceGrant[]> {
|
|
846
|
-
const rows = await ctx.db
|
|
847
|
-
.selectFrom("resource_grants")
|
|
848
|
-
.where("tenant_id", "=", tenantId)
|
|
849
|
-
.where("resource_type", "=", resourceType)
|
|
850
|
-
.where("resource_id", "=", resourceId)
|
|
851
|
-
.selectAll()
|
|
852
|
-
.execute();
|
|
853
|
-
|
|
854
|
-
return rows.map((row) => ({
|
|
855
|
-
resourceType: row.resource_type,
|
|
856
|
-
resourceId: row.resource_id,
|
|
857
|
-
granteeType: row.grantee_type as "user" | "role",
|
|
858
|
-
granteeId: row.grantee_id,
|
|
859
|
-
permissions: JSON.parse(row.permissions),
|
|
860
|
-
}));
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
// =====================================================================
|
|
864
|
-
// CONTEXT BUILDER
|
|
865
|
-
// =====================================================================
|
|
866
|
-
|
|
867
|
-
/**
|
|
868
|
-
* Build permission context for a user in a tenant
|
|
869
|
-
* Useful for passing to client
|
|
870
|
-
*/
|
|
871
|
-
async function buildContext(
|
|
872
|
-
userId: string,
|
|
873
|
-
tenantId: string
|
|
874
|
-
): Promise<PermissionContext> {
|
|
875
|
-
const roles = await getUserRoles(userId, tenantId);
|
|
876
|
-
const permissions = await getUserPermissions(userId, tenantId);
|
|
877
|
-
|
|
878
|
-
return {
|
|
879
|
-
userId,
|
|
880
|
-
tenantId,
|
|
881
|
-
roles,
|
|
882
|
-
permissions,
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
/**
|
|
887
|
-
* Serialize context for client (permissions as array)
|
|
888
|
-
*/
|
|
889
|
-
async function getClientContext(
|
|
890
|
-
userId: string,
|
|
891
|
-
tenantId: string
|
|
892
|
-
): Promise<{
|
|
893
|
-
tenantId: string;
|
|
894
|
-
roles: Array<{ id: string; name: string }>;
|
|
895
|
-
permissions: string[];
|
|
896
|
-
}> {
|
|
897
|
-
const context = await buildContext(userId, tenantId);
|
|
898
|
-
return {
|
|
899
|
-
tenantId: context.tenantId,
|
|
900
|
-
roles: context.roles.map((r) => ({ id: r.id, name: r.name })),
|
|
901
|
-
permissions: Array.from(context.permissions),
|
|
902
|
-
};
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
// =====================================================================
|
|
906
|
-
// RETURN SERVICE
|
|
907
|
-
// =====================================================================
|
|
908
|
-
|
|
909
|
-
return {
|
|
910
|
-
// Tenant methods
|
|
911
|
-
createTenant,
|
|
912
|
-
getTenant,
|
|
913
|
-
getTenantBySlug,
|
|
914
|
-
isTenantMember,
|
|
915
|
-
addTenantMember,
|
|
916
|
-
removeTenantMember,
|
|
917
|
-
getUserTenants,
|
|
918
|
-
|
|
919
|
-
// Role methods
|
|
920
|
-
createRole,
|
|
921
|
-
getRole,
|
|
922
|
-
getTenantRoles,
|
|
923
|
-
assignRole,
|
|
924
|
-
revokeRole,
|
|
925
|
-
getUserRoles,
|
|
926
|
-
|
|
927
|
-
// Permission methods
|
|
928
|
-
hasPermission,
|
|
929
|
-
requirePermission,
|
|
930
|
-
getUserPermissions,
|
|
931
|
-
|
|
932
|
-
// Resource grant methods
|
|
933
|
-
grantAccess,
|
|
934
|
-
revokeAccess,
|
|
935
|
-
canAccess,
|
|
936
|
-
requireAccess,
|
|
937
|
-
getResourceGrants,
|
|
938
|
-
|
|
939
|
-
// Context
|
|
940
|
-
buildContext,
|
|
941
|
-
getClientContext,
|
|
942
|
-
|
|
943
|
-
// Config access
|
|
944
|
-
getPermissionDefinitions: () => config.permissions,
|
|
945
|
-
getTenantHeader: () => tenantHeader,
|
|
946
|
-
};
|
|
947
|
-
},
|
|
948
|
-
|
|
949
|
-
middleware: (ctx, service) => ({
|
|
950
|
-
/**
|
|
951
|
-
* Tenant middleware - resolves and validates tenant context
|
|
952
|
-
*/
|
|
953
|
-
tenant: createMiddleware<{ required?: boolean }>(
|
|
954
|
-
async (req, reqCtx, next, options) => {
|
|
955
|
-
const tenantHeader = service.getTenantHeader();
|
|
956
|
-
const tenantId = req.headers.get(tenantHeader);
|
|
957
|
-
|
|
958
|
-
if (!tenantId) {
|
|
959
|
-
if (options?.required !== false) {
|
|
960
|
-
return Response.json(
|
|
961
|
-
{ error: "Tenant context required", code: "TENANT_REQUIRED" },
|
|
962
|
-
{ status: 400 }
|
|
963
|
-
);
|
|
964
|
-
}
|
|
965
|
-
return next();
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
const tenant = await service.getTenant(tenantId);
|
|
969
|
-
if (!tenant) {
|
|
970
|
-
return Response.json(
|
|
971
|
-
{ error: "Tenant not found", code: "TENANT_NOT_FOUND" },
|
|
972
|
-
{ status: 404 }
|
|
973
|
-
);
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
// Check membership if user is authenticated
|
|
977
|
-
if (reqCtx.user?.id) {
|
|
978
|
-
const isMember = await service.isTenantMember(reqCtx.user.id, tenantId);
|
|
979
|
-
if (!isMember) {
|
|
980
|
-
return Response.json(
|
|
981
|
-
{ error: "Not a tenant member", code: "NOT_TENANT_MEMBER" },
|
|
982
|
-
{ status: 403 }
|
|
983
|
-
);
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
(reqCtx as any).tenant = tenant;
|
|
988
|
-
(reqCtx as any).tenantId = tenant.id;
|
|
989
|
-
return next();
|
|
990
|
-
}
|
|
991
|
-
),
|
|
992
|
-
|
|
993
|
-
/**
|
|
994
|
-
* Permission middleware - checks static permissions
|
|
995
|
-
*/
|
|
996
|
-
permissions: createMiddleware<{ require: string | string[] }>(
|
|
997
|
-
async (req, reqCtx, next, options) => {
|
|
998
|
-
if (!reqCtx.user?.id) {
|
|
999
|
-
return Response.json(
|
|
1000
|
-
{ error: "Authentication required", code: "UNAUTHORIZED" },
|
|
1001
|
-
{ status: 401 }
|
|
1002
|
-
);
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
if (!(reqCtx as any).tenantId) {
|
|
1006
|
-
return Response.json(
|
|
1007
|
-
{ error: "Tenant context required", code: "TENANT_REQUIRED" },
|
|
1008
|
-
{ status: 400 }
|
|
1009
|
-
);
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
const required = Array.isArray(options.require)
|
|
1013
|
-
? options.require
|
|
1014
|
-
: [options.require];
|
|
1015
|
-
|
|
1016
|
-
for (const permission of required) {
|
|
1017
|
-
const has = await service.hasPermission(
|
|
1018
|
-
reqCtx.user.id,
|
|
1019
|
-
(reqCtx as any).tenantId,
|
|
1020
|
-
permission
|
|
1021
|
-
);
|
|
1022
|
-
if (!has) {
|
|
1023
|
-
return Response.json(
|
|
1024
|
-
{
|
|
1025
|
-
error: `Missing permission: ${permission}`,
|
|
1026
|
-
code: "PERMISSION_DENIED",
|
|
1027
|
-
},
|
|
1028
|
-
{ status: 403 }
|
|
1029
|
-
);
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
return next();
|
|
1034
|
-
}
|
|
1035
|
-
),
|
|
1036
|
-
}),
|
|
1037
|
-
|
|
1038
|
-
init: async (ctx, service) => {
|
|
1039
|
-
ctx.core.logger.info("Permissions plugin initialized", {
|
|
1040
|
-
permissions: Object.keys(config.permissions),
|
|
1041
|
-
defaultRoles: config.defaultRoles?.map((r) => r.name) || [],
|
|
1042
|
-
});
|
|
1043
|
-
},
|
|
1044
|
-
});
|
|
1045
|
-
|
|
1046
|
-
// Call factory with config to get the actual Plugin
|
|
1047
|
-
return factory(config);
|
|
1048
|
-
};
|