prepare app migration to db.lasereverything.net
- remove hardcoded makearmy.io references - convert utilities to relative paths - fix external link detection - add svgnest middleware rule - update background remover navigation
This commit is contained in:
parent
e08d4d81b3
commit
3614acd297
3 changed files with 208 additions and 73 deletions
|
|
@ -6,11 +6,11 @@ import { useRouter, useSearchParams } from "next/navigation";
|
|||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Item = {
|
||||
key: string; // used in ?t=
|
||||
key: string; // used in ?t=
|
||||
label: string;
|
||||
note?: string;
|
||||
icon?: string; // optional icon (public/images/utils/<icon>)
|
||||
href?: string; // optional absolute URL (used if no component)
|
||||
icon?: string; // optional icon (public/images/utils/<icon>)
|
||||
href?: string; // optional absolute URL (used if no component)
|
||||
component?: React.ComponentType<{ embedded?: boolean }>;
|
||||
};
|
||||
|
||||
|
|
@ -19,10 +19,9 @@ const BackgroundRemoverPanel = dynamic(
|
|||
() => import("@/components/utilities/BackgroundRemoverPanel"),
|
||||
{ ssr: false }
|
||||
);
|
||||
const SVGNestPanel = dynamic(
|
||||
() => import("@/components/utilities/SVGNestPanel"),
|
||||
{ ssr: false }
|
||||
);
|
||||
const SVGNestPanel = dynamic(() => import("@/components/utilities/SVGNestPanel"), {
|
||||
ssr: false,
|
||||
});
|
||||
const LaserToolkitSwitcher = dynamic(
|
||||
() => import("@/components/portal/LaserToolkitSwitcher"),
|
||||
{ ssr: false }
|
||||
|
|
@ -34,33 +33,99 @@ const FileBrowserPanel = dynamic(
|
|||
);
|
||||
|
||||
const ITEMS: Item[] = [
|
||||
{ key: "laser-toolkit", label: "Laser Toolkit", note: "convert laser settings, interval and more", icon: "toolkit.png", component: LaserToolkitSwitcher, href: "https://makearmy.io/laser-toolkit" },
|
||||
{ key: "files", label: "File Server", note: "download from our file explorer", icon: "fs.png", component: FileBrowserPanel, href: "https://makearmy.io/files" },
|
||||
{ key: "svgnest", label: "SVGnest", note: "automatically nests parts and exports svg", icon: "nest.png", component: SVGNestPanel, href: "https://makearmy.io/svgnest" },
|
||||
{ key: "background-remover", label: "BG Remover", note: "open source background remover", icon: "bgrm.png", component: BackgroundRemoverPanel, href: "https://makearmy.io/background-remover" },
|
||||
{ key: "picsur", label: "Picsur", note: "Simple Image Host", icon: "picsur.png", href: "https://images.makearmy.io" },
|
||||
{ key: "privatebin", label: "PrivateBin", note: "Encrypted internet clipboard", icon: "privatebin.png", href: "https://paste.makearmy.io/" },
|
||||
{ key: "forgejo", label: "Forgejo", note: "git for our community members", icon: "forge.png", href: "https://forge.makearmy.io" },
|
||||
{
|
||||
key: "laser-toolkit",
|
||||
label: "Laser Toolkit",
|
||||
note: "convert laser settings, interval and more",
|
||||
icon: "toolkit.png",
|
||||
component: LaserToolkitSwitcher,
|
||||
href: "/laser-toolkit",
|
||||
},
|
||||
{
|
||||
key: "files",
|
||||
label: "File Server",
|
||||
note: "download from our file explorer",
|
||||
icon: "fs.png",
|
||||
component: FileBrowserPanel,
|
||||
href: "/files",
|
||||
},
|
||||
{
|
||||
key: "svgnest",
|
||||
label: "SVGnest",
|
||||
note: "automatically nests parts and exports svg",
|
||||
icon: "nest.png",
|
||||
component: SVGNestPanel,
|
||||
href: "/svgnest",
|
||||
},
|
||||
{
|
||||
key: "background-remover",
|
||||
label: "BG Remover",
|
||||
note: "open source background remover",
|
||||
icon: "bgrm.png",
|
||||
component: BackgroundRemoverPanel,
|
||||
href: "/background-remover",
|
||||
},
|
||||
|
||||
// These stay on makearmy (external services)
|
||||
{
|
||||
key: "picsur",
|
||||
label: "Picsur",
|
||||
note: "Simple Image Host",
|
||||
icon: "picsur.png",
|
||||
href: "https://images.makearmy.io",
|
||||
},
|
||||
{
|
||||
key: "privatebin",
|
||||
label: "PrivateBin",
|
||||
note: "Encrypted internet clipboard",
|
||||
icon: "privatebin.png",
|
||||
href: "https://paste.makearmy.io/",
|
||||
},
|
||||
{
|
||||
key: "forgejo",
|
||||
label: "Forgejo",
|
||||
note: "git for our community members",
|
||||
icon: "forge.png",
|
||||
href: "https://forge.makearmy.io",
|
||||
},
|
||||
];
|
||||
|
||||
function isExternal(urlStr: string | undefined) {
|
||||
if (!urlStr) return false;
|
||||
function isAbsoluteUrl(href: string) {
|
||||
return /^https?:\/\//i.test(href);
|
||||
}
|
||||
|
||||
/**
|
||||
* External if it's an absolute URL and NOT same-origin as the current site.
|
||||
* Relative paths are always internal.
|
||||
*/
|
||||
function isExternalHref(href?: string) {
|
||||
if (!href) return false;
|
||||
if (!isAbsoluteUrl(href)) return false;
|
||||
|
||||
try {
|
||||
const u = new URL(urlStr);
|
||||
return u.hostname !== "makearmy.io";
|
||||
const u = new URL(href);
|
||||
if (typeof window === "undefined") return true;
|
||||
return u.origin !== window.location.origin;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function toOnsitePath(urlStr: string): string {
|
||||
/**
|
||||
* If href is absolute AND same-origin, convert it to a site-relative path.
|
||||
* Otherwise return as-is.
|
||||
*/
|
||||
function toSameOriginPath(href: string) {
|
||||
if (!isAbsoluteUrl(href)) return href;
|
||||
|
||||
try {
|
||||
const u = new URL(urlStr);
|
||||
if (u.hostname === "makearmy.io") {
|
||||
const u = new URL(href);
|
||||
if (typeof window === "undefined") return href;
|
||||
if (u.origin === window.location.origin) {
|
||||
return `${u.pathname}${u.search}${u.hash}`;
|
||||
}
|
||||
} catch {}
|
||||
return urlStr;
|
||||
return href;
|
||||
}
|
||||
|
||||
function Panel({ item }: { item: Item }) {
|
||||
|
|
@ -74,18 +139,25 @@ function Panel({ item }: { item: Item }) {
|
|||
);
|
||||
}
|
||||
|
||||
const external = isExternal(item.href);
|
||||
const href = item.href || "/";
|
||||
const external = isExternalHref(href);
|
||||
|
||||
if (external) {
|
||||
return (
|
||||
<div className="space-y-2 text-sm">
|
||||
<a href={item.href} target="_blank" rel="noopener noreferrer" className="underline">
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Open {item.label}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const src = toOnsitePath(item.href || "/");
|
||||
const src = toSameOriginPath(href);
|
||||
return (
|
||||
<iframe
|
||||
key={src}
|
||||
|
|
@ -105,11 +177,11 @@ export default function UtilitySwitcher() {
|
|||
|
||||
const activeKey = useMemo(() => {
|
||||
const t = (sp.get("t") || ITEMS[0].key).toLowerCase();
|
||||
return ITEMS.some(i => i.key === t) ? t : ITEMS[0].key;
|
||||
return ITEMS.some((i) => i.key === t) ? t : ITEMS[0].key;
|
||||
}, [sp]);
|
||||
|
||||
const activeItem = useMemo(
|
||||
() => ITEMS.find(i => i.key === activeKey) || ITEMS[0],
|
||||
() => ITEMS.find((i) => i.key === activeKey) || ITEMS[0],
|
||||
[activeKey]
|
||||
);
|
||||
|
||||
|
|
@ -119,22 +191,25 @@ export default function UtilitySwitcher() {
|
|||
router.replace(`/portal/utilities?${q.toString()}`, { scroll: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional auto-open behavior for external tools when selected via URL (?t=...).
|
||||
* RECOMMENDED: keep this off to avoid popup blockers / surprise tabs.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const item = activeItem;
|
||||
if (!item) return;
|
||||
if (!item?.href) return;
|
||||
if (item.component) return;
|
||||
const external = isExternal(item.href);
|
||||
if (!external) return;
|
||||
if (!isExternalHref(item.href)) return;
|
||||
|
||||
if (openedRef.current === item.key) return;
|
||||
openedRef.current = item.key;
|
||||
|
||||
const AUTO_OPEN_ON_FIRST_PAINT = true;
|
||||
const AUTO_OPEN_ON_FIRST_PAINT = false;
|
||||
if (AUTO_OPEN_ON_FIRST_PAINT || !firstPaint) {
|
||||
window.open(item.href!, "_blank", "noopener,noreferrer");
|
||||
window.open(item.href, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeItem?.key, activeItem?.href]);
|
||||
}, [activeItem?.key]);
|
||||
|
||||
useEffect(() => {
|
||||
setFirstPaint(false);
|
||||
|
|
@ -146,9 +221,10 @@ export default function UtilitySwitcher() {
|
|||
<div className="mb-4 flex flex-wrap items-center gap-2">
|
||||
{ITEMS.map((it) => {
|
||||
const isInline = Boolean(it.component);
|
||||
const external = !isInline && isExternal(it.href);
|
||||
const external = !isInline && isExternalHref(it.href);
|
||||
const iconSrc = it.icon ? `/images/utils/${it.icon}` : null;
|
||||
const isActive = it.key === activeKey;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={it.key}
|
||||
|
|
@ -160,7 +236,9 @@ export default function UtilitySwitcher() {
|
|||
}}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-md border px-3 py-1.5 text-sm transition",
|
||||
isActive ? "bg-primary text-primary-foreground" : "hover:bg-muted"
|
||||
isActive
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "hover:bg-muted"
|
||||
)}
|
||||
title={it.note || it.label}
|
||||
>
|
||||
|
|
@ -177,7 +255,9 @@ export default function UtilitySwitcher() {
|
|||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<span className="truncate">{it.label}</span>
|
||||
|
||||
{!isInline && external && (
|
||||
<span className="rounded bg-muted px-1 py-0.5 text-[10px] uppercase tracking-wide text-muted-foreground">
|
||||
new tab
|
||||
|
|
|
|||
|
|
@ -78,9 +78,13 @@ export default function BackgroundRemoverPage() {
|
|||
const [sourceUrl, setSourceUrl] = useState<string | null>(null);
|
||||
const [natural, setNatural] = useState<{ w: number; h: number } | null>(null);
|
||||
|
||||
const [status, setStatus] = useState<Record<Canonical, Status>>(
|
||||
() => Object.fromEntries(METHODS.map((m) => [m.key, "idle"])) as Record<Canonical, Status>
|
||||
);
|
||||
const [status, setStatus] = useState<Record<Canonical, Status>>(() => {
|
||||
return Object.fromEntries(METHODS.map((m) => [m.key, "idle"])) as Record<
|
||||
Canonical,
|
||||
Status
|
||||
>;
|
||||
});
|
||||
|
||||
const [results, setResults] = useState<ResultMap>({});
|
||||
const resultsRef = useRef<ResultMap>({});
|
||||
useEffect(() => {
|
||||
|
|
@ -182,7 +186,9 @@ export default function BackgroundRemoverPage() {
|
|||
|
||||
const runOne = async (key: Canonical) => {
|
||||
// When GPU-safe is on, try progressively smaller long-edge previews.
|
||||
const sizes = gpuSafe ? BATCH_SIZES : [Math.max(natural?.w || 0, natural?.h || 0) || 4096];
|
||||
const sizes = gpuSafe
|
||||
? BATCH_SIZES
|
||||
: [Math.max(natural?.w || 0, natural?.h || 0) || 4096];
|
||||
|
||||
let lastErr: string | null = null;
|
||||
const t0 = performance.now();
|
||||
|
|
@ -195,10 +201,15 @@ export default function BackgroundRemoverPage() {
|
|||
fd.append("method", key);
|
||||
|
||||
// CHANGED: call local proxy instead of a hardcoded service
|
||||
const res = await fetch("/api/bgbye/process", { method: "POST", body: fd });
|
||||
const res = await fetch("/api/bgbye/process", {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const txt = await res.text().catch(() => "");
|
||||
const retryable = /out of memory|onnxruntime|cuda|allocate|500/i.test(txt);
|
||||
const retryable =
|
||||
/out of memory|onnxruntime|cuda|allocate|500/i.test(txt);
|
||||
if (gpuSafe && retryable) {
|
||||
lastErr = txt || `HTTP ${res.status}`;
|
||||
continue; // try next smaller size
|
||||
|
|
@ -261,12 +272,16 @@ export default function BackgroundRemoverPage() {
|
|||
const f = e.dataTransfer.files?.[0];
|
||||
if (f) onPick(f);
|
||||
};
|
||||
|
||||
const onSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const f = e.target.files?.[0] ?? null;
|
||||
onPick(f);
|
||||
};
|
||||
|
||||
const aspect = useMemo(() => (!natural ? 16 / 9 : natural.w / natural.h || 16 / 9), [natural]);
|
||||
const aspect = useMemo(
|
||||
() => (!natural ? 16 / 9 : natural.w / natural.h || 16 / 9),
|
||||
[natural]
|
||||
);
|
||||
|
||||
const updateByClientX = useCallback((clientX: number) => {
|
||||
const el = frameRef.current;
|
||||
|
|
@ -275,6 +290,7 @@ export default function BackgroundRemoverPage() {
|
|||
const pct = ((clientX - rect.left) / rect.width) * 100;
|
||||
setReveal(Math.min(100, Math.max(0, pct)));
|
||||
}, []);
|
||||
|
||||
const onMouseDown = (e: React.MouseEvent) => {
|
||||
draggingRef.current = true;
|
||||
updateByClientX(e.clientX);
|
||||
|
|
@ -284,6 +300,7 @@ export default function BackgroundRemoverPage() {
|
|||
updateByClientX(e.clientX);
|
||||
};
|
||||
const onMouseUp = () => (draggingRef.current = false);
|
||||
|
||||
const onTouchStart = (e: React.TouchEvent) => {
|
||||
draggingRef.current = true;
|
||||
updateByClientX(e.touches[0].clientX);
|
||||
|
|
@ -321,24 +338,36 @@ export default function BackgroundRemoverPage() {
|
|||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
fd.append("method", active);
|
||||
|
||||
// CHANGED: call local proxy instead of a hardcoded service
|
||||
const res = await fetch("/api/bgbye/process", { method: "POST", body: fd });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
|
||||
const outBlob = await res.blob();
|
||||
const ms = performance.now() - t0;
|
||||
const previewBlob = await makePreview(outBlob);
|
||||
const previewUrl = URL.createObjectURL(previewBlob);
|
||||
const prev = resultsRef.current[active];
|
||||
if (prev) revoke(prev.previewUrl);
|
||||
setResults((r) => ({ ...r, [active]: { fullBlob: outBlob, previewUrl, bytes: outBlob.size, ms } }));
|
||||
|
||||
setResults((r) => ({
|
||||
...r,
|
||||
[active]: { fullBlob: outBlob, previewUrl, bytes: outBlob.size, ms },
|
||||
}));
|
||||
setStatus((s) => ({ ...s, [active]: "ok" }));
|
||||
} catch {
|
||||
setStatus((s) => ({ ...s, [active]: "error" }));
|
||||
}
|
||||
}, [file, active]);
|
||||
|
||||
const doneCount = useMemo(() => METHODS.filter((m) => status[m.key] === "ok").length, [status]);
|
||||
const pendingCount = useMemo(() => METHODS.filter((m) => status[m.key] === "pending").length, [status]);
|
||||
const doneCount = useMemo(
|
||||
() => METHODS.filter((m) => status[m.key] === "ok").length,
|
||||
[status]
|
||||
);
|
||||
const pendingCount = useMemo(
|
||||
() => METHODS.filter((m) => status[m.key] === "pending").length,
|
||||
[status]
|
||||
);
|
||||
|
||||
function StatusDot({ s }: { s: Status }) {
|
||||
const cls =
|
||||
|
|
@ -362,7 +391,7 @@ export default function BackgroundRemoverPage() {
|
|||
<div className="mb-4 flex items-center justify-between gap-3">
|
||||
<h1 className="text-2xl font-semibold">Background Remover</h1>
|
||||
<a
|
||||
href="https://makearmy.io"
|
||||
href="/"
|
||||
className="px-3 py-1 rounded-md border border-zinc-700 hover:bg-zinc-800/60 text-sm"
|
||||
>
|
||||
Back to main
|
||||
|
|
@ -379,7 +408,12 @@ export default function BackgroundRemoverPage() {
|
|||
<div
|
||||
ref={frameRef}
|
||||
className="app-frame checkerboard relative w-full rounded-2xl shadow-inner"
|
||||
style={{ aspectRatio: `${aspect}`, maxWidth: "1200px", maxHeight: "80vh", marginInline: "auto" }}
|
||||
style={{
|
||||
aspectRatio: `${aspect}`,
|
||||
maxWidth: "1200px",
|
||||
maxHeight: "80vh",
|
||||
marginInline: "auto",
|
||||
}}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={onDrop}
|
||||
onMouseDown={onMouseDown}
|
||||
|
|
@ -437,7 +471,9 @@ export default function BackgroundRemoverPage() {
|
|||
{/* Divider & Thumb */}
|
||||
<div
|
||||
className="slider-handle"
|
||||
style={{ "--reveal": `${Math.min(100, Math.max(0, reveal))}` } as React.CSSProperties}
|
||||
style={
|
||||
{ "--reveal": `${Math.min(100, Math.max(0, reveal))}` } as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div className="slider-thumb">
|
||||
<div className="w-1.5 h-4 bg-white/80 rounded" />
|
||||
|
|
@ -453,7 +489,9 @@ export default function BackgroundRemoverPage() {
|
|||
<button
|
||||
key={key}
|
||||
className={`w-full justify-center px-3 py-2 rounded-md border flex items-center gap-2 ${
|
||||
active === key ? "border-blue-400 bg-blue-500/20" : "border-zinc-700 hover:bg-zinc-800/60"
|
||||
active === key
|
||||
? "border-blue-400 bg-blue-500/20"
|
||||
: "border-zinc-700 hover:bg-zinc-800/60"
|
||||
}`}
|
||||
onClick={() => setActive(key)}
|
||||
disabled={!file}
|
||||
|
|
@ -479,13 +517,20 @@ export default function BackgroundRemoverPage() {
|
|||
|
||||
{/* GPU-safe toggle */}
|
||||
<label className="flex items-center gap-2 text-sm text-zinc-300 cursor-pointer select-none order-1">
|
||||
<input type="checkbox" checked={gpuSafe} onChange={(e) => setGpuSafe(e.target.checked)} /> GPU-safe mode
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={gpuSafe}
|
||||
onChange={(e) => setGpuSafe(e.target.checked)}
|
||||
/>{" "}
|
||||
GPU-safe mode
|
||||
</label>
|
||||
|
||||
<div className="text-zinc-400 text-sm order-2">
|
||||
{file ? (
|
||||
pendingCount > 0 ? (
|
||||
<span>Processing… {doneCount}/{METHODS.length} finished</span>
|
||||
<span>
|
||||
Processing… {doneCount}/{METHODS.length} finished
|
||||
</span>
|
||||
) : doneCount > 0 ? (
|
||||
<span>Done: {doneCount} methods succeeded</span>
|
||||
) : (
|
||||
|
|
@ -516,7 +561,13 @@ export default function BackgroundRemoverPage() {
|
|||
? "border-sky-600 bg-sky-600/20 hover:bg-sky-600/30"
|
||||
: "border-zinc-700 text-zinc-400 cursor-not-allowed"
|
||||
}`}
|
||||
title={!file ? "Select a file first" : !active ? "Choose a method" : "Render selected method at full resolution"}
|
||||
title={
|
||||
!file
|
||||
? "Select a file first"
|
||||
: !active
|
||||
? "Choose a method"
|
||||
: "Render selected method at full resolution"
|
||||
}
|
||||
>
|
||||
Full-res render
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { NextResponse, NextRequest } from "next/server";
|
|||
* Everything else is considered protected (including most /api/*).
|
||||
*/
|
||||
const PUBLIC_PAGES = new Set<string>([
|
||||
"/", // splash page is public
|
||||
"/", // splash page is public
|
||||
"/auth/sign-in",
|
||||
"/auth/sign-up",
|
||||
]);
|
||||
|
|
@ -16,15 +16,19 @@ import { NextResponse, NextRequest } from "next/server";
|
|||
* Keep this list tiny; add broad /api/webhooks to allow ALL webhook endpoints.
|
||||
*/
|
||||
const PUBLIC_API_PREFIXES: string[] = [
|
||||
"/api/auth", // login/refresh/callback endpoints
|
||||
"/api/files/list", // read-only file endpoints
|
||||
"/api/auth", // login/refresh/callback endpoints
|
||||
"/api/files/list", // read-only file endpoints
|
||||
"/api/files/raw",
|
||||
"/api/files/download",
|
||||
"/api/webhooks", // ← allow ALL webhooks (e.g. /api/webhooks/kofi, /api/webhooks/*)
|
||||
"/api/webhooks", // allow ALL webhook 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(/\/$/, "");
|
||||
const DIRECTUS = (
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ||
|
||||
process.env.DIRECTUS_URL ||
|
||||
""
|
||||
).replace(/\/$/, "");
|
||||
|
||||
type MapResult = { pathname: string; query?: Record<string, string> };
|
||||
|
||||
|
|
@ -73,11 +77,9 @@ import { NextResponse, NextRequest } from "next/server";
|
|||
|
||||
const res = NextResponse.redirect(url);
|
||||
|
||||
// Only clear auth markers in true re-auth scenarios
|
||||
if (wantReauth) {
|
||||
res.cookies.set("ma_at", "", { maxAge: 0, path: "/" });
|
||||
res.cookies.set("ma_v", "", { maxAge: 0, path: "/" }); // throttle marker
|
||||
// res.cookies.set("ma_rt", "", { maxAge: 0, path: "/" }); // if you use refresh tokens
|
||||
res.cookies.set("ma_v", "", { maxAge: 0, path: "/" });
|
||||
}
|
||||
|
||||
return res;
|
||||
|
|
@ -87,13 +89,12 @@ import { NextResponse, NextRequest } from "next/server";
|
|||
const url = req.nextUrl.clone();
|
||||
const { pathname } = url;
|
||||
|
||||
// ── -1) Always allow ALL webhook endpoints (no mapping, no gating, no redirects)
|
||||
// This lets external providers (Ko-fi, Patreon, etc.) POST without auth.
|
||||
// ── -1) Always allow ALL webhook endpoints
|
||||
if (pathname === "/api/webhooks" || pathname.startsWith("/api/webhooks/")) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// ── 0) Root must never redirect (no mapping, no gating).
|
||||
// ── 0) Root must never redirect
|
||||
if (pathname === "/") return NextResponse.next();
|
||||
|
||||
// ── 1) Legacy → Portal mapping (before auth gating)
|
||||
|
|
@ -113,7 +114,8 @@ import { NextResponse, NextRequest } from "next/server";
|
|||
|
||||
const forceAuth =
|
||||
isAuthRoute &&
|
||||
(url.searchParams.get("reauth") === "1" || url.searchParams.get("force") === "1");
|
||||
(url.searchParams.get("reauth") === "1" ||
|
||||
url.searchParams.get("force") === "1");
|
||||
|
||||
if (!token && isProtected) {
|
||||
return kickToSignIn(req, { reauth: false });
|
||||
|
|
@ -174,20 +176,21 @@ import { NextResponse, NextRequest } from "next/server";
|
|||
function legacyMap(pathname: string): MapResult | null {
|
||||
if (pathname === "/" || pathname.startsWith("/portal")) return null;
|
||||
|
||||
// detail mappings elided for brevity…
|
||||
|
||||
const listRules: Array<[RegExp, MapResult]> = [
|
||||
[/^\/background-remover\/?$/i, { pathname: "/portal/utilities", query: { t: "background-remover" } }],
|
||||
[/^\/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" } }],
|
||||
[/^\/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;
|
||||
}
|
||||
|
||||
|
|
@ -208,10 +211,11 @@ import { NextResponse, NextRequest } from "next/server";
|
|||
if (pathname.startsWith("/api/")) {
|
||||
return startsWithAny(pathname, PUBLIC_API_PREFIXES);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Match all except the usual static assets; webhooks are handled above.
|
||||
// Match all except the usual static assets
|
||||
export const config = {
|
||||
matcher: ["/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|images|static).*)"],
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue