はじめに
Next.jsとRailsを使ってオリジナルのWebアプリを開発しています。
github.com
ログイン機能を備えたアプリにするために、Google認証でログインする方法を選択しました。この記事では、実装過程において調べた情報や試行錯誤しハマった経験などをまとめたいと思います。
前編、後編の2部構成でお届けします。
前編
Next.jsのNextAuth.jsを使用しGoogle認証ログイン、ログアウトに焦点を当てた内容です。
前編はこちら
後編
ログインしたユーザーの情報をデータベースに保存する方法について書きます。Rails APIを叩いてデータベースに保存する方法です。Next.jsとRailsを使ったユーザー登録と削除についてまとめたいと思います。
技術スタック
フロントエンド:
- Next.js '13.2.4'
- 使用するnpm
- next-auth '4.20.1'
- axios '1.3.4'
バックエンド:
データベース:
github.com
Rails APIを叩いてユーザー情報をDBに保存する
Railsのプロジェクトを作成する
$ rails new . --api --database=postgresql --skip-webpack-install --skip-javascript --skip-test
オプションの説明
--api
: APIモードでRailsアプリケーションを作成します。不要なミドルウェアやジェネレータを削除します。
--database=postgresql
: デフォルトのデータベースとしてPostgreSQLを指定します。
--skip-webpack-install
: Webpackerのインストールをスキップします。Next.jsでフロントエンドを構築するためWebPackは不要です。Rails7ではデフォルトで使用しなくなったっぽいですが、念のため付けておきます。
--skip-javascript
: JavaScriptのサポートを無効にします。
--skip-test
: RailsのデフォルトのテストフレームワークであるMinitestをスキップし、代わりにRSpecとCypressを使用することを想定しています。(今回はテストは省略しています)
.gitを削除する
$ rm -rf .git
.git
ディレクトリを削除します。.git
を削除するなら最初から--skip-git
することもできますが、初期の.gitignore
が欲しいのでスキップはせずに後から削除します。
因みにファイル構成は以下のようにしているため.git
はプロジェクトのトップで管理しています。
.
├── .git
│ ├── COMMIT_EDITMSG
│ ├── FETCH_HEAD
│ ├── HEAD
│ ├── config
│ ├── description
│ ├── hooks
│ ├── index
│ ├── info
│ ├── logs
│ ├── objects
│ └── refs
└── next-rails-google-auth
├── backend(Rails)
└── frontend(Next.js)
モジュール名を変更する
初期値だとディレクトリ名のBackendになっています。プロジェクト名に名前を変更しておきます。
module NextRailsGoogleAuthApi
データベースを作成しモデルを準備する
最低限必要なカラムだけ設定し作成します。細かい制約、バリデーションなどは省略します。
create_table "users", force: :cascade do |t|
t.string "provider"
t.string "uid"
t.string "name"
t.string "email"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
ルーティングを設定する
User登録、削除のためのルーティングを設定します。
post 'auth/:provider/callback', to: 'api/v1/users#create'
delete 'users/:email', to: 'api/v1/users#destroy', constraints: { email: %r{[^/]+} }
コントローラーを準備する
createが成功したらhead :ok
と返します。
当初、ここでredirect_to 'http://localhost:4000/'
としてリダイレクト処理をしていました。しかしステータスコード302
を返しているのが原因でエラーが出ていました。
今回やりたいことは「ユーザー登録が成功したかどうか?」の確認がしたいだけなのでリダイレクトは不要でした。ステータスコード200
を返すだけで十分なので、head :ok
と変更し、無事ログイン処理は成功しました。
module Api
module V1
class UsersController < ApplicationController
def create
user = User.find_or_create_by(provider: params[:provider], uid: params[:uid], name: params[:name], email: params[:email])
if user
head :ok
else
render json: { error: "ログインに失敗しました" }, status: :unprocessable_entity
end
rescue StandardError => e
render json: { error: e.message }, status: :internal_server_error
end
def destroy
user = User.find_by(email: params[:email])
if user
user.destroy
else
render json: { error: "ユーザーが見つかりませんでした" }, status: :not_found
end
rescue StandardError => e
render json: { error: e.message }, status: :internal_server_error
end
end
end
end
200
以外はfalseを返す処理にしています。
if (response.status === 200) {
return true
} else {
return false
}
フロントエンドとバックエンドの連携を設定する
rack-cors
Rails APIは、rack-corsのgemを使って簡単にCORS(オリジン間リソース共有)の設定を行うことができます。このgemを利用することで、Next.js と Rails APIの連携が可能になります。
APIモードでRailsアプリを開発しているので、既にGemfileにrack-cors
が記載されています。コメントアウトを解除してbundle install
します。
gem "rack-cors"
インストール後、cnfig/initializers/cors.rb
ファイルに設定内容を記述します。
origins "http://localhost:4000"
とすることで、リクエストを受け取れるようにします。"http://localhost:4000"
からのリクエストは許可しますよ。という意味です。
今回はNext.js側でhttp://localhost:4000
を使っているため指定したいと思います。ちなみに、origins を*
とすると、どこからでもRails APIを叩けることになるので危険です。
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins "http://localhost:4000"
resource "*",
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
axios
axiosは、非同期通信でデータを取得するためのモジュール、ライブラリです。Node.jsで書かれています。ブラウザや node.js で動く Promise ベースの HTTP クライアントです。REST APIを実行したいときなど、このnpmを使うと実装が簡単にできます。
今回は、Rails APIを叩くために利用したいと思います。
前編でインストール済みですが、まだの場合はインストールします。
$ npm install axios
Next.js側からRails APIを叩き、ユーザー情報をDBに保存する
コールバックを追加します。signIn()
が実行されたときに呼び出す内容を定義します。
ユーザー情報を取得し、Rails APIに向けてPOSTします。apiUrl
変数は、環境変数で管理しているので http://localhost:3000
となります。
import NextAuth from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
import axios from 'axios'
const apiUrl = process.env.NEXT_PUBLIC_API_URL
export default NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
callbacks: {
async signIn({ user, account, profile }) {
const provider = account.provider
const uid = account.sub
const name = user.name
const email = user.email
try {
const response = await axios.post(`${apiUrl}/auth/${provider}/callback`, {
provider,
uid,
name,
email,
})
if (response.status === 200) {
return true
} else {
return false
}
} catch (error) {
console.log('エラー', error)
return false
}
},
},
})
signIn()
の引数に渡している変数の中身を確認してみます。
userのid: '109704921881887261253',
と accoutのsub: '109704921881887261253'
は同じです。どっちがいいのかは分かりませんが、今回はaccount.sub
で値を取得することにしました。
// user
{
id: '109704921881887261253',
name: 'ネクスト太郎',
email: 'next.taro.1234@gmail.com',
image: 'https://lh3.googleusercontent.com/a/AGNmyxaK1E15pp3Qp1MrPG0lH6MBjZK3aVeEBnh3hZ9M=s96-c'
}
// account
{
provider: 'google',
type: 'oauth',
providerAccountId: '109704921881887261253',
access_token: 'ya29.a0Ael9sCOqoygeGWIZD4vl3xmYzd09PE29hogehogehogehoge',
expires_at: 1680098462,
scope: 'openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile',
token_type: 'Bearer',
id_token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjFhYWU4ZDdjOTIwNThiNWVlYTQ1Njg5NWJmODkwODQ1NzFlMzA2ZjMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI0Mzg1MzU4NTU3MTctZWVkdm00bmYxODU5ZDFnYjJ1czhpdDNydTRhaHRhMWkuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI0Mzg1MzU4NTU3MTctZWVkdm00bmYxODU5ZDFnYjJ1czhpdDNydTRhaHRhMWkuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMDk3MDQ5MjE4ODE4ODcyNjEyNTMiLCJlbWFpbCI6Im5leHQudGFyby4xMjM0QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoidTh1bF95UkdvWEtUTk81RlRVSndvdyIsIm5hbWUiOiLjg43jgq_jgrnjghogehogehogehoge'
}
profile
{
iss: 'https://accounts.google.com',
azp: '438535855717-eedvm4nf1859d1gb2us8it3ru4ahta1i.apps.googleusercontent.com',
aud: '438535855717-eedvm4nf1859d1gb2us8it3ru4ahta1i.apps.googleusercontent.com',
sub: '109704921881887261253',
email: 'next.taro.1234@gmail.com',
email_verified: true,
at_hash: 'u8ul_yRGoXKTNO5FTUJwow',
name: 'ネクスト太郎',
picture: 'https://lh3.googleusercontent.com/a/AGNmyxaK1E15pp3Qp1MrPG0lH6MBjZK3aVeEBnh3hZ9M=s96-c',
given_name: '太郎',
family_name: 'ネクスト',
locale: 'ja',
iat: 1680094863,
exp: 1680098463
}
ユーザー情報を削除するコンポーネントも用意しておく
ログイン機能とログアウト機能のコンポーネントはすでに前編で作成済みですが、削除するためのコンポーネントがないので用意します。
import React from 'react'
import { useSession, signOut } from 'next-auth/react'
import axios from 'axios'
const DeleteUser = () => {
const { data: session } = useSession()
const apiUrl = process.env.NEXT_PUBLIC_API_URL
const handleDeleteUser = async () => {
if (!session || !session.user) {
console.error('セッションが存在しません')
return
}
try {
const response = await axios.delete(`${apiUrl}/users/${session.user.email}`)
if (response.status === 204) {
signOut()
} else {
console.error('アカウント削除に失敗しました')
}
} catch (error) {
console.log('エラーだよ全員集合!', error)
}
}
if (session) {
return (
<div>
<button onClick={() => handleDeleteUser()}>アカウントを削除する</button>
</div>
)
}
return null
}
export default DeleteUser
indexページに、作成したコンポーネントを配置する
Login.jsx
Logout.jsx
DeleteUser.jsx
import React from 'react'
import Head from 'next/head'
import { useSession } from 'next-auth/react'
import Login from '@/components/Login'
import Logout from '@/components/logout'
import DeleteUser from '@/components/deleteUser'
export default function Home() {
const { data: session, status } = useSession()
return (
<>
<Head>
<title>next-rails-google-auth</title>
</Head>
<div>
<h1>next-rails-google-auth</h1>
{status === 'authenticated' ? (
<div>
<p>セッションの期限:{session.expires}</p>
<p>ようこそ、{session.user.name}さん</p>
<img src={session.user.image} alt='' style={{ borderRadius: '50px' }} />
<div>
<Logout />
</div>
<div>
<DeleteUser />
</div>
</div>
) : (
<Login />
)}
</div>
</>
)
}
実際に動かしてみる
アプリを起動させます。
ログイン画面
rails console
コマンドでデータベースに保存されているか確認してみます。
irb(main):001:0> User.all
User Load (0.5ms) SELECT "users".* FROM "users"
=>
[#<User:0x000000010d540688
id: 5,
provider: "google",
uid: "109704921881887261253",
name: "ネクスト太郎",
email: "next.taro.1234@gmail.com",
created_at: Mon, 27 Mar 2023 13:52:50.830761000 UTC +00:00,
updated_at: Mon, 27 Mar 2023 13:52:50.830761000 UTC +00:00>]
ユーザー情報の登録ができていることが確認できました!
ログイン後の画面
「アカウントを削除する」を実行した後、同じくrails console
コマンドで確認してみます。
irb(main):002:0> User.all
User Load (0.4ms) SELECT "users".* FROM "users"
=> []
ユーザー情報が削除されたことも確認できました!
おわりに
当初はomniauth-google-oauth2を使ってGoogle認証を行う予定でしたが、Next.jsを使う場合、NextAuth.jsの選択肢があることを知り、途中から切り替えて実装しました。
バックエンドでのユーザー管理や認証のロジックを一元化したい場合はomniauth-google-oauth2がおすすめのようですが、特にその予定はなかったのと、Railsではdevise を使ったユーザー認証は経験していたので、今回はフロントエンドでの認証フローを経験してみたいと思いNextAuth.jsを選択しました。
Next.js × Railsでアプリ開発をしNextAuth.jsの利用を検討している方の参考になれば幸いです。
最後までお読みいただきありがとうございました!