diff --git a/app/app/api/bgbye/process/route.ts b/app/app/api/bgbye/process/route.ts index 7a91181..d41da54 100644 --- a/app/app/api/bgbye/process/route.ts +++ b/app/app/api/bgbye/process/route.ts @@ -1,7 +1,6 @@ export const runtime = "nodejs"; import { NextResponse } from "next/server"; -import { getUserBearerFromRequest } from "@/lib/directus"; import { rateLimit, requestIp } from "@/lib/rate-limit"; 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)) { return NextResponse.json({ error: "Too many processing requests" }, { status: 429 }); } diff --git a/app/app/page.tsx b/app/app/page.tsx index 739763a..cf20182 100644 --- a/app/app/page.tsx +++ b/app/app/page.tsx @@ -1,49 +1,15 @@ -// app/page.tsx -import { cookies } from "next/headers"; -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"; +import { Suspense } from "react"; +import TemporaryUtilityHub from "@/components/TemporaryUtilityHub"; -type SearchParams = { [key: string]: string | string[] | undefined }; - -export default async function HomePage({ - 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 const metadata = { + title: "Laser Everything Community Utilities", + description: "Free laser calculators, public files, and image background removal tools.", +}; +export default function HomePage() { return ( -
- {reauth && ( -

- Your session expired. Please sign in again. -

- )} - -
-

MakeArmy

-

- Free to use. Manage laser rigs, settings, and projects—all in one - place. -

-
- -
- - -
- -
-

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

-

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.

-
-
+ Loading utilities…}> + + ); } diff --git a/app/components/TemporaryUtilityHub.tsx b/app/components/TemporaryUtilityHub.tsx new file mode 100644 index 0000000..3f03332 --- /dev/null +++ b/app/components/TemporaryUtilityHub.tsx @@ -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 ( +
+
+

+ Laser Everything +

+

Community Utilities

+

+ Our database and member features are temporarily offline while we change backend + systems. These free utilities remain available without an account. +

+
+ + + +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + +
+

{active.label}

+

{active.description}

+
+
+ +
+ + +
+ ); +} diff --git a/app/components/portal/LaserToolkitSwitcher.tsx b/app/components/portal/LaserToolkitSwitcher.tsx index 34e7673..69ddfda 100644 --- a/app/components/portal/LaserToolkitSwitcher.tsx +++ b/app/components/portal/LaserToolkitSwitcher.tsx @@ -6,7 +6,7 @@ import { useSearchParams, useRouter } from "next/navigation"; import { cn } from "@/lib/utils"; 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 router = useRouter(); @@ -24,7 +24,7 @@ export default function LaserToolkitSwitcher() { function setTab(k: string) { const q = new URLSearchParams(sp.toString()); q.set("lt", k); - router.replace(`/portal/utilities?${q.toString()}`, { scroll: false }); + router.replace(`${basePath}?${q.toString()}`, { scroll: false }); } if (!active) { diff --git a/app/components/utilities/files/FileBrowserPanel.tsx b/app/components/utilities/files/FileBrowserPanel.tsx index 28033d9..fae849a 100644 --- a/app/components/utilities/files/FileBrowserPanel.tsx +++ b/app/components/utilities/files/FileBrowserPanel.tsx @@ -208,8 +208,8 @@ export default function FileBrowserPanel() {
{path || "/"}
-
-
+
+
Name
Type
diff --git a/app/middleware.ts b/app/middleware.ts index 2b5bf99..a178aa6 100644 --- a/app/middleware.ts +++ b/app/middleware.ts @@ -1,217 +1,62 @@ -// middleware.ts -import { NextResponse, NextRequest } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; + +const PUBLIC_UTILITY_APIS = new Set([ + "/api/bgbye/process", + "/api/files/list", + "/api/files/raw", + "/api/files/download", +]); + +const LEGACY_TOOL_PATHS: Record = { + "/laser-toolkit": "laser-toolkit", + "/files": "files", + "/background-remover": "background-remover", +}; /** - * Public pages that should remain reachable without being signed in. - * Everything else is considered protected (including most /api/*). + * Temporary utility-only site. + * + * 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. */ - const PUBLIC_PAGES = new Set([ - "/", // splash page is public - "/auth/sign-in", - "/auth/sign-up", - ]); +export function middleware(req: NextRequest) { + const { pathname } = req.nextUrl; - /** - * API paths that are explicitly allowed without auth. - * Keep this list tiny; add broad /api/webhooks to allow ALL webhook endpoints. - */ - const PUBLIC_API_PREFIXES: string[] = [ - "/api/auth/", // login/refresh/callback endpoints - ]; + if (pathname === "/") return NextResponse.next(); - /** 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 legacyTool = LEGACY_TOOL_PATHS[pathname.replace(/\/$/, "")]; + if (legacyTool) { + const url = req.nextUrl.clone(); + url.pathname = "/"; + url.search = ""; + url.searchParams.set("tool", legacyTool); + return NextResponse.redirect(url); + } - type MapResult = { pathname: string; query?: Record }; + if (pathname === "/portal/utilities") { + const url = req.nextUrl.clone(); + 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); + } - /** Helper: does the path start with any prefix in a list? */ - function startsWithAny(pathname: string, prefixes: string[]) { - return prefixes.some((p) => pathname.startsWith(p)); - } + if (PUBLIC_UTILITY_APIS.has(pathname)) return NextResponse.next(); - /** 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; - } + if (pathname.startsWith("/api/")) { + return NextResponse.json({ error: "Not available on the temporary utility site" }, { status: 404 }); + } - /** 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; - } - } + const home = req.nextUrl.clone(); + home.pathname = "/"; + home.search = ""; + return NextResponse.redirect(home); +} - /** - * Build redirect to /auth/sign-in?next=. - * Only set reauth=1 (and clear cookies) when opts.reauth === true. - */ - function kickToSignIn(req: NextRequest, opts?: { reauth?: boolean }) { - const wantReauth = !!opts?.reauth; - - 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(); - - // ── 1) Legacy → Portal mapping (before auth gating) - const mapped = legacyMap(pathname); - if (mapped && !isSameUrl(req, mapped)) { - 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/"); - 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 = ""; - return NextResponse.redirect(url); - } - - if (isProtected) { - if (expired) { - return kickToSignIn(req, { reauth: true }); - } - - if (DIRECTUS) { - 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/")) { - return startsWithAny(pathname, PUBLIC_API_PREFIXES); - } - - return false; - } - - // Match all except the usual static assets - export const config = { - matcher: ["/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|images|static).*)"], - }; +export const config = { + matcher: ["/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|images|static).*)"], +};