Hello! In this blog I would like to write about my most recent bootcamp project, a full stack application that manages events. This will be a brief introduction of the backbone of my project, and I’d like to emphasize a bit more on what I thought was most confusing - user authentication and validations.
A basic explanation of what this application does is that users may sign up and log in, and create events or browse events that other users have created. A user may view a specific event, edit it, or make a registration.
Repository: https://github.com/fusion407/event-management-system
Deployment: https://event-management-system-qmse.onrender.com/
The 3 models I have created are Users, Registrations, and Events.
The relationship of these models may be visualized like so:
Users * → 1- Registrations -1 ← * Events
Users and Events share a many-to-one relationship to registration, which will be the join table carrying the two foreign keys from users and events. As a result, Users and Events have a many-to-many relationship through the registration model. This means that Users can have many registrations, and many Events. Events can have many registrations and many users. But Registrations can only have a single user and event. Here is an example of my models with their associations.
class Event < ApplicationRecord
has_many :registrations
has_many :users, through: :registrations
class Registration < ApplicationRecord
belongs_to :user
belongs_to :event
class User < ApplicationRecord
has_many :registrations
has_many :events, through: :registrations
has_secure_password
Once I have finished writing my migrations, here is what my schema is going to look like.
create_table "events", force: :cascade do |t|
t.string "title"
t.string "description"
t.string "location"
t.string "start_date"
t.string "end_date"
t.string "created_by"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "registrations", force: :cascade do |t|
t.integer "user_id"
t.integer "event_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.integer "participants"
end
create_table "users", force: :cascade do |t|
t.string "username"
t.string "password_digest"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
In my events table I have all the basic parameters for an event, such as title, description, location, date, and created by. My registration has the two foreign keys from the other models, user_id, and event_id. There is also a participants column that carries an integer that is used to express the number of ‘attendees’ for that user's registration. Finally, the users table will carry the username of the user, and the password_digest - for when a user signs up or logs in the application.
Now that I have my schema, it’s time to make serializers that will control what attributes I would like to share, since I obviously don’t need to include information like password_digest and the created_at / updated_at in all of my models.
class EventSerializer < ActiveModel::Serializer
attributes :id, :title, :description, :location, :start_date, :end_date, :created_by
has_many :users
has_many :registrations
end
class RegistrationSerializer < ActiveModel::Serializer
attributes :id, :user_id, :event_id, :participants, :created_at
has_one :user
has_one :event
end
class UserSerializer < ActiveModel::Serializer
attributes :id, :username
has_many :registrations
has_many :events
end
Now it’s time to write my controllers and give my application some authorization. I am going to need a controller for each of my models, and I will need a sessions controller that will be used to authenticate a user and use cookies in order to keep that user in session whenever they close or refresh the page so they may be automatically logged in when they return.
Before I do all that, let me show show you my application_controller which will show how I manage my errors and user authorization.
class ApplicationController < ActionController::API
include ActionController::Cookies
rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity_response
before_action :authorize
private
def authorize
@current_user = User.find_by(id: session[:user_id])
render json: { errors: ["Not authorized"] }, status: :unauthorized unless @current_user
end
def render_unprocessable_entity_response(exception)
render json: { errors: exception.record.errors.full_messages }, status: :unprocessable_entity
end
end
What this basically says is before any actions are made, the authorize function is going to run and identify the session of the current user. If no session is found the json will render “Not authorized”. The other function render_unprocessable_entity_response is used when my other models run into an error with validations, like if a certain parameter is not present, it will return the error response.
The rules for these validations will go into the model for which you want to write the validation for, here is an example from my events model and the validations I’ve written for it:
validates :title, :description, :location, :start_date, :end_date, presence: true
validates :title, uniqueness: true
So when a user is creating a new event, the server is going to check if these validations have passed or not, so what my validations say is that each parameter must be present, and the title must be unique - meaning the title can’t be the same as any other title in the database.
Back to the controllers. BUT before I do that, you need to understand what routes that are going to be used. Here is a snippet of my routes.rb file:
Rails.application.routes.draw do
resources :events
resources :registrations
get "/users", to: "users#show"
post "/login", to: "sessions#create"
delete "/logout", to: "sessions#destroy"
post "/signup", to: "users#create"
get "/me", to: "users#show"
get "/me/registrations", to: "users#showMyRegs"
end
In short, the events and registrations have ‘resources’ for each route for full CRUD implementation ready to be coded. Each route after that is made explicitly through the sessions and users model because full CRUD is not needed for those models.
How does a user create their account? Here is the code from my users_controller for the create route.
class UsersController < ApplicationController
skip_before_action :authorize, only: [:create]
def create
user = User.create(user_params)
if user.valid?
session[:user_id] = user.id
render json: user, status: :created
else
render json: { error: user.errors.full_messages }, status: :unprocessable_entity
end
end
What this will do is create the new User, check if that user passes its validations and then cookies are used to make a session using the id of the newly created user, and then JSON returns the data for the new user.
Ok, so now how does a user log in to an account that's already created? This will go down in the sessions_controller using the same route ‘create’.
class SessionsController < ApplicationController
skip_before_action :authorize, only: [:create]
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
Except instead of a new user being created, the server must find the username through the database and then it needs to authenticate it with its password. If this authentication succeeds and the user has input the correct username and password, a session will be created for that user and rendering the json for that user, else it will return an error saying that the user has input an “Invalid username or password”.
When a user wishes to log out, within the sessions controller the destroy method is called:
def destroy
session.delete :user_id
head :no_content
end
That will delete the session entirely.
This is all I would like to write for today, the rest of my code is mostly writing basic CRUD for my other models, feel free to check out my repository. Thank you for reading, happy coding!
Top comments (0)