Add temporary public utility-only site
This commit is contained in:
parent
4884b5d367
commit
2a9f4df15f
6 changed files with 193 additions and 259 deletions
|
|
@ -1,7 +1,6 @@
|
||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
|
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { getUserBearerFromRequest } from "@/lib/directus";
|
|
||||||
import { rateLimit, requestIp } from "@/lib/rate-limit";
|
import { rateLimit, requestIp } from "@/lib/rate-limit";
|
||||||
|
|
||||||
const MAX_BODY_BYTES = 30 * 1024 * 1024;
|
const MAX_BODY_BYTES = 30 * 1024 * 1024;
|
||||||
|
|
@ -21,9 +20,6 @@ export async function POST(req: Request) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!getUserBearerFromRequest(req)) {
|
|
||||||
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
|
|
||||||
}
|
|
||||||
if (!rateLimit(`bgbye:${requestIp(req)}`, 10, 60_000)) {
|
if (!rateLimit(`bgbye:${requestIp(req)}`, 10, 60_000)) {
|
||||||
return NextResponse.json({ error: "Too many processing requests" }, { status: 429 });
|
return NextResponse.json({ error: "Too many processing requests" }, { status: 429 });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,15 @@
|
||||||
// app/page.tsx
|
import { Suspense } from "react";
|
||||||
import { cookies } from "next/headers";
|
import TemporaryUtilityHub from "@/components/TemporaryUtilityHub";
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
import SignIn from "@/app/auth/sign-in/sign-in";
|
|
||||||
import SignUp from "@/app/auth/sign-up/sign-up";
|
|
||||||
import { isJwtValid } from "@/lib/jwt";
|
|
||||||
|
|
||||||
type SearchParams = { [key: string]: string | string[] | undefined };
|
export const metadata = {
|
||||||
|
title: "Laser Everything Community Utilities",
|
||||||
export default async function HomePage({
|
description: "Free laser calculators, public files, and image background removal tools.",
|
||||||
searchParams,
|
};
|
||||||
}: {
|
|
||||||
searchParams?: SearchParams;
|
|
||||||
}) {
|
|
||||||
// If already signed in with a VALID token, go straight to the app
|
|
||||||
const ck = await cookies();
|
|
||||||
const at = ck.get("ma_at")?.value;
|
|
||||||
if (isJwtValid(at)) redirect("/portal");
|
|
||||||
|
|
||||||
const reauth = searchParams?.reauth === "1";
|
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto max-w-5xl px-4 py-12">
|
<Suspense fallback={<main className="mx-auto max-w-7xl px-6 py-10">Loading utilities…</main>}>
|
||||||
{reauth && (
|
<TemporaryUtilityHub />
|
||||||
<p className="mb-6 rounded-md border bg-yellow-50 p-3 text-sm text-yellow-900">
|
</Suspense>
|
||||||
Your session expired. Please sign in again.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<section className="mb-10 text-center">
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">MakeArmy</h1>
|
|
||||||
<p className="mt-2 text-base text-muted-foreground">
|
|
||||||
Free to use. Manage laser rigs, settings, and projects—all in one
|
|
||||||
place.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="grid gap-6 md:grid-cols-2">
|
|
||||||
<SignUp nextPath="/portal" />
|
|
||||||
<SignIn nextPath="/portal" reauth={reauth} />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="mt-8 text-center text-xs text-muted-foreground">
|
|
||||||
<p>This is the beta build v0.1.1 - this site is an active BETA. Some features may not be available or work as intended. If you're experiencing issues please report them here: https://forge.makearmy.io/makearmy/makearmy-app/issues </p>
|
|
||||||
<p>PRIVACY: We only use cookies strictly necessary to operate the site (e.g., your sign-in session). We do not store user data or telemetry other than what you provide and never share or sell data to third parties. Ever.</p>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
127
app/components/TemporaryUtilityHub.tsx
Normal file
127
app/components/TemporaryUtilityHub.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const LaserToolkit = dynamic(
|
||||||
|
() => import("@/components/portal/LaserToolkitSwitcher"),
|
||||||
|
{ ssr: false }
|
||||||
|
);
|
||||||
|
const FileBrowser = dynamic(
|
||||||
|
() => import("@/components/utilities/files/FileBrowserPanel"),
|
||||||
|
{ ssr: false }
|
||||||
|
);
|
||||||
|
const BackgroundRemover = dynamic(
|
||||||
|
() => import("@/components/utilities/BackgroundRemoverPanel"),
|
||||||
|
{ ssr: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const TOOLS = [
|
||||||
|
{
|
||||||
|
key: "laser-toolkit",
|
||||||
|
label: "Laser Toolkit",
|
||||||
|
description: "Calculators for beam size, overlap, hatch spacing, job time, and more.",
|
||||||
|
icon: "/images/utils/toolkit.png",
|
||||||
|
component: LaserToolkit,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "files",
|
||||||
|
label: "File Server",
|
||||||
|
description: "Browse, preview, and download files from the public library.",
|
||||||
|
icon: "/images/utils/fs.png",
|
||||||
|
component: FileBrowser,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "background-remover",
|
||||||
|
label: "Background Remover",
|
||||||
|
description: "Remove image backgrounds using our self-hosted processing service.",
|
||||||
|
icon: "/images/utils/bgrm.png",
|
||||||
|
component: BackgroundRemover,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export default function TemporaryUtilityHub() {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const activeKey = searchParams.get("tool") || TOOLS[0].key;
|
||||||
|
const active = useMemo(
|
||||||
|
() => TOOLS.find((tool) => tool.key === activeKey) || TOOLS[0],
|
||||||
|
[activeKey]
|
||||||
|
);
|
||||||
|
const ActiveTool = active.component;
|
||||||
|
|
||||||
|
function selectTool(key: string) {
|
||||||
|
const query = new URLSearchParams(searchParams.toString());
|
||||||
|
query.set("tool", key);
|
||||||
|
if (key !== "laser-toolkit") query.delete("lt");
|
||||||
|
router.replace(`/?${query.toString()}`, { scroll: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="mx-auto min-h-screen max-w-7xl px-4 py-6 sm:px-6 sm:py-10">
|
||||||
|
<header className="mb-8 border-b pb-6">
|
||||||
|
<p className="mb-2 text-sm font-medium uppercase tracking-[0.2em] text-muted-foreground">
|
||||||
|
Laser Everything
|
||||||
|
</p>
|
||||||
|
<h1 className="text-3xl font-semibold tracking-tight sm:text-4xl">Community Utilities</h1>
|
||||||
|
<p className="mt-3 max-w-3xl text-sm leading-6 text-muted-foreground sm:text-base">
|
||||||
|
Our database and member features are temporarily offline while we change backend
|
||||||
|
systems. These free utilities remain available without an account.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav aria-label="Utilities" className="mb-8 grid gap-3 md:grid-cols-3">
|
||||||
|
{TOOLS.map((tool) => {
|
||||||
|
const selected = tool.key === active.key;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tool.key}
|
||||||
|
type="button"
|
||||||
|
onClick={() => selectTool(tool.key)}
|
||||||
|
aria-pressed={selected}
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-24 items-start gap-4 rounded-xl border p-4 text-left transition",
|
||||||
|
selected
|
||||||
|
? "border-primary bg-primary text-primary-foreground shadow-sm"
|
||||||
|
: "bg-card hover:border-muted-foreground/50 hover:bg-muted/50"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img src={tool.icon} alt="" className="h-10 w-10 rounded-lg object-cover" />
|
||||||
|
<span>
|
||||||
|
<span className="block font-semibold">{tool.label}</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"mt-1 block text-sm leading-5",
|
||||||
|
selected ? "text-primary-foreground/80" : "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{tool.description}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section aria-labelledby="active-tool-title" className="rounded-xl border bg-card p-3 sm:p-5">
|
||||||
|
<div className="mb-5 flex items-center gap-3 border-b pb-4">
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img src={active.icon} alt="" className="h-8 w-8 rounded-md object-cover" />
|
||||||
|
<div>
|
||||||
|
<h2 id="active-tool-title" className="text-xl font-semibold">{active.label}</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">{active.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ActiveTool basePath="/" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer className="mt-8 text-center text-xs leading-5 text-muted-foreground">
|
||||||
|
No account is required. Database-backed profiles, projects, settings, and submissions
|
||||||
|
are intentionally unavailable on this temporary site.
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ import { useSearchParams, useRouter } from "next/navigation";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { TOOLKIT_TABS } from "@/components/utilities/laser-toolkit/registry";
|
import { TOOLKIT_TABS } from "@/components/utilities/laser-toolkit/registry";
|
||||||
|
|
||||||
export default function LaserToolkitSwitcher() {
|
export default function LaserToolkitSwitcher({ basePath = "/portal/utilities" }: { basePath?: string }) {
|
||||||
const sp = useSearchParams();
|
const sp = useSearchParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@ export default function LaserToolkitSwitcher() {
|
||||||
function setTab(k: string) {
|
function setTab(k: string) {
|
||||||
const q = new URLSearchParams(sp.toString());
|
const q = new URLSearchParams(sp.toString());
|
||||||
q.set("lt", k);
|
q.set("lt", k);
|
||||||
router.replace(`/portal/utilities?${q.toString()}`, { scroll: false });
|
router.replace(`${basePath}?${q.toString()}`, { scroll: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!active) {
|
if (!active) {
|
||||||
|
|
|
||||||
|
|
@ -208,8 +208,8 @@ export default function FileBrowserPanel() {
|
||||||
|
|
||||||
<div className="select-all text-xs text-muted-foreground">{path || "/"}</div>
|
<div className="select-all text-xs text-muted-foreground">{path || "/"}</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-[minmax(560px,1fr)_minmax(420px,42vw)] items-start gap-3">
|
<div className="grid grid-cols-1 items-start gap-3 xl:grid-cols-[minmax(560px,1fr)_minmax(420px,42vw)]">
|
||||||
<div className="overflow-hidden rounded-md border">
|
<div className="overflow-x-auto rounded-md border">
|
||||||
<div className="fs-table bg-muted px-2 py-2 fs-cell fs-tight">
|
<div className="fs-table bg-muted px-2 py-2 fs-cell fs-tight">
|
||||||
<div className="px-1">Name</div>
|
<div className="px-1">Name</div>
|
||||||
<div className="text-center">Type</div>
|
<div className="text-center">Type</div>
|
||||||
|
|
|
||||||
|
|
@ -1,217 +1,62 @@
|
||||||
// middleware.ts
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { NextResponse, NextRequest } from "next/server";
|
|
||||||
|
|
||||||
/**
|
const PUBLIC_UTILITY_APIS = new Set([
|
||||||
* Public pages that should remain reachable without being signed in.
|
"/api/bgbye/process",
|
||||||
* Everything else is considered protected (including most /api/*).
|
"/api/files/list",
|
||||||
*/
|
"/api/files/raw",
|
||||||
const PUBLIC_PAGES = new Set<string>([
|
"/api/files/download",
|
||||||
"/", // splash page is public
|
|
||||||
"/auth/sign-in",
|
|
||||||
"/auth/sign-up",
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
const LEGACY_TOOL_PATHS: Record<string, string> = {
|
||||||
* API paths that are explicitly allowed without auth.
|
"/laser-toolkit": "laser-toolkit",
|
||||||
* Keep this list tiny; add broad /api/webhooks to allow ALL webhook endpoints.
|
"/files": "files",
|
||||||
*/
|
"/background-remover": "background-remover",
|
||||||
const PUBLIC_API_PREFIXES: string[] = [
|
};
|
||||||
"/api/auth/", // login/refresh/callback endpoints
|
|
||||||
];
|
|
||||||
|
|
||||||
/** Directus base (used to remotely validate the token after restarts). */
|
|
||||||
const DIRECTUS = (
|
|
||||||
process.env.NEXT_PUBLIC_API_BASE_URL ||
|
|
||||||
process.env.DIRECTUS_URL ||
|
|
||||||
""
|
|
||||||
).replace(/\/$/, "");
|
|
||||||
|
|
||||||
type MapResult = { pathname: string; query?: Record<string, string> };
|
|
||||||
|
|
||||||
/** Helper: does the path start with any prefix in a list? */
|
|
||||||
function startsWithAny(pathname: string, prefixes: string[]) {
|
|
||||||
return prefixes.some((p) => pathname.startsWith(p));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Helper: are we about to redirect to the same URL? */
|
|
||||||
function isSameUrl(req: NextRequest, mapped: MapResult) {
|
|
||||||
const dest = new URL(req.url);
|
|
||||||
dest.pathname = mapped.pathname;
|
|
||||||
if (mapped.query) {
|
|
||||||
for (const [k, v] of Object.entries(mapped.query)) dest.searchParams.set(k, v);
|
|
||||||
}
|
|
||||||
return dest.href === req.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Decode JWT exp (seconds since epoch). We don't verify signature here. */
|
|
||||||
function jwtExp(token: string): number | null {
|
|
||||||
try {
|
|
||||||
const [, payload] = token.split(".");
|
|
||||||
if (!payload) return null;
|
|
||||||
const json = JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/")));
|
|
||||||
return typeof json.exp === "number" ? json.exp : null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build redirect to /auth/sign-in?next=<original>.
|
* Temporary utility-only site.
|
||||||
* Only set reauth=1 (and clear cookies) when opts.reauth === true.
|
*
|
||||||
|
* Directus-backed pages and APIs remain in source history but cannot be reached
|
||||||
|
* through this deployment. This middleware deliberately performs no session or
|
||||||
|
* Directus request.
|
||||||
*/
|
*/
|
||||||
function kickToSignIn(req: NextRequest, opts?: { reauth?: boolean }) {
|
export function middleware(req: NextRequest) {
|
||||||
const wantReauth = !!opts?.reauth;
|
const { pathname } = req.nextUrl;
|
||||||
|
|
||||||
const orig = new URL(req.url);
|
|
||||||
const next = orig.pathname + (orig.search || "");
|
|
||||||
|
|
||||||
const url = new URL(req.url);
|
|
||||||
url.pathname = "/auth/sign-in";
|
|
||||||
url.search = "";
|
|
||||||
if (wantReauth) url.searchParams.set("reauth", "1");
|
|
||||||
url.searchParams.set("next", next);
|
|
||||||
|
|
||||||
const res = NextResponse.redirect(url);
|
|
||||||
|
|
||||||
if (wantReauth) {
|
|
||||||
res.cookies.set("ma_at", "", { maxAge: 0, path: "/" });
|
|
||||||
res.cookies.set("ma_v", "", { maxAge: 0, path: "/" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function middleware(req: NextRequest) {
|
|
||||||
const url = req.nextUrl.clone();
|
|
||||||
const { pathname } = url;
|
|
||||||
|
|
||||||
// ── -1) Allow only the explicitly configured external webhook.
|
|
||||||
if (pathname === "/api/webhooks/kofi") {
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 0) Root must never redirect
|
|
||||||
if (pathname === "/") return NextResponse.next();
|
if (pathname === "/") return NextResponse.next();
|
||||||
|
|
||||||
// ── 1) Legacy → Portal mapping (before auth gating)
|
const legacyTool = LEGACY_TOOL_PATHS[pathname.replace(/\/$/, "")];
|
||||||
const mapped = legacyMap(pathname);
|
if (legacyTool) {
|
||||||
if (mapped && !isSameUrl(req, mapped)) {
|
const url = req.nextUrl.clone();
|
||||||
url.pathname = mapped.pathname;
|
url.pathname = "/";
|
||||||
if (mapped.query) {
|
|
||||||
for (const [k, v] of Object.entries(mapped.query)) url.searchParams.set(k, v);
|
|
||||||
}
|
|
||||||
return NextResponse.redirect(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 2) Auth gating
|
|
||||||
const token = req.cookies.get("ma_at")?.value ?? "";
|
|
||||||
const isAuthRoute = pathname.startsWith("/auth/");
|
|
||||||
const isProtected = !isPublicPath(pathname);
|
|
||||||
|
|
||||||
const forceAuth =
|
|
||||||
isAuthRoute &&
|
|
||||||
(url.searchParams.get("reauth") === "1" ||
|
|
||||||
url.searchParams.get("force") === "1");
|
|
||||||
|
|
||||||
if (!token && isProtected) {
|
|
||||||
return kickToSignIn(req, { reauth: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
const exp = jwtExp(token);
|
|
||||||
const expired = !exp || exp * 1000 <= Date.now();
|
|
||||||
|
|
||||||
if (isAuthRoute && !expired && !forceAuth) {
|
|
||||||
url.pathname = "/portal";
|
|
||||||
url.search = "";
|
url.search = "";
|
||||||
|
url.searchParams.set("tool", legacyTool);
|
||||||
return NextResponse.redirect(url);
|
return NextResponse.redirect(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isProtected) {
|
if (pathname === "/portal/utilities") {
|
||||||
if (expired) {
|
const url = req.nextUrl.clone();
|
||||||
return kickToSignIn(req, { reauth: true });
|
const requested = url.searchParams.get("t");
|
||||||
|
url.pathname = "/";
|
||||||
|
url.search = "";
|
||||||
|
if (requested && ["laser-toolkit", "files", "background-remover"].includes(requested)) {
|
||||||
|
url.searchParams.set("tool", requested);
|
||||||
|
}
|
||||||
|
return NextResponse.redirect(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DIRECTUS) {
|
if (PUBLIC_UTILITY_APIS.has(pathname)) return NextResponse.next();
|
||||||
const nowMinute = Math.floor(Date.now() / 60000).toString();
|
|
||||||
const lastValidated = req.cookies.get("ma_v")?.value;
|
|
||||||
|
|
||||||
if (lastValidated !== nowMinute) {
|
|
||||||
try {
|
|
||||||
const r = await fetch(`${DIRECTUS}/users/me?fields=id`, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
Accept: "application/json",
|
|
||||||
},
|
|
||||||
cache: "no-store",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!r.ok) {
|
|
||||||
return kickToSignIn(req, { reauth: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = NextResponse.next();
|
|
||||||
res.cookies.set("ma_v", nowMinute, {
|
|
||||||
path: "/",
|
|
||||||
httpOnly: false,
|
|
||||||
sameSite: "lax",
|
|
||||||
maxAge: 90,
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
} catch {
|
|
||||||
return kickToSignIn(req, { reauth: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
function legacyMap(pathname: string): MapResult | null {
|
|
||||||
if (pathname === "/" || pathname.startsWith("/portal")) return null;
|
|
||||||
|
|
||||||
const listRules: Array<[RegExp, MapResult]> = [
|
|
||||||
[/^\/background-remover\/?$/i, { pathname: "/portal/utilities", query: { t: "background-remover" } }],
|
|
||||||
[/^\/svgnest\/?$/i, { pathname: "/portal/utilities", query: { t: "svgnest" } }],
|
|
||||||
[/^\/laser-toolkit\/?$/i, { pathname: "/portal/utilities", query: { t: "laser-toolkit" } }],
|
|
||||||
[/^\/files\/?$/i, { pathname: "/portal/utilities", query: { t: "files" } }],
|
|
||||||
[/^\/buying-guide\/?$/i, { pathname: "/portal/buying-guide" }],
|
|
||||||
[/^\/lasers\/?$/i, { pathname: "/portal/laser-sources" }],
|
|
||||||
[/^\/projects\/?$/i, { pathname: "/portal/projects" }],
|
|
||||||
[/^\/my\/rigs\/?$/i, { pathname: "/portal/rigs", query: { t: "my" } }],
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const [re, dest] of listRules) {
|
|
||||||
if (re.test(pathname)) return dest;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPublicPath(pathname: string): boolean {
|
|
||||||
if (PUBLIC_PAGES.has(pathname)) return true;
|
|
||||||
|
|
||||||
if (
|
|
||||||
pathname.startsWith("/_next/") ||
|
|
||||||
pathname.startsWith("/static/") ||
|
|
||||||
pathname.startsWith("/images/") ||
|
|
||||||
pathname === "/favicon.ico" ||
|
|
||||||
pathname === "/robots.txt" ||
|
|
||||||
pathname === "/sitemap.xml"
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathname.startsWith("/api/")) {
|
if (pathname.startsWith("/api/")) {
|
||||||
return startsWithAny(pathname, PUBLIC_API_PREFIXES);
|
return NextResponse.json({ error: "Not available on the temporary utility site" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
const home = req.nextUrl.clone();
|
||||||
|
home.pathname = "/";
|
||||||
|
home.search = "";
|
||||||
|
return NextResponse.redirect(home);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match all except the usual static assets
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ["/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|images|static).*)"],
|
matcher: ["/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|images|static).*)"],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue