From aa0986986f1b6544e918d3fd6af13336547feacd Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Tue, 19 Nov 2024 00:26:44 +0100 Subject: [PATCH 1/6] Install Rails' authentication generator --- Gemfile | 2 + Gemfile.lock | 2 + app/channels/application_cable/connection.rb | 14 ++++- app/controllers/application_controller.rb | 1 + app/controllers/concerns/authentication.rb | 55 ++++++++++++++++++++ app/controllers/passwords_controller.rb | 33 ++++++++++++ app/controllers/sessions_controller.rb | 21 ++++++++ app/mailers/passwords_mailer.rb | 6 +++ app/models/current.rb | 4 ++ app/models/session.rb | 22 ++++++++ app/models/user.rb | 20 +++++++ app/views/passwords/edit.html.erb | 9 ++++ app/views/passwords/new.html.erb | 8 +++ app/views/passwords_mailer/reset.html.erb | 4 ++ app/views/passwords_mailer/reset.text.erb | 2 + app/views/sessions/new.html.erb | 11 ++++ config/routes.rb | 2 + db/migrate/20241118232609_create_users.rb | 11 ++++ db/migrate/20241118232618_create_sessions.rb | 11 ++++ db/schema.rb | 22 ++++++-- 20 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 app/controllers/concerns/authentication.rb create mode 100644 app/controllers/passwords_controller.rb create mode 100644 app/controllers/sessions_controller.rb create mode 100644 app/mailers/passwords_mailer.rb create mode 100644 app/models/current.rb create mode 100644 app/models/session.rb create mode 100644 app/models/user.rb create mode 100644 app/views/passwords/edit.html.erb create mode 100644 app/views/passwords/new.html.erb create mode 100644 app/views/passwords_mailer/reset.html.erb create mode 100644 app/views/passwords_mailer/reset.text.erb create mode 100644 app/views/sessions/new.html.erb create mode 100644 db/migrate/20241118232609_create_users.rb create mode 100644 db/migrate/20241118232618_create_sessions.rb diff --git a/Gemfile b/Gemfile index 81b24ff..a62f8ee 100644 --- a/Gemfile +++ b/Gemfile @@ -39,3 +39,5 @@ end gem 'chroma' gem 'solid_queue', '~> 1.0' + +gem "bcrypt", "~> 3.1" diff --git a/Gemfile.lock b/Gemfile.lock index 9ea8033..ca48703 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -81,6 +81,7 @@ GEM babel-source (>= 4.0, < 6) execjs (~> 2.0) base64 (0.2.0) + bcrypt (3.1.20) benchmark (0.4.0) bigdecimal (3.1.8) bindex (0.8.1) @@ -363,6 +364,7 @@ PLATFORMS DEPENDENCIES annotaterb + bcrypt (~> 3.1) bootsnap chroma csv diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 1cc97c6..4264c74 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -1,6 +1,16 @@ -# Copyright (C) 2024 Manuel Bustillo - module ApplicationCable class Connection < ActionCable::Connection::Base + identified_by :current_user + + def connect + set_current_user || reject_unauthorized_connection + end + + private + def set_current_user + if session = Session.find_by(id: cookies.signed[:session_id]) + self.current_user = session.user + end + end end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 57926f3..54431ec 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,6 +1,7 @@ # Copyright (C) 2024 Manuel Bustillo class ApplicationController < ActionController::Base + include Authentication after_action :set_csrf_cookie skip_before_action :verify_authenticity_token, if: :development_swagger? diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb new file mode 100644 index 0000000..771b21d --- /dev/null +++ b/app/controllers/concerns/authentication.rb @@ -0,0 +1,55 @@ +module Authentication + extend ActiveSupport::Concern + + included do + before_action :require_authentication + helper_method :authenticated? + end + + class_methods do + def allow_unauthenticated_access(**options) + skip_before_action :require_authentication, **options + end + end + + private + def authenticated? + resume_session + end + + def require_authentication + resume_session || request_authentication + end + + + def resume_session + Current.session ||= find_session_by_cookie + end + + def find_session_by_cookie + Session.find_by(id: cookies.signed[:session_id]) + end + + + def request_authentication + session[:return_to_after_authenticating] = request.url + redirect_to new_session_path + end + + def after_authentication_url + session.delete(:return_to_after_authenticating) || root_url + end + + + def start_new_session_for(user) + user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session| + Current.session = session + cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax } + end + end + + def terminate_session + Current.session.destroy + cookies.delete(:session_id) + end +end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb new file mode 100644 index 0000000..0c4b4a8 --- /dev/null +++ b/app/controllers/passwords_controller.rb @@ -0,0 +1,33 @@ +class PasswordsController < ApplicationController + allow_unauthenticated_access + before_action :set_user_by_token, only: %i[ edit update ] + + def new + end + + def create + if user = User.find_by(email_address: params[:email_address]) + PasswordsMailer.reset(user).deliver_later + end + + redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)." + end + + def edit + end + + def update + if @user.update(params.permit(:password, :password_confirmation)) + redirect_to new_session_path, notice: "Password has been reset." + else + redirect_to edit_password_path(params[:token]), alert: "Passwords did not match." + end + end + + private + def set_user_by_token + @user = User.find_by_password_reset_token!(params[:token]) + rescue ActiveSupport::MessageVerifier::InvalidSignature + redirect_to new_password_path, alert: "Password reset link is invalid or has expired." + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..9785c92 --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,21 @@ +class SessionsController < ApplicationController + allow_unauthenticated_access only: %i[ new create ] + rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." } + + def new + end + + def create + if user = User.authenticate_by(params.permit(:email_address, :password)) + start_new_session_for user + redirect_to after_authentication_url + else + redirect_to new_session_path, alert: "Try another email address or password." + end + end + + def destroy + terminate_session + redirect_to new_session_path + end +end diff --git a/app/mailers/passwords_mailer.rb b/app/mailers/passwords_mailer.rb new file mode 100644 index 0000000..4f0ac7f --- /dev/null +++ b/app/mailers/passwords_mailer.rb @@ -0,0 +1,6 @@ +class PasswordsMailer < ApplicationMailer + def reset(user) + @user = user + mail subject: "Reset your password", to: user.email_address + end +end diff --git a/app/models/current.rb b/app/models/current.rb new file mode 100644 index 0000000..2bef56d --- /dev/null +++ b/app/models/current.rb @@ -0,0 +1,4 @@ +class Current < ActiveSupport::CurrentAttributes + attribute :session + delegate :user, to: :session, allow_nil: true +end diff --git a/app/models/session.rb b/app/models/session.rb new file mode 100644 index 0000000..688bbd3 --- /dev/null +++ b/app/models/session.rb @@ -0,0 +1,22 @@ +# == Schema Information +# +# Table name: sessions +# +# id :bigint not null, primary key +# ip_address :string +# user_agent :string +# created_at :datetime not null +# updated_at :datetime not null +# user_id :bigint not null +# +# Indexes +# +# index_sessions_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (user_id => users.id) +# +class Session < ApplicationRecord + belongs_to :user +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..6b2fd8a --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,20 @@ +# == Schema Information +# +# Table name: users +# +# id :bigint not null, primary key +# email_address :string not null +# password_digest :string not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_users_on_email_address (email_address) UNIQUE +# +class User < ApplicationRecord + has_secure_password + has_many :sessions, dependent: :destroy + + normalizes :email_address, with: ->(e) { e.strip.downcase } +end diff --git a/app/views/passwords/edit.html.erb b/app/views/passwords/edit.html.erb new file mode 100644 index 0000000..9f0c87c --- /dev/null +++ b/app/views/passwords/edit.html.erb @@ -0,0 +1,9 @@ +

Update your password

+ +<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> + +<%= form_with url: password_path(params[:token]), method: :put do |form| %> + <%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72 %>
+ <%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72 %>
+ <%= form.submit "Save" %> +<% end %> diff --git a/app/views/passwords/new.html.erb b/app/views/passwords/new.html.erb new file mode 100644 index 0000000..44efb2b --- /dev/null +++ b/app/views/passwords/new.html.erb @@ -0,0 +1,8 @@ +

Forgot your password?

+ +<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> + +<%= form_with url: passwords_path do |form| %> + <%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %>
+ <%= form.submit "Email reset instructions" %> +<% end %> diff --git a/app/views/passwords_mailer/reset.html.erb b/app/views/passwords_mailer/reset.html.erb new file mode 100644 index 0000000..4a06619 --- /dev/null +++ b/app/views/passwords_mailer/reset.html.erb @@ -0,0 +1,4 @@ +

+ You can reset your password within the next 15 minutes on + <%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>. +

diff --git a/app/views/passwords_mailer/reset.text.erb b/app/views/passwords_mailer/reset.text.erb new file mode 100644 index 0000000..2cf03fc --- /dev/null +++ b/app/views/passwords_mailer/reset.text.erb @@ -0,0 +1,2 @@ +You can reset your password within the next 15 minutes on this password reset page: +<%= edit_password_url(@user.password_reset_token) %> diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb new file mode 100644 index 0000000..ff641c4 --- /dev/null +++ b/app/views/sessions/new.html.erb @@ -0,0 +1,11 @@ +<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> +<%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %> + +<%= form_with url: session_path do |form| %> + <%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %>
+ <%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72 %>
+ <%= form.submit "Sign in" %> +<% end %> +
+ +<%= link_to "Forgot password?", new_password_path %> diff --git a/config/routes.rb b/config/routes.rb index c9d7ee6..537876a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,8 @@ # Copyright (C) 2024 Manuel Bustillo Rails.application.routes.draw do + resource :session + resources :passwords, param: :token mount Rswag::Ui::Engine => '/api-docs' mount Rswag::Api::Engine => '/api-docs' resources :groups, only: :index diff --git a/db/migrate/20241118232609_create_users.rb b/db/migrate/20241118232609_create_users.rb new file mode 100644 index 0000000..2075edf --- /dev/null +++ b/db/migrate/20241118232609_create_users.rb @@ -0,0 +1,11 @@ +class CreateUsers < ActiveRecord::Migration[8.0] + def change + create_table :users do |t| + t.string :email_address, null: false + t.string :password_digest, null: false + + t.timestamps + end + add_index :users, :email_address, unique: true + end +end diff --git a/db/migrate/20241118232618_create_sessions.rb b/db/migrate/20241118232618_create_sessions.rb new file mode 100644 index 0000000..8102f13 --- /dev/null +++ b/db/migrate/20241118232618_create_sessions.rb @@ -0,0 +1,11 @@ +class CreateSessions < ActiveRecord::Migration[8.0] + def change + create_table :sessions do |t| + t.references :user, null: false, foreign_key: true + t.string :ip_address + t.string :user_agent + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index c54ec05..98095a5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1,5 +1,3 @@ -# Copyright (C) 2024 Manuel Bustillo - # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. @@ -12,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_11_11_063741) do +ActiveRecord::Schema[8.0].define(version: 2024_11_18_232618) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -60,6 +58,15 @@ ActiveRecord::Schema[8.0].define(version: 2024_11_11_063741) do t.index ["tables_arrangement_id"], name: "index_seats_on_tables_arrangement_id" end + create_table "sessions", force: :cascade do |t| + t.bigint "user_id", null: false + t.string "ip_address" + t.string "user_agent" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_sessions_on_user_id" + end + create_table "solid_queue_blocked_executions", force: :cascade do |t| t.bigint "job_id", null: false t.string "queue_name", null: false @@ -188,10 +195,19 @@ ActiveRecord::Schema[8.0].define(version: 2024_11_11_063741) do t.string "name", null: false end + create_table "users", force: :cascade do |t| + t.string "email_address", null: false + t.string "password_digest", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["email_address"], name: "index_users_on_email_address", unique: true + end + add_foreign_key "groups", "groups", column: "parent_id" add_foreign_key "guests", "groups" add_foreign_key "seats", "guests" add_foreign_key "seats", "tables_arrangements", on_delete: :cascade + add_foreign_key "sessions", "users" 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 add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade -- 2.47.1 From 134bf27955e6caab0f7cc731f72551daae6f2a17 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Mon, 18 Nov 2024 23:27:50 +0000 Subject: [PATCH 2/6] Add copyright notice --- app/channels/application_cable/connection.rb | 2 ++ app/controllers/concerns/authentication.rb | 2 ++ app/controllers/passwords_controller.rb | 2 ++ app/controllers/sessions_controller.rb | 2 ++ app/mailers/passwords_mailer.rb | 2 ++ app/models/current.rb | 2 ++ app/models/session.rb | 2 ++ app/models/user.rb | 2 ++ app/views/passwords/edit.html.erb | 2 ++ app/views/passwords/new.html.erb | 2 ++ app/views/passwords_mailer/reset.html.erb | 2 ++ app/views/passwords_mailer/reset.text.erb | 2 ++ app/views/sessions/new.html.erb | 2 ++ db/migrate/20241118232609_create_users.rb | 2 ++ db/migrate/20241118232618_create_sessions.rb | 2 ++ db/schema.rb | 2 ++ 16 files changed, 32 insertions(+) diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 4264c74..abaef27 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -1,3 +1,5 @@ +# Copyright (C) 2024 Manuel Bustillo + module ApplicationCable class Connection < ActionCable::Connection::Base identified_by :current_user diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index 771b21d..5275e10 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -1,3 +1,5 @@ +# Copyright (C) 2024 Manuel Bustillo + module Authentication extend ActiveSupport::Concern diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 0c4b4a8..49e8595 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -1,3 +1,5 @@ +# Copyright (C) 2024 Manuel Bustillo + class PasswordsController < ApplicationController allow_unauthenticated_access before_action :set_user_by_token, only: %i[ edit update ] diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 9785c92..6a46998 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,3 +1,5 @@ +# Copyright (C) 2024 Manuel Bustillo + class SessionsController < ApplicationController allow_unauthenticated_access only: %i[ new create ] rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." } diff --git a/app/mailers/passwords_mailer.rb b/app/mailers/passwords_mailer.rb index 4f0ac7f..f3692f0 100644 --- a/app/mailers/passwords_mailer.rb +++ b/app/mailers/passwords_mailer.rb @@ -1,3 +1,5 @@ +# Copyright (C) 2024 Manuel Bustillo + class PasswordsMailer < ApplicationMailer def reset(user) @user = user diff --git a/app/models/current.rb b/app/models/current.rb index 2bef56d..f7b1ab9 100644 --- a/app/models/current.rb +++ b/app/models/current.rb @@ -1,3 +1,5 @@ +# Copyright (C) 2024 Manuel Bustillo + class Current < ActiveSupport::CurrentAttributes attribute :session delegate :user, to: :session, allow_nil: true diff --git a/app/models/session.rb b/app/models/session.rb index 688bbd3..9ba68b4 100644 --- a/app/models/session.rb +++ b/app/models/session.rb @@ -1,3 +1,5 @@ +# Copyright (C) 2024 Manuel Bustillo + # == Schema Information # # Table name: sessions diff --git a/app/models/user.rb b/app/models/user.rb index 6b2fd8a..6c752a9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,3 +1,5 @@ +# Copyright (C) 2024 Manuel Bustillo + # == Schema Information # # Table name: users diff --git a/app/views/passwords/edit.html.erb b/app/views/passwords/edit.html.erb index 9f0c87c..8b76bbf 100644 --- a/app/views/passwords/edit.html.erb +++ b/app/views/passwords/edit.html.erb @@ -1,3 +1,5 @@ +<%# Copyright (C) 2024 Manuel Bustillo %> +

Update your password

<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> diff --git a/app/views/passwords/new.html.erb b/app/views/passwords/new.html.erb index 44efb2b..2572313 100644 --- a/app/views/passwords/new.html.erb +++ b/app/views/passwords/new.html.erb @@ -1,3 +1,5 @@ +<%# Copyright (C) 2024 Manuel Bustillo %> +

Forgot your password?

<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> diff --git a/app/views/passwords_mailer/reset.html.erb b/app/views/passwords_mailer/reset.html.erb index 4a06619..9b536ff 100644 --- a/app/views/passwords_mailer/reset.html.erb +++ b/app/views/passwords_mailer/reset.html.erb @@ -1,3 +1,5 @@ +<%# Copyright (C) 2024 Manuel Bustillo %> +

You can reset your password within the next 15 minutes on <%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>. diff --git a/app/views/passwords_mailer/reset.text.erb b/app/views/passwords_mailer/reset.text.erb index 2cf03fc..0ce9c12 100644 --- a/app/views/passwords_mailer/reset.text.erb +++ b/app/views/passwords_mailer/reset.text.erb @@ -1,2 +1,4 @@ +<%# Copyright (C) 2024 Manuel Bustillo %> + You can reset your password within the next 15 minutes on this password reset page: <%= edit_password_url(@user.password_reset_token) %> diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index ff641c4..e78774d 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -1,3 +1,5 @@ +<%# Copyright (C) 2024 Manuel Bustillo %> + <%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> <%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %> diff --git a/db/migrate/20241118232609_create_users.rb b/db/migrate/20241118232609_create_users.rb index 2075edf..b1f1381 100644 --- a/db/migrate/20241118232609_create_users.rb +++ b/db/migrate/20241118232609_create_users.rb @@ -1,3 +1,5 @@ +# Copyright (C) 2024 Manuel Bustillo + class CreateUsers < ActiveRecord::Migration[8.0] def change create_table :users do |t| diff --git a/db/migrate/20241118232618_create_sessions.rb b/db/migrate/20241118232618_create_sessions.rb index 8102f13..3cc4be3 100644 --- a/db/migrate/20241118232618_create_sessions.rb +++ b/db/migrate/20241118232618_create_sessions.rb @@ -1,3 +1,5 @@ +# Copyright (C) 2024 Manuel Bustillo + class CreateSessions < ActiveRecord::Migration[8.0] def change create_table :sessions do |t| diff --git a/db/schema.rb b/db/schema.rb index 98095a5..02eeea6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1,3 +1,5 @@ +# Copyright (C) 2024 Manuel Bustillo + # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. -- 2.47.1 From 3e38630eb45ec494e25aa9fb3435e8b7655a60a6 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Tue, 19 Nov 2024 00:32:24 +0100 Subject: [PATCH 3/6] Refine controllers --- app/controllers/passwords_controller.rb | 25 ++++++++++--------------- app/controllers/sessions_controller.rb | 14 ++++++-------- config/routes.rb | 4 ++-- 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 49e8595..9877034 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -2,34 +2,29 @@ class PasswordsController < ApplicationController allow_unauthenticated_access - before_action :set_user_by_token, only: %i[ edit update ] - - def new - end + before_action :set_user_by_token, only: :update def create if user = User.find_by(email_address: params[:email_address]) PasswordsMailer.reset(user).deliver_later end - redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)." - end - - def edit + render json: {}, status: :ok end def update if @user.update(params.permit(:password, :password_confirmation)) - redirect_to new_session_path, notice: "Password has been reset." + render json: {}, status: :ok else - redirect_to edit_password_path(params[:token]), alert: "Passwords did not match." + render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity end end private - def set_user_by_token - @user = User.find_by_password_reset_token!(params[:token]) - rescue ActiveSupport::MessageVerifier::InvalidSignature - redirect_to new_password_path, alert: "Password reset link is invalid or has expired." - end + + def set_user_by_token + @user = User.find_by_password_reset_token!(params[:token]) + rescue ActiveSupport::MessageVerifier::InvalidSignature + redirect_to new_password_path, alert: 'Password reset link is invalid or has expired.' + end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 6a46998..aa74562 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,23 +1,21 @@ # Copyright (C) 2024 Manuel Bustillo class SessionsController < ApplicationController - allow_unauthenticated_access only: %i[ new create ] - rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." } - - def new - end + allow_unauthenticated_access only: :create + rate_limit to: 10, within: 3.minutes, only: :create, + with: -> { render json: { errors: ['Rate limit exceeded'] }, status: :too_many_requests } def create if user = User.authenticate_by(params.permit(:email_address, :password)) start_new_session_for user - redirect_to after_authentication_url + render json: {}, status: :created else - redirect_to new_session_path, alert: "Try another email address or password." + render json: { errors: ['Invalid email address or password'] }, status: :unauthorized end end def destroy terminate_session - redirect_to new_session_path + render json: {}, status: :ok end end diff --git a/config/routes.rb b/config/routes.rb index 537876a..ed59be2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,8 +1,8 @@ # Copyright (C) 2024 Manuel Bustillo Rails.application.routes.draw do - resource :session - resources :passwords, param: :token + resource :session, only: %i[create destroy] + resources :passwords, param: :token, only: %w[create update] mount Rswag::Ui::Engine => '/api-docs' mount Rswag::Api::Engine => '/api-docs' resources :groups, only: :index -- 2.47.1 From 8b336164362bada541f1dbac48dd46419638d5f5 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Tue, 19 Nov 2024 00:33:26 +0100 Subject: [PATCH 4/6] Remove unnecessary views --- app/views/passwords/edit.html.erb | 11 ----------- app/views/passwords/new.html.erb | 10 ---------- app/views/sessions/new.html.erb | 13 ------------- 3 files changed, 34 deletions(-) delete mode 100644 app/views/passwords/edit.html.erb delete mode 100644 app/views/passwords/new.html.erb delete mode 100644 app/views/sessions/new.html.erb diff --git a/app/views/passwords/edit.html.erb b/app/views/passwords/edit.html.erb deleted file mode 100644 index 8b76bbf..0000000 --- a/app/views/passwords/edit.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -<%# Copyright (C) 2024 Manuel Bustillo %> - -

