@ahmadubaidillah/core 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/index.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 -1
package/dist/index.js CHANGED
@@ -26349,17 +26349,57 @@ var init_plugin_registry = __esm(() => {
26349
26349
  // packages/core/src/plugins/plugin-installer.ts
26350
26350
  var exports_plugin_installer = {};
26351
26351
  __export(exports_plugin_installer, {
26352
- installPlugin: () => installPlugin
26352
+ installPlugin: () => installPlugin,
26353
+ getPluginsRoot: () => getPluginsRoot
26353
26354
  });
26354
26355
  import { join as join5 } from "path";
26356
+ import { fileURLToPath } from "url";
26357
+ import { dirname } from "path";
26358
+ function getPluginsRoot() {
26359
+ const searchPaths = [
26360
+ join5(__dirname2, "plugins"),
26361
+ join5(__dirname2, "..", "plugins"),
26362
+ join5(__dirname2, "..", "..", "plugins"),
26363
+ join5(process.cwd(), "node_modules", "@ahmadubaidillah", "core", "plugins"),
26364
+ join5(process.cwd(), "packages", "plugins")
26365
+ ];
26366
+ for (const p of searchPaths) {
26367
+ if (existsSync4(p)) {
26368
+ if (existsSync4(join5(p, "auth")))
26369
+ return p;
26370
+ }
26371
+ }
26372
+ let current = __dirname2;
26373
+ const root = "/";
26374
+ while (current !== root) {
26375
+ const p = join5(current, "plugins");
26376
+ if (existsSync4(p) && existsSync4(join5(p, "auth"))) {
26377
+ return p;
26378
+ }
26379
+ const parent = dirname(current);
26380
+ if (parent === current)
26381
+ break;
26382
+ current = parent;
26383
+ }
26384
+ return join5(process.cwd(), "packages", "plugins");
26385
+ }
26355
26386
  async function installPlugin(pluginName, projectDir, options) {
26356
- let pluginPath = join5(process.cwd(), "packages", "plugins", pluginName);
26387
+ const aliases = {
26388
+ payment: "payments",
26389
+ "file-upload": "file_upload",
26390
+ "file-uploads": "file_upload",
26391
+ "github-action": "github-actions",
26392
+ "deployment-vercel": "deployment"
26393
+ };
26394
+ const normalizedName = aliases[pluginName] || pluginName;
26395
+ const pluginsRoot = getPluginsRoot();
26396
+ let pluginPath = join5(pluginsRoot, normalizedName);
26357
26397
  if (!existsSync4(pluginPath)) {
26358
- console.log(`[INSTALL] Local plugin "${pluginName}" not found. Checking remote registry...`);
26398
+ console.log(`[INSTALL] Local plugin "${normalizedName}" not found. Checking remote registry...`);
26359
26399
  try {
26360
- pluginPath = await PluginRegistry.downloadPlugin(pluginName);
26400
+ pluginPath = await PluginRegistry.downloadPlugin(normalizedName);
26361
26401
  } catch (e) {
26362
- throw new Error(`Plugin "${pluginName}" not found locally or in the remote registry: ${e.message}`);
26402
+ throw new Error(`Plugin "${normalizedName}" not found locally or in the remote registry: ${e.message}`);
26363
26403
  }
26364
26404
  }
26365
26405
  const config = loadPlugin(pluginPath);
@@ -26407,13 +26447,15 @@ async function installPlugin(pluginName, projectDir, options) {
26407
26447
  pluginName
26408
26448
  };
26409
26449
  }
26410
- var import_fs_extra3, copySync2, readFileSync4, writeFileSync2, existsSync4;
26450
+ var import_fs_extra3, copySync2, readFileSync4, writeFileSync2, existsSync4, __filename2, __dirname2;
26411
26451
  var init_plugin_installer = __esm(() => {
26412
26452
  init_plugin_loader();
26413
26453
  init_index_min();
26414
26454
  init_plugin_registry();
26415
26455
  import_fs_extra3 = __toESM(require_lib(), 1);
26416
26456
  ({ copySync: copySync2, readFileSync: readFileSync4, writeFileSync: writeFileSync2, existsSync: existsSync4 } = import_fs_extra3.default);
26457
+ __filename2 = fileURLToPath(import.meta.url);
26458
+ __dirname2 = dirname(__filename2);
26417
26459
  });
26418
26460
 
26419
26461
  // packages/core/src/engine/template-loader.ts
@@ -26464,7 +26506,7 @@ function composeTemplate(templatePath, targetDir, options) {
26464
26506
  });
26465
26507
  }
26466
26508
  // packages/core/src/engine/scaffolder.ts
26467
- import { join as join6, dirname } from "path";
26509
+ import { join as join6, dirname as dirname2 } from "path";
26468
26510
  import { existsSync as existsSync5, mkdirSync } from "fs";
26469
26511
  init_lib();
26470
26512
 
@@ -26483,16 +26525,16 @@ class DevForgeError extends Error {
26483
26525
  }
26484
26526
 
26485
26527
  // packages/core/src/engine/scaffolder.ts
26486
- import { fileURLToPath } from "url";
26487
- var __filename2 = fileURLToPath(import.meta.url);
26488
- var __dirname2 = dirname(__filename2);
26528
+ import { fileURLToPath as fileURLToPath2 } from "url";
26529
+ var __filename3 = fileURLToPath2(import.meta.url);
26530
+ var __dirname3 = dirname2(__filename3);
26489
26531
  function getTemplatesRoot() {
26490
26532
  const searchPaths = [
26491
- join6(__dirname2, "templates"),
26492
- join6(__dirname2, "..", "templates"),
26493
- join6(__dirname2, "..", "..", "templates"),
26494
- join6(__dirname2, "..", "..", "..", "templates"),
26495
- join6(__dirname2, "..", "..", "..", "..", "templates"),
26533
+ join6(__dirname3, "templates"),
26534
+ join6(__dirname3, "..", "templates"),
26535
+ join6(__dirname3, "..", "..", "templates"),
26536
+ join6(__dirname3, "..", "..", "..", "templates"),
26537
+ join6(__dirname3, "..", "..", "..", "..", "templates"),
26496
26538
  join6(process.cwd(), "node_modules", "@ahmadubaidillah", "core", "templates"),
26497
26539
  join6(process.cwd(), "templates")
26498
26540
  ];
@@ -26503,14 +26545,14 @@ function getTemplatesRoot() {
26503
26545
  return p;
26504
26546
  }
26505
26547
  }
26506
- let current = __dirname2;
26548
+ let current = __dirname3;
26507
26549
  const root = "/";
26508
26550
  while (current !== root) {
26509
26551
  const p = join6(current, "templates");
26510
26552
  if (existsSync5(p) && existsSync5(join6(p, "saas", "template.config.json"))) {
26511
26553
  return p;
26512
26554
  }
26513
- const parent = dirname(current);
26555
+ const parent = dirname2(current);
26514
26556
  if (parent === current)
26515
26557
  break;
26516
26558
  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
+ });