Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<%= page_with_sidebar do %>
<%= page_with_sidebar_main do %>

<%= render component('ui/panel').new(title: Spree.user_class.model_name.human) do %>
<%= render component('ui/panel').new(title: Spree.admin_user_class.model_name.human) do %>
<%= solidus_form_for @user, url: solidus_admin.user_path(@user), html: { id: form_id, autocomplete: "off" } do |f| %>
<div class="py-1.5">
<%= f.text_field(:email) %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class SolidusAdmin::Users::Index::Component < SolidusAdmin::UsersAndRoles::Compo
include SolidusAdmin::LastLoginHelper

def model_class
Spree.user_class
Spree.admin_user_class
end

def search_key
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ def title
def tabs
[
{
text: Spree.user_class.model_name.human(count: 2),
text: Spree.admin_user_class.model_name.human(count: 2),
href: solidus_admin.users_path,
current: model_class == Spree.user_class
current: model_class == Spree.admin_user_class
},
{
text: Spree::Role.model_name.human(count: 2),
Expand Down
2 changes: 1 addition & 1 deletion admin/app/controllers/solidus_admin/orders_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def variants_for
def customers_for
load_order

@users = Spree.user_class
@users = Spree.admin_user_class
.where.not(id: @order.user_id)
.order(created_at: :desc, id: :desc)
.ransack(params[:q])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def set_store_credit
end

def set_user
@user = Spree.user_class.find(params[:user_id])
@user = Spree.admin_user_class.find(params[:user_id])
end

def set_store_credit_reasons
Expand Down
10 changes: 5 additions & 5 deletions admin/app/controllers/solidus_admin/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class UsersController < SolidusAdmin::BaseController

def index
users = apply_search_to(
Spree.user_class.order(created_at: :desc, id: :desc),
Spree.admin_user_class.order(created_at: :desc, id: :desc),
param: :q
)

Expand Down Expand Up @@ -73,9 +73,9 @@ def edit
end

def destroy
@users = Spree.user_class.where(id: params[:id])
@users = Spree.admin_user_class.where(id: params[:id])

Spree.user_class.transaction { @users.destroy_all }
Spree.admin_user_class.transaction { @users.destroy_all }

flash[:notice] = t(".success")
redirect_back_or_to users_path, status: :see_other
Expand All @@ -84,7 +84,7 @@ def destroy
private

def set_user
@user = Spree.user_class.find(params[:id])
@user = Spree.admin_user_class.find(params[:id])
end

def user_params
Expand Down Expand Up @@ -123,7 +123,7 @@ def set_items
end

def authorization_subject
Spree.user_class
Spree.admin_user_class
end
end
end
12 changes: 6 additions & 6 deletions admin/docs/index_pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ class SolidusAdmin::UsersController < SolidusAdmin::BaseController
include SolidusAdmin::ControllerHelpers::Search

def index
users = apply_search_to(Spree.user_class.order(id: :desc), param: :q)
users = apply_search_to(Spree.admin_user_class.order(id: :desc), param: :q)
# ...
```

For pagination support, the index action should also call the `set_page_and_extract_portion_from` method provided by the `geared_pagination` gem. This method sets the `@page` instance variable to the paginated collection and returns the portion of the collection to be displayed on the current page.

```ruby
def index
users = apply_search_to(Spree.user_class.order(id: :desc), param: :q)
users = apply_search_to(Spree.admin_user_class.order(id: :desc), param: :q)
set_page_and_extract_portion_from(users)
# ...
```
Expand All @@ -33,7 +33,7 @@ Finally, the index action should render the `index` component passing the `@page

```ruby
def index
users = apply_search_to(Spree.user_class.order(id: :desc), param: :q)
users = apply_search_to(Spree.admin_user_class.order(id: :desc), param: :q)
set_page_and_extract_portion_from(users)
render component('users/index').new(page: @page)
end
Expand All @@ -50,7 +50,7 @@ The index component requires only the `page` argument during initialization, all
```ruby
class SolidusAdmin::Users::Index < Solidus::Admin::UI::Pages::Index
def model_class
Spree.user_class
Spree.admin_user_class
end
end

Expand Down Expand Up @@ -92,7 +92,7 @@ end
```ruby
# in the controller
def delete
@users = Spree.user_class.where(id: params[:id])
@users = Spree.admin_user_class.where(id: params[:id])
@users.destroy_all
flash[:notice] = "Admin users deleted"
redirect_to solidus_admin.users_path, status: :see_other
Expand Down Expand Up @@ -124,7 +124,7 @@ module SolidusAdmin
search_scope(:all)

def index
users = apply_search_to(Spree.user_class.order(id: :desc), param: :q)
users = apply_search_to(Spree.admin_user_class.order(id: :desc), param: :q)
# ...
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def update
if @order.contents.update_cart(order_params)

if should_associate_user?
requested_user = Spree.user_class.find(params[:user_id])
requested_user = Spree.admin_user_class.find(params[:user_id])
@order.associate_user!(requested_user, @order.email.blank?)
end

Expand Down
2 changes: 1 addition & 1 deletion backend/app/controllers/spree/admin/orders_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def index
end

def new
user = Spree.user_class.find_by(id: params[:user_id]) if params[:user_id]
user = Spree.admin_user_class.find_by(id: params[:user_id]) if params[:user_id]
order_importer_params = order_params
order_importer_params[:bill_address] = user&.bill_address
order_importer_params[:ship_address] = user&.ship_address
Expand Down
4 changes: 2 additions & 2 deletions backend/app/controllers/spree/admin/search_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ class SearchController < Spree::Admin::BaseController
def users
@users = if params[:ids]
# split here may be String#split or Array#split, so we must flatten the results
Spree.user_class.where(id: params[:ids].split(",").flatten)
Spree.admin_user_class.where(id: params[:ids].split(",").flatten)
else
Spree.user_class.ransack({
Spree.admin_user_class.ransack({
m: "or",
email_start: params[:q],
name_start: params[:q]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module Spree
module Admin
class StoreCreditsController < ResourceController
belongs_to "spree/user", model_class: Spree.user_class
belongs_to "spree/user", model_class: Spree.admin_user_class
before_action :load_categories, only: [:new]
before_action :load_reasons, only: [:edit_amount, :edit_validity]
before_action :ensure_store_credit_reason, only: [:update_amount, :invalidate]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def destroy
private

def user
@user ||= Spree.user_class.find(params[:user_id])
@user ||= Spree.admin_user_class.find(params[:user_id])
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions backend/app/controllers/spree/admin/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def show
end

def create
@user = Spree.user_class.new(user_params)
@user = Spree.admin_user_class.new(user_params)
if @user.save
set_roles
set_stock_locations
Expand Down Expand Up @@ -82,7 +82,7 @@ def items
end

def model_class
Spree.user_class
Spree.admin_user_class
end

private
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,11 @@ def store_credit_event_originator_link(store_credit_event)
private

# Cannot set the value for a user originator
# because Spree.user_class is not defined at that time.
# because Spree.admin_user_class is not defined at that time.
# Spree::UserClassHandle does not work here either as
# the assignment is evaluated before user_class is set
def add_user_originator_link
originator_links[Spree.user_class.to_s] = {
originator_links[Spree.admin_user_class.to_s] = {
new_tab: true,
href_type: :user,
translation_key: "admin.store_credits.user_originator"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<% admin_breadcrumb(link_to plural_resource_name(Spree::LegacyUser), spree.admin_users_path) %>
<% admin_breadcrumb(link_to plural_resource_name(Spree.admin_user_class), spree.admin_users_path) %>
<% admin_breadcrumb(link_to @user.email, edit_admin_user_url(@user)) %>
<% admin_breadcrumb(link_to plural_resource_name(Spree::StoreCredit), spree.admin_user_store_credits_path(@user)) %>
<% admin_breadcrumb(link_to Spree::StoreCredit.model_name.human, admin_user_store_credit_path(@user, @store_credit)) %>
Expand Down
2 changes: 1 addition & 1 deletion backend/app/views/spree/admin/users/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<%= render partial: 'spree/admin/users/user_page_actions' %>

<fieldset data-hook="admin_user_edit_general_settings">
<legend><%= Spree.user_class.model_name.human %></legend>
<legend><%= Spree.admin_user_class.model_name.human %></legend>

<div data-hook="admin_user_edit_form_header">
<%= render partial: 'spree/shared/error_messages', locals: { target: @user } %>
Expand Down
12 changes: 6 additions & 6 deletions backend/app/views/spree/admin/users/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<% admin_breadcrumb(plural_resource_name(Spree::LegacyUser)) %>
<% admin_breadcrumb(plural_resource_name(Spree.admin_user_class)) %>

<% content_for :page_actions do %>
<% if can?(:admin, Spree.user_class) && can?(:create, Spree.user_class) %>
<% if can?(:admin, Spree.admin_user_class) && can?(:create, Spree.admin_user_class) %>
<li>
<%= link_to t('spree.new_user'), new_admin_user_url, id: 'admin_new_user_link', class: 'btn btn-primary' %>
</li>
Expand All @@ -24,7 +24,7 @@
</div>
<div class="col-xs-12 col-md-3">
<div class="form-group">
<%= f.label :spree_roles_id_in, Spree.user_class.human_attribute_name(:spree_roles) %>
<%= f.label :spree_roles_id_in, Spree.admin_user_class.human_attribute_name(:spree_roles) %>
<%= f.collection_select :spree_roles_id_in, @roles, :id, :name, {},
multiple: true, class: 'select2 fullwidth' %>
</div>
Expand Down Expand Up @@ -69,10 +69,10 @@
</colgroup>
<thead>
<tr data-hook="admin_users_index_headers">
<th><%= sort_link @search, :email, Spree.user_class.human_attribute_name(:email), {title: 'users_email_title'} %></th>
<th><%= Spree.user_class.human_attribute_name(:spree_roles) %></th>
<th><%= sort_link @search, :email, Spree.admin_user_class.human_attribute_name(:email), {title: 'users_email_title'} %></th>
<th><%= Spree.admin_user_class.human_attribute_name(:spree_roles) %></th>
<th class="align-center"><%= t('spree.num_orders') %></th>
<th class="align-center"><%= Spree.user_class.human_attribute_name(:lifetime_value) %></th>
<th class="align-center"><%= Spree.admin_user_class.human_attribute_name(:lifetime_value) %></th>
<th class="align-center"><%= sort_link @search, :created_at, t('spree.member_since') %></th>
<th data-hook="admin_users_index_header_actions" class="actions"></th>
</tr>
Expand Down
2 changes: 1 addition & 1 deletion backend/lib/spree/backend_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ def menu_items
label: :users,
icon: admin_updated_navbar ? "ri-user-line" : "user",
match_path: %r{/(users|store_credits)},
condition: -> { Spree.user_class && can?(:admin, Spree.user_class) },
condition: -> { Spree.admin_user_class && can?(:admin, Spree.admin_user_class) },
url: :admin_users_path
),
MenuItem.new(
Expand Down
53 changes: 53 additions & 0 deletions core/docs/admin_user_class.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Admin User Class Configuration

Historically Solidus used a single user model for both storefront customers and
admin users, with access to the backoffice decided purely by roles. That means a
customer record and an administrator record share one table, one set of
validations and one authentication configuration, so any weakness in customer
signup is a weakness in admin access.

`Spree.admin_user_class` lets an application separate the two:

- `Spree.user_class` is the storefront (customer) user model.
- `Spree.admin_user_class` is the admin/backoffice user model.

Separating them lets each side carry its own rules: stricter password
requirements, a restricted email domain, or a different authentication setup
entirely for admins, without imposing any of that on customers.

## Configuration

In `config/initializers/spree.rb`:

```ruby
# Storefront user model
Spree.user_class = 'YourApp::User'

# Admin/backoffice user model
Spree.admin_user_class = 'YourApp::AdminUser'
```

`Spree.admin_user_class` falls back to `Spree.user_class` when it is not set, so
existing applications keep working unchanged with a single user model. Note that
the install generator writes both settings explicitly, pointing them at the same
class.

Both settings must be assigned a String or Symbol, never a Class — they are
resolved lazily, because user models are often loaded before this initializer
runs.

## Where it's used

Admin and backoffice code resolves the user model through
`Spree.admin_user_class`, including the classic backend controllers, views and
navigation, and the Solidus Admin controllers and components. In specs, the
`:admin_user` factory builds a `Spree.admin_user_class`.

Storefront and checkout code continues to use `Spree.user_class`.

## What this does not do yet

Setting `Spree.admin_user_class` changes which model the admin interfaces read
and write. It does not by itself give that model authentication — there is no
generator for a separate admin Devise scope yet, so wiring up admin login for a
dedicated model is currently left to the application.
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ end
Spree.user_class = <%= options[:user_class].inspect %>
<% end -%>

# By default, admin users share the same class as storefront users.
# Override this if you use a separate model for admin users.
<% if options[:user_class].present? -%>
Spree.admin_user_class = <%= options[:user_class].inspect %>
<% end -%>

# Rules for avoiding to store the current path into session for redirects
# When at least one rule is matched, the request path will not be stored
# in session.
Expand Down
17 changes: 17 additions & 0 deletions core/lib/spree/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ module Spree
autoload :Deprecation, "spree/deprecation"

mattr_accessor :user_class, default: "Spree::LegacyUser"
mattr_accessor :admin_user_class, default: nil

def self.user_class
if @@user_class.is_a?(Class)
Expand All @@ -42,6 +43,22 @@ def self.user_class_name
@@user_class
end

# The class used for admin/backoffice users.
# Falls back to `Spree.user_class` for backwards compatibility.
def self.admin_user_class
return user_class if @@admin_user_class.nil?

if @@admin_user_class.is_a?(Class)
raise "Spree.admin_user_class MUST be a String or Symbol object, not a Class object."
elsif @@admin_user_class.is_a?(String) || @@admin_user_class.is_a?(Symbol)
@@admin_user_class.to_s.constantize
end
end

def self.admin_user_class_name
@@admin_user_class || @@user_class
end

# Load the same version defaults for all available Solidus components
#
# @see Spree::Preferences::Configuration#load_defaults
Expand Down
19 changes: 19 additions & 0 deletions core/lib/spree/testing_support/factories/admin_user_factory.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

FactoryBot.define do
# Builds an instance of `Spree.admin_user_class` carrying the admin role.
#
# `Spree.admin_user_class` falls back to `Spree.user_class`, so this behaves
# exactly like a `:user` with the admin role until a host application
# configures a dedicated admin user model.
factory :admin_user, class: Spree::AdminUserClassHandle.new do
email { generate(:email) }
password { "secret" }
password_confirmation { password }

after(:create) do |user, _|
admin_role = Spree::Role.find_by(name: "admin") || create(:role, name: "admin")
user.spree_roles << admin_role
end
end
end
Loading
Loading