makearmy-app/middleware.ts

134 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// middleware.ts
import { NextResponse, NextRequest } from "next/server";
const PUBLIC_PATHS = new Set<string>(["/auth/sign-in", "/auth/sign-up"]);
/** 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) {
// start from existing params so we preserve any others
for (const [k, v] of Object.entries(mapped.query)) dest.searchParams.set(k, v);
}
return dest.href === req.url;
}
export function middleware(req: NextRequest) {
const url = req.nextUrl.clone();
const { pathname } = url;
// ── 1) Legacy → Portal / Canonical mapping (runs before auth gating)
const mapped = legacyMap(pathname);
if (mapped && !isSameUrl(req, mapped)) {
// Build destination on the same URL object to keep host/proto
url.pathname = mapped.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/");
// Authed users on any /auth/* route → /portal
if (token && isAuthRoute) {
url.pathname = "/portal";
url.search = "";
return NextResponse.redirect(url);
}
// Unauthed users on protected paths → sign-in (no ?next=)
if (!token && !isPublicPath(pathname)) {
url.pathname = "/auth/sign-in";
url.search = "";
return NextResponse.redirect(url);
}
return NextResponse.next();
}
type MapResult = { pathname: string; query?: Record<string, string> };
function legacyMap(pathname: string): MapResult | null {
// If were already inside the portal, dont try to remap again.
if (pathname.startsWith("/portal")) return null;
// 1) DETAIL PAGES: map legacy detail URLs straight into the portal with ?id=
// NOTE: We intentionally DO NOT remap `/lasers/:id` and `/projects/:id`
// so the portal iframes can load those canonical pages without recursion.
const detailRules: Array<[RegExp, (m: RegExpExecArray) => MapResult]> = [
// Laser settings
[/^\/fiber-settings\/([^/]+)\/?$/i, (m) => ({ pathname: "/portal/laser-settings", query: { t: "fiber", id: m[1] } })],
[/^\/uv-settings\/([^/]+)\/?$/i, (m) => ({ pathname: "/portal/laser-settings", query: { t: "uv", id: m[1] } })],
[/^\/co2-galvo-settings\/([^/]+)\/?$/i, (m) => ({ pathname: "/portal/laser-settings", query: { t: "co2-galvo", id: m[1] } })],
[/^\/co2-gantry-settings\/([^/]+)\/?$/i, (m) => ({ pathname: "/portal/laser-settings", query: { t: "co2-gantry", id: m[1] } })],
[/^\/co2gantry-settings\/([^/]+)\/?$/i, (m) => ({ pathname: "/portal/laser-settings", query: { t: "co2-gantry", id: m[1] } })],
// Materials
[/^\/materials\/([^/]+)\/?$/i, (m) => ({ pathname: "/portal/materials", query: { t: "materials", id: m[1] } })],
[/^\/materials-coatings\/([^/]+)\/?$/i, (m) => ({ pathname: "/portal/materials", query: { t: "materials-coatings", id: m[1] } })],
// (no lasers/projects detail remap here on purpose)
];
for (const [re, to] of detailRules) {
const m = re.exec(pathname);
if (m) return to(m);
}
// 2) LIST PAGES: legacy lists → portal lists (with tab param) or sections
const listRules: Array<[RegExp, MapResult]> = [
// Laser settings lists
[/^\/fiber-settings\/?$/i, { pathname: "/portal/laser-settings", query: { t: "fiber" } }],
[/^\/uv-settings\/?$/i, { pathname: "/portal/laser-settings", query: { t: "uv" } }],
[/^\/co2-galvo-settings\/?$/i, { pathname: "/portal/laser-settings", query: { t: "co2-galvo" } }],
[/^\/co2-ganry-settings\/?$/i, { pathname: "/portal/laser-settings", query: { t: "co2-gantry" } }], // typo catch
[/^\/co2-gantry-settings\/?$/i, { pathname: "/portal/laser-settings", query: { t: "co2-gantry" } }],
[/^\/co2gantry-settings\/?$/i, { pathname: "/portal/laser-settings", query: { t: "co2-gantry" } }], // old alias
// Materials lists
[/^\/materials\/?$/i, { pathname: "/portal/materials", query: { t: "materials" } }],
[/^\/materials\/materials\/?$/i, { pathname: "/portal/materials", query: { t: "materials" } }],
[/^\/materials\/materials-coatings\/?$/i,
{ pathname: "/portal/materials", query: { t: "materials-coatings" } }],
[/^\/materials-coatings\/?$/i, { pathname: "/portal/materials", query: { t: "materials-coatings" } }],
// Other lists
[/^\/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_PATHS.has(pathname)) return true;
// Static assets / internals
if (
pathname.startsWith("/_next/") ||
pathname.startsWith("/static/") ||
pathname.startsWith("/images/") ||
pathname === "/favicon.ico" ||
pathname === "/robots.txt" ||
pathname === "/sitemap.xml"
) return true;
// API routes arent gated here; each route should enforce auth as needed
if (pathname.startsWith("/api/")) return true;
// Everything else is protected
return false;
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|images|static).*)"],
};