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:
Jon Meyers 2024-02-19 12:12:58 +11:00 committed by GitHub
parent 12411e9fe0
commit 5b48db23b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 174 additions and 92 deletions

View file

@ -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`);
}

View file

@ -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}

View 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>
);
}

View file

@ -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() {

View 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>
);
}

View file

@ -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{" "}

View file

@ -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>
);
}

View file

@ -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