行動すれば次の現実

テック中心の個人ブログ

devise-token-authでemailではなくusernameでもログインさせる方法

email以外のキーを使った認証方法について、devise単体では同じような記事がいくつもありましたが、devise-token-authを交えた記事があまりなかったので作成しました。

How To: Allow users to sign in using their username or email address · heartcombo/devise Wiki · GitHub

上記の記事等を参考にしましたが、devise-token-auth側でdevise本体の挙動が色々とオーバーライドされており、そのままの設定では動作しない部分がありました。

右往左往しましたが、結果的には思っていたよりも少ない修正で実現できましたので、同様のことでお悩みの方の参考になれば幸いです。

usersテーブルにusernameを追加する

以下のようにユニーク制約を付与してstring型のusernameをusersテーブルに追加します。

class AddUsernameToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :username, :string
    add_index :users, :username, unique: true
  end
end

Userモデルへの設定

authentication_keysの設定

deviseメソッドのauthentication_keysにemailとusernameを設定します。

  devise :database_authenticatable, :registerable, :rememberable, :trackable, :validatable,
         authentication_keys: %i[email username] # ←この部分を設定します

こうすることで、emailまたはusernameが認証キーとして設定されるようになります ログイン時はどちらか一方のキーで認証することが可能となります。

バリデーションの設定

このままでは、usernameに対してバリデーションが全く効いていない状態なので、以下のようにバリデーションを設定します。

validates :username, presence: true, uniqueness: true

emailに対しては:validatableが有効であれば自動でバリデーションチェックが走ります。

任意で細かなチューニング

基本的には上記の設定変更のみでemailまたはusernameでログインすることが可能となります。

ただし現状のままでは不完全な部分があります。

例えばemailとusernameに同じ値が設定できてしまうので考慮が必要です。 また、ユーザー情報変更(account_update)時にusernameが変更できませんので、そこの考慮もする必要があるかと思います。

必要に応じて以下の記事を参考にチューニングを行ってください。

github.com

Rubymineのファイル保存時にPrettierでフォーマットさせる方法

Rubymineの保存時にPrettierによるフォーマットを適用する方法を説明します。

開発環境にPrettierをインストールする

以下のコマンドで開発環境にprettierをインストールします。 インストール先はどこでも良いのですが、私はプロジェクト配下にしました。

yarn add -D prettier

インストールが完了したら.prettierrcというprettier用の設定ファイルをプロジェクトのルートに配置しておきます。

{
  "singleQuote": true
}

Rubymine側の設定

RubymineではデフォルトでPrettierのプラグインがインストールされていますので、ここではセットアップのみ説明します。

Preferences → Languages & Frameworks → Javascript → Prettier を開きます。

Prettier packageに先程インストールしたPrettierを選択します。 プロジェクト配下にインストールしたのであれば、自動で検出されます。

On Reformat Code actionとOn saveにチェックを付ければセットアップは完了です。

これでファイル保存時および、Reformat時にPrettierによりフォーマットされるようになります。

Next.jsとdevise_token_authを使って認証周りを実装する

前回、Rails7 APIモードの認証機能をdevise_token_authで実装するという記事で、devise_token_authの導入方法や使い方を一通り説明いたしました。

今回はNext.jsとdevise_token_authを使ってログイン処理と認証制御周りを具体的な実装例を踏まえて説明します。

認証処理はクライアントサイドで行うか、サーバーサイドで行うか

まず実装に入る前に、認証処理をクライアントサイドで行うのか、サーバーサイドで行うのかを決める必要があります。

クライアントサイドで認証

クライアントサイドで認証を行う場合は、認証用のAPIを別途設けて、その結果により遷移先を分けるという実装をします。 認証結果が真の場合は、該当ページへの遷移を許可し、偽の場合はログインページへリダイレクトさせます。

クライアントサイドでの認証の場合、ページリクエストの前に毎回認証用APIが実行されるので、リクエストが2回発生することになります。

サーバーサイドで認証

サーバーサイドで認証する場合は、認証処理も含めてサーバーサイドで行われます。認証結果が真の場合は、該当アクションのデータを取得し、偽の場合はログインページへリダイレクトさせます。

リクエスト回数が1回で済みますので、私はサーバーサイドでの認証を採用することにしました。

ログイントークンはCookieで保持するか、LocalStorageで保持するか

devise-token-authではaccess-token、client、uidという3つのトークン情報を用いて認証を行います。 これら3つの情報をリクエストヘッダーにセットしておくことで認証処理が行われますので、トークン情報をクライアント側でも保持しておく必要があります。

トークンを保持するにはCookieかlocalStorageを使用するのが一般的かと思います。

私は初めにlocalStorageを検討しました。サーバーサイド認証を実装するにはServerSidePropsを使用するのですが、ServerSidePropsではlocalStorageが読み取れない仕様のようです。

そのため、今回はCookieを使用してトークン情報を保持することにしました。

具体的な実装例

ログインページ

ログインページの実装例を記します。ポイントは/api/v1/auth/sign_inへのログイン処理の結果をCookieにセットしている部分です。 js-cookieを使用してaccess-token、client、uidをそれぞれCookieにセットしています。

// pages/login.tsx

import React, { ReactElement, useState } from "react";
import { useRouter } from "next/router";
import {
  Alert,
  Box,
  Button,
  Container,
  TextField,
  Typography,
} from "@mui/material/";
import axios from "axios";
import Cookies from "js-cookie";

const Login = () => {
  const router = useRouter();
  const [isError, setIsError] = useState<boolean>(false);
  const [errorMessage, setErrorMessage] = useState<string>("");

  const handleSubmit = (event) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    const axiosInstance = axios.create({
      baseURL: `http://localhost:3000/api/v1/`,
      headers: {
        "content-type": "application/json",
      },
    });
    (async () => {
      setIsError(false);
      setErrorMessage("");
      return await axiosInstance
        .post("auth/sign_in", {
          email: data.get("email"),
          password: data.get("password"),
        })
        .then(function (response) {
          // Cookieにトークンをセットしています
          Cookies.set("uid", response.headers["uid"]);
          Cookies.set("client", response.headers["client"]);
          Cookies.set("access-token", response.headers["access-token"]);
          router.push("/home");
        })
        .catch(function (error) {
          // Cookieからトークンを削除しています
          Cookies.remove("uid");
          Cookies.remove("client");
          Cookies.remove("access-token");
          setIsError(true);
          setErrorMessage(error.response.data.errors[0]);
        });
    })();
  };

  return (
    <Container component="main" maxWidth="xs">
      <Box>
        <Typography component="h1" variant="h5">
          ログイン
        </Typography>
        <Box component="form" onSubmit={handleSubmit}>
          <TextField
            id="email"
            label="メールアドレス"
            name="email"
            autoComplete="email"
            autoFocus
          />
          <TextField
            name="password"
            label="パスワード"
            type="password"
            id="password"
            autoComplete="current-password"
          />
          <Button
            type="submit"
            variant="contained"
            sx={{ mt: 3, mb: 2 }}
          >
            ログイン
          </Button>
          {isError ? (
            <Alert
              onClose={() => {
                setIsError(false);
                setErrorMessage("");
              }}
              severity="error"
            >
              {errorMessage}
            </Alert>
          ) : null}
        </Box>
      </Box>
    </Container>
  );
};

ServerSidePropsのラッパー関数

getServerSideProps関数に認証機能の制御をもたせたラッパー関数を実装します。 APIリクエスト時にCookieからトークンを取得してリクエストヘッダーにセットします。

レスポンスが認証失敗(401)だった場合はログイン画面へリダイレクトさせます。成功した場合はレスポンスデータをpropsとして返却します。

// lib/auth.tsx

import { GetServerSideProps } from "next";

export const withAuthServerSideProps = (url: string): GetServerSideProps => {
  return async (context) => {
    const { req, res } = context;

    const response = await fetch(`${process.env.API_ORIGIN}/${url}`, {
      headers: {
        "Content-Type": "application/json",
        uid: req.cookies["uid"],
        client: req.cookies["client"],
        "access-token": req.cookies["access-token"],
      },
    });
    if (!response.ok && response.status === 401) {
      return {
        redirect: {
          destination: "/login",
          permanent: false,
        },
      };
    }
    // TODO: 他にも500エラーを考慮した分岐も必要
    const props = await response.json();
    return { props };
  };
};

認証が必要なページ

認証が必要なページのgetServerSideProps関数を、上記で実装したwithAuthServerSideProps関数でラッパーした状態で実行します。 それによりサーバーサイドレンダリング時に認証処理を制御することが可能になります。

// pages/home.tsx
import * as React from "react";
import styles from "../styles/Home.module.css";
import Head from "next/head";
import { GetServerSideProps } from "next";
import { withAuthServerSideProps } from "../lib/auth";

export const getServerSideProps: GetServerSideProps =
  withAuthServerSideProps("/api/v1/home");

const Home = () => {
  return (
    <>
      <div className={styles.container}>
        <main className={styles.main}>
          <h1 className={styles.title}>HOME</h1>
          <p className={styles.description}>ホーム画面です</p>
        </main>
      </div>
    </>
  );
};

export default Home;

Rails側の処理

Railsのアクションではbefore_actionでdevise_token_authによる認証メソッド(authenticate_api_v1_user!)を実行します。 これにより認証処理が実行されて、認証が失敗した場合は401が返却されます。

class Api::V1::HomeController < ApplicationController
  before_action :authenticate_api_v1_user!

  def index
    render json: { message: 'hello' }
  end
end

終わりに

Next.jsとdevise_token_authを使った認証制御周りの実装は以上になります。

上記の実装以外にも「ログアウト処理」や「認証用Layoutと通常Layoutの2つを用意してナビゲーションバーの共通化する」などの考慮が必要になると思いますが本記事では省略しております。また機会がありましたら記事にしようと思います。

inputタグに入力した数字を3桁ごとのカンマ区切りにして表示する方法

input要素に入力した数字を3桁ごとのカンマ区切りにして表示する方法を説明します。 本記事はReactをベースに実装例を記述していますが、考え方は他のフレームワークでも同じですので、Reactを使用していない方も参考になるかと思います。

input type属性は「tel」がおすすめ

実装に入る前に前提知識として抑えておいてほしいポイントがあります。

それはinput type属性による入力制御の違いです。

input type="number"を使用することで数字のみが入力できるようになります。 モバイルの場合も同様に、数字のキーボードが表示されますので、ユーザビリティ的には問題有りません。

ただし今回のようにカンマ区切りされた数字は実質的には文字列と認識されますので、type="number"は使用できません。

そのため、input type="tel"を使用することおすすめします。 input type="tel"は、電話番号のように数字主体の文字列の入力を想定しています。

あくまでも入力できるのは文字列になりますが、type="text"との違いはモバイルの場合に数字のキーボードが表示されることです。

数字キーボード

以下に各input typeごとの挙動をまとめました。

input type 入力できる値 スマホ入力時 使用例
text 文字列 文字列キーボード 氏名
number 数字のみ 数字キーボード 年齢
tel 文字列 数字キーボード クレカ番号

具体的な実装例

上記の仕様を踏まえた上でtype="tel"を採用してサンプルプログラムを実装します。

サンプルではReactを使用していますが、onChangeのイベントハンドラ以外は他のフレームワークでも使用できるかと思います。

まずは、数字文字列に対して3桁ごとにカンマ区切りを適用させる関数を定義します。

function replaceFormatNumber(numberText) {
  if (!numberText) {
    return '';
  }
  return String(numberText).replace(/\d+/, function (m) {
    return m.replace(/(\d)(?=(?:\d{3})+$)/g, "$1,");
  });
}

次に、3桁区切りされた数字文字列をフラットな数字文字列に変換させる関数も定義します。

function replaceNumberText(formatNumber) {
  if (!formatNumber) {
    return '';
  }
  return formatNumber.replace(/,/g, '');
};

上記2つの関数を使用してonChangeイベントでstateを制御します。

handleChange(e) {
  let value = e.target.value;
  // カンマ区切りされた文字列をフラットな数字文字列に変換します
  value = String(replaceNumberText(value));
  // フラットな数字文字列に対してカンマ区切りを適用します
  value = replaceFormatNumber(value);
  // stateにセットします
  this.setState({[e.target.name]: value});
}

// render部分
<Input
  type="tel"
  name="number"
  value={number}
  onChange={this.handleChange}
/>

Herokuで適切なDynoタイプと台数を決める方法

Herokuでアプリを本番稼働させる上で、「Dynoタイプをどうするか」「台数をどうするか」については一番最初に悩むポイントかと思います。

恥ずかしながら私自身も、アプリ稼働当初は何となくの感覚でDynoタイプ、台数を決めていました。それにより根拠無くオーバースペックな構成にしてしまい、無駄な維持費を掛けてしまっていたことがあります。

今回の記事では定量的にDynoタイプと台数を選定する方法についてまとめてました。同様のことでお悩みの方の参考になれば幸いです。

全てはMetricsタブから

Heroku管理画面のMetricsタブを見ることである適切なDynoを検討することが可能です。 主に見る項目は「Memory Usage」「Response Time」「Throughput」「Dyno Load」の4つです。

Memory UsageとDyno loadはDynoタイプの選定に有効

Memory UsageとDyno loadにより、適切なDynoタイプを判断することができます。もちろんDynoの台数を増やすことの判断材料としても有効ですので積極的に値を確認しましょう。

Memory Usage

Memory UsageではDynoのメモリ使用量を把握することができます。

Memory Usage

主に見るのは「MEM QUOTA」「MAX RSS」です。 MEM QUOTAはDynoが許容できるメモリ使用量です。MAX RSSは実際に使用しているメモリ使用量です。 MAX RSSがMEM QUOTAを超えると基本的にNGです。

超えてしまうとスワップメモリが頻繁に発生するようになります。スワップメモリが発生するとレスポンス速度が著しく低下してしまいユーザ体験の低下に繋がります。

MAX RSSがMAX QUATAを超えないようにDynoタイプもしくはDynoの台数を調整する必要があります。

一概には言えませんが、個人的な経験則ではDynoの台数を増やすよりもDynoタイプをアップグレードするほうが効果的であると感じます。

より詳細にアプリケーションレベルで原因を追求したい場合はScoutAPM等のアドオンを入れる必要があります。 詳しくは下記記事をご参照ください。

HerokuのR14エラーをScoutAPMで対策する方法 | Rails - 行動すれば次の現実

また、Herokuの設定等によってもメモリ消費量を抑えることができます。詳しくは下記記事を参考にしてチューニングすると良いでしょう。

R14 - Memory Quota Exceeded in Ruby (MRI) | Heroku Dev Center

Dyno load

Dyno loadとは直訳すると「Dynoの負荷」という意味で、Dynoに対してどのくらいのCPU負荷がかかっているのかを測定した結果が表示されます。

Dyno load

HerokuではDynoタイプごとに負荷の許容値が設定されています。

free: 1
hobby: 1
standard-1x: 1
standard-2x: 1
performance-m: 2
performance-l: 8

Performance Dyno以外は1が設定されています。1を超えるとCPUの許容を超えたことを意味します。

許容を超えるとCPUが占領されていることになりますので、待ち時間が発生してしまいます。

一時的に許容値を超える分には大きな問題はないと思いますが、なるべく許容値を下回るように考慮しておくほうが望ましいです。

1を超えるようであれば台数を増やしてみる、またはDynoタイプを上げてみて数値の変動を監視すると良いでしょう。 ちなみにすでに複数Dynoを並行稼動使用している場合は、稼働しているDynoの平均値が計算されて評価されるようです。

ThroughputとResponse TimeはDynoの台数を増やすかどうかの指標

ThroughputとResponse Timeの結果によりDynoの台数が適切であるかどうかの判断ができます。

※この2つの指標はwebプロセスのみ有効な指標です。workerプロセスでは表示されません。

Throughput

Throughputではrpsという単位(1秒あたりに捌いているリクエスト数)でグラフ化しています。 rpsが大きい時間帯がアクセスのピークを示しています。

例えば下記の図のようなグラフであればPM3時あたりとPM6時あたりがピークであることが把握できます。

Throughput

Response Time

Response Timeでは時間帯ごとのレスポンス時間がグラフで確認できます。 Response Timeの数値が高い時間帯のThroughputのrpsを確認すると、ある程度の相関関係があることが確認できるかと思います。

