diff --git a/middleware.ts b/middleware.ts
new file mode 100644
index 0000000..0f3c614
--- /dev/null
+++ b/middleware.ts
@@ -0,0 +1,91 @@
+import { createServerClient } from '@supabase/ssr'
+import { NextResponse, type NextRequest } from 'next/server'
+
+const ADMIN_LOGIN_PATH = '/admin/login'
+const DEFAULT_ADMIN_EMAIL = 'dev.sxhd@gmail.com'
+
+function getAllowedAdminEmails(): string[] {
+ return (process.env.ADMIN_EMAILS ?? process.env.NEXT_PUBLIC_ADMIN_EMAIL ?? DEFAULT_ADMIN_EMAIL)
+ .split(',')
+ .map((email) => email.trim().toLowerCase())
+ .filter(Boolean)
+}
+
+export async function middleware(request: NextRequest) {
+ const { pathname, search } = request.nextUrl
+ const isLoginPage = pathname === ADMIN_LOGIN_PATH
+ const isAdminApi = pathname.startsWith('/api/admin/')
+
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
+ const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
+
+ if (!supabaseUrl || !supabaseAnonKey) {
+ if (isAdminApi) {
+ return NextResponse.json(
+ { error: 'Admin services are not configured.' },
+ { status: 503 },
+ )
+ }
+
+ if (!isLoginPage) {
+ const loginUrl = request.nextUrl.clone()
+ loginUrl.pathname = ADMIN_LOGIN_PATH
+ loginUrl.searchParams.set('error', 'configuration')
+ return NextResponse.redirect(loginUrl)
+ }
+
+ return NextResponse.next()
+ }
+
+ let response = NextResponse.next({ request })
+
+ const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
+ cookies: {
+ getAll() {
+ return request.cookies.getAll()
+ },
+ setAll(cookiesToSet) {
+ cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
+ response = NextResponse.next({ request })
+ cookiesToSet.forEach(({ name, value, options }) =>
+ response.cookies.set(name, value, options),
+ )
+ },
+ },
+ })
+
+ const {
+ data: { user },
+ } = await supabase.auth.getUser()
+
+ const allowedEmails = getAllowedAdminEmails()
+ const isAdmin = Boolean(
+ user &&
+ (user.app_metadata?.role === 'admin' ||
+ (user.email && allowedEmails.includes(user.email.toLowerCase()))),
+ )
+
+ if (isLoginPage && isAdmin) {
+ const dashboardUrl = request.nextUrl.clone()
+ dashboardUrl.pathname = '/admin/dashboard'
+ dashboardUrl.search = ''
+ return NextResponse.redirect(dashboardUrl)
+ }
+
+ if (!isLoginPage && !isAdmin) {
+ if (isAdminApi) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const loginUrl = request.nextUrl.clone()
+ loginUrl.pathname = ADMIN_LOGIN_PATH
+ loginUrl.searchParams.set('next', `${pathname}${search}`)
+ return NextResponse.redirect(loginUrl)
+ }
+
+ return response
+}
+
+export const config = {
+ matcher: ['/admin/:path*', '/api/admin/:path*'],
+}
diff --git a/src/app/admin/Sidebar.tsx b/src/app/admin/Sidebar.tsx
index d2692f8..5bd41fe 100644
--- a/src/app/admin/Sidebar.tsx
+++ b/src/app/admin/Sidebar.tsx
@@ -1,67 +1,37 @@
"use client";
import Link from "next/link";
-import { Bell,
- LayoutDashboard,
- Folder,
+import {
+ Activity,
Award,
- MessageSquare,
+ Bell,
+ Folder,
+ LayoutDashboard,
Layers,
- Settings,
- Menu,
- X,
LogOut,
+ Menu,
+ MessageSquare,
+ Settings,
Sparkles,
+ X,
} from "lucide-react";
import { usePathname, useRouter } from "next/navigation";
-import { motion, AnimatePresence } from "framer-motion";
-import { useState, useEffect } from "react";
+import { AnimatePresence, motion } from "framer-motion";
+import { useEffect, useState } from "react";
import { supabase } from "@/lib/supabase";
const menus = [
- {
- name: "Dashboard",
- icon: LayoutDashboard,
- path: "/admin",
- },
- {
- name: "Projects",
- icon: Folder,
- path: "/admin/projects",
- },
- {
- name: "Certificates",
- icon: Award,
- path: "/admin/certificates",
- },
- {
- name: "Comments",
- icon: MessageSquare,
- path: "/admin/comments",
- },
- {
- name: "Technologies",
- icon: Layers,
- path: "/admin/technologies",
- },
- {
- name: "3D Scene",
- icon: Sparkles,
- path: "/admin/scene3d",
- },
- {
- name: "Settings",
- icon: Settings,
- path: "/admin/settings",
- },
- {
- name: "Webhook",
- icon: Bell,
- path: "/admin/webhook",
- },
+ { name: "Dashboard", icon: LayoutDashboard, path: "/admin" },
+ { name: "Projects", icon: Folder, path: "/admin/projects" },
+ { name: "Certificates", icon: Award, path: "/admin/certificates" },
+ { name: "Comments", icon: MessageSquare, path: "/admin/comments" },
+ { name: "Technologies", icon: Layers, path: "/admin/technologies" },
+ { name: "3D Scene", icon: Sparkles, path: "/admin/scene3d" },
+ { name: "System Status", icon: Activity, path: "/admin/status" },
+ { name: "Settings", icon: Settings, path: "/admin/settings" },
+ { name: "Webhook", icon: Bell, path: "/admin/webhook" },
];
-// Fix: use startsWith so /admin/dashboard and /admin also match Dashboard
function isActive(menuPath: string, currentPath: string): boolean {
if (menuPath === "/admin") {
return currentPath === "/admin" || currentPath === "/admin/dashboard";
@@ -69,24 +39,24 @@ function isActive(menuPath: string, currentPath: string): boolean {
return currentPath.startsWith(menuPath);
}
-const SidebarContent = ({
+function SidebarContent({
hideTitle = false,
onLinkClick,
}: {
hideTitle?: boolean;
onLinkClick?: () => void;
-}) => {
+}) {
const pathname = usePathname();
const router = useRouter();
const handleLogout = async () => {
await supabase.auth.signOut();
- router.push("/admin/login");
+ router.replace("/admin/login");
+ router.refresh();
};
return (
<>
- {/* TOP */}
{!hideTitle && (
@@ -94,66 +64,39 @@ const SidebarContent = ({
)}
-
- {/* BOTTOM */}
-
- © 2026 Admin
-
+
© 2026 Admin
>
);
-};
+}
export default function Sidebar() {
const [open, setOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
- const checkMobile = () => {
- setIsMobile(window.innerWidth < 1024);
- };
-
+ const checkMobile = () => setIsMobile(window.innerWidth < 1024);
checkMobile();
window.addEventListener("resize", checkMobile);
-
- return () =>
- window.removeEventListener("resize", checkMobile);
+ return () => window.removeEventListener("resize", checkMobile);
}, []);
return (
<>
- {/* DESKTOP */}
{!isMobile && (
-