diff --git a/app/controllers/organization_subscriptions_controller.rb b/app/controllers/organization_subscriptions_controller.rb new file mode 100644 index 000000000..1ee93eb02 --- /dev/null +++ b/app/controllers/organization_subscriptions_controller.rb @@ -0,0 +1,34 @@ +class OrganizationSubscriptionsController < ApplicationController + AUTHENTICATION_TOKEN_EXPIRY_TIME = 2.weeks + before_action :validate_token + before_action :skip_authorization + + def edit + end + + def update + @user.update!(subscribed_organization_ids: params[:user][:subscribed_organization_ids]) + flash[:notice] = 'Subscriptions Updated. Thanks!' + redirect_to root_url + end + + private + + def validate_token + @email_token = params[:token] + @user = User.find_by(email_authentication_token: @email_token) + if @user.nil? + redirect_to root_url + return + end + + created_at = @user.email_authentication_created_at + + if created_at < AUTHENTICATION_TOKEN_EXPIRY_TIME.ago + flash[:notice] = 'This link has expired.'\ + ' Please log in to change your email preferences' + redirect_to root_url + return + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 43653c92f..7834d4af1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -25,6 +25,7 @@ class User < ActiveRecord::Base validates_presence_of :first_name, :last_name, :profile validates_inclusion_of :time_zone, in: ActiveSupport::TimeZone.all.map(&:name), allow_blank: true + validates :email_authentication_token, length: { is: 32 }, allow_nil: true def self.from_omniauth(omniauth) authentication = Authentication.where(provider: omniauth['provider'], uid: omniauth['uid'].to_s).first @@ -86,4 +87,12 @@ def event_role(event) def event_checkiner?(event) event_attendance(event)[:checkiner] end + + def generate_email_authentication_token! + new_token = SecureRandom.base64[0..15] + SecureRandom.base64[0..15] + self.email_authentication_token = new_token + self.email_authentication_created_at = Time.now + return generate_email_authentication_token! unless self.save + new_token + end end diff --git a/app/views/organization_subscriptions/edit.html.erb b/app/views/organization_subscriptions/edit.html.erb new file mode 100644 index 000000000..b004da67e --- /dev/null +++ b/app/views/organization_subscriptions/edit.html.erb @@ -0,0 +1,12 @@ +<%= simple_form_for(@user, :url => "/organization_subscriptions/#{@email_token}", :html => {:method => 'put'}) do |f| %> + <%= devise_error_messages! %> + +
+ <%= f.label :subscribed_organization_ids, 'Allow these Bridge organizations to (potentially) send you email newsletters:' %> +
+ <%= f.collection_check_boxes(:subscribed_organization_ids, Organization.order(:name), :id, :name, item_wrapper_class: 'checkbox_row', item_wrapper_tag: 'div') %> +
+
+ +
<%= f.submit 'Update Email Preferences', class: 'btn btn-submit', data: {disable_with: 'Please wait...'} %>
+<% end %> diff --git a/config/routes.rb b/config/routes.rb index d9221e84a..c686285dd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -109,6 +109,9 @@ get :raise_exception end + get "/organization_subscriptions/:token" => "organization_subscriptions#edit" + put "/organization_subscriptions/:token" => "organization_subscriptions#update" + if Rails.env.development? get "/style_guide" => "static_pages#style_guide" end diff --git a/db/migrate/20160820181944_add_email_authentication_token_to_user.rb b/db/migrate/20160820181944_add_email_authentication_token_to_user.rb new file mode 100644 index 000000000..0adf96d11 --- /dev/null +++ b/db/migrate/20160820181944_add_email_authentication_token_to_user.rb @@ -0,0 +1,8 @@ +class AddEmailAuthenticationTokenToUser < ActiveRecord::Migration + def change + add_column :users, :email_authentication_token, :string + add_column :users, :email_authentication_created_at, :timestamp + + add_index :users, :email_authentication_token, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index b637a3267..c6c6d24de 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160807055624) do +ActiveRecord::Schema.define(version: 20160820181944) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" enable_extension "unaccent" @@ -281,12 +281,12 @@ end create_table "users", force: :cascade do |t| - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0 + t.integer "sign_in_count", default: 0 t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" t.string "current_sign_in_ip" @@ -294,21 +294,24 @@ t.string "confirmation_token" t.datetime "confirmed_at" t.datetime "confirmation_sent_at" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "admin", default: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "admin", default: false t.string "first_name" t.string "last_name" t.string "time_zone" t.string "gender" - t.boolean "allow_event_email", default: true - t.boolean "publisher", default: false - t.boolean "spammer", default: false - t.integer "authentications_count", default: 0 + t.boolean "allow_event_email", default: true + t.boolean "publisher", default: false + t.boolean "spammer", default: false + t.integer "authentications_count", default: 0 + t.string "email_authentication_token" + t.datetime "email_authentication_created_at" end add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true add_index "users", ["email"], name: "index_users_on_email", unique: true + add_index "users", ["email_authentication_token"], name: "index_users_on_email_authentication_token", unique: true add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true add_foreign_key "authentications", "users" diff --git a/spec/controllers/organization_subscriptions_controller_spec.rb b/spec/controllers/organization_subscriptions_controller_spec.rb new file mode 100644 index 000000000..ac02ced87 --- /dev/null +++ b/spec/controllers/organization_subscriptions_controller_spec.rb @@ -0,0 +1,85 @@ +require 'rails_helper' + +describe OrganizationSubscriptionsController do + before { @authorization_token = 'andyandhaseebaregarbageteacherss' } + + + describe '#edit' do + subject { get :edit, token: @authorization_token } + + context 'a user exists with the specified email_authorization_token' do + before do + @user = create(:user, + email_authentication_token: @authorization_token, + email_authentication_created_at: Time.now) + end + + it 'renders the edit form' do + expect(subject).to render_template(:edit) + end + + context 'the token was generated more than 2 weeks ago' do + before { @user.update! email_authentication_created_at: 3.weeks.ago } + + it 'redirects to the root' do + expect(subject).to redirect_to(root_url) + expect(flash[:notice]).to match('link has expired.') + end + end + end + + context 'a user does not exist with the specified email_authorization_token' do + it 'redirects to the root' do + expect(subject).to redirect_to(root_url) + end + end + end + + describe '#update' do + subject do + put :update, + token: @authorization_token, + user: { + subscribed_organization_ids: @subscribed_organizations + } + end + + context 'a user exists with the token' do + before do + @user = create(:user, + email_authentication_token: @authorization_token, + email_authentication_created_at: Time.now) + + 3.times do + create(:organization) + end + + @subscribed_organizations = Organization.all.pluck(:id).drop(1) + end + + it 'updates the email preferences of the user, without logging in' do + subject + expect(@user.subscribed_organizations) + .to match_array Organization.where(id: @subscribed_organizations) + expect(flash[:notice]).to match(/thanks/i) + expect(controller.current_user).to be_nil + end + + context 'the token was generated more than 2 weeks ago' do + before { @user.update! email_authentication_created_at: 3.weeks.ago } + + it 'redirects to the root' do + expect { + expect(subject).to redirect_to(root_url) + }.not_to change { @user.reload.subscribed_organizations } + end + end + end + + context 'a user does not exist with the token' do + it 'redirects to the root' do + expect(subject).to redirect_to(root_url) + end + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index bdb56cd69..d42f88efb 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -56,4 +56,24 @@ expect(@user.profile_path).to eq(Rails.application.routes.url_helpers.user_profile_path(@user)) end end + + describe '#generate_email_authentication_token' do + it 'generates a UID of length 32' do + @user.generate_email_authentication_token! + expect(@user.email_authentication_token.length).to eq 32 + end + + it 'sets the email_authentication_created_at' do + @user.generate_email_authentication_token! + expect(@user.email_authentication_created_at).to be > 1.minute.ago + end + + it 'does not reuse the same token when generated again' do + @user.generate_email_authentication_token! + first_token = @user.email_authentication_token + @user.generate_email_authentication_token! + second_token = @user.email_authentication_token + expect(first_token).to_not eq second_token + end + end end