Update your password

- -<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> - -<%= form_with url: password_path(params[:token]), method: :put do |form| %> - <%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72 %>
- <%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72 %>
- <%= form.submit "Save" %> -<% end %> diff --git a/app/views/passwords/new.html.erb b/app/views/passwords/new.html.erb deleted file mode 100644 index 2572313..0000000 --- a/app/views/passwords/new.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -<%# Copyright (C) 2024 Manuel Bustillo %> - -

Forgot your password?

- -<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> - -<%= form_with url: passwords_path do |form| %> - <%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %>
- <%= form.submit "Email reset instructions" %> -<% end %> diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb deleted file mode 100644 index e78774d..0000000 --- a/app/views/sessions/new.html.erb +++ /dev/null @@ -1,13 +0,0 @@ -<%# Copyright (C) 2024 Manuel Bustillo %> - -<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> -<%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %> - -<%= form_with url: session_path do |form| %> - <%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %>
- <%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72 %>
- <%= form.submit "Sign in" %> -<% end %> -
- -<%= link_to "Forgot password?", new_password_path %> -- 2.47.1 From 0e0da9c765457a1a9ef3137c3bf2f6f6939971a4 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Tue, 19 Nov 2024 08:56:51 +0100 Subject: [PATCH 5/6] Refine and document controllers --- app/controllers/passwords_controller.rb | 4 +-- spec/requests/passwords_spec.rb | 40 +++++++++++++++++++++++++ spec/requests/sessions_spec.rb | 29 ++++++++++++++++++ spec/swagger_response_helper.rb | 16 ++++++++++ 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 spec/requests/passwords_spec.rb create mode 100644 spec/requests/sessions_spec.rb diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 9877034..1189710 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -9,7 +9,7 @@ class PasswordsController < ApplicationController PasswordsMailer.reset(user).deliver_later end - render json: {}, status: :ok + render json: {}, status: :created end def update @@ -25,6 +25,6 @@ class PasswordsController < ApplicationController def set_user_by_token @user = User.find_by_password_reset_token!(params[:token]) rescue ActiveSupport::MessageVerifier::InvalidSignature - redirect_to new_password_path, alert: 'Password reset link is invalid or has expired.' + render json: { errors: ['Password reset link is invalid or has expired.'] }, status: :unprocessable_entity end end diff --git a/spec/requests/passwords_spec.rb b/spec/requests/passwords_spec.rb new file mode 100644 index 0000000..510177a --- /dev/null +++ b/spec/requests/passwords_spec.rb @@ -0,0 +1,40 @@ +require 'swagger_helper' + +RSpec.describe 'passwords', type: :request do + path '/passwords' do + post('send a password (re)set email') do + tags 'Passwords' + consumes 'application/json' + produces 'application/json' + parameter name: :body, in: :body, schema: { + type: :object, + required: [:email_address], + properties: { + email_address: { type: :string, format: :email } + } + } + + response_empty_201 + end + end + + path '/passwords/{token}' do + parameter name: 'token', in: :path, type: :string, description: 'token' + put('update password') do + tags 'Passwords' + consumes 'application/json' + produces 'application/json' + parameter name: :body, in: :body, schema: { + type: :object, + required: %i[password password_confirmation], + properties: { + password: { type: :string }, + password_confirmation: { type: :string } + } + } + + response_empty_200 + response_422 + end + end +end diff --git a/spec/requests/sessions_spec.rb b/spec/requests/sessions_spec.rb new file mode 100644 index 0000000..3800088 --- /dev/null +++ b/spec/requests/sessions_spec.rb @@ -0,0 +1,29 @@ +require 'swagger_helper' + +RSpec.describe 'sessions', type: :request do + path '/session' do + delete('delete session') do + tags 'Sessions' + produces 'application/json' + response_empty_200 + end + + post('create session') do + tags 'Sessions' + consumes 'application/json' + produces 'application/json' + parameter name: :body, in: :body, schema: { + type: :object, + required: %i[email_address password], + properties: { + email_address: { type: :string, format: :email }, + password: { type: :string } + } + } + + response_empty_201 + response_401 + response_429 + end + end +end diff --git a/spec/swagger_response_helper.rb b/spec/swagger_response_helper.rb index 1f84ebd..d0812dd 100644 --- a/spec/swagger_response_helper.rb +++ b/spec/swagger_response_helper.rb @@ -9,6 +9,22 @@ module SwaggerResponseHelper end end + def response_429 + response(429, 'Rate limit exceeded') do + produces 'application/json' + error_schema + xit + end + end + + def response_401 + response(401, 'Unauthorized') do + produces 'application/json' + error_schema + xit + end + end + def response_empty_200 response(200, 'Success') do produces 'application/json' -- 2.47.1 From b215e8a3b40be089e7359cfd424f807c2d54b1c2 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Tue, 19 Nov 2024 07:57:42 +0000 Subject: [PATCH 6/6] Add copyright notice --- spec/requests/passwords_spec.rb | 2 ++ spec/requests/sessions_spec.rb | 2 ++ 2 files changed, 4 insertions(+) diff --git a/spec/requests/passwords_spec.rb b/spec/requests/passwords_spec.rb index 510177a..c83a450 100644 --- a/spec/requests/passwords_spec.rb +++ b/spec/requests/passwords_spec.rb @@ -1,3 +1,5 @@ +# Copyright (C) 2024 Manuel Bustillo + require 'swagger_helper' RSpec.describe 'passwords', type: :request do diff --git a/spec/requests/sessions_spec.rb b/spec/requests/sessions_spec.rb index 3800088..f836a93 100644 --- a/spec/requests/sessions_spec.rb +++ b/spec/requests/sessions_spec.rb @@ -1,3 +1,5 @@ +# Copyright (C) 2024 Manuel Bustillo + require 'swagger_helper' RSpec.describe 'sessions', type: :request do -- 2.47.1