sign in and register fixes for looped and forwarded pages
This commit is contained in:
parent
5257a0d2fe
commit
ffccff85d4
6 changed files with 68 additions and 20 deletions
|
|
@ -2,6 +2,7 @@
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import SignIn from "./sign-in";
|
import SignIn from "./sign-in";
|
||||||
|
import { isJwtValid } from "@/lib/jwt";
|
||||||
|
|
||||||
export default async function SignInPage({
|
export default async function SignInPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
|
|
@ -22,7 +23,7 @@ export default async function SignInPage({
|
||||||
if (!reauth) {
|
if (!reauth) {
|
||||||
const ck = await cookies();
|
const ck = await cookies();
|
||||||
const at = ck.get("ma_at")?.value;
|
const at = ck.get("ma_at")?.value;
|
||||||
if (at) redirect("/portal");
|
if (isJwtValid(at)) redirect("/portal");
|
||||||
}
|
}
|
||||||
|
|
||||||
return <SignIn nextPath={nextPath} reauth={reauth} />;
|
return <SignIn nextPath={nextPath} reauth={reauth} />;
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,16 @@ export default function SignIn({ nextPath = "/portal", reauth = false }: Props)
|
||||||
setErr(null);
|
setErr(null);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
const body = {
|
||||||
|
identifier: identifier.trim(),
|
||||||
|
password,
|
||||||
|
};
|
||||||
|
|
||||||
const res = await fetch("/api/auth/login", {
|
const res = await fetch("/api/auth/login", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||||
body: JSON.stringify({ identifier, password }),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
|
|
||||||
const txt = await res.text();
|
const txt = await res.text();
|
||||||
|
|
@ -34,7 +39,10 @@ export default function SignIn({ nextPath = "/portal", reauth = false }: Props)
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
if (!res.ok) {
|
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.
|
// 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]
|
[identifier, password, nextPath, router, reauth]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const createHref = `/auth/sign-up?next=${encodeURIComponent(nextPath)}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-md rounded-lg border p-6 space-y-4">
|
<div className="mx-auto max-w-md rounded-lg border p-6 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -116,14 +126,13 @@ export default function SignIn({ nextPath = "/portal", reauth = false }: Props)
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{!reauth && (
|
{/* Always show a sign-up path, even on reauth, to avoid dead-ends for first-time visitors */}
|
||||||
<div className="mt-2 text-center text-sm">
|
<div className="mt-2 text-center text-sm">
|
||||||
<span className="opacity-70">New here?</span>{" "}
|
<span className="opacity-70">{reauth ? "Don't have an account?" : "New here?"}</span>{" "}
|
||||||
<a className="underline" href="/auth/sign-up">
|
<a className="underline" href={createHref}>
|
||||||
Create an account
|
Create an account
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import SignUp from "./sign-up";
|
import SignUp from "./sign-up";
|
||||||
|
import { isJwtValid } from "@/lib/jwt";
|
||||||
|
|
||||||
export default async function SignUpPage({
|
export default async function SignUpPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
|
|
@ -10,7 +11,7 @@ export default async function SignUpPage({
|
||||||
}) {
|
}) {
|
||||||
const ck = await cookies();
|
const ck = await cookies();
|
||||||
const at = ck.get("ma_at")?.value;
|
const at = ck.get("ma_at")?.value;
|
||||||
if (at) redirect("/portal");
|
if (isJwtValid(at)) redirect("/portal");
|
||||||
|
|
||||||
const sp = searchParams ?? {};
|
const sp = searchParams ?? {};
|
||||||
const nextParam = Array.isArray(sp.next) ? sp.next[0] : sp.next;
|
const nextParam = Array.isArray(sp.next) ? sp.next[0] : sp.next;
|
||||||
|
|
|
||||||
29
app/page.tsx
29
app/page.tsx
|
|
@ -3,19 +3,35 @@ import { cookies } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import SignIn from "@/app/auth/sign-in/sign-in";
|
import SignIn from "@/app/auth/sign-in/sign-in";
|
||||||
import SignUp from "@/app/auth/sign-up/sign-up";
|
import SignUp from "@/app/auth/sign-up/sign-up";
|
||||||
|
import { isJwtValid } from "@/lib/jwt";
|
||||||
|
|
||||||
export default async function HomePage() {
|
type SearchParams = { [key: string]: string | string[] | undefined };
|
||||||
// If already signed in, go straight to the app
|
|
||||||
|
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 ck = await cookies();
|
||||||
const at = ck.get("ma_at")?.value;
|
const at = ck.get("ma_at")?.value;
|
||||||
if (at) redirect("/portal");
|
if (isJwtValid(at)) redirect("/portal");
|
||||||
|
|
||||||
|
const reauth = searchParams?.reauth === "1";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto max-w-5xl px-4 py-12">
|
<main className="mx-auto max-w-5xl px-4 py-12">
|
||||||
|
{reauth && (
|
||||||
|
<p className="mb-6 rounded-md border bg-yellow-50 p-3 text-sm text-yellow-900">
|
||||||
|
Your session expired. Please sign in again.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<section className="mb-10 text-center">
|
<section className="mb-10 text-center">
|
||||||
<h1 className="text-3xl font-bold tracking-tight">MakeArmy</h1>
|
<h1 className="text-3xl font-bold tracking-tight">MakeArmy</h1>
|
||||||
<p className="mt-2 text-base text-muted-foreground">
|
<p className="mt-2 text-base text-muted-foreground">
|
||||||
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.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -29,12 +45,13 @@ export default async function HomePage() {
|
||||||
<div className="rounded-lg border p-6">
|
<div className="rounded-lg border p-6">
|
||||||
<h2 className="mb-3 text-lg font-semibold">Sign in</h2>
|
<h2 className="mb-3 text-lg font-semibold">Sign in</h2>
|
||||||
{/* Uses your existing sign-in component */}
|
{/* Uses your existing sign-in component */}
|
||||||
<SignIn nextPath="/portal" reauth={false} />
|
<SignIn nextPath="/portal" reauth={reauth} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mt-8 text-center text-xs text-muted-foreground">
|
<section className="mt-8 text-center text-xs text-muted-foreground">
|
||||||
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).
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
15
lib/jwt.ts
Normal file
15
lib/jwt.ts
Normal file
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -83,6 +83,11 @@ import { NextResponse, NextRequest } from "next/server";
|
||||||
const url = req.nextUrl.clone();
|
const url = req.nextUrl.clone();
|
||||||
const { pathname } = url;
|
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)
|
// ── 1) Legacy → Portal / Canonical mapping (runs before auth gating)
|
||||||
const mapped = legacyMap(pathname);
|
const mapped = legacyMap(pathname);
|
||||||
if (mapped && !isSameUrl(req, mapped)) {
|
if (mapped && !isSameUrl(req, mapped)) {
|
||||||
|
|
@ -174,8 +179,8 @@ import { NextResponse, NextRequest } from "next/server";
|
||||||
type MapResult = { pathname: string; query?: Record<string, string> };
|
type MapResult = { pathname: string; query?: Record<string, string> };
|
||||||
|
|
||||||
function legacyMap(pathname: string): MapResult | null {
|
function legacyMap(pathname: string): MapResult | null {
|
||||||
// If we’re already inside the portal, don’t try to remap again.
|
// Never map the homepage, and if we’re already inside the portal, don’t remap again.
|
||||||
if (pathname.startsWith("/portal")) return null;
|
if (pathname === "/" || pathname.startsWith("/portal")) return null;
|
||||||
|
|
||||||
// 1) DETAIL PAGES: map legacy detail URLs straight into the portal with ?id=
|
// 1) DETAIL PAGES: map legacy detail URLs straight into the portal with ?id=
|
||||||
// NOTE: We intentionally DO NOT remap `/lasers/:id` and `/projects/:id`
|
// NOTE: We intentionally DO NOT remap `/lasers/:id` and `/projects/:id`
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue