@ahmadubaidillah/cli 1.1.3 → 1.1.5
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/dist/bin.js +59 -17
- package/dist/plugins/admin-panel/files/src/modules/admin/components/OverviewChart.tsx +56 -0
- package/dist/plugins/admin-panel/files/src/modules/admin/services/admin.service.ts +40 -0
- package/dist/plugins/admin-panel/files/src/modules/admin/views/AdminDashboard.tsx +106 -0
- package/dist/plugins/admin-panel/plugin.config.json +10 -0
- package/dist/plugins/admin-panel/tsconfig.json +12 -0
- package/dist/plugins/analytics/files/src/modules/analytics/services/analytics.service.ts +71 -0
- package/dist/plugins/analytics/plugin.config.json +8 -0
- package/dist/plugins/auth/files/src/modules/auth/auth.schema.ts +21 -0
- package/dist/plugins/auth/files/src/modules/auth/db/rls.ts +31 -0
- package/dist/plugins/auth/files/src/modules/auth/db/schema.ts +103 -0
- package/dist/plugins/auth/files/src/modules/auth/middleware/auth.middleware.ts +12 -0
- package/dist/plugins/auth/files/src/modules/auth/middleware/tenant.middleware.ts +50 -0
- package/dist/plugins/auth/files/src/modules/auth/routes/auth.routes.ts +40 -0
- package/dist/plugins/auth/files/src/modules/auth/services/auth.service.ts +113 -0
- package/dist/plugins/auth/plugin.config.json +9 -0
- package/dist/plugins/cms/files/src/modules/cms/cms.schema.ts +24 -0
- package/dist/plugins/cms/files/src/modules/cms/db/schema.ts +88 -0
- package/dist/plugins/cms/files/src/modules/cms/routes/cms.routes.ts +67 -0
- package/dist/plugins/cms/files/src/modules/cms/services/cms.service.ts +99 -0
- package/dist/plugins/cms/plugin.config.json +9 -0
- package/dist/plugins/deployment/files/Dockerfile +33 -0
- package/dist/plugins/deployment/files/docker-compose.yml +27 -0
- package/dist/plugins/deployment/files/vercel.json +14 -0
- package/dist/plugins/deployment/plugin.config.json +5 -0
- package/dist/plugins/email/files/src/modules/email/services/email.service.ts +30 -0
- package/dist/plugins/email/plugin.config.json +9 -0
- package/dist/plugins/file_upload/files/src/modules/storage/services/storage.service.ts +39 -0
- package/dist/plugins/file_upload/plugin.config.json +10 -0
- package/dist/plugins/github-actions/files/.github/workflows/ci.yml +34 -0
- package/dist/plugins/github-actions/plugin.config.json +14 -0
- package/dist/plugins/openapi/files/src/modules/openapi/openapi.routes.ts +17 -0
- package/dist/plugins/openapi/files/src/modules/openapi/openapi.schema.ts +10 -0
- package/dist/plugins/openapi/plugin.config.json +10 -0
- package/dist/plugins/payments/files/src/modules/billing/billing.schema.ts +14 -0
- package/dist/plugins/payments/files/src/modules/billing/routes/billing.routes.ts +57 -0
- package/dist/plugins/payments/files/src/modules/billing/services/stripe.service.ts +47 -0
- package/dist/plugins/payments/plugin.config.json +10 -0
- package/dist/plugins/queue/files/src/modules/queue/services/queue.service.ts +61 -0
- package/dist/plugins/queue/plugin.config.json +10 -0
- package/dist/plugins/search/files/src/modules/search/services/search.service.ts +98 -0
- package/dist/plugins/search/plugin.config.json +9 -0
- package/dist/plugins/websocket/files/src/modules/websocket/services/ws.service.ts +51 -0
- package/dist/plugins/websocket/plugin.config.json +7 -0
- package/dist/templates/saas/files/package.json +1 -1
- package/package.json +2 -2
- package/dist/templates/templates/ai_wrapper/files/README.md +0 -3
- package/dist/templates/templates/ai_wrapper/files/package.json +0 -16
- package/dist/templates/templates/ai_wrapper/files/src/app.ts +0 -23
- package/dist/templates/templates/ai_wrapper/files/src/modules/prompts/prompts.routes.ts +0 -64
- package/dist/templates/templates/ai_wrapper/files/src/modules/usage/usage.routes.ts +0 -12
- package/dist/templates/templates/ai_wrapper/template.config.json +0 -16
- package/dist/templates/templates/booking/files/README.md +0 -3
- package/dist/templates/templates/booking/files/package.json +0 -16
- package/dist/templates/templates/booking/files/src/app.ts +0 -23
- package/dist/templates/templates/booking/files/src/modules/availability/availability.routes.ts +0 -12
- package/dist/templates/templates/booking/files/src/modules/calendar/calendar.routes.ts +0 -12
- package/dist/templates/templates/booking/template.config.json +0 -17
- package/dist/templates/templates/cms/files/frontend/src/components/editor/Editor.tsx +0 -36
- package/dist/templates/templates/cms/files/package.json +0 -22
- package/dist/templates/templates/cms/files/src/app.ts +0 -23
- package/dist/templates/templates/cms/files/src/modules/media/media.routes.ts +0 -26
- package/dist/templates/templates/cms/files/src/modules/posts/post.routes.ts +0 -33
- package/dist/templates/templates/cms/template.config.json +0 -17
- package/dist/templates/templates/crm/files/README.md +0 -3
- package/dist/templates/templates/crm/files/package.json +0 -16
- package/dist/templates/templates/crm/files/src/app.ts +0 -23
- package/dist/templates/templates/crm/files/src/modules/contacts/contacts.routes.ts +0 -12
- package/dist/templates/templates/crm/files/src/modules/pipelines/pipelines.routes.ts +0 -12
- package/dist/templates/templates/crm/template.config.json +0 -19
- package/dist/templates/templates/finance/files/README.md +0 -3
- package/dist/templates/templates/finance/files/package.json +0 -16
- package/dist/templates/templates/finance/files/src/app.ts +0 -23
- package/dist/templates/templates/finance/files/src/modules/reports/reports.routes.ts +0 -12
- package/dist/templates/templates/finance/files/src/modules/transactions/transactions.routes.ts +0 -12
- package/dist/templates/templates/finance/template.config.json +0 -15
- package/dist/templates/templates/landing/files/README.md +0 -10
- package/dist/templates/templates/landing/files/package.json +0 -17
- package/dist/templates/templates/landing/template.config.json +0 -15
- package/dist/templates/templates/marketplace/files/README.md +0 -3
- package/dist/templates/templates/marketplace/files/package.json +0 -16
- package/dist/templates/templates/marketplace/files/src/app.ts +0 -25
- package/dist/templates/templates/marketplace/files/src/modules/orders/orders.routes.ts +0 -12
- package/dist/templates/templates/marketplace/files/src/modules/products/products.routes.ts +0 -12
- package/dist/templates/templates/marketplace/files/src/modules/vendors/vendors.routes.ts +0 -12
- package/dist/templates/templates/marketplace/template.config.json +0 -25
- package/dist/templates/templates/preact/files/package.json +0 -18
- package/dist/templates/templates/preact/files/src/main.jsx +0 -4
- package/dist/templates/templates/preact/template.config.json +0 -11
- package/dist/templates/templates/saas/files/package.json +0 -24
- package/dist/templates/templates/saas/files/playwright.config.ts +0 -25
- package/dist/templates/templates/saas/files/src/app.ts +0 -32
- package/dist/templates/templates/saas/files/src/core/env.ts +0 -24
- package/dist/templates/templates/saas/files/src/core/errors.ts +0 -39
- package/dist/templates/templates/saas/files/src/modules/users/repositories/user.repository.ts +0 -25
- package/dist/templates/templates/saas/files/src/modules/users/routes/user.routes.ts +0 -33
- package/dist/templates/templates/saas/files/src/modules/users/services/user.service.ts +0 -24
- package/dist/templates/templates/saas/files/src/modules/users/validators/user.validator.ts +0 -17
- package/dist/templates/templates/saas/files/tests/e2e/basic.spec.ts +0 -13
- package/dist/templates/templates/saas/template.config.json +0 -26
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
|
-
|
|
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 "${
|
|
32349
|
+
console.log(`[INSTALL] Local plugin "${normalizedName}" not found. Checking remote registry...`);
|
|
32310
32350
|
try {
|
|
32311
|
-
pluginPath = await PluginRegistry.downloadPlugin(
|
|
32351
|
+
pluginPath = await PluginRegistry.downloadPlugin(normalizedName);
|
|
32312
32352
|
} catch (e) {
|
|
32313
|
-
throw new Error(`Plugin "${
|
|
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
|
|
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
|
|
34442
|
-
var
|
|
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(
|
|
34446
|
-
join8(
|
|
34447
|
-
join8(
|
|
34448
|
-
join8(
|
|
34449
|
-
join8(
|
|
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 =
|
|
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 =
|
|
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,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,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
|
+
});
|