前回(第2回) では、Deviseの基本的な認証フロー(サインアップ、サインイン、サインアウト)を実装し、current_user
などのヘルパーを使ってアクセス制御を行いました。これで機能は動くようになりましたが、いくつかの課題が残っていましたね。
- 認証画面(ログイン、サインアップ等)の見た目がDeviseデフォルトのままで、管理画面のデザインと統一感がない。
- 認証画面に管理画面のレイアウト(ヘッダーやサイドバー)が適用されていない。
- ログインページのURLが
/admin/sign_in
であり、管理画面の入り口である/admin
に直接アクセスしてもログインフォームは表示されない。
今回の第3回では、これらの課題を解決し、ユーザーにとってシームレスで見た目も美しい管理画面認証体験を実現します!具体的には以下のステップを進めます。
- 1. ゴール設定
-
/admin
を認証の起点とし、未ログインならログインフォーム、ログイン済みならダッシュボードへ誘導する流れを定義します。 - 2. ルーティングとコントローラーの準備
-
/admin
パスをログインフォームに結びつけ、Deviseコントローラーに管理画面レイアウトを適用させます。 - リダイレクト設定
-
ログイン成功後に管理画面トップへ自動遷移するようにします。
- ビューのTablerカスタマイズ
-
Deviseの各認証画面(フォームなど)をTablerのデザインに統一します。
この記事を読み終えれば、機能的にもデザイン的にも完全に統合された管理画面認証が完成します。
前提条件
この記事は、以下の環境と設定が完了していることを前提としています。
- DockerおよびDocker Compose
- Tablerレイアウト (
admin.html.erb
など) - Devise導入済み (第1, 2回の内容、認証URLは
/admin
配下設定済み) - Flashメッセージ表示設定済み (パーシャル
admin/shared/_flash_messages.html.erb
を含む)
Step 1: ゴール設定 – /admin 起点の認証フローとは?
まず、今回のゴールとなる理想的な認証フローを確認しましょう。
- 未ログインで
/admin
→ ログインフォーム表示 (Adminレイアウト適用) - ログイン成功 →
/admin
(ダッシュボード) へリダイレクト - ログイン済みで
/admin/sign_in
→/admin
(ダッシュボード) が表示される - ログアウトでログインフォームにリダイレクト
- サインアップ等は
/admin/sign_up
など (Adminレイアウト適用) - 各種Deviseフォームにデザインレイアウト適用
この理想的な流れを実現するために、ルーティング、コントローラー、ビューを順に設定・カスタマイズしていきます。
Step 2: ルーティングとコントローラーの準備
/admin
を認証の起点とし、認証画面に admin
レイアウトを適用するための準備を行います。
a. カスタムコントローラーファイルの生成
Deviseのコントローラーをカスタマイズするためのファイルを、admin
スコープを指定してapp/controllers/admin/
ディレクトリ配下に生成します。
# sessions, registrations, passwords コントローラーを admin スコープで生成
bin/rails generate devise:controllers admin -c sessions registrations passwords
このコマンドにより、以下のファイルが正しいクラス名 (Admin::...Controller
) で生成されます。
app/controllers/admin/sessions_controller.rb
app/controllers/admin/registrations_controller.rb
app/controllers/admin/passwords_controller.rb
b. ルーティング設定 (config/routes.rb
)
config/routes.rb
を編集し、/admin
へのマッピングと devise_for
の controllers
オプションで、今準備した admin/
配下のコントローラーを指定します。
# config/routes.rb
Rails.application.routes.draw do
root to: "home#index" # 一般ユーザー向けトップなど
# 管理画面の名前空間
namespace :admin do
root to: "dashboard#index" # 管理画面トップ /admin (ログイン後に表示)
end
# Deviseの他のルーティングを /admin スコープ内に定義
scope "/admin" do
# path: '' でパスから /users を除去
# controllers: で admin/ 配下のコントローラーを指定
devise_for :users, path: "", path_names: {
edit: "account" # /admin/account アカウント編集画面に遷移
},
controllers: {
sessions: "admin/sessions",
registrations: "admin/registrations",
passwords: "admin/passwords"
# confirmations: 'admin/confirmations',
# unlocks: 'admin/unlocks'
}
end
# ...
end
これで、/admin
がログインフォームの入り口となり、認証処理は app/controllers/admin/
配下のコントローラーが担当する、という設定が完了しました。
Step 3: 認証画面専用のTablerレイアウト作成と適用
a. 認証画面用レイアウトファイルの作成
# レイアウトファイルを作成
touch app/views/layouts/devise.html.erb
作成した devise.html.erb
に以下の内容を記述します。Tablerの認証画面に適したクラスを使用し、ヘッダー/サイドメニュー/フッターは含みません。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title><%= content_for?(:title) ? yield(:title) : "認証" %> - <%= Rails.application.class.module_parent_name %></title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "tabler.min", data_turbo_track: "reload" %>
<%= stylesheet_link_tag "admin", data_turbo_track: "reload" %>
<%= javascript_importmap_tags %>
</head>
<body class="d-flex flex-column">
<div class="page page-center">
<div class="container container-tight py-4">
<%= render "admin/shared/flash_messages" %>
<%= yield %>
</div>
</div>
</body>
</html>
b. 各カスタムコントローラーにレイアウトを指定
生成された各コントローラーファイルを開き、クラス定義の直下に layout "devise"
を追記します。
# app/controllers/admin/sessions_controller.rb
class Admin::SessionsController < Devise::SessionsController
layout "devise"
protected
# ログイン後のリダイレクト先
def after_sign_in_path_for(resource)
stored_location_for(resource) || admin_root_path
end
# ログアウト後のリダイレクト先
def after_sign_out_path_for(resource_or_scope)
new_admin_user_session_path
end
end
- ログイン後とログアウト後のリダイレクト先を変更します。
# app/controllers/admin/registrations_controller.rb
class Admin::RegistrationsController < Devise::RegistrationsController
layout :resolve_layout # レイアウトを出し分け
private
def resolve_layout
case action_name
when "new", "create" # サインアップ関連 (ログイン前)
"devise"
when "edit", "update", "destroy" # アカウント編集関連 (ログイン後)
"admin"
else
"admin" # デフォルト
end
end
protected
# ログイン後のリダイレクト先
def after_sign_in_path_for(resource)
admin_root_path
end
# サインアップ後のリダイレクト先
def after_sign_up_path_for(resource)
admin_root_path
end
end
- ログイン前ページとログイン後ページでレイアウトを出し分けています。
- ログイン後とサインアップ後のリダイレクト先を
admin_root
に変更します。
# app/controllers/admin/passwords_controller.rb
class Admin::PasswordsController < Devise::PasswordsController
layout "devise"
protected
# パスワード変更後のリダイレクト先
def after_resetting_password_path_for(resource)
admin_root_path
end
end
- パスワード変更後のリダイレクト先を変更しています。
c. 動作確認 (レイアウト適用)
開発サーバーを再起動し、ブラウザで /admin
(未ログイン時) や /admin/sign_up
にアクセスします。
今度は、ヘッダーやサイドメニューが表示されず、中央にフォームが表示されるシンプルな devise.html.erb
レイアウトが適用されているはずです。
Step 4: Deviseビューファイルの生成と配置
次に、認証画面のHTML(フォームなど)をカスタマイズするために、Deviseのビューファイルを準備します。標準のビューファイルを生成し、必要なものを admin/
配下や admin/shared/
配下に手動で配置します。
a. 標準ビューファイルの生成
まず、Deviseの標準ビューファイルを app/views/devise/
配下に生成します。
bin/rails generate devise:views
これにより app/views/devise/
ディレクトリと、その中に sessions
, registrations
, passwords
, shared
などのサブディレクトリとビューファイルが作成されます。
b. カスタマイズ用ディレクトリの作成
# 必要なディレクトリを作成
mkdir -p app/views/admin/sessions
mkdir -p app/views/admin/registrations
mkdir -p app/views/admin/passwords
# 必要に応じて confirmations, unlocks も
c. 必要なビューファイルを手動で移動
app/views/devise/
ディレクトリの中から、今回カスタマイズする主要なビューファイル(サインイン、サインアップ、アカウント編集など)を、先ほど作成した app/views/admin/
配下の対応するディレクトリに移動させます。
ターミナルで以下のコマンドを実行します。
# --- 必須: サインイン画面のビューを移動 ---
mv app/views/devise/sessions/new.html.erb app/views/admin/sessions/
# --- 必須: サインアップ画面とアカウント編集画面のビューを移動 ---
mv app/views/devise/registrations/new.html.erb app/views/admin/registrations/
mv app/views/devise/registrations/edit.html.erb app/views/admin/registrations/
# --- 以下は任意: パスワード関連のビューもカスタマイズする場合 ---
mv app/views/devise/passwords/new.html.erb app/views/admin/passwords/
mv app/views/devise/passwords/edit.html.erb app/views/admin/passwords/
# --- 以下は任意: メール確認関連のビューもカスタマイズする場合 ---
# mkdir -p app/views/admin/confirmations
# mv app/views/devise/confirmations/new.html.erb app/views/admin/confirmations/
# --- 以下は任意: アカウントロック解除関連のビューもカスタマイズする場合 ---
# mkdir -p app/views/admin/unlocks
# mv app/views/devise/unlocks/new.html.erb app/views/admin/unlocks/
上記は主要なファイルの移動例です。実際にカスタマイズしたいファイルに応じてコマンドを実行してください。
ファイルを移動する代わりに cp
コマンドでコピーすることも可能です。
d. 共有パーシャルの配置
app/views/devise/shared/
に生成された共有パーシャル (_links.html.erb
, _error_messages.html.erb
) は、app/views/admin/shared/
ディレクトリに移動またはコピーします。
# まだ admin/shared/ に配置していなければ移動/コピー (例)
mkdir -p app/views/admin/shared # 念のため
mv app/views/devise/shared/_links.html.erb app/views/admin/shared/
mv app/views/devise/shared/_error_messages.html.erb app/views/admin/shared/
e. 不要になったディレクトリの削除 (任意)
ビューファイルの移動・コピーが完了したら、元の app/views/devise/
ディレクトリは削除しても構いません。
rm -rf app/views/devise
これで、カスタマイズ対象のビューファイルが app/views/admin/
配下に配置され、コントローラー (Admin::...Controller
) がこれらを読み込む準備が整いました。
Step 5: 共有パーシャルの調整
共有パーシャル(admin/shared/
)についてもTablerクラスを適用していきます。
エラーメッセージ
app/views/admin/shared/_error_messages.html.erb
にデザインを適用します。
<% if resource.errors.any? %>
<div id="error_explanation" class="alert alert-danger alert-dismissible alert-important" role="alert" data-turbo-cache="false">
<div>
<h4 class="alert-title">
<%= I18n.t("errors.messages.not_saved", count: resource.errors.count, resource: resource.class.model_name.human.downcase) %>
</h4>
<ul class="list-unstyled mb-0">
<% resource.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<% end %>
各種リンク
app/views/admin/shared/_links.html.erb
にデザインを適用を適用します。
<div class="d-flex justify-content-center flex-wrap gap-3">
<%# サインインページへのリンク (ログイン画面以外で表示) %>
<%- if controller_name != 'sessions' %>
<div> <%# 個々のリンクをdivで囲むとgapが適用されやすい %>
<%= link_to t(".sign_in", default: "ログイン"), new_session_path(resource_name) %>
</div>
<% end %>
<%# 新規登録ページへのリンク (登録が許可されていて、登録画面以外で表示) %>
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
<div>
<%= link_to t(".sign_up", default: "新規登録"), new_registration_path(resource_name) %>
</div>
<% end %>
<%# パスワード再設定ページへのリンク (パスワードリセットが許可されていて、関連画面以外で表示) %>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
<div>
<%= link_to t(".forgot_your_password", default: "パスワードをお忘れですか?"), new_password_path(resource_name) %>
</div>
<% end %>
<%# メールアドレス確認手順ページへのリンク (メール確認が許可されていて、関連画面以外で表示) %>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
<div>
<%= link_to t('.didn_t_receive_confirmation_instructions', default: "確認メールが届かない場合"), new_confirmation_path(resource_name) %>
</div>
<% end %>
<%# アカウントロック解除手順ページへのリンク (アカウントロックが許可されていて、関連画面以外で表示) %>
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
<div>
<%= link_to t('.didn_t_receive_unlock_instructions', default: "アカウントロック解除手順"), new_unlock_path(resource_name) %>
</div>
<% end %>
</div>
Step 6: 各種画面にTablerデザインを適用
サインイン画面
app/views/admin/sessions/new.html.erb
にデザインを適用を適用します。
<div class="container container-tight py-4">
<div class="text-center mb-4"><h1 class="h3 mb-3 fw-normal">管理画面ログイン</h1></div>
<div class="card card-md">
<div class="card-body">
<h2 class="card-title text-center mb-4">アカウントにログイン</h2>
<%= form_for(resource, as: resource_name, url: new_user_session_path) do |f| %>
<%= render "admin/shared/error_messages", resource: resource %>
<div class="mb-3">
<%= f.label :email, class: "form-label" %>
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control", required: true %>
</div>
<div class="mb-2">
<label class="form-label">
<%= f.label :password %>
<span class="form-label-description">
<%- if devise_mapping.recoverable? %><%# パスワード忘れリンク %>
<%= link_to "パスワードをお忘れですか?", new_user_password_path(resource_name) %>
<% end %>
</span>
</label>
<%= f.password_field :password, autocomplete: "current-password", class: "form-control", required: true %>
</div>
<% if devise_mapping.rememberable? %>
<div class="mb-3">
<label class="form-check">
<%= f.check_box :remember_me, class: "form-check-input" %>
<%= f.label :remember_me, class: "form-check-label" %>
</label>
</div>
<% end %>
<div class="form-footer"> <%= f.submit "ログイン", class: "btn btn-primary w-100" %> </div>
<% end %>
</div>
</div>
<div class="text-center text-muted mt-3">
アカウントをお持ちでないですか? <%= render "admin/shared/links" %>
</div>
</div>

