0% found this document useful (0 votes)
8 views24 pages

Core Authentication Module (Weeks 2-3)

The document outlines the development of a Core Authentication Module using Ruby on Rails, focusing on user login, registration, and JWT-based API authentication. It emphasizes code quality, test coverage, and productivity, while detailing coding standards, PR review guidelines, deployment practices, and CRUD operations for user and article management. Key technologies include Devise, Doorkeeper, Redis, and Docker, with a structured approach to implementing features and ensuring security and performance.

Uploaded by

duybao.vn
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
8 views24 pages

Core Authentication Module (Weeks 2-3)

The document outlines the development of a Core Authentication Module using Ruby on Rails, focusing on user login, registration, and JWT-based API authentication. It emphasizes code quality, test coverage, and productivity, while detailing coding standards, PR review guidelines, deployment practices, and CRUD operations for user and article management. Key technologies include Devise, Doorkeeper, Redis, and Docker, with a structured approach to implementing features and ensuring security and performance.

Uploaded by

duybao.vn
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 24

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:

You might also like