認証・API・テスト
Ruby on Rails
Devise・JWT・RSpec・FactoryBot
API モード
APIコントローラー・シリアライザー・JWT認証
# API モード(ActionController::API を継承)
module Api
module V1
class PostsController < ApplicationController
before_action :authenticate_jwt!
before_action :set_post, only: [:show, :update, :destroy]
def index
posts = Post.published.recent.page(params[:page])
render json: {
data: ActiveModelSerializers::SerializableResource.new(posts),
meta: pagination_meta(posts)
}
end
def show
render json: PostSerializer.new(@post).as_json
end
def create
post = current_user.posts.build(post_params)
if post.save
render json: PostSerializer.new(post), status: :created
else
render json: { errors: post.errors.full_messages }, status: :unprocessable_entity
end
end
private
def set_post = @post = Post.find(params[:id])
def post_params = params.require(:post).permit(:title, :body, :status)
end
end
end
# JWT 認証
class AuthController < ApplicationController
skip_before_action :authenticate_jwt!, only: [:create]
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password]) # has_secure_password
token = JWT.encode(
{ user_id: user.id, exp: 24.hours.from_now.to_i },
Rails.application.credentials.jwt_secret
)
render json: { token: token, user: UserSerializer.new(user) }
else
render json: { error: '認証失敗' }, status: :unauthorized
end
end
end
# application_controller.rb
def authenticate_jwt!
header = request.headers['Authorization']
token = header&.split(' ')&.last
payload = JWT.decode(token, Rails.application.credentials.jwt_secret).first
@current_user = User.find(payload['user_id'])
rescue JWT::DecodeError, ActiveRecord::RecordNotFound
render json: { error: '認証が必要です' }, status: :unauthorized
endDevise 認証
インストール・カスタマイズ・ヘルパー
# Gemfile
# gem 'devise'
bundle install
rails generate devise:install
rails generate devise User
rails generate devise:views # ビューをカスタマイズする場合
rails db:migrate
# 生成されるルート
# GET /users/sign_up → registrations#new
# POST /users/sign_up → registrations#create
# GET /users/sign_in → sessions#new
# POST /users/sign_in → sessions#create
# DELETE /users/sign_out → sessions#destroy# モデル設定
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:confirmable, :lockable, :trackable,
:omniauthable, omniauth_providers: [:google_oauth2]
end
# コントローラーで使用できるヘルパー
authenticate_user! # 未ログインをリダイレクト
current_user # 現在のユーザー
user_signed_in? # ログイン状態の確認
# ビューでのリンク
# link_to 'ログイン', new_user_session_path
# link_to 'ログアウト', destroy_user_session_path, method: :delete
# link_to '登録', new_user_registration_path
# ApplicationController でのカスタマイズ
class ApplicationController < ActionController::Base
before_action :authenticate_user!
def after_sign_in_path_for(resource)
stored_location_for(resource) || dashboard_path
end
def after_sign_out_path_for(resource_or_scope)
root_path
end
end
# カスタムフィールドの追加
class Users::RegistrationsController < Devise::RegistrationsController
private
def sign_up_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
def account_update_params
params.require(:user).permit(:name, :email, :password, :password_confirmation, :current_password)
end
endRSpec と FactoryBot
モデル・リクエストスペック・FactoryBot
# FactoryBot の定義
FactoryBot.define do
factory :user do
name { Faker::Name.name }
email { Faker::Internet.unique.email }
password { 'password123' }
role { 'user' }
active { true }
# トレイト(バリエーション)
trait :admin do
role { 'admin' }
end
trait :inactive do
active { false }
end
# 関連
trait :with_posts do
after(:create) do |user|
create_list(:post, 3, user: user)
end
end
end
factory :post do
title { Faker::Lorem.sentence }
body { Faker::Lorem.paragraphs(number: 3).join('\n') }
status { 'published' }
association :user
trait :draft do
status { 'draft' }
end
end
endrequire 'rails_helper'
RSpec.describe 'Posts API', type: :request do
let(:user) { create(:user) }
let(:post) { create(:post, user: user) }
let(:headers) { { 'Authorization' => "Bearer #{jwt_token(user)}" } }
describe 'GET /api/v1/posts' do
before { create_list(:post, 3, :published) }
it 'returns published posts' do
get '/api/v1/posts', headers: headers
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['data'].length).to eq(3)
end
end
describe 'POST /api/v1/posts' do
let(:params) { { post: { title: 'New Post', body: 'Content', status: 'published' } } }
context 'with valid params' do
it 'creates a post' do
expect {
post '/api/v1/posts', params: params, headers: headers
}.to change(Post, :count).by(1)
expect(response).to have_http_status(:created)
end
end
context 'with invalid params' do
it 'returns errors' do
post '/api/v1/posts',
params: { post: { title: '' } },
headers: headers
expect(response).to have_http_status(:unprocessable_entity)
expect(JSON.parse(response.body)['errors']).to be_present
end
end
end
end