@ahmadubaidillah/cli 1.1.3 → 1.1.4

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 (45) hide show
  1. package/dist/bin.js +59 -17
  2. package/dist/plugins/admin-panel/files/src/modules/admin/components/OverviewChart.tsx +56 -0
  3. package/dist/plugins/admin-panel/files/src/modules/admin/services/admin.service.ts +40 -0
  4. package/dist/plugins/admin-panel/files/src/modules/admin/views/AdminDashboard.tsx +106 -0
  5. package/dist/plugins/admin-panel/plugin.config.json +10 -0
  6. package/dist/plugins/admin-panel/tsconfig.json +12 -0
  7. package/dist/plugins/analytics/files/src/modules/analytics/services/analytics.service.ts +71 -0
  8. package/dist/plugins/analytics/plugin.config.json +8 -0
  9. package/dist/plugins/auth/files/src/modules/auth/auth.schema.ts +21 -0
  10. package/dist/plugins/auth/files/src/modules/auth/db/rls.ts +31 -0
  11. package/dist/plugins/auth/files/src/modules/auth/db/schema.ts +103 -0
  12. package/dist/plugins/auth/files/src/modules/auth/middleware/auth.middleware.ts +12 -0
  13. package/dist/plugins/auth/files/src/modules/auth/middleware/tenant.middleware.ts +50 -0
  14. package/dist/plugins/auth/files/src/modules/auth/routes/auth.routes.ts +40 -0
  15. package/dist/plugins/auth/files/src/modules/auth/services/auth.service.ts +113 -0
  16. package/dist/plugins/auth/plugin.config.json +9 -0
  17. package/dist/plugins/cms/files/src/modules/cms/cms.schema.ts +24 -0
  18. package/dist/plugins/cms/files/src/modules/cms/db/schema.ts +88 -0
  19. package/dist/plugins/cms/files/src/modules/cms/routes/cms.routes.ts +67 -0
  20. package/dist/plugins/cms/files/src/modules/cms/services/cms.service.ts +99 -0
  21. package/dist/plugins/cms/plugin.config.json +9 -0
  22. package/dist/plugins/deployment/files/Dockerfile +33 -0
  23. package/dist/plugins/deployment/files/docker-compose.yml +27 -0
  24. package/dist/plugins/deployment/files/vercel.json +14 -0
  25. package/dist/plugins/deployment/plugin.config.json +5 -0
  26. package/dist/plugins/email/files/src/modules/email/services/email.service.ts +30 -0
  27. package/dist/plugins/email/plugin.config.json +9 -0
  28. package/dist/plugins/file_upload/files/src/modules/storage/services/storage.service.ts +39 -0
  29. package/dist/plugins/file_upload/plugin.config.json +10 -0
  30. package/dist/plugins/github-actions/files/.github/workflows/ci.yml +34 -0
  31. package/dist/plugins/github-actions/plugin.config.json +14 -0
  32. package/dist/plugins/openapi/files/src/modules/openapi/openapi.routes.ts +17 -0
  33. package/dist/plugins/openapi/files/src/modules/openapi/openapi.schema.ts +10 -0
  34. package/dist/plugins/openapi/plugin.config.json +10 -0
  35. package/dist/plugins/payments/files/src/modules/billing/billing.schema.ts +14 -0
  36. package/dist/plugins/payments/files/src/modules/billing/routes/billing.routes.ts +57 -0
  37. package/dist/plugins/payments/files/src/modules/billing/services/stripe.service.ts +47 -0
  38. package/dist/plugins/payments/plugin.config.json +10 -0
  39. package/dist/plugins/queue/files/src/modules/queue/services/queue.service.ts +61 -0
  40. package/dist/plugins/queue/plugin.config.json +10 -0
  41. package/dist/plugins/search/files/src/modules/search/services/search.service.ts +98 -0
  42. package/dist/plugins/search/plugin.config.json +9 -0
  43. package/dist/plugins/websocket/files/src/modules/websocket/services/ws.service.ts +51 -0
  44. package/dist/plugins/websocket/plugin.config.json +7 -0
  45. package/package.json +2 -2
package/dist/bin.js CHANGED
@@ -32300,17 +32300,57 @@ var init_plugin_registry = __esm(() => {
32300
32300
  // packages/core/src/plugins/plugin-installer.ts
32301
32301
  var exports_plugin_installer = {};
32302
32302
  __export(exports_plugin_installer, {
32303
- installPlugin: () => installPlugin
32303
+ installPlugin: () => installPlugin,
32304
+ getPluginsRoot: () => getPluginsRoot
32304
32305
  });
32305
32306
  import { join as join7 } from "path";
32307
+ import { fileURLToPath } from "url";
32308
+ import { dirname as dirname2 } from "path";
32309
+ function getPluginsRoot() {
32310
+ const searchPaths = [
32311
+ join7(__dirname2, "plugins"),
32312
+ join7(__dirname2, "..", "plugins"),
32313
+ join7(__dirname2, "..", "..", "plugins"),
32314
+ join7(process.cwd(), "node_modules", "@ahmadubaidillah", "core", "plugins"),
32315
+ join7(process.cwd(), "packages", "plugins")
32316
+ ];
32317
+ for (const p of searchPaths) {
32318
+ if (existsSync6(p)) {
32319
+ if (existsSync6(join7(p, "auth")))
32320
+ return p;
32321
+ }
32322
+ }
32323
+ let current = __dirname2;
32324
+ const root = "/";
32325
+ while (current !== root) {
32326
+ const p = join7(current, "plugins");
32327
+ if (existsSync6(p) && existsSync6(join7(p, "auth"))) {
32328
+ return p;
32329
+ }
32330
+ const parent = dirname2(current);
32331
+ if (parent === current)
32332
+ break;
32333
+ current = parent;
32334
+ }
32335
+ return join7(process.cwd(), "packages", "plugins");
32336
+ }
32306
32337
  async function installPlugin(pluginName, projectDir, options) {
32307
- let pluginPath = join7(process.cwd(), "packages", "plugins", pluginName);
32338
+ const aliases = {
32339
+ payment: "payments",
32340
+ "file-upload": "file_upload",
32341
+ "file-uploads": "file_upload",
32342
+ "github-action": "github-actions",
32343
+ "deployment-vercel": "deployment"
32344
+ };
32345
+ const normalizedName = aliases[pluginName] || pluginName;
32346
+ const pluginsRoot = getPluginsRoot();
32347
+ let pluginPath = join7(pluginsRoot, normalizedName);
32308
32348
  if (!existsSync6(pluginPath)) {
32309
- console.log(`[INSTALL] Local plugin "${pluginName}" not found. Checking remote registry...`);
32349
+ console.log(`[INSTALL] Local plugin "${normalizedName}" not found. Checking remote registry...`);
32310
32350
  try {
32311
- pluginPath = await PluginRegistry.downloadPlugin(pluginName);
32351
+ pluginPath = await PluginRegistry.downloadPlugin(normalizedName);
32312
32352
  } catch (e) {
32313
- throw new Error(`Plugin "${pluginName}" not found locally or in the remote registry: ${e.message}`);
32353
+ throw new Error(`Plugin "${normalizedName}" not found locally or in the remote registry: ${e.message}`);
32314
32354
  }
32315
32355
  }
32316
32356
  const config = loadPlugin(pluginPath);
@@ -32358,13 +32398,15 @@ async function installPlugin(pluginName, projectDir, options) {
32358
32398
  pluginName
32359
32399
  };
32360
32400
  }
32361
- var import_fs_extra3, copySync2, readFileSync4, writeFileSync4, existsSync6;
32401
+ var import_fs_extra3, copySync2, readFileSync4, writeFileSync4, existsSync6, __filename2, __dirname2;
32362
32402
  var init_plugin_installer = __esm(() => {
32363
32403
  init_plugin_loader();
32364
32404
  init_index_min();
32365
32405
  init_plugin_registry();
32366
32406
  import_fs_extra3 = __toESM(require_lib2(), 1);
32367
32407
  ({ copySync: copySync2, readFileSync: readFileSync4, writeFileSync: writeFileSync4, existsSync: existsSync6 } = import_fs_extra3.default);
32408
+ __filename2 = fileURLToPath(import.meta.url);
32409
+ __dirname2 = dirname2(__filename2);
32368
32410
  });
32369
32411
 
32370
32412
  // packages/core/src/utils/doctor-checks.ts
@@ -34367,7 +34409,7 @@ var promptAgentKey = async () => {
34367
34409
  };
34368
34410
 
34369
34411
  // packages/core/src/engine/scaffolder.ts
34370
- import { join as join8, dirname as dirname2 } from "path";
34412
+ import { join as join8, dirname as dirname3 } from "path";
34371
34413
  import { existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
34372
34414
 
34373
34415
  // packages/core/src/engine/template-loader.ts
@@ -34437,16 +34479,16 @@ class DevForgeError extends Error {
34437
34479
  }
34438
34480
 
34439
34481
  // packages/core/src/engine/scaffolder.ts
34440
- import { fileURLToPath } from "url";
34441
- var __filename2 = fileURLToPath(import.meta.url);
34442
- var __dirname2 = dirname2(__filename2);
34482
+ import { fileURLToPath as fileURLToPath2 } from "url";
34483
+ var __filename3 = fileURLToPath2(import.meta.url);
34484
+ var __dirname3 = dirname3(__filename3);
34443
34485
  function getTemplatesRoot() {
34444
34486
  const searchPaths = [
34445
- join8(__dirname2, "templates"),
34446
- join8(__dirname2, "..", "templates"),
34447
- join8(__dirname2, "..", "..", "templates"),
34448
- join8(__dirname2, "..", "..", "..", "templates"),
34449
- join8(__dirname2, "..", "..", "..", "..", "templates"),
34487
+ join8(__dirname3, "templates"),
34488
+ join8(__dirname3, "..", "templates"),
34489
+ join8(__dirname3, "..", "..", "templates"),
34490
+ join8(__dirname3, "..", "..", "..", "templates"),
34491
+ join8(__dirname3, "..", "..", "..", "..", "templates"),
34450
34492
  join8(process.cwd(), "node_modules", "@ahmadubaidillah", "core", "templates"),
34451
34493
  join8(process.cwd(), "templates")
34452
34494
  ];
@@ -34457,14 +34499,14 @@ function getTemplatesRoot() {
34457
34499
  return p;
34458
34500
  }
34459
34501
  }
34460
- let current = __dirname2;
34502
+ let current = __dirname3;
34461
34503
  const root = "/";
34462
34504
  while (current !== root) {
34463
34505
  const p = join8(current, "templates");
34464
34506
  if (existsSync7(p) && existsSync7(join8(p, "saas", "template.config.json"))) {
34465
34507
  return p;
34466
34508
  }
34467
- const parent = dirname2(current);
34509
+ const parent = dirname3(current);
34468
34510
  if (parent === current)
34469
34511
  break;
34470
34512
  current = parent;
@@ -0,0 +1,56 @@
1
+ import React from 'react';
2
+ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
3
+
4
+ const data = [
5
+ { name: 'Mon', tenants: 40, users: 240 },
6
+ { name: 'Tue', tenants: 45, users: 310 },
7
+ { name: 'Wed', tenants: 50, users: 400 },
8
+ { name: 'Thu', tenants: 52, users: 430 },
9
+ { name: 'Fri', tenants: 60, users: 500 },
10
+ { name: 'Sat', tenants: 65, users: 520 },
11
+ { name: 'Sun', tenants: 75, users: 600 },
12
+ ];
13
+
14
+ export function OverviewChart() {
15
+ return (
16
+ <ResponsiveContainer width="100%" height="100%">
17
+ <LineChart
18
+ data={data}
19
+ margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
20
+ >
21
+ <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#E5E7EB" />
22
+ <XAxis
23
+ dataKey="name"
24
+ axisLine={false}
25
+ tickLine={false}
26
+ tick={{ fill: '#6B7280', fontSize: 12 }}
27
+ dy={10}
28
+ />
29
+ <YAxis
30
+ axisLine={false}
31
+ tickLine={false}
32
+ tick={{ fill: '#6B7280', fontSize: 12 }}
33
+ dx={-10}
34
+ />
35
+ <Tooltip
36
+ contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)' }}
37
+ />
38
+ <Line
39
+ type="monotone"
40
+ dataKey="users"
41
+ stroke="#4F46E5"
42
+ strokeWidth={3}
43
+ dot={{ r: 4, fill: '#4F46E5', strokeWidth: 2, stroke: '#fff' }}
44
+ activeDot={{ r: 6 }}
45
+ />
46
+ <Line
47
+ type="monotone"
48
+ dataKey="tenants"
49
+ stroke="#10B981"
50
+ strokeWidth={3}
51
+ dot={{ r: 4, fill: '#10B981', strokeWidth: 2, stroke: '#fff' }}
52
+ />
53
+ </LineChart>
54
+ </ResponsiveContainer>
55
+ );
56
+ }
@@ -0,0 +1,40 @@
1
+ import { EventBus } from '../../../../../../../core/src/hooks/event-bus';
2
+
3
+ export class AdminMetricsService {
4
+ constructor(private eventBus: EventBus) {}
5
+
6
+ /**
7
+ * Broadcasts the current metrics to all listeners via the EventBus
8
+ */
9
+ async broadcastMetrics(db: any) {
10
+ try {
11
+ // These queries would hit the actual Drizzle instances
12
+ // For scaffolding demonstration, using mock values if DB not provided
13
+
14
+ let activeTenants = 68;
15
+ let totalUsers = 420;
16
+
17
+ if (db) {
18
+ // activeTenants = await db.select({ count: sql`count(*)` }).from(organization);
19
+ // totalUsers = await db.select({ count: sql`count(*)` }).from(user);
20
+ }
21
+
22
+ const payload = {
23
+ type: 'PLATFORM_METRICS_UPDATE',
24
+ payload: {
25
+ activeTenants,
26
+ totalUsers,
27
+ sysHealth: 'optimal'
28
+ }
29
+ };
30
+
31
+ // This will be caught by the WebSocketService from Sprint 7
32
+ this.eventBus.dispatch('notification.broadcast', payload);
33
+
34
+ return payload.payload;
35
+ } catch (error: any) {
36
+ console.error(`[AdminMetricsService] Failed to broadcast metrics: ${error.message}`);
37
+ throw error;
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,106 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Users, Activity, Settings, Database, Server } from 'lucide-react';
3
+ import { OverviewChart } from '../components/OverviewChart';
4
+
5
+ export function AdminDashboard() {
6
+ const [stats, setStats] = useState({
7
+ activeTenants: 0,
8
+ totalUsers: 0,
9
+ sysHealth: 'checking...',
10
+ });
11
+
12
+ useEffect(() => {
13
+ // 1. We connect to our WebSocket Plugin (Sprint 7)
14
+ const ws = new WebSocket(`ws://${window.location.host}/ws/admin`);
15
+
16
+ ws.onmessage = (event) => {
17
+ const data = JSON.parse(event.data);
18
+ if (data.type === 'PLATFORM_METRICS_UPDATE') {
19
+ setStats(data.payload);
20
+ }
21
+ };
22
+
23
+ return () => ws.close();
24
+ }, []);
25
+
26
+ return (
27
+ <div className="min-h-screen bg-gray-50 flex">
28
+ {/* Sidebar Navigation */}
29
+ <nav className="w-64 bg-white border-r border-gray-200 p-6 flex flex-col space-y-4">
30
+ <div className="text-xl font-bold tracking-tight text-gray-900 mb-8">
31
+ Platform Admin
32
+ </div>
33
+ <a href="#overview" className="flex items-center space-x-3 text-blue-600 bg-blue-50 px-3 py-2 rounded-lg font-medium">
34
+ <Activity className="w-5 h-5" />
35
+ <span>Overview</span>
36
+ </a>
37
+ <a href="#tenants" className="flex items-center space-x-3 text-gray-600 hover:bg-gray-50 px-3 py-2 rounded-lg font-medium transition-colors">
38
+ <Database className="w-5 h-5" />
39
+ <span>Tenants / Orgs</span>
40
+ </a>
41
+ <a href="#users" className="flex items-center space-x-3 text-gray-600 hover:bg-gray-50 px-3 py-2 rounded-lg font-medium transition-colors">
42
+ <Users className="w-5 h-5" />
43
+ <span>Global Users</span>
44
+ </a>
45
+ <a href="#system" className="flex items-center space-x-3 text-gray-600 hover:bg-gray-50 px-3 py-2 rounded-lg font-medium transition-colors">
46
+ <Server className="w-5 h-5" />
47
+ <span>System Health</span>
48
+ </a>
49
+ <div className="flex-grow" />
50
+ <a href="#settings" className="flex items-center space-x-3 text-gray-500 hover:text-gray-900 px-3 py-2 rounded-lg font-medium transition-colors">
51
+ <Settings className="w-5 h-5" />
52
+ <span>Settings</span>
53
+ </a>
54
+ </nav>
55
+
56
+ {/* Main Content Area */}
57
+ <main className="flex-1 p-8">
58
+ <header className="mb-8">
59
+ <h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
60
+ <p className="text-gray-500 mt-1">Real-time platform overview and metrics.</p>
61
+ </header>
62
+
63
+ {/* Top Stat Cards */}
64
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
65
+ <div className="bg-white p-6 rounded-xl border border-gray-200 shadow-sm flex items-center shadow-sm">
66
+ <div className="p-3 bg-blue-100 rounded-lg text-blue-600 mr-4">
67
+ <Database className="w-6 h-6" />
68
+ </div>
69
+ <div>
70
+ <p className="text-sm font-medium text-gray-500">Active Tenants</p>
71
+ <h3 className="text-2xl font-bold text-gray-900">{stats.activeTenants}</h3>
72
+ </div>
73
+ </div>
74
+
75
+ <div className="bg-white p-6 rounded-xl border border-gray-200 shadow-sm flex items-center shadow-sm">
76
+ <div className="p-3 bg-indigo-100 rounded-lg text-indigo-600 mr-4">
77
+ <Users className="w-6 h-6" />
78
+ </div>
79
+ <div>
80
+ <p className="text-sm font-medium text-gray-500">Total Global Users</p>
81
+ <h3 className="text-2xl font-bold text-gray-900">{stats.totalUsers}</h3>
82
+ </div>
83
+ </div>
84
+
85
+ <div className="bg-white p-6 rounded-xl border border-gray-200 shadow-sm flex items-center shadow-sm">
86
+ <div className="p-3 bg-emerald-100 rounded-lg text-emerald-600 mr-4">
87
+ <Activity className="w-6 h-6" />
88
+ </div>
89
+ <div>
90
+ <p className="text-sm font-medium text-gray-500">System Status</p>
91
+ <h3 className="text-xl font-bold text-gray-900 capitalize">{stats.sysHealth}</h3>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ {/* Charts & Tables Area */}
97
+ <div className="bg-white border border-gray-200 rounded-xl shadow-sm p-6">
98
+ <h2 className="text-lg font-semibold text-gray-900 mb-6">Activity Timeline</h2>
99
+ <div className="h-72 w-full">
100
+ <OverviewChart />
101
+ </div>
102
+ </div>
103
+ </main>
104
+ </div>
105
+ );
106
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "admin-panel",
3
+ "description": "God Mode Dashboard and Super-Admin Management Interface.",
4
+ "compatibleTemplates": ["saas", "marketplace"],
5
+ "packageDependencies": {
6
+ "lucide-react": "^0.300.0",
7
+ "recharts": "^2.10.0"
8
+ },
9
+ "packageDevDependencies": {}
10
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "node",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true
11
+ }
12
+ }
@@ -0,0 +1,71 @@
1
+ import { PostHog } from 'posthog-node';
2
+
3
+ /**
4
+ * AnalyticsService
5
+ *
6
+ * Provides a unified way to track user events across the application.
7
+ * Defaults to PostHog but follows a pattern that can be extended.
8
+ */
9
+ export class AnalyticsService {
10
+ private client: PostHog | null = null;
11
+
12
+ constructor(apiKey?: string, host?: string) {
13
+ if (apiKey) {
14
+ this.client = new PostHog(apiKey, {
15
+ host: host || 'https://app.posthog.com',
16
+ });
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Capture a custom event.
22
+ */
23
+ async capture(distinctId: string, event: string, properties?: Record<string, any>) {
24
+ try {
25
+ if (!this.client) {
26
+ console.log(`[Analytics Mock] Event: ${event} for ${distinctId}`, properties);
27
+ return;
28
+ }
29
+
30
+ this.client.capture({
31
+ distinctId,
32
+ event,
33
+ properties,
34
+ });
35
+ } catch (error: any) {
36
+ console.error(`[AnalyticsService] Capture failed: ${error.message}`);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Identify a user with custom traits.
42
+ */
43
+ async identify(distinctId: string, properties: Record<string, any>) {
44
+ try {
45
+ if (!this.client) {
46
+ console.log(`[Analytics Mock] Identify: ${distinctId}`, properties);
47
+ return;
48
+ }
49
+
50
+ this.client.identify({
51
+ distinctId,
52
+ properties,
53
+ });
54
+ } catch (error: any) {
55
+ console.error(`[AnalyticsService] Identify failed: ${error.message}`);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Flush pending events (useful for serverless environments).
61
+ */
62
+ async flush() {
63
+ try {
64
+ if (this.client) {
65
+ await this.client.flush();
66
+ }
67
+ } catch (error: any) {
68
+ console.error(`[AnalyticsService] Flush failed: ${error.message}`);
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "analytics",
3
+ "description": "Event tracking and analytics integration for PostHog or Segment.",
4
+ "compatibleTemplates": ["saas", "marketplace", "cms", "ai_wrapper", "booking"],
5
+ "packageDependencies": {
6
+ "posthog-node": "latest"
7
+ }
8
+ }
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+
3
+ export const signupSchema = z.object({
4
+ email: z.string().email(),
5
+ password: z.string().min(8),
6
+ name: z.string().optional(),
7
+ });
8
+
9
+ export const loginSchema = z.object({
10
+ email: z.string().email(),
11
+ password: z.string(),
12
+ });
13
+
14
+ export const organizationSchema = z.object({
15
+ name: z.string().min(1),
16
+ slug: z.string().min(1),
17
+ });
18
+
19
+ export type SignupInput = z.infer<typeof signupSchema>;
20
+ export type LoginInput = z.infer<typeof loginSchema>;
21
+ export type OrganizationInput = z.infer<typeof organizationSchema>;
@@ -0,0 +1,31 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import { NodePgDatabase } from 'drizzle-orm/node-postgres';
3
+
4
+ /**
5
+ * setTenantSession
6
+ *
7
+ * Sets the current organization ID in the Postgres session.
8
+ * Used for Row Level Security (RLS).
9
+ */
10
+ export async function setTenantSession(db: NodePgDatabase<any>, organizationId: string) {
11
+ await db.execute(sql`SET LOCAL app.current_organization_id = ${organizationId}`);
12
+ }
13
+
14
+ /**
15
+ * clearTenantSession
16
+ *
17
+ * Clears the organization ID from the session.
18
+ */
19
+ export async function clearTenantSession(db: NodePgDatabase<any>) {
20
+ await db.execute(sql`SET LOCAL app.current_organization_id = ''`);
21
+ }
22
+
23
+ /**
24
+ * rlsPolicy
25
+ *
26
+ * Returns the SQL expression for the RLS policy.
27
+ * Usage in Postgres:
28
+ * CREATE POLICY tenant_isolation ON table_name
29
+ * USING (organization_id = current_setting('app.current_organization_id', true));
30
+ */
31
+ export const rlsPolicySql = sql`organization_id = current_setting('app.current_organization_id', true)`;
@@ -0,0 +1,103 @@
1
+ import { pgTable, text, timestamp, boolean, index } from "drizzle-orm/pg-core";
2
+
3
+ export const user = pgTable("user", {
4
+ id: text("id").primaryKey(),
5
+ name: text("name").notNull(),
6
+ email: text("email").notNull().unique(),
7
+ emailVerified: boolean("email_verified").default(false).notNull(),
8
+ image: text("image"),
9
+ createdAt: timestamp("created_at").defaultNow().notNull(),
10
+ updatedAt: timestamp("updated_at")
11
+ .defaultNow()
12
+ .$onUpdate(() => new Date())
13
+ .notNull(),
14
+ });
15
+
16
+ export const session = pgTable("session", {
17
+ id: text("id").primaryKey(),
18
+ expiresAt: timestamp("expires_at").notNull(),
19
+ token: text("token").notNull().unique(),
20
+ createdAt: timestamp("created_at").defaultNow().notNull(),
21
+ updatedAt: timestamp("updated_at")
22
+ .defaultNow()
23
+ .$onUpdate(() => new Date())
24
+ .notNull(),
25
+ ipAddress: text("ip_address"),
26
+ userAgent: text("user_agent"),
27
+ userId: text("user_id")
28
+ .notNull()
29
+ .references(() => user.id, { onDelete: "cascade" }),
30
+ });
31
+
32
+ export const account = pgTable("account", {
33
+ id: text("id").primaryKey(),
34
+ accountId: text("account_id").notNull(),
35
+ providerId: text("provider_id").notNull(),
36
+ userId: text("user_id")
37
+ .notNull()
38
+ .references(() => user.id, { onDelete: "cascade" }),
39
+ accessToken: text("access_token"),
40
+ refreshToken: text("refresh_token"),
41
+ idToken: text("id_token"),
42
+ accessTokenExpiresAt: timestamp("access_token_expires_at"),
43
+ refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
44
+ scope: text("scope"),
45
+ password: text("password"),
46
+ createdAt: timestamp("created_at").defaultNow().notNull(),
47
+ updatedAt: timestamp("updated_at")
48
+ .defaultNow()
49
+ .$onUpdate(() => new Date())
50
+ .notNull(),
51
+ });
52
+
53
+ export const verification = pgTable("verification", {
54
+ id: text("id").primaryKey(),
55
+ identifier: text("identifier").notNull(),
56
+ value: text("value").notNull(),
57
+ expiresAt: timestamp("expires_at").notNull(),
58
+ createdAt: timestamp("created_at").defaultNow().notNull(),
59
+ updatedAt: timestamp("updated_at")
60
+ .defaultNow()
61
+ .$onUpdate(() => new Date())
62
+ .notNull(),
63
+ });
64
+
65
+ /**
66
+ * Organization Plugin Tables
67
+ */
68
+ export const organization = pgTable("organization", {
69
+ id: text("id").primaryKey(),
70
+ name: text("name").notNull(),
71
+ slug: text("slug").notNull().unique(),
72
+ logo: text("logo"),
73
+ createdAt: timestamp("created_at").defaultNow().notNull(),
74
+ metadata: text("metadata"),
75
+ }, (table) => [
76
+ index("organization_slug_idx").on(table.slug),
77
+ ]);
78
+
79
+ export const member = pgTable("member", {
80
+ id: text("id").primaryKey(),
81
+ organizationId: text("organization_id")
82
+ .notNull()
83
+ .references(() => organization.id, { onDelete: "cascade" }),
84
+ userId: text("user_id")
85
+ .notNull()
86
+ .references(() => user.id, { onDelete: "cascade" }),
87
+ role: text("role").notNull(),
88
+ createdAt: timestamp("created_at").defaultNow().notNull(),
89
+ });
90
+
91
+ export const invitation = pgTable("invitation", {
92
+ id: text("id").primaryKey(),
93
+ organizationId: text("organization_id")
94
+ .notNull()
95
+ .references(() => organization.id, { onDelete: "cascade" }),
96
+ email: text("email").notNull(),
97
+ role: text("role").notNull(),
98
+ status: text("status").notNull(), // pending, accepted, rejected
99
+ expiresAt: timestamp("expires_at").notNull(),
100
+ inviterId: text("inviter_id")
101
+ .notNull()
102
+ .references(() => user.id, { onDelete: "cascade" }),
103
+ });
@@ -0,0 +1,12 @@
1
+ import { createMiddleware } from 'hono/factory';
2
+
3
+ export const authMiddleware = createMiddleware(async (c, next) => {
4
+ const token = c.req.header('Authorization');
5
+
6
+ if (!token) {
7
+ return c.json({ error: 'Unauthorized' }, 401);
8
+ }
9
+
10
+ // Token verification logic
11
+ await next();
12
+ });
@@ -0,0 +1,50 @@
1
+ import { createMiddleware } from 'hono/factory';
2
+ import { HTTPException } from 'hono/http-exception';
3
+ import { AuthService } from '../services/auth.service';
4
+
5
+ /**
6
+ * tenantMiddleware
7
+ *
8
+ * Extracts the organization slug from the URL path (/ :tenant /...)
9
+ * and validates its existence. It also checks if the user has access
10
+ * to this organization if a session exists.
11
+ */
12
+ export const tenantMiddleware = (authService: AuthService) => createMiddleware(async (c, next) => {
13
+ const tenantSlug = c.req.param('tenant');
14
+
15
+ if (!tenantSlug) {
16
+ return await next();
17
+ }
18
+
19
+ // Look up organization by slug
20
+ // In a real implementation, we'd use a repository or the better-auth api
21
+ const org = await authService.auth.api.getOrganizationBySlug({
22
+ query: { slug: tenantSlug }
23
+ });
24
+
25
+ if (!org) {
26
+ throw new HTTPException(404, { message: 'Organization not found' });
27
+ }
28
+
29
+ // Set organization in context
30
+ c.set('organization', org);
31
+ c.set('organizationId', org.id);
32
+
33
+ // Check user membership if authenticated
34
+ const session = await authService.getSession(c.req.raw);
35
+ if (session) {
36
+ const member = await authService.auth.api.getMember({
37
+ query: {
38
+ organizationId: org.id,
39
+ userId: session.user.id
40
+ }
41
+ });
42
+
43
+ if (!member) {
44
+ throw new HTTPException(403, { message: 'You are not a member of this organization' });
45
+ }
46
+ c.set('member', member);
47
+ }
48
+
49
+ await next();
50
+ });