サインアップ画面
app/views/admin/registrations/new.html.erb
にデザインを適用を適用します。
<div class="container container-tight py-4">
<div class="text-center mb-4"><h1 class="h3 mb-3 fw-normal">アカウント作成</h1></div>
<div class="card card-md">
<div class="card-body">
<h2 class="card-title text-center mb-4">新しいアカウントを作成</h2>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= render "admin/shared/error_messages", resource: resource %>
<div class="mb-3">
<%= f.label :email, class: "form-label" %>
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control", required: true %>
</div>
<div class="mb-3">
<%= f.label :password, class: "form-label" %>
<%= f.password_field :password, autocomplete: "new-password", class: "form-control", required: true %>
</div>
<div class="mb-3">
<%= f.label :password_confirmation, class: "form-label" %>
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: "form-control", required: true %>
</div>
<div class="form-footer">
<%= f.submit "アカウント作成", class: "btn btn-primary w-100" %>
</div>
<% end %>
</div>
</div>
<div class="text-center text-muted mt-3">
すでにアカウントをお持ちですか? <%= render "admin/shared/links" %>
</div>
</div>

アカウント編集画面
app/views/admin/registrations/edit.html.erb
にデザインを適用を適用します。
<div class="container-xl">
<div class="card">
<div class="card-header"><h3 class="card-title">アカウント設定変更</h3></div>
<div class="card-body">
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
<%= render "admin/shared/error_messages", resource: resource %>
<div class="mb-3">
<%= f.label :email, class: "form-label" %>
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control", required: true %>
</div>
<div class="mb-3">
<%= f.label :password, class: "form-label" %>
<%= f.password_field :password, autocomplete: "new-password", class: "form-control" %>
</div>
<div class="mb-3">
<%= f.label :password_confirmation, class: "form-label" %>
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: "form-control" %>
</div>
<hr class="my-4">
<div class="mb-3">
<%= f.label :current_password, class: "form-label" %>
<%= f.password_field :current_password, autocomplete: "current-password", class: "form-control", required: true %>
</div>
<div class="form-footer text-end">
<%= f.submit "更新する", class: "btn btn-primary" %>
</div>
<% end %>
</div>
<div class="card-footer bg-light">
<h3 class="card-title">アカウント削除</h3>
<p class="text-muted">アカウントを削除すると元に戻すことはできません。</p>
<div><%= button_to "アカウントを削除する", registration_path(resource_name), data: { confirm: "アカウントを削除します。よろしいですか?", turbo_confirm: "アカウントを削除します。よろしいですか?" }, method: :delete, class: "btn btn-danger" %></div>
</div>
</div>
</div>

パスワードリセット要求画面
app/views/admin/passwords/new.html.erb
にデザインを適用を適用します。
<div class="container container-tight py-4">
<div class="text-center mb-4">
<h1 class="h3 mb-3 fw-normal">パスワード再設定</h1>
</div>
<div class="card card-md">
<div class="card-body">
<h2 class="card-title text-center mb-4">パスワードの再設定手順を送信します</h2>
<p class="text-muted mb-4">ご登録のメールアドレスを入力してください。<br>パスワード再設定のためのリンクを記載したメールを送信します。</p>
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
<%= render "admin/shared/error_messages", resource: resource %>
<div class="mb-3">
<%= f.label :email, class: "form-label" %>
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control", required: true %>
</div>
<div class="form-footer">
<%= f.submit t('.send_me_reset_password_instructions', default: "再設定メールを送信"), class: "btn btn-primary w-100" %>
</div>
<% end %>
</div>
</div>
<div class="text-center text-muted mt-3">
<%= render "admin/shared/links" %>
</div>
</div>

メールアドレスを入力して再設定メールを送信すると、ログに以下のようなメール送信内容が出力されます。
記載されているパスワード変更画面URLにアクセスします。
<p>Someone has requested a link to change your password. You can do this through the link below.</p>
<p><a href="http://localhost:3000/admin/password/edit?reset_password_token=xxxxx">Change my password</a></p>
パスワード変更画面
app/views/admin/passwords/edit.html.erb
にデザインを適用を適用します。
<div class="container container-tight py-4">
<div class="text-center mb-4">
<h1 class="h3 mb-3 fw-normal">新しいパスワードを設定</h1>
</div>
<div class="card card-md">
<div class="card-body">
<h2 class="card-title text-center mb-4">新しいパスワードを入力してください</h2>
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %>
<%= render "admin/shared/error_messages", resource: resource %>
<%= f.hidden_field :reset_password_token %>
<div class="mb-3">
<%= f.label :password, t(".new_password", default: "新しいパスワード"), class: "form-label" %>
<%= f.password_field :password, autofocus: true, autocomplete: "new-password", class: "form-control", required: true %>
</div>
<div class="mb-3">
<%= f.label :password_confirmation, t(".confirm_new_password", default: "新しいパスワード(確認)"), class: "form-label" %>
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: "form-control", required: true %>
</div>
<div class="form-footer">
<%= f.submit t(".change_my_password", default: "パスワードを変更する"), class: "btn btn-primary w-100" %>
</div>
<% end %>
</div>
</div>
<div class="text-center text-muted mt-3">
<%= render "admin/shared/links" %>
</div>
</div>

