diff --git a/Gemfile b/Gemfile
index e6bdae6..53cca0c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -24,7 +24,7 @@ gem "solid_queue"
# gem "kredis"
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
-# gem "bcrypt", "~> 3.1.7"
+gem "bcrypt"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ]
diff --git a/Gemfile.lock b/Gemfile.lock
index 1d6edb2..02434c0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -102,6 +102,7 @@ GEM
public_suffix (>= 2.0.2, < 7.0)
ast (2.4.2)
base64 (0.2.0)
+ bcrypt (3.1.20)
bigdecimal (3.1.8)
bindex (0.8.1)
bootsnap (1.18.3)
@@ -305,6 +306,7 @@ PLATFORMS
x86_64-linux
DEPENDENCIES
+ bcrypt
bootsnap
brakeman
capybara
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
new file mode 100644
index 0000000..2f1084d
--- /dev/null
+++ b/app/controllers/users_controller.rb
@@ -0,0 +1,70 @@
+class UsersController < ApplicationController
+ before_action :set_user, only: %i[ show edit update destroy ]
+
+ # GET /users or /users.json
+ def index
+ @users = User.all
+ end
+
+ # GET /users/1 or /users/1.json
+ def show
+ end
+
+ # GET /users/new
+ def new
+ @user = User.new
+ end
+
+ # GET /users/1/edit
+ def edit
+ end
+
+ # POST /users or /users.json
+ def create
+ @user = User.new(user_params)
+
+ respond_to do |format|
+ if @user.save
+ format.html { redirect_to user_url(@user), notice: "User was successfully created." }
+ format.json { render :show, status: :created, location: @user }
+ else
+ format.html { render :new, status: :unprocessable_entity }
+ format.json { render json: @user.errors, status: :unprocessable_entity }
+ end
+ end
+ end
+
+ # PATCH/PUT /users/1 or /users/1.json
+ def update
+ respond_to do |format|
+ if @user.update(user_params)
+ format.html { redirect_to user_url(@user), notice: "User was successfully updated." }
+ format.json { render :show, status: :ok, location: @user }
+ else
+ format.html { render :edit, status: :unprocessable_entity }
+ format.json { render json: @user.errors, status: :unprocessable_entity }
+ end
+ end
+ end
+
+ # DELETE /users/1 or /users/1.json
+ def destroy
+ @user.destroy!
+
+ respond_to do |format|
+ format.html { redirect_to users_url, notice: "User was successfully destroyed." }
+ format.json { head :no_content }
+ end
+ end
+
+ private
+ # Use callbacks to share common setup or constraints between actions.
+ def set_user
+ @user = User.find(params[:id])
+ end
+
+ # Only allow a list of trusted parameters through.
+ def user_params
+ params.require(:user).permit(:email, :password, :password_confirmation)
+ end
+end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
new file mode 100644
index 0000000..2310a24
--- /dev/null
+++ b/app/helpers/users_helper.rb
@@ -0,0 +1,2 @@
+module UsersHelper
+end
diff --git a/app/models/user.rb b/app/models/user.rb
new file mode 100644
index 0000000..d67da20
--- /dev/null
+++ b/app/models/user.rb
@@ -0,0 +1,3 @@
+class User < ApplicationRecord
+ has_secure_password
+end
diff --git a/app/views/users/_form.html.erb b/app/views/users/_form.html.erb
new file mode 100644
index 0000000..5ea1ca7
--- /dev/null
+++ b/app/views/users/_form.html.erb
@@ -0,0 +1,32 @@
+<%= form_with(model: user, class: "contents") do |form| %>
+ <% if user.errors.any? %>
+
+
<%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:
+
+
+ <% user.errors.each do |error| %>
+ - <%= error.full_message %>
+ <% end %>
+
+
+ <% end %>
+
+
+ <%= form.label :email %>
+ <%= form.text_field :email, class: "block shadow rounded-md border border-gray-400 outline-none px-3 py-2 mt-2 w-full" %>
+
+
+
+ <%= form.label :password %>
+ <%= form.password_field :password, class: "block shadow rounded-md border border-gray-400 outline-none px-3 py-2 mt-2 w-full" %>
+
+
+
+ <%= form.label :password_confirmation %>
+ <%= form.password_field :password_confirmation, class: "block shadow rounded-md border border-gray-400 outline-none px-3 py-2 mt-2 w-full" %>
+
+
+
+ <%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
+
+<% end %>
diff --git a/app/views/users/_user.html.erb b/app/views/users/_user.html.erb
new file mode 100644
index 0000000..8c202c1
--- /dev/null
+++ b/app/views/users/_user.html.erb
@@ -0,0 +1,7 @@
+
+
+ Email:
+ <%= user.email %>
+
+
+
diff --git a/app/views/users/_user.json.jbuilder b/app/views/users/_user.json.jbuilder
new file mode 100644
index 0000000..8b12e34
--- /dev/null
+++ b/app/views/users/_user.json.jbuilder
@@ -0,0 +1,2 @@
+json.extract! user, :id, :email, :created_at, :updated_at
+json.url user_url(user, format: :json)
diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb
new file mode 100644
index 0000000..e700c3e
--- /dev/null
+++ b/app/views/users/edit.html.erb
@@ -0,0 +1,8 @@
+
+
Editing user
+
+ <%= render "form", user: @user %>
+
+ <%= link_to "Show this user", @user, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
+ <%= link_to "Back to users", users_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
+
diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb
new file mode 100644
index 0000000..ee6f295
--- /dev/null
+++ b/app/views/users/index.html.erb
@@ -0,0 +1,21 @@
+
+ <% if notice.present? %>
+
<%= notice %>
+ <% end %>
+
+ <% content_for :title, "Users" %>
+
+
+
Users
+ <%= link_to "New user", new_user_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %>
+
+
+
+ <% @users.each do |user| %>
+ <%= render user %>
+
+ <%= link_to "Show this user", user, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
+
+ <% end %>
+
+
diff --git a/app/views/users/index.json.jbuilder b/app/views/users/index.json.jbuilder
new file mode 100644
index 0000000..98788da
--- /dev/null
+++ b/app/views/users/index.json.jbuilder
@@ -0,0 +1 @@
+json.array! @users, partial: "users/user", as: :user
diff --git a/app/views/users/new.html.erb b/app/views/users/new.html.erb
new file mode 100644
index 0000000..ea5b08d
--- /dev/null
+++ b/app/views/users/new.html.erb
@@ -0,0 +1,7 @@
+
+
New user
+
+ <%= render "form", user: @user %>
+
+ <%= link_to "Back to users", users_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
+
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb
new file mode 100644
index 0000000..8e2d55c
--- /dev/null
+++ b/app/views/users/show.html.erb
@@ -0,0 +1,15 @@
+
+
+ <% if notice.present? %>
+
<%= notice %>
+ <% end %>
+
+ <%= render @user %>
+
+ <%= link_to "Edit this user", edit_user_path(@user), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
+ <%= link_to "Back to users", users_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
+
+ <%= button_to "Destroy this user", @user, method: :delete, class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 font-medium" %>
+
+
+
diff --git a/app/views/users/show.json.jbuilder b/app/views/users/show.json.jbuilder
new file mode 100644
index 0000000..ff40bb9
--- /dev/null
+++ b/app/views/users/show.json.jbuilder
@@ -0,0 +1 @@
+json.partial! "users/user", user: @user
diff --git a/config/routes.rb b/config/routes.rb
index 4930193..74d76c3 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,4 +1,5 @@
Rails.application.routes.draw do
+ resources :users
resources :credit_card_bills
resource :dashboard, only: :show
resources :incomes
diff --git a/db/migrate/20240727022931_create_users.rb b/db/migrate/20240727022931_create_users.rb
new file mode 100644
index 0000000..f077285
--- /dev/null
+++ b/db/migrate/20240727022931_create_users.rb
@@ -0,0 +1,11 @@
+class CreateUsers < ActiveRecord::Migration[8.0]
+ def change
+ create_table :users do |t|
+ t.string :email
+ t.string :password_digest
+
+ t.timestamps
+ end
+ add_index :users, :email, unique: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index d134a4c..a8caf00 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.0].define(version: 2024_07_27_002531) do
+ActiveRecord::Schema[8.0].define(version: 2024_07_27_022931) do
create_table "credit_card_bills", force: :cascade do |t|
t.string "description"
t.decimal "amount"
@@ -148,6 +148,14 @@ ActiveRecord::Schema[8.0].define(version: 2024_07_27_002531) do
t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true
end
+ create_table "users", force: :cascade do |t|
+ t.string "email"
+ t.string "password_digest"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["email"], name: "index_users_on_email", unique: true
+ end
+
add_foreign_key "incomes", "members"
add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb
new file mode 100644
index 0000000..71dd43d
--- /dev/null
+++ b/test/controllers/users_controller_test.rb
@@ -0,0 +1,55 @@
+require "test_helper"
+
+class UsersControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @user = users(:one)
+ end
+
+ test "should get index" do
+ get users_url
+ assert_response :success
+ end
+
+ test "should get new" do
+ get new_user_url
+ assert_response :success
+ end
+
+ test "should create user" do
+ assert_difference("User.count") do
+ params = {
+ user: {
+ email: "userthree@example.local",
+ password: "secret",
+ password_confirmation: "secret"
+ }
+ }
+ post users_url, params: params
+ end
+
+ assert_redirected_to user_url(User.last)
+ end
+
+ test "should show user" do
+ get user_url(@user)
+ assert_response :success
+ end
+
+ test "should get edit" do
+ get edit_user_url(@user)
+ assert_response :success
+ end
+
+ test "should update user" do
+ patch user_url(@user), params: { user: { email: @user.email, password: "secret", password_confirmation: "secret" } }
+ assert_redirected_to user_url(@user)
+ end
+
+ test "should destroy user" do
+ assert_difference("User.count", -1) do
+ delete user_url(@user)
+ end
+
+ assert_redirected_to users_url
+ end
+end
diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml
new file mode 100644
index 0000000..99db479
--- /dev/null
+++ b/test/fixtures/users.yml
@@ -0,0 +1,9 @@
+# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+one:
+ email: userone@example.local
+ password_digest: <%= BCrypt::Password.create("secret") %>
+
+two:
+ email: usertwo@example.local
+ password_digest: <%= BCrypt::Password.create("secret") %>
diff --git a/test/models/user_test.rb b/test/models/user_test.rb
new file mode 100644
index 0000000..5c07f49
--- /dev/null
+++ b/test/models/user_test.rb
@@ -0,0 +1,7 @@
+require "test_helper"
+
+class UserTest < ActiveSupport::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end
diff --git a/test/system/users_test.rb b/test/system/users_test.rb
new file mode 100644
index 0000000..00e5e87
--- /dev/null
+++ b/test/system/users_test.rb
@@ -0,0 +1,45 @@
+require "application_system_test_case"
+
+class UsersTest < ApplicationSystemTestCase
+ setup do
+ @user = users(:one)
+ end
+
+ test "visiting the index" do
+ visit users_url
+ assert_selector "h1", text: "Users"
+ end
+
+ test "should create user" do
+ visit users_url
+ click_on "New user"
+
+ fill_in "Email", with: "userthree@example.local"
+ fill_in "Password", with: "secret"
+ fill_in "Password confirmation", with: "secret"
+ click_on "Create User"
+
+ assert_text "User was successfully created"
+ click_on "Back"
+ end
+
+ test "should update User" do
+ visit user_url(@user)
+ click_on "Edit this user", match: :first
+
+ fill_in "Email", with: "newemail@example.local"
+ fill_in "Password", with: "newpassword"
+ fill_in "Password confirmation", with: "newpassword"
+ click_on "Update User"
+
+ assert_text "User was successfully updated"
+ click_on "Back"
+ end
+
+ test "should destroy User" do
+ visit user_url(@user)
+ click_on "Destroy this user", match: :first
+
+ assert_text "User was successfully destroyed"
+ end
+end