diff --git a/app/auth/sign-in/page.tsx b/app/auth/sign-in/page.tsx index fc337cac..fccc1ca5 100644 --- a/app/auth/sign-in/page.tsx +++ b/app/auth/sign-in/page.tsx @@ -3,21 +3,10 @@ import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import SignIn from "./sign-in"; -export default async function SignInPage({ - searchParams, -}: { - searchParams?: Record; -}) { +export default async function SignInPage() { const at = (await cookies()).get("ma_at")?.value; - if (at) { - redirect("/portal"); - } + if (at) redirect("/portal"); - const nextParam = toStr(searchParams?.next) || "/portal"; - return ; -} - -function toStr(v: string | string[] | undefined): string | undefined { - if (!v) return undefined; - return Array.isArray(v) ? v[0] : v; + // Always land on /portal after sign-in + return ; } diff --git a/app/auth/sign-in/sign-in.tsx b/app/auth/sign-in/sign-in.tsx index 34e0c91b..588bcd50 100644 --- a/app/auth/sign-in/sign-in.tsx +++ b/app/auth/sign-in/sign-in.tsx @@ -1,101 +1,112 @@ // app/auth/sign-in/sign-in.tsx "use client"; -import * as React from "react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useState, useCallback } from "react"; +import { useRouter } from "next/navigation"; type Props = { nextPath?: string }; export default function SignIn({ nextPath = "/portal" }: Props) { const router = useRouter(); - const sp = useSearchParams(); - const [email, setEmail] = React.useState(""); - const [password, setPassword] = React.useState(""); - const [loading, setLoading] = React.useState(false); - const [error, setError] = React.useState(null); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [loading, setLoading] = useState(false); + const [err, setErr] = useState(null); - const next = sp.get("next") || nextPath; - - async function onSubmit(e: React.FormEvent) { + const onSubmit = useCallback(async (e: React.FormEvent) => { e.preventDefault(); - setError(null); + setErr(null); setLoading(true); + try { const res = await fetch("/api/auth/login", { method: "POST", + credentials: "include", headers: { "Content-Type": "application/json", Accept: "application/json" }, - credentials: "include", // ensure cookie (ma_at) is set - body: JSON.stringify({ email, password }), + body: JSON.stringify({ email, password }), }); + const txt = await res.text(); + let j: any = null; + try { j = txt ? JSON.parse(txt) : null; } catch {} + if (!res.ok) { - let msg = res.statusText || "Sign-in failed"; - try { - const j = txt ? JSON.parse(txt) : null; - msg = j?.error || j?.message || msg; - } catch {} - throw new Error(msg); + const message = j?.error || j?.message || `Sign-in failed (${res.status})`; + throw new Error(message); } - // success → land on next (or /portal) - router.replace(next || "/portal"); - } catch (err: any) { - setError(err?.message || "Sign-in failed"); + + router.replace(nextPath); // ALWAYS /portal + router.refresh(); + } catch (e: any) { + setErr(e?.message || "Unable to sign in."); } finally { setLoading(false); } - } + }, [email, password, nextPath, router]); return ( -
-

Sign In

-
-
- +
+

Sign In

+

Welcome back! Enter your credentials to continue.

+ + +
+ setEmail(e.currentTarget.value)} - autoComplete="email" - required - /> -
-
- - setPassword(e.currentTarget.value)} - autoComplete="current-password" required />
- {error && ( +
+
+ + +
+ setPassword(e.currentTarget.value)} + required + /> +
+ + {err && (
- {error} + {err}
)} -

- Don’t have an account?{" "} - - Sign up +

+ + ); } diff --git a/app/auth/sign-up/page.tsx b/app/auth/sign-up/page.tsx index dd8c12a6..7c3b350e 100644 --- a/app/auth/sign-up/page.tsx +++ b/app/auth/sign-up/page.tsx @@ -3,21 +3,8 @@ import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import SignUp from "./sign-up"; -export default async function SignUpPage({ - searchParams, -}: { - searchParams?: Record; -}) { +export default async function SignUpPage() { const at = (await cookies()).get("ma_at")?.value; - if (at) { - redirect("/portal"); - } - - const nextParam = toStr(searchParams?.next) || "/portal"; - return ; -} - -function toStr(v: string | string[] | undefined): string | undefined { - if (!v) return undefined; - return Array.isArray(v) ? v[0] : v; + if (at) redirect("/portal"); + return ; } diff --git a/app/auth/sign-up/sign-up.tsx b/app/auth/sign-up/sign-up.tsx index e8f54590..f41246f9 100644 --- a/app/auth/sign-up/sign-up.tsx +++ b/app/auth/sign-up/sign-up.tsx @@ -4,15 +4,12 @@ import { useState, useCallback } from "react"; import { useRouter } from "next/navigation"; -type Props = { - nextPath?: string; // where to go after successful sign-up -}; +type Props = { nextPath?: string }; export default function SignUp({ nextPath = "/portal" }: Props) { const router = useRouter(); - const [username, setUsername] = useState(""); - const [email, setEmail] = useState(""); // optional per your backend flow + const [email, setEmail] = useState(""); // optional if your backend allows const [password, setPassword] = useState(""); const [showPassword, setShowPassword] = useState(false); const [loading, setLoading] = useState(false); @@ -28,11 +25,7 @@ export default function SignUp({ nextPath = "/portal" }: Props) { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", Accept: "application/json" }, - body: JSON.stringify({ - username, - email: email || undefined, - password, - }), + body: JSON.stringify({ username, email: email || undefined, password }), }); const txt = await res.text(); @@ -40,16 +33,11 @@ export default function SignUp({ nextPath = "/portal" }: Props) { try { j = txt ? JSON.parse(txt) : null; } catch {} if (!res.ok) { - const message = - j?.error || - j?.message || - (typeof j === "string" ? j : "") || - `Sign-up failed (${res.status})`; + const message = j?.error || j?.message || `Sign-up failed (${res.status})`; throw new Error(message); } - // Expect server to create user + set cookies (ma_at, etc.) - router.replace(nextPath || "/portal"); + router.replace(nextPath); // ALWAYS /portal router.refresh(); } catch (e: any) { setErr(e?.message || "Unable to sign up."); @@ -129,9 +117,7 @@ export default function SignUp({ nextPath = "/portal" }: Props) {
Already have an account?{" "} - - Sign in - + Sign in
); diff --git a/middleware.ts b/middleware.ts index b1d7e7cc..53cd0e3b 100644 --- a/middleware.ts +++ b/middleware.ts @@ -4,18 +4,15 @@ import { NextResponse, NextRequest } from "next/server"; const PUBLIC_PATHS = new Set([ "/auth/sign-in", "/auth/sign-up", + // add oauth/callback endpoints here if you use them, e.g.: "/auth/callback" ]); -// If you have additional public pages (e.g., marketing), add them here. -// Keep API endpoints out of this middleware unless you explicitly want to block them. - export function middleware(req: NextRequest) { - const { pathname, search } = req.nextUrl; - const isPublic = isPublicPath(pathname); + const { pathname } = req.nextUrl; const isAuthRoute = pathname.startsWith("/auth/"); const token = req.cookies.get("ma_at")?.value ?? ""; - // 1) If already authed and on an auth route, dump to /portal + // If already authed and hitting an auth route, always go to the portal if (token && isAuthRoute) { const url = req.nextUrl.clone(); url.pathname = "/portal"; @@ -23,26 +20,22 @@ export function middleware(req: NextRequest) { return NextResponse.redirect(url); } - // 2) If not authed and path is protected → send to sign-in with next= - if (!token && !isPublic) { + // If not authed and path is protected → send to sign-in (no ?next=) + if (!token && !isPublicPath(pathname)) { const url = req.nextUrl.clone(); url.pathname = "/auth/sign-in"; - // Default to /portal after login, but preserve deep-link if present - const next = pathname + (search || ""); - url.search = next ? `?next=${encodeURIComponent(next)}` : `?next=${encodeURIComponent("/portal")}`; + url.search = ""; // IMPORTANT: drop next so login always goes to /portal return NextResponse.redirect(url); } - // 3) Otherwise, allow through return NextResponse.next(); } // Helpers function isPublicPath(pathname: string): boolean { - // Public routes if (PUBLIC_PATHS.has(pathname)) return true; - // Static assets and framework internals + // Static assets / internals if ( pathname.startsWith("/_next/") || pathname.startsWith("/static/") || @@ -52,7 +45,7 @@ function isPublicPath(pathname: string): boolean { pathname === "/sitemap.xml" ) return true; - // API routes: by default we *do not* block /api/* in middleware (let routes handle auth) + // API routes aren't gated here; each route should enforce auth as needed if (pathname.startsWith("/api/")) return true; // Everything else is protected @@ -60,7 +53,6 @@ function isPublicPath(pathname: string): boolean { } export const config = { - // Run middleware for all paths except the most common static files (belt & suspenders) matcher: [ "/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|images|static).*)", ],