Core Authentication Module
Overview
The objective is to build a robust authentication module that covers user login, registration,
and JWT-based API authentication. The focus areas include:
• Code Quality: Writing clean, maintainable, and well-structured code.
• Test Coverage: Ensuring a minimum of 70% code coverage through comprehensive
unit and integration tests.
• Productivity: Implementing efficient development practices and leveraging proven
libraries and tools.
Key technologies and tools 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.
Ruby on Rails Coding Standards and PR Review Guidelines
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
General Review Checklist
• Code follows DDD structure and principles.
• Follows naming conventions.
• No business logic in controllers or background jobs.
• Code is well tested (RSpec/FactoryBot).
• Methods are small and have a single responsibility.
Security Review Checklist
• Uses parameterized queries to prevent SQL injection.
• No sensitive data exposed.
• Implements role-based access control (RBAC).
Performance Review Checklist
• Avoids N+1 queries.
• Indexes are added to frequently queried columns.
• Uses caching for frequently accessed data.
Deployment and CI/CD Guidelines
• Use GitHub Actions/GitLab CI for automated testing.
• Ensure migrations are reviewed before merging.
• Use feature flags for risky deployments.
• Maintain backward compatibility in API changes.
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
• Ruby (>= 3.0.0)
• Bundler (gem install bundler)
• Docker & Docker Compose (if using containers)
Local Setup
1. Clone the repository
git clone https://github.com/your-repo/companyframework.git
cd companyframework
2. Install dependencies
bundle install
3. Set up the database
rails db:setup
4. Start the Rails server
rails server
The application will be available at http://localhost:3000.
Docker Setup
1. Build the Docker image
docker-compose build
2. Run the application
docker-compose up -d
3. Check running containers
docker ps
Running Tests
To run tests, use:
rails test
rubocop
Deployment
Deploying with Docker
1. Build the production image
docker build -t companyframework:latest .
2. Run the container
docker run -p 3000:3000 companyframework:latest
Deploying to a Cloud Provider
• Configure your environment variables.
• Use AWS for deployment.
• Ensure you set RAILS_ENV=production before running migrations.
Example CRUD:
• Service objects for business logic
• Form objects for input validation
• Policies for authorization
• Error handling for standardized exceptions
• Transaction handling for data integrity
1. Controller Structure
The controller is placed inside the Api::V1 namespace, which helps organize API versions.
module Api
module V1
class UsersController < ApplicationController
• ApplicationController: The base controller where common behaviors (such as
authentication and error handling) are defined.
• Namespacing (Api::V1): This ensures that the API can support multiple versions.
2. Callbacks (before_action)
Before executing the main actions, before_action callbacks ensure that:
• Authorization is checked before processing a request.
• The user is fetched from the database before performing show or destroy.
before_action :authorize_user
before_action :set_user, only: [ :show, :destroy ]
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
Now, let's go through the actual CRUD operations:
3.1 Index (GET /users)
def index
@presenters = user_presenter
return render "users/index" if @presenters.users.present?
raise ::Error::User::Read::UserNotFound
end
• Calls user_presenter to fetch users based on search parameters.
• If users exist, it renders the index view.
• If no users are found, it raises a UserNotFound error.
3.2 Show (GET /users/:id)
def show
return render "users/show" if @user.present?
raise ::Error::User::Read::UserNotFound
end
• Uses set_user callback to fetch the user.
• If the user exists, it renders the show view.
• If the user doesn’t exist, it raises UserNotFound.
3.3 Create (POST /users)
def create
form = UserForm.new(user_params)
return render_success(:created) if transaction(-> {
User::CreateService.call(form, current_user)
})
raise Error::User::Write::UserCreationFailed
end
• Validates input using UserForm.
• Uses User::CreateService to handle user creation logic.
• Runs within a transaction to ensure database integrity.
• If creation succeeds, returns an HTTP 201 Created status.
• If creation fails, raises UserCreationFailed.
3.4 Destroy (DELETE /users/:id)
def destroy
return render_success(:no_content) if transaction(-> {
@user.destroy
})
raise Error::User::Write::UserDeletionFailed
end
• Uses set_user to fetch the user.
• Calls destroy on the user within a transaction to ensure rollback if needed.
• If deletion succeeds, returns 204 No Content.
• If deletion fails, raises UserDeletionFailed.
4. Private Helper Methods
These methods keep the controller clean by abstracting logic.
4.1 Fetch Users (Presenter)
def user_presenter
::Users::IndexPresenter.new(search)
end
• Uses Users::IndexPresenter to encapsulate logic for fetching and structuring user
data.
4.2 Strong Parameters
def user_params
params.require(:user).permit(:email, :password, :password_confirmation, :api_id)
end
• Prevents mass assignment vulnerabilities by whitelisting allowed parameters.
4.3 Authorization
def authorize_user
authorize @current_user # This checks `Policy#method?`
end
• Uses Pundit (or a custom authorization layer) to enforce permissions.
4.4 Searching Parameters
def search
params.permit(:search_text, :page)
end
• Filters search queries based on search_text and page.
4.5 Find User Before Show/Delete
def set_user
@user = User.find(params[:id])
end
• 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:
Create a new Article model:
rails generate model Article title:string content:text user:references
To apply new migration, we need to restart app container by a command:
docker restart companyframework-app-1
Create a new controller in API in V1:
rails generate controller Api::V1::Articles
# companyframework/app/controllers/api/v1/articles_controller.rb
class Api::V1::ArticlesController < ApplicationController
Add article routes into routes.rb
# companyframework/config/routes.rb
resources :articles, only: [ :index, :show, :create, :update, :destroy ]
Implement authorization for article:
rails generate pundit:policy article
#/companyframework/app/policies/article_policy.rb
class ArticlePolicy < ApplicationPolicy
ERROR_CREATE = Error::Authorize::Permission::NotAllowedCreate
ERROR_READ = Error::Authorize::Permission::NotAllowedRead
ERROR_UPDATE = Error::Authorize::Permission::NotAllowedUpdate
ERROR_DELETE = Error::Authorize::Permission::NotAllowedDelete
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
class Scope < ApplicationPolicy::Scope
# def resolve
# if user.admin?
# scope.all
# else
# scope.where(user_id: user.id)
# end
# end
end
private
def check_permission!(permission, error)
raise error unless user.has_permission?("user_management", permission)
true
end
end
To add authorize function:
# companyframework/app/controllers/api/v1/articles_controller.rb
before_action :authorize_user
def authorize_user
authorize @current_user # This checks `Policy#method?`
end
Implement the Article Creation:
# 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
To implement create, we need to create ArticleForm Class, article_params function,
Article::CreateService class, Error::Article::Write::ArticleCreationFailed class as the
User Controller
Tip: clone the User’s workflow as Article’s workflow
Implement ArticleForm Class:
# companyframework/app/forms/article_form.rb
class ArticleForm < FormBase
attr_accessor :title, :content
validates :title, presence: true
validates :content, presence: true
def initialize(params = {})
super(params)
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
Implement Article::CreateService class
# typed: false
# frozen_string_literal: true
# companyframework/app/services/article/create_service.rb
class Article::CreateService < ApplicationService
private
def initialize(form, current_user)
@form = form
@current_user = current_user
end
def call
create_article!
end
def create_article!
Article.create!(
@form.form_attrs.merge(user_id: @current_user.id)
)
end
end
Implement Error::Article::Write::ArticleCreationFailed class
# 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:
Implement index action in articles controller
# 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
To implement create, we need to create Articles::IndexPresenter Class,
article_presenter function, ArticleFinder class, Error::Article::Read::ArticleNotFound
class as the User Controller
Implement article_presenter function
# companyframework/app/controllers/api/v1/articles_controller.rb
def article_presenter
::Articles::IndexPresenter.new(search)
end
Implement article views jbuilder
# companyframework/app/views/articles/_article.json.jbuilder
json.id article.id
json.title article.title
json.content article.content
json.created_at format_timestamp(article.created_at)
json.updated_at format_timestamp(article.updated_at)
# companyframework/app/views/articles/index.json.jbuilder
json.success "success"
json.data do
json.articles @presenters.articles, partial: "articles/article", as: :article
end
Update article model
# companyframework/app/models/article.rb
class Article < ApplicationRecord
belongs_to :user
validates :title, presence: true
validates :content, presence: true
scope :search_like, lambda { |search|
where(arel_table[:title].matches("%#{search}%"))
.or(Article.where(arel_table[:content].matches("%#{search}%")))
}
end
Implement ArticleFinder class
# companyframework/app/finders/article_finder.rb
Class ArticleFinder < ApplicationFinder
model Article
attribute :search_text
rule :search_cond, if: -> { search_text.present? }
def search_cond
model.search_like(search_text)
end
end
Implement Articles::IndexPresenter class
# 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
Implement Error::Article::Read::ArticleNotFound class
# 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
Implement Json as Jbuilder
# 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
Implement destroy action of controller
# companyframework/app/controllers/api/v1/articles_controller.rb
def destroy
return render_success(:no_content) if transaction(-> {
@article.destroy
})
raise Error::Article::Write::ArticleDeletionFailed
end
Result:
Update an article:
# 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: