diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx
index 804a4ff..f7db1ea 100644
--- a/app/admin/layout.tsx
+++ b/app/admin/layout.tsx
@@ -1,7 +1,10 @@
"use client";
+
import type React from "react";
-import { useState } from "react";
+import { useEffect, useMemo, useState } from "react";
+import { usePathname, useRouter } from "next/navigation";
import AdminLeftNavigation from "@/components/AdminLeftNavigation";
+import { useAuth } from "@/context/AuthContext";
import {
LayoutDashboard,
Download,
@@ -15,113 +18,96 @@ import {
} from "lucide-react";
const navItems = [
- {
- label: "Dashboard",
- href: "/admin",
- icon: ,
- },
- {
- label: "Imports",
- href: "/admin/import-status",
- icon: ,
- },
- {
- label: "Mappings",
- href: "/admin/mapping",
- icon: ,
- },
- {
- label: "Products",
- href: "/admin/products",
- icon: ,
- },
- {
- label: "Merchants",
- href: "/admin/merchants",
- icon: ,
- },
- {
- label: "Platforms",
- href: "/admin/platforms",
- icon: ,
- },
- {
- label: "Enrichment",
- href: "/admin/enrichment",
- icon: ,
- },
- {
- label: "Users",
- href: "/admin/users",
- icon: ,
- },
- {
- label: "Settings",
- href: "/admin/settings",
- icon: ,
- },
- {
- label: "List Emails",
- href: "/admin/email",
- icon: ,
- },
- {
- label: "Send a Email",
- href: "/admin/email/send",
- icon: ,
- },
-
+ { label: "Dashboard", href: "/admin", icon: },
+ { label: "Imports", href: "/admin/import-status", icon: },
+ { label: "Mappings", href: "/admin/mapping", icon: },
+ { label: "Products", href: "/admin/products", icon: },
+ { label: "Merchants", href: "/admin/merchants", icon: },
+ { label: "Platforms", href: "/admin/platforms", icon: },
+ { label: "Enrichment", href: "/admin/enrichment", icon: },
+ { label: "Users", href: "/admin/users", icon: },
+ { label: "Settings", href: "/admin/settings", icon: },
+ { label: "List Emails", href: "/admin/email", icon: },
+ { label: "Send an Email", href: "/admin/email/send", icon: },
];
-// ... existing code ...
-// ADMIN CHECK FOR LOGIN
-// const { user, loading } = useAuth();
-
-// if (!loading && user?.role !== "ADMIN") {
-// redirect("/"); // or /login
-// }
-
export default function AdminLayout({
- children,
- }: {
+ children,
+}: {
children: React.ReactNode;
}) {
const [collapsed, setCollapsed] = useState(false);
+ const { user, loading } = useAuth();
+
+ const router = useRouter();
+ const pathname = usePathname();
+
+ // Where to send people back after login
+ const next = useMemo(
+ () => encodeURIComponent(pathname || "/admin"),
+ [pathname]
+ );
+
+ /**
+ * ✅ AUTH GUARD
+ * Redirects happen in useEffect (NOT during render)
+ */
+ useEffect(() => {
+ if (loading) return;
+
+ // Not logged in
+ if (!user) {
+ router.replace(`/login?next=${next}`);
+ return;
+ }
+
+ // Logged in but not admin
+ if (user.role !== "ADMIN") {
+ router.replace("/");
+ }
+ }, [loading, user, router, next]);
+
+ // While loading OR redirecting, render nothing
+ if (loading) return null;
+ if (!user) return null;
+ if (user.role !== "ADMIN") return null;
return (
-
-
setCollapsed((v) => !v)}
- />
+
+
setCollapsed((v) => !v)}
+ items={navItems}
+ />
- {/* Main column */}
-
);
}
\ No newline at end of file
diff --git a/app/api/auth/session/route.ts b/app/api/auth/session/route.ts
new file mode 100644
index 0000000..ec6b25a
--- /dev/null
+++ b/app/api/auth/session/route.ts
@@ -0,0 +1,38 @@
+import { NextResponse } from "next/server";
+
+export async function POST(req: Request) {
+ const { token, role } = await req.json();
+
+ if (!token) {
+ return NextResponse.json({ ok: false }, { status: 400 });
+ }
+
+ const res = NextResponse.json({ ok: true });
+
+ // Server-readable, JS-unreadable
+ res.cookies.set("bb_access_token", token, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ path: "/",
+ });
+
+ // Optional: for quick middleware role checks (not “secure” by itself)
+ if (role) {
+ res.cookies.set("bb_role", role, {
+ httpOnly: false,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ path: "/",
+ });
+ }
+
+ return res;
+}
+
+export async function DELETE() {
+ const res = NextResponse.json({ ok: true });
+ res.cookies.set("bb_access_token", "", { path: "/", maxAge: 0 });
+ res.cookies.set("bb_role", "", { path: "/", maxAge: 0 });
+ return res;
+}
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
index 623c225..d9f8f51 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,6 +1,8 @@
// app/layout.tsx
import "./globals.css";
import type { ReactNode } from "react";
+import Script from "next/script";
+
import { AuthProvider } from "@/context/AuthContext";
import { Banner } from "@/components/Banner";
@@ -38,6 +40,9 @@ export default function RootLayout({ children }: { children: ReactNode }) {
{" "}
+
{children}
diff --git a/components/AdminLeftNavigation.tsx b/components/AdminLeftNavigation.tsx
index 5a5d1bb..27457b9 100644
--- a/components/AdminLeftNavigation.tsx
+++ b/components/AdminLeftNavigation.tsx
@@ -1,6 +1,7 @@
"use client";
import type React from "react";
+import Link from "next/link";
import {
LayoutDashboard,
Download,
@@ -12,74 +13,37 @@ import {
LucideMail,
Wand2,
} from "lucide-react";
-import Link from "next/link";
+
+export type AdminNavItem = {
+ label: string;
+ href: string;
+ icon: React.ReactNode;
+};
type AdminLeftNavigationProps = {
collapsed: boolean;
onToggleCollapsed: () => void;
+ items?: AdminNavItem[]; // ✅ NEW
};
-const navItems = [
- {
- label: "Dashboard",
- href: "/admin",
- icon: ,
- },
- {
- label: "Imports",
- href: "/admin/import-status",
- icon: ,
- },
- {
- label: "Mappings",
- href: "/admin/mapping",
- icon: ,
- },
- {
- label: "Products",
- href: "/admin/products",
- icon: ,
- },
- {
- label: "Merchants",
- href: "/admin/merchants",
- icon: ,
- },
- {
- label: "Platforms",
- href: "/admin/platforms",
- icon: ,
- },
- {
- label: "Enrichment",
- href: "/admin/enrichment",
- icon: ,
- },
- {
- label: "Users",
- href: "/admin/users",
- icon: ,
- },
- {
- label: "Settings",
- href: "/admin/settings",
- icon: ,
- },
- {
- label: "Manage Emails",
- href: "/admin/email/manage",
- icon: ,
- },
- {
- label: "Send a Email",
- href: "/admin/email/send",
- icon: ,
- },
+const defaultNavItems: AdminNavItem[] = [
+ { label: "Dashboard", href: "/admin", icon: },
+ { label: "Imports", href: "/admin/import-status", icon: },
+ { label: "Mappings", href: "/admin/mapping", icon: },
+ { label: "Products", href: "/admin/products", icon: },
+ { label: "Merchants", href: "/admin/merchants", icon: },
+ { label: "Platforms", href: "/admin/platforms", icon: },
+ { label: "Enrichment", href: "/admin/enrichment", icon: },
+ { label: "Users", href: "/admin/users", icon: },
+ { label: "Settings", href: "/admin/settings", icon: },
+ { label: "Manage Emails", href: "/admin/email/manage", icon: },
+ { label: "Send an Email", href: "/admin/email/send", icon: },
];
export default function AdminLeftNavigation({
collapsed,
onToggleCollapsed,
+ items = defaultNavItems, // ✅ NEW default
}: AdminLeftNavigationProps) {
return (
@@ -112,8 +78,8 @@ export default function AdminLeftNavigation({
{!collapsed && (
-
- Status
-
+
Status
Import engine: online
@@ -140,4 +104,4 @@ export default function AdminLeftNavigation({
)}
);
-}
+}
\ No newline at end of file
diff --git a/context/AuthContext.tsx b/context/AuthContext.tsx
index f048b81..96f4628 100644
--- a/context/AuthContext.tsx
+++ b/context/AuthContext.tsx
@@ -49,6 +49,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState
(null);
const [loading, setLoading] = useState(true);
+ /**
+ * Persist auth to localStorage
+ */
const persistAuth = useCallback(
(nextToken: string | null, nextUser: AuthUser) => {
if (typeof window === "undefined") return;
@@ -68,19 +71,34 @@ export function AuthProvider({ children }: { children: ReactNode }) {
[]
);
- // Hydrate from localStorage on first load
+ /**
+ * ✅ Hydrate from localStorage ONCE
+ * IMPORTANT: loading only flips false AFTER user/token are set (if present)
+ */
useEffect(() => {
if (typeof window === "undefined") return;
const storedToken = window.localStorage.getItem(TOKEN_KEY);
const storedUser = window.localStorage.getItem(USER_KEY);
- if (storedToken) setToken(storedToken);
-
- if (storedUser) {
+ if (storedToken && storedUser) {
try {
- setUser(JSON.parse(storedUser));
+ const parsedUser = JSON.parse(storedUser);
+
+ setToken(storedToken);
+ setUser(parsedUser);
+
+ // ✅ Re-sync cookie so middleware can allow /admin on hard refresh / direct nav
+ fetch("/api/auth/session", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ token: storedToken,
+ role: parsedUser?.role,
+ }),
+ }).catch(() => {});
} catch {
+ window.localStorage.removeItem(TOKEN_KEY);
window.localStorage.removeItem(USER_KEY);
}
}
@@ -88,11 +106,24 @@ export function AuthProvider({ children }: { children: ReactNode }) {
setLoading(false);
}, []);
+ /**
+ * Used by login/register/magic-link
+ */
const setSession = useCallback(
(nextToken: string, nextUser: NonNullable) => {
setToken(nextToken);
setUser(nextUser);
persistAuth(nextToken, nextUser);
+
+ // ✅ Make middleware / server aware of the session
+ fetch("/api/auth/session", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ token: nextToken,
+ role: nextUser.role,
+ }),
+ }).catch(() => {});
},
[persistAuth]
);
@@ -118,6 +149,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const nextUser: AuthUser =
data.user ??
({
+ uuid: data.uuid,
email: data.email ?? email,
displayName: data.displayName ?? null,
role: data.role ?? "USER",
@@ -160,6 +192,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const nextUser: AuthUser =
data.user ??
({
+ uuid: data.uuid,
email: data.email ?? email,
displayName: data.displayName ?? displayName ?? null,
role: data.role ?? "USER",
@@ -177,6 +210,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
setToken(null);
setUser(null);
persistAuth(null, null);
+
+ // Clear server session cookies
+ fetch("/api/auth/session", { method: "DELETE" }).catch(() => {});
}, [persistAuth]);
const getAuthHeaders = useCallback((): HeadersInit => {
@@ -191,7 +227,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
register,
logout,
getAuthHeaders,
- setSession, // ✅ important
+ setSession,
};
return {children};
@@ -203,4 +239,4 @@ export function useAuth(): AuthContextValue {
throw new Error("useAuth must be used within an AuthProvider");
}
return ctx;
-}
\ No newline at end of file
+}
diff --git a/middleware.ts b/middleware.ts
index c1032d0..a78fdc1 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -12,15 +12,37 @@ const ALWAYS_ALLOW = new Set([
"/sitemap.xml",
]);
-// Match common public/static file extensions so middleware never blocks them
-const PUBLIC_FILE = /\.(?:svg|png|jpg|jpeg|gif|webp|ico|css|js|map|txt|xml|json|woff|woff2|ttf|eot)$/i;
+const PUBLIC_FILE =
+ /\.(?:svg|png|jpg|jpeg|gif|webp|ico|css|js|map|txt|xml|json|woff|woff2|ttf|eot)$/i;
export function middleware(req: NextRequest) {
- if (!LAUNCH_ONLY_ROOT) return NextResponse.next();
-
const { pathname } = req.nextUrl;
- // ✅ Always allow Next internals + static assets (public/ and _next/)
+// =========================
+// 1) Admin gate (always on)
+// =========================
+if (pathname.startsWith("/admin")) {
+ const token = req.cookies.get("bb_access_token")?.value;
+ const role = req.cookies.get("bb_role")?.value; // optional
+
+ const ok = !!token && (!role || role === "ADMIN");
+
+ if (!ok) {
+ const url = req.nextUrl.clone();
+ url.pathname = "/login";
+ url.searchParams.set("next", pathname);
+ return NextResponse.redirect(url);
+ }
+
+ return NextResponse.next();
+}
+
+ // =====================================
+ // 2) Launch-only gate (your existing one)
+ // =====================================
+ if (!LAUNCH_ONLY_ROOT) return NextResponse.next();
+
+ // ✅ Always allow Next internals + static assets
if (
pathname.startsWith("/_next") ||
pathname.startsWith("/favicon") ||
@@ -31,17 +53,17 @@ export function middleware(req: NextRequest) {
return NextResponse.next();
}
- // ✅ Allow the root + a few public files
if (ALWAYS_ALLOW.has(pathname)) return NextResponse.next();
- // 🚫 Everything else: hide it
return NextResponse.rewrite(new URL("/404", req.url));
}
+
+` 1`
export const config = {
- // Run middleware on everything *except* Next internals and obvious static files.
- // This keeps launch gating from ever breaking your logo/images/fonts/etc.
matcher: [
+ // run on everything except Next internals + obvious static files
"/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|css|js|map|txt|xml|json|woff|woff2|ttf|eot)).*)",
],
+
};
\ No newline at end of file