From fa4f77c365d0636f71f8dced4fe282b250f38d44 Mon Sep 17 00:00:00 2001 From: ricola Date: Sat, 7 Jun 2025 18:38:33 -0600 Subject: [PATCH] Add password reset Closes #15 --- .gitignore | 1 + Gemfile | 1 + Gemfile.lock | 19 +++++- .../20250607233053_add_reset_to_users.rb | 5 ++ db/schema.rb | 3 +- po/ca/vedia.po | 61 ++++++++++++++----- po/vedia.pot | 57 +++++++++++++---- vedia.rb | 58 ++++++++++++++++++ views/login.erb | 5 +- views/reset.erb | 9 +++ views/reset_change.erb | 21 +++++++ views/reset_email.erb | 3 + views/reset_invalid.erb | 5 ++ views/reset_sent.erb | 6 ++ 14 files changed, 222 insertions(+), 32 deletions(-) create mode 100644 db/migrate/20250607233053_add_reset_to_users.rb create mode 100644 views/reset.erb create mode 100644 views/reset_change.erb create mode 100644 views/reset_email.erb create mode 100644 views/reset_invalid.erb create mode 100644 views/reset_sent.erb diff --git a/.gitignore b/.gitignore index 3e6f269..9020593 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ locale *.swp db/*.sqlite3 db/*.sqlite3-* +config/environments/development.rb diff --git a/Gemfile b/Gemfile index 866365e..30898d0 100644 --- a/Gemfile +++ b/Gemfile @@ -11,3 +11,4 @@ gem 'forwardable', '1.3.2' gem 'bcrypt' gem 'gettext' gem 'chartkick' +gem 'mail' diff --git a/Gemfile.lock b/Gemfile.lock index 86848de..c009320 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -26,6 +26,7 @@ GEM chartkick (5.1.4) concurrent-ruby (1.3.5) connection_pool (2.5.0) + date (3.4.1) drb (2.2.1) erubi (1.13.1) forwardable (1.3.2) @@ -39,10 +40,25 @@ GEM concurrent-ruby (~> 1.0) locale (2.1.4) logger (1.6.6) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + mini_mime (1.1.5) mini_portile2 (2.8.8) minitest (5.25.5) mustermann (3.0.3) ruby2_keywords (~> 0.0.1) + net-imap (0.5.8) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol nio4r (2.7.4) prime (0.1.3) forwardable @@ -76,8 +92,6 @@ GEM singleton (0.3.0) sqlite3 (2.6.0) mini_portile2 (~> 2.8.0) - sqlite3 (2.6.0-arm64-darwin) - sqlite3 (2.6.0-x86_64-darwin) text (1.3.1) tilt (2.6.0) timeout (0.4.3) @@ -96,6 +110,7 @@ DEPENDENCIES chartkick forwardable (= 1.3.2) gettext + mail puma rackup rake diff --git a/db/migrate/20250607233053_add_reset_to_users.rb b/db/migrate/20250607233053_add_reset_to_users.rb new file mode 100644 index 0000000..40aa9a7 --- /dev/null +++ b/db/migrate/20250607233053_add_reset_to_users.rb @@ -0,0 +1,5 @@ +class AddResetToUsers < ActiveRecord::Migration[7.2] + def change + add_column :users, :reset, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index f42b321..347a427 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[7.2].define(version: 2025_03_28_014902) do +ActiveRecord::Schema[7.2].define(version: 2025_06_07_233053) do create_table "candidates", force: :cascade do |t| t.integer "vote_id" t.string "name" @@ -44,6 +44,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_03_28_014902) do t.string "password" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "reset" end create_table "votes", force: :cascade do |t| diff --git a/po/ca/vedia.po b/po/ca/vedia.po index ae9e132..0682d88 100644 --- a/po/ca/vedia.po +++ b/po/ca/vedia.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-06-07 15:08-0600\n" +"POT-Creation-Date: 2025-06-07 18:22-0600\n" "PO-Revision-Date: 2025-03-29 20:41-0600\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,34 +17,38 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -#: ../vedia.rb:62 +#: ../vedia.rb:64 msgid "Awful" msgstr "Molt malament" -#: ../vedia.rb:63 +#: ../vedia.rb:65 msgid "Very bad" msgstr "Malament" -#: ../vedia.rb:64 +#: ../vedia.rb:66 msgid "Bad" msgstr "Poc bé" -#: ../vedia.rb:65 +#: ../vedia.rb:67 msgid "Mediocre" msgstr "Mig bé" -#: ../vedia.rb:66 +#: ../vedia.rb:68 msgid "Good" msgstr "Bé" -#: ../vedia.rb:67 +#: ../vedia.rb:69 msgid "Very good" msgstr "Molt bé" -#: ../vedia.rb:114 +#: ../vedia.rb:121 msgid "Incorrect email or password." msgstr "Correu o contrasenya incorrecte." +#: ../vedia.rb:139 +msgid "Reset your password" +msgstr "Reiniciar contrasenya" + #: ../views/home.erb:1 ../views/layout.erb:14 msgid "Home" msgstr "Inici" @@ -93,12 +97,13 @@ msgstr "Desconnexió" msgid "Login" msgstr "Connexió" -#: ../views/login.erb:9 ../views/signup.erb:24 ../views/votes_edit.erb:51 +#: ../views/login.erb:9 ../views/reset.erb:5 ../views/reset_change.erb:13 +#: ../views/signup.erb:24 ../views/votes_edit.erb:51 #: ../views/votes_show_closed.erb:85 ../views/votes_show_open.erb:58 msgid "Email" msgstr "Correu" -#: ../views/login.erb:13 ../views/signup.erb:28 +#: ../views/login.erb:13 ../views/reset_change.erb:17 ../views/signup.erb:28 msgid "Password" msgstr "Contrasenya" @@ -106,10 +111,40 @@ msgstr "Contrasenya" msgid "Create account" msgstr "Crear un compte" -#: ../views/signup.erb:6 +#: ../views/login.erb:19 ../views/reset.erb:1 ../views/reset.erb:8 +#: ../views/reset_change.erb:1 ../views/reset_change.erb:20 +#: ../views/reset_invalid.erb:1 ../views/reset_sent.erb:1 +msgid "Reset password" +msgstr "Reiniciar contrasenya" + +#: ../views/reset_change.erb:6 ../views/signup.erb:6 msgid "Specify a password." msgstr "Entra una contrasenya." +#: ../views/reset_email.erb:1 +msgid "Visit the following link to reset your password:" +msgstr "Visita aquest enllaç per reiniciar la teva contrasenya:" + +#: ../views/reset_invalid.erb:3 +msgid "This password reset link has expired or is invalid." +msgstr "Aquest enllaç per reiniciar una contrasenya ha expirat o és invàlid." + +#: ../views/reset_invalid.erb:5 +msgid "Try resetting your password again." +msgstr "Intenta reiniciar la teva contrasenya una altre vegada." + +#: ../views/reset_sent.erb:3 +msgid "" +"If an account exists for %{email}, you will get an email with a link\n" +"to reset your password." +msgstr "" +"Si existeix un compte per %{email}, rebràs un email amb un enllaç\n" +"per reiniciar la teva contrasenya." + +#: ../views/reset_sent.erb:6 +msgid "If you don't receive the email, please check your spam folder." +msgstr "Si no reps l'email, revisa el teu correu brossa." + #: ../views/signup.erb:14 msgid "Email is not a valid email address." msgstr "El correu no és una direcció de correu vàlida." @@ -238,7 +273,3 @@ msgstr "Tornar a l'esborrany de votació" #: ../views/votes_show_open.erb:51 msgid "Close votes and show results" msgstr "Tancar la votació i veure els resultats" - -#: ../views/layout.erb:5 -#~ msgid "Vote" -#~ msgstr "Votació" diff --git a/po/vedia.pot b/po/vedia.pot index 8ceaebc..089cb68 100644 --- a/po/vedia.pot +++ b/po/vedia.pot @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-06-07 15:08-0600\n" -"PO-Revision-Date: 2025-06-07 15:08-0600\n" +"POT-Creation-Date: 2025-06-07 18:22-0600\n" +"PO-Revision-Date: 2025-06-07 18:22-0600\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" @@ -18,34 +18,38 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -#: ../vedia.rb:62 +#: ../vedia.rb:64 msgid "Awful" msgstr "" -#: ../vedia.rb:63 +#: ../vedia.rb:65 msgid "Very bad" msgstr "" -#: ../vedia.rb:64 +#: ../vedia.rb:66 msgid "Bad" msgstr "" -#: ../vedia.rb:65 +#: ../vedia.rb:67 msgid "Mediocre" msgstr "" -#: ../vedia.rb:66 +#: ../vedia.rb:68 msgid "Good" msgstr "" -#: ../vedia.rb:67 +#: ../vedia.rb:69 msgid "Very good" msgstr "" -#: ../vedia.rb:114 +#: ../vedia.rb:121 msgid "Incorrect email or password." msgstr "" +#: ../vedia.rb:139 +msgid "Reset your password" +msgstr "" + #: ../views/home.erb:1 ../views/layout.erb:14 msgid "Home" msgstr "" @@ -94,12 +98,13 @@ msgstr "" msgid "Login" msgstr "" -#: ../views/login.erb:9 ../views/signup.erb:24 ../views/votes_edit.erb:51 +#: ../views/login.erb:9 ../views/reset.erb:5 ../views/reset_change.erb:13 +#: ../views/signup.erb:24 ../views/votes_edit.erb:51 #: ../views/votes_show_closed.erb:85 ../views/votes_show_open.erb:58 msgid "Email" msgstr "" -#: ../views/login.erb:13 ../views/signup.erb:28 +#: ../views/login.erb:13 ../views/reset_change.erb:17 ../views/signup.erb:28 msgid "Password" msgstr "" @@ -107,10 +112,38 @@ msgstr "" msgid "Create account" msgstr "" -#: ../views/signup.erb:6 +#: ../views/login.erb:19 ../views/reset.erb:1 ../views/reset.erb:8 +#: ../views/reset_change.erb:1 ../views/reset_change.erb:20 +#: ../views/reset_invalid.erb:1 ../views/reset_sent.erb:1 +msgid "Reset password" +msgstr "" + +#: ../views/reset_change.erb:6 ../views/signup.erb:6 msgid "Specify a password." msgstr "" +#: ../views/reset_email.erb:1 +msgid "Visit the following link to reset your password:" +msgstr "" + +#: ../views/reset_invalid.erb:3 +msgid "This password reset link has expired or is invalid." +msgstr "" + +#: ../views/reset_invalid.erb:5 +msgid "Try resetting your password again." +msgstr "" + +#: ../views/reset_sent.erb:3 +msgid "" +"If an account exists for %{email}, you will get an email with a link\n" +"to reset your password." +msgstr "" + +#: ../views/reset_sent.erb:6 +msgid "If you don't receive the email, please check your spam folder." +msgstr "" + #: ../views/signup.erb:14 msgid "Email is not a valid email address." msgstr "" diff --git a/vedia.rb b/vedia.rb index a4f7886..0b5eb2c 100644 --- a/vedia.rb +++ b/vedia.rb @@ -4,7 +4,9 @@ require 'bcrypt' require 'gettext' require 'securerandom' require 'chartkick' +require 'mail' require_relative 'mj' +require_relative "config/environments/#{settings.environment}" class Vote < ActiveRecord::Base has_many :candidates, dependent: :destroy @@ -65,6 +67,7 @@ set :values, [ { :id => 1, :label => _("Awful"), :color => '#ff4500' }, { :id => 4, :label => _("Mediocre"), :color => '#9acd32' }, { :id => 5, :label => _("Good"), :color => '#228b22' }, { :id => 6, :label => _("Very good"), :color => '#006400' } ] +set :admin_email, 'vedia@potager.org' MajorityJudgment.values = settings.values get '/' do @@ -107,6 +110,10 @@ end post '/login' do user = User.find_by(email: params[:email]) if user && verify_password(params[:password], user.password) + if not user.reset.nil? + user.reset = nil + user.save + end session.clear session[:user_id] = user.id redirect '/' @@ -116,6 +123,57 @@ post '/login' do end end +get '/reset' do + erb :reset +end + +post '/reset' do + @user = User.find_by(email: params[:email]) + if @user + @user.reset = SecureRandom.uuid + @user.save + mail = Mail.new + mail.from = settings.admin_email + mail.to = @user.email + mail.subject = _("Reset your password") + mail.body = erb :reset_email, :layout => false + mail.deliver + end + erb :reset_sent +end + +get '/reset/:uuid' do + @user = User.find_by(reset: params[:uuid]) + if @user + erb :reset_change + else + erb :reset_invalid + end +end + +post '/reset/:uuid' do + @user = User.find_by(reset: params[:uuid]) + if @user + @errors = [] + if params[:password].empty? + @errors << OpenStruct.new(:attribute => :password, :type => :blank) + else + @user.password = hash_password(params[:password]) + end + if @errors.empty? and @user.valid? + @user.reset = nil + @user.save + session.clear + session[:user_id] = @user.id + redirect '/' + else + erb :reset_change + end + else + erb :reset_invalid + end +end + get '/logout' do session.clear redirect '/login' diff --git a/views/login.erb b/views/login.erb index ada5979..dc71df6 100644 --- a/views/login.erb +++ b/views/login.erb @@ -4,7 +4,7 @@

<%= @error %>

<% end %> -
+

@@ -15,4 +15,5 @@

-<%= _("Create account") %> +

<%= _("Create account") %>

+

<%= _("Reset password") %>

diff --git a/views/reset.erb b/views/reset.erb new file mode 100644 index 0000000..4e9beb9 --- /dev/null +++ b/views/reset.erb @@ -0,0 +1,9 @@ +

<%= _("Reset password") %>

+ +
+

+ + +

+ +
diff --git a/views/reset_change.erb b/views/reset_change.erb new file mode 100644 index 0000000..36caf8d --- /dev/null +++ b/views/reset_change.erb @@ -0,0 +1,21 @@ +

<%= _("Reset password") %>

+ +<% if @errors %> +<% @errors.each do |error| %> + <% if error.attribute == :password and error.type == :blank %> +

<%= _("Specify a password.") %>

+ <% end %> +<% end %> +<% end %> + +
+

+ + <%= @user.email %> +

+

+ + +

+ +
diff --git a/views/reset_email.erb b/views/reset_email.erb new file mode 100644 index 0000000..4a2bd5a --- /dev/null +++ b/views/reset_email.erb @@ -0,0 +1,3 @@ +<%= _("Visit the following link to reset your password:") %> + +<%= "#{settings.base_url}reset/#{@user.reset}" %> diff --git a/views/reset_invalid.erb b/views/reset_invalid.erb new file mode 100644 index 0000000..47255cd --- /dev/null +++ b/views/reset_invalid.erb @@ -0,0 +1,5 @@ +

<%= _("Reset password") %>

+ +

<%= _("This password reset link has expired or is invalid.") %>

+ +

<%= _("Try resetting your password again.") %>

diff --git a/views/reset_sent.erb b/views/reset_sent.erb new file mode 100644 index 0000000..57ec828 --- /dev/null +++ b/views/reset_sent.erb @@ -0,0 +1,6 @@ +

<%= _("Reset password") %>

+ +

<%= _("If an account exists for %{email}, you will get an email with a link +to reset your password.") % { email: params[:email] } %>

+ +

<%= _("If you don't receive the email, please check your spam folder.") %>