Skip to content

Instantly share code, notes, and snippets.

@labe
Last active October 27, 2022 04:24
Show Gist options
  • Save labe/5763905 to your computer and use it in GitHub Desktop.
Save labe/5763905 to your computer and use it in GitHub Desktop.
Creating user accounts with Sinatra (no BCrypt)

##User accounts with Sinatra (no BCrypt)

Create migration

A very basic schema for a user typically looks like this:

def change
  create_table :users do |t|
    t.string  :username
    t.string  :email
    t.string  :password
    t.timestamps
  end
end

Users can have plenty of other information about them or their account stored in their table, of course-- full_name, hometown, is_private. If ever stumped about what to include in the schema, think of apps you use and the information included in your profile.

Make validations on the model

class User < ActiveRecord::Base
  validates :username, :presence => true, 
                       :uniqueness => true
  validates :email,    :presence => true,
                       :uniqueness => true
                       :format => {:with => /\w+@\w+\.\w+/)
  validates :password, :presence => true
end

Validations mean a User object won't ever be saved unless they meet this criteria. The above code says that a User cannot exist in the database unless it has

  • a username (which is unique)
  • an email address (which is unique and is formatted based on a RegEx (this is a very loose RegEx example that says "at least one word character (letter, number, underscore) followed by an "@", followed by at least one word character followed by a ".", followed by at least one word character"))
  • a password

###Make the routes

Routes will depend on your UX design. Should login and sign up be on separate pages? If creating an account requires a lot of fields, each should probably have its own page:

get '/login' do
  erb :login
end

get '/new_account' do
  erb :new_account
end

If creating an account is not much different from logging in, however, it may make more sense to consolidate both forms on the same page.

get '/login' do
  erb :login
end

Regardless, each form will need to POST to its own route.

post '/login' do
  # params are passed in from the form
  # if user is authenticated, 
  #    "logs user in"
  #    (which really just means setting the session cookie)
  #    redirects the user somewhere (maybe '/' ?)
  # else if user auth fails,
  #    redirects user, probably back to '/login'
end

post '/new_account' do
  # (params are passed in from the form)
  user = User.create(params[:user])
  session[:user_id] = user.id
  # redirects user somewhere (maybe '/' ?)
end

And of course you'll need:

get '/logout' do
  session[:user_id] = nil
  redirect_to '/'
end

Might your users also have a profile page? Maybe something like:

get '/users/:username' do
  erb :user
end

Make the current_user helper method

The Sinatra skeleton contains a "helpers" folder under "app". Methods saved in this folder are accessible from all your controllers and views.

Create a file called user_helper.rb (or something akin to that), and create a method current_user:

def current_user
  User.find(session[:user_id]) if session[:user_id]
end

If a user has signed in and session[:user_id] has been set, calling this method will return the relevant User object. Else, the method will return nil.

There are a few other ways to write code that produces the same results. This one is the simplest.

Link to the views

Users should probably be able to sign in or logout or access their profile page at any time, regardless of which page they're on, right? (Probably right.) Put those babies in a header above your <%= yield %> statement in your layout.erb view.

<body>
  <div class="header">
    <h5><a href="/">Home</a></h5>
    <% if current_user %>
      <h5><a href='/logout'>Logout</a></h5>
      <h5>Signed in as: <a href="/users/<%= current_user.name %>"><%= current_user.name %></a></h5>
    <% else %>
      <h5><a href="/login">Login</a></h5>
    <% end %>
  </div>
  <div class="container">
      <%= yield %>
  </div>
</body>

The above specifies that if the current_user helper method returns true (meaning, a user has logged in):

  • display a "Logout" link
  • display that they are "Signed in as [username]" (and the username links to their profile page)

Otherwise, just display a link to the login page (and to the "create an account" page, if you've chosen to separate them).

Make the views

You know how to create forms, so I won't belabor the point here. Things to remember:

  • When naming fields, match them to the database table column names (e.g.: username,email,password)
  • Get fancy and put them in a hash
    • user[username],user[email],user[password] will POST:
      • params => {user => {username: < username >, email: < email >, password: < password > }
      • NOTE: there is no colon (":") used in the input field names. It's just user[username].
  • make sure your form actions match the appropriate routes!
  • Get in the habit of using autofocus in forms, usually in whatever is the first input field on the entire page. It makes your users happier, even if they don't realize it at the time.

Back to the controller

So now that data can be sent through, let's build out those POST routes.

New accounts are fairly straight-forward, since we're not doing anything with password encryption (BCrypt) just yet. Take in the form params and use them to create a new User, then set the session cookie:

post '/new_account' do
  user = User.create(params[:user])
  session[:user_id] = user.id
  redirect '/'
end

You can choose to redirect them wherever it makes the most sense to you to send your users after they've made a new account. Maybe it's back to the home page? Maybe it's to their profile page (redirect "/users/#{user.username}"? Maybe it's something fancier? Totally up to you.

Logging in is a little tricker, since we'll need to make sure the user has submitted the correct password.

If your login form takes in a username and password, the process should go:

  1. Find the user based on params[:user][:username]
  2. Check if user.password matches params[:user][:password]
  3. If it matches, redirect the user to wherever (see above).
  4. If it doesn't match, you'll probably want to just send them back to the login page so they can try again.
  5. (You can get fancy and show an error message on the login page so the user knows why they've been sent back there!)
post '/login' do
  user = User.find_by_username(params[:user][:username])
  if user.password == params[:user][:password]
  	session[:user_id] = user.id
  	redirect '/'
  else
    redirect '/login'
  end
end

Except, oh man, model code in the controller! This can be refactored to:

post '/login' do
  if user = User.authenticate(params[:user])
  	session[:user_id] = user.id
  	redirect '/'
  else
    redirect '/login'
  end
end
class User < ActiveRecord::Base

  def self.authenticate(params)
    user = User.find_by_name(params[:username])
    (user && user.password == params[:password]) ? user : nil
  end
  
end

This creates a User class method authenticate which takes in a params hash. The controller sends this method params[:user] as that hash.

Why a class method? Because you need to find a specific user, but you don't want to make the controller do that work. The model should do that work, but without a specific user (yeah, it gets kind of circular), you can't use an instance method… so you have to use a class method.

Speaking of! What is this class method doing?

The first line is trying to find a specific user based on the username that was submitted via the login form, and storing whatever it finds in the local variable user.

The second line is saying:

  • IF a user was found (because if the submitted username didn't actually exist in the database, User.find_by… would have returned nil, which is the same as false
  • AND IF that found user's password matches the password that was submitted in the form
  • THEN this function will return user
  • ELSE this function will return nil

So if you look back at the refactored route, it's saying:

  • If User.authenticate returns a user, store that user in a local variable user, set the session cookie for this user and redirect to the root path
  • Else redirect to the login page so the user can try again

Back to the helper method

Maybe your app has routes you only want logged-in users to access. I.e., only logged-in users can create blog posts or upload photos or submit links or leave comments (etc.).

Let's pretend this is a blogging app, and you have a route for the page that contains the form used to create a new post:

get '/create_post' do
end

Because you have that helper method, you can do something like:

get '/create_post' do
  if current_user
    erb :create_post
  else
    redirect '/login'
  end
end

which just says, if current_user returns a user, load this page and show the form for creating a new post. Otherwise, if current_user returns nil, redirect the user to the login page.

Bonus funtastical features to think about

  • If a non-logged in user clicks on a link to a protected route (meaning, only logged-in users can see that page) and is redirected to the login page, and then the user successfully signs in… wouldn't it be nice if the user could be redirected to the page they were trying to access in the first place?
  • How can the app know when to display an error message?
  • Remember: you can store ~ 4 Kb in a session
  • If a user is logged in, are there still pages that user shouldn't be able to access? (Think about editing a profile page. Should users be able to access the profile edit page of other users? (Easy answer: no)).
@AlwinaO
Copy link

AlwinaO commented Jan 17, 2019

This is amazing! Thank you for sharing!

@djixer
Copy link

djixer commented Mar 17, 2020

thanks! you helped me out

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment