使用 Vercel、Supabase、Stripe 打造 OpenAI 计费系统:2. 处理用户注册/登录

嗯,是的,我回来填坑了。既然要打广告,就要对得起各位金主,总是水文是不行的,还是要输出干货。

广告继续。本博客即日起开始常年招商,欢迎各位想推广产品的老板投广告,目前定价 4800/年,亦可增加评测文章、教学文章配合,详情可与我联系。具体详情更新在 本站广告 2023 年招商


Supabase 用户体系入门

Supabase 提供 Serverless 数据库,自带用户体系,开发用户系统非常简单。而且 SDK 很完善,适配各种框架,大部分功能开箱即用;官方教程方面做得也很好。所以,首先请大家先认真阅读这两篇官方文档、教程:

第二篇教程文章详细的介绍了使用 Supabae 开发用户管理系统的过程,配合上面的文档,相信大家都可以轻松入门。唯一的难点(对部分同学来说)可能是英文,我就先不复述。

接下来的文章主要介绍官方没有细说的最佳实践和各种实用细节。

邮件登录与邮件模版

虽然官方提供了很多方案,但是我想,对于大部分同学来说,邮件+密码登录才是最常用的,最多再加上第三方比如 Gmail 登录。

Supabase 提供了发邮件的功能,方便用户校验邮箱、找回密码。但是,正如很多邮件服务器那样,它的到达率并不理想。我猜测有些用户即使没有滥用 Supabase 的邮件服务,也会因为邮件模版过于简单而被误杀。所以大家第一步应该先配置邮件模版,尽量多体现自己的产品相关信息,尽量跟大家的通用模版区分开,减少误报误杀的可能。

Supabase 默认要求用户校验邮箱之后才能登录,如果你暂时不关心这一点,可以考虑在 Auth ProviderS > Email关掉这个功能。此后,用户完成注册后就可以登录,不过他们的用户状态在数据里仍然是未验证,我们也可以借此对用户进行一些差异化的控制。

注册与登录,自动注册

有一点我不太喜欢。Supabase 提供两个方法:signUpsignInWithPassword,分别用来注册和登录。但是通常情况下,我并不希望区分这两个方法。我认为我们的网站对用户来说,只是一个小工具,我觉得他们不会在意是否在这里注册过,他们只希望做他们要做的事情,注册登录只是我们为了方便自己而做的步骤。

所以我一般仍然会通过不同的路径区分用户是想注册,还是想登录。但是我会在用户 signUp 后检查错误信息,如果是重复注册,就自动帮他们登录。

const { data: { user }, error } = await supabase.auth[method]({
  email,
  password,
  options,
});
if (error) {
  // registered, login
  if (error.message === 'User already registered') {
    return login(email, password);
  }
throw error;
}

Nuxt3+Pinia 实现用户身份校验及额度查询

教程中的例子比较简单,通常来说我们的产品会复杂很多,比如,我们可能需要全局状态管理工具,方便不同页面不同组件中使用。在 Nuxt3 中使用 Pinia 需要用到特有的 Module:@pinia/nuxt,简单安装官方教程配置一下即可。这里主要分享我摸索的其它未提及要点。

额度查询

Supabase 在创建项目的时候已经帮我们建好了用户表,并且放在默认不对外的库里面。所以我们最好不要直接修改表,而是如教程所讲,创建一个新的表,把用户账户相关的其它数据放进去,比如在这样一款典型的 ChatGPT 产品中,就是帐户额度,然后通过关联 id 的方式访问。

这就带来一个问题:什么时候去读这个表?

对 SPA 来说,这个问题比较简单,登录之后,以及网页初始化的时候读取即可,反正所有请求都由我们全权控制。但是对于 Nuxt3 来说就值得讨论一下。因为 Nuxt3 要进行服务器端渲染,所以我们通常希望它在渲染页面的时候,这个数据已经就位了。

这里我推荐配合 Pinia Store,使用 supabase.auth.onAuthStateChange 函数。我会建立一个 userStore,在里面完成用户注册、登录、登出等动作,同时注册 onAuthStateChange 事件,完成用户状态变更后的处理。

const useUserStore = defineStore('user', () => {
  const supabase = useSupabaseClient();
  const user = useSupabaseUser();

  async function login(email: string, password: string, isSignUp = false): Promise<void> {}
  async function loginWithGoogle(): Promise<void> {}
  async function logout(): Promise<void> {}
  
  // 注册事件处理函数
  supabase.auth.onAuthStateChange((event, session) => {
    switch (event) {
      case 'INITIAL_SESSION':
      case 'SIGNED_IN':
      // 在这里处理登录后和初始化时拿到用户身份后的操作,比如加载用户账户的额度
      // 这里需要注意:此时 `user.value` 尚未被填充,所以我们不能依赖它。有两个方案:1. 延迟执行 `setTimeout`;2. 直接从 `session` 参数里拿到用户信息然后处理
      // 我的做法是,如果需要请求 API,那就选择方案2;否则,使用方案1,等 100ms
    }
  });
});
export useUserStore;

请仔细看上方代码的注释,非常重要,看起来很简单,但其实是血泪铺就的道路。

判断第三方登录,及 UI 呈现

当用户选择第三方登录,比如 Gmail 的时候,Supabase 目前只支持跳转这一种方式,不如 Auth0,可以用 popup,省很多事。

完整的第三方登录流程是:

  1. 跳转到第三方登录页面
  2. 登录
  3. 跳转到 Supabase 中间页
  4. 带着 access_token 跳回我们的网站,Supabase SDK 负责读取 access_token,验证用户身份等工作

其中涉及到两个(或更多)网站的配置,因为文档中写的有,我就不再详述,需要的同学留言我再补充。

最麻烦的是(4),跳转回来之后,从页面初始化成功到完成登录,中间可能会隔很久,短则1、2秒,长则10s 都有可能。这期间用户可以继续操作,会产生各种误会和误操作。这里我建议:

  1. 登录时传递 redirectTo,要求最终跳转页面是某个登录落地页
  2. 初始化的时候,检查 URL 里的 #access_token=
  3. 如果有,说明是第三方登录返回,然后就显示登录中的状态
  4. 等待 user.value 变化,说明完成登录,跳转到原先应该去的页面,或者继续之前没做完的操作
  5. 如果没有 access_token,直接跳转到其他落地页。

总结

上面这些内容看起来不复杂,但实际上对用户体验影响很大,并且文档、教程说得也不清楚,导致我的老板对产品一直不太满意。希望这篇文章可以节省大家的宝贵时间。

如果有关于 Supabase、Vercel、Nuxt3 的问题,欢迎评论里面留言。

系列文章

  1. 使用 Vercel、Supabase、Stripe 打造 OpenAI 计费系统:1. 系统篇
  2. 【本文】使用 Vercel、Supabase、Stripe 打造 OpenAI 计费系统:2. 处理用户注册/登录

建议阅读

如果您觉得文章内容对您有用,不妨支持我创作更多有价值的分享:


已发布

分类

来自

评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据