@jhits/dashboard 0.0.1
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/README.md +36 -0
- package/next.config.ts +32 -0
- package/package.json +79 -0
- package/postcss.config.mjs +7 -0
- package/src/api/README.md +72 -0
- package/src/api/masterRouter.ts +150 -0
- package/src/api/pluginRouter.ts +135 -0
- package/src/app/[locale]/(auth)/layout.tsx +30 -0
- package/src/app/[locale]/(auth)/login/page.tsx +201 -0
- package/src/app/[locale]/catch-all/page.tsx +10 -0
- package/src/app/[locale]/dashboard/[...pluginRoute]/page.tsx +98 -0
- package/src/app/[locale]/dashboard/layout.tsx +42 -0
- package/src/app/[locale]/dashboard/page.tsx +121 -0
- package/src/app/[locale]/dashboard/preferences/page.tsx +295 -0
- package/src/app/[locale]/dashboard/profile/page.tsx +491 -0
- package/src/app/[locale]/layout.tsx +28 -0
- package/src/app/actions/preferences.ts +40 -0
- package/src/app/actions/user.ts +191 -0
- package/src/app/api/auth/[...nextauth]/route.ts +6 -0
- package/src/app/api/plugin-images/list/route.ts +96 -0
- package/src/app/api/plugin-images/upload/route.ts +88 -0
- package/src/app/api/telemetry/log/route.ts +10 -0
- package/src/app/api/telemetry/route.ts +12 -0
- package/src/app/api/uploads/[filename]/route.ts +33 -0
- package/src/app/globals.css +181 -0
- package/src/app/layout.tsx +4 -0
- package/src/assets/locales/en/common.json +47 -0
- package/src/assets/locales/nl/common.json +48 -0
- package/src/assets/locales/sv/common.json +48 -0
- package/src/assets/plugins.json +42 -0
- package/src/assets/public/Logo_JH_black.jpg +0 -0
- package/src/assets/public/Logo_JH_black.png +0 -0
- package/src/assets/public/Logo_JH_white.png +0 -0
- package/src/assets/public/animated-logo-white.svg +5 -0
- package/src/assets/public/logo_black.svg +5 -0
- package/src/assets/public/logo_white.svg +5 -0
- package/src/assets/public/noimagefound.jpg +0 -0
- package/src/components/DashboardCatchAll.tsx +95 -0
- package/src/components/DashboardRootLayout.tsx +37 -0
- package/src/components/PluginNotFound.tsx +24 -0
- package/src/components/Providers.tsx +59 -0
- package/src/components/dashboard/Sidebar.tsx +263 -0
- package/src/components/dashboard/Topbar.tsx +363 -0
- package/src/components/page.tsx +130 -0
- package/src/config.ts +230 -0
- package/src/i18n/navigation.ts +7 -0
- package/src/i18n/request.ts +41 -0
- package/src/i18n/routing.ts +35 -0
- package/src/i18n/translations.ts +20 -0
- package/src/index.tsx +69 -0
- package/src/lib/auth.ts +159 -0
- package/src/lib/db.ts +11 -0
- package/src/lib/get-website-info.ts +78 -0
- package/src/lib/modules-config.ts +68 -0
- package/src/lib/mongodb.ts +32 -0
- package/src/lib/plugin-registry.tsx +77 -0
- package/src/lib/website-context.tsx +39 -0
- package/src/proxy.ts +55 -0
- package/src/router.tsx +45 -0
- package/src/routes.tsx +3 -0
- package/src/server.ts +8 -0
- package/src/types/plugin.ts +24 -0
- package/src/types/preferences.ts +13 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// packages/jhits-dashboard/src/config.ts
|
|
2
|
+
import type { NextConfig } from "next";
|
|
3
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Automatically creates a catch-all route that handles dashboard routes
|
|
8
|
+
* This runs at build time when withJhitsDashboard() is called
|
|
9
|
+
* No dashboard folder needed - routes are handled via catch-all at [locale] level
|
|
10
|
+
*/
|
|
11
|
+
async function ensureDashboardRoutes() {
|
|
12
|
+
try {
|
|
13
|
+
// Find the host app directory (where next.config.ts is)
|
|
14
|
+
let appDir = process.cwd();
|
|
15
|
+
const possiblePaths = [
|
|
16
|
+
appDir,
|
|
17
|
+
join(appDir, '..'),
|
|
18
|
+
join(appDir, '..', '..'),
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
for (const basePath of possiblePaths) {
|
|
22
|
+
const configPath = join(basePath, 'next.config.ts');
|
|
23
|
+
if (existsSync(configPath)) {
|
|
24
|
+
appDir = basePath;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const localeDir = join(appDir, 'src', 'app', '[locale]');
|
|
30
|
+
const catchAllDir = join(localeDir, '[...path]');
|
|
31
|
+
const catchAllPath = join(catchAllDir, 'page.tsx');
|
|
32
|
+
|
|
33
|
+
// Check if catch-all already exists
|
|
34
|
+
if (existsSync(catchAllPath)) {
|
|
35
|
+
// Read existing file and check if it already handles dashboard
|
|
36
|
+
const fs = await import('fs');
|
|
37
|
+
const existingContent = fs.readFileSync(catchAllPath, 'utf8');
|
|
38
|
+
if (existingContent.includes('@jhits/dashboard') || existingContent.includes('DashboardRouter')) {
|
|
39
|
+
// Already set up, skip
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// If it exists but doesn't handle dashboard, we need to modify it
|
|
43
|
+
// Read the file and prepend dashboard handling
|
|
44
|
+
const dashboardHandler = `
|
|
45
|
+
// Dashboard route handling (auto-added by @jhits/dashboard)
|
|
46
|
+
if (path.length > 0 && path[0] === 'dashboard') {
|
|
47
|
+
const { DashboardRouter, DashboardRouterLayout } = await import('@jhits/dashboard');
|
|
48
|
+
const dashboardPath = path.slice(1);
|
|
49
|
+
return (
|
|
50
|
+
<DashboardRouterLayout>
|
|
51
|
+
<DashboardRouter path={dashboardPath} params={Promise.resolve({ locale: resolvedParams.locale })} />
|
|
52
|
+
</DashboardRouterLayout>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
`;
|
|
56
|
+
// Insert dashboard handling at the beginning of the function body
|
|
57
|
+
const modifiedContent = existingContent.replace(
|
|
58
|
+
/(export default async function \w+\([^)]+\) \{[\s\S]*?const resolvedParams = await props\.params;[\s\S]*?const path = resolvedParams\.path \|\| \[\];)/,
|
|
59
|
+
`$1${dashboardHandler}`
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (modifiedContent !== existingContent) {
|
|
63
|
+
fs.writeFileSync(catchAllPath, modifiedContent);
|
|
64
|
+
} else {
|
|
65
|
+
// If replacement didn't work, append at a safe location
|
|
66
|
+
const safeInsertPoint = existingContent.indexOf('const path = resolvedParams.path');
|
|
67
|
+
if (safeInsertPoint > -1) {
|
|
68
|
+
const before = existingContent.substring(0, safeInsertPoint);
|
|
69
|
+
const after = existingContent.substring(safeInsertPoint);
|
|
70
|
+
const newContent = before + dashboardHandler.trim() + '\n ' + after;
|
|
71
|
+
fs.writeFileSync(catchAllPath, newContent);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Create new catch-all route that handles dashboard routes
|
|
78
|
+
mkdirSync(catchAllDir, { recursive: true });
|
|
79
|
+
writeFileSync(catchAllPath, `// Auto-generated by @jhits/dashboard - do not edit manually
|
|
80
|
+
export { default } from '@jhits/dashboard/catch-all';
|
|
81
|
+
`);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
// Silently fail - routes might already exist or be manually created
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Dynamically find all @jhits/* packages from package.json
|
|
89
|
+
* Reads dependencies and devDependencies to build transpilePackages list
|
|
90
|
+
*
|
|
91
|
+
* This function finds the app's package.json by looking for next.config.ts
|
|
92
|
+
* in the same directory, ensuring we read from the correct location in monorepos
|
|
93
|
+
*/
|
|
94
|
+
function findJhitsPackages(): string[] {
|
|
95
|
+
try {
|
|
96
|
+
// Find the app directory (where next.config.ts is located)
|
|
97
|
+
const appDir = process.cwd();
|
|
98
|
+
const possiblePaths = [
|
|
99
|
+
appDir,
|
|
100
|
+
join(appDir, '..'),
|
|
101
|
+
join(appDir, '..', '..'),
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
let packageJsonPath: string | null = null;
|
|
105
|
+
|
|
106
|
+
for (const basePath of possiblePaths) {
|
|
107
|
+
const configPath = join(basePath, 'next.config.ts');
|
|
108
|
+
const pkgPath = join(basePath, 'package.json');
|
|
109
|
+
if (existsSync(configPath) && existsSync(pkgPath)) {
|
|
110
|
+
packageJsonPath = pkgPath;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Fallback to process.cwd() if we can't find next.config.ts
|
|
116
|
+
if (!packageJsonPath) {
|
|
117
|
+
packageJsonPath = join(process.cwd(), 'package.json');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!existsSync(packageJsonPath)) {
|
|
121
|
+
// Fallback to wildcard if package.json not found
|
|
122
|
+
return ['@jhits'];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Use readFileSync instead of require() to avoid module resolution issues
|
|
126
|
+
const packageJsonContent = readFileSync(packageJsonPath, 'utf-8');
|
|
127
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
128
|
+
|
|
129
|
+
const allDeps = {
|
|
130
|
+
...(packageJson.dependencies || {}),
|
|
131
|
+
...(packageJson.devDependencies || {}),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Filter for packages starting with @jhits/
|
|
135
|
+
const jhitsPackages = Object.keys(allDeps).filter(
|
|
136
|
+
(pkg) => pkg.startsWith('@jhits/')
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Always include the base @jhits pattern for any other packages
|
|
140
|
+
return jhitsPackages.length > 0 ? jhitsPackages : ['@jhits'];
|
|
141
|
+
} catch (error) {
|
|
142
|
+
// Fallback to wildcard on any error
|
|
143
|
+
console.warn('[withJhitsDashboard] Could not read package.json, using @jhits wildcard');
|
|
144
|
+
return ['@jhits'];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* JHITS Dashboard Plugin Configuration
|
|
150
|
+
*
|
|
151
|
+
* Usage:
|
|
152
|
+
* import { withJhitsDashboard } from '@jhits/dashboard/config';
|
|
153
|
+
* export default withJhitsDashboard(nextConfig);
|
|
154
|
+
*
|
|
155
|
+
* This automatically sets up all dashboard routes - no manual setup needed!
|
|
156
|
+
*/
|
|
157
|
+
export function withJhitsDashboard(nextConfig: NextConfig = {}): NextConfig {
|
|
158
|
+
// Auto-create dashboard routes at build time
|
|
159
|
+
// NOTE: User management routes are now handled by plugin-users via the unified plugin router
|
|
160
|
+
// No need to auto-generate /api/users routes anymore
|
|
161
|
+
if (typeof window === 'undefined') {
|
|
162
|
+
try {
|
|
163
|
+
ensureDashboardRoutes();
|
|
164
|
+
// ensureUserManagementRoutes(); // Disabled - routes are handled by plugin-users via plugin router
|
|
165
|
+
} catch (error) {
|
|
166
|
+
// Ignore errors - routes might already exist
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
...nextConfig,
|
|
172
|
+
|
|
173
|
+
// 1. AUTO-TRANSPILE: Dynamically find all @jhits/* packages from package.json
|
|
174
|
+
// This ensures Turbopack knows exactly which workspace folders to link
|
|
175
|
+
transpilePackages: [
|
|
176
|
+
...(nextConfig.transpilePackages || []),
|
|
177
|
+
...findJhitsPackages()
|
|
178
|
+
],
|
|
179
|
+
|
|
180
|
+
experimental: {
|
|
181
|
+
...nextConfig.experimental,
|
|
182
|
+
// 2. MONOREPO RESOLUTION: This is the secret sauce.
|
|
183
|
+
// It allows Next.js to follow symlinks in the pnpm workspace
|
|
184
|
+
// even if they aren't explicitly listed.
|
|
185
|
+
externalDir: true,
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
// Exclude server-only third-party packages from client bundles
|
|
189
|
+
// Note: @jhits/* packages should NOT be here - they're in transpilePackages instead
|
|
190
|
+
// Adding them here would conflict with transpilePackages in Turbopack
|
|
191
|
+
serverExternalPackages: [
|
|
192
|
+
...(nextConfig.serverExternalPackages || []),
|
|
193
|
+
'mongodb',
|
|
194
|
+
'bcrypt',
|
|
195
|
+
'bcryptjs',
|
|
196
|
+
'jsonwebtoken',
|
|
197
|
+
'nodemailer'
|
|
198
|
+
],
|
|
199
|
+
|
|
200
|
+
webpack: (config, { isServer }) => {
|
|
201
|
+
// Ensure Node.js modules are not bundled for client
|
|
202
|
+
if (!isServer) {
|
|
203
|
+
config.resolve.fallback = {
|
|
204
|
+
...config.resolve.fallback,
|
|
205
|
+
fs: false,
|
|
206
|
+
path: false,
|
|
207
|
+
'fs/promises': false,
|
|
208
|
+
child_process: false,
|
|
209
|
+
net: false,
|
|
210
|
+
tls: false,
|
|
211
|
+
crypto: false,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// Prevent server-only plugins from being bundled into client
|
|
215
|
+
// plugin-dep is server-only and should never be imported by client code
|
|
216
|
+
config.externals = config.externals || [];
|
|
217
|
+
if (Array.isArray(config.externals)) {
|
|
218
|
+
config.externals.push('@jhits/plugin-dep');
|
|
219
|
+
} else if (typeof config.externals === 'object') {
|
|
220
|
+
config.externals['@jhits/plugin-dep'] = '@jhits/plugin-dep';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return config;
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
// Routes are automatically created and managed by the plugin
|
|
227
|
+
// The dashboard folder is completely transparent - you never need to touch it
|
|
228
|
+
// Just use withJhitsDashboard() and everything works!
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createNavigation } from 'next-intl/navigation';
|
|
2
|
+
import { routing } from './routing';
|
|
3
|
+
|
|
4
|
+
// Lightweight wrappers around Next.js' navigation
|
|
5
|
+
// APIs that consider the routing configuration
|
|
6
|
+
export const { Link, redirect, usePathname, useRouter, getPathname } = createNavigation(routing);
|
|
7
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getRequestConfig } from 'next-intl/server';
|
|
2
|
+
import { routing } from './routing';
|
|
3
|
+
import { PLATFORM_MODULES } from '../lib/modules-config';
|
|
4
|
+
|
|
5
|
+
export default getRequestConfig(async ({ requestLocale }) => {
|
|
6
|
+
let locale = await requestLocale;
|
|
7
|
+
|
|
8
|
+
if (!locale || !routing.locales.includes(locale as "en" | "nl" | "sv")) {
|
|
9
|
+
locale = routing.defaultLocale;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// 1. LOAD COMMON (Must-have)
|
|
13
|
+
const common = (await import(`../assets/locales/${locale}/common.json`)).default;
|
|
14
|
+
|
|
15
|
+
// 2. LOAD MODULES (Optional/Modular)
|
|
16
|
+
const moduleMessages: Record<string, Record<string, string>> = {};
|
|
17
|
+
|
|
18
|
+
// We map through our registry and try to fetch the JSON for each
|
|
19
|
+
await Promise.all(
|
|
20
|
+
PLATFORM_MODULES.map(async (mod) => {
|
|
21
|
+
try {
|
|
22
|
+
// We use a template string that is specific enough for Webpack to follow
|
|
23
|
+
const modFile = await import(`../assets/locales/${locale}/${mod.namespace}.json`);
|
|
24
|
+
moduleMessages[mod.namespace] = modFile.default;
|
|
25
|
+
} catch (e) {
|
|
26
|
+
// If the file doesn't exist, we just provide an empty object
|
|
27
|
+
// This prevents the "Module Not Found" crash
|
|
28
|
+
moduleMessages[mod.namespace] = {};
|
|
29
|
+
console.info(`Optional module [${mod.namespace}] not found for locale [${locale}]`);
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
locale,
|
|
36
|
+
messages: {
|
|
37
|
+
common,
|
|
38
|
+
...moduleMessages // Spreads all optional modules into the root
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineRouting } from 'next-intl/routing';
|
|
2
|
+
import { createNavigation } from 'next-intl/navigation';
|
|
3
|
+
import { PLATFORM_MODULES } from '../lib/modules-config';
|
|
4
|
+
|
|
5
|
+
// 1. Transform the modules array into the format next-intl expects
|
|
6
|
+
const dynamicPathnames: Record<string, Record<string, string>> = {
|
|
7
|
+
'/': {
|
|
8
|
+
en: '/',
|
|
9
|
+
nl: '/',
|
|
10
|
+
sv: '/'
|
|
11
|
+
},
|
|
12
|
+
'/login': {
|
|
13
|
+
en: '/login',
|
|
14
|
+
nl: '/login',
|
|
15
|
+
sv: '/login'
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
PLATFORM_MODULES.forEach((mod) => {
|
|
20
|
+
dynamicPathnames[mod.path] = {
|
|
21
|
+
en: mod.aliases.en,
|
|
22
|
+
nl: mod.aliases.nl || mod.aliases.en, // Fallback to English
|
|
23
|
+
sv: mod.aliases.sv || mod.aliases.en
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// 2. Define routing using the dynamic object
|
|
28
|
+
export const routing = defineRouting({
|
|
29
|
+
locales: ['en', 'nl', 'sv'],
|
|
30
|
+
defaultLocale: 'en',
|
|
31
|
+
localePrefix: 'never',
|
|
32
|
+
pathnames: dynamicPathnames
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const { Link, redirect, usePathname, useRouter } = createNavigation(routing);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Export function to load dashboard translations
|
|
2
|
+
// This allows host apps to merge dashboard translations into their own messages
|
|
3
|
+
|
|
4
|
+
export async function getDashboardMessages(locale: string): Promise<Record<string, Record<string, string>>> {
|
|
5
|
+
try {
|
|
6
|
+
// Load dashboard common translations
|
|
7
|
+
const common = (await import(`../assets/locales/${locale}/common.json`)).default;
|
|
8
|
+
|
|
9
|
+
// Return translations in the format expected by next-intl
|
|
10
|
+
// The common object contains dashboard, sidebar, etc. keys
|
|
11
|
+
// This will be merged into the host app's common namespace
|
|
12
|
+
return {
|
|
13
|
+
common
|
|
14
|
+
};
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.warn(`Dashboard translations not found for locale: ${locale}`, error);
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Main export file for the dashboard plugin
|
|
2
|
+
// This exports all the dashboard components and pages that can be used by host apps
|
|
3
|
+
|
|
4
|
+
// Router components - inlined to avoid module resolution issues
|
|
5
|
+
import DashboardLayout from './app/[locale]/dashboard/layout';
|
|
6
|
+
import DashboardHome from './app/[locale]/dashboard/page';
|
|
7
|
+
import DashboardPreferences from './app/[locale]/dashboard/preferences/page';
|
|
8
|
+
import DashboardProfile from './app/[locale]/dashboard/profile/page';
|
|
9
|
+
import DashboardPluginRoute from './app/[locale]/dashboard/[...pluginRoute]/page';
|
|
10
|
+
|
|
11
|
+
export async function DashboardRouter({
|
|
12
|
+
path,
|
|
13
|
+
params
|
|
14
|
+
}: {
|
|
15
|
+
path: string[];
|
|
16
|
+
params: Promise<{ locale: string }>;
|
|
17
|
+
}) {
|
|
18
|
+
const resolvedParams = await params;
|
|
19
|
+
const locale = resolvedParams.locale;
|
|
20
|
+
|
|
21
|
+
// Get the actual route (first segment of path)
|
|
22
|
+
const route = path.length === 0 ? 'home' : path[0];
|
|
23
|
+
|
|
24
|
+
// Handle different dashboard routes
|
|
25
|
+
switch (route) {
|
|
26
|
+
case 'preferences':
|
|
27
|
+
return <DashboardPreferences />;
|
|
28
|
+
case 'profile':
|
|
29
|
+
return <DashboardProfile />;
|
|
30
|
+
case 'home':
|
|
31
|
+
case '':
|
|
32
|
+
return <DashboardHome />;
|
|
33
|
+
default:
|
|
34
|
+
// This is a plugin route - pass the full path as pluginRoute
|
|
35
|
+
return <DashboardPluginRoute params={Promise.resolve({
|
|
36
|
+
locale,
|
|
37
|
+
pluginRoute: path
|
|
38
|
+
})} />;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Layout wrapper
|
|
43
|
+
export function DashboardRouterLayout({ children }: { children: React.ReactNode }) {
|
|
44
|
+
return <DashboardLayout>{children}</DashboardLayout>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Components (for advanced usage)
|
|
48
|
+
export { default as Sidebar } from './components/dashboard/Sidebar';
|
|
49
|
+
export { default as Topbar } from './components/dashboard/Topbar';
|
|
50
|
+
export { PluginNotFound } from './components/PluginNotFound';
|
|
51
|
+
export { Providers, AuthGuard } from './components/Providers';
|
|
52
|
+
|
|
53
|
+
// Plugin system
|
|
54
|
+
export { PluginRegistry } from './lib/plugin-registry';
|
|
55
|
+
export { PLATFORM_MODULES } from './lib/modules-config';
|
|
56
|
+
|
|
57
|
+
// Config
|
|
58
|
+
export { withJhitsDashboard } from './config';
|
|
59
|
+
|
|
60
|
+
// Translations
|
|
61
|
+
export { getDashboardMessages } from './i18n/translations';
|
|
62
|
+
|
|
63
|
+
// Catch-all route handler (for client app integration)
|
|
64
|
+
export { default as DashboardCatchAll } from './components/DashboardCatchAll';
|
|
65
|
+
|
|
66
|
+
// Website context (for accessing website info in dashboard components)
|
|
67
|
+
export { WebsiteProvider, useWebsite } from './lib/website-context';
|
|
68
|
+
export type { WebsiteInfo } from './lib/website-context';
|
|
69
|
+
|
package/src/lib/auth.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { NextAuthOptions, Session } from "next-auth";
|
|
2
|
+
import CredentialsProvider from "next-auth/providers/credentials";
|
|
3
|
+
import clientPromise from "./mongodb";
|
|
4
|
+
import bcrypt from "bcrypt";
|
|
5
|
+
|
|
6
|
+
export const authOptions: NextAuthOptions = {
|
|
7
|
+
providers: [
|
|
8
|
+
CredentialsProvider({
|
|
9
|
+
name: "Credentials",
|
|
10
|
+
credentials: {
|
|
11
|
+
email: { label: "Email", type: "email" },
|
|
12
|
+
password: { label: "Password", type: "password" }
|
|
13
|
+
},
|
|
14
|
+
async authorize(credentials, req) {
|
|
15
|
+
if (!credentials?.email || !credentials?.password) return null;
|
|
16
|
+
|
|
17
|
+
const client = await clientPromise;
|
|
18
|
+
const db = client.db();
|
|
19
|
+
|
|
20
|
+
const user = await db.collection("users").findOne({
|
|
21
|
+
email: credentials.email
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!user) return null;
|
|
25
|
+
|
|
26
|
+
const isPasswordValid = await bcrypt.compare(
|
|
27
|
+
credentials.password,
|
|
28
|
+
user.password
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
if (!isPasswordValid) return null;
|
|
32
|
+
|
|
33
|
+
// --- LOG ACTIVITY ---
|
|
34
|
+
const userAgent = req?.headers?.["user-agent"] || "Unknown";
|
|
35
|
+
const ip = req?.headers?.["x-forwarded-for"] || "127.0.0.1";
|
|
36
|
+
|
|
37
|
+
await db.collection("user_activities").updateOne(
|
|
38
|
+
{ userId: user._id.toString(), userAgent: userAgent },
|
|
39
|
+
{
|
|
40
|
+
$set: {
|
|
41
|
+
userId: user._id.toString(),
|
|
42
|
+
userAgent,
|
|
43
|
+
ip,
|
|
44
|
+
lastActive: new Date()
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{ upsert: true }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Return object becomes the initial 'user' in the JWT callback
|
|
51
|
+
return {
|
|
52
|
+
id: user._id.toString(),
|
|
53
|
+
email: user.email,
|
|
54
|
+
name: user.name,
|
|
55
|
+
role: user.role || "user",
|
|
56
|
+
image: user.image || null
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
],
|
|
61
|
+
callbacks: {
|
|
62
|
+
async jwt({ token, user, trigger, session }) {
|
|
63
|
+
// 1. INITIAL LOGIN: Transfer data from 'user' (from authorize) to 'token'
|
|
64
|
+
if (user) {
|
|
65
|
+
token.id = user.id;
|
|
66
|
+
token.role = (user as unknown as { role: string }).role;
|
|
67
|
+
token.image = user.image;
|
|
68
|
+
|
|
69
|
+
// Capture User Agent on the server side to stick it into the JWT
|
|
70
|
+
if (typeof window === 'undefined') {
|
|
71
|
+
const { headers } = await import('next/headers');
|
|
72
|
+
const headersList = await headers();
|
|
73
|
+
token.userAgent = headersList.get("user-agent");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 2. CLIENT-SIDE UPDATE: When you call update({ user: { image: '...' } })
|
|
78
|
+
if (trigger === "update" && session?.user) {
|
|
79
|
+
if (session.user.name) token.name = session.user.name;
|
|
80
|
+
if (session.user.email) token.email = session.user.email;
|
|
81
|
+
if (session.user.role) token.role = session.user.role;
|
|
82
|
+
|
|
83
|
+
// This is the specific fix for your Topbar image
|
|
84
|
+
if (session.user.image !== undefined) {
|
|
85
|
+
token.image = session.user.image;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return token;
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
async session({ session, token }) {
|
|
93
|
+
try {
|
|
94
|
+
// --- SECURITY CHECK: DATABASE VALIDATION ---
|
|
95
|
+
// This runs every time the session is checked.
|
|
96
|
+
const client = await clientPromise;
|
|
97
|
+
const db = client.db();
|
|
98
|
+
|
|
99
|
+
const activeSession = await db.collection("user_activities").findOne({
|
|
100
|
+
userId: token.id as string,
|
|
101
|
+
userAgent: token.userAgent as string
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// If the record was deleted (revoked from Profile page), log them out
|
|
105
|
+
if (!activeSession) {
|
|
106
|
+
return null as unknown as Session;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 3. MAP TOKEN TO SESSION: Make data available in useSession()
|
|
110
|
+
if (session.user) {
|
|
111
|
+
(session.user as unknown as { id: string }).id = token.id as string;
|
|
112
|
+
(session.user as unknown as { role: string }).role = token.role as string;
|
|
113
|
+
session.user.name = token.name as string;
|
|
114
|
+
session.user.email = token.email as string;
|
|
115
|
+
session.user.image = token.image as string | null; // Topbar UI reads this
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return session;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
// If there's any error (including JWT decryption failures), return null to invalidate session
|
|
121
|
+
console.error("Session callback error:", error);
|
|
122
|
+
return null as unknown as Session;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
session: {
|
|
127
|
+
strategy: "jwt",
|
|
128
|
+
maxAge: 30 * 24 * 60 * 60, // 30 Days
|
|
129
|
+
},
|
|
130
|
+
pages: {
|
|
131
|
+
signIn: '/login'
|
|
132
|
+
},
|
|
133
|
+
secret: (() => {
|
|
134
|
+
const secret = process.env.NEXTAUTH_SECRET;
|
|
135
|
+
if (!secret) {
|
|
136
|
+
console.warn('[NextAuth] Warning: NEXTAUTH_SECRET is not set. Using fallback secret. This should be set in production!');
|
|
137
|
+
return 'fallback-secret-change-in-production-' + Date.now();
|
|
138
|
+
}
|
|
139
|
+
return secret;
|
|
140
|
+
})(),
|
|
141
|
+
debug: process.env.NODE_ENV === 'development',
|
|
142
|
+
// Custom logger to suppress JWT decryption errors (happens with old/invalid session cookies)
|
|
143
|
+
logger: {
|
|
144
|
+
error: (code: string, metadata: Error | { [key: string]: unknown; error: Error }) => {
|
|
145
|
+
// Suppress JWT_SESSION_ERROR for decryption failures (old/invalid cookies)
|
|
146
|
+
if (code === 'JWT_SESSION_ERROR') {
|
|
147
|
+
const errorMessage = metadata instanceof Error
|
|
148
|
+
? metadata.message
|
|
149
|
+
: (metadata as { message?: string })?.message || '';
|
|
150
|
+
if (errorMessage.includes('decryption')) {
|
|
151
|
+
// Silently ignore - user will need to log in again
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Log all other errors
|
|
156
|
+
console.error(`[next-auth][error][${code}]`, metadata);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
package/src/lib/db.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// apps/dashboard/lib/db.ts
|
|
2
|
+
|
|
3
|
+
// Mock implementation - Replace with your actual database call (Prisma, Drizzle, Supabase, etc.)
|
|
4
|
+
export async function getSiteConfig(siteId: string) {
|
|
5
|
+
// In a real app: return await db.siteConfigs.findUnique({ where: { siteId } });
|
|
6
|
+
return {
|
|
7
|
+
siteId,
|
|
8
|
+
// This list determines what actually "renders" in the catch-all route
|
|
9
|
+
installedPlugins: ["blog-system", "analytics-pro"],
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { headers } from 'next/headers';
|
|
2
|
+
import type { WebsiteInfo } from './website-context';
|
|
3
|
+
import clientPromise from './mongodb';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Gets website information from the host app
|
|
7
|
+
* This function reads from headers set by the host app's middleware
|
|
8
|
+
* Host apps should set x-site-name and x-site-tagline headers in their middleware
|
|
9
|
+
* Also fetches logo information from the client app's settings
|
|
10
|
+
*/
|
|
11
|
+
export async function getWebsiteInfo(locale: string): Promise<WebsiteInfo> {
|
|
12
|
+
try {
|
|
13
|
+
const headersList = await headers();
|
|
14
|
+
const host = headersList.get('host') || '';
|
|
15
|
+
const protocol = headersList.get('x-forwarded-proto') ||
|
|
16
|
+
(process.env.NODE_ENV === 'production' ? 'https' : 'http');
|
|
17
|
+
|
|
18
|
+
// Get website info from headers (set by host app's middleware)
|
|
19
|
+
const siteName = headersList.get('x-site-name') ||
|
|
20
|
+
process.env.NEXT_PUBLIC_SITE_NAME ||
|
|
21
|
+
'Website';
|
|
22
|
+
const siteTagline = headersList.get('x-site-tagline') ||
|
|
23
|
+
process.env.NEXT_PUBLIC_SITE_TAGLINE ||
|
|
24
|
+
'';
|
|
25
|
+
|
|
26
|
+
// Construct the base URL for the website home page
|
|
27
|
+
const baseUrl = `${protocol}://${host}`;
|
|
28
|
+
const homeUrl = locale === 'en' ? baseUrl : `${baseUrl}/${locale}`;
|
|
29
|
+
|
|
30
|
+
// Try to fetch logo from client app's settings
|
|
31
|
+
let logo: { light: string; dark: string } | undefined;
|
|
32
|
+
try {
|
|
33
|
+
const client = await clientPromise;
|
|
34
|
+
const db = client.db();
|
|
35
|
+
const settings = await db.collection("settings").findOne({ identifier: "site_config" });
|
|
36
|
+
|
|
37
|
+
if (settings) {
|
|
38
|
+
// Check if logo paths are stored in settings
|
|
39
|
+
// Common patterns: LogoIcon.svg, LogoIcon-inverted.svg, or custom paths
|
|
40
|
+
// Also check for common logo file names used in client apps
|
|
41
|
+
const logoLight = settings.logoLight ||
|
|
42
|
+
settings.logo?.light ||
|
|
43
|
+
'/LogoIcon.svg';
|
|
44
|
+
const logoDark = settings.logoDark ||
|
|
45
|
+
settings.logo?.dark ||
|
|
46
|
+
'/LogoIcon-inverted.svg';
|
|
47
|
+
|
|
48
|
+
// Only set logo if at least one path exists
|
|
49
|
+
if (logoLight || logoDark) {
|
|
50
|
+
logo = {
|
|
51
|
+
light: logoLight || logoDark || '/LogoIcon.svg', // Fallback to dark if light not found
|
|
52
|
+
dark: logoDark || logoLight || '/LogoIcon-inverted.svg' // Fallback to light if dark not found
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch (dbError) {
|
|
57
|
+
// If database fetch fails, continue without logo
|
|
58
|
+
console.warn('Failed to fetch logo from settings:', dbError);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
name: siteName,
|
|
63
|
+
tagline: siteTagline,
|
|
64
|
+
homeUrl: homeUrl,
|
|
65
|
+
locale,
|
|
66
|
+
logo
|
|
67
|
+
};
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// Fallback
|
|
70
|
+
return {
|
|
71
|
+
name: 'Website',
|
|
72
|
+
tagline: '',
|
|
73
|
+
homeUrl: `/${locale}`,
|
|
74
|
+
locale
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|