diff --git a/app/auth/sign-in/page.tsx b/app/auth/sign-in/page.tsx
index 59709b12..2938212c 100644
--- a/app/auth/sign-in/page.tsx
+++ b/app/auth/sign-in/page.tsx
@@ -2,6 +2,7 @@
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import SignIn from "./sign-in";
+import { isJwtValid } from "@/lib/jwt";
export default async function SignInPage({
searchParams,
@@ -22,7 +23,7 @@ export default async function SignInPage({
if (!reauth) {
const ck = await cookies();
const at = ck.get("ma_at")?.value;
- if (at) redirect("/portal");
+ if (isJwtValid(at)) redirect("/portal");
}
return ;
diff --git a/app/auth/sign-in/sign-in.tsx b/app/auth/sign-in/sign-in.tsx
index 55b24cdc..ce4c6868 100644
--- a/app/auth/sign-in/sign-in.tsx
+++ b/app/auth/sign-in/sign-in.tsx
@@ -20,11 +20,16 @@ export default function SignIn({ nextPath = "/portal", reauth = false }: Props)
setErr(null);
setLoading(true);
try {
+ const body = {
+ identifier: identifier.trim(),
+ password,
+ };
+
const res = await fetch("/api/auth/login", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json", Accept: "application/json" },
- body: JSON.stringify({ identifier, password }),
+ body: JSON.stringify(body),
});
const txt = await res.text();
@@ -34,7 +39,10 @@ export default function SignIn({ nextPath = "/portal", reauth = false }: Props)
} catch {}
if (!res.ok) {
- throw new Error(j?.error || j?.message || `Sign-in failed (${res.status})`);
+ // surface server-provided message when available
+ const msg =
+ j?.error || j?.message || (res.status === 401 ? "Invalid credentials." : `Sign-in failed (${res.status})`);
+ throw new Error(msg);
}
// If this sign-in is being used as a re-auth step, set a short-lived 'recent auth' marker.
@@ -56,6 +64,8 @@ export default function SignIn({ nextPath = "/portal", reauth = false }: Props)
[identifier, password, nextPath, router, reauth]
);
+ const createHref = `/auth/sign-up?next=${encodeURIComponent(nextPath)}`;
+
return (
@@ -116,14 +126,13 @@ export default function SignIn({ nextPath = "/portal", reauth = false }: Props)
- {!reauth && (
-
- )}
+ {/* Always show a sign-up path, even on reauth, to avoid dead-ends for first-time visitors */}
+
);
}
diff --git a/app/auth/sign-up/page.tsx b/app/auth/sign-up/page.tsx
index 569ae57c..96ce741d 100644
--- a/app/auth/sign-up/page.tsx
+++ b/app/auth/sign-up/page.tsx
@@ -2,6 +2,7 @@
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import SignUp from "./sign-up";
+import { isJwtValid } from "@/lib/jwt";
export default async function SignUpPage({
searchParams,
@@ -10,7 +11,7 @@ export default async function SignUpPage({
}) {
const ck = await cookies();
const at = ck.get("ma_at")?.value;
- if (at) redirect("/portal");
+ if (isJwtValid(at)) redirect("/portal");
const sp = searchParams ?? {};
const nextParam = Array.isArray(sp.next) ? sp.next[0] : sp.next;
diff --git a/app/page.tsx b/app/page.tsx
index 245481bc..29fd3114 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -3,19 +3,35 @@ 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";
-export default async function HomePage() {
- // If already signed in, go straight to the app
+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 (at) redirect("/portal");
+ if (isJwtValid(at)) redirect("/portal");
+
+ const reauth = searchParams?.reauth === "1";
return (
+ {reauth && (
+
+ Your session expired. Please sign in again.
+
+ )}
+
MakeArmy
- Free to use. Manage laser rigs, settings, and projects—all in one place.
+ Free to use. Manage laser rigs, settings, and projects—all in one
+ place.
@@ -29,12 +45,13 @@ export default async function HomePage() {
Sign in
{/* Uses your existing sign-in component */}
-
+
- We only use cookies strictly necessary to operate the site (e.g., your sign-in session).
+ We only use cookies strictly necessary to operate the site (e.g., your
+ sign-in session).
);
diff --git a/lib/jwt.ts b/lib/jwt.ts
new file mode 100644
index 00000000..287a9ff2
--- /dev/null
+++ b/lib/jwt.ts
@@ -0,0 +1,15 @@
+// lib/jwt.ts
+export function jwtExp(token?: string | null): number | null {
+ if (!token) return null;
+ try {
+ const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString("utf8"));
+ return typeof payload?.exp === "number" ? payload.exp : null;
+ } catch {
+ return null;
+ }
+}
+
+export function isJwtValid(token?: string | null): boolean {
+ const exp = jwtExp(token);
+ return !!exp && exp * 1000 > Date.now();
+}
diff --git a/middleware.ts b/middleware.ts
index ac77008f..a535742e 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -83,6 +83,11 @@ import { NextResponse, NextRequest } from "next/server";
const url = req.nextUrl.clone();
const { pathname } = url;
+ // ── 0) Absolute rule: the homepage must never redirect (no mapping, no gating).
+ if (pathname === "/") {
+ return NextResponse.next();
+ }
+
// ── 1) Legacy → Portal / Canonical mapping (runs before auth gating)
const mapped = legacyMap(pathname);
if (mapped && !isSameUrl(req, mapped)) {
@@ -174,8 +179,8 @@ import { NextResponse, NextRequest } from "next/server";
type MapResult = { pathname: string; query?: Record
};
function legacyMap(pathname: string): MapResult | null {
- // If we’re already inside the portal, don’t try to remap again.
- if (pathname.startsWith("/portal")) return null;
+ // Never map the homepage, and if we’re already inside the portal, don’t remap again.
+ if (pathname === "/" || 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`