A Sinatra Web App for Boulderers
My Sinatra-powered web app — Bolderer, is designed for boulderers who are eager to track their climbing progress, strengths and weaknesses by logging routes/ problems that they have climbed. They can also see others' profiles since bouldering is a personal yet social sport, where the climbing community is supportive and would always cheer for your 'sends'.
I started my project by mapping out the models and their relationships with one another. This helped me plan the models, database tables, and the has_many
, belongs_to
and many-to-many
relationships on the models.
Bolderer Association Diagram:
Key Concepts
I would like to highlight some key building blocks of my Sinatra project — MVC, Sessions, CRUD Actions, and RESTful Routes, which have allowed me to create a functional app and taught me the base concepts of a Content Management System (CMS).
Model-View-Controller (MVC)
An organized way to build frameworks for web apps.
I found this immensely useful in helping me structure my code. It provided a separation of concerns by grouping my files by functionalities -- Models (logic, where data is manipulated and saved), Views (front-end), and Controllers (middleman - relays data from browser to app, and from app to the browser).
My MVC directories:
User Password Encryption with BCrypt
I used the Ruby Gem BCrypt to secure the users' data by encrypting their passwords. It allowed my app to sign up and log-in a user with a secure password.
Implementing Bcrypt by adding the password column as pasword_digest
in my users
table:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :username
t.string :password_digest
end
end
end
I also added ActiveRecord's macro has_secure_password
to my User model, which let me access the user's password
attribute even though the users database only had the column called password_digest
.
The macro also provided another useful method, authenticate
:
post '/login' do
user = User.find_by(username: params[:username])
if user && user.authenticate(params[:password])
#storing user_id key in session hash
session[:user_id] = user.id
redirect "/users/#{user.id}"
else
@error = true
erb :"/users/login"
end
end
The User's authenticate
method turned the user's password input into a hash that would get compared with the hashed password stored in the database. If the user authenticated, the method would return the User instance; otherwise, it would return false
. This was helpful for logging in users.
Sessions and User Authentication and Authorization
- Sessions were used to verify a user's identity, as the users were requested to verify their identity by logging in with valid credentials
-
Hence, I used sessions to filter and dictate what a user could see or edit:
- The users could only view the Problems index page if they were logged in
- The users could only edit/ delete the resources (problems) that they created
To use sessions in Sinatra, I enabled my application to use the
sessions
keyword to access the session hash:
configure do
set :public_folder, 'public'
set :views, 'app/views'
enable :sessions
set :session_secret, "session_encryption"
end
- When a user signs in successfuly, the
user_id
key would get stored in the session hash:session[:user_id] = user.id
.
Defining RESTful routes
Another key lesson was learning to use restful routes to implement CRUD actions. I realized how important it was to plan my routes in order to handle the HTTP verbs and URLs in an organized and standardized way.
I designed my ProblemsController
, UsersController
and SessionsController
controller actions to follow restful conventions and to map the HTTP verbs (get
, post
, patch
, delete
) to the controller CRUD actions.
For my app, I wanted a logged-in user to be able to access all CRUD actions: the ability to create sessions (login)/ problems, read, update or delete their own problems.
My Problems Controller:
HTTP Verb | Route | CRUD Action | Used for/ result |
---|---|---|---|
GET | / | index | index page to welcome user - login/ signup |
GET | /problems | index | displays all problem (all problems are rendered) |
POST | /problems | create | creates a problem; save to db |
GET | /problems/:id | show | displays one problem based on ID in the url (just one problem is rendered) |
GET | /problems/:id/edit | edit | displays edit form based on ID in the URL |
PATCH | /problems/:id | update | modifies an existing problem based on ID in the url |
DELETE | /problems/:id | delete | deletes one article based on ID in the URL |
My Users Controller and Sessions Controller:
HTTP Verb | Route | CRUD Action | Used for/ result |
---|---|---|---|
GET | /signup | index | display signup form |
POST | /signup | create | creates a user |
GET | /login | index | displays login form |
POST | /login | create | create a session |
GET | /logout | delete | delete session/ log out |
GET | /users/:username | show | display one user’s problems based on :username in the url |
Other resources:
- Seeding Data: The dummy data was very important as it helped to demonstrate to the first user(s) how this web app could be used. There was the option of using APIs to seed data (especially for general topics like recipes), but I decided to create my own instances of Users, Problems, and Styles and their associations to make my data more personal (as it reflected my actual bouldering logs)
- Bootstrap: This allowed me to add styling to my forms, buttons, etc. without having to build the CSS from scratch
Top comments (0)