Response Time

rpsとResponse Timeの両方が高い場合、Dynoが捌ききれるキャパシティを超えている可能性があります。 レスポンス時間がサービスの許容している時間を超えてしまっている状況が続くと、ユーザー体験の低下に繋がります。そのため、ピーク時間に合わせてDynoの台数を増やすなどの対策をすることでレスポンス時間の削減を図ることができます。

参考

https://help.heroku.com/88G3XLA6/what-is-an-acceptable-amount-of-dyno-load https://devcenter.heroku.com/articles/dyno-types https://devcenter.heroku.com/articles/ruby-memory-use#detecting-a-problem

Rails7 APIモードの認証機能をdevise_token_authで実装する

Rails7 APIモードの認証機能を実装するにあたり、Deviseのトークン認証を可能にするdevise_token_authというライブラリを使用することにしました。

導入に少しハマったところなどもありましたので記事にまとめました。同じような構成を検討している方の参考になれば幸いです。

環境構成

フロントエンド:Next.js 12.1
バックエンド:Rails 7.0
開発環境:Docker、docker-compose

devise_token_authの準備

Gemのインストール

Gemfileに以下を記載してbundle installします。

**Gemfile**

gem 'devise'
gem 'devise_token_auth'
gem 'devise-i18n'
gem 'rack-cors'

rack-corsはCORS(Cross-Origin Resource Sharing)を実現させるために必要となりますので、一緒にインストールします。 devise-i18nはメッセージの日本語化のために必要となるパッケージです。

インストールタスクの実行

deviseとdevise_token_authのインストールタスクを実行します。 今回はUserモデルに対してDeviseを適用したいと思います。

rails g devise:install
rails g devise_token_auth:install User auth

作成されるconfig/initializers/devise_token_auth.rbというファイルに各種設定を記述します。

私の場合config.change_headers_on_each_request = falseのみ変更しました。 これ設定をすることでリクエストの都度トークンが変更されなくなるので、トークンの管理が楽になります。

routesとmigrationファイルの修正

今回は/api/v1/authというパス上に認証用のエンドポイントを作成したいと思うので以下のようにroutes.rbを修正します。

**config/routes.rb**

namespace :api do
  namespace :v1 do
    mount_devise_token_auth_for 'User', at: 'auth'
  end
end

また、devise_token_auth:installではログイン日時などの情報をトラッキングするためのマイグレーション情報が作成されないようなので、以下のように手動で修正を加えます。

**db/migrate/xxxxx_devise_token_auth_create_users.rb**

# Trackable用のカラムを追加します
t.integer  :sign_in_count, default: 0, null: false
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.string  :current_sign_in_ip
t.string  :last_sign_in_ip

メッセージの日本語化

devise-i18nを使用してデフォルト英語のメッセージを日本語に変換させます。

rails g devise:i18n:locale ja

を実行すると/config/locales/devise.views.ja.ymlというファイルが作成されます。

このままでは日本語化されませんので、application_controller.rbに以下の定義を記載します。

**app/controllers/application_controller.rb**

before_action do
  I18n.locale = :ja
end

CORSの設定

originsにフロントのホスト(localhost:8000)を記載して以下のようにCORSの設定ファイルを作成します。

exposeには%w[access-token uid client]を記述しています。これによりクロスドメインでもヘッダー情報が公開されるようになります。

**config/initializers/cors.rb**

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "localhost:8000"

    resource "*",
             headers: :any,
             expose: %w[access-token uid client],
             methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

各メソッドの動作検証

ここまで準備が完了しましたらrails db:migrateを実行してdeviseの動作検証をしてきます。

devise_token_authはHTTPリクエストのヘッダーにuid、access-token、clientを付与することで認証が行われます。 ちなみにcurlに-iオプションをつけるとHTTPレスポンスのヘッダー情報が出力されますので便利です。

ユーザ登録

  • / にPOSTでアクセスします
  • email、password、password_confirmationをparamsに設定して送信することでユーザが作成されます
# コマンド実行例
curl localhost:3000/api/v1/auth -X POST -d '{"email":"test@example.com", "password":"password", "password_confirmation": "password"}' -H "content-type:application/json" -i

