Rails: Resources for a Secure Database
Tags: ruby, rails, javascript
Published: 2022-11-30 18:35:44

While building my first full stack application with rails, I learned several new skills about working with a database. This includes how to utilize gems and ruby on rails to keep the database secure. Below are my notes about the benefits of strong params, validating data, serializers, error handling, password encryption and cookies. 

All code examples are from my [BookClub](https://github.com/JordanTaylorJ/book_club) application, built as a space to share and discuss favorite books. 

# Strong Params

In the controllers I used strong params to define the attributes the controller expects to receive from the frontend. I created a private method of book_params to pass to methods within the controller. 
```  
def book_params
    params.permit(:title, :author, :image)
end 
```
With the following create action `Book.create(book_params)`, a hash containing the attribute `:person` would return the error: `Unpermitted parameters: :person`. By only permitting the specified attributes, we can safely use the book_params hash for mass assignment. 

# Validations 

I utilized the built in Ruby validator in my models to protect the database from invalid data. This consists of the `validates` keyword, an attribute, and a hash of options. 
```
validates :username, presence: true, uniqueness: true
validates :password, presence: true, length: {in: 3..20}
```
These validations in the User model ensure that there are no duplicated usernames and that a password's length is between three and twenty characters. 

* Active Record validates will only be checked when adding or updating data via Ruby/Rails. The validations will not run with SQL code via the command line. `#valid?` is the only way to trigger validation without touching the database.

# Serializing Associations 

The book_serializer.rb defines which attributes are included in the response data. The data for each book is nested three layers deep: 
```
{
    "id": 96,
    "title": "the Sun and Her Flowers",
    "author": "Rupi Kaur",
    "image": "https://images.unsplash.com/photo-1545239351-cefa43af60f3?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=735&q=80",
    "reviews": [
      {
        "id": 261,
        "user_id": 83,
        "book_id": 96,
        "comment": "Impedit quia ex sunt.",
        "favorite": false,
        "user": {
          "id": 83,
          "username": "Ringo"
        }
      }
    ]
}
```
I utilize macros as well as the include keyword to get all three layers in our response data in order to display the has_many through relationship. 

I did not include the timestamps created_at and updated_at, as the frontend will not require this information. The has_many macro is used to attach the reviews to the associated books.
``` 
class BookSerializer < ActiveModel::Serializer
  attributes :id, :title, :author, :image, :reviews
  has_many :reviews 
end
```
By default, AMS only nests associations one level deep. In order to override this behavior, I used the `include` keyword in books_controller.rb show method:
```
render json: books, include: 'reviews.user', status: :ok
```

# React Error Handling

To make use of the errors from Ruby, they are rendered to the user in the frontend. All components with a POST and/or PATCH request have errors kept in state 
``` const [errors, setErrors] = useState([]); ```
After a fetch, we check the response status. If it's okay, we can use the response as intended, otherwise, use setErrors with the error response to display the data. 
```
        .then((r) => {
            if (r.ok){
                r.json().then((r) => setUser(r))
                setErrors([])
            } else {
                r.json().then((r) => setErrors(r.errors))
            }
        })
```
In the example below, the validation errors are displayed conditionally when invalid data is used to try to create a new user account.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1zc68vh7iqyl2y040f8l.png)

# Password Encryption 

The macro `has_secure_password` comes from the bcrypt gem. It's used to hash and salt all passwords for security. 
This gem requires the users table to have the column `password_digest`. The macro is added to the User model.

# Cookies and Sessions

User authentication allows the application to keep track of who is or is not logged in. This is done by setting the user's username to a session hash when the user logs in. 

First, we install middleware in our config/application.rb file:
``` 
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore
config.action_dispatch.cookies_same_site_protection = :strict
```

In the application controller, I included Cookies so that the rest of our controllers can access them. I also defined the method :authorized and set it as a before_action.
```
class ApplicationController < ActionController::API
  include ActionController::Cookies

  before_action :authorized 

  def authorized 
    return render json: {error: "Not Authorized"}, status: 
    :unauthorized unless session.include? :user_id
  end
end
```
This authorized method checks sessions to see if a user is currently logged in. The before_action will run the authorized method before every request to check that a user is logged in before proceeding. 

Using rails generator, I created a sessions_controller `rails g controller Session`. Here, I define create and destroy actions for logging in and logging out. 

The destroy (logout) is straight forward, simply removing the user_id from session, but the create (login) action needs to be authenticated by a password. The code `user&.authenticate(params[:password])` checks that a user was found and the password in params matches what is saved in the database for that user. 
```
 def create
        user = User.find_by(username: params[:username])
        if user&.authenticate(params[:password])
            session[:user_id] = user.id 
            render json: user, status: :created
        else 
            render json: {error: "Invalid username or 
            password"}, status: :unauthorized 
        end 
    end
```
If the user is found, we save the `user.id` to the sessions hash. 

*** Because the authorized before action was placed in application_controller.rb, it is inherited by all the other controllers. In the sessions_controller.rb, we have to remember to skip the :authorized action for our create (login) route. We can skip :authorized any time we don't need to check that a user is logged in before proceeding with an action. 
`skip_before_action :authorized, only: :create `


I hope these notes give you some insight as to how rails can be used to keep data secure and help you in the creation of your own database. 

Happy Coding!