37 Signals released Fizzy today, which is their take on a Kanban board. What surprised everybody was that they also released the source code for it. There’s a lot to look at, which I cover in this video.
Let’s look at a few things I call out in the video that I need to elaborate on.
Nested resources
I particularly enjoyed the approach to nesting resources with controller modules. Here’s an example from their routes.rb file. We’ll be focusing on the not_now resource.
namespace :columns do
resources :cards do
scope module: :cards do
namespace :drops do
resource :not_now # Maps to Columns::Cards::Drops::NotNowsController
resource :stream
resource :closure
resource :column
end
end
end
end
Here’s the controller it maps to.
class Columns::Cards::Drops::NotNowsController < ApplicationController
include CardScoped
def create
@card.postpone
end
end
Simple! I immediately know a POST /columns/cards/:card_id/drops/not_now will do something.
Restomatic
In my projects, I take the same approach, but I’ve cleaned up the routes a bit with a gem called Restomatic. Here’s how the gem would clean up the Fizzy routes file.
namespace :columns do
nest :cards do
namespace :drops do
resource :not_now # Maps to Columns::Cards::Drops::NotNowsController
resource :stream
resource :closure
resource :column
end
end
end
I get the same end result in a slightly more readable format.
Magic link authentication
I was pleased to see 37 Signals’ use of magic links for logins. Usernames and passwords are a nightmare for customer support because people often sign up with the wrong email address, swear it’s right, then email support insisting they did it all right.
class SessionsController < ApplicationController
disallow_account_scope
require_unauthenticated_access except: :destroy
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }
layout "public"
def new
end
def create
if identity = Identity.find_by_email_address(email_address)
magic_link = identity.send_magic_link
serve_development_magic_link(magic_link)
end
redirect_to session_magic_link_path
end
def destroy
terminate_session
redirect_to_logout_url
end
private
def email_address
params.expect(:email_address)
end
end
This actually creates a bit of a security risk because a customer might complain a mistyped email is theirs when it could be somebody else’s.
Magic link authentication eliminates that because a user can only access the application if they enter the correct email address.
Here’s what the Fizzy sessions controller looks like:
class SessionsController < ApplicationController
disallow_account_scope
require_unauthenticated_access except: :destroy
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }
layout "public"
def new
end
def create
if identity = Identity.find_by_email_address(email_address)
magic_link = identity.send_magic_link
serve_development_magic_link(magic_link)
end
redirect_to session_magic_link_path
end
def destroy
terminate_session
redirect_to_logout_url
end
private
def email_address
params.expect(:email_address)
end
end
It gets an email address, sends off the magic link, and if the user clicks it within 15 minutes, they’re logged in.
I did notice a few potential issues with their approach, which surprised me since I don’t fancy myself as a security professional.
Magic links should be bound to browser sessions
One problem with magic link logins is they can be intercepted since they’re sent over email. One way to combat this is to bind the magic link to the browser that initiates the magic link request by setting a secret in a cookie or a token in the session.
Why? If an adversary intercepts the magic link and they try to open it in their browser, they’d get an error because the secret does not exist in their session.
One downside to this approach is if a user initiates the magic link from their main browser, then their email client opens in an in-app browser, they won’t be authenticated. The fix? Show a message to the user, “Copy the link $LINK and paste it into the browser you used to log in”.
Code generators should be random
The code 37 Signals uses to generate random characters for its magic links is pseudorandom.
module MagicLink::Code
CODE_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ".chars.freeze
CODE_SUBSTITUTIONS = { "O" => "0", "I" => "1", "L" => "1" }.freeze
class << self
def generate(length)
length.times.map { CODE_ALPHABET.sample }.join
end
def sanitize(code)
return nil if code.blank?
normalize_code(code)
.then { apply_substitutions(_1) }
.then { remove_invalid_characters(_1) }
end
private
def normalize_code(code)
code.to_s.upcase
end
def apply_substitutions(code)
CODE_SUBSTITUTIONS.reduce(code) { |result, (from, to)| result.gsub(from, to) }
end
def remove_invalid_characters(code)
code.gsub(/[^#{CODE_ALPHABET.join}]/, "")
end
end
end
So the offending line of code? It’s CODE_ALPHABET.sample. A better approach is to use the SecureRandom library to generate these characters, the easiest approach being SecureRandom.hex.
If you want something other than a string of hex characters to generate codes, check out the Anybase gem, which uses SecureRandom to generate a map of bytes that it maps to an array of characters.
Overall a great reference Rails project
It shouldn’t surprise anybody that when the creator of Rails builds a SaaS, the resulting source code is going to be a great example of writing clear and concise Rails code.
Overall, I’m pleased to see more SaaS companies open-sourcing their software, using magic links, and nesting controller resources instead of using Rails’ shallow routes.