@donkeylabs/cli 1.1.16 → 1.1.18

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.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/src/commands/generate.ts +153 -3
  3. package/templates/sveltekit-app/package.json +3 -3
  4. package/templates/sveltekit-app/src/lib/permissions.ts +213 -0
  5. package/templates/sveltekit-app/src/routes/+page.server.ts +1 -1
  6. package/templates/sveltekit-app/src/routes/workflows/+page.server.ts +1 -1
  7. package/templates/sveltekit-app/src/server/index.ts +46 -1
  8. package/templates/sveltekit-app/src/server/plugins/auth/auth.test.ts +377 -0
  9. package/templates/sveltekit-app/src/server/plugins/auth/index.ts +7 -7
  10. package/templates/sveltekit-app/src/server/plugins/auth/schema.ts +65 -0
  11. package/templates/sveltekit-app/src/server/plugins/email/email.test.ts +369 -0
  12. package/templates/sveltekit-app/src/server/plugins/email/schema.ts +24 -0
  13. package/templates/sveltekit-app/src/server/plugins/permissions/index.ts +1048 -0
  14. package/templates/sveltekit-app/src/server/plugins/permissions/migrations/001_create_tenants.ts +63 -0
  15. package/templates/sveltekit-app/src/server/plugins/permissions/migrations/002_create_roles.ts +90 -0
  16. package/templates/sveltekit-app/src/server/plugins/permissions/migrations/003_create_resource_grants.ts +50 -0
  17. package/templates/sveltekit-app/src/server/plugins/permissions/permissions.test.ts +566 -0
  18. package/templates/sveltekit-app/src/server/plugins/permissions/schema.ts +67 -0
  19. package/templates/sveltekit-app/src/server/plugins/workflow-demo/index.ts +3 -2
  20. package/templates/sveltekit-app/src/server/routes/auth/handlers/login.handler.ts +4 -6
  21. package/templates/sveltekit-app/src/server/routes/auth/handlers/logout.handler.ts +5 -8
  22. package/templates/sveltekit-app/src/server/routes/auth/handlers/me.handler.ts +4 -7
  23. package/templates/sveltekit-app/src/server/routes/auth/handlers/refresh.handler.ts +4 -6
  24. package/templates/sveltekit-app/src/server/routes/auth/handlers/register.handler.ts +4 -6
  25. package/templates/sveltekit-app/src/server/routes/auth/handlers/update-profile.handler.ts +5 -8
  26. package/templates/sveltekit-app/src/server/routes/auth/index.ts +6 -7
  27. package/templates/sveltekit-app/src/server/routes/example/handlers/greet.handler.ts +3 -5
  28. package/templates/sveltekit-app/src/server/routes/permissions/index.ts +248 -0
  29. package/templates/sveltekit-app/src/server/routes/tenants/index.ts +339 -0
@@ -1,14 +1,12 @@
1
- import type { Handler, Routes, AppContext } from "$server/api";
2
-
3
1
  /**
4
2
  * Refresh Handler - Get new access token using refresh token
5
3
  * Only available with refresh-token strategy
6
4
  */
7
- export class RefreshHandler implements Handler<Routes.Auth.Refresh> {
8
- constructor(private ctx: AppContext) {}
5
+ export class RefreshHandler {
6
+ constructor(private ctx: any) {}
9
7
 
10
- async handle(input: Routes.Auth.Refresh.Input): Promise<Routes.Auth.Refresh.Output> {
11
- const tokens = await this.ctx.plugins.auth.refresh(input.refreshToken);
8
+ async handle(input: { refreshToken: string }) {
9
+ const tokens = await (this.ctx.plugins as any).auth.refresh(input.refreshToken);
12
10
 
13
11
  return {
14
12
  accessToken: tokens.accessToken,
@@ -1,13 +1,11 @@
1
- import type { Handler, Routes, AppContext } from "$server/api";
2
-
3
1
  /**
4
2
  * Register Handler - Create a new user account
5
3
  */
6
- export class RegisterHandler implements Handler<Routes.Auth.Register> {
7
- constructor(private ctx: AppContext) {}
4
+ export class RegisterHandler {
5
+ constructor(private ctx: any) {}
8
6
 
9
- async handle(input: Routes.Auth.Register.Input): Promise<Routes.Auth.Register.Output> {
10
- const result = await this.ctx.plugins.auth.register({
7
+ async handle(input: { email: string; password: string; name: string }) {
8
+ const result = await (this.ctx.plugins as any).auth.register({
11
9
  email: input.email,
12
10
  password: input.password,
13
11
  name: input.name,
@@ -1,20 +1,17 @@
1
- import type { Handler, Routes, AppContext } from "$server/api";
2
-
3
1
  /**
4
2
  * Update Profile Handler - Update current user's profile
5
3
  */
6
- export class UpdateProfileHandler implements Handler<Routes.Auth.UpdateProfile> {
7
- constructor(private ctx: AppContext) {}
4
+ export class UpdateProfileHandler {
5
+ constructor(private ctx: any) {}
8
6
 
9
- async handle(input: Routes.Auth.UpdateProfile.Input): Promise<Routes.Auth.UpdateProfile.Output> {
10
- // Get user from request context (set by auth middleware)
11
- const user = (this.ctx as any).user;
7
+ async handle(input: { name?: string; email?: string }) {
8
+ const user = this.ctx.user;
12
9
 
13
10
  if (!user) {
14
11
  throw this.ctx.errors.Unauthorized("Not authenticated");
15
12
  }
16
13
 
17
- const updated = await this.ctx.plugins.auth.updateProfile(user.id, {
14
+ const updated = await (this.ctx.plugins as any).auth.updateProfile(user.id, {
18
15
  name: input.name,
19
16
  email: input.email,
20
17
  });
@@ -5,9 +5,9 @@
5
5
  * - auth.register - Create new account
6
6
  * - auth.login - Login and get tokens
7
7
  * - auth.refresh - Refresh access token (refresh-token strategy only)
8
- * - auth.logout - Invalidate session/token (requires auth)
9
- * - auth.me - Get current user (optional auth)
10
- * - auth.updateProfile - Update profile (requires auth)
8
+ * - auth.logout - Invalidate session/token
9
+ * - auth.me - Get current user
10
+ * - auth.updateProfile - Update profile
11
11
  */
12
12
 
13
13
  import { createRouter } from "@donkeylabs/server";
@@ -51,22 +51,21 @@ export const authRouter = createRouter("auth")
51
51
  handle: RefreshHandler,
52
52
  })
53
53
 
54
- // Optional auth - returns user if logged in, null otherwise
55
- .middleware.auth({ required: false })
54
+ // Get current user (returns null if not authenticated)
56
55
  .route("me").typed({
57
56
  input: z.object({}),
58
57
  output: userSchema.nullable(),
59
58
  handle: MeHandler,
60
59
  })
61
60
 
62
- // Protected routes - require authentication
63
- .middleware.auth({ required: true })
61
+ // Logout (invalidate session/token)
64
62
  .route("logout").typed({
65
63
  input: z.object({}),
66
64
  output: logoutResponseSchema,
67
65
  handle: LogoutHandler,
68
66
  })
69
67
 
68
+ // Update profile
70
69
  .route("updateProfile").typed({
71
70
  input: updateProfileSchema,
72
71
  output: userSchema,
@@ -1,15 +1,13 @@
1
- import type { Handler, Routes, AppContext } from "$server/api";
2
-
3
1
  /**
4
2
  * Greet Handler
5
3
  *
6
4
  * Example handler demonstrating the feature module pattern.
7
5
  * Business logic lives directly in the handler - no separate model layer needed.
8
6
  */
9
- export class GreetHandler implements Handler<Routes.Example.Greet> {
10
- constructor(private ctx: AppContext) {}
7
+ export class GreetHandler {
8
+ constructor(private ctx: any) {}
11
9
 
12
- handle(input: Routes.Example.Greet.Input): Routes.Example.Greet.Output {
10
+ handle(input: { name: string; formal?: boolean }) {
13
11
  // Business logic directly in handler
14
12
  const greeting = input.formal
15
13
  ? `Good day, ${input.name}. How may I assist you?`
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Permissions Routes - Client-side permission checking
3
+ *
4
+ * These routes allow the frontend to check permissions for UI locking.
5
+ */
6
+
7
+ import { createRouter, defineRoute } from "@donkeylabs/server";
8
+ import { z } from "zod";
9
+
10
+ const permissions = createRouter("permissions");
11
+
12
+ /**
13
+ * Get current user's permission context for a tenant
14
+ * Returns roles and all static permissions
15
+ */
16
+ permissions.route("context").typed(
17
+ defineRoute({
18
+ input: z.object({
19
+ tenantId: z.string(),
20
+ }),
21
+ output: z.object({
22
+ tenantId: z.string(),
23
+ roles: z.array(z.object({
24
+ id: z.string(),
25
+ name: z.string(),
26
+ })),
27
+ permissions: z.array(z.string()),
28
+ }),
29
+ handle: async (input, ctx) => {
30
+ if (!ctx.user?.id) {
31
+ throw ctx.errors.Unauthorized("Authentication required");
32
+ }
33
+
34
+ return (ctx.plugins as any).permissions.getClientContext(ctx.user.id, input.tenantId);
35
+ },
36
+ })
37
+ );
38
+
39
+ /**
40
+ * Check if user has specific static permissions
41
+ * Returns a map of permission -> boolean
42
+ */
43
+ permissions.route("check").typed(
44
+ defineRoute({
45
+ input: z.object({
46
+ tenantId: z.string(),
47
+ permissions: z.array(z.string()),
48
+ }),
49
+ output: z.record(z.string(), z.boolean()),
50
+ handle: async (input, ctx) => {
51
+ if (!ctx.user?.id) {
52
+ throw ctx.errors.Unauthorized("Authentication required");
53
+ }
54
+
55
+ const results: Record<string, boolean> = {};
56
+
57
+ for (const permission of input.permissions) {
58
+ results[permission] = await (ctx.plugins as any).permissions.hasPermission(
59
+ ctx.user.id,
60
+ input.tenantId,
61
+ permission
62
+ );
63
+ }
64
+
65
+ return results;
66
+ },
67
+ })
68
+ );
69
+
70
+ /**
71
+ * Check if user can access specific resources
72
+ * Returns a map of "resourceType:resourceId:action" -> boolean
73
+ */
74
+ permissions.route("canAccess").typed(
75
+ defineRoute({
76
+ input: z.object({
77
+ tenantId: z.string(),
78
+ checks: z.array(z.object({
79
+ resourceType: z.string(),
80
+ resourceId: z.string(),
81
+ action: z.enum(["create", "read", "write", "delete", "admin"]),
82
+ ownerId: z.string().optional(), // For owner check
83
+ })),
84
+ }),
85
+ output: z.record(z.string(), z.boolean()),
86
+ handle: async (input, ctx) => {
87
+ if (!ctx.user?.id) {
88
+ throw ctx.errors.Unauthorized("Authentication required");
89
+ }
90
+
91
+ const results: Record<string, boolean> = {};
92
+
93
+ for (const check of input.checks) {
94
+ const key = `${check.resourceType}:${check.resourceId}:${check.action}`;
95
+ results[key] = await (ctx.plugins as any).permissions.canAccess(
96
+ ctx.user.id,
97
+ input.tenantId,
98
+ check.resourceType,
99
+ check.resourceId,
100
+ check.action,
101
+ check.ownerId
102
+ );
103
+ }
104
+
105
+ return results;
106
+ },
107
+ })
108
+ );
109
+
110
+ /**
111
+ * Get all grants for a specific resource
112
+ * Useful for showing "shared with" UI
113
+ */
114
+ permissions.route("grants").typed(
115
+ defineRoute({
116
+ input: z.object({
117
+ tenantId: z.string(),
118
+ resourceType: z.string(),
119
+ resourceId: z.string(),
120
+ }),
121
+ output: z.array(z.object({
122
+ resourceType: z.string(),
123
+ resourceId: z.string(),
124
+ granteeType: z.enum(["user", "role"]),
125
+ granteeId: z.string(),
126
+ permissions: z.array(z.enum(["create", "read", "write", "delete", "admin"])),
127
+ })),
128
+ handle: async (input, ctx) => {
129
+ if (!ctx.user?.id) {
130
+ throw ctx.errors.Unauthorized("Authentication required");
131
+ }
132
+
133
+ // Check if user can admin this resource (to see grants)
134
+ const canAdmin = await (ctx.plugins as any).permissions.canAccess(
135
+ ctx.user.id,
136
+ input.tenantId,
137
+ input.resourceType,
138
+ input.resourceId,
139
+ "admin"
140
+ );
141
+
142
+ if (!canAdmin) {
143
+ throw ctx.errors.Forbidden("Cannot view grants for this resource");
144
+ }
145
+
146
+ return (ctx.plugins as any).permissions.getResourceGrants(
147
+ input.tenantId,
148
+ input.resourceType,
149
+ input.resourceId
150
+ );
151
+ },
152
+ })
153
+ );
154
+
155
+ /**
156
+ * Grant access to a resource
157
+ */
158
+ permissions.route("grant").typed(
159
+ defineRoute({
160
+ input: z.object({
161
+ tenantId: z.string(),
162
+ resourceType: z.string(),
163
+ resourceId: z.string(),
164
+ granteeType: z.enum(["user", "role"]),
165
+ granteeId: z.string(),
166
+ permissions: z.array(z.enum(["create", "read", "write", "delete", "admin"])),
167
+ }),
168
+ output: z.object({ success: z.boolean() }),
169
+ handle: async (input, ctx) => {
170
+ if (!ctx.user?.id) {
171
+ throw ctx.errors.Unauthorized("Authentication required");
172
+ }
173
+
174
+ // Check if user can admin this resource
175
+ const canAdmin = await (ctx.plugins as any).permissions.canAccess(
176
+ ctx.user.id,
177
+ input.tenantId,
178
+ input.resourceType,
179
+ input.resourceId,
180
+ "admin"
181
+ );
182
+
183
+ if (!canAdmin) {
184
+ throw ctx.errors.Forbidden("Cannot grant access to this resource");
185
+ }
186
+
187
+ await (ctx.plugins as any).permissions.grantAccess({
188
+ tenantId: input.tenantId,
189
+ resourceType: input.resourceType,
190
+ resourceId: input.resourceId,
191
+ granteeType: input.granteeType,
192
+ granteeId: input.granteeId,
193
+ permissions: input.permissions,
194
+ grantedBy: ctx.user.id,
195
+ });
196
+
197
+ return { success: true };
198
+ },
199
+ })
200
+ );
201
+
202
+ /**
203
+ * Revoke access to a resource
204
+ */
205
+ permissions.route("revoke").typed(
206
+ defineRoute({
207
+ input: z.object({
208
+ tenantId: z.string(),
209
+ resourceType: z.string(),
210
+ resourceId: z.string(),
211
+ granteeType: z.enum(["user", "role"]),
212
+ granteeId: z.string(),
213
+ permissions: z.array(z.enum(["create", "read", "write", "delete", "admin"])).optional(),
214
+ }),
215
+ output: z.object({ success: z.boolean() }),
216
+ handle: async (input, ctx) => {
217
+ if (!ctx.user?.id) {
218
+ throw ctx.errors.Unauthorized("Authentication required");
219
+ }
220
+
221
+ // Check if user can admin this resource
222
+ const canAdmin = await (ctx.plugins as any).permissions.canAccess(
223
+ ctx.user.id,
224
+ input.tenantId,
225
+ input.resourceType,
226
+ input.resourceId,
227
+ "admin"
228
+ );
229
+
230
+ if (!canAdmin) {
231
+ throw ctx.errors.Forbidden("Cannot revoke access to this resource");
232
+ }
233
+
234
+ await (ctx.plugins as any).permissions.revokeAccess({
235
+ tenantId: input.tenantId,
236
+ resourceType: input.resourceType,
237
+ resourceId: input.resourceId,
238
+ granteeType: input.granteeType,
239
+ granteeId: input.granteeId,
240
+ permissions: input.permissions,
241
+ });
242
+
243
+ return { success: true };
244
+ },
245
+ })
246
+ );
247
+
248
+ export { permissions as permissionsRouter };
@@ -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 as any).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 as any).permissions.getTenantBySlug(input.slug);
55
+ if (existing) {
56
+ throw ctx.errors.BadRequest("Tenant slug already taken");
57
+ }
58
+
59
+ return (ctx.plugins as any).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 as any).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 as any).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 as any).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 as any).permissions.getTenantRoles(input.tenantId);
127
+ return roles.map((r: any) => ({
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 as any).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 as any).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 as any).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 as any).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 as any).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 as any).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 as any).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 as any).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 as any).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 as any).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 };