From 39cfda47299e53bb028eec757e16efc92dec96a6 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Fri, 16 Jan 2026 00:12:11 +0100 Subject: [PATCH] feat: implement SQL dump import functionality and add Dev Management navigation --- .../admin/dev-management/hooks/executeSql.ts | 49 +++++ src/app/admin/dev-management/page.tsx | 197 ++++++++++++++++++ src/app/admin/page.tsx | 61 +++++- src/app/components/nav/Header.tsx | 22 ++ 4 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 src/app/admin/dev-management/hooks/executeSql.ts create mode 100644 src/app/admin/dev-management/page.tsx diff --git a/src/app/admin/dev-management/hooks/executeSql.ts b/src/app/admin/dev-management/hooks/executeSql.ts new file mode 100644 index 0000000..bfa8b7f --- /dev/null +++ b/src/app/admin/dev-management/hooks/executeSql.ts @@ -0,0 +1,49 @@ +import { authFetch } from '../../../utils/authFetch'; +import { log } from '../../../utils/logger'; + +export type SqlExecutionData = { + result?: any; + isMulti?: boolean; +}; + +export type SqlExecutionMeta = { + durationMs?: number; +}; + +export async function importSqlDump(file: File): Promise<{ ok: boolean; data?: SqlExecutionData; meta?: SqlExecutionMeta; message?: string }>{ + const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'; + const url = `${BASE_URL}/api/admin/dev/sql`; + + log('🧪 Dev SQL Import: POST', url); + try { + const form = new FormData(); + form.append('file', file); + const res = await authFetch(url, { + method: 'POST', + headers: { Accept: 'application/json' }, + body: form + }); + + let body: any = null; + try { + body = await res.clone().json(); + } catch { + body = null; + } + + if (res.status === 401) { + return { ok: false, message: 'Unauthorized. Please log in.' }; + } + if (res.status === 403) { + return { ok: false, message: body?.error || 'Forbidden. Admin access required.' }; + } + if (!res.ok) { + return { ok: false, message: body?.error || 'SQL execution failed.' }; + } + + return { ok: true, data: body?.data, meta: body?.meta }; + } catch (e: any) { + log('❌ Dev SQL: network error', e?.message || e); + return { ok: false, message: 'Network error while executing SQL.' }; + } +} diff --git a/src/app/admin/dev-management/page.tsx b/src/app/admin/dev-management/page.tsx new file mode 100644 index 0000000..aadeb04 --- /dev/null +++ b/src/app/admin/dev-management/page.tsx @@ -0,0 +1,197 @@ +'use client' + +import React from 'react' +import Header from '../../components/nav/Header' +import Footer from '../../components/Footer' +import PageTransitionEffect from '../../components/animation/pageTransitionEffect' +import { CommandLineIcon, PlayIcon, TrashIcon, ExclamationTriangleIcon, ArrowUpTrayIcon } from '@heroicons/react/24/outline' +import useAuthStore from '../../store/authStore' +import { useRouter } from 'next/navigation' +import { importSqlDump, SqlExecutionData, SqlExecutionMeta } from './hooks/executeSql' + +export default function DevManagementPage() { + const router = useRouter() + const user = useAuthStore(s => s.user) + + const isAdmin = + !!user && + ( + (user as any)?.role === 'admin' || + (user as any)?.userType === 'admin' || + (user as any)?.isAdmin === true || + ((user as any)?.roles?.includes?.('admin')) || + (user as any)?.role === 'super_admin' || + (user as any)?.userType === 'super_admin' || + (user as any)?.isSuperAdmin === true || + ((user as any)?.roles?.includes?.('super_admin')) + ) + + const [authChecked, setAuthChecked] = React.useState(false) + React.useEffect(() => { + if (user === null) { + router.replace('/login') + return + } + if (user && !isAdmin) { + router.replace('/admin') + return + } + setAuthChecked(true) + }, [user, isAdmin, router]) + + const [selectedFile, setSelectedFile] = React.useState(null) + const [loading, setLoading] = React.useState(false) + const [error, setError] = React.useState('') + const [result, setResult] = React.useState(null) + const [meta, setMeta] = React.useState(null) + const fileInputRef = React.useRef(null) + + const runImport = async () => { + setError('') + if (!selectedFile) { + setError('Please select a SQL dump file.') + return + } + setLoading(true) + try { + const res = await importSqlDump(selectedFile) + if (!res.ok) { + setError(res.message || 'Failed to import SQL dump.') + return + } + setResult(res.data || null) + setMeta(res.meta || null) + } finally { + setLoading(false) + } + } + + const clearResults = () => { + setResult(null) + setMeta(null) + setError('') + } + + const onImportFile = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + setSelectedFile(file) + e.target.value = '' + } + + if (!authChecked) return null + + return ( + +
+
+
+
+
+
+
+ +
+
+

Dev Management

+

Import SQL dump files to run database migrations.

+
+
+
+ +
+
Use with caution
+
SQL dumps run immediately and can modify production data.
+
+
+
+ +
+
+
+

SQL Dump Import

+
+ + + +
+
+ +
+
Select a .sql dump file using Import SQL.
+
Only SQL dump files are supported.
+ {selectedFile && ( +
Selected: {selectedFile.name}
+ )} +
+ + + {error && ( +
+ {error} +
+ )} +
+ +
+

Result Summary

+
+
+ Result Sets + + {Array.isArray(result?.result) ? (result?.result as any[]).length : result?.result ? 1 : 0} + +
+
+ Multi-statement + {result?.isMulti ? 'Yes' : 'No'} +
+
+ Duration + {meta?.durationMs ? `${meta.durationMs} ms` : '-'} +
+
+
+ Multi-statement SQL and dump files are supported. Use with caution. +
+
+
+ +
+
+

Import Results

+
+ + {!result && ( +
No results yet. Import a SQL dump to see output.
+ )} + + {result?.result && ( +
+                  {JSON.stringify(result.result, null, 2)}
+                
+ )} +
+
+
+
+
+
+ ) +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 31d33d7..b085e30 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -11,20 +11,36 @@ import { ArrowRightIcon, Squares2X2Icon, BanknotesIcon, - ClipboardDocumentListIcon + ClipboardDocumentListIcon, + CommandLineIcon } from '@heroicons/react/24/outline' import { useMemo, useState, useEffect } from 'react' import { useRouter } from 'next/navigation' import { useAdminUsers } from '../hooks/useAdminUsers' +import useAuthStore from '../store/authStore' // env-based feature flags const DISPLAY_MATRIX = process.env.NEXT_PUBLIC_DISPLAY_MATRIX !== 'false' const DISPLAY_ABONEMENTS = process.env.NEXT_PUBLIC_DISPLAY_ABONEMMENTS !== 'false' const DISPLAY_NEWS = process.env.NEXT_PUBLIC_DISPLAY_NEWS !== 'false' +const DISPLAY_DEV_MANAGEMENT = process.env.NEXT_PUBLIC_DISPLAY_DEV_MANAGEMENT !== 'false' export default function AdminDashboardPage() { const router = useRouter() const { userStats, isAdmin } = useAdminUsers() + const user = useAuthStore(s => s.user) + const isAdminOrSuper = + !!user && + ( + (user as any)?.role === 'admin' || + (user as any)?.userType === 'admin' || + (user as any)?.isAdmin === true || + ((user as any)?.roles?.includes?.('admin')) || + (user as any)?.role === 'super_admin' || + (user as any)?.userType === 'super_admin' || + (user as any)?.isSuperAdmin === true || + ((user as any)?.roles?.includes?.('super_admin')) + ) const [isClient, setIsClient] = useState(false) const [isMobile, setIsMobile] = useState(false) @@ -318,6 +334,49 @@ export default function AdminDashboardPage() { }`} /> + + {/* Dev Management */} + diff --git a/src/app/components/nav/Header.tsx b/src/app/components/nav/Header.tsx index 5a56d83..4ee6df4 100644 --- a/src/app/components/nav/Header.tsx +++ b/src/app/components/nav/Header.tsx @@ -327,6 +327,18 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) { ) const isAdmin = mounted && rawIsAdmin + const rawIsSuperAdmin = + !!user && + ( + (user as any)?.role === 'super_admin' || + (user as any)?.userType === 'super_admin' || + (user as any)?.isSuperAdmin === true || + ((user as any)?.roles?.includes?.('super_admin')) + ) + const isSuperAdmin = mounted && rawIsSuperAdmin + + const isAdminOrSuper = mounted && (rawIsAdmin || rawIsSuperAdmin) + // Only gate visibility by scroll on parallax-enabled pages const headerVisible = parallaxEnabled ? animateIn && scrollY > 24 : animateIn const parallaxOffset = parallaxEnabled ? Math.max(-16, -scrollY * 0.15) : 0 @@ -656,6 +668,16 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) { News Management )} + + {isAdminOrSuper && ( + + )} )}