♨️

[Next.js] とりあえずsupabase触ってみる

2022/05/03に公開

はじめに

supabase公式チュートリアルのQuickstart: Next.jsを試した際のメモになります

supabaseプロジェクトの作成

  • supabaseのコンソール画面から、 New project をクリックする
  • 下記のプロジェクトの情報を追加し、 Create new project をクリック
    • Organization: プロジェクトを作成するGithub Organizationを選択
    • Name: 作成するプロジェクト名
    • Database Password: データベースのパスワード
    • Region: データベースのリージョン(今回は東京リージョンを選択)
    • Pricing Plan: 料金プラン(今回は無料プランを選択)

データベースのセットアップ

  • サイドバーより SQL Editor を選択

  • Quick start セクションより、 User Management Starter をクリック

  • 画面右側の Run をクリックし、テーブルを作成する

    ※今回実行されるSQLは下記

-- Create a table for Public Profiles
create table profiles (
  id uuid references auth.users not null,
  updated_at timestamp with time zone,
  username text unique,
  avatar_url text,
  website text,

  primary key (id),
  unique(username),
  constraint username_length check (char_length(username) >= 3)
);

alter table profiles
  enable row level security;

create policy "Public profiles are viewable by everyone." on profiles
  for select using (true);

create policy "Users can insert their own profile." on profiles
  for insert with check (auth.uid() = id);

create policy "Users can update own profile." on profiles
  for update using (auth.uid() = id);

-- Set up Realtime!
begin;
  drop publication if exists supabase_realtime;
  create publication supabase_realtime;
commit;
alter publication supabase_realtime
  add table profiles;

-- Set up Storage!
insert into storage.buckets (id, name)
  values ('avatars', 'avatars');

create policy "Avatar images are publicly accessible." on storage.objects
  for select using (bucket_id = 'avatars');

create policy "Anyone can upload an avatar." on storage.objects
  for insert with check (bucket_id = 'avatars');

create policy "Anyone can update an avatar." on storage.objects
  for update with check (bucket_id = 'avatars');

APIキーの取得

supabaseの auto-generated API を使ってデータを挿入するためのURLとAPIキーを取得する

  • サイドバーのSettingsを選択し、APIセクションの Configuraton より URL をコピーする
  • 同セクションの Project API keys より anonservice_role をコピーする

パッケージのインストール

  • Next.jsのプロジェクトに @supabase/supabase-js を追加する
npm install @supabase/supabase-js

環境変数の作成

  • Next.jsプロジェクト直下に .env.local ファイルを作成し、APIキーの取得 で取得したURLとAPIキーを記載する
.env.local
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY

Supabaseクライアントの初期化

Supabaseクライアントを初期化する utils/supabaseClient.js を作成する。
Next.jsの機能の process.env.キー名 を使って環境変数を読み込み、
読み込んだ環境変数を createClient() の引数に渡して初期化する

utils/supabaseClient.js
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY

export const supabase = createClient(supabaseUrl, supabaseAnonKey)

コンポーネントの作成

Magic Link を使ってログインを行うコンポーネントを作成する
Magic Linkとは、パスワードを使わずにログインする方法
詳細は下記を参照

マジックリンク(Magic Link)とは、パスワードレスログインの一形態です。ユーザーがサインインのためにログイン資格情報を入力する代わりに、メールで(場合によってはSMS経由で)埋め込まれたトークンを含むURLが送信されます。

参照元

  • components/Auth.jsを作成する
components/Auth.js
import { useState } from 'react'
import { supabase } from '../utils/supabaseClient'

export default function Auth() {
  const [loading, setLoading] = useState(false)
  const [email, setEmail] = useState('')

  const handleLogin = async (email) => {
    try {
      setLoading(true)
      const { error } = await supabase.auth.signIn({ email })
      if (error) throw error
      alert('Check your email for the login link!')
    } catch (error) {
      alert(error.error_description || error.message)
    } finally {
      setLoading(false)
    }
  }

  return (
    <div className="row flex flex-center">
      <div className="col-6 form-widget">
        <h1 className="header">Supabase + Next.js</h1>
        <p className="description">Sign in via magic link with your email below</p>
        <div>
          <input
            className="inputField"
            type="email"
            placeholder="Your email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div>
          <button
            onClick={(e) => {
              e.preventDefault()
              handleLogin(email)
            }}
            className="button block"
            disabled={loading}
          >
            <span>{loading ? 'Loading' : 'Send magic link'}</span>
          </button>
        </div>
      </div>
    </div>
  )
}
  • ログイン後に表示するアカウント管理画面を作成する
