Skip to content

Commit

Permalink
examples: improve Supabase example (#69308)
Browse files Browse the repository at this point in the history
Co-authored-by: Terry Sutton <saltcod@gmail.com>
Co-authored-by: Lee Robinson <me@leerob.io>
  • Loading branch information
3 people committed Aug 28, 2024
1 parent c1a7463 commit 00f70f2
Show file tree
Hide file tree
Showing 50 changed files with 1,270 additions and 676 deletions.
1 change: 1 addition & 0 deletions examples/with-supabase/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ yarn-error.log*

# local env files
.env*.local
.env

# vercel
.vercel
Expand Down
3 changes: 3 additions & 0 deletions examples/with-supabase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
- It just works!
- supabase-ssr. A package to configure Supabase Auth to use cookies
- Styling with [Tailwind CSS](https://tailwindcss.com)
- Components with [shadcn/ui](https://ui.shadcn.com/)
- Optional deployment with [Supabase Vercel Integration and Vercel deploy](#deploy-your-own)
- Environment variables automatically assigned to Vercel project

Expand Down Expand Up @@ -80,6 +81,8 @@ If you wish to just develop locally and not deploy to Vercel, [follow the steps

The starter kit should now be running on [localhost:3000](http://localhost:3000/).

6. This template comes with the default shadcn/ui style initialized. If you instead want other ui.shadcn styles, delete `components.json` and [re-install shadcn/ui](https://ui.shadcn.com/docs/installation/next)

> Check out [the docs for Local Development](https://supabase.com/docs/guides/getting-started/local-development) to also run Supabase locally.
## Feedback and issues
Expand Down
38 changes: 38 additions & 0 deletions examples/with-supabase/app/(auth-pages)/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { forgotPasswordAction } from "@/app/actions";
import { FormMessage, Message } from "@/components/form-message";
import { SubmitButton } from "@/components/submit-button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import Link from "next/link";
import { SmtpMessage } from "../smtp-message";

export default function ForgotPassword({
searchParams,
}: {
searchParams: Message;
}) {
return (
<>
<form className="flex-1 flex flex-col w-full gap-2 text-foreground [&>input]:mb-6 min-w-64 max-w-64 mx-auto">
<div>
<h1 className="text-2xl font-medium">Reset Password</h1>
<p className="text-sm text-secondary-foreground">
Already have an account?{" "}
<Link className="text-primary underline" href="/sign-in">
Sign in
</Link>
</p>
</div>
<div className="flex flex-col gap-2 [&>input]:mb-3 mt-8">
<Label htmlFor="email">Email</Label>
<Input name="email" placeholder="you@example.com" required />
<SubmitButton formAction={forgotPasswordAction}>
Reset Password
</SubmitButton>
<FormMessage message={searchParams} />
</div>
</form>
<SmtpMessage />
</>
);
}
9 changes: 9 additions & 0 deletions examples/with-supabase/app/(auth-pages)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="max-w-7xl flex flex-col gap-12 items-start">{children}</div>
);
}
43 changes: 43 additions & 0 deletions examples/with-supabase/app/(auth-pages)/sign-in/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { signInAction } from "@/app/actions";
import { FormMessage, Message } from "@/components/form-message";
import { SubmitButton } from "@/components/submit-button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import Link from "next/link";

export default function Login({ searchParams }: { searchParams: Message }) {
return (
<form className="flex-1 flex flex-col min-w-64">
<h1 className="text-2xl font-medium">Sign in</h1>
<p className="text-sm text-foreground">
Don't have an account?{" "}
<Link className="text-foreground font-medium underline" href="/sign-up">
Sign up
</Link>
</p>
<div className="flex flex-col gap-2 [&>input]:mb-3 mt-8">
<Label htmlFor="email">Email</Label>
<Input name="email" placeholder="you@example.com" required />
<div className="flex justify-between items-center">
<Label htmlFor="password">Password</Label>
<Link
className="text-xs text-foreground underline"
href="/forgot-password"
>
Forgot Password?
</Link>
</div>
<Input
type="password"
name="password"
placeholder="Your password"
required
/>
<SubmitButton pendingText="Signing In..." formAction={signInAction}>
Sign in
</SubmitButton>
<FormMessage message={searchParams} />
</div>
</form>
);
}
48 changes: 48 additions & 0 deletions examples/with-supabase/app/(auth-pages)/sign-up/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { signUpAction } from "@/app/actions";
import { FormMessage, Message } from "@/components/form-message";
import { SubmitButton } from "@/components/submit-button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import Link from "next/link";
import { SmtpMessage } from "../smtp-message";

export default function Signup({ searchParams }: { searchParams: Message }) {
if ("message" in searchParams) {
return (
<div className="w-full flex-1 flex items-center h-screen sm:max-w-md justify-center gap-2 p-4">
<FormMessage message={searchParams} />
</div>
);
}

return (
<>
<form className="flex flex-col min-w-64 max-w-64 mx-auto">
<h1 className="text-2xl font-medium">Sign up</h1>
<p className="text-sm text text-foreground">
Already have an account?{" "}
<Link className="text-primary font-medium underline" href="/sign-in">
Sign in
</Link>
</p>
<div className="flex flex-col gap-2 [&>input]:mb-3 mt-8">
<Label htmlFor="email">Email</Label>
<Input name="email" placeholder="you@example.com" required />
<Label htmlFor="password">Password</Label>
<Input
type="password"
name="password"
placeholder="Your password"
minLength={6}
required
/>
<SubmitButton formAction={signUpAction} pendingText="Signing up...">
Sign up
</SubmitButton>
<FormMessage message={searchParams} />
</div>
</form>
<SmtpMessage />
</>
);
}
25 changes: 25 additions & 0 deletions examples/with-supabase/app/(auth-pages)/smtp-message.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ArrowUpRight, InfoIcon } from "lucide-react";
import Link from "next/link";

export function SmtpMessage() {
return (
<div className="bg-muted/50 px-5 py-3 border rounded-md flex gap-4">
<InfoIcon size={16} className="mt-0.5" />
<div className="flex flex-col gap-1">
<small className="text-sm text-secondary-foreground">
<strong> Note:</strong> Emails are limited to 4 per hour. Enable a
custom SMTP endpoint to increase the rate limit.
</small>
<div>
<Link
href="https://supabase.com/docs/guides/auth/auth-smtp"
target="_blank"
className="text-primary/50 hover:text-primary flex items-center text-sm gap-1"
>
Learn more <ArrowUpRight size={14} />
</Link>
</div>
</div>
</div>
);
}
130 changes: 130 additions & 0 deletions examples/with-supabase/app/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"use server";

import { encodedRedirect } from "@/utils/utils";
import { createClient } from "@/utils/supabase/server";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

export const signUpAction = async (formData: FormData) => {
const email = formData.get("email")?.toString();
const password = formData.get("password")?.toString();
const supabase = createClient();
const origin = headers().get("origin");

if (!email || !password) {
return { error: "Email and password are required" };
}

const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${origin}/auth/callback`,
},
});

if (error) {
console.error(error.code + " " + error.message);
return encodedRedirect("error", "/sign-up", error.message);
} else {
return encodedRedirect(
"success",
"/sign-up",
"Thanks for signing up! Please check your email for a verification link.",
);
}
};

export const signInAction = async (formData: FormData) => {
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const supabase = createClient();

const { error } = await supabase.auth.signInWithPassword({
email,
password,
});

if (error) {
return encodedRedirect("error", "/sign-in", error.message);
}

return redirect("/protected");
};

export const forgotPasswordAction = async (formData: FormData) => {
const email = formData.get("email")?.toString();
const supabase = createClient();
const origin = headers().get("origin");
const callbackUrl = formData.get("callbackUrl")?.toString();

if (!email) {
return encodedRedirect("error", "/forgot-password", "Email is required");
}

const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${origin}/auth/callback?redirect_to=/protected/reset-password`,
});

if (error) {
console.error(error.message);
return encodedRedirect(
"error",
"/forgot-password",
"Could not reset password",
);
}

if (callbackUrl) {
return redirect(callbackUrl);
}

return encodedRedirect(
"success",
"/forgot-password",
"Check your email for a link to reset your password.",
);
};

export const resetPasswordAction = async (formData: FormData) => {
const supabase = createClient();

const password = formData.get("password") as string;
const confirmPassword = formData.get("confirmPassword") as string;

if (!password || !confirmPassword) {
encodedRedirect(
"error",
"/protected/reset-password",
"Password and confirm password are required",
);
}

if (password !== confirmPassword) {
encodedRedirect(
"error",
"/protected/reset-password",
"Passwords do not match",
);
}

const { error } = await supabase.auth.updateUser({
password: password,
});

if (error) {
encodedRedirect(
"error",
"/protected/reset-password",
"Password update failed",
);
}

encodedRedirect("success", "/protected/reset-password", "Password updated");
};

export const signOutAction = async () => {
const supabase = createClient();
await supabase.auth.signOut();
return redirect("/sign-in");
};
Loading

0 comments on commit 00f70f2

Please sign in to comment.