♨️
[Next.js] とりあえずsupabase触ってみる
はじめに
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 より anon と service_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