ユーザ削除

  • /にDELETEでアクセスします
  • HTTPヘッダーにuid、access-token、clientを付与することで、該当のユーザが削除されます
# コマンド実行例
curl http://localhost:3000/api/v1/auth -H "uid: test@example.com" -H "client: hpHyxPAgFmVxFA75uJOhYA" -H "access-token: glXm3iysPovJSi4q_ffpwQ" -X DELETE
ActionDispatch::Request::Session::DisabledSessionErrorについて

ちなみにRails7の場合、そのままの状態だと以下のエラーが発生します

ActionDispatch::Request::Session::DisabledSessionError (Your application has sessions disabled. To write to the session you must first configure a session store):

Rails7より、APIモードのようにセッションを使用しない場合にセッションへのアクセスがあるとエラーを発生させる機構が備わったことによるエラーです。

deviseでは内部でセッションにアクセスしている箇所があるので当該エラーが発生する模様です。

私の場合、ワークアラウンドとして以下の処理を入れることで一旦回避させました。 根本対応されましたら、きちんと修正しておくことにします。

**app/controllers/application_controller.rb**

 class ApplicationController < ActionController::API
    include DeviseTokenAuth::Concerns::SetUserByToken
    include DeviseHackFakeSession # この部分を追加します
 end
**app/controllers/concerns/devise_hack_fake_session.rb**

module DeviseHackFakeSession
  extend ActiveSupport::Concern

  class FakeSession < Hash
    def enabled?
      false
    end

    def destroy
    end
  end

  included do
    before_action :set_fake_session

    private

    def set_fake_session
      if Rails.configuration.respond_to?(:api_only) && Rails.configuration.api_only
        request.env['rack.session'] ||= ::DeviseHackFakeSession::FakeSession.new
      end
    end
  end
end

参考: https://github.com/heartcombo/devise/issues/5443

ユーザ変更

  • /にPUTでアクセスします
  • HTTPヘッダーにuid、access-token、clientを付与して該当のユーザを変更します
  • デフォルトはemailとpasswordのみ変更可能なので、必要に応じてconfigure_permitted_parametersにパラメータを追記します
# コマンド実行例
curl http://localhost:3000/api/v1/auth -d '{"password":"password", "name":"taro"}'  -H "content-type:application/json" -H "uid: test@example.com" -H "client: fy5z545K8TwgunV_D2LJXg" -H "access-token: B5z0QZ8jro5zZbqcuHAtow" -X PUT
**app/controllers/application_controller.rb**

  before_action :configure_permitted_parameters, if: :devise_controller?

  # nameを変更可能にする例
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:account_update, keys: [:name])
  end

ログイン

  • /sign_inにPOSTでアクセスします
  • 認証が成功するとHTTPレスポンスヘッダーにuid、access-token、clientが付与されてきます。認証が必要なアクションに対してリクエストヘッダーそれらを付与することでアクセス可能になります
curl localhost:3000/api/v1/auth/sign_in -X POST -d '{"email":"test@example.com", "password":"password"}' -H "content-type:application/json" -i

# レスポンス一部抜粋
access-token: OtzRYGvcy-W3zvXzQijB4Q
token-type: Bearer
client: r7zZ0MBaFSnNcGsiRZQetQ
expiry: 1657875966
uid: test@example.com

ログアウト

  • /sing_outにDELETEでアクセスします
  • HTTPヘッダーにuid、access-token、clientを付与して該当のユーザがログアウトされます
curl http://localhost:3000/api/v1/auth/sign_out -H "content-type:application/json" -H "uid: test@example.com" -H "client: fy5z545K8TwgunV_D2LJXg" -H "access-token: B5z0QZ8jro5zZbqcuHAtow" -X DELETE

パスワードリセット

  • /auth/passwordにPOSTでアクセスします
  • emailとredirect_urlをパラメータに付与することで、該当のアドレスに対してパスワードリセットメールが送信されます。メール内に記載されたパスワードリセットフォームのURLにredirect_urlが設定されます
  • devise_token_auth(v1.2.2)時点でなぜかrails g devise_token_auth:install_viewsでメールテンプレートファイルを作成しなければredirect_urlが動作しない事象が発生しています。
