I hate the name “User” it means all and nothing. If you build blog system, what is user? A person who writes posts? Or maybe a person who writes comments? Could we name it Blogger? In a more complicated system probably we have more personas like that. In the current system, I don’t see a reason to have users, but it would be nice to have accounts where I can keep credentials for them.
I saw a lot of projects where developers started from users. Even if they don’t need them. Let’s say we have a small e-commerce. Do we need users? I don’t think so. Having product lists, orders, and payments, that’s important. And even that, not users but customers.
So why I’m going to introduce accounts? My application has to sync data from external services (services in the future). It would be nice to connect private keys with some entity in the app.
I split this topic into at least 3 parts: accounts(this post), authentication, scope data to the user. One important note! You should always use already-built solutions, especially for production apps. Building something from scratch is super insecure, and in my opinion, is an antipattern. If you want to keep credentials on your side there are devise or rodauth But you should consider keeping it outside the app with auth0 firebase auth or supabase Don’t do this yourself.
But for educational reasons, I want to do as much as possible from scratch.
class CreateAccounts < ActiveRecord::Migration[7.0]
def change
create_table :accounts do |t|
t.string :email, null: false
t.string :password_digest, null: false
t.timestamps
end
end
end
A lot of stuff doesn’t look different in comparison to other components. But in Contract I want to verify if email already exists in the database.
require "dry/validation"
require_relative "./repository"
module Accounts
class Contract < Dry::Validation::Contract
params do
required(:email).filled(:string)
required(:password).filled(:string)
end
rule(:email) do
key.failure("Email has to be unique") if Accounts::Repository.new.find_by_email(value).present?
end
end
end
“Email has to be unique” is an error that we could log internally, but we should never show this information outside. For security reasons, “wrong credential” when you aren’t able to recognize if an account exists or not is much more secure.
In migration I created field password_digest
instead of password
. Active Record has method has_secure_password
which handle that for us. It adds methods to set and authenticate against a BCrypt password. This mechanism requires you to have a XXX_digest attribute. Where XXX is the attribute name of your desired password. The following validations are added automatically: Password must be present on creation. Password length should be less than or equal to 72 bytes. Confirmation of password (using a XXX_confirmation attribute)
require "active_record"
module Db
module Records
class Account < ActiveRecord::Base
has_secure_password
end
end
end
But we could do something similar from scratch, you can check code of that on github and bcrypt-ruby documentation
module Accounts
class Controller
def create(request)
account_data = Accounts::Contract.new.call(JSON.parse(request.body.read))
if account_data.errors.to_h.any?
[500, {"content-type" => "text/plain"}, ["Error creating account"]]
else
account = Accounts::Repository.new.create(
email: account_data["email"],
password: account_data["password"]
)
if account
[201, {"content-type" => "text/plain"}, ["Create"]]
else
[500, {"content-type" => "text/plain"}, ["Error creating account"]]
end
end
end
...
From controller’s perspective, this is typical CRUD (without index method, there is no sense to show all accounts in the system). Temporary I don’t touch anything frontend related. It stays as it is, I’m going to write it from scratch soon. From the app perspective, having users affect nothing yet. But is always nice to split stuff into small steps. Like always, the whole code is on github