Step 7: 最終動作確認
すべてのカスタマイズが完了したら、動作を最終確認しましょう。
【未ログイン状態での確認】
アクセス | 期待する結果 |
---|---|
サインイン ( /admin/sign_in ) | 認証専用レイアウトが適用されていること。 Tablerデザインのログインフォームが表示されていること。 ログイン後、ダッシュボード( /admin )にリダイレクトされること。 |
サインアップ ( /admin/sign_up ) | 認証専用レイアウトが適用されていること。 Tablerデザインのサインアップフォームが表示されていること。 登録後、ダッシュボード( /admin )にリダイレクトされること。 |
パスワードリセット ( /admin/password/new ) | 認証専用レイアウトが適用されていること。 Tablerデザインのパスワードリセット要求フォームが表示されていること。 送信後、サインイン( /sign_in )にリダイレクトされること。 |
パスワード変更 ( /admin/password/edit ) | 認証専用レイアウトが適用されていること。 Tablerデザインのパスワード設定フォームが表示されていること。 送信後、ダッシュボード( /admin )にリダイレクトされること。 |
ログイン必須ページ ( /admin ) | サインイン画面(/admin/sign_in )にリダイレクトされること。 |
サインアップ/ログインエラー (パスワード不一致) | 認証専用レイアウトが適用されていること。 Tablerデザインのエラーメッセージが表示されていること。 |
【ログイン状態での確認】
アクセス | 期待する結果 |
---|---|
サインイン ( /admin/sign_in ) | /admin (ダッシュボード) へリダイレクトされることTablerデザインのエラーメッセージが表示されていること。 |
サインアップ ( /admin/sign_up ) | /admin (ダッシュボード) へリダイレクトされることTablerデザインのエラーメッセージが表示されていること。 |
パスワードリセット ( /admin/password/new ) | 認証専用レイアウトが適用されていること。 Tablerデザインのパスワードリセット要求フォームが表示されていること。 |
ログイン必須ページ(/admin ) | ダッシュボード(/admin )が表示されること |
ログアウト | サインイン(/admin/sign_in )にリダイレクトされること。 |
すべてが意図通りに動作し、見た目も統一されていれば完了です!
まとめと次回予告
第3回では、Deviseの認証プロセスを管理画面の入り口である /admin
に統合し、認証画面自体にも管理画面のTablerレイアウトを適用しました。
さらに、Deviseコントローラーとビューを admin/
配下に配置し、主要なフォーム等をTablerデザインにカスタマイズしました。
これで、デザイン面でも一貫性のある、使いやすい認証機能が完成しました。
これでDeviseの基本的な導入とカスタマイズは一通り完了です。
次回(第4回または応用編) は、メールアドレス確認 (confirmable
) やアカウントロック (lockable
) など、Deviseのより高度な機能の導入について見ていく予定です。
Deviseは非常に多機能なので、ぜひドキュメントなども参照しながら、あなたのアプリケーションに必要な機能を実装していってください。