diff --git a/app/controllers/alchemy/accounts_controller.rb b/app/controllers/alchemy/accounts_controller.rb
new file mode 100644
index 00000000..ba8a843d
--- /dev/null
+++ b/app/controllers/alchemy/accounts_controller.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Alchemy
+ class AccountsController < ::Devise::RegistrationsController
+ helper "Alchemy::Pages"
+
+ def show
+ authorize! :show, current_alchemy_user
+ @user = current_alchemy_user
+ end
+
+ private
+
+ def permission_denied(*)
+ store_location_for(:user, account_path)
+ flash[:warning] = t(:unauthenticated, scope: "devise.failure")
+ redirect_to alchemy.login_path
+ end
+ end
+end
diff --git a/app/controllers/alchemy/admin/passwords_controller.rb b/app/controllers/alchemy/admin/passwords_controller.rb
index df569e81..77d351a1 100644
--- a/app/controllers/alchemy/admin/passwords_controller.rb
+++ b/app/controllers/alchemy/admin/passwords_controller.rb
@@ -22,16 +22,8 @@ def new_session_path(resource_name)
alchemy.admin_login_path
end
- def admin_edit_password_url(_resource, options = {})
- alchemy.admin_edit_password_url(options)
- end
-
def after_resetting_password_path_for(resource)
- if can? :index, :alchemy_admin_dashboard
- alchemy.admin_dashboard_path
- else
- alchemy.root_path
- end
+ alchemy.admin_dashboard_path
end
end
end
diff --git a/app/controllers/alchemy/confirmations_controller.rb b/app/controllers/alchemy/confirmations_controller.rb
new file mode 100644
index 00000000..354b1ed4
--- /dev/null
+++ b/app/controllers/alchemy/confirmations_controller.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Alchemy
+ class ConfirmationsController < ::Devise::ConfirmationsController
+ helper "Alchemy::Pages"
+
+ private
+
+ def new_session_path(*)
+ alchemy.login_path
+ end
+ end
+end
diff --git a/app/controllers/alchemy/passwords_controller.rb b/app/controllers/alchemy/passwords_controller.rb
new file mode 100644
index 00000000..c90176fc
--- /dev/null
+++ b/app/controllers/alchemy/passwords_controller.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Alchemy
+ class PasswordsController < ::Devise::PasswordsController
+ helper "Alchemy::Pages"
+
+ private
+
+ def new_session_path(*)
+ alchemy.login_path
+ end
+ end
+end
diff --git a/app/controllers/alchemy/sessions_controller.rb b/app/controllers/alchemy/sessions_controller.rb
new file mode 100644
index 00000000..89954af0
--- /dev/null
+++ b/app/controllers/alchemy/sessions_controller.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Alchemy
+ class SessionsController < ::Devise::SessionsController
+ helper "Alchemy::Pages"
+
+ private
+
+ def after_sign_in_path_for(user)
+ stored_location_for(user) || alchemy.account_path
+ end
+ end
+end
diff --git a/app/mailers/alchemy/notifications.rb b/app/mailers/alchemy/notifications.rb
index 33327df3..d2fede33 100644
--- a/app/mailers/alchemy/notifications.rb
+++ b/app/mailers/alchemy/notifications.rb
@@ -1,14 +1,15 @@
-module Alchemy
- class Notifications < ActionMailer::Base
+# frozen_string_literal: true
- default(from: Config.get(:mailer)['mail_from'])
+module Alchemy
+ class Notifications < ::Devise::Mailer
+ default(from: Config.get(:mailer)["mail_from"])
def member_created(user)
@user = user
mail(
to: user.email,
- subject: Alchemy.t("Your user credentials")
+ subject: Alchemy.t("Your user credentials"),
)
end
@@ -17,16 +18,34 @@ def alchemy_user_created(user)
@url = admin_url
mail(
to: user.email,
- subject: Alchemy.t("Your Alchemy Login")
+ subject: Alchemy.t("Your Alchemy Login"),
+ )
+ end
+
+ def member_reset_password_instructions(user, token, _opts = {})
+ @user = user
+ @token = token
+ mail(
+ to: user.email,
+ subject: Alchemy.t("Reset password instructions"),
+ )
+ end
+
+ def reset_password_instructions(user, token, _opts = {})
+ @user = user
+ @token = token
+ mail(
+ to: user.email,
+ subject: Alchemy.t("Reset password instructions"),
)
end
- def reset_password_instructions(user, token, opts={})
+ def confirmation_instructions(user, token, _opts = {})
@user = user
@token = token
mail(
to: user.email,
- subject: Alchemy.t("Reset password instructions")
+ subject: Alchemy.t("Account confirmation instructions"),
)
end
end
diff --git a/app/models/alchemy/user.rb b/app/models/alchemy/user.rb
index bd574c88..d423f5de 100644
--- a/app/models/alchemy/user.rb
+++ b/app/models/alchemy/user.rb
@@ -1,5 +1,7 @@
-require 'devise/orm/active_record'
-require 'userstamp'
+# frozen_string_literal: true
+
+require "devise/orm/active_record"
+require "userstamp"
module Alchemy
class User < ActiveRecord::Base
@@ -12,7 +14,7 @@ class User < ActiveRecord::Base
:password,
:password_confirmation,
:send_credentials,
- :tag_list
+ :tag_list,
]
devise *Alchemy.devise_modules
@@ -29,9 +31,9 @@ class User < ActiveRecord::Base
# Unlock all locked pages before destroy.
before_destroy :unlock_pages!
- scope :admins, -> { where(arel_table[:alchemy_roles].matches('%admin%')) }
- scope :logged_in, -> { where('last_request_at > ?', logged_in_timeout.seconds.ago) }
- scope :logged_out, -> { where('last_request_at is NULL or last_request_at <= ?', logged_in_timeout.seconds.ago) }
+ scope :admins, -> { where(arel_table[:alchemy_roles].matches("%admin%")) }
+ scope :logged_in, -> { where("last_request_at > ?", logged_in_timeout.seconds.ago) }
+ scope :logged_out, -> { where("last_request_at is NULL or last_request_at <= ?", logged_in_timeout.seconds.ago) }
ROLES = Config.get(:user_roles)
@@ -51,10 +53,7 @@ def logged_in_timeout
def search(query)
query = "%#{query.downcase}%"
- where arel_table[:login].lower.matches(query)
- .or arel_table[:email].lower.matches(query)
- .or arel_table[:firstname].lower.matches(query)
- .or arel_table[:lastname].lower.matches(query)
+ where arel_table[:login].lower.matches(query).or arel_table[:email].lower.matches(query).or arel_table[:firstname].lower.matches(query).or arel_table[:lastname].lower.matches(query)
end
end
@@ -67,12 +66,12 @@ def role
end
def alchemy_roles
- read_attribute(:alchemy_roles).split(' ')
+ read_attribute(:alchemy_roles).split(" ")
end
def alchemy_roles=(roles_string)
if roles_string.is_a? Array
- write_attribute(:alchemy_roles, roles_string.join(' '))
+ write_attribute(:alchemy_roles, roles_string.join(" "))
elsif roles_string.is_a? String
write_attribute(:alchemy_roles, roles_string)
end
@@ -84,8 +83,9 @@ def add_role(role)
# Returns true if the user ahs admin role
def is_admin?
- has_role? 'admin'
+ has_role? "admin"
end
+
alias_method :admin?, :is_admin?
# Returns true if the user has the given role.
@@ -105,6 +105,7 @@ def unlock_pages!
def pages_locked_by_me
Page.locked_by(self).order(:updated_at)
end
+
alias_method :locked_pages, :pages_locked_by_me
# Returns the firstname and lastname as a string
@@ -118,11 +119,12 @@ def fullname(options = {})
if lastname.blank? && firstname.blank?
login
else
- options = {:flipped => false}.merge(options)
+ options = { flipped: false }.merge(options)
fullname = options[:flipped] ? "#{lastname}, #{firstname}" : "#{firstname} #{lastname}"
fullname.squeeze(" ").strip
end
end
+
alias_method :name, :fullname
alias_method :alchemy_display_name, :fullname
@@ -150,13 +152,22 @@ def store_request_time!
# Delivers a welcome mail depending from user's role.
#
def deliver_welcome_mail
- if has_role?('author') || has_role?('editor') || has_role?('admin')
+ if has_role?("author") || has_role?("editor") || has_role?("admin")
Notifications.alchemy_user_created(self).deliver_later
else
Notifications.member_created(self).deliver_later
end
end
+ # Overwritten to send a different email to members
+ def send_reset_password_instructions_notification(token)
+ if has_role?("member")
+ send_devise_notification(:member_reset_password_instructions, token, {})
+ else
+ send_devise_notification(:reset_password_instructions, token, {})
+ end
+ end
+
private
def logged_in_timeout
diff --git a/app/views/alchemy/accounts/show.html.erb b/app/views/alchemy/accounts/show.html.erb
new file mode 100644
index 00000000..a5c0a172
--- /dev/null
+++ b/app/views/alchemy/accounts/show.html.erb
@@ -0,0 +1,3 @@
+
Hallo <%= @user.fullname %>
+
+<%= link_to 'Edit account', alchemy.edit_account_path %>
diff --git a/app/views/alchemy/devise/shared/_links.html.erb b/app/views/alchemy/devise/shared/_links.html.erb
new file mode 100644
index 00000000..cd17113f
--- /dev/null
+++ b/app/views/alchemy/devise/shared/_links.html.erb
@@ -0,0 +1,15 @@
+<%- if controller_name != 'sessions' %>
+ <%= link_to t('.login', default: 'Log in'), alchemy.login_path %>
+<% end %>
+
+<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
+ <%= link_to t('.sign_up', default: 'Sign up'), alchemy.new_account_path %>
+<% end %>
+
+<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
+ <%= link_to t('.forgot_password', default: 'Forgot your password?'), alchemy.new_password_path %>
+<% end %>
+
+<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
+ <%= link_to t('.confirmation_instructions', default: "Didn't receive confirmation instructions?"), alchemy.new_confirmation_path %>
+<% end %>
diff --git a/app/views/alchemy/notifications/confirmation_instructions.de.text.erb b/app/views/alchemy/notifications/confirmation_instructions.de.text.erb
new file mode 100644
index 00000000..d96c9e19
--- /dev/null
+++ b/app/views/alchemy/notifications/confirmation_instructions.de.text.erb
@@ -0,0 +1,5 @@
+Willkommen <%= @user.fullname %>!
+
+Bitte bestätigen Sie Ihre E-Mail-Adresse durch klicken auf folgenden Link:
+
+<%= alchemy.confirmation_url(@user, confirmation_token: @token) %>
diff --git a/app/views/alchemy/notifications/confirmation_instructions.en.text.erb b/app/views/alchemy/notifications/confirmation_instructions.en.text.erb
new file mode 100644
index 00000000..1aacf311
--- /dev/null
+++ b/app/views/alchemy/notifications/confirmation_instructions.en.text.erb
@@ -0,0 +1,5 @@
+Welcome <%= @user.fullname %>!
+
+You can confirm your account email through the link below:
+
+<%= alchemy.confirmation_url(@user, confirmation_token: @token) %>
diff --git a/app/views/alchemy/notifications/member_reset_password_instructions.de.text.erb b/app/views/alchemy/notifications/member_reset_password_instructions.de.text.erb
new file mode 100644
index 00000000..f306fa0b
--- /dev/null
+++ b/app/views/alchemy/notifications/member_reset_password_instructions.de.text.erb
@@ -0,0 +1,8 @@
+Hallo <%= @user.fullname %>.
+
+Sie haben angefordert Ihr Passwort zurückzusetzen. Dies kann durch anklicken des nachfolgenden Links bestätigt werden.
+
+<%= alchemy.edit_password_url(@user, reset_password_token: @token) %>
+
+Wenn Sie diese Zurücksetzung nicht angefragt haben, dann können Sie diese E-Mail einfach ignorieren.
+Ihr Passwort wird erst dann zurückgesetzt, wenn Sie den Link anklicken.
diff --git a/app/views/alchemy/notifications/member_reset_password_instructions.en.text.erb b/app/views/alchemy/notifications/member_reset_password_instructions.en.text.erb
new file mode 100644
index 00000000..e1150cbf
--- /dev/null
+++ b/app/views/alchemy/notifications/member_reset_password_instructions.en.text.erb
@@ -0,0 +1,8 @@
+Hello <%= @user.name %>.
+
+You have requested to change your password. Please confirm this by clicking the link below.
+
+<%= alchemy.edit_password_url(@user, reset_password_token: @token) %>
+
+If you didn't request this, please ignore this email.
+Your password won't change until you access the link above and create a new one.
diff --git a/app/views/alchemy/notifications/member_reset_password_instructions.es.text.erb b/app/views/alchemy/notifications/member_reset_password_instructions.es.text.erb
new file mode 100644
index 00000000..55d62afc
--- /dev/null
+++ b/app/views/alchemy/notifications/member_reset_password_instructions.es.text.erb
@@ -0,0 +1,8 @@
+Hola <%= @user.name %>.
+
+Has solicitado modificar tu contraseña. Por favor, confírmalo pulsando en el siguiente enlace.
+
+<%= alchemy.edit_password_url(@user, reset_password_token: @token) %>
+
+Si no has sido tu el que ha hecho la solicitud, ignora este correo.
+Tu contraseña no cambiará hasta que no accedas al enlace de arriba y generes una nueva.
diff --git a/app/views/alchemy/notifications/member_reset_password_instructions.ru.text.erb b/app/views/alchemy/notifications/member_reset_password_instructions.ru.text.erb
new file mode 100644
index 00000000..c4ecfdca
--- /dev/null
+++ b/app/views/alchemy/notifications/member_reset_password_instructions.ru.text.erb
@@ -0,0 +1,8 @@
+Здравствуйте, <%= @user.name %>.
+
+Вы сделали запрос на смену пароля. Пожалуйста подтвердите это, нажав на ссылку ниже.
+
+<%= alchemy.edit_password_url(@user, reset_password_token: @token) %>
+
+Если вы не делали запрос, просто проигнорируйте это письмо.
+Ваш пароль не изменится до тех пор, пока вы не перейдете по ссылке и сами не измените его.
diff --git a/app/views/alchemy/passwords/edit.html.erb b/app/views/alchemy/passwords/edit.html.erb
new file mode 100644
index 00000000..786a7a88
--- /dev/null
+++ b/app/views/alchemy/passwords/edit.html.erb
@@ -0,0 +1,27 @@
+<%= t('.title', default: 'Change your password') %>
+
+<%= simple_form_for(@user, as: :user, url: alchemy.password_path, html: { method: :put }) do |f| %>
+ <%= f.error_notification %>
+
+ <%= f.input :reset_password_token, as: :hidden %>
+ <%= f.full_error :reset_password_token %>
+
+
+ <%= f.input :password,
+ label: t('.password.label', default: 'New password'),
+ required: true,
+ autofocus: true,
+ hint: t('.hint', default: '%{minimum} characters minimum', minimum: @minimum_password_length) if @minimum_password_length,
+ input_html: { autocomplete: "new-password" } %>
+ <%= f.input :password_confirmation,
+ label: t('.password_confirmation.label', default: 'Confirm your new password'),
+ required: true,
+ input_html: { autocomplete: "new-password" } %>
+
+
+
+ <%= f.button :submit, t('.button.label', default: 'Change my password') %>
+
+<% end %>
+
+<%= render "alchemy/devise/shared/links" %>
diff --git a/app/views/alchemy/passwords/new.html.erb b/app/views/alchemy/passwords/new.html.erb
new file mode 100644
index 00000000..30090daa
--- /dev/null
+++ b/app/views/alchemy/passwords/new.html.erb
@@ -0,0 +1,18 @@
+<%= t('.title', default: 'Forgot your password?') %>
+
+<%= simple_form_for(@user, as: :user, url: alchemy.password_path, html: { method: :post }) do |f| %>
+ <%= f.error_notification %>
+
+
+ <%= f.input :email,
+ required: true,
+ autofocus: true,
+ input_html: { autocomplete: "email" } %>
+
+
+
+ <%= f.button :submit, t('.button.label', default: 'Send me reset password instructions') %>
+
+<% end %>
+
+<%= render "alchemy/devise/shared/links" %>
diff --git a/app/views/alchemy/sessions/new.html.erb b/app/views/alchemy/sessions/new.html.erb
new file mode 100644
index 00000000..5b5954b6
--- /dev/null
+++ b/app/views/alchemy/sessions/new.html.erb
@@ -0,0 +1,32 @@
+<%= t('.title', default: 'Log in') %>
+
+<% if flash[:alert] %>
+
+ <%= flash[:alert] %>
+
+<% end %>
+
+<% if flash[:notice] %>
+
+ <%= flash[:notice] %>
+
+<% end %>
+
+<%= simple_form_for(@user, as: :user, url: alchemy.login_path) do |f| %>
+
+ <%= f.input :email,
+ required: false,
+ autofocus: true,
+ input_html: { autocomplete: "email" } %>
+ <%= f.input :password,
+ required: false,
+ input_html: { autocomplete: "current-password" } %>
+ <%= f.input :remember_me, as: :boolean if devise_mapping.rememberable? %>
+
+
+
+ <%= f.button :submit, t('.button.label', default: 'Log in') %>
+
+<% end %>
+
+<%= render "alchemy/devise/shared/links" %>
diff --git a/config/routes.rb b/config/routes.rb
index 055cce5c..c3cfbe78 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,37 +1,70 @@
+# frozen_string_literal: true
+
Alchemy::Engine.routes.draw do
+ if Alchemy::Devise.enable_user_accounts?
+ devise_for :user,
+ class_name: "Alchemy::User",
+ singular: :user,
+ skip: :all,
+ controllers: {
+ registrations: Alchemy::Devise.registrations_enabled? ? "alchemy/accounts" : nil,
+ confirmations: Alchemy::Devise.confirmations_enabled? ? "alchemy/confirmations" : nil,
+ sessions: "alchemy/sessions",
+ passwords: "alchemy/passwords",
+ },
+ path: :account,
+ router_name: :alchemy
+
+ scope :account do
+ devise_scope :user do
+ get "/login" => "sessions#new"
+ post "/login" => "sessions#create"
+ match "/logout" => "sessions#destroy", via: Devise.sign_out_via
+
+ if Alchemy::Devise.confirmations_enabled?
+ resource :confirmation, only: %i[new create show]
+ end
+ resource :password, only: %i[new create edit update]
+ end
+ end
+
+ devise_scope :user do
+ resource :account, except: Alchemy::Devise.registrations_enabled? ? [] : %i[new create]
+ end
+ end
+
namespace :admin, {
path: Alchemy.admin_path,
- constraints: Alchemy.admin_constraints
+ constraints: Alchemy.admin_constraints,
} do
-
devise_for :user,
- class_name: 'Alchemy::User',
+ class_name: "Alchemy::User",
singular: :user,
skip: :all,
controllers: {
- sessions: 'alchemy/admin/user_sessions',
- passwords: 'alchemy/admin/passwords'
+ sessions: "alchemy/admin/user_sessions",
+ passwords: "alchemy/admin/passwords",
},
router_name: :alchemy
devise_scope :user do
- get '/dashboard' => 'dashboard#index',
+ get "/dashboard" => "dashboard#index",
:as => :user_root
- get '/signup' => 'users#signup',
+ get "/signup" => "users#signup",
:as => :signup
- get '/login' => 'user_sessions#new',
+ get "/login" => "user_sessions#new",
:as => :login
- post '/login' => 'user_sessions#create'
- match '/logout' => 'user_sessions#destroy',
+ post "/login" => "user_sessions#create"
+ match "/logout" => "user_sessions#destroy",
:as => :logout, via: Devise.sign_out_via
- get '/passwords' => 'passwords#new',
+ get "/passwords" => "passwords#new",
:as => :new_password
- get '/passwords/:id/edit/:reset_password_token' => 'passwords#edit',
+ get "/passwords/:id/edit/:reset_password_token" => "passwords#edit",
:as => :edit_password
- post '/passwords' => 'passwords#create',
+ post "/passwords" => "passwords#create",
:as => :reset_password
- patch '/passwords' => 'passwords#update',
+ patch "/passwords" => "passwords#update",
:as => :update_password
end
diff --git a/lib/alchemy/devise.rb b/lib/alchemy/devise.rb
index 65eb8a28..658e1ddb 100644
--- a/lib/alchemy/devise.rb
+++ b/lib/alchemy/devise.rb
@@ -6,12 +6,12 @@ module Alchemy
# === Default modules
#
# [
- #. :database_authenticatable,
+ # :database_authenticatable,
# :trackable,
# :validatable,
# :timeoutable,
# :recoverable
- #. ]
+ # ]
#
# If you want to add additional modules into the Alchemy user class append
# them to this collection in an initializer in your app.
@@ -36,6 +36,25 @@ def self.devise_modules
]
end
+ def self.devise_modules=(modules)
+ @devise_modules = modules
+ end
+
module Devise
+ def self.enable_user_accounts?
+ @enable_user_accounts ||= false
+ end
+
+ def self.enable_user_accounts=(val)
+ @enable_user_accounts = val
+ end
+
+ def self.registrations_enabled?
+ Alchemy.devise_modules.include?(:registerable)
+ end
+
+ def self.confirmations_enabled?
+ Alchemy.devise_modules.include?(:confirmable)
+ end
end
end
diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb
new file mode 100644
index 00000000..0122c45f
--- /dev/null
+++ b/spec/controllers/accounts_controller_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe Alchemy::AccountsController do
+ routes { Alchemy::Engine.routes }
+
+ context "with user accounts enabled" do
+ before do
+ allow(Alchemy::Devise).to receive(:enable_user_accounts?) { true }
+ Rails.application.reload_routes!
+ @request.env["devise.mapping"] = Devise.mappings[:user]
+ end
+
+ describe "#show" do
+ let(:user) { create(:alchemy_member_user) }
+
+ context "with authorized user" do
+ before { authorize_user(user) }
+
+ render_views
+
+ it "shows account" do
+ get :show
+ is_expected.to render_template(:show)
+ end
+ end
+
+ context "with unauthorized user" do
+ it "redirects to login" do
+ get :show
+ is_expected.to redirect_to(login_path)
+ end
+
+ it "stores current location" do
+ get :show
+ expect(session[:user_return_to]).to eq(account_path)
+ end
+
+ it "shows warning message" do
+ get :show
+ expect(flash[:warning]).to eq I18n.t(:unauthenticated, scope: "devise.failure")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/confirmations_controller_spec.rb b/spec/controllers/confirmations_controller_spec.rb
new file mode 100644
index 00000000..21f9879a
--- /dev/null
+++ b/spec/controllers/confirmations_controller_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe Alchemy::ConfirmationsController do
+ routes { Alchemy::Engine.routes }
+
+ context "with user accounts enabled" do
+ before do
+ allow(Alchemy::Devise).to receive(:enable_user_accounts?) { true }
+ end
+
+ context "with confirmations enabled" do
+ let(:user) { double(email: "mail@example.com") }
+
+ before do
+ allow(Alchemy::Devise).to receive(:confirmations_enabled?) { true }
+ Rails.application.reload_routes!
+ @request.env["devise.mapping"] = Devise.mappings[:user]
+ expect(Alchemy::User).to receive(:send_confirmation_instructions) { user }
+ end
+
+ describe "#create" do
+ context "with valid params" do
+ before do
+ expect(user).to receive(:errors) { [] }
+ end
+
+ it "redirects to account" do
+ post :create, params: { user: { email: user.email } }
+ expect(response).to redirect_to(login_path)
+ end
+ end
+
+ context "without valid params" do
+ before do
+ expect(user).to receive(:errors).twice { ["Email not found"] }
+ end
+
+ it "renders form" do
+ post :create, params: { user: { email: "not@found" } }
+ is_expected.to render_template(:new)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/passwords_controller_spec.rb b/spec/controllers/passwords_controller_spec.rb
new file mode 100644
index 00000000..79d3a223
--- /dev/null
+++ b/spec/controllers/passwords_controller_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe Alchemy::PasswordsController do
+ routes { Alchemy::Engine.routes }
+
+ context "with user accounts enabled" do
+ before(:all) do
+ Alchemy::Devise.enable_user_accounts = true
+ Rails.application.reload_routes!
+ end
+
+ before do
+ @request.env["devise.mapping"] = Devise.mappings[:user]
+ end
+
+ let!(:user) { create(:alchemy_user) }
+
+ describe "#create" do
+ context "with valid params" do
+ it "redirects to login" do
+ post :create, params: { user: { email: user.email } }
+ expect(response).to redirect_to(login_path)
+ end
+ end
+
+ context "without valid params" do
+ it "renders form" do
+ post :create, params: { user: { email: "not@found" } }
+ is_expected.to render_template(:new)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
new file mode 100644
index 00000000..497da995
--- /dev/null
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe Alchemy::SessionsController do
+ routes { Alchemy::Engine.routes }
+
+ context "with user accounts enabled" do
+ before(:all) do
+ Alchemy::Devise.enable_user_accounts = true
+ Rails.application.reload_routes!
+ end
+
+ before do
+ @request.env["devise.mapping"] = Devise.mappings[:user]
+ end
+
+ let(:user) { create(:alchemy_user) }
+
+ describe "#create" do
+ context "with valid user" do
+ let(:user_params) do
+ {
+ login: user.login,
+ password: "s3cr3t",
+ }
+ end
+
+ before { user }
+
+ context "without redirect path in session" do
+ it "redirects to account" do
+ post :create, params: { user: user_params }
+ expect(response).to redirect_to(account_path)
+ end
+ end
+
+ context "with redirect path in session" do
+ it "redirects to these params" do
+ session[:user_return_to] = "/secret_page"
+ post :create, params: { user: user_params }
+ expect(response).to redirect_to("/secret_page")
+ end
+ end
+
+ context "without valid params" do
+ it "renders login form" do
+ post :create, params: { user: { login: "" } }
+ is_expected.to render_template(:new)
+ end
+ end
+ end
+ end
+
+ describe "#destroy" do
+ before do
+ authorize_user(user)
+ end
+
+ it "redirects to root" do
+ delete :destroy
+ is_expected.to redirect_to(root_path)
+ end
+ end
+ end
+end
diff --git a/spec/features/login_feature_spec.rb b/spec/features/admin/login_feature_spec.rb
similarity index 98%
rename from spec/features/login_feature_spec.rb
rename to spec/features/admin/login_feature_spec.rb
index da7a6fe8..1742e56c 100644
--- a/spec/features/login_feature_spec.rb
+++ b/spec/features/admin/login_feature_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe "Login: " do
+describe "Admin Login: " do
context "If user is present" do
let!(:user) do
Alchemy::User.create!(
diff --git a/spec/features/password_reset_feature_spec.rb b/spec/features/admin/password_reset_feature_spec.rb
similarity index 87%
rename from spec/features/password_reset_feature_spec.rb
rename to spec/features/admin/password_reset_feature_spec.rb
index 30d1d3d4..a537fcc7 100644
--- a/spec/features/password_reset_feature_spec.rb
+++ b/spec/features/admin/password_reset_feature_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe "Password reset feature." do
+describe "Admin password reset feature." do
let(:user) { create(:alchemy_admin_user) }
it "User can visit password reset form." do
@@ -20,7 +20,7 @@
end
it "User can change password." do
- allow(Alchemy::User)
+ expect(Alchemy::User)
.to receive(:reset_password_by_token)
.and_return(user)
@@ -30,6 +30,8 @@
fill_in :user_password_confirmation, with: 'secret123'
click_button 'Change password'
+ expect(page.current_path).to eq(alchemy.admin_dashboard_path)
+
expect(page)
.to have_content('Your password has been changed successfully.')
end
diff --git a/spec/mailers/notifications_spec.rb b/spec/mailers/notifications_spec.rb
index 377735b9..705467b6 100644
--- a/spec/mailers/notifications_spec.rb
+++ b/spec/mailers/notifications_spec.rb
@@ -1,21 +1,22 @@
-require 'rails_helper'
+# frozen_string_literal: true
+
+require "rails_helper"
module Alchemy
describe Notifications do
-
context "when a member user was created" do
let(:user) do
- mock_model 'User',
+ mock_model "User",
alchemy_roles: %w(member),
- email: 'jon@doe.com',
- name: 'John Doe',
- login: 'jon.doe'
+ email: "jon@doe.com",
+ name: "John Doe",
+ login: "jon.doe"
end
let(:mail) { Notifications.member_created(user) }
it "delivers a mail to user" do
expect(mail.to).to eq([user.email])
- expect(mail.subject).to eq('Your user credentials')
+ expect(mail.subject).to eq("Your user credentials")
end
it "mail body includes users name" do
@@ -32,12 +33,12 @@ module Alchemy
end
context "when an admin user was created" do
- let(:user) { mock_model('User', alchemy_roles: %w(admin), email: 'jon@doe.com', name: 'John Doe', login: 'jon.doe') }
+ let(:user) { mock_model("User", alchemy_roles: %w(admin), email: "jon@doe.com", name: "John Doe", login: "jon.doe") }
let(:mail) { Notifications.alchemy_user_created(user) }
it "delivers a mail to user" do
expect(mail.to).to eq([user.email])
- expect(mail.subject).to eq('Your Alchemy Login')
+ expect(mail.subject).to eq("Your Alchemy Login")
end
it "mail body includes users login" do
@@ -49,17 +50,17 @@ module Alchemy
end
end
- describe '#reset_password_instructions' do
+ describe "#reset_password_instructions" do
let(:user) do
- mock_model 'User',
+ mock_model "User",
alchemy_roles: %w(member),
- email: 'jon@doe.com',
- name: 'John Doe',
- login: 'jon.doe',
- fullname: 'John Doe'
+ email: "jon@doe.com",
+ name: "John Doe",
+ login: "jon.doe",
+ fullname: "John Doe"
end
- let(:token) { '123456789' }
+ let(:token) { "123456789" }
let(:mail) do
Notifications.reset_password_instructions(user, token)
@@ -67,7 +68,7 @@ module Alchemy
it "delivers a mail to user" do
expect(mail.to).to eq([user.email])
- expect(mail.subject).to eq('Reset password instructions')
+ expect(mail.subject).to eq("Reset password instructions")
end
it "mail body includes users name" do
@@ -78,5 +79,43 @@ module Alchemy
expect(mail.body.raw_source).to match /#{Regexp.escape(admin_edit_password_url(user, reset_password_token: token, only_path: true))}/
end
end
+
+ context "with user accounts and confirmations enabled" do
+ before do
+ allow(Alchemy::Devise).to receive(:enable_user_accounts?) { true }
+ allow(Alchemy::Devise).to receive(:confirmations_enabled?) { true }
+ Rails.application.reload_routes!
+ end
+
+ describe "#confirmation_instructions" do
+ let(:user) do
+ mock_model "User",
+ alchemy_roles: %w(member),
+ email: "jon@doe.com",
+ name: "John Doe",
+ login: "jon.doe",
+ fullname: "John Doe"
+ end
+
+ let(:token) { "123456789" }
+
+ let(:mail) do
+ Notifications.confirmation_instructions(user, token)
+ end
+
+ it "delivers a mail to user" do
+ expect(mail.to).to eq([user.email])
+ expect(mail.subject).to eq("Account confirmation instructions")
+ end
+
+ it "mail body includes users name" do
+ expect(mail.body.raw_source).to match /#{user.fullname}/
+ end
+
+ it "mail body includes reset instructions" do
+ expect(mail.body.raw_source).to match /#{Regexp.escape(confirmation_url(user, confirmation_token: token, only_path: true))}/
+ end
+ end
+ end
end
end
diff --git a/spec/routing/account_routing_spec.rb b/spec/routing/account_routing_spec.rb
new file mode 100644
index 00000000..a2212986
--- /dev/null
+++ b/spec/routing/account_routing_spec.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe "Account Routing" do
+ context "if user accounts are enabled" do
+ before(:all) do
+ Alchemy::Devise.enable_user_accounts = true
+ Rails.application.reload_routes!
+ end
+
+ it "routes to show account" do
+ expect({
+ get: "/account",
+ }).to route_to(
+ controller: "alchemy/accounts",
+ action: "show",
+ )
+ end
+
+ it "routes to edit account" do
+ expect({
+ get: "/account/edit",
+ }).to route_to(
+ controller: "alchemy/accounts",
+ action: "edit",
+ )
+ end
+
+ it "routes to update account" do
+ expect({
+ patch: "/account",
+ }).to route_to(
+ controller: "alchemy/accounts",
+ action: "update",
+ )
+ end
+
+ it "routes to destroy account" do
+ expect({
+ delete: "/account",
+ }).to route_to(
+ controller: "alchemy/accounts",
+ action: "destroy",
+ )
+ end
+
+ context "when registrations are enabled" do
+ before do
+ allow(Alchemy::Devise).to receive(:registrations_enabled?) { true }
+ Rails.application.reload_routes!
+ end
+
+ it "routes to new account" do
+ expect({
+ get: "/account/new",
+ }).to route_to(
+ controller: "alchemy/accounts",
+ action: "new",
+ )
+ end
+
+ it "routes to create account" do
+ expect({
+ post: "/account",
+ }).to route_to(
+ controller: "alchemy/accounts",
+ action: "create",
+ )
+ end
+ end
+ end
+
+ context "if user accounts are disabled" do
+ before(:all) do
+ Alchemy::Devise.enable_user_accounts = false
+ Rails.application.reload_routes!
+ end
+
+ it "does not route to show account" do
+ expect({
+ get: "/account",
+ }).to_not route_to(
+ controller: "alchemy/accounts",
+ action: "show",
+ )
+ end
+
+ it "does not route to edit account" do
+ expect({
+ get: "/account/edit",
+ }).to_not route_to(
+ controller: "alchemy/accounts",
+ action: "edit",
+ )
+ end
+
+ it "does not route to update account" do
+ expect({
+ patch: "/account",
+ }).to_not route_to(
+ controller: "alchemy/accounts",
+ action: "update",
+ )
+ end
+
+ it "does not route to destroy account" do
+ expect({
+ delete: "/account",
+ }).to_not route_to(
+ controller: "alchemy/accounts",
+ action: "destroy",
+ )
+ end
+
+ it "does not route to new account" do
+ expect({
+ get: "/account/new",
+ }).to_not route_to(
+ controller: "alchemy/accounts",
+ action: "new",
+ )
+ end
+
+ it "does not route to create account" do
+ expect({
+ post: "/account",
+ }).to_not route_to(
+ controller: "alchemy/accounts",
+ action: "create",
+ )
+ end
+ end
+end
diff --git a/spec/routing/admin_password_routing_spec.rb b/spec/routing/admin_password_routing_spec.rb
new file mode 100644
index 00000000..7d0f372e
--- /dev/null
+++ b/spec/routing/admin_password_routing_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe "Admin Password Routing" do
+ routes { Alchemy::Engine.routes }
+
+ it "routes to new password" do
+ expect({
+ get: "/admin/passwords",
+ }).to route_to(
+ controller: "alchemy/admin/passwords",
+ action: "new",
+ )
+ end
+
+ it "routes to reset password" do
+ expect({
+ post: "/admin/passwords",
+ }).to route_to(
+ controller: "alchemy/admin/passwords",
+ action: "create",
+ )
+ end
+
+ it "routes to edit password" do
+ expect({
+ get: "/admin/passwords/123/edit/12345",
+ }).to route_to(
+ controller: "alchemy/admin/passwords",
+ action: "edit",
+ id: "123",
+ reset_password_token: "12345",
+ )
+ end
+
+ it "routes to update password" do
+ expect({
+ patch: "/admin/passwords",
+ }).to route_to(
+ controller: "alchemy/admin/passwords",
+ action: "update",
+ )
+ end
+end
diff --git a/spec/routing/admin_user_session_routing_spec.rb b/spec/routing/admin_user_session_routing_spec.rb
new file mode 100644
index 00000000..49d267d7
--- /dev/null
+++ b/spec/routing/admin_user_session_routing_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe "Admin User Session Routing" do
+ routes { Alchemy::Engine.routes }
+
+ it "routes to login" do
+ expect({
+ get: "/admin/login",
+ }).to route_to(
+ controller: "alchemy/admin/user_sessions",
+ action: "new",
+ )
+ end
+
+ it "routes to create session" do
+ expect({
+ post: "/admin/login",
+ }).to route_to(
+ controller: "alchemy/admin/user_sessions",
+ action: "create",
+ )
+ end
+
+ it "routes to logout" do
+ expect({
+ delete: "/admin/logout",
+ }).to route_to(
+ controller: "alchemy/admin/user_sessions",
+ action: "destroy",
+ )
+ end
+end
diff --git a/spec/routing/password_routing_spec.rb b/spec/routing/password_routing_spec.rb
index 008e577e..d5e3839b 100644
--- a/spec/routing/password_routing_spec.rb
+++ b/spec/routing/password_routing_spec.rb
@@ -1,43 +1,91 @@
-require 'rails_helper'
+# frozen_string_literal: true
+
+require "rails_helper"
describe "Password Routing" do
- routes { Alchemy::Engine.routes }
-
- it "routes to new password" do
- expect({
- get: "/admin/passwords"
- }).to route_to(
- controller: "alchemy/admin/passwords",
- action: "new"
- )
- end
+ context "if user accounts are enabled" do
+ before(:all) do
+ Alchemy::Devise.enable_user_accounts = true
+ Rails.application.reload_routes!
+ end
- it "routes to reset password" do
- expect({
- post: "/admin/passwords"
- }).to route_to(
- controller: "alchemy/admin/passwords",
- action: "create"
- )
- end
+ it "routes to new password" do
+ expect({
+ get: "/account/password/new",
+ }).to route_to(
+ controller: "alchemy/passwords",
+ action: "new",
+ )
+ end
+
+ it "routes to reset password" do
+ expect({
+ post: "/account/password",
+ }).to route_to(
+ controller: "alchemy/passwords",
+ action: "create",
+ )
+ end
+
+ it "routes to edit password" do
+ expect({
+ get: "/account/password/edit",
+ }).to route_to(
+ controller: "alchemy/passwords",
+ action: "edit",
+ )
+ end
- it "routes to edit password" do
- expect({
- get: "/admin/passwords/123/edit/12345"
- }).to route_to(
- controller: "alchemy/admin/passwords",
- action: "edit",
- id: "123",
- reset_password_token: "12345"
- )
+ it "routes to update password" do
+ expect({
+ patch: "/account/password",
+ }).to route_to(
+ controller: "alchemy/passwords",
+ action: "update",
+ )
+ end
end
- it "routes to update password" do
- expect({
- patch: "/admin/passwords"
- }).to route_to(
- controller: "alchemy/admin/passwords",
- action: "update"
- )
+ context "if user accounts are disabled" do
+ before(:all) do
+ Alchemy::Devise.enable_user_accounts = false
+ Rails.application.reload_routes!
+ end
+
+ it "does not route to new password" do
+ expect({
+ get: "/account/password/new",
+ }).to_not route_to(
+ controller: "alchemy/passwords",
+ action: "new",
+ )
+ end
+
+ it "does not route to reset password" do
+ expect({
+ post: "/account/password",
+ }).to_not route_to(
+ controller: "alchemy/passwords",
+ action: "create",
+ )
+ end
+
+ it "does not route to edit password" do
+ expect({
+ get: "/account/password/edit",
+ }).to_not route_to(
+ controller: "alchemy/passwords",
+ action: "edit",
+ )
+ end
+
+ it "does not route to update password" do
+ expect({
+ patch: "/account/password",
+ }).to_not route_to(
+ controller: "alchemy/passwords",
+ action: "update",
+ )
+ end
end
end
diff --git a/spec/routing/session_routing_spec.rb b/spec/routing/session_routing_spec.rb
index 0d6b9a9e..304a8da5 100644
--- a/spec/routing/session_routing_spec.rb
+++ b/spec/routing/session_routing_spec.rb
@@ -1,32 +1,73 @@
-require 'rails_helper'
+# frozen_string_literal: true
+
+require "rails_helper"
describe "Session Routing" do
- routes { Alchemy::Engine.routes }
-
- it "routes to login" do
- expect({
- get: "/admin/login"
- }).to route_to(
- controller: "alchemy/admin/user_sessions",
- action: "new"
- )
- end
+ context "if user accounts are enabled" do
+ before(:all) do
+ Alchemy::Devise.enable_user_accounts = true
+ Rails.application.reload_routes!
+ end
+
+ it "routes to login" do
+ expect({
+ get: "/account/login",
+ }).to route_to(
+ controller: "alchemy/sessions",
+ action: "new",
+ )
+ end
+
+ it "routes to create session" do
+ expect({
+ post: "/account/login",
+ }).to route_to(
+ controller: "alchemy/sessions",
+ action: "create",
+ )
+ end
- it "routes to create session" do
- expect({
- post: "/admin/login"
- }).to route_to(
- controller: "alchemy/admin/user_sessions",
- action: "create"
- )
+ it "routes to logout" do
+ expect({
+ delete: "/account/logout",
+ }).to route_to(
+ controller: "alchemy/sessions",
+ action: "destroy",
+ )
+ end
end
- it "routes to logout" do
- expect({
- delete: "/admin/logout"
- }).to route_to(
- controller: "alchemy/admin/user_sessions",
- action: "destroy"
- )
+ context "if user accounts are disabled" do
+ before(:all) do
+ Alchemy::Devise.enable_user_accounts = false
+ Rails.application.reload_routes!
+ end
+
+ it "does not route to login" do
+ expect({
+ get: "/account/login",
+ }).to_not route_to(
+ controller: "alchemy/sessions",
+ action: "new",
+ )
+ end
+
+ it "does not route to create session" do
+ expect({
+ post: "/account/login",
+ }).to_not route_to(
+ controller: "alchemy/sessions",
+ action: "create",
+ )
+ end
+
+ it "does not route to logout" do
+ expect({
+ delete: "/account/logout",
+ }).to_not route_to(
+ controller: "alchemy/sessions",
+ action: "destroy",
+ )
+ end
end
end