curl http://localhost:3000/api/v1/auth/password -H "content-type:application/json" -d '{"email":"hello@example.com", "redirect_url":"http://localhost:3000/pages"}' -X POST

パスワードリセットメール

疎通確認

では実際に、認証が必要なエンドポイントを設けて実際に疎通確認をしてみます。

/api/v1/homeというエンドポイントを設けます。

ログインしている場合のみアクセスできるようにbefore_action :authenticate_api_v1_user!を定義しています。authenticate_api_v1_userのメソッド名はdeviseがマウントされているpathによって動的に命名されますので、api/v1という構成でない場合は読み替えてください。

Rails.application.routes.draw do
  resources :pages, only: [:index]
  namespace :api do
    namespace :v1 do
      resources :home, only: [:index]

      mount_devise_token_auth_for 'User', at: 'auth'
    end
  end
end
class Api::V1::HomeController < ApplicationController
  before_action :authenticate_api_v1_user!

  def index
    render json: { message: 'hello' }
  end
end

tokenが正しい場合

curl http://localhost:3000/api/v1/home -H "content-type:application/json" -H "uid: hello@example.com" -H "client: NdQxhsz2PiRdBR25xIfzJg" -H "access-token: Zv5thbTxDEWEyn7Nhl_SLg" -X GET

{"message":"hello"}

tokenが正しくない場合

curl http://localhost:3000/api/v1/home -H "content-type:application/json" -H "uid: hello@example.com" -H "client: NdQxhsz2PiRdBR25xIfzJg" -H "access-token: hoge" -X GET

{"errors":["ログインもしくはアカウント登録してください。"]}

終わりに

今回の記事ではdevise_token_authのインストール方法と使い方をメインに説明しました。 次回はNext.jsとの繋ぎ込み部分に関してのセッション管理をどうするか、認証によるページ制御をどうするかについて説明しようと思います。

Next.jsとdevise_token_authを使って認証周りを実装する

Rails+Postgresql+Dockerで発生する「There is an issue connecting to your database with your username/password, username xxx」でハマった件

Rails、Postgresqlの構成でdocker composeによる開発環境を構築していたところ、RailsからDBに接続する際に、以下のエラーが発生しました。

There is an issue connecting to your database with your username/password, username: app.

Please check your database configuration to ensure the username/password are valid.
Couldn't create 'app_development' database. Please check your configuration.
rails aborted!
ActiveRecord::DatabaseConnectionError: There is an issue connecting to your database with your username/password, username: app.

Please check your database configuration to ensure the username/password are valid.

このエラーが発生する場合、database.ymlにusernameやpasswordを設定し忘れているケースがほとんどです。

実際に私の環境では以下のような設定になっておりました。

# database.yml

default: &default
  adapter: postgresql
  encoding: utf8
  host: db
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: app
  password: password

development:
  <<: *default
  database: app_development
# docker-compose.yml(dbの部分のみ抜粋)
  db:
    image: postgres:13.6
    volumes:
      - postgres:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: password
      POSTGRES_DB: app_development
      TZ: "Asia/Tokyo"
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=C"

volumeを初期化したら解決

設定自体は正しそうでしたので、DBが記録されているvolumeを削除してみることにしました。

$ docker volume ls
・・省略
local     app_postgres
・・省略

$ docker volume rm app_postgres
$ docker-compose up -d -build

volumeを削除後にもう一度docker-composeで起動してみたところ、エラーが発生せずに正常にアプリとDBが接続することができました。

なぜvolumeを初期化することで解決できたかというと、database.ymlのusernameをdocker-composeの初期起動時から変更してしまったためです。

docker-compose.yml(dbの部分のみ抜粋)

  db:
    image: postgres:13.6
    volumes:
      - postgres:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: sample # ← sampleという名前で構築して後からappに変更した

そのため、実質的には、docker-composeでの初期起動時に記載されたusernameとdatabase.ymlに記載されたusernameと不一致の状態で接続を試みていたことになります。

小一時間ほどハマってしまったので、同じ境遇の方のお役に立てば幸いです。