diff --git a/Gemfile b/Gemfile index 17e0125cca..4d81ebd240 100644 --- a/Gemfile +++ b/Gemfile @@ -24,7 +24,6 @@ gem 'spree_api', github: 'openfoodfoundation/spree', branch: '2-0-4-stable' gem 'spree_backend', github: 'openfoodfoundation/spree', branch: '2-0-4-stable' gem 'spree_core', github: 'openfoodfoundation/spree', branch: '2-0-4-stable' -gem 'spree_auth_devise', github: 'spree/spree_auth_devise', branch: '2-0-stable' gem 'spree_i18n', github: 'spree/spree_i18n', branch: '1-3-stable' # Our branch contains two changes @@ -37,6 +36,8 @@ gem 'stripe' # which is needed for Pin Payments (and possibly others). gem 'activemerchant', '~> 1.78' +gem 'devise', '~> 2.2.5' +gem 'devise-encryptable', '0.1.2' gem 'jwt', '~> 2.2' gem 'oauth2', '~> 1.4.1' # Used for Stripe Connect diff --git a/Gemfile.lock b/Gemfile.lock index fb2d102817..d840fed854 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -66,26 +66,6 @@ GIT state_machine (= 1.2.0) stringex (~> 1.5.1) truncate_html (= 0.9.2) - spree_frontend (2.0.4) - canonical-rails - deface (>= 0.9.0) - jquery-rails (~> 3.0.0) - rails (~> 3.2.13) - spree_api (= 2.0.4) - spree_core (= 2.0.4) - stringex (~> 1.5.1) - -GIT - remote: https://github.com/spree/spree_auth_devise.git - revision: 0181835fb6ac77a05191d26f6f32a0f4a548d851 - branch: 2-0-stable - specs: - spree_auth_devise (2.0.0) - devise (~> 2.2.5) - devise-encryptable (= 0.1.2) - spree_backend (~> 2.0.0) - spree_core (~> 2.0.0) - spree_frontend (~> 2.0.0) GIT remote: https://github.com/spree/spree_i18n.git @@ -180,7 +160,7 @@ GEM json (~> 1.4) nokogiri (>= 1.4.4) uuidtools (~> 2.1) - bcrypt (3.1.11) + bcrypt (3.1.13) bcrypt-ruby (3.1.5) bcrypt (>= 3.1.3) blockenspiel (0.5.0) @@ -189,8 +169,6 @@ GEM builder (3.0.4) byebug (9.0.6) cancan (1.6.10) - canonical-rails (0.1.0) - rails (>= 3.1, < 5.1) capybara (2.18.0) addressable mini_mime (>= 0.1.3) @@ -801,6 +779,8 @@ DEPENDENCIES deface (= 1.0.2) delayed_job_active_record delayed_job_web + devise (~> 2.2.5) + devise-encryptable (= 0.1.2) diffy eventmachine (>= 1.2.3) factory_bot_rails @@ -861,7 +841,6 @@ DEPENDENCIES skylight (< 2.0) spinjs-rails spree_api! - spree_auth_devise! spree_backend! spree_core! spree_i18n! diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 80625cfa55..d5600915d7 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -15,7 +15,6 @@ //= require angular-animate //= require angular-sanitize //= require admin/spree_backend -//= require admin/spree_auth //= require admin/spree_paypal_express //= require ../shared/ng-infinite-scroll.min.js //= require ../shared/ng-tags-input.min.js diff --git a/app/assets/stylesheets/admin/all.scss b/app/assets/stylesheets/admin/all.scss index 0e075ccc90..376c924105 100644 --- a/app/assets/stylesheets/admin/all.scss +++ b/app/assets/stylesheets/admin/all.scss @@ -5,7 +5,6 @@ * *= require admin/spree_backend - *= require admin/spree_auth *= require jquery-ui-timepicker-addon *= require shared/textAngular diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ece563a420..0edfdd76b7 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,5 @@ require 'open_food_network/referer_parser' +require 'spree/authentication_helpers' class ApplicationController < ActionController::Base protect_from_forgery @@ -7,6 +8,7 @@ class ApplicationController < ActionController::Base before_filter :set_cache_headers # prevent cart emptying via cache when using back button #1213 include EnterprisesHelper + include Spree::AuthenticationHelpers def redirect_to(options = {}, response_status = {}) ::Rails.logger.error("Redirected by #{begin diff --git a/app/controllers/checkout_controller.rb b/app/controllers/checkout_controller.rb index 1ef23d92a9..c1fe12ce53 100644 --- a/app/controllers/checkout_controller.rb +++ b/app/controllers/checkout_controller.rb @@ -8,7 +8,6 @@ class CheckoutController < Spree::CheckoutController prepend_before_filter :require_order_cycle prepend_before_filter :require_distributor_chosen - skip_before_filter :check_registration before_filter :enable_embedded_shopfront include OrderCyclesHelper diff --git a/app/controllers/metal_decorator.rb b/app/controllers/metal_decorator.rb new file mode 100644 index 0000000000..18ec22e4c8 --- /dev/null +++ b/app/controllers/metal_decorator.rb @@ -0,0 +1,6 @@ +# For the API +ActionController::Metal.class_eval do + def spree_current_user + @spree_current_user ||= env['warden'].user + end +end diff --git a/app/controllers/spree/admin/base_controller_decorator.rb b/app/controllers/spree/admin/base_controller_decorator.rb index 6bdd57f0e8..941b679c59 100644 --- a/app/controllers/spree/admin/base_controller_decorator.rb +++ b/app/controllers/spree/admin/base_controller_decorator.rb @@ -47,6 +47,16 @@ Spree::Admin::BaseController.class_eval do end end + protected + + def model_class + const_name = controller_name.classify + if Spree.const_defined?(const_name) + return "Spree::#{const_name}".constantize + end + nil + end + private def active_distributors_not_ready_for_checkout diff --git a/app/controllers/spree/admin/orders/customer_details_controller_decorator.rb b/app/controllers/spree/admin/orders/customer_details_controller_decorator.rb index 41a3d721b1..47e829d3cb 100644 --- a/app/controllers/spree/admin/orders/customer_details_controller_decorator.rb +++ b/app/controllers/spree/admin/orders/customer_details_controller_decorator.rb @@ -1,4 +1,5 @@ Spree::Admin::Orders::CustomerDetailsController.class_eval do + before_filter :check_authorization before_filter :set_guest_checkout_status, only: :update def update @@ -25,6 +26,17 @@ Spree::Admin::Orders::CustomerDetailsController.class_eval do private + def check_authorization + load_order + session[:access_token] ||= params[:token] + + resource = @order + action = params[:action].to_sym + action = :edit if action == :show # show route renders :edit for this controller + + authorize! action, resource, session[:access_token] + end + def set_guest_checkout_status registered_user = Spree::User.find_by_email(params[:order][:email]) diff --git a/app/controllers/spree/admin/resource_controller_decorator.rb b/app/controllers/spree/admin/resource_controller_decorator.rb index 19293545ec..e7ba7ceb5b 100644 --- a/app/controllers/spree/admin/resource_controller_decorator.rb +++ b/app/controllers/spree/admin/resource_controller_decorator.rb @@ -14,3 +14,7 @@ module AuthorizeOnLoadResource end Spree::Admin::ResourceController.prepend(AuthorizeOnLoadResource) + +Spree::Admin::ResourceController.class_eval do + rescue_from CanCan::AccessDenied, :with => :unauthorized +end diff --git a/app/controllers/spree/admin/users_controller.rb b/app/controllers/spree/admin/users_controller.rb new file mode 100644 index 0000000000..17ab1d041d --- /dev/null +++ b/app/controllers/spree/admin/users_controller.rb @@ -0,0 +1,131 @@ +module Spree + module Admin + class UsersController < ResourceController + rescue_from Spree::User::DestroyWithOrdersError, with: :user_destroy_with_orders_error + + after_filter :sign_in_if_change_own_password, only: :update + + # http://spreecommerce.com/blog/2010/11/02/json-hijacking-vulnerability/ + before_filter :check_json_authenticity, only: :index + before_filter :load_roles, only: [:edit, :new, :update, :create, + :generate_api_key, :clear_api_key] + + def index + respond_with(@collection) do |format| + format.html + format.json { render json: json_data } + end + end + + def create + if params[:user] + roles = params[:user].delete("spree_role_ids") + end + + @user = Spree::User.new(params[:user]) + if @user.save + + if roles + @user.spree_roles = roles.reject(&:blank?).collect{ |r| Spree::Role.find(r) } + end + + flash.now[:success] = Spree.t(:created_successfully) + render :edit + else + render :new + end + end + + def update + if params[:user] + roles = params[:user].delete("spree_role_ids") + end + + if @user.update_attributes(params[:user]) + if roles + @user.spree_roles = roles.reject(&:blank?).collect{ |r| Spree::Role.find(r) } + end + + flash.now[:success] = Spree.t(:account_updated) + end + render :edit + end + + def generate_api_key + if @user.generate_spree_api_key! + flash[:success] = Spree.t('api.key_generated') + end + redirect_to edit_admin_user_path(@user) + end + + def clear_api_key + if @user.clear_spree_api_key! + flash[:success] = Spree.t('api.key_cleared') + end + redirect_to edit_admin_user_path(@user) + end + + protected + + def collection + return @collection if @collection.present? + if request.xhr? && params[:q].present? + # Disabling proper nested include here due to rails 3.1 bug + @collection = Spree::User. + includes(:bill_address, :ship_address). + where("spree_users.email #{LIKE} :search + OR (spree_addresses.firstname #{LIKE} :search + AND spree_addresses.id = spree_users.bill_address_id) + OR (spree_addresses.lastname #{LIKE} :search + AND spree_addresses.id = spree_users.bill_address_id) + OR (spree_addresses.firstname #{LIKE} :search + AND spree_addresses.id = spree_users.ship_address_id) + OR (spree_addresses.lastname #{LIKE} :search + AND spree_addresses.id = spree_users.ship_address_id)", + search: "#{params[:q].strip}%"). + limit(params[:limit] || 100) + else + @search = Spree::User.registered.ransack(params[:q]) + @collection = @search. + result. + page(params[:page]). + per(Spree::Config[:admin_products_per_page]) + end + end + + private + + # handling raise from Spree::Admin::ResourceController#destroy + def user_destroy_with_orders_error + invoke_callbacks(:destroy, :fails) + render status: :forbidden, text: Spree.t(:error_user_destroy_with_orders) + end + + # Allow different formats of json data to suit different ajax calls + def json_data + json_format = params[:json_format] || 'default' + case json_format + when 'basic' + collection.map { |u| { 'id' => u.id, 'name' => u.email } }.to_json + else + address_fields = [:firstname, :lastname, :address1, :address2, :city, + :zipcode, :phone, :state_name, :state_id, :country_id] + includes = { only: address_fields, include: { state: { only: :name }, + country: { only: :name } } } + + collection.to_json(only: [:id, :email], include: + { bill_address: includes, ship_address: includes }) + end + end + + def sign_in_if_change_own_password + return unless spree_current_user == @user && @user.password.present? + sign_in(@user, event: :authentication, bypass: true) + end + + def load_roles + @roles = Spree::Role.scoped + end + end + end +end diff --git a/app/controllers/spree/user_passwords_controller.rb b/app/controllers/spree/user_passwords_controller.rb new file mode 100644 index 0000000000..bf8b13d379 --- /dev/null +++ b/app/controllers/spree/user_passwords_controller.rb @@ -0,0 +1,44 @@ +module Spree + class UserPasswordsController < Devise::PasswordsController + helper 'spree/base', 'spree/store' + + include Spree::Core::ControllerHelpers::Auth + include Spree::Core::ControllerHelpers::Common + include Spree::Core::ControllerHelpers::Order + include Spree::Core::ControllerHelpers::SSL + + ssl_required + + # Overridden due to bug in Devise. + # respond_with resource, :location => new_session_path(resource_name) + # is generating bad url /session/new.user + # + # overridden to: + # respond_with resource, :location => spree.login_path + # + def create + self.resource = resource_class.send_reset_password_instructions(params[resource_name]) + + if resource.errors.empty? + set_flash_message(:notice, :send_instructions) if is_navigational_format? + respond_with resource, location: spree.login_path + else + respond_with_navigational(resource) { render :new } + end + end + + # Devise::PasswordsController allows for blank passwords. + # Silly Devise::PasswordsController! + # Fixes spree/spree#2190. + def update + if params[:spree_user][:password].blank? + self.resource = resource_class.new + resource.reset_password_token = params[:spree_user][:reset_password_token] + set_flash_message(:error, :cannot_be_blank) + render :edit + else + super + end + end + end +end diff --git a/app/controllers/spree/user_registrations_controller.rb b/app/controllers/spree/user_registrations_controller.rb new file mode 100644 index 0000000000..36f0852cef --- /dev/null +++ b/app/controllers/spree/user_registrations_controller.rb @@ -0,0 +1,65 @@ +module Spree + class UserRegistrationsController < Devise::RegistrationsController + helper 'spree/base', 'spree/store' + + include Spree::Core::ControllerHelpers::Auth + include Spree::Core::ControllerHelpers::Common + include Spree::Core::ControllerHelpers::Order + include Spree::Core::ControllerHelpers::SSL + + ssl_required + before_filter :check_permissions, only: [:edit, :update] + skip_before_filter :require_no_authentication + + # GET /resource/sign_up + def new + super + @user = resource + end + + # POST /resource/sign_up + def create + @user = build_resource(params[:spree_user]) + if resource.save + set_flash_message(:notice, :signed_up) + sign_in(:spree_user, @user) + session[:spree_user_signup] = true + associate_user + respond_with resource, location: after_sign_up_path_for(resource) + else + clean_up_passwords(resource) + render :new + end + end + + # GET /resource/edit + def edit + super + end + + # PUT /resource + def update + super + end + + # DELETE /resource + def destroy + super + end + + # GET /resource/cancel + # Forces the session data which is usually expired after sign + # in to be expired now. This is useful if the user wants to + # cancel oauth signing in/up in the middle of the process, + # removing all OAuth session data. + def cancel + super + end + + protected + + def check_permissions + authorize!(:create, resource) + end + end +end diff --git a/app/controllers/spree/user_sessions_controller.rb b/app/controllers/spree/user_sessions_controller.rb new file mode 100644 index 0000000000..ad40063d8a --- /dev/null +++ b/app/controllers/spree/user_sessions_controller.rb @@ -0,0 +1,56 @@ +module Spree + class UserSessionsController < Devise::SessionsController + helper 'spree/base', 'spree/store' + + include Spree::Core::ControllerHelpers::Auth + include Spree::Core::ControllerHelpers::Common + include Spree::Core::ControllerHelpers::Order + include Spree::Core::ControllerHelpers::SSL + + ssl_required :new, :create, :destroy, :update + ssl_allowed :login_bar + + before_filter :set_checkout_redirect, only: :create + + def create + authenticate_spree_user! + + if spree_user_signed_in? + respond_to do |format| + format.html { + flash[:success] = t('devise.success.logged_in_succesfully') + redirect_back_or_default(after_sign_in_path_for(spree_current_user)) + } + format.js { + render json: { email: spree_current_user.login }, status: :ok + } + end + else + respond_to do |format| + format.html { + flash.now[:error] = t('devise.failure.invalid') + render :new + } + format.js { + render json: { message: t('devise.failure.invalid') }, status: :unauthorized + } + end + end + end + + def nav_bar + render partial: 'spree/shared/nav_bar' + end + + private + + def accurate_title + Spree.t(:login) + end + + def redirect_back_or_default(default) + redirect_to(session["spree_user_return_to"] || default) + session["spree_user_return_to"] = nil + end + end +end diff --git a/app/controllers/spree/user_sessions_controller_decorator.rb b/app/controllers/spree/user_sessions_controller_decorator.rb deleted file mode 100644 index cdc202cca5..0000000000 --- a/app/controllers/spree/user_sessions_controller_decorator.rb +++ /dev/null @@ -1,29 +0,0 @@ -Spree::UserSessionsController.class_eval do - before_filter :set_checkout_redirect, only: :create - - def create - authenticate_spree_user! - - if spree_user_signed_in? - respond_to do |format| - format.html { - flash[:success] = t('devise.success.logged_in_succesfully') - redirect_back_or_default(after_sign_in_path_for(spree_current_user)) - } - format.js { - render json: { email: spree_current_user.login }, status: :ok - } - end - else - respond_to do |format| - format.html { - flash.now[:error] = t('devise.failure.invalid') - render :new - } - format.js { - render json: { message: t('devise.failure.invalid') }, status: :unauthorized - } - end - end - end -end diff --git a/app/controllers/spree/users_controller.rb b/app/controllers/spree/users_controller.rb new file mode 100644 index 0000000000..41d48c71de --- /dev/null +++ b/app/controllers/spree/users_controller.rb @@ -0,0 +1,74 @@ +module Spree + class UsersController < Spree::StoreController + layout 'darkswarm' + ssl_required + skip_before_filter :set_current_order, only: :show + prepend_before_filter :load_object, only: [:show, :edit, :update] + prepend_before_filter :authorize_actions, only: :new + + include Spree::Core::ControllerHelpers + include I18nHelper + + before_filter :set_locale + before_filter :enable_embedded_shopfront + + # Ignores invoice orders, only order where state: 'complete' + def show + @orders = @user.orders.where(state: 'complete').order('completed_at desc') + @unconfirmed_email = spree_current_user.unconfirmed_email + end + + # Endpoint for queries to check if a user is already registered + def registered_email + user = Spree.user_class.find_by_email params[:email] + render json: { registered: user.present? } + end + + def create + @user = Spree::User.new(params[:user]) + if @user.save + + if current_order + session[:guest_token] = nil + end + + redirect_back_or_default(root_url) + else + render :new + end + end + + def update + if @user.update_attributes(params[:user]) + if params[:user][:password].present? + # this logic needed b/c devise wants to log us out after password changes + Spree::User.reset_password_by_token(params[:user]) + sign_in(@user, event: :authentication, + bypass: true) + end + redirect_to spree.account_url, notice: Spree.t(:account_updated) + else + render :edit + end + end + + private + + def load_object + @user ||= spree_current_user + if @user + authorize! params[:action].to_sym, @user + else + redirect_to spree.login_path + end + end + + def authorize_actions + authorize! params[:action].to_sym, Spree::User.new + end + + def accurate_title + Spree.t(:my_account) + end + end +end diff --git a/app/controllers/spree/users_controller_decorator.rb b/app/controllers/spree/users_controller_decorator.rb deleted file mode 100644 index e8073e7bbb..0000000000 --- a/app/controllers/spree/users_controller_decorator.rb +++ /dev/null @@ -1,20 +0,0 @@ -Spree::UsersController.class_eval do - layout 'darkswarm' - include I18nHelper - - before_filter :set_locale - before_filter :enable_embedded_shopfront - - # Override of spree_auth_devise default - # Ignores invoice orders, only order where state: 'complete' - def show - @orders = @user.orders.where(state: 'complete').order('completed_at desc') - @unconfirmed_email = spree_current_user.unconfirmed_email - end - - # Endpoint for queries to check if a user is already registered - def registered_email - user = Spree.user_class.find_by_email params[:email] - render json: { registered: user.present? } - end -end diff --git a/app/mailers/spree/user_mailer.rb b/app/mailers/spree/user_mailer.rb new file mode 100644 index 0000000000..68d37c4b99 --- /dev/null +++ b/app/mailers/spree/user_mailer.rb @@ -0,0 +1,47 @@ +# This mailer is configured to be the Devise mailer +# Some methods here override Devise::Mailer methods +module Spree + class UserMailer < BaseMailer + include I18nHelper + + # Overrides `Devise::Mailer.reset_password_instructions` + def reset_password_instructions(user) + recipient = user.respond_to?(:id) ? user : Spree.user_class.find(user) + @edit_password_reset_url = spree. + edit_spree_user_password_url(reset_password_token: recipient.reset_password_token) + + mail(to: recipient.email, from: from_address, + subject: Spree::Config[:site_name] + ' ' + + I18n.t(:subject, scope: [:devise, :mailer, :reset_password_instructions])) + end + + # This is a OFN specific email, not from Devise::Mailer + def signup_confirmation(user) + @user = user + I18n.with_locale valid_locale(@user) do + mail(to: user.email, from: from_address, + subject: t(:welcome_to) + Spree::Config[:site_name]) + end + end + + # Overrides `Devise::Mailer.confirmation_instructions` + def confirmation_instructions(user, _opts) + @user = user + @instance = Spree::Config[:site_name] + @contact = ContentConfig.footer_email + + I18n.with_locale valid_locale(@user) do + subject = t('spree.user_mailer.confirmation_instructions.subject') + mail(to: confirmation_email_address, + from: from_address, + subject: subject) + end + end + + private + + def confirmation_email_address + @user.pending_reconfirmation? ? @user.unconfirmed_email : @user.email + end + end +end diff --git a/app/mailers/spree/user_mailer_decorator.rb b/app/mailers/spree/user_mailer_decorator.rb deleted file mode 100644 index 52266a60b0..0000000000 --- a/app/mailers/spree/user_mailer_decorator.rb +++ /dev/null @@ -1,32 +0,0 @@ -Spree::UserMailer.class_eval do - include I18nHelper - - def signup_confirmation(user) - @user = user - I18n.with_locale valid_locale(@user) do - mail(to: user.email, from: from_address, - subject: t(:welcome_to) + Spree::Config[:site_name]) - end - end - - # Overriding `Spree::UserMailer.confirmation_instructions` which is - # overriding `Devise::Mailer.confirmation_instructions`. - def confirmation_instructions(user, _opts) - @user = user - @instance = Spree::Config[:site_name] - @contact = ContentConfig.footer_email - - I18n.with_locale valid_locale(@user) do - subject = t('spree.user_mailer.confirmation_instructions.subject') - mail(to: confirmation_email_address, - from: from_address, - subject: subject) - end - end - - private - - def confirmation_email_address - @user.pending_reconfirmation? ? @user.unconfirmed_email : @user.email - end -end diff --git a/app/models/spree/user.rb b/app/models/spree/user.rb new file mode 100644 index 0000000000..cad09a6073 --- /dev/null +++ b/app/models/spree/user.rb @@ -0,0 +1,184 @@ +module Spree + class User < ActiveRecord::Base + include Core::UserBanners + + devise :database_authenticatable, :token_authenticatable, :registerable, :recoverable, + :rememberable, :trackable, :validatable, :encryptable, encryptor: 'authlogic_sha512' + + has_many :orders + belongs_to :ship_address, foreign_key: 'ship_address_id', class_name: 'Spree::Address' + belongs_to :bill_address, foreign_key: 'bill_address_id', class_name: 'Spree::Address' + + before_validation :set_login + before_destroy :check_completed_orders + + # Setup accessible (or protected) attributes for your model + attr_accessible :email, :password, :password_confirmation, + :remember_me, :persistence_token, :login + + users_table_name = User.table_name + roles_table_name = Role.table_name + + scope :admin, lambda { includes(:spree_roles).where("#{roles_table_name}.name" => "admin") } + scope :registered, -> { where("#{users_table_name}.email NOT LIKE ?", "%@example.net") } + + has_many :enterprise_roles, dependent: :destroy + has_many :enterprises, through: :enterprise_roles + has_many :owned_enterprises, class_name: 'Enterprise', + foreign_key: :owner_id, inverse_of: :owner + has_many :owned_groups, class_name: 'EnterpriseGroup', + foreign_key: :owner_id, inverse_of: :owner + has_many :customers + has_many :credit_cards + + accepts_nested_attributes_for :enterprise_roles, allow_destroy: true + + accepts_nested_attributes_for :bill_address + accepts_nested_attributes_for :ship_address + + attr_accessible :enterprise_ids, :enterprise_roles_attributes, :enterprise_limit, + :locale, :bill_address_attributes, :ship_address_attributes + after_create :associate_customers + + validate :limit_owned_enterprises + + # We use the same options as Spree and add :confirmable + devise :confirmable, reconfirmable: true + # TODO: Later versions of devise have a dedicated after_confirmation callback, so use that + after_update :welcome_after_confirm, if: lambda { + confirmation_token_changed? && confirmation_token.nil? + } + + class DestroyWithOrdersError < StandardError; end + + # Creates an anonymous user. An anonymous user is basically an auto-generated +User+ account + # that is created for the customer behind the scenes and it's transparent to the customer. + # All +Orders+ must have a +User+ so this is necessary when adding to the "cart" (an order) + # and before the customer has a chance to provide an email or to register. + def self.anonymous! + token = User.generate_token(:persistence_token) + User.create(email: "#{token}@example.net", + password: token, password_confirmation: token, persistence_token: token) + end + + def self.admin_created? + User.admin.count > 0 + end + + def admin? + has_spree_role?('admin') + end + + def anonymous? + email =~ /@example.net$/ ? true : false + end + + def send_reset_password_instructions + generate_reset_password_token! + UserMailer.reset_password_instructions(id).deliver + end + # handle_asynchronously will define send_reset_password_instructions_with_delay. + # If handle_asynchronously is called twice, we get an infinite job loop. + handle_asynchronously :send_reset_password_instructions unless method_defined? :send_reset_password_instructions_with_delay + + def known_users + if admin? + Spree::User.scoped + else + Spree::User + .includes(:enterprises) + .where("enterprises.id IN (SELECT enterprise_id FROM enterprise_roles WHERE user_id = ?)", + id) + end + end + + def build_enterprise_roles + Enterprise.all.find_each do |enterprise| + unless enterprise_roles.find_by_enterprise_id enterprise.id + enterprise_roles.build(enterprise: enterprise) + end + end + end + + def customer_of(enterprise) + return nil unless enterprise + customers.find_by_enterprise_id(enterprise) + end + + def welcome_after_confirm + # Send welcome email if we are confirming an user's email + # Note: this callback only runs on email confirmation + return unless confirmed? && unconfirmed_email.nil? && !unconfirmed_email_changed? + send_signup_confirmation + end + + def send_signup_confirmation + Delayed::Job.enqueue ConfirmSignupJob.new(id) + end + + def associate_customers + self.customers = Customer.where(email: email) + end + + def can_own_more_enterprises? + owned_enterprises(:reload).size < enterprise_limit + end + + def default_card + credit_cards.where(is_default: true).first + end + + # Checks whether the specified user is a superadmin, with full control of the + # instance + # + # @return [Boolean] + def superadmin? + has_spree_role?('admin') + end + + protected + + def password_required? + !persisted? || password.present? || password_confirmation.present? + end + + private + + def check_completed_orders + raise DestroyWithOrdersError if orders.complete.present? + end + + def set_login + # for now force login to be same as email, eventually we will make this configurable, etc. + self.login ||= email if email + end + + # Generate a friendly string randomically to be used as token. + def self.friendly_token + SecureRandom.base64(15).tr('+/=', '-_ ').strip.delete("\n") + end + + # Generate a token by looping and ensuring does not already exist. + def self.generate_token(column) + loop do + token = friendly_token + break token unless find(:first, conditions: { column => token }) + end + end + + def limit_owned_enterprises + return unless owned_enterprises.size > enterprise_limit + errors.add(:owned_enterprises, I18n.t(:spree_user_enterprise_limit_error, + email: email, + enterprise_limit: enterprise_limit)) + end + + def remove_payments_in_checkout(enterprises) + enterprises.each do |enterprise| + enterprise.distributed_orders.each do |order| + order.payments.keep_if { |payment| payment.state != "checkout" } + end + end + end + end +end diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb deleted file mode 100644 index 15d62b82f2..0000000000 --- a/app/models/spree/user_decorator.rb +++ /dev/null @@ -1,98 +0,0 @@ -Spree.user_class.class_eval do - # handle_asynchronously will define send_reset_password_instructions_with_delay. - # If handle_asynchronously is called twice, we get an infinite job loop. - handle_asynchronously :send_reset_password_instructions unless method_defined? :send_reset_password_instructions_with_delay - - has_many :enterprise_roles, dependent: :destroy - has_many :enterprises, through: :enterprise_roles - has_many :owned_enterprises, class_name: 'Enterprise', foreign_key: :owner_id, inverse_of: :owner - has_many :owned_groups, class_name: 'EnterpriseGroup', foreign_key: :owner_id, inverse_of: :owner - has_many :customers - has_many :credit_cards - - accepts_nested_attributes_for :enterprise_roles, allow_destroy: true - - accepts_nested_attributes_for :bill_address - accepts_nested_attributes_for :ship_address - - attr_accessible :enterprise_ids, :enterprise_roles_attributes, :enterprise_limit, :locale, :bill_address_attributes, :ship_address_attributes - after_create :associate_customers - - validate :limit_owned_enterprises - - # We use the same options as Spree and add :confirmable - devise :confirmable, reconfirmable: true - # TODO: Later versions of devise have a dedicated after_confirmation callback, so use that - after_update :welcome_after_confirm, if: lambda { confirmation_token_changed? && confirmation_token.nil? } - - def known_users - if admin? - Spree::User.scoped - else - Spree::User - .includes(:enterprises) - .where("enterprises.id IN (SELECT enterprise_id FROM enterprise_roles WHERE user_id = ?)", id) - end - end - - def build_enterprise_roles - Enterprise.all.find_each do |enterprise| - unless enterprise_roles.find_by_enterprise_id enterprise.id - enterprise_roles.build(enterprise: enterprise) - end - end - end - - def customer_of(enterprise) - return nil unless enterprise - customers.find_by_enterprise_id(enterprise) - end - - def welcome_after_confirm - # Send welcome email if we are confirming an user's email - # Note: this callback only runs on email confirmation - if confirmed? && unconfirmed_email.nil? && !unconfirmed_email_changed? - send_signup_confirmation - end - end - - def send_signup_confirmation - Delayed::Job.enqueue ConfirmSignupJob.new(id) - end - - def associate_customers - self.customers = Customer.where(email: email) - end - - def can_own_more_enterprises? - owned_enterprises(:reload).size < enterprise_limit - end - - def default_card - credit_cards.where(is_default: true).first - end - - # Checks whether the specified user is a superadmin, with full control of the - # instance - # - # @return [Boolean] - def superadmin? - has_spree_role?('admin') - end - - private - - def limit_owned_enterprises - if owned_enterprises.size > enterprise_limit - errors.add(:owned_enterprises, I18n.t(:spree_user_enterprise_limit_error, email: email, enterprise_limit: enterprise_limit)) - end - end - - def remove_payments_in_checkout(enterprises) - enterprises.each do |enterprise| - enterprise.distributed_orders.each do |order| - order.payments.keep_if { |payment| payment.state != "checkout" } - end - end - end -end diff --git a/app/overrides/admin_tab.rb b/app/overrides/admin_tab.rb new file mode 100644 index 0000000000..186a7a06f7 --- /dev/null +++ b/app/overrides/admin_tab.rb @@ -0,0 +1,6 @@ +Deface::Override.new(virtual_path: "spree/layouts/admin", + name: "user_admin_tabs", + insert_bottom: "[data-hook='admin_tabs'], #admin_tabs[data-hook]", + partial: "spree/admin/users_tab", + disabled: false, + original: '031652cf5a054796022506622082ab6d2693699f') diff --git a/app/overrides/auth_admin_login_navigation_bar.rb b/app/overrides/auth_admin_login_navigation_bar.rb new file mode 100644 index 0000000000..8a171041af --- /dev/null +++ b/app/overrides/auth_admin_login_navigation_bar.rb @@ -0,0 +1,5 @@ +Deface::Override.new(virtual_path: "spree/layouts/admin", + name: "auth_admin_login_navigation_bar", + insert_top: "[data-hook='admin_login_navigation_bar'], #admin_login_navigation_bar[data-hook]", + partial: "spree/layouts/admin/login_nav", + original: '841227d0aedf7909d62237d8778df99100087715') diff --git a/app/overrides/auth_shared_login_bar.rb b/app/overrides/auth_shared_login_bar.rb new file mode 100644 index 0000000000..734c9f8112 --- /dev/null +++ b/app/overrides/auth_shared_login_bar.rb @@ -0,0 +1,6 @@ +Deface::Override.new(virtual_path: "spree/shared/_nav_bar", + name: "auth_shared_login_bar", + insert_before: "li#search-bar", + partial: "spree/shared/login_bar", + disabled: false, + original: 'eb3fa668cd98b6a1c75c36420ef1b238a1fc55ac') diff --git a/app/views/spree/admin/_users_tab.html.haml b/app/views/spree/admin/_users_tab.html.haml new file mode 100644 index 0000000000..8be1861f50 --- /dev/null +++ b/app/views/spree/admin/_users_tab.html.haml @@ -0,0 +1,2 @@ +- if can? :admin, Spree::User + = tab(:users, url: spree.admin_users_path, icon: 'icon-user') diff --git a/app/views/spree/admin/users/new.html.haml b/app/views/spree/admin/users/new.html.haml new file mode 100644 index 0000000000..9fb68e689f --- /dev/null +++ b/app/views/spree/admin/users/new.html.haml @@ -0,0 +1,15 @@ += content_for :page_title do + = Spree.t(:new_user) + += content_for :page_actions do + %li + = button_link_to Spree.t(:back_to_users_list), spree.admin_users_path, icon: 'icon-arrow-left' + +%div + = render partial: 'spree/shared/error_messages', locals: { target: @user } + +%div + = form_for [:admin, @user] do |f| + = render partial: 'form', locals: { f: f } + %div + = render partial: 'spree/admin/shared/new_resource_links' diff --git a/app/views/spree/shared/_user_form.html.haml b/app/views/spree/shared/_user_form.html.haml new file mode 100644 index 0000000000..ddee06e421 --- /dev/null +++ b/app/views/spree/shared/_user_form.html.haml @@ -0,0 +1,15 @@ +%p + = f.label :email, Spree.t(:email) + %br + = f.email_field :email, class: 'title' + +%div{"id" => "password-credentials"} + %p + = f.label :password, Spree.t(:password) + %br + = f.password_field :password, class: 'title' + + %p + = f.label :password_confirmation, Spree.t(:confirm_password) + %br + = f.password_field :password_confirmation, class: 'title' diff --git a/app/views/spree/user_passwords/new.html.haml b/app/views/spree/user_passwords/new.html.haml new file mode 100755 index 0000000000..604f7ebf8c --- /dev/null +++ b/app/views/spree/user_passwords/new.html.haml @@ -0,0 +1,14 @@ += render partial: 'spree/shared/error_messages', locals: { target: @spree_user } + +%div{"id" => "forgot-password"} + %h6= Spree.t(:forgot_password) + + %p= Spree.t(:instructions_to_reset_password) + + = form_for Spree::User.new, as: :spree_user, url: spree.reset_password_path do |f| + %p + = f.label :email, Spree.t(:email) + %br + = f.email_field :email + %p + = f.submit Spree.t(:reset_password), class: 'button primary' diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 9fef52fc8c..6f0a508ac4 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -1,5 +1,146 @@ +# Use this hook to configure devise mailer, warden hooks and so forth. The first +# four configuration values can also be set straight in your models. Devise.setup do |config| + # ==> Mailer Configuration + # Configure the e-mail address which will be shown in DeviseMailer. + config.mailer_sender = 'please-change-me@config-initializers-devise.com' + + # Configure the class responsible to send e-mails. + config.mailer = 'Spree::UserMailer' + + # ==> ORM configuration + # Load and configure the ORM. Supports :active_record (default) and + # :mongoid (bson_ext recommended) by default. Other ORMs may be + # available as additional gems. + require 'devise/orm/active_record' + + # ==> Configuration for any authentication mechanism + # Configure which keys are used when authenticating an user. By default is + # just :email. You can configure it to use [:username, :subdomain], so for + # authenticating an user, both parameters are required. Remember that those + # parameters are used only when authenticating and not when retrieving from + # session. If you need permissions, you should implement that in a before filter. + # config.authentication_keys = [ :email ] + + # Tell if authentication through request.params is enabled. True by default. + # config.params_authenticatable = true + + # Tell if authentication through HTTP Basic Auth is enabled. False by default. + config.http_authenticatable = true + + # Set this to true to use Basic Auth for AJAX requests. True by default. + #config.http_authenticatable_on_xhr = false + + # The realm used in Http Basic Authentication + config.http_authentication_realm = 'Spree Application' + + # ==> Configuration for :database_authenticatable + # For bcrypt, this is the cost for hashing the password and defaults to 10. If + # using other encryptors, it sets how many times you want the password re-encrypted. + config.stretches = Rails.env.test? ? 1 : 20 + + # Setup a pepper to generate the encrypted password. + config.pepper = Rails.configuration.secret_token + + # ==> Configuration for :confirmable + # The time you want to give your user to confirm his account. During this time + # he will be able to access your application without confirming. Default is nil. + # When confirm_within is zero, the user won't be able to sign in without confirming. + # You can use this to let your user access some features of your application + # without confirming the account, but blocking it after a certain period + # (ie 2 days). + # config.confirm_within = 2.days + + # ==> Configuration for :rememberable + # The time the user will be remembered without asking for credentials again. + # config.remember_for = 2.weeks + + # If true, a valid remember token can be re-used between multiple browsers. + # config.remember_across_browsers = true + + # If true, extends the user's remember period when remembered via cookie. + # config.extend_remember_period = false + + # ==> Configuration for :validatable + # Range for password length + # config.password_length = 6..20 + + # Regex to use to validate the email address + config.email_regexp = /^([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})$/i + + # ==> Configuration for :timeoutable + # The time you want to timeout the user session without activity. After this + # time the user will be asked for credentials again. + # config.timeout_in = 10.minutes + + # ==> Configuration for :lockable + # Defines which strategy will be used to lock an account. + # :failed_attempts = Locks an account after a number of failed attempts to sign in. + # :none = No lock strategy. You should handle locking by yourself. + # config.lock_strategy = :failed_attempts + + # Defines which strategy will be used to unlock an account. + # :email = Sends an unlock link to the user email + # :time = Re-enables login after a certain amount of time (see :unlock_in below) + # :both = Enables both strategies + # :none = No unlock strategy. You should handle unlocking by yourself. + # config.unlock_strategy = :both + + # Number of authentication tries before locking an account if lock_strategy + # is failed attempts. + # config.maximum_attempts = 20 + + # Time interval to unlock the account if :time is enabled as unlock_strategy. + # config.unlock_in = 1.hour + + # ==> Configuration for :token_authenticatable + # Defines name of the authentication token params key + config.token_authentication_key = :auth_token + + # ==> Scopes configuration + # Turn scoped views on. Before rendering 'sessions/new', it will first check for + # 'users/sessions/new'. It's turned off by default because it's slower if you + # are using only default views. + # config.scoped_views = true + + # Configure the default scope given to Warden. By default it's the first + # devise role declared in your routes. # Add a default scope to devise, to prevent it from checking # whether other devise enabled models are signed into a session or not config.default_scope = :spree_user -end \ No newline at end of file + + # Configure sign_out behavior. + # By default sign_out is scoped (i.e. /users/sign_out affects only :user scope). + # In case of sign_out_all_scopes set to true any logout action will sign out all active scopes. + # config.sign_out_all_scopes = false + + # ==> Navigation configuration + # Lists the formats that should be treated as navigational. Formats like + # :html, should redirect to the sign in page when the user does not have + # access, but formats like :xml or :json, should return 401. + # If you have any extra navigational formats, like :iphone or :mobile, you + # should add them to the navigational formats lists. Default is [:html] + config.navigational_formats = [:html, :json, :xml] + + # ==> Warden configuration + # If you want to use other strategies, that are not (yet) supported by Devise, + # you can configure them inside the config.warden block. The example below + # allows you to setup OAuth, using http://github.com/roman/warden_oauth + # + # config.warden do |manager| + # manager.oauth(:twitter) do |twitter| + # twitter.consumer_secret = + # twitter.consumer_key = + # twitter.options :site => 'http://twitter.com' + # end + # manager.default_strategies(:scope => :user).unshift :twitter_oauth + # end + # + # Time interval you can reset your password with a reset password key. + # Don't put a too small interval or your users won't have the time to + # change their passwords. + config.reset_password_within = 6.hours + config.sign_out_via = :get + + config.case_insensitive_keys = [:email] +end diff --git a/config/initializers/spree.rb b/config/initializers/spree.rb index df21b91421..c2f6a118a1 100644 --- a/config/initializers/spree.rb +++ b/config/initializers/spree.rb @@ -45,9 +45,6 @@ end # Spree 2.0 recommends explicitly setting this here when using spree_auth_devise Spree.user_class = 'Spree::User' -# Don't log users out when setting a new password -Spree::Auth::Config[:signout_after_password_change] = false - # TODO Work out why this is necessary # Seems like classes within OFN module become 'uninitialized' when server reloads # unless the empty module is explicity 'registered' here. Something to do with autoloading? diff --git a/config/initializers/spree_auth_devise.rb b/config/initializers/spree_auth_devise.rb deleted file mode 100644 index a59badf636..0000000000 --- a/config/initializers/spree_auth_devise.rb +++ /dev/null @@ -1,16 +0,0 @@ -# `spree_auth_devise` gem decorators get loaded in a `to_prepare` callback -# referring to Spree classes that have not been loaded yet -# -# When this initializer is loaded we're sure that those Spree classes have been -# loaded and we load again the `spree_auth_devise` decorators to effectively -# apply them. -# -# Give a look at `if defined?(Spree::Admin::BaseController)` in the following file -# to get an example: -# https://github.com/openfoodfoundation/spree_auth_devise/blob/spree-upgrade-intermediate/app/controllers/spree/admin/admin_controller_decorator.rb#L1 -# -# TODO: remove this hack once we get to Spree 3.0 -gem_dir = Gem::Specification.find_by_name("spree_auth_devise").gem_dir -Dir.glob(File.join(gem_dir, 'app/**/*_decorator*.rb')) do |c| - load c -end diff --git a/config/locales/en.yml b/config/locales/en.yml index 1d18cce5bc..afb422310b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -103,8 +103,8 @@ en: confirmation_not_sent: "Error sending confirmation email" user_registrations: spree_user: - signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please open the link to activate your account." - unknown_error: "Something went wrong while creating your account. Check your email address and try again." + signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please open the link to activate your account." + unknown_error: "Something went wrong while creating your account. Check your email address and try again." failure: invalid: | Invalid email or password. @@ -116,6 +116,8 @@ en: user_passwords: spree_user: updated_not_active: "Your password has been reset, but your email has not been confirmed yet." + updated: "Your password was changed successfully. You are now signed in." + send_instructions: "You will receive an email with instructions about how to confirm your account in a few minutes." models: order_cycle: @@ -2933,6 +2935,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using inventory: Inventory zipcode: Postcode weight: Weight (per kg) + error_user_destroy_with_orders: "Users with completed orders may not be deleted" actions: update: "Update" diff --git a/config/routes/spree.rb b/config/routes/spree.rb index cc221fc8d0..5613ae9c11 100644 --- a/config/routes/spree.rb +++ b/config/routes/spree.rb @@ -9,6 +9,32 @@ Spree::Core::Engine.routes.draw do :skip => [:unlocks, :omniauth_callbacks], :path_names => { :sign_out => 'logout' }, :path_prefix => :user + + resources :users, :only => [:edit, :update] + + devise_scope :spree_user do + get '/login' => 'user_sessions#new', :as => :login + post '/login' => 'user_sessions#create', :as => :create_new_session + get '/logout' => 'user_sessions#destroy', :as => :logout + get '/signup' => 'user_registrations#new', :as => :signup + post '/signup' => 'user_registrations#create', :as => :registration + get '/password/recover' => 'user_passwords#new', :as => :recover_password + post '/password/recover' => 'user_passwords#create', :as => :reset_password + get '/password/change' => 'user_passwords#edit', :as => :edit_password + put '/password/change' => 'user_passwords#update', :as => :update_password + end + + resource :session do + member do + get :nav_bar + end + end + + resource :account, :controller => 'users' + + namespace :admin do + resources :users + end end Spree::Core::Engine.routes.prepend do diff --git a/db/default/users.rb b/db/default/users.rb new file mode 100644 index 0000000000..bf60502187 --- /dev/null +++ b/db/default/users.rb @@ -0,0 +1,82 @@ +require 'highline/import' + +# see last line where we create an admin if there is none, asking for email and password +def prompt_for_admin_password + if ENV['ADMIN_PASSWORD'] + password = ENV['ADMIN_PASSWORD'].dup + say "Admin Password #{password}" + else + password = ask('Password [spree123]: ') do |q| + q.echo = false + q.validate = /^(|.{5,40})$/ + q.responses[:not_valid] = 'Invalid password. Must be at least 5 characters long.' + q.whitespace = :strip + end + password = 'spree123' if password.blank? + end + + password +end + +def prompt_for_admin_email + if ENV['ADMIN_EMAIL'] + email = ENV['ADMIN_EMAIL'].dup + say "Admin User #{email}" + else + email = ask('Email [spree@example.com]: ') do |q| + q.echo = true + q.whitespace = :strip + end + email = 'spree@example.com' if email.blank? + end + + email +end + +def create_admin_user + if ENV['AUTO_ACCEPT'] + password = 'spree123' + email = 'spree@example.com' + else + puts 'Create the admin user (press enter for defaults).' + #name = prompt_for_admin_name unless name + email = prompt_for_admin_email + password = prompt_for_admin_password + end + attributes = { + :password => password, + :password_confirmation => password, + :email => email, + :login => email + } + + load 'spree/user.rb' + + if Spree::User.find_by_email(email) + say "\nWARNING: There is already a user with the email: #{email}, so no account changes were made. If you wish to create an additional admin user, please run rake spree_auth:admin:create again with a different email.\n\n" + else + admin = Spree::User.new(attributes) + if admin.save + role = Spree::Role.find_or_create_by_name 'admin' + admin.spree_roles << role + admin.save + say "Done!" + else + say "There was some problems with persisting new admin user:" + admin.errors.full_messages.each do |error| + say error + end + end + end +end + +if Spree::User.admin.empty? + create_admin_user +else + puts 'Admin user has already been previously created.' + if agree('Would you like to create a new admin user? (yes/no)') + create_admin_user + else + puts 'No admin user created.' + end +end diff --git a/lib/spree/authentication_helpers.rb b/lib/spree/authentication_helpers.rb new file mode 100644 index 0000000000..3754bb0e2a --- /dev/null +++ b/lib/spree/authentication_helpers.rb @@ -0,0 +1,26 @@ +module Spree + module AuthenticationHelpers + def self.included(receiver) + receiver.public_send :helper_method, :spree_current_user + receiver.public_send :helper_method, :spree_login_path + receiver.public_send :helper_method, :spree_signup_path + receiver.public_send :helper_method, :spree_logout_path + end + + def spree_current_user + current_spree_user + end + + def spree_login_path + spree.login_path + end + + def spree_signup_path + spree.signup_path + end + + def spree_logout_path + spree.logout_path + end + end +end diff --git a/lib/spree/core/controller_helpers/order_decorator.rb b/lib/spree/core/controller_helpers/order_decorator.rb index cb644bb701..6da1b42ee6 100644 --- a/lib/spree/core/controller_helpers/order_decorator.rb +++ b/lib/spree/core/controller_helpers/order_decorator.rb @@ -15,7 +15,7 @@ Spree::Core::ControllerHelpers::Order.class_eval do end alias_method_chain :current_order, :scoped_variants - # Override definition in spree/auth/app/controllers/spree/base_controller_decorator.rb + # Override definition in Spree::Core::ControllerHelpers::Order # Do not attempt to merge incomplete and current orders. Instead, destroy the incomplete orders. def set_current_order if user = try_spree_current_user diff --git a/spec/controllers/spree/admin/users_controller_spec.rb b/spec/controllers/spree/admin/users_controller_spec.rb new file mode 100644 index 0000000000..a42630964d --- /dev/null +++ b/spec/controllers/spree/admin/users_controller_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' +require 'spree/testing_support/bar_ability' + +describe Spree::Admin::UsersController do + context '#authorize_admin' do + let(:user) { create(:user) } + let(:test_user) { create(:user) } + + before do + allow(controller).to receive_messages spree_current_user: user + allow(Spree::User).to receive(:find).with(test_user.id.to_s).and_return(test_user) + user.spree_roles.clear + end + + it 'should grant access to users with an admin role' do + user.spree_roles << Spree::Role.find_or_create_by_name('admin') + spree_post :index + expect(response).to render_template :index + end + + it "allows admins to update a user's API key" do + user.spree_roles << Spree::Role.find_or_create_by_name('admin') + expect(test_user).to receive(:generate_spree_api_key!).and_return(true) + puts user.id + puts test_user.id + spree_put :generate_api_key, id: test_user.id + expect(response).to redirect_to(spree.edit_admin_user_path(test_user)) + end + + it "allows admins to clear a user's API key" do + user.spree_roles << Spree::Role.find_or_create_by_name('admin') + expect(test_user).to receive(:clear_spree_api_key!).and_return(true) + spree_put :clear_api_key, id: test_user.id + expect(response).to redirect_to(spree.edit_admin_user_path(test_user)) + end + + it 'should deny access to users with an bar role' do + user.spree_roles << Spree::Role.find_or_create_by_name('bar') + Spree::Ability.register_ability(BarAbility) + spree_post :index + expect(response).to redirect_to('/unauthorized') + end + + it 'should deny access to users with an bar role' do + user.spree_roles << Spree::Role.find_or_create_by_name('bar') + Spree::Ability.register_ability(BarAbility) + spree_post :update, id: '9' + expect(response).to redirect_to('/unauthorized') + end + + it 'should deny access to users without an admin role' do + allow(user).to receive_messages has_spree_role?: false + spree_post :index + expect(response).to redirect_to('/unauthorized') + end + end +end diff --git a/spec/controllers/spree/users_controller_spec.rb b/spec/controllers/spree/users_controller_spec.rb index 95001084dd..8e68b73720 100644 --- a/spec/controllers/spree/users_controller_spec.rb +++ b/spec/controllers/spree/users_controller_spec.rb @@ -54,4 +54,19 @@ describe Spree::UsersController, type: :controller do expect(json_response['registered']).to eq false end end + + context '#load_object' do + it 'should redirect to signup path if user is not found' do + allow(controller).to receive_messages(spree_current_user: nil) + spree_put :update, user: { email: 'foobar@example.com' } + expect(response).to redirect_to('/login') + end + end + + context '#create' do + it 'should create a new user' do + spree_post :create, user: { email: 'foobar@example.com', password: 'foobar123', password_confirmation: 'foobar123' } + expect(assigns[:user].new_record?).to be_falsey + end + end end diff --git a/spec/features/admin/products_spec.rb b/spec/features/admin/products_spec.rb index 7b91c80909..1756cdc539 100644 --- a/spec/features/admin/products_spec.rb +++ b/spec/features/admin/products_spec.rb @@ -17,6 +17,12 @@ feature ' @enterprise_fees = (0..2).map { |i| create(:enterprise_fee, enterprise: @distributors[i]) } end + context "as anonymous user" do + it "is redirected to login page when attempting to access product listing" do + expect { visit spree.admin_products_path }.not_to raise_error + end + end + describe "creating a product" do let!(:tax_category) { create(:tax_category, name: 'Test Tax Category') } diff --git a/spec/features/admin/users_spec.rb b/spec/features/admin/users_spec.rb index 862e0fe6e0..d58ba686a6 100644 --- a/spec/features/admin/users_spec.rb +++ b/spec/features/admin/users_spec.rb @@ -10,6 +10,63 @@ feature "Managing users" do quick_login_as_admin end + context "from the index page" do + before do + create(:user, :email => "a@example.com") + create(:user, :email => "b@example.com") + + visit spree.admin_path + click_link "Users" + end + + context "users index page with sorting" do + before(:each) do + click_link "users_email_title" + end + + it "should be able to list users with order email asc" do + expect(page).to have_css('table#listing_users') + within("table#listing_users") do + expect(page).to have_content("a@example.com") + expect(page).to have_content("b@example.com") + end + end + + it "should be able to list users with order email desc" do + click_link "users_email_title" + within("table#listing_users") do + expect(page).to have_content("a@example.com") + expect(page).to have_content("b@example.com") + end + end + end + + context "searching users" do + it "should display the correct results for a user search" do + fill_in "q_email_cont", :with => "a@example" + click_button "Search" + within("table#listing_users") do + expect(page).to have_content("a@example") + expect(page).not_to have_content("b@example") + end + end + end + + context "editing users" do + before(:each) do + click_link("a@example.com") + end + + it "should let me edit the user password" do + fill_in "user_password", :with => "welcome" + fill_in "user_password_confirmation", :with => "welcome" + click_button "Update" + + expect(page).to have_content("Account updated") + end + end + end + describe "creating a user" do it "shows no confirmation message to start with" do visit spree.new_admin_user_path diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb index bd1d7ad78c..d73042c67a 100644 --- a/spec/models/spree/user_spec.rb +++ b/spec/models/spree/user_spec.rb @@ -180,4 +180,45 @@ describe Spree.user_class do end end end + + before(:all) { Spree::Role.create name: 'admin' } + + it '#admin?' do + expect(create(:admin_user).admin?).to be_truthy + expect(create(:user).admin?).to be_falsey + end + + context '#create' do + let(:user) { build(:user) } + + it 'should not be anonymous' do + expect(user).not_to be_anonymous + end + end + + context '#destroy' do + it 'can not delete if it has completed orders' do + order = build(:order, completed_at: Time.zone.now) + order.save + user = order.user + + expect { user.destroy }.to raise_exception(Spree::User::DestroyWithOrdersError) + end + end + + context 'anonymous!' do + let(:user) { Spree::User.anonymous! } + + it 'should create a new user' do + expect(user.new_record?).to be_falsey + end + + it 'should create a user with an example.net email' do + expect(user.email).to match(/@example.net$/) + end + + it 'should be anonymous' do + expect(user).to be_anonymous + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index cad940fac8..499a810302 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -128,7 +128,6 @@ RSpec.configure do |config| spree_config.auto_capture = true end - Spree::Auth::Config[:signout_after_password_change] = false Spree::Api::Config[:requires_authentication] = true end