Core Authentication Module (Weeks 2-3)
Core Authentication Module (Weeks 2-3)
Overview
The objective is to build a robust authentication module that covers user login, registration,
and JWT-based API authentication. The focus areas include:
• Devise and bcrypt for user model management and password encryption.
• Doorkeeper for integrating JWT-based API authentication.
• Redis for securely storing and managing user sessions and tokens.
• Comprehensive documentation to facilitate maintenance and onboarding.
General Principles
• Follow MVC to ensure a clear separation of concerns.
• Maintain high cohesion and loose coupling between components.
• Use SOLID principles to enhance maintainability and testability.
• Ensure consistency across the codebase.
• Write self-documenting code with meaningful names.
Project Structure
First, we need to read the official tutorial to gain a basic understanding of Ruby on Rails:
https://guides.rubyonrails.org/getting_started.html
Naming Conventions
• Use snake_case for file names and method names.
• Use PascalCase for class names.
• Use UPPER_CASE for constants.
• Use explicit names for variables and avoid abbreviations.
PR Review Guidelines
Features
• Rails 8 - The latest stable version of Ruby on Rails.
• SQLite - Default database for development.
• Puma - High-performance web server.
• Turbo & Stimulus - Enhance frontend interactivity.
• Jbuilder - JSON API support.
• Dockerized Setup - Easy deployment with Docker and Docker Compose.
Installation
Prerequisites
Local Setup
2. Install dependencies
bundle install
rails db:setup
rails server
Docker Setup
1. Build the Docker image
docker-compose build
docker-compose up -d
docker ps
Running Tests
To run tests, use:
rails test
rubocop
Deployment
Example CRUD:
The controller is placed inside the Api::V1 namespace, which helps organize API versions.
module Api
module V1
class UsersController < ApplicationController
2. Callbacks (before_action)
Callback Explanations:
• authorize_user: Ensures that the current user has permission to perform the
requested action.
• set_user: Finds the user from the database when accessing show or destroy.
3. CRUD Actions
• Ensures that the user is retrieved before executing actions that need a user
instance.
5. Summary
HTTP Method Path Action Description
Fetch all users with search
GET /users index
filters
GET /users/:id show Retrieve a specific user
POST /users create Create a new user
PUT /users/:id update Update a user
DELETE /users/:id destroy Delete a user
How to implement CRUD for The Article:
# companyframework/app/controllers/api/v1/articles_controller.rb
class Api::V1::ArticlesController < ApplicationController
def index?
check_permission!(Policy::USER_MANAGEMENT_PERMISSIONS[:read], ERROR_READ)
end
def show?
check_permission!(Policy::USER_MANAGEMENT_PERMISSIONS[:read], ERROR_READ)
end
def create?
check_permission!(Policy::USER_MANAGEMENT_PERMISSIONS[:write], ERROR_CREATE)
end
def update?
check_permission!(Policy::USER_MANAGEMENT_PERMISSIONS[:write], ERROR_UPDATE)
end
def destroy?
check_permission!(Policy::USER_MANAGEMENT_PERMISSIONS[:delete], ERROR_DELETE)
end
private
def authorize_user
authorize @current_user # This checks `Policy#method?`
end
# companyframework/app/controllers/api/v1/articles_controller.rb
def create
form = ArticleForm.new(article_params)
return render_success(:created) if transaction(-> {
Article::CreateService.call(form, current_user)
})
raise Error::Article::Write::ArticleCreationFailed
end
def form_attrs
{
title: @title,
content: @content,
}
end
end
# companyframework/app/controllers/api/v1/articles_controller.rb
def article_params
params.require(:article).permit(:title, :content)
end
def call
create_article!
end
def create_article!
Article.create!(
@form.form_attrs.merge(user_id: @current_user.id)
)
end
end
# companyframework/app/commons/error/article/base.rb
module Error
module Article
class Base < Error::Base
attr_reader :status, :error, :message
def initialize(_error = nil, _status = nil, _message = nil)
@error = _error || :standard_error
@status = _status || :service_unavailable
@message = _message || "User service unavailable"
end
end
end
end
# companyframework/app/commons/error/article/write.rb
module Error
module Article::Write
class ArticleCreationFailed < Base
def initialize
super(:article_creation_failed, :bad_request, "Article is invalid")
end
end
end
end
The result:
Retrieve all articles with pagination:
# companyframework/app/controllers/api/v1/articles_controller.rb
def index
@presenters = article_presenter
return render "articles/index" if @presenters.articles.present?
raise ::Error::Article::Read::ArticleNotFound
end
# companyframework/app/views/articles/index.json.jbuilder
json.success "success"
json.data do
json.articles @presenters.articles, partial: "articles/article", as: :article
end
# companyframework/app/models/article.rb
class Article < ApplicationRecord
belongs_to :user
# companyframework/app/finders/article_finder.rb
Class ArticleFinder < ApplicationFinder
model Article
attribute :search_text
# companyframework/app/presenters/articles/index_presenter.rb
module Articles
class IndexPresenter < ApplicationPresenter
attribute :search_text
attribute :page
PER_PAGE = 30
def articles
@articles ||= model_finder.paginate(page: page || FIRST_PAGE, per_page: PER_PAGE)
end
def model_finder
ArticleFinder.call(search_text:)
end
end
end
# companyframework/app/commons/error/article/read.rb
module Error
module Article::Read
class ArticleNotFound < Base
def initialize
super(:article_not_found, :not_found, "Article not found")
end
end
end
end
Show an article:
Implement show action of controller
# companyframework/app/controllers/api/v1/articles_controller.rb
def show
return render "articles/show" if @article.present?
raise ::Error::User::Read::UserNotFound
end
def set_article
@article = Article.find(params[:id])
end
# companyframework/app/views/articles/show.json.jbuilder
json.success "success"
json.data do
json.partial! "articles/article", user: @article
end
Result:
Destroy an article:
Implement Error Body when destroy unsuccessfully
# companyframework/app/commons/error/article/write.rb
class ArticleDeletionFailed < Base
def initialize
super(:article_deletion_failed, :bad_request, "Destroy Article failed")
end
end
# companyframework/app/controllers/api/v1/articles_controller.rb
def update
form = ArticleForm.new(article_params)
return render_success(:created) if transaction(-> {
Article::UpdateService.call(@article, form, current_user)
})
raise Error::Article::Write::ArticleCreationFailed
end
# companyframework/app/services/article/update_service.rb
class Article::UpdateService < ApplicationService
private
def initialize(article, form, current_user)
@article = article
@form = form
@current_user = current_user
end
def call
create_article!
end
def create_article!
@article.update!(
@form.form_attrs
)
end
end
# companyframework/app/commons/error/article/write.rb
class ArticleUpdateFailed < Base
def initialize
super(:article_update_failed, :bad_request, "Update Article failed")
end
end
Result: