Allow setting a different user class for backend users - #6329
Draft
elia wants to merge 4 commits into
Draft
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6329 +/- ##
==========================================
+ Coverage 91.31% 92.13% +0.82%
==========================================
Files 813 1036 +223
Lines 17088 21172 +4084
==========================================
+ Hits 15604 19507 +3903
- Misses 1484 1665 +181 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Member
|
This seems like a good idea to me. This separation could be useful for some marketplace stores we work on as well. |
Member
Author
|
@jarednorman wanted to do this for a long time, and never got the time until I was able to delegate 😅 |
elia
force-pushed
the
elia/admin-user
branch
2 times, most recently
from
August 2, 2026 14:36
3d18008 to
3482d51
Compare
…o user_class. Update initializer template, menu conditions, controllers, views, and helper.
`Spree::UserClassHandle` existed to defer resolving `Spree.user_class` until runtime, since classes can be loaded before the initializer sets it. `Spree.admin_user_class` needs exactly the same treatment, so pull the deferral into a reusable `Spree::ClassProxy`. The proxy takes the setting's name plus a block that reads the current value, and both handles become three-line subclasses. Naming the setting means the "has not been set yet" error points at whichever one is missing rather than always blaming `Spree.user_class`. `UserClassHandle#to_s` is unchanged in behaviour: it still reads `Spree.user_class_name` and still raises the same message. Only `AdminUserClassHandle` is new. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The `:admin_user` factory was nested inside `:user`, so it inherited `Spree::UserClassHandle` and always built a `Spree.user_class`. Now that admin users can be a separate model, it should follow `Spree.admin_user_class` instead. Move it to its own file, built on `Spree::AdminUserClassHandle`, and give it its own spec. It no longer inherits from `:user`, so the `email`, `password` and `password_confirmation` attributes are repeated here; the `:with_api_key` and `:with_orders` traits are not, since an admin user model is not assumed to have an API key or orders. This changes what the existing callers of `create(:admin_user)` build. It is a no-op today because `Spree.admin_user_class` falls back to `Spree.user_class`, but applications configuring a dedicated admin user model will get that model in specs from now on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Explain why the setting exists, not only what it does: sharing one model between customers and administrators means customer signup and admin access share validations and authentication, which is the problem this separation solves. Placed under core/docs/ following the per-gem convention of admin/docs/ and storefront/docs/, since the setting is defined in core. Records two constraints that are easy to trip over: both settings must be given a String or Symbol rather than a Class, because they are resolved lazily; and the install generator writes both explicitly, pointing them at the same class, so the fallback is not what a generated application relies on. Also states what is still missing, so nobody reads the setting as more than it is: it redirects which model the admin interfaces use, but provides no authentication for that model and no generator for a separate admin Devise scope. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Solidus has always used a single user model for both storefront customers and
backoffice operators, with access decided purely by roles. One table, one set of
validations, one authentication configuration — so any weakness in customer
signup is a weakness in admin access.
This PR adds
Spree.admin_user_classand routes every admin/backoffice lookupthrough it, so an application can separate the two. It defaults to
Spree.user_class, so nothing changes until you opt in.Separating the models buys three things:
when the two sets of records are siloed.
(domain-restricted Google signup for admins, magic links for customers,
stricter password policies). Separate models mean separate Devise scopes.
authentication systems don't overlap.
What's in here
Spree.admin_user_classis amattr_accessoralongsideSpree.user_class,with the same String/Symbol-only contract and a
admin_user_class_namecounterpart for lazy resolution. It returns
Spree.user_classwhen unset.Call sites updated to use it: the classic backend (users, orders, search, store
credits, API key controllers, the matching views and the store-credit-events
helper), the users menu item condition, and Solidus Admin (users, orders and
store-credits controllers plus the users/users-and-roles components). The
install generator's initializer template now writes both settings.
Spree::UserClassHandle— the lazy proxy that defers resolving a class nameuntil runtime, because user models often load before the initializer runs — is
generalized into
Spree::ClassProxy, withUserClassHandleand a newAdminUserClassHandleas thin subclasses.UserClassHandlebehaviour isunchanged.
The
:admin_userfactory moves out ofuser_factory.rb, where it was nestedinside
:userand therefore always built aSpree.user_class, into its ownfile built on
AdminUserClassHandle.New doc at
core/docs/admin_user_class.md.What this deliberately does not do
Setting
Spree.admin_user_classchanges which model the admin interfaces readand write. It does not give that model authentication. There is no generator
for a separate admin Devise scope yet, so an application pointing this at a
dedicated model has to wire up admin login itself.
That's the intended next step — a generator rather than a new extension — but
it's out of scope here to keep this reviewable.
Three unrelated test-environment fixes that used to live on this branch have been
extracted to #6532. This PR is now only the feature.
Behaviour changes worth flagging
create(:admin_user)now buildsSpree.admin_user_class. A no-op whilethat falls back to
Spree.user_class, but applications configuring a distinctadmin model will get that model in specs.
Spree.admin_user_classexplicitly, pointingit at the same class as
Spree.user_class. Generated apps therefore neverexercise the fallback.
Spree.admin_user_class, so theUsers menu is gated on admin access to the admin user model.
breadcrumb, the Users menu label and the email / roles / lifetime-value column
headers all call
model_name.humanorhuman_attribute_nameonSpree.admin_user_class. Configure a separateAdminUserand those stringscome from
activerecord.models.admin_userandactiverecord.attributes.admin_user.*instead of thespree/userkeys — so adedicated admin model needs its own translations to avoid humanized fallbacks.
Worth a reviewer's opinion on whether these labels should keep pointing at
Spree.user_class.Spree::LegacyUser; it nowfollows the configured class, which is a fix in its own right for apps with a
custom user model.
How to test
Default behaviour is unchanged — the existing suite covers that, since
Spree.admin_user_classresolves toSpree.user_classeverywhere.To exercise the split, point it at a separate model in
config/initializers/spree.rb:The admin user index, order customer search, store credits and the Users menu
should all operate on
AdminUser, while storefront and checkout continue onUser. Note you'll need to provide admin authentication yourself — see above.Factory specs:
Checklist
Check out our PR guidelines for more details.
The following are mandatory for all PRs:
Spree.admin_user_class.The following are not always needed: