Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ gem "bootsnap", require: false
gem "thruster", require: false
gem "image_processing", "~> 1.2"
gem "alba"
gem "bcrypt", "~> 3.1.7"

group :development, :test do
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ GEM
alba (3.11.0)
ast (2.4.3)
base64 (0.3.0)
bcrypt (3.1.22)
bigdecimal (4.1.2)
bootsnap (1.24.6)
msgpack (~> 1.2)
Expand Down Expand Up @@ -332,6 +333,7 @@ PLATFORMS

DEPENDENCIES
alba
bcrypt (~> 3.1.7)
bootsnap
brakeman
bundler-audit
Expand Down Expand Up @@ -364,6 +366,7 @@ CHECKSUMS
alba (3.11.0) sha256=a9e7ad2db045490955b2d58cc5a2eeb0db7462ce3c32ec6de1f536e7fe506769
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
bcrypt (3.1.22) sha256=1f0072e88c2d705d94aff7f2c5cb02eb3f1ec4b8368671e19112527489f29032
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
bootsnap (1.24.6) sha256=c60bab88c70332290f0a2636a288f675299eb4f804a02a3c085b42eca9da164a
brakeman (8.0.5) sha256=03735f9690d3fd4b32d66aacbf0a6d15a84266bdd06b32c05c8ecc8f6021d2be
Expand Down
18 changes: 18 additions & 0 deletions app/controllers/api/v1/registrations_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module Api
module V1
class RegistrationsController < BaseController
def create
user = User.new(registration_params)
user.save!

render json: UserSerializer.new(user).serialize, status: :created
end

private

def registration_params
params.expect(user: [ :email, :name, :password ])
end
end
end
end
4 changes: 4 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
class User < ApplicationRecord
has_secure_password

EMAIL_FORMAT = URI::MailTo::EMAIL_REGEXP
MINIMUM_PASSWORD_LENGTH = 12

before_validation :normalize_email

validates :email, presence: true, uniqueness: { case_sensitive: false }, format: { with: EMAIL_FORMAT }
validates :name, presence: true, length: { maximum: 100 }
validates :password, length: { minimum: MINIMUM_PASSWORD_LENGTH }, allow_nil: true

private

Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace :api do
namespace :v1 do
resources :users, only: [ :show ]
resource :registration, only: [ :create ]
end
end

Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20260807094314_add_password_digest_to_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddPasswordDigestToUsers < ActiveRecord::Migration[8.1]
def change
add_column :users, :password_digest, :string, null: false
end
end
3 changes: 2 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions spec/factories/users.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
factory :user do
sequence(:email) { |n| "user#{n}@example.com" }
name { "John Doe" }
password { "password1234" }
end
end
11 changes: 11 additions & 0 deletions spec/models/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
it { is_expected.to validate_presence_of(:email) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:email).case_insensitive }
it { is_expected.to have_secure_password }

it "downcases and strips the email before validation" do
user = create(:user, email: " MiXeD@Example.COM ")
Expand All @@ -15,4 +16,14 @@
it "rejects a malformed email" do
expect(build(:user, email: "not-an-email")).not_to be_valid
end

it "rejects a short password" do
expect(build(:user, password: "short")).not_to be_valid
end

it "authenticates with the correct password" do
user = create(:user, password: "password1234")
expect(user.authenticate("password1234")).to eq(user)
expect(user.authenticate("wrong")).to be false
end
end
32 changes: 32 additions & 0 deletions spec/requests/api/v1/registrations_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
require "rails_helper"

RSpec.describe "Api::V1::Registrations", type: :request do
describe "POST /api/v1/registration" do
it "creates a user" do
expect {
post "/api/v1/registration", params: {
user: { email: "new@example.com", name: "John Doe", password: "password1234" }
}
}.to change(User, :count).by(1)

expect(response).to have_http_status(:created)
expect(json_body["email"]).to eq("new@example.com")
end

it "never returns the password digest" do
post "/api/v1/registration", params: {
user: { email: "new@example.com", name: "John Doe", password: "password1234" }
}

expect(json_body).not_to have_key("password_digest")
end

it "returns a structured 422 for invalid input" do
post "/api/v1/registration", params: { user: { email: "bad", name: "", password: "x" } }

expect(response).to have_http_status(:unprocessable_entity)
expect(json_body["error"]["code"]).to eq("validation_failed")
expect(json_body["error"]["details"]).to have_key("email")
end
end
end