From 4bd13199ea45faa69e3ab58c399f9f2f30ada081 Mon Sep 17 00:00:00 2001 From: Alan Roddick Date: Wed, 3 Nov 2021 00:32:07 -0700 Subject: [PATCH 1/3] Add register and login functionality with jwt and bcrypt --- .gitignore | 2 + Dockerfile | 2 +- Gemfile | 11 +++++- Gemfile.lock | 24 ++++++++++++ app/assets/stylesheets/login.scss | 3 ++ app/assets/stylesheets/register.scss | 3 ++ app/controllers/application_controller.rb | 37 +++++++++++++++++++ app/controllers/login_controller.rb | 21 +++++++++++ app/controllers/register_controller.rb | 19 ++++++++++ app/helpers/login_helper.rb | 2 + app/helpers/register_helper.rb | 2 + app/models/user.rb | 5 +++ app/serializers/user_serializer.rb | 3 ++ config/initializers/cors.rb | 7 ++++ config/routes.rb | 2 + ...01054308_remove_quantity_from_doughnuts.rb | 2 +- db/migrate/20211103044717_create_users.rb | 11 ++++++ db/schema.rb | 10 ++++- test/controllers/login_controller_test.rb | 7 ++++ test/controllers/register_controller_test.rb | 7 ++++ test/fixtures/users.yml | 11 ++++++ test/models/user_test.rb | 7 ++++ 22 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 app/assets/stylesheets/login.scss create mode 100644 app/assets/stylesheets/register.scss create mode 100644 app/controllers/login_controller.rb create mode 100644 app/controllers/register_controller.rb create mode 100644 app/helpers/login_helper.rb create mode 100644 app/helpers/register_helper.rb create mode 100644 app/models/user.rb create mode 100644 app/serializers/user_serializer.rb create mode 100644 config/initializers/cors.rb create mode 100644 db/migrate/20211103044717_create_users.rb create mode 100644 test/controllers/login_controller_test.rb create mode 100644 test/controllers/register_controller_test.rb create mode 100644 test/fixtures/users.yml create mode 100644 test/models/user_test.rb diff --git a/.gitignore b/.gitignore index f22dd34..be7cba1 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ /yarn-error.log yarn-debug.log* .yarn-integrity + +*.DS_Store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 144a5bc..475fdca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM ruby:2.7.4 RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add \ && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ - && apt-get update && apt-get install -y nodejs yarn --no-install-recommends \ + && apt-get update && apt-get install -y nodejs yarn vim --no-install-recommends \ && gem install rails WORKDIR /app diff --git a/Gemfile b/Gemfile index 03a7ee1..3b5513c 100644 --- a/Gemfile +++ b/Gemfile @@ -14,7 +14,7 @@ gem 'sass-rails', '>= 6' # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker gem 'webpacker', '~> 5.0' # Use Active Model has_secure_password -# gem 'bcrypt', '~> 3.1.7' +gem 'bcrypt', '~> 3.1.7' # Use Active Storage variant # gem 'image_processing', '~> 1.2' @@ -22,6 +22,9 @@ gem 'webpacker', '~> 5.0' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', '>= 1.4.4', require: false +# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible +gem 'rack-cors' + group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] @@ -41,3 +44,9 @@ end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] gem 'sassc', '~> 2.1.0' + +gem "jwt", "~> 2.3" + +gem "active_model_serializers", "~> 0.10.12" + +gem "faker", "~> 2.19" diff --git a/Gemfile.lock b/Gemfile.lock index 8cace3d..a53b2eb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -39,6 +39,11 @@ GEM erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) + active_model_serializers (0.10.12) + actionpack (>= 4.1, < 6.2) + activemodel (>= 4.1, < 6.2) + case_transform (>= 0.2) + jsonapi-renderer (>= 0.1.1.beta1, < 0.3) activejob (6.1.4.1) activesupport (= 6.1.4.1) globalid (>= 0.3.6) @@ -60,19 +65,30 @@ GEM minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) + bcrypt (3.1.16) bindex (0.8.1) bootsnap (1.9.1) msgpack (~> 1.0) builder (3.2.4) byebug (11.1.3) + case_transform (0.2) + activesupport concurrent-ruby (1.1.9) crass (1.0.6) + dotenv (2.7.6) + dotenv-rails (2.7.6) + dotenv (= 2.7.6) + railties (>= 3.2) erubi (1.10.0) + faker (2.19.0) + i18n (>= 1.6, < 2) ffi (1.15.4) globalid (0.5.2) activesupport (>= 5.0) i18n (1.8.10) concurrent-ruby (~> 1.0) + jsonapi-renderer (0.2.2) + jwt (2.3.0) listen (3.7.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) @@ -96,6 +112,8 @@ GEM nio4r (~> 2.0) racc (1.6.0) rack (2.2.3) + rack-cors (1.1.1) + rack (>= 2.0.0) rack-mini-profiler (2.3.3) rack (>= 1.2.0) rack-proxy (0.7.0) @@ -174,11 +192,17 @@ PLATFORMS ruby DEPENDENCIES + active_model_serializers (~> 0.10.12) + bcrypt (~> 3.1.7) bootsnap (>= 1.4.4) byebug + dotenv-rails + faker (~> 2.19) + jwt (~> 2.3) listen (~> 3.3) pg (~> 1.1) puma (~> 5.0) + rack-cors rack-mini-profiler (~> 2.0) rails (~> 6.1.4, >= 6.1.4.1) sass-rails (>= 6) diff --git a/app/assets/stylesheets/login.scss b/app/assets/stylesheets/login.scss new file mode 100644 index 0000000..6e719a1 --- /dev/null +++ b/app/assets/stylesheets/login.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the login controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/assets/stylesheets/register.scss b/app/assets/stylesheets/register.scss new file mode 100644 index 0000000..1899c95 --- /dev/null +++ b/app/assets/stylesheets/register.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the register controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 09705d1..66de716 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,2 +1,39 @@ class ApplicationController < ActionController::Base + before_action :authorized + SECRET_KEY = Rails.application.secrets.secret_key_base. to_s + + def encode_token(payload, exp = 2.hours.from_now) + payload[:exp] = exp.to_i + JWT.encode(payload, SECRET_KEY) + end + + def auth_header + request.headers['Authorization'] + end + + def decoded_token + if auth_header + token = auth_header.split(' ')[1] + begin + JWT.decode(token, SECRET_KEY, true, algorithm: 'HS256') + rescue JWT::DecodeError + nil + end + end + end + + def current_user + if decoded_token + user_id = decoded_token[0]['user_id'] + @user = User.find_by(id: user_id) + end + end + + def logged_in? + !!current_user + end + + def authorized + render json: { message: 'Please log in' }, status: :unauthorized unless logged_in? + end end diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb new file mode 100644 index 0000000..e06207b --- /dev/null +++ b/app/controllers/login_controller.rb @@ -0,0 +1,21 @@ +class LoginController < ApplicationController + skip_before_action :verify_authenticity_token, :authorized, only: [:create] + + def create + puts user_login_params + @user = User.find_by(username: user_login_params[:username]) + #User#authenticate comes from BCrypt + if @user && @user.authenticate(user_login_params[:password]) + @token = encode_token({ user_id: @user.id }) + time = Time.now + 2.hours.to_i + render json: { user: UserSerializer.new(@user), jwt: @token, exp: time }, status: :accepted + else + render json: { message: 'Invalid username or password' }, status: :unauthorized + end + end + + private + def user_login_params + params.require(:user).permit(:username, :password) + end +end diff --git a/app/controllers/register_controller.rb b/app/controllers/register_controller.rb new file mode 100644 index 0000000..5b8d67a --- /dev/null +++ b/app/controllers/register_controller.rb @@ -0,0 +1,19 @@ +class RegisterController < ApplicationController + skip_before_action :verify_authenticity_token, :authorized, only: [:create] + + def create + @user = User.create(user_params) + if @user.valid? + @token = encode_token({ user_id: @user.id }) + time = Time.now + 2.hours.to_i + render json: { user: UserSerializer.new(@user), jwt: @token, exp: time }, status: :created + else + render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity + end + end + + private + def user_params + params.require(:user).permit(:username, :password, :role) + end +end diff --git a/app/helpers/login_helper.rb b/app/helpers/login_helper.rb new file mode 100644 index 0000000..a0418e3 --- /dev/null +++ b/app/helpers/login_helper.rb @@ -0,0 +1,2 @@ +module LoginHelper +end diff --git a/app/helpers/register_helper.rb b/app/helpers/register_helper.rb new file mode 100644 index 0000000..862bf4d --- /dev/null +++ b/app/helpers/register_helper.rb @@ -0,0 +1,2 @@ +module RegisterHelper +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..7431f07 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,5 @@ +class User < ApplicationRecord + has_secure_password + validates :username, uniqueness: { case_sensitive: false } + validates :role, presence: true +end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb new file mode 100644 index 0000000..76e2230 --- /dev/null +++ b/app/serializers/user_serializer.rb @@ -0,0 +1,3 @@ +class UserSerializer < ActiveModel::Serializer + attributes :username, :role +end diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb new file mode 100644 index 0000000..06e3aa1 --- /dev/null +++ b/config/initializers/cors.rb @@ -0,0 +1,7 @@ +Rails.application.config.middleware.insert_before 0, Rack::Cors do + allow do + # Change to frontend domain + origins '*' + resource '*', headers: :any, methods: :any + end +end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 421cdfa..0a33218 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,4 +2,6 @@ root "doughnuts#index" # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html resources :doughnuts + resources :register, only: [:create] + resources :login, only: [:create] end diff --git a/db/migrate/20211101054308_remove_quantity_from_doughnuts.rb b/db/migrate/20211101054308_remove_quantity_from_doughnuts.rb index bb54084..2e17e94 100644 --- a/db/migrate/20211101054308_remove_quantity_from_doughnuts.rb +++ b/db/migrate/20211101054308_remove_quantity_from_doughnuts.rb @@ -1,5 +1,5 @@ class RemoveQuantityFromDoughnuts < ActiveRecord::Migration[6.1] def change - remove_column :doughnuts, :quantity, :float + # remove_column :doughnuts, :quantity, :float end end diff --git a/db/migrate/20211103044717_create_users.rb b/db/migrate/20211103044717_create_users.rb new file mode 100644 index 0000000..d029e47 --- /dev/null +++ b/db/migrate/20211103044717_create_users.rb @@ -0,0 +1,11 @@ +class CreateUsers < ActiveRecord::Migration[6.1] + def change + create_table :users do |t| + t.string :username + t.string :password_digest + t.string :role + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 9cc2472..0570291 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.define(version: 2021_11_01_054516) do +ActiveRecord::Schema.define(version: 2021_11_03_044717) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -24,4 +24,12 @@ t.integer "quantity" end + create_table "users", force: :cascade do |t| + t.string "username" + t.string "password_digest" + t.string "role" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + end diff --git a/test/controllers/login_controller_test.rb b/test/controllers/login_controller_test.rb new file mode 100644 index 0000000..5abb022 --- /dev/null +++ b/test/controllers/login_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class LoginControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/register_controller_test.rb b/test/controllers/register_controller_test.rb new file mode 100644 index 0000000..54e4e67 --- /dev/null +++ b/test/controllers/register_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class RegisterControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml new file mode 100644 index 0000000..e7f1ada --- /dev/null +++ b/test/fixtures/users.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + username: MyString + password_digest: MyString + role: MyString + +two: + username: MyString + password_digest: MyString + role: MyString 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 From d70dd8b126a31d01fe761051974c5eba27459ac4 Mon Sep 17 00:00:00 2001 From: Alan Roddick Date: Wed, 3 Nov 2021 20:30:33 -0700 Subject: [PATCH 2/3] Return different status code based on username match --- app/controllers/login_controller.rb | 9 +++++++-- app/controllers/register_controller.rb | 4 ++++ app/models/user.rb | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb index e06207b..4bd1add 100644 --- a/app/controllers/login_controller.rb +++ b/app/controllers/login_controller.rb @@ -1,9 +1,8 @@ class LoginController < ApplicationController skip_before_action :verify_authenticity_token, :authorized, only: [:create] + before_action :find_user def create - puts user_login_params - @user = User.find_by(username: user_login_params[:username]) #User#authenticate comes from BCrypt if @user && @user.authenticate(user_login_params[:password]) @token = encode_token({ user_id: @user.id }) @@ -14,6 +13,12 @@ def create end end + def find_user + @user = User.find_by_username!(user_login_params[:username]) + rescue ActiveRecord::RecordNotFound + render json: { error: 'User not found' }, status: :not_found + end + private def user_login_params params.require(:user).permit(:username, :password) diff --git a/app/controllers/register_controller.rb b/app/controllers/register_controller.rb index 5b8d67a..8b15d55 100644 --- a/app/controllers/register_controller.rb +++ b/app/controllers/register_controller.rb @@ -2,6 +2,10 @@ class RegisterController < ApplicationController skip_before_action :verify_authenticity_token, :authorized, only: [:create] def create + if User.exists?(username: user_params[:username]) + render json: { error: 'Username taken'}, status: 409 + return + end @user = User.create(user_params) if @user.valid? @token = encode_token({ user_id: @user.id }) diff --git a/app/models/user.rb b/app/models/user.rb index 7431f07..d8b99a8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,5 @@ class User < ApplicationRecord has_secure_password - validates :username, uniqueness: { case_sensitive: false } + validates :username, presence: true, uniqueness: { case_sensitive: false } validates :role, presence: true end From 66cd19c2f40076623347c8664fc57234efcf9b5f Mon Sep 17 00:00:00 2001 From: Alan Roddick Date: Wed, 3 Nov 2021 21:51:09 -0700 Subject: [PATCH 3/3] Remove 404 from login --- Gemfile.lock | 5 ----- app/controllers/login_controller.rb | 8 +------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a53b2eb..bf67f2c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -75,10 +75,6 @@ GEM activesupport concurrent-ruby (1.1.9) crass (1.0.6) - dotenv (2.7.6) - dotenv-rails (2.7.6) - dotenv (= 2.7.6) - railties (>= 3.2) erubi (1.10.0) faker (2.19.0) i18n (>= 1.6, < 2) @@ -196,7 +192,6 @@ DEPENDENCIES bcrypt (~> 3.1.7) bootsnap (>= 1.4.4) byebug - dotenv-rails faker (~> 2.19) jwt (~> 2.3) listen (~> 3.3) diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb index 4bd1add..67e072d 100644 --- a/app/controllers/login_controller.rb +++ b/app/controllers/login_controller.rb @@ -1,8 +1,8 @@ class LoginController < ApplicationController skip_before_action :verify_authenticity_token, :authorized, only: [:create] - before_action :find_user def create + @user = User.find_by(username: user_login_params[:username]) #User#authenticate comes from BCrypt if @user && @user.authenticate(user_login_params[:password]) @token = encode_token({ user_id: @user.id }) @@ -13,12 +13,6 @@ def create end end - def find_user - @user = User.find_by_username!(user_login_params[:username]) - rescue ActiveRecord::RecordNotFound - render json: { error: 'User not found' }, status: :not_found - end - private def user_login_params params.require(:user).permit(:username, :password)