components/Account.js
import { useState, useEffect } from 'react'
import { supabase } from '../utils/supabaseClient'

export default function Account({ session }) {
  const [loading, setLoading] = useState(true)
  const [username, setUsername] = useState(null)
  const [website, setWebsite] = useState(null)
  const [avatar_url, setAvatarUrl] = useState(null)

  useEffect(() => {
    getProfile()
  }, [session])

  async function getProfile() {
    try {
      setLoading(true)
      const user = supabase.auth.user()

      let { data, error, status } = await supabase
        .from('profiles')
        .select(`username, website, avatar_url`)
        .eq('id', user.id)
        .single()

      if (error && status !== 406) {
        throw error
      }

      if (data) {
        setUsername(data.username)
        setWebsite(data.website)
        setAvatarUrl(data.avatar_url)
      }
    } catch (error) {
      alert(error.message)
    } finally {
      setLoading(false)
    }
  }

  async function updateProfile({ username, website, avatar_url }) {
    try {
      setLoading(true)
      const user = supabase.auth.user()

      const updates = {
        id: user.id,
        username,
        website,
        avatar_url,
        updated_at: new Date(),
      }

      let { error } = await supabase.from('profiles').upsert(updates, {
        returning: 'minimal', // Don't return the value after inserting
      })

      if (error) {
        throw error
      }
    } catch (error) {
      alert(error.message)
    } finally {
      setLoading(false)
    }
  }

  return (
    <div className="form-widget">
      <div>
        <label htmlFor="email">Email</label>
        <input id="email" type="text" value={session.user.email} disabled />
      </div>
      <div>
        <label htmlFor="username">Name</label>
        <input
          id="username"
          type="text"
          value={username || ''}
          onChange={(e) => setUsername(e.target.value)}
        />
      </div>
      <div>
        <label htmlFor="website">Website</label>
        <input
          id="website"
          type="website"
          value={website || ''}
          onChange={(e) => setWebsite(e.target.value)}
        />
      </div>

      <div>
        <button
          className="button block primary"
          onClick={() => updateProfile({ username, website, avatar_url })}
          disabled={loading}
        >
          {loading ? 'Loading ...' : 'Update'}
        </button>
      </div>

      <div>
        <button className="button block" onClick={() => supabase.auth.signOut()}>
          Sign Out
        </button>
      </div>
    </div>
  )
}

  • pages/index.jsを編集する
pages/index.js
import { useState, useEffect } from 'react'
import { supabase } from '../utils/supabaseClient'
import Auth from '../components/Auth'
import Account from '../components/Account'

export default function Home() {
  const [session, setSession] = useState(null)

  useEffect(() => {
    setSession(supabase.auth.session())

    supabase.auth.onAuthStateChange((_event, session) => {
      setSession(session)
    })
  }, [])

  return (
    <div className="container" style={{ padding: '50px 0 100px 0' }}>
      {!session ? <Auth /> : <Account key={session.user.id} session={session} />}
    </div>
  )
}

CSSの編集

  • global.cssこちらのものに編集する
    ※公式チュートリアルのリンク先が404だったため、supabaseのgithubを参照しました

動作確認

  • ローカルで起動し、メールアドレスを入力する
npm run dev

  • 受信したメールの Confirm your mail をクリックする
  • ログイン後の画面に遷移することを確認

おわりに

比較的簡単にログイン画面を作成することができました。
今回はとりあえずsupabaseを触るという部分だけだったので、
Row Level Securityやストレージの使い方など別途メモにしていきたいと思います

参考

supabase公式ドキュメント
マジックリンク(Magic Link)とは:パスワードレスのログインをユーザーに提供
マジックリンク認証の落とし穴

Discussion