Add protected page and pending state to with-supabase example (#62211)
### What? [1] Add protected page [2] Add pending state ### Why? [1] People using incorrect ways to protect pages [2] No feedback that the login page is doing anything when buttons clicked ### How? [1] Redirect user to protected page after successful authentication [2] Use useFormStatus to determine whether the form is in a pending state --------- Co-authored-by: Sam Ko <sam@vercel.com>
This commit is contained in:
parent
12411e9fe0
commit
5b48db23b1
11 changed files with 174 additions and 92 deletions
|
@ -7,12 +7,13 @@ export async function GET(request: Request) {
|
|||
// https://supabase.com/docs/guides/auth/server-side/nextjs
|
||||
const requestUrl = new URL(request.url);
|
||||
const code = requestUrl.searchParams.get("code");
|
||||
const origin = requestUrl.origin;
|
||||
|
||||
if (code) {
|
||||
const supabase = createClient();
|
||||
await supabase.auth.exchangeCodeForSession(code);
|
||||
}
|
||||
|
||||
// URL to redirect to after sign in process completes
|
||||
return NextResponse.redirect(requestUrl.origin);
|
||||
// URL to redirect to after sign up process completes
|
||||
return NextResponse.redirect(`${origin}/protected`);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import Link from "next/link";
|
|||
import { headers } from "next/headers";
|
||||
import { createClient } from "@/utils/supabase/server";
|
||||
import { redirect } from "next/navigation";
|
||||
import { SubmitButton } from "./submit-button";
|
||||
|
||||
export default function Login({
|
||||
searchParams,
|
||||
|
@ -24,7 +25,7 @@ export default function Login({
|
|||
return redirect("/login?message=Could not authenticate user");
|
||||
}
|
||||
|
||||
return redirect("/");
|
||||
return redirect("/protected");
|
||||
};
|
||||
|
||||
const signUp = async (formData: FormData) => {
|
||||
|
@ -73,10 +74,7 @@ export default function Login({
|
|||
Back
|
||||
</Link>
|
||||
|
||||
<form
|
||||
className="animate-in flex-1 flex flex-col w-full justify-center gap-2 text-foreground"
|
||||
action={signIn}
|
||||
>
|
||||
<form className="animate-in flex-1 flex flex-col w-full justify-center gap-2 text-foreground">
|
||||
<label className="text-md" htmlFor="email">
|
||||
Email
|
||||
</label>
|
||||
|
@ -96,15 +94,20 @@ export default function Login({
|
|||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
<button className="bg-green-700 rounded-md px-4 py-2 text-foreground mb-2">
|
||||
<SubmitButton
|
||||
formAction={signIn}
|
||||
className="bg-green-700 rounded-md px-4 py-2 text-foreground mb-2"
|
||||
pendingText="Signing In..."
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
<button
|
||||
</SubmitButton>
|
||||
<SubmitButton
|
||||
formAction={signUp}
|
||||
className="border border-foreground/20 rounded-md px-4 py-2 text-foreground mb-2"
|
||||
pendingText="Signing Up..."
|
||||
>
|
||||
Sign Up
|
||||
</button>
|
||||
</SubmitButton>
|
||||
{searchParams?.message && (
|
||||
<p className="mt-4 p-4 bg-foreground/10 text-foreground text-center">
|
||||
{searchParams.message}
|
||||
|
|
20
examples/with-supabase/app/login/submit-button.tsx
Normal file
20
examples/with-supabase/app/login/submit-button.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
"use client";
|
||||
|
||||
import { useFormStatus } from "react-dom";
|
||||
import { type ComponentProps } from "react";
|
||||
|
||||
type Props = ComponentProps<"button"> & {
|
||||
pendingText?: string;
|
||||
};
|
||||
|
||||
export function SubmitButton({ children, pendingText, ...props }: Props) {
|
||||
const { pending, action } = useFormStatus();
|
||||
|
||||
const isPending = pending && action === props.formAction;
|
||||
|
||||
return (
|
||||
<button {...props} type="submit" aria-disabled={pending}>
|
||||
{isPending ? pendingText : children}
|
||||
</button>
|
||||
);
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import DeployButton from "../components/DeployButton";
|
||||
import AuthButton from "../components/AuthButton";
|
||||
import { createClient } from "@/utils/supabase/server";
|
||||
import ConnectSupabaseSteps from "@/components/ConnectSupabaseSteps";
|
||||
import SignUpUserSteps from "@/components/SignUpUserSteps";
|
||||
import ConnectSupabaseSteps from "@/components/tutorial/ConnectSupabaseSteps";
|
||||
import SignUpUserSteps from "@/components/tutorial/SignUpUserSteps";
|
||||
import Header from "@/components/Header";
|
||||
|
||||
export default async function Index() {
|
||||
|
|
57
examples/with-supabase/app/protected/page.tsx
Normal file
57
examples/with-supabase/app/protected/page.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import DeployButton from "@/components/DeployButton";
|
||||
import AuthButton from "@/components/AuthButton";
|
||||
import { createClient } from "@/utils/supabase/server";
|
||||
import FetchDataSteps from "@/components/tutorial/FetchDataSteps";
|
||||
import Header from "@/components/Header";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function ProtectedPage() {
|
||||
const supabase = createClient();
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (!user) {
|
||||
return redirect("/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 w-full flex flex-col gap-20 items-center">
|
||||
<div className="w-full">
|
||||
<div className="py-6 font-bold bg-purple-950 text-center">
|
||||
This is a protected page that you can only see as an authenticated
|
||||
user
|
||||
</div>
|
||||
<nav className="w-full flex justify-center border-b border-b-foreground/10 h-16">
|
||||
<div className="w-full max-w-4xl flex justify-between items-center p-3 text-sm">
|
||||
<DeployButton />
|
||||
<AuthButton />
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="animate-in flex-1 flex flex-col gap-20 opacity-0 max-w-4xl px-3">
|
||||
<Header />
|
||||
<main className="flex-1 flex flex-col gap-6">
|
||||
<h2 className="font-bold text-4xl mb-4">Next steps</h2>
|
||||
<FetchDataSteps />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer className="w-full border-t border-t-foreground/10 p-8 flex justify-center text-center text-xs">
|
||||
<p>
|
||||
Powered by{" "}
|
||||
<a
|
||||
href="https://supabase.com/?utm_source=create-next-app&utm_medium=template&utm_term=nextjs"
|
||||
target="_blank"
|
||||
className="font-bold hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Supabase
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
import Link from "next/link";
|
||||
import Step from "./Step";
|
||||
import Code from "@/components/Code";
|
||||
import Code from "./Code";
|
||||
|
||||
const create = `
|
||||
create table notes (
|
||||
id serial primary key,
|
||||
id bigserial primary key,
|
||||
title text
|
||||
);
|
||||
|
||||
|
@ -48,23 +47,9 @@ export default function Page() {
|
|||
}
|
||||
`.trim();
|
||||
|
||||
export default function SignUpUserSteps() {
|
||||
export default function FetchDataSteps() {
|
||||
return (
|
||||
<ol className="flex flex-col gap-6">
|
||||
<Step title="Sign up your first user">
|
||||
<p>
|
||||
Head over to the{" "}
|
||||
<Link
|
||||
href="/login"
|
||||
className="font-bold hover:underline text-foreground/80"
|
||||
>
|
||||
Login
|
||||
</Link>{" "}
|
||||
page and sign up your first user. It's okay if this is just you for
|
||||
now. Your awesome idea will have plenty of users later!
|
||||
</p>
|
||||
</Step>
|
||||
|
||||
<Step title="Create some tables and insert some data">
|
||||
<p>
|
||||
Head over to the{" "}
|
|
@ -0,0 +1,22 @@
|
|||
import Link from "next/link";
|
||||
import Step from "./Step";
|
||||
|
||||
export default function SignUpUserSteps() {
|
||||
return (
|
||||
<ol className="flex flex-col gap-6">
|
||||
<Step title="Sign up your first user">
|
||||
<p>
|
||||
Head over to the{" "}
|
||||
<Link
|
||||
href="/login"
|
||||
className="font-bold hover:underline text-foreground/80"
|
||||
>
|
||||
Login
|
||||
</Link>{" "}
|
||||
page and sign up your first user. It's okay if this is just you for
|
||||
now. Your awesome idea will have plenty of users later!
|
||||
</p>
|
||||
</Step>
|
||||
</ol>
|
||||
);
|
||||
}
|
|
@ -1,70 +1,64 @@
|
|||
import { createServerClient, type CookieOptions } from "@supabase/ssr";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export const createClient = (request: NextRequest) => {
|
||||
// Create an unmodified response
|
||||
let response = NextResponse.next({
|
||||
request: {
|
||||
headers: request.headers,
|
||||
},
|
||||
});
|
||||
export const updateSession = async (request: NextRequest) => {
|
||||
// This `try/catch` block is only here for the interactive tutorial.
|
||||
// Feel free to remove once you have Supabase connected.
|
||||
try {
|
||||
// Create an unmodified response
|
||||
let response = NextResponse.next({
|
||||
request: {
|
||||
headers: request.headers,
|
||||
},
|
||||
});
|
||||
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
get(name: string) {
|
||||
return request.cookies.get(name)?.value;
|
||||
},
|
||||
set(name: string, value: string, options: CookieOptions) {
|
||||
// If the cookie is updated, update the cookies for the request and response
|
||||
request.cookies.set({
|
||||
name,
|
||||
value,
|
||||
...options,
|
||||
});
|
||||
response = NextResponse.next({
|
||||
request: {
|
||||
headers: request.headers,
|
||||
},
|
||||
});
|
||||
response.cookies.set({
|
||||
name,
|
||||
value,
|
||||
...options,
|
||||
});
|
||||
},
|
||||
remove(name: string, options: CookieOptions) {
|
||||
// If the cookie is removed, update the cookies for the request and response
|
||||
request.cookies.set({
|
||||
name,
|
||||
value: "",
|
||||
...options,
|
||||
});
|
||||
response = NextResponse.next({
|
||||
request: {
|
||||
headers: request.headers,
|
||||
},
|
||||
});
|
||||
response.cookies.set({
|
||||
name,
|
||||
value: "",
|
||||
...options,
|
||||
});
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
get(name: string) {
|
||||
return request.cookies.get(name)?.value;
|
||||
},
|
||||
set(name: string, value: string, options: CookieOptions) {
|
||||
// If the cookie is updated, update the cookies for the request and response
|
||||
request.cookies.set({
|
||||
name,
|
||||
value,
|
||||
...options,
|
||||
});
|
||||
response = NextResponse.next({
|
||||
request: {
|
||||
headers: request.headers,
|
||||
},
|
||||
});
|
||||
response.cookies.set({
|
||||
name,
|
||||
value,
|
||||
...options,
|
||||
});
|
||||
},
|
||||
remove(name: string, options: CookieOptions) {
|
||||
// If the cookie is removed, update the cookies for the request and response
|
||||
request.cookies.set({
|
||||
name,
|
||||
value: "",
|
||||
...options,
|
||||
});
|
||||
response = NextResponse.next({
|
||||
request: {
|
||||
headers: request.headers,
|
||||
},
|
||||
});
|
||||
response.cookies.set({
|
||||
name,
|
||||
value: "",
|
||||
...options,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return { supabase, response };
|
||||
};
|
||||
|
||||
export const updateSession = async (request: NextRequest) => {
|
||||
try {
|
||||
// This `try/catch` block is only here for the interactive tutorial.
|
||||
// Feel free to remove once you have Supabase connected.
|
||||
const { supabase, response } = createClient(request);
|
||||
);
|
||||
|
||||
// This will refresh session if expired - required for Server Components
|
||||
// https://supabase.com/docs/guides/auth/server-side/nextjs
|
||||
|
|
Loading…
Reference in a new issue