Ilence Ye

Supabase Auth

因为最近在做 Olinkics,用到了 Supabase,所以这段时间都在研究它。今天这篇文章主要分享下 Supabase Auth 这一块。

一、为什么选择 OAuth

一开始我计划的是让用户通过邮箱来注册账号,后面发现这种方式比较麻烦。

一是,你不仅仅是提供注册和登录功能就好了,你还需要提供重置密码的功能,这样用户在他们忘记密码时才能重置密码。这意味着我需要再画一些 UI,虽然也不难,但毫无疑问增加了我的工作量,而我很懒。

二是,重置密码一般来说需要由你发送一封重置密码的邮件到用户的邮箱,这就涉及到邮件发送服务,虽然 Supabase 有提供这个服务,但是发送邮件是有次数限制的,并且他也推荐你自己搞一个 STMP server,这对我来说就显得有点麻烦了。

所以最后我决定放弃邮箱/密码这种组合方式,改成用 OAuth,将 Auth 这块的功能“外包”给第三方。而这个方式我看到最常用的就是 Google 和 Apple,后者由于我没有苹果账号肯定是搞不了,所以最后选择了前者,当然这意味着到时用户必须翻墙才能使用。

二、谷歌登录

具体怎么做呢?Supabase 的文档已经给出了非常详细的说明。

首先跟着 文档里的这部分 先后在 Google 和 Supabase 这两个地方完成配置。

然后回到代码端,跟着 文档里的这部分 完成一个 PKCE flow 的 Server-Side Auth:

src\app\(auth)\login\page.tsx

tsx
import { signin } from "./actions";

export default function LoginPage() {
  return (
    <form>
      <button formAction={signin}>Log in</button>
    </form>
  );
}

src\app\(auth)\login\actions.ts

ts
"use server";

import { createClient } from "@/utils/supabase/server";

export async function signin() {
  const supabase = await createClient();

  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: "google",
    options: {
      redirectTo: "http://localhost:3000/callback", // 🔴 根据自己的实际项目修改
    },
  });

  if (data.url) {
    redirect(data.url); // use the redirect API for your server framework
  }
}

ts
"use server";

import { getURL } from "@/utils/helpers";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";

export async function signin() {
  const supabase = createClient();

  const redirectURL = getURL("/callback");

  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: "google",
    options: {
      redirectTo: redirectURL,
    },
  });

  if (error) {
    redirect(`/login?message=${error.message}`);
  }

  redirect(data.url);
}

src\app\(auth)\callback\route.ts

ts
import { NextResponse } from "next/server";
// The client you created from the Server-Side Auth instructions
import { createClient } from "@/utils/supabase/server";

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url);
  const code = searchParams.get("code");
  // if "next" is in param, use it as the redirect URL
  const next = searchParams.get("next") ?? "/mine"; // 🔴 根据自己的实际项目修改

  if (code) {
    const supabase = createClient();
    const { error } = await supabase.auth.exchangeCodeForSession(code);
    if (!error) {
      const forwardedHost = request.headers.get("x-forwarded-host"); // original origin before load balancer
      const isLocalEnv = process.env.NODE_ENV === "development";
      if (isLocalEnv) {
        // we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
        return NextResponse.redirect(`${origin}${next}`);
      } else if (forwardedHost) {
        return NextResponse.redirect(`https://${forwardedHost}${next}`);
      } else {
        return NextResponse.redirect(`${origin}${next}`);
      }
    }
  }

  // return the user to an error page with instructions
  return NextResponse.redirect(`${origin}/auth/auth-code-error`);
}

三、登出

最后是登出,这个跟着文档走也可以搞定。

src\app\(private)\mine\page.tsx

tsx
import { signOut } from "./actions";

export default async function page() {
  return (
		<form>
			<button formAction={signOut}>sign out</button>
		</form>
  );
}

src\app\(private)\mine\actions.ts

ts
"use server";

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

export async function signOut() {
  const supabase = createClient();
  await supabase.auth.signOut({ scope: "local" });
  redirect("/login");
}

四、路由权限

For public routes:

src\app\page.tsx

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

export default async function page() {
  const supabase = createClient();

  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (user) {
    redirect("/mine");
  }

  return <>...</>;
}

For private routes:

src\app\(private)\mine\page.tsx

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

export default async function page() {
  const supabase = createClient();

  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (!user) {
    redirect("/login");
  }

  return (
		<div>{user.email}</div>
  );
}

Bonus、helper function for constructing redirect url

参考 这里 的用于 generate redirect url 的 get site base url helper function:

ts
export const getURL = (path: string = '') => {
    // Check if NEXT_PUBLIC_SITE_URL is set and non-empty. Set this to your site URL in production env.
    let url =
        process?.env?.NEXT_PUBLIC_SITE_URL &&
            process.env.NEXT_PUBLIC_SITE_URL.trim() !== ''
            ? process.env.NEXT_PUBLIC_SITE_URL
            : // If not set, check for NEXT_PUBLIC_VERCEL_URL, which is automatically set by Vercel.
            process?.env?.NEXT_PUBLIC_VERCEL_URL &&
                process.env.NEXT_PUBLIC_VERCEL_URL.trim() !== ''
                ? process.env.NEXT_PUBLIC_VERCEL_URL
                : // If neither is set, default to localhost for local development.
                'http://localhost:3000/';

    // Trim the URL and remove trailing slash if exists.
    url = url.replace(/\/+$/, '');
    // Make sure to include `https://` when not localhost.
    url = url.includes('http') ? url : `https://${url}`;
    // Ensure path starts without a slash to avoid double slashes in the final URL.
    path = path.replace(/^\/+/, '');

    // Concatenate the URL and the path.
    return path ? `${url}/${path}` : url;
};