@donkeylabs/cli 1.1.15 → 1.1.17
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/templates/sveltekit-app/.env.example +3 -1
- package/templates/sveltekit-app/src/lib/permissions.ts +203 -0
- package/templates/sveltekit-app/src/server/index.ts +56 -0
- package/templates/sveltekit-app/src/server/plugins/auth/migrations/004_create_passkeys.ts +60 -0
- package/templates/sveltekit-app/src/server/plugins/email/index.ts +411 -0
- package/templates/sveltekit-app/src/server/plugins/email/migrations/001_create_email_tokens.ts +33 -0
- package/templates/sveltekit-app/src/server/plugins/permissions/index.ts +1045 -0
- package/templates/sveltekit-app/src/server/plugins/permissions/migrations/001_create_tenants.ts +63 -0
- package/templates/sveltekit-app/src/server/plugins/permissions/migrations/002_create_roles.ts +90 -0
- package/templates/sveltekit-app/src/server/plugins/permissions/migrations/003_create_resource_grants.ts +50 -0
- package/templates/sveltekit-app/src/server/routes/permissions/index.ts +248 -0
- package/templates/sveltekit-app/src/server/routes/tenants/index.ts +339 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenants Routes - Multi-tenant management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createRouter, defineRoute } from "@donkeylabs/server";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
const tenants = createRouter("tenants");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get all tenants the current user is a member of
|
|
12
|
+
*/
|
|
13
|
+
tenants.route("mine").typed(
|
|
14
|
+
defineRoute({
|
|
15
|
+
input: z.object({}),
|
|
16
|
+
output: z.array(z.object({
|
|
17
|
+
id: z.string(),
|
|
18
|
+
name: z.string(),
|
|
19
|
+
slug: z.string(),
|
|
20
|
+
settings: z.record(z.string(), z.unknown()).nullable(),
|
|
21
|
+
})),
|
|
22
|
+
handle: async (input, ctx) => {
|
|
23
|
+
if (!ctx.user?.id) {
|
|
24
|
+
throw ctx.errors.Unauthorized("Authentication required");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return ctx.plugins.permissions.getUserTenants(ctx.user.id);
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a new tenant (user becomes owner/admin)
|
|
34
|
+
*/
|
|
35
|
+
tenants.route("create").typed(
|
|
36
|
+
defineRoute({
|
|
37
|
+
input: z.object({
|
|
38
|
+
name: z.string().min(1).max(100),
|
|
39
|
+
slug: z.string().min(1).max(50).regex(/^[a-z0-9-]+$/, "Slug must be lowercase alphanumeric with hyphens"),
|
|
40
|
+
settings: z.record(z.string(), z.unknown()).optional(),
|
|
41
|
+
}),
|
|
42
|
+
output: z.object({
|
|
43
|
+
id: z.string(),
|
|
44
|
+
name: z.string(),
|
|
45
|
+
slug: z.string(),
|
|
46
|
+
settings: z.record(z.string(), z.unknown()).nullable(),
|
|
47
|
+
}),
|
|
48
|
+
handle: async (input, ctx) => {
|
|
49
|
+
if (!ctx.user?.id) {
|
|
50
|
+
throw ctx.errors.Unauthorized("Authentication required");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check if slug is taken
|
|
54
|
+
const existing = await ctx.plugins.permissions.getTenantBySlug(input.slug);
|
|
55
|
+
if (existing) {
|
|
56
|
+
throw ctx.errors.BadRequest("Tenant slug already taken");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return ctx.plugins.permissions.createTenant({
|
|
60
|
+
name: input.name,
|
|
61
|
+
slug: input.slug,
|
|
62
|
+
ownerId: ctx.user.id,
|
|
63
|
+
settings: input.settings,
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get tenant by ID
|
|
71
|
+
*/
|
|
72
|
+
tenants.route("get").typed(
|
|
73
|
+
defineRoute({
|
|
74
|
+
input: z.object({
|
|
75
|
+
tenantId: z.string(),
|
|
76
|
+
}),
|
|
77
|
+
output: z.object({
|
|
78
|
+
id: z.string(),
|
|
79
|
+
name: z.string(),
|
|
80
|
+
slug: z.string(),
|
|
81
|
+
settings: z.record(z.string(), z.unknown()).nullable(),
|
|
82
|
+
}).nullable(),
|
|
83
|
+
handle: async (input, ctx) => {
|
|
84
|
+
if (!ctx.user?.id) {
|
|
85
|
+
throw ctx.errors.Unauthorized("Authentication required");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Verify membership
|
|
89
|
+
const isMember = await ctx.plugins.permissions.isTenantMember(ctx.user.id, input.tenantId);
|
|
90
|
+
if (!isMember) {
|
|
91
|
+
throw ctx.errors.Forbidden("Not a member of this tenant");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return ctx.plugins.permissions.getTenant(input.tenantId);
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get tenant roles
|
|
101
|
+
*/
|
|
102
|
+
tenants.route("roles").typed(
|
|
103
|
+
defineRoute({
|
|
104
|
+
input: z.object({
|
|
105
|
+
tenantId: z.string(),
|
|
106
|
+
}),
|
|
107
|
+
output: z.array(z.object({
|
|
108
|
+
id: z.string(),
|
|
109
|
+
name: z.string(),
|
|
110
|
+
description: z.string().nullable(),
|
|
111
|
+
permissions: z.array(z.string()),
|
|
112
|
+
inheritsFrom: z.string().nullable(),
|
|
113
|
+
isDefault: z.boolean(),
|
|
114
|
+
})),
|
|
115
|
+
handle: async (input, ctx) => {
|
|
116
|
+
if (!ctx.user?.id) {
|
|
117
|
+
throw ctx.errors.Unauthorized("Authentication required");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Verify membership
|
|
121
|
+
const isMember = await ctx.plugins.permissions.isTenantMember(ctx.user.id, input.tenantId);
|
|
122
|
+
if (!isMember) {
|
|
123
|
+
throw ctx.errors.Forbidden("Not a member of this tenant");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const roles = await ctx.plugins.permissions.getTenantRoles(input.tenantId);
|
|
127
|
+
return roles.map(r => ({
|
|
128
|
+
id: r.id,
|
|
129
|
+
name: r.name,
|
|
130
|
+
description: r.description,
|
|
131
|
+
permissions: r.permissions,
|
|
132
|
+
inheritsFrom: r.inheritsFrom,
|
|
133
|
+
isDefault: r.isDefault,
|
|
134
|
+
}));
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Create a role in a tenant
|
|
141
|
+
*/
|
|
142
|
+
tenants.route("createRole").typed(
|
|
143
|
+
defineRoute({
|
|
144
|
+
input: z.object({
|
|
145
|
+
tenantId: z.string(),
|
|
146
|
+
name: z.string().min(1).max(50),
|
|
147
|
+
description: z.string().max(200).optional(),
|
|
148
|
+
permissions: z.array(z.string()),
|
|
149
|
+
inheritsFrom: z.string().optional(),
|
|
150
|
+
isDefault: z.boolean().optional(),
|
|
151
|
+
}),
|
|
152
|
+
output: z.object({
|
|
153
|
+
id: z.string(),
|
|
154
|
+
name: z.string(),
|
|
155
|
+
description: z.string().nullable(),
|
|
156
|
+
permissions: z.array(z.string()),
|
|
157
|
+
inheritsFrom: z.string().nullable(),
|
|
158
|
+
isDefault: z.boolean(),
|
|
159
|
+
}),
|
|
160
|
+
handle: async (input, ctx) => {
|
|
161
|
+
if (!ctx.user?.id) {
|
|
162
|
+
throw ctx.errors.Unauthorized("Authentication required");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check admin permission
|
|
166
|
+
const hasAdmin = await ctx.plugins.permissions.hasPermission(
|
|
167
|
+
ctx.user.id,
|
|
168
|
+
input.tenantId,
|
|
169
|
+
"roles.manage"
|
|
170
|
+
);
|
|
171
|
+
if (!hasAdmin) {
|
|
172
|
+
throw ctx.errors.Forbidden("Cannot manage roles");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const role = await ctx.plugins.permissions.createRole({
|
|
176
|
+
tenantId: input.tenantId,
|
|
177
|
+
name: input.name,
|
|
178
|
+
description: input.description,
|
|
179
|
+
permissions: input.permissions,
|
|
180
|
+
inheritsFrom: input.inheritsFrom,
|
|
181
|
+
isDefault: input.isDefault,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
id: role.id,
|
|
186
|
+
name: role.name,
|
|
187
|
+
description: role.description,
|
|
188
|
+
permissions: role.permissions,
|
|
189
|
+
inheritsFrom: role.inheritsFrom,
|
|
190
|
+
isDefault: role.isDefault,
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Invite user to tenant (add as member)
|
|
198
|
+
*/
|
|
199
|
+
tenants.route("addMember").typed(
|
|
200
|
+
defineRoute({
|
|
201
|
+
input: z.object({
|
|
202
|
+
tenantId: z.string(),
|
|
203
|
+
userId: z.string(),
|
|
204
|
+
}),
|
|
205
|
+
output: z.object({ success: z.boolean() }),
|
|
206
|
+
handle: async (input, ctx) => {
|
|
207
|
+
if (!ctx.user?.id) {
|
|
208
|
+
throw ctx.errors.Unauthorized("Authentication required");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check permission
|
|
212
|
+
const canInvite = await ctx.plugins.permissions.hasPermission(
|
|
213
|
+
ctx.user.id,
|
|
214
|
+
input.tenantId,
|
|
215
|
+
"members.invite"
|
|
216
|
+
);
|
|
217
|
+
if (!canInvite) {
|
|
218
|
+
throw ctx.errors.Forbidden("Cannot invite members");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
await ctx.plugins.permissions.addTenantMember(
|
|
222
|
+
input.tenantId,
|
|
223
|
+
input.userId,
|
|
224
|
+
ctx.user.id
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
return { success: true };
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Remove user from tenant
|
|
234
|
+
*/
|
|
235
|
+
tenants.route("removeMember").typed(
|
|
236
|
+
defineRoute({
|
|
237
|
+
input: z.object({
|
|
238
|
+
tenantId: z.string(),
|
|
239
|
+
userId: z.string(),
|
|
240
|
+
}),
|
|
241
|
+
output: z.object({ success: z.boolean() }),
|
|
242
|
+
handle: async (input, ctx) => {
|
|
243
|
+
if (!ctx.user?.id) {
|
|
244
|
+
throw ctx.errors.Unauthorized("Authentication required");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check permission
|
|
248
|
+
const canRemove = await ctx.plugins.permissions.hasPermission(
|
|
249
|
+
ctx.user.id,
|
|
250
|
+
input.tenantId,
|
|
251
|
+
"members.remove"
|
|
252
|
+
);
|
|
253
|
+
if (!canRemove) {
|
|
254
|
+
throw ctx.errors.Forbidden("Cannot remove members");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
await ctx.plugins.permissions.removeTenantMember(input.tenantId, input.userId);
|
|
258
|
+
|
|
259
|
+
return { success: true };
|
|
260
|
+
},
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Assign role to user
|
|
266
|
+
*/
|
|
267
|
+
tenants.route("assignRole").typed(
|
|
268
|
+
defineRoute({
|
|
269
|
+
input: z.object({
|
|
270
|
+
tenantId: z.string(),
|
|
271
|
+
userId: z.string(),
|
|
272
|
+
roleId: z.string(),
|
|
273
|
+
}),
|
|
274
|
+
output: z.object({ success: z.boolean() }),
|
|
275
|
+
handle: async (input, ctx) => {
|
|
276
|
+
if (!ctx.user?.id) {
|
|
277
|
+
throw ctx.errors.Unauthorized("Authentication required");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check permission
|
|
281
|
+
const canManage = await ctx.plugins.permissions.hasPermission(
|
|
282
|
+
ctx.user.id,
|
|
283
|
+
input.tenantId,
|
|
284
|
+
"roles.assign"
|
|
285
|
+
);
|
|
286
|
+
if (!canManage) {
|
|
287
|
+
throw ctx.errors.Forbidden("Cannot assign roles");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
await ctx.plugins.permissions.assignRole(
|
|
291
|
+
input.userId,
|
|
292
|
+
input.roleId,
|
|
293
|
+
input.tenantId,
|
|
294
|
+
ctx.user.id
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
return { success: true };
|
|
298
|
+
},
|
|
299
|
+
})
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Revoke role from user
|
|
304
|
+
*/
|
|
305
|
+
tenants.route("revokeRole").typed(
|
|
306
|
+
defineRoute({
|
|
307
|
+
input: z.object({
|
|
308
|
+
tenantId: z.string(),
|
|
309
|
+
userId: z.string(),
|
|
310
|
+
roleId: z.string(),
|
|
311
|
+
}),
|
|
312
|
+
output: z.object({ success: z.boolean() }),
|
|
313
|
+
handle: async (input, ctx) => {
|
|
314
|
+
if (!ctx.user?.id) {
|
|
315
|
+
throw ctx.errors.Unauthorized("Authentication required");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Check permission
|
|
319
|
+
const canManage = await ctx.plugins.permissions.hasPermission(
|
|
320
|
+
ctx.user.id,
|
|
321
|
+
input.tenantId,
|
|
322
|
+
"roles.assign"
|
|
323
|
+
);
|
|
324
|
+
if (!canManage) {
|
|
325
|
+
throw ctx.errors.Forbidden("Cannot revoke roles");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
await ctx.plugins.permissions.revokeRole(
|
|
329
|
+
input.userId,
|
|
330
|
+
input.roleId,
|
|
331
|
+
input.tenantId
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
return { success: true };
|
|
335
|
+
},
|
|
336
|
+
})
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
export { tenants as tenantsRouter };
|