auth redirect updated for portal

This commit is contained in:
makearmy 2025-09-27 14:41:56 -04:00
parent 7b897e3672
commit bce0c5063b
5 changed files with 85 additions and 120 deletions

View file

@ -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<string, string | string[] | undefined>;
}) {
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 <SignIn nextPath={nextParam} />;
}
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 <SignIn nextPath="/portal" />;
}

View file

@ -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<string | null>(null);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [err, setErr] = useState<string | null>(null);
const next = sp.get("next") || nextPath;
async function onSubmit(e: React.FormEvent) {
const onSubmit = useCallback(async (e: React.FormEvent<HTMLFormElement>) => {
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 (
<main className="mx-auto max-w-md px-6 py-10">
<h1 className="mb-6 text-2xl font-semibold tracking-tight">Sign In</h1>
<form onSubmit={onSubmit} className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium">Email</label>
<div className="mx-auto max-w-md rounded-lg border p-6">
<h1 className="mb-1 text-2xl font-semibold">Sign In</h1>
<p className="mb-6 text-sm opacity-70">Welcome back! Enter your credentials to continue.</p>
<form className="space-y-4" onSubmit={onSubmit}>
<div className="space-y-1">
<label className="text-sm font-medium">Email</label>
<input
type="email"
autoComplete="email"
className="w-full rounded-md border px-3 py-2"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.currentTarget.value)}
autoComplete="email"
required
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium">Password</label>
<input
type="password"
className="w-full rounded-md border px-3 py-2"
value={password}
onChange={(e) => setPassword(e.currentTarget.value)}
autoComplete="current-password"
required
/>
</div>
{error && (
<div className="space-y-1">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Password</label>
<button
type="button"
className="text-xs opacity-70 hover:opacity-100"
onClick={() => setShowPassword((s) => !s)}
>
{showPassword ? "Hide" : "Show"}
</button>
</div>
<input
type={showPassword ? "text" : "password"}
autoComplete="current-password"
className="w-full rounded-md border px-3 py-2"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.currentTarget.value)}
required
/>
</div>
{err && (
<div className="rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700">
{error}
{err}
</div>
)}
<button
type="submit"
disabled={loading}
className="inline-flex w-full items-center justify-center rounded-md bg-black px-4 py-2 text-white disabled:opacity-60"
className="w-full rounded-md bg-black px-3 py-2 text-white disabled:opacity-60"
>
{loading ? "Signing in…" : "Sign In"}
</button>
</form>
<p className="mt-4 text-center text-sm opacity-70">
Dont have an account?{" "}
<a
className="underline"
href={`/auth/sign-up?next=${encodeURIComponent(next || "/portal")}`}
>
Sign up
<div className="mt-4 text-center text-sm">
<span className="opacity-70">New here?</span>{" "}
<a className="underline" href={"/auth/sign-up"}>
Create an account
</a>
</p>
</main>
</div>
</div>
);
}

View file

@ -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<string, string | string[] | undefined>;
}) {
export default async function SignUpPage() {
const at = (await cookies()).get("ma_at")?.value;
if (at) {
redirect("/portal");
}
const nextParam = toStr(searchParams?.next) || "/portal";
return <SignUp nextPath={nextParam} />;
}
function toStr(v: string | string[] | undefined): string | undefined {
if (!v) return undefined;
return Array.isArray(v) ? v[0] : v;
if (at) redirect("/portal");
return <SignUp nextPath="/portal" />;
}

View file

@ -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) {
<div className="mt-4 text-center text-sm">
<span className="opacity-70">Already have an account?</span>{" "}
<a className="underline" href={`/auth/sign-in?next=${encodeURIComponent(nextPath)}`}>
Sign in
</a>
<a className="underline" href={"/auth/sign-in"}>Sign in</a>
</div>
</div>
);

View file

@ -4,18 +4,15 @@ import { NextResponse, NextRequest } from "next/server";
const PUBLIC_PATHS = new Set<string>([
"/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=<original>
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).*)",
],