set admin behind a cookie and check role on profile
This commit is contained in:
+52
-66
@@ -1,7 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type React from "react";
|
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 AdminLeftNavigation from "@/components/AdminLeftNavigation";
|
||||||
|
import { useAuth } from "@/context/AuthContext";
|
||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Download,
|
Download,
|
||||||
@@ -15,84 +18,66 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{
|
{ label: "Dashboard", href: "/admin", icon: <LayoutDashboard className="h-4 w-4" /> },
|
||||||
label: "Dashboard",
|
{ label: "Imports", href: "/admin/import-status", icon: <Download className="h-4 w-4" /> },
|
||||||
href: "/admin",
|
{ label: "Mappings", href: "/admin/mapping", icon: <Layers className="h-4 w-4" /> },
|
||||||
icon: <LayoutDashboard className="h-4 w-4" />,
|
{ label: "Products", href: "/admin/products", icon: <Boxes className="h-4 w-4" /> },
|
||||||
},
|
{ label: "Merchants", href: "/admin/merchants", icon: <Store className="h-4 w-4" /> },
|
||||||
{
|
{ label: "Platforms", href: "/admin/platforms", icon: <Layers className="h-4 w-4" /> },
|
||||||
label: "Imports",
|
{ label: "Enrichment", href: "/admin/enrichment", icon: <Wand2 className="h-4 w-4" /> },
|
||||||
href: "/admin/import-status",
|
{ label: "Users", href: "/admin/users", icon: <Users className="h-4 w-4" /> },
|
||||||
icon: <Download className="h-4 w-4" />,
|
{ label: "Settings", href: "/admin/settings", icon: <Settings className="h-4 w-4" /> },
|
||||||
},
|
{ label: "List Emails", href: "/admin/email", icon: <LucideMail className="h-4 w-4" /> },
|
||||||
{
|
{ label: "Send an Email", href: "/admin/email/send", icon: <LucideMail className="h-4 w-4" /> },
|
||||||
label: "Mappings",
|
|
||||||
href: "/admin/mapping",
|
|
||||||
icon: <Layers className="h-4 w-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Products",
|
|
||||||
href: "/admin/products",
|
|
||||||
icon: <Boxes className="h-4 w-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Merchants",
|
|
||||||
href: "/admin/merchants",
|
|
||||||
icon: <Store className="h-4 w-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Platforms",
|
|
||||||
href: "/admin/platforms",
|
|
||||||
icon: <Layers className="h-4 w-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Enrichment",
|
|
||||||
href: "/admin/enrichment",
|
|
||||||
icon: <Wand2 className="h-4 w-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Users",
|
|
||||||
href: "/admin/users",
|
|
||||||
icon: <Users className="h-4 w-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Settings",
|
|
||||||
href: "/admin/settings",
|
|
||||||
icon: <Settings className="h-4 w-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "List Emails",
|
|
||||||
href: "/admin/email",
|
|
||||||
icon: <LucideMail className="h-4 w-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Send a Email",
|
|
||||||
href: "/admin/email/send",
|
|
||||||
icon: <LucideMail className="h-4 w-4" />,
|
|
||||||
},
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// ... existing code ...
|
|
||||||
// ADMIN CHECK FOR LOGIN
|
|
||||||
// const { user, loading } = useAuth();
|
|
||||||
|
|
||||||
// if (!loading && user?.role !== "ADMIN") {
|
|
||||||
// redirect("/"); // or /login
|
|
||||||
// }
|
|
||||||
|
|
||||||
export default function AdminLayout({
|
export default function AdminLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
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 (
|
return (
|
||||||
<div className="flex min-h-screen bg-black text-zinc-50">
|
<div className="flex min-h-screen bg-black text-zinc-50">
|
||||||
<AdminLeftNavigation
|
<AdminLeftNavigation
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
onToggleCollapsed={() => setCollapsed((v) => !v)}
|
onToggleCollapsed={() => setCollapsed((v) => !v)}
|
||||||
|
items={navItems}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Main column */}
|
{/* Main column */}
|
||||||
@@ -107,6 +92,7 @@ export default function AdminLayout({
|
|||||||
Battl Builders Control Panel
|
Battl Builders Control Panel
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="rounded-full border border-amber-500/30 bg-amber-500/10 px-3 py-1 text-[11px] font-medium text-amber-300">
|
<span className="rounded-full border border-amber-500/30 bg-amber-500/10 px-3 py-1 text-[11px] font-medium text-amber-300">
|
||||||
Internal • v0.1
|
Internal • v0.1
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
// app/layout.tsx
|
// app/layout.tsx
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
import Script from "next/script";
|
||||||
|
|
||||||
import { AuthProvider } from "@/context/AuthContext";
|
import { AuthProvider } from "@/context/AuthContext";
|
||||||
import { Banner } from "@/components/Banner";
|
import { Banner } from "@/components/Banner";
|
||||||
|
|
||||||
@@ -38,6 +40,9 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
<body className="bg-dark text-zinc-950 dark:bg-black dark:text-zinc-50">
|
<body className="bg-dark text-zinc-950 dark:bg-black dark:text-zinc-50">
|
||||||
{" "}
|
{" "}
|
||||||
|
<Script src="https://umami.ash.gofwd.group/script.js" data-website-id="ad310b4a-aa5e-471a-938b-47a6c0f4108d"
|
||||||
|
strategy="lazyOnload"
|
||||||
|
/>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<Banner />
|
<Banner />
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Download,
|
Download,
|
||||||
@@ -12,74 +13,37 @@ import {
|
|||||||
LucideMail,
|
LucideMail,
|
||||||
Wand2,
|
Wand2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
|
||||||
|
export type AdminNavItem = {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
type AdminLeftNavigationProps = {
|
type AdminLeftNavigationProps = {
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
onToggleCollapsed: () => void;
|
onToggleCollapsed: () => void;
|
||||||
|
items?: AdminNavItem[]; // ✅ NEW
|
||||||
};
|
};
|
||||||
|
|
||||||
const navItems = [
|
const defaultNavItems: AdminNavItem[] = [
|
||||||
{
|
{ label: "Dashboard", href: "/admin", icon: <LayoutDashboard className="h-4 w-4" /> },
|
||||||
label: "Dashboard",
|
{ label: "Imports", href: "/admin/import-status", icon: <Download className="h-4 w-4" /> },
|
||||||
href: "/admin",
|
{ label: "Mappings", href: "/admin/mapping", icon: <Layers className="h-4 w-4" /> },
|
||||||
icon: <LayoutDashboard className="h-4 w-4" />,
|
{ label: "Products", href: "/admin/products", icon: <Boxes className="h-4 w-4" /> },
|
||||||
},
|
{ label: "Merchants", href: "/admin/merchants", icon: <Store className="h-4 w-4" /> },
|
||||||
{
|
{ label: "Platforms", href: "/admin/platforms", icon: <Layers className="h-4 w-4" /> },
|
||||||
label: "Imports",
|
{ label: "Enrichment", href: "/admin/enrichment", icon: <Wand2 className="h-4 w-4" /> },
|
||||||
href: "/admin/import-status",
|
{ label: "Users", href: "/admin/users", icon: <Users className="h-4 w-4" /> },
|
||||||
icon: <Download className="h-4 w-4" />,
|
{ label: "Settings", href: "/admin/settings", icon: <Settings className="h-4 w-4" /> },
|
||||||
},
|
{ label: "Manage Emails", href: "/admin/email/manage", icon: <LucideMail className="h-4 w-4" /> },
|
||||||
{
|
{ label: "Send an Email", href: "/admin/email/send", icon: <LucideMail className="h-4 w-4" /> },
|
||||||
label: "Mappings",
|
|
||||||
href: "/admin/mapping",
|
|
||||||
icon: <Layers className="h-4 w-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Products",
|
|
||||||
href: "/admin/products",
|
|
||||||
icon: <Boxes className="h-4 w-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Merchants",
|
|
||||||
href: "/admin/merchants",
|
|
||||||
icon: <Store className="h-4 w-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Platforms",
|
|
||||||
href: "/admin/platforms",
|
|
||||||
icon: <Layers className="h-4 w-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Enrichment",
|
|
||||||
href: "/admin/enrichment",
|
|
||||||
icon: <Wand2 className="h-4 w-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Users",
|
|
||||||
href: "/admin/users",
|
|
||||||
icon: <Users className="h-4 w-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Settings",
|
|
||||||
href: "/admin/settings",
|
|
||||||
icon: <Settings className="h-4 w-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Manage Emails",
|
|
||||||
href: "/admin/email/manage",
|
|
||||||
icon: <LucideMail className="h-4 w-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Send a Email",
|
|
||||||
href: "/admin/email/send",
|
|
||||||
icon: <LucideMail className="h-4 w-4" />,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function AdminLeftNavigation({
|
export default function AdminLeftNavigation({
|
||||||
collapsed,
|
collapsed,
|
||||||
onToggleCollapsed,
|
onToggleCollapsed,
|
||||||
|
items = defaultNavItems, // ✅ NEW default
|
||||||
}: AdminLeftNavigationProps) {
|
}: AdminLeftNavigationProps) {
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
@@ -104,7 +68,9 @@ export default function AdminLeftNavigation({
|
|||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-zinc-500">
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-zinc-500">
|
||||||
<Link href={"/"} title={"Back to Battl Dashboard"} >Battl Builders</Link>
|
<Link href="/" title="Back to Battl Dashboard">
|
||||||
|
Battl Builders
|
||||||
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[10px] text-zinc-600">Admin Command</p>
|
<p className="text-[10px] text-zinc-600">Admin Command</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,8 +78,8 @@ export default function AdminLeftNavigation({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex-1 space-y-1 text-sm">
|
<nav className="flex-1 space-y-1 text-sm">
|
||||||
{navItems.map((item) => (
|
{items.map((item) => (
|
||||||
<a
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="flex items-center justify-between rounded-md px-2 py-1.5 text-zinc-300 transition hover:bg-zinc-900 hover:text-amber-300"
|
className="flex items-center justify-between rounded-md px-2 py-1.5 text-zinc-300 transition hover:bg-zinc-900 hover:text-amber-300"
|
||||||
@@ -123,15 +89,13 @@ export default function AdminLeftNavigation({
|
|||||||
{!collapsed && <span>{item.label}</span>}
|
{!collapsed && <span>{item.label}</span>}
|
||||||
</div>
|
</div>
|
||||||
{!collapsed && <span className="text-[10px] text-zinc-600">›</span>}
|
{!collapsed && <span className="text-[10px] text-zinc-600">›</span>}
|
||||||
</a>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<div className="mt-6 border-t border-zinc-900 pt-4 text-[11px] text-zinc-600">
|
<div className="mt-6 border-t border-zinc-900 pt-4 text-[11px] text-zinc-600">
|
||||||
<p className="text-[10px] uppercase tracking-[0.16em] text-zinc-500">
|
<p className="text-[10px] uppercase tracking-[0.16em] text-zinc-500">Status</p>
|
||||||
Status
|
|
||||||
</p>
|
|
||||||
<p className="mt-1">
|
<p className="mt-1">
|
||||||
Import engine: <span className="text-emerald-400">online</span>
|
Import engine: <span className="text-emerald-400">online</span>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
+42
-6
@@ -49,6 +49,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const [user, setUser] = useState<AuthUser>(null);
|
const [user, setUser] = useState<AuthUser>(null);
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist auth to localStorage
|
||||||
|
*/
|
||||||
const persistAuth = useCallback(
|
const persistAuth = useCallback(
|
||||||
(nextToken: string | null, nextUser: AuthUser) => {
|
(nextToken: string | null, nextUser: AuthUser) => {
|
||||||
if (typeof window === "undefined") return;
|
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(() => {
|
useEffect(() => {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
const storedToken = window.localStorage.getItem(TOKEN_KEY);
|
const storedToken = window.localStorage.getItem(TOKEN_KEY);
|
||||||
const storedUser = window.localStorage.getItem(USER_KEY);
|
const storedUser = window.localStorage.getItem(USER_KEY);
|
||||||
|
|
||||||
if (storedToken) setToken(storedToken);
|
if (storedToken && storedUser) {
|
||||||
|
|
||||||
if (storedUser) {
|
|
||||||
try {
|
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 {
|
} catch {
|
||||||
|
window.localStorage.removeItem(TOKEN_KEY);
|
||||||
window.localStorage.removeItem(USER_KEY);
|
window.localStorage.removeItem(USER_KEY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,11 +106,24 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by login/register/magic-link
|
||||||
|
*/
|
||||||
const setSession = useCallback(
|
const setSession = useCallback(
|
||||||
(nextToken: string, nextUser: NonNullable<AuthUser>) => {
|
(nextToken: string, nextUser: NonNullable<AuthUser>) => {
|
||||||
setToken(nextToken);
|
setToken(nextToken);
|
||||||
setUser(nextUser);
|
setUser(nextUser);
|
||||||
persistAuth(nextToken, 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]
|
[persistAuth]
|
||||||
);
|
);
|
||||||
@@ -118,6 +149,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const nextUser: AuthUser =
|
const nextUser: AuthUser =
|
||||||
data.user ??
|
data.user ??
|
||||||
({
|
({
|
||||||
|
uuid: data.uuid,
|
||||||
email: data.email ?? email,
|
email: data.email ?? email,
|
||||||
displayName: data.displayName ?? null,
|
displayName: data.displayName ?? null,
|
||||||
role: data.role ?? "USER",
|
role: data.role ?? "USER",
|
||||||
@@ -160,6 +192,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const nextUser: AuthUser =
|
const nextUser: AuthUser =
|
||||||
data.user ??
|
data.user ??
|
||||||
({
|
({
|
||||||
|
uuid: data.uuid,
|
||||||
email: data.email ?? email,
|
email: data.email ?? email,
|
||||||
displayName: data.displayName ?? displayName ?? null,
|
displayName: data.displayName ?? displayName ?? null,
|
||||||
role: data.role ?? "USER",
|
role: data.role ?? "USER",
|
||||||
@@ -177,6 +210,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setToken(null);
|
setToken(null);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
persistAuth(null, null);
|
persistAuth(null, null);
|
||||||
|
|
||||||
|
// Clear server session cookies
|
||||||
|
fetch("/api/auth/session", { method: "DELETE" }).catch(() => {});
|
||||||
}, [persistAuth]);
|
}, [persistAuth]);
|
||||||
|
|
||||||
const getAuthHeaders = useCallback((): HeadersInit => {
|
const getAuthHeaders = useCallback((): HeadersInit => {
|
||||||
@@ -191,7 +227,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
register,
|
register,
|
||||||
logout,
|
logout,
|
||||||
getAuthHeaders,
|
getAuthHeaders,
|
||||||
setSession, // ✅ important
|
setSession,
|
||||||
};
|
};
|
||||||
|
|
||||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||||
|
|||||||
+31
-9
@@ -12,15 +12,37 @@ const ALWAYS_ALLOW = new Set<string>([
|
|||||||
"/sitemap.xml",
|
"/sitemap.xml",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Match common public/static file extensions so middleware never blocks them
|
const PUBLIC_FILE =
|
||||||
const PUBLIC_FILE = /\.(?:svg|png|jpg|jpeg|gif|webp|ico|css|js|map|txt|xml|json|woff|woff2|ttf|eot)$/i;
|
/\.(?:svg|png|jpg|jpeg|gif|webp|ico|css|js|map|txt|xml|json|woff|woff2|ttf|eot)$/i;
|
||||||
|
|
||||||
export function middleware(req: NextRequest) {
|
export function middleware(req: NextRequest) {
|
||||||
if (!LAUNCH_ONLY_ROOT) return NextResponse.next();
|
|
||||||
|
|
||||||
const { pathname } = req.nextUrl;
|
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 (
|
if (
|
||||||
pathname.startsWith("/_next") ||
|
pathname.startsWith("/_next") ||
|
||||||
pathname.startsWith("/favicon") ||
|
pathname.startsWith("/favicon") ||
|
||||||
@@ -31,17 +53,17 @@ export function middleware(req: NextRequest) {
|
|||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Allow the root + a few public files
|
|
||||||
if (ALWAYS_ALLOW.has(pathname)) return NextResponse.next();
|
if (ALWAYS_ALLOW.has(pathname)) return NextResponse.next();
|
||||||
|
|
||||||
// 🚫 Everything else: hide it
|
|
||||||
return NextResponse.rewrite(new URL("/404", req.url));
|
return NextResponse.rewrite(new URL("/404", req.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
` 1`
|
||||||
export const config = {
|
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: [
|
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)).*)",
|
"/((?!_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)).*)",
|
||||||
],
|
],
|
||||||
|
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user