set admin behind a cookie and check role on profile
This commit is contained in:
+80
-94
@@ -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: <LayoutDashboard className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
label: "Imports",
|
||||
href: "/admin/import-status",
|
||||
icon: <Download 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" />,
|
||||
},
|
||||
|
||||
{ label: "Dashboard", href: "/admin", icon: <LayoutDashboard className="h-4 w-4" /> },
|
||||
{ label: "Imports", href: "/admin/import-status", icon: <Download 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 an 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({
|
||||
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 (
|
||||
<div className="flex min-h-screen bg-black text-zinc-50">
|
||||
<AdminLeftNavigation
|
||||
collapsed={collapsed}
|
||||
onToggleCollapsed={() => setCollapsed((v) => !v)}
|
||||
/>
|
||||
<div className="flex min-h-screen bg-black text-zinc-50">
|
||||
<AdminLeftNavigation
|
||||
collapsed={collapsed}
|
||||
onToggleCollapsed={() => setCollapsed((v) => !v)}
|
||||
items={navItems}
|
||||
/>
|
||||
|
||||
{/* Main column */}
|
||||
<div className="flex min-h-screen flex-1 flex-col">
|
||||
{/* Top bar */}
|
||||
<header className="flex items-center justify-between border-b border-zinc-900 bg-zinc-950/70 px-4 py-3">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.18em] text-zinc-500">
|
||||
Admin
|
||||
</p>
|
||||
<p className="text-sm text-zinc-200">
|
||||
Battl Builders Control Panel
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Main column */}
|
||||
<div className="flex min-h-screen flex-1 flex-col">
|
||||
{/* Top bar */}
|
||||
<header className="flex items-center justify-between border-b border-zinc-900 bg-zinc-950/70 px-4 py-3">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.18em] text-zinc-500">
|
||||
Admin
|
||||
</p>
|
||||
<p className="text-sm text-zinc-200">
|
||||
Battl Builders Control Panel
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
Internal • v0.1
|
||||
</span>
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-zinc-900 text-[11px] text-zinc-300">
|
||||
ADMIN
|
||||
</div>
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-zinc-900 text-[11px] text-zinc-300">
|
||||
ADMIN
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<main className="mx-auto flex w-full max-w-6xl flex-1 flex-col px-4 py-6">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
{/* Content */}
|
||||
<main className="mx-auto flex w-full max-w-6xl flex-1 flex-col px-4 py-6">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
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 }) {
|
||||
|
||||
<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>
|
||||
<Banner />
|
||||
{children}
|
||||
|
||||
@@ -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: <LayoutDashboard className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
label: "Imports",
|
||||
href: "/admin/import-status",
|
||||
icon: <Download 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" />,
|
||||
},
|
||||
const defaultNavItems: AdminNavItem[] = [
|
||||
{ label: "Dashboard", href: "/admin", icon: <LayoutDashboard className="h-4 w-4" /> },
|
||||
{ label: "Imports", href: "/admin/import-status", icon: <Download 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 an Email", href: "/admin/email/send", icon: <LucideMail className="h-4 w-4" /> },
|
||||
];
|
||||
|
||||
export default function AdminLeftNavigation({
|
||||
collapsed,
|
||||
onToggleCollapsed,
|
||||
items = defaultNavItems, // ✅ NEW default
|
||||
}: AdminLeftNavigationProps) {
|
||||
return (
|
||||
<aside
|
||||
@@ -104,7 +68,9 @@ export default function AdminLeftNavigation({
|
||||
{!collapsed && (
|
||||
<div>
|
||||
<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 className="text-[10px] text-zinc-600">Admin Command</p>
|
||||
</div>
|
||||
@@ -112,8 +78,8 @@ export default function AdminLeftNavigation({
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 space-y-1 text-sm">
|
||||
{navItems.map((item) => (
|
||||
<a
|
||||
{items.map((item) => (
|
||||
<Link
|
||||
key={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"
|
||||
@@ -123,15 +89,13 @@ export default function AdminLeftNavigation({
|
||||
{!collapsed && <span>{item.label}</span>}
|
||||
</div>
|
||||
{!collapsed && <span className="text-[10px] text-zinc-600">›</span>}
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{!collapsed && (
|
||||
<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">
|
||||
Status
|
||||
</p>
|
||||
<p className="text-[10px] uppercase tracking-[0.16em] text-zinc-500">Status</p>
|
||||
<p className="mt-1">
|
||||
Import engine: <span className="text-emerald-400">online</span>
|
||||
</p>
|
||||
@@ -140,4 +104,4 @@ export default function AdminLeftNavigation({
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
}
|
||||
+43
-7
@@ -49,6 +49,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<AuthUser>(null);
|
||||
const [loading, setLoading] = useState<boolean>(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<AuthUser>) => {
|
||||
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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
@@ -203,4 +239,4 @@ export function useAuth(): AuthContextValue {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
|
||||
+31
-9
@@ -12,15 +12,37 @@ const ALWAYS_ALLOW = new Set<string>([
|
||||
"/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)).*)",
|
||||
],
|
||||
|
||||
};
|
||||
Reference in New Issue
Block a user