@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.
Files changed (63) hide show
  1. package/README.md +36 -0
  2. package/next.config.ts +32 -0
  3. package/package.json +79 -0
  4. package/postcss.config.mjs +7 -0
  5. package/src/api/README.md +72 -0
  6. package/src/api/masterRouter.ts +150 -0
  7. package/src/api/pluginRouter.ts +135 -0
  8. package/src/app/[locale]/(auth)/layout.tsx +30 -0
  9. package/src/app/[locale]/(auth)/login/page.tsx +201 -0
  10. package/src/app/[locale]/catch-all/page.tsx +10 -0
  11. package/src/app/[locale]/dashboard/[...pluginRoute]/page.tsx +98 -0
  12. package/src/app/[locale]/dashboard/layout.tsx +42 -0
  13. package/src/app/[locale]/dashboard/page.tsx +121 -0
  14. package/src/app/[locale]/dashboard/preferences/page.tsx +295 -0
  15. package/src/app/[locale]/dashboard/profile/page.tsx +491 -0
  16. package/src/app/[locale]/layout.tsx +28 -0
  17. package/src/app/actions/preferences.ts +40 -0
  18. package/src/app/actions/user.ts +191 -0
  19. package/src/app/api/auth/[...nextauth]/route.ts +6 -0
  20. package/src/app/api/plugin-images/list/route.ts +96 -0
  21. package/src/app/api/plugin-images/upload/route.ts +88 -0
  22. package/src/app/api/telemetry/log/route.ts +10 -0
  23. package/src/app/api/telemetry/route.ts +12 -0
  24. package/src/app/api/uploads/[filename]/route.ts +33 -0
  25. package/src/app/globals.css +181 -0
  26. package/src/app/layout.tsx +4 -0
  27. package/src/assets/locales/en/common.json +47 -0
  28. package/src/assets/locales/nl/common.json +48 -0
  29. package/src/assets/locales/sv/common.json +48 -0
  30. package/src/assets/plugins.json +42 -0
  31. package/src/assets/public/Logo_JH_black.jpg +0 -0
  32. package/src/assets/public/Logo_JH_black.png +0 -0
  33. package/src/assets/public/Logo_JH_white.png +0 -0
  34. package/src/assets/public/animated-logo-white.svg +5 -0
  35. package/src/assets/public/logo_black.svg +5 -0
  36. package/src/assets/public/logo_white.svg +5 -0
  37. package/src/assets/public/noimagefound.jpg +0 -0
  38. package/src/components/DashboardCatchAll.tsx +95 -0
  39. package/src/components/DashboardRootLayout.tsx +37 -0
  40. package/src/components/PluginNotFound.tsx +24 -0
  41. package/src/components/Providers.tsx +59 -0
  42. package/src/components/dashboard/Sidebar.tsx +263 -0
  43. package/src/components/dashboard/Topbar.tsx +363 -0
  44. package/src/components/page.tsx +130 -0
  45. package/src/config.ts +230 -0
  46. package/src/i18n/navigation.ts +7 -0
  47. package/src/i18n/request.ts +41 -0
  48. package/src/i18n/routing.ts +35 -0
  49. package/src/i18n/translations.ts +20 -0
  50. package/src/index.tsx +69 -0
  51. package/src/lib/auth.ts +159 -0
  52. package/src/lib/db.ts +11 -0
  53. package/src/lib/get-website-info.ts +78 -0
  54. package/src/lib/modules-config.ts +68 -0
  55. package/src/lib/mongodb.ts +32 -0
  56. package/src/lib/plugin-registry.tsx +77 -0
  57. package/src/lib/website-context.tsx +39 -0
  58. package/src/proxy.ts +55 -0
  59. package/src/router.tsx +45 -0
  60. package/src/routes.tsx +3 -0
  61. package/src/server.ts +8 -0
  62. package/src/types/plugin.ts +24 -0
  63. 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
+
@@ -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
+