@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.
- package/package.json +1 -1
- package/src/commands/generate.ts +153 -3
- package/templates/sveltekit-app/package.json +3 -3
- package/templates/sveltekit-app/src/lib/permissions.ts +213 -0
- package/templates/sveltekit-app/src/routes/+page.server.ts +1 -1
- package/templates/sveltekit-app/src/routes/workflows/+page.server.ts +1 -1
- package/templates/sveltekit-app/src/server/index.ts +46 -1
- package/templates/sveltekit-app/src/server/plugins/auth/auth.test.ts +377 -0
- package/templates/sveltekit-app/src/server/plugins/auth/index.ts +7 -7
- package/templates/sveltekit-app/src/server/plugins/auth/schema.ts +65 -0
- package/templates/sveltekit-app/src/server/plugins/email/email.test.ts +369 -0
- package/templates/sveltekit-app/src/server/plugins/email/schema.ts +24 -0
- package/templates/sveltekit-app/src/server/plugins/permissions/index.ts +1048 -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/plugins/permissions/permissions.test.ts +566 -0
- package/templates/sveltekit-app/src/server/plugins/permissions/schema.ts +67 -0
- package/templates/sveltekit-app/src/server/plugins/workflow-demo/index.ts +3 -2
- package/templates/sveltekit-app/src/server/routes/auth/handlers/login.handler.ts +4 -6
- package/templates/sveltekit-app/src/server/routes/auth/handlers/logout.handler.ts +5 -8
- package/templates/sveltekit-app/src/server/routes/auth/handlers/me.handler.ts +4 -7
- package/templates/sveltekit-app/src/server/routes/auth/handlers/refresh.handler.ts +4 -6
- package/templates/sveltekit-app/src/server/routes/auth/handlers/register.handler.ts +4 -6
- package/templates/sveltekit-app/src/server/routes/auth/handlers/update-profile.handler.ts +5 -8
- package/templates/sveltekit-app/src/server/routes/auth/index.ts +6 -7
- package/templates/sveltekit-app/src/server/routes/example/handlers/greet.handler.ts +3 -5
- package/templates/sveltekit-app/src/server/routes/permissions/index.ts +248 -0
- package/templates/sveltekit-app/src/server/routes/tenants/index.ts +339 -0
package/package.json
CHANGED
package/src/commands/generate.ts
CHANGED
|
@@ -83,6 +83,110 @@ async function extractMiddlewareNames(pluginPath: string): Promise<string[]> {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
interface ServiceDefinitionInfo {
|
|
87
|
+
name: string;
|
|
88
|
+
exportName: string;
|
|
89
|
+
filePath: string;
|
|
90
|
+
returnType: string | null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extract defineService calls from source files.
|
|
95
|
+
* Looks for patterns like: export const myService = defineService("name", ...)
|
|
96
|
+
*/
|
|
97
|
+
async function extractServiceDefinitions(filePath: string): Promise<ServiceDefinitionInfo[]> {
|
|
98
|
+
try {
|
|
99
|
+
const content = await readFile(filePath, "utf-8");
|
|
100
|
+
const services: ServiceDefinitionInfo[] = [];
|
|
101
|
+
|
|
102
|
+
// Match: export const serviceName = defineService("name", ...)
|
|
103
|
+
// Also try to capture return type annotation if present
|
|
104
|
+
const pattern = /export\s+const\s+(\w+)\s*=\s*defineService\s*\(\s*["'](\w+)["']/g;
|
|
105
|
+
|
|
106
|
+
let match;
|
|
107
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
108
|
+
const exportName = match[1] || "";
|
|
109
|
+
const serviceName = match[2] || "";
|
|
110
|
+
|
|
111
|
+
// Try to find return type from explicit annotation or factory return
|
|
112
|
+
// Look for patterns like: defineService<"name", NVRService>(...) or ): Promise<NVR> =>
|
|
113
|
+
let returnType: string | null = null;
|
|
114
|
+
|
|
115
|
+
// Check for generic type parameter: defineService<"name", ReturnType>
|
|
116
|
+
const genericMatch = content.slice(match.index).match(
|
|
117
|
+
/defineService\s*<\s*["']\w+["']\s*,\s*([^>]+)>/
|
|
118
|
+
);
|
|
119
|
+
if (genericMatch?.[1]) {
|
|
120
|
+
returnType = genericMatch[1].trim();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check for return type annotation on the factory: (ctx): Promise<Type> =>
|
|
124
|
+
if (!returnType) {
|
|
125
|
+
const factoryMatch = content.slice(match.index, match.index + 500).match(
|
|
126
|
+
/,\s*(?:async\s*)?\([^)]*\)\s*(?::\s*(?:Promise\s*<\s*)?([A-Z]\w+))?/
|
|
127
|
+
);
|
|
128
|
+
if (factoryMatch?.[1]) {
|
|
129
|
+
returnType = factoryMatch[1].trim();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
services.push({
|
|
134
|
+
name: serviceName,
|
|
135
|
+
exportName,
|
|
136
|
+
filePath,
|
|
137
|
+
returnType,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return services;
|
|
142
|
+
} catch {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Find all service definitions in the project.
|
|
149
|
+
* Scans common locations: server entry, services directory, etc.
|
|
150
|
+
*/
|
|
151
|
+
async function findServiceDefinitions(entryPath: string, servicesPattern?: string): Promise<ServiceDefinitionInfo[]> {
|
|
152
|
+
const allServices: ServiceDefinitionInfo[] = [];
|
|
153
|
+
|
|
154
|
+
// Scan the entry file
|
|
155
|
+
const entryFullPath = join(process.cwd(), entryPath);
|
|
156
|
+
if (existsSync(entryFullPath)) {
|
|
157
|
+
const entryServices = await extractServiceDefinitions(entryFullPath);
|
|
158
|
+
allServices.push(...entryServices);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Scan services directory if it exists
|
|
162
|
+
const servicesDir = join(process.cwd(), "src/server/services");
|
|
163
|
+
if (existsSync(servicesDir)) {
|
|
164
|
+
const entries = await readdir(servicesDir, { withFileTypes: true });
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
167
|
+
const filePath = join(servicesDir, entry.name);
|
|
168
|
+
const services = await extractServiceDefinitions(filePath);
|
|
169
|
+
allServices.push(...services);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Also check src/lib/services for SvelteKit projects
|
|
175
|
+
const libServicesDir = join(process.cwd(), "src/lib/services");
|
|
176
|
+
if (existsSync(libServicesDir)) {
|
|
177
|
+
const entries = await readdir(libServicesDir, { withFileTypes: true });
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
180
|
+
const filePath = join(libServicesDir, entry.name);
|
|
181
|
+
const services = await extractServiceDefinitions(filePath);
|
|
182
|
+
allServices.push(...services);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return allServices;
|
|
188
|
+
}
|
|
189
|
+
|
|
86
190
|
interface ExtractedRoute {
|
|
87
191
|
name: string;
|
|
88
192
|
handler: string;
|
|
@@ -519,9 +623,18 @@ export async function generateCommand(_args: string[]): Promise<void> {
|
|
|
519
623
|
const entryPath = config.entry || "./src/index.ts";
|
|
520
624
|
const serverRoutes = await extractRoutesFromServer(entryPath);
|
|
521
625
|
|
|
626
|
+
// Find custom service definitions
|
|
627
|
+
const services = await findServiceDefinitions(entryPath);
|
|
628
|
+
if (services.length > 0) {
|
|
629
|
+
console.log(
|
|
630
|
+
pc.green("Found services:"),
|
|
631
|
+
services.map((s) => pc.dim(s.name)).join(", ")
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
|
|
522
635
|
// Generate all files
|
|
523
636
|
await generateRegistry(plugins, outPath);
|
|
524
|
-
await generateContext(plugins, outPath);
|
|
637
|
+
await generateContext(plugins, services, outPath);
|
|
525
638
|
await generateRouteTypes(fileRoutes, outPath);
|
|
526
639
|
|
|
527
640
|
const generated = ["registry", "context", "routes"];
|
|
@@ -668,22 +781,57 @@ ${middlewareBuilderMethods}
|
|
|
668
781
|
|
|
669
782
|
async function generateContext(
|
|
670
783
|
plugins: { name: string; path: string; exportName: string }[],
|
|
784
|
+
services: ServiceDefinitionInfo[],
|
|
671
785
|
outPath: string
|
|
672
786
|
) {
|
|
673
787
|
const schemaIntersection =
|
|
674
788
|
plugins.map((p) => `PluginRegistry["${p.name}"]["schema"]`).join(" & ") ||
|
|
675
789
|
"{}";
|
|
676
790
|
|
|
791
|
+
// Generate service imports and type entries
|
|
792
|
+
const serviceImports: string[] = [];
|
|
793
|
+
const serviceEntries: string[] = [];
|
|
794
|
+
|
|
795
|
+
for (const service of services) {
|
|
796
|
+
// Calculate relative path from outPath to service file
|
|
797
|
+
const serviceAbsPath = service.filePath.replace(/\.ts$/, "");
|
|
798
|
+
const relativePath = relative(outPath, serviceAbsPath);
|
|
799
|
+
const importPath = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
800
|
+
|
|
801
|
+
serviceImports.push(`import type { ${service.exportName} } from "${importPath}";`);
|
|
802
|
+
|
|
803
|
+
// Use the inferred return type if available, otherwise use Awaited<ReturnType<factory>>
|
|
804
|
+
if (service.returnType) {
|
|
805
|
+
serviceEntries.push(` ${service.name}: ${service.returnType};`);
|
|
806
|
+
} else {
|
|
807
|
+
// Infer type from the service definition
|
|
808
|
+
serviceEntries.push(` ${service.name}: Awaited<ReturnType<typeof ${service.exportName}["factory"]>>;`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const serviceImportsBlock = serviceImports.length > 0 ? serviceImports.join("\n") + "\n" : "";
|
|
813
|
+
const servicesType = serviceEntries.length > 0
|
|
814
|
+
? `{\n${serviceEntries.join("\n")}\n }`
|
|
815
|
+
: "Record<string, never>";
|
|
816
|
+
|
|
677
817
|
const content = `// Auto-generated by donkeylabs generate
|
|
678
818
|
// App context - import as: import type { AppContext } from ".@donkeylabs/server/context";
|
|
679
819
|
|
|
680
820
|
/// <reference path="./registry.d.ts" />
|
|
681
|
-
import type { PluginRegistry, CoreServices, Errors } from "@donkeylabs/server";
|
|
821
|
+
import type { PluginRegistry, CoreServices, Errors, ServiceRegistry } from "@donkeylabs/server";
|
|
682
822
|
import type { Kysely } from "kysely";
|
|
683
|
-
|
|
823
|
+
${serviceImportsBlock}
|
|
684
824
|
/** Merged database schema from all plugins */
|
|
685
825
|
export type DatabaseSchema = ${schemaIntersection};
|
|
686
826
|
|
|
827
|
+
/** Custom services registered via defineService() */
|
|
828
|
+
export interface AppServices ${servicesType}
|
|
829
|
+
|
|
830
|
+
// Augment the ServiceRegistry for type inference in ctx.services
|
|
831
|
+
declare module "@donkeylabs/server" {
|
|
832
|
+
interface ServiceRegistry extends AppServices {}
|
|
833
|
+
}
|
|
834
|
+
|
|
687
835
|
/**
|
|
688
836
|
* Fully typed application context.
|
|
689
837
|
* Use this instead of ServerContext for typed plugin access.
|
|
@@ -701,6 +849,8 @@ export interface AppContext {
|
|
|
701
849
|
errors: Errors;
|
|
702
850
|
/** Application config */
|
|
703
851
|
config: Record<string, any>;
|
|
852
|
+
/** Custom user-registered services */
|
|
853
|
+
services: AppServices;
|
|
704
854
|
/** Client IP address */
|
|
705
855
|
ip: string;
|
|
706
856
|
/** Unique request ID */
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"vite": "^7.2.6"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@donkeylabs/cli": "
|
|
28
|
-
"@donkeylabs/adapter-sveltekit": "
|
|
29
|
-
"@donkeylabs/server": "
|
|
27
|
+
"@donkeylabs/cli": "^1.1.18",
|
|
28
|
+
"@donkeylabs/adapter-sveltekit": "^1.1.18",
|
|
29
|
+
"@donkeylabs/server": "^1.1.18",
|
|
30
30
|
"bits-ui": "^2.15.4",
|
|
31
31
|
"clsx": "^2.1.1",
|
|
32
32
|
"kysely": "^0.27.6",
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permissions Helper for Svelte UI
|
|
3
|
+
*
|
|
4
|
+
* Provides reactive permission checking for UI locking.
|
|
5
|
+
*
|
|
6
|
+
* Usage in +page.server.ts:
|
|
7
|
+
* ```ts
|
|
8
|
+
* export const load = async ({ locals }) => {
|
|
9
|
+
* const api = createApi({ locals });
|
|
10
|
+
* const permissions = await api.permissions.context({ tenantId });
|
|
11
|
+
* return { permissions };
|
|
12
|
+
* };
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Usage in +page.svelte:
|
|
16
|
+
* ```svelte
|
|
17
|
+
* <script>
|
|
18
|
+
* import { createPermissions } from "$lib/permissions";
|
|
19
|
+
* let { data } = $props();
|
|
20
|
+
* const can = createPermissions(data.permissions);
|
|
21
|
+
* </script>
|
|
22
|
+
*
|
|
23
|
+
* {#if can.has("documents.create")}
|
|
24
|
+
* <Button>Create Document</Button>
|
|
25
|
+
* {/if}
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
// Note: createApi is imported dynamically to avoid compile-time dependency
|
|
30
|
+
// on the generated API file which may not have permissions routes yet
|
|
31
|
+
|
|
32
|
+
export interface PermissionContext {
|
|
33
|
+
tenantId: string;
|
|
34
|
+
roles: Array<{ id: string; name: string }>;
|
|
35
|
+
permissions: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PermissionsHelper {
|
|
39
|
+
/** Check if user has a static permission */
|
|
40
|
+
has: (permission: string) => boolean;
|
|
41
|
+
|
|
42
|
+
/** Check if user has all of the specified permissions */
|
|
43
|
+
hasAll: (...permissions: string[]) => boolean;
|
|
44
|
+
|
|
45
|
+
/** Check if user has any of the specified permissions */
|
|
46
|
+
hasAny: (...permissions: string[]) => boolean;
|
|
47
|
+
|
|
48
|
+
/** Check if user has a specific role */
|
|
49
|
+
hasRole: (roleName: string) => boolean;
|
|
50
|
+
|
|
51
|
+
/** Get all permissions */
|
|
52
|
+
all: () => string[];
|
|
53
|
+
|
|
54
|
+
/** Get all roles */
|
|
55
|
+
roles: () => Array<{ id: string; name: string }>;
|
|
56
|
+
|
|
57
|
+
/** Get tenant ID */
|
|
58
|
+
tenantId: () => string;
|
|
59
|
+
|
|
60
|
+
/** Check resource access (async - makes API call) */
|
|
61
|
+
canAccess: (
|
|
62
|
+
resourceType: string,
|
|
63
|
+
resourceId: string,
|
|
64
|
+
action: "create" | "read" | "write" | "delete" | "admin",
|
|
65
|
+
ownerId?: string
|
|
66
|
+
) => Promise<boolean>;
|
|
67
|
+
|
|
68
|
+
/** Batch check resource access (async - makes single API call) */
|
|
69
|
+
canAccessMany: (
|
|
70
|
+
checks: Array<{
|
|
71
|
+
resourceType: string;
|
|
72
|
+
resourceId: string;
|
|
73
|
+
action: "create" | "read" | "write" | "delete" | "admin";
|
|
74
|
+
ownerId?: string;
|
|
75
|
+
}>
|
|
76
|
+
) => Promise<Record<string, boolean>>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create a permissions helper from context
|
|
81
|
+
*/
|
|
82
|
+
export function createPermissions(context: PermissionContext, api?: any): PermissionsHelper {
|
|
83
|
+
const permissionSet = new Set(context.permissions);
|
|
84
|
+
|
|
85
|
+
// Lazy load API if not provided
|
|
86
|
+
const getApi = () => {
|
|
87
|
+
if (api) return api;
|
|
88
|
+
// Dynamic import to avoid compile-time dependency
|
|
89
|
+
const { createApi } = require("./api");
|
|
90
|
+
return createApi();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
has(permission: string): boolean {
|
|
95
|
+
// Exact match
|
|
96
|
+
if (permissionSet.has(permission)) return true;
|
|
97
|
+
|
|
98
|
+
// Wildcard match
|
|
99
|
+
if (permissionSet.has("*")) return true;
|
|
100
|
+
|
|
101
|
+
// Resource wildcard (e.g., "documents.*")
|
|
102
|
+
const [resource] = permission.split(".");
|
|
103
|
+
if (permissionSet.has(`${resource}.*`)) return true;
|
|
104
|
+
|
|
105
|
+
return false;
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
hasAll(...permissions: string[]): boolean {
|
|
109
|
+
return permissions.every((p) => this.has(p));
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
hasAny(...permissions: string[]): boolean {
|
|
113
|
+
return permissions.some((p) => this.has(p));
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
hasRole(roleName: string): boolean {
|
|
117
|
+
return context.roles.some(
|
|
118
|
+
(r) => r.name.toLowerCase() === roleName.toLowerCase()
|
|
119
|
+
);
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
all(): string[] {
|
|
123
|
+
return context.permissions;
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
roles(): Array<{ id: string; name: string }> {
|
|
127
|
+
return context.roles;
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
tenantId(): string {
|
|
131
|
+
return context.tenantId;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
async canAccess(
|
|
135
|
+
resourceType: string,
|
|
136
|
+
resourceId: string,
|
|
137
|
+
action: "create" | "read" | "write" | "delete" | "admin",
|
|
138
|
+
ownerId?: string
|
|
139
|
+
): Promise<boolean> {
|
|
140
|
+
const apiClient = getApi();
|
|
141
|
+
const result = await apiClient.permissions.canAccess({
|
|
142
|
+
tenantId: context.tenantId,
|
|
143
|
+
checks: [{ resourceType, resourceId, action, ownerId }],
|
|
144
|
+
});
|
|
145
|
+
return result[`${resourceType}:${resourceId}:${action}`] ?? false;
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
async canAccessMany(
|
|
149
|
+
checks: Array<{
|
|
150
|
+
resourceType: string;
|
|
151
|
+
resourceId: string;
|
|
152
|
+
action: "create" | "read" | "write" | "delete" | "admin";
|
|
153
|
+
ownerId?: string;
|
|
154
|
+
}>
|
|
155
|
+
): Promise<Record<string, boolean>> {
|
|
156
|
+
const apiClient = getApi();
|
|
157
|
+
return apiClient.permissions.canAccess({
|
|
158
|
+
tenantId: context.tenantId,
|
|
159
|
+
checks,
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Svelte component helper - use in templates
|
|
167
|
+
*
|
|
168
|
+
* Usage:
|
|
169
|
+
* ```svelte
|
|
170
|
+
* <script>
|
|
171
|
+
* import { Can } from "$lib/permissions";
|
|
172
|
+
* let { data } = $props();
|
|
173
|
+
* </script>
|
|
174
|
+
*
|
|
175
|
+
* <Can permission="documents.create" context={data.permissions}>
|
|
176
|
+
* <Button>Create</Button>
|
|
177
|
+
* </Can>
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
export function canCheck(
|
|
181
|
+
context: PermissionContext,
|
|
182
|
+
permission: string
|
|
183
|
+
): boolean {
|
|
184
|
+
const helper = createPermissions(context);
|
|
185
|
+
return helper.has(permission);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Type helper for defining permissions in your app
|
|
190
|
+
*
|
|
191
|
+
* Usage:
|
|
192
|
+
* ```ts
|
|
193
|
+
* const PERMISSIONS = definePermissions({
|
|
194
|
+
* documents: ["create", "read", "write", "delete", "admin"],
|
|
195
|
+
* users: ["invite", "remove", "manage"],
|
|
196
|
+
* billing: ["view", "manage"],
|
|
197
|
+
* } as const);
|
|
198
|
+
*
|
|
199
|
+
* // Type: "documents.create" | "documents.read" | ...
|
|
200
|
+
* type Permission = typeof PERMISSIONS[number];
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
export function definePermissions<
|
|
204
|
+
T extends Record<string, readonly string[]>
|
|
205
|
+
>(permissions: T): Array<`${Extract<keyof T, string>}.${T[keyof T][number]}`> {
|
|
206
|
+
const result: string[] = [];
|
|
207
|
+
for (const [resource, actions] of Object.entries(permissions)) {
|
|
208
|
+
for (const action of actions) {
|
|
209
|
+
result.push(`${resource}.${action}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return result as any;
|
|
213
|
+
}
|
|
@@ -8,7 +8,7 @@ export const load: PageServerLoad = async ({ locals }) => {
|
|
|
8
8
|
|
|
9
9
|
try {
|
|
10
10
|
// Direct service call through typed client
|
|
11
|
-
const result = await client.
|
|
11
|
+
const result = await (client as any).demo.counter.get({});
|
|
12
12
|
return {
|
|
13
13
|
count: result.count,
|
|
14
14
|
loadedAt: new Date().toISOString(),
|
|
@@ -7,7 +7,7 @@ export const load: PageServerLoad = async ({ locals }) => {
|
|
|
7
7
|
|
|
8
8
|
try {
|
|
9
9
|
// Load initial workflow instances
|
|
10
|
-
const result = await client.
|
|
10
|
+
const result = await (client as any).demo.workflow.list({});
|
|
11
11
|
return {
|
|
12
12
|
instances: result.instances || [],
|
|
13
13
|
loadedAt: new Date().toISOString(),
|
|
@@ -7,9 +7,12 @@ import { demoPlugin } from "./plugins/demo";
|
|
|
7
7
|
import { workflowDemoPlugin } from "./plugins/workflow-demo";
|
|
8
8
|
import { authPlugin } from "./plugins/auth";
|
|
9
9
|
import { emailPlugin } from "./plugins/email";
|
|
10
|
+
import { permissionsPlugin } from "./plugins/permissions";
|
|
10
11
|
import demoRoutes from "./routes/demo";
|
|
11
12
|
import { exampleRouter } from "./routes/example";
|
|
12
13
|
import { authRouter } from "./routes/auth";
|
|
14
|
+
import { permissionsRouter } from "./routes/permissions";
|
|
15
|
+
import { tenantsRouter } from "./routes/tenants";
|
|
13
16
|
|
|
14
17
|
// Simple in-memory database
|
|
15
18
|
const db = new Kysely<{}>({
|
|
@@ -56,7 +59,7 @@ export const server = new AppServer({
|
|
|
56
59
|
// =============================================================================
|
|
57
60
|
|
|
58
61
|
// Using default session strategy for this template
|
|
59
|
-
server.registerPlugin(authPlugin());
|
|
62
|
+
server.registerPlugin(authPlugin({}));
|
|
60
63
|
|
|
61
64
|
// Email plugin - supports Resend or console (for development)
|
|
62
65
|
// Configure with process.env.RESEND_API_KEY for production
|
|
@@ -67,11 +70,53 @@ server.registerPlugin(emailPlugin({
|
|
|
67
70
|
baseUrl: process.env.PUBLIC_BASE_URL || "http://localhost:5173",
|
|
68
71
|
}));
|
|
69
72
|
|
|
73
|
+
// =============================================================================
|
|
74
|
+
// PERMISSIONS PLUGIN - Multi-tenant RBAC
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// Define your app's permissions here for type-safe checking.
|
|
77
|
+
// Client can use: api.permissions.check({ permissions: ["documents.create"] })
|
|
78
|
+
//
|
|
79
|
+
server.registerPlugin(permissionsPlugin({
|
|
80
|
+
permissions: {
|
|
81
|
+
// Documents
|
|
82
|
+
documents: ["create", "read", "write", "delete", "admin"],
|
|
83
|
+
// Members & Roles
|
|
84
|
+
members: ["invite", "remove", "list"],
|
|
85
|
+
roles: ["create", "assign", "manage"],
|
|
86
|
+
// Billing (example)
|
|
87
|
+
billing: ["view", "manage"],
|
|
88
|
+
} as const,
|
|
89
|
+
defaultRoles: [
|
|
90
|
+
{
|
|
91
|
+
name: "Admin",
|
|
92
|
+
permissions: ["*"], // Full access
|
|
93
|
+
isDefault: false,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "Member",
|
|
97
|
+
permissions: [
|
|
98
|
+
"documents.create",
|
|
99
|
+
"documents.read",
|
|
100
|
+
"documents.write",
|
|
101
|
+
"members.list",
|
|
102
|
+
],
|
|
103
|
+
isDefault: true, // Auto-assigned to new members
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "Viewer",
|
|
107
|
+
permissions: ["documents.read", "members.list"],
|
|
108
|
+
isDefault: false,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
}));
|
|
112
|
+
|
|
70
113
|
server.registerPlugin(demoPlugin);
|
|
71
114
|
server.registerPlugin(workflowDemoPlugin);
|
|
72
115
|
|
|
73
116
|
// Register routes
|
|
74
117
|
server.use(authRouter);
|
|
118
|
+
server.use(permissionsRouter);
|
|
119
|
+
server.use(tenantsRouter);
|
|
75
120
|
server.use(demoRoutes);
|
|
76
121
|
server.use(exampleRouter);
|
|
77
122
|
|