Skip to content

Multi-user mode & UI reworks#258

Open
oskardotglobal wants to merge 20 commits into
usetrmnl:mainfrom
spark-hpi:ui
Open

Multi-user mode & UI reworks#258
oskardotglobal wants to merge 20 commits into
usetrmnl:mainfrom
spark-hpi:ui

Conversation

@oskardotglobal

Copy link
Copy Markdown
Contributor

This PR reworks multi-user mode, adding the concept of admins and shared plugins. It also redesignes the polling section of the recipe editor and hides the "updates" setting when running inside Docker.

The unprivileged user can now use auto-join, devices joined this way are "unassigned" and can be edited by everyone. Admins can view other people's devices.

image

Redesigned recipe editor for polling. The transform card shown in that screenshot is part of a serverless transform feature, which I will PR later. The settings card contains the polling-specific fields that used to be below (except bleed margin + dark mode)

Bildschirmfoto 2026-06-26 um 12 03 26

Registration stays open, but users can't do anything until they are confirmed by an admin. Admins can manage users and approve/revoke them.

image

Plugins can be shared between users, then they can be copied to the user's account manually ("forked").

image

I agree to license this contribution as MIT. Supersedes #257

oskardotglobal and others added 19 commits June 26, 2026 11:33
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added 'is_shared' boolean cast to Plugin model's $casts array and implemented
scopeVisibleTo(Builder $query, User $user) scope method. The scope allows users
to see their own plugins and all shared plugins, while admins see all plugins.
Also updated PluginFactory to include is_shared=false default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Registers the `confirmed` middleware alias that logs out unconfirmed
authenticated users, invalidates their session, and redirects to login
with a status flash. Applied to all auth-guarded route groups in
web.php and settings.php.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rmed

Ensures newly registered users via Fortify registration form are created with
confirmed_at = null. Also verifies unconfirmed users are blocked from accessing
protected routes by EnsureUserIsConfirmed middleware.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…able auto-join

- Auto-joined devices now have user_id=null instead of being assigned to the auto-join user
- Device names are now generic 'Auto-Joined TRMNL' instead of user-specific
- Auto-join toggle (Permit Auto-Join) is now visible to all confirmed users, not just id=1
- Updated tests to reflect new behavior: auto-joined devices remain unowned

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates DevicePolicy (view/update/delete/reassign) with admin bypass and
unowned-device semantics, wires it into DisplayStatusController via the
AuthorizesRequests trait, and fixes the id=1 auto-admin edge case in
affected tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces PluginPolicy with view/update/delete/share/reassign/copy
abilities, wires it into PluginSettingsController::destroy() and
PluginArchiveController::export(), and fixes id=1 auto-admin hazard
in UserModelTest and PluginArchiveTest.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The DeviceAutoJoin component no longer has the $isFirstUser property.
The toggle is now visible to ALL confirmed users, not just id=1.

Updated tests:
- Removed all assertions checking $isFirstUser
- Changed 'identifies first user correctly' test to verify the toggle
  is visible to all users (both id=1 and other users)
- Updated property-setting test to use valid component properties only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds /settings/admin/users Volt component with Livewire actions for
confirming, revoking, promoting, and deleting users; admin nav link in
settings sidebar; route registration; and 11 feature tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d reassignment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nment

Add is_shared toggle, Shared Plugins tab, admin Show All toggle, Install Copy action, and plugin ownership reassignment to the plugins index Livewire component; enforce authorization via PluginPolicy on all new actions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Device reassignment moved from devices/manage table row to the edit
  modal in devices/configure (admin section with Owner select)
- Plugin reassignment moved from plugins/index card footer to a header
  row in plugins/recipe (visible to admins below the plugin title)
- Admin can now access configure/recipe pages for any device/plugin;
  updateDevice and deleteDevice also allow admins
- Admin show-all toggles on manage and index now use explicit <span>
  labels instead of relying solely on the flux:switch label prop
- Tests updated to call reassign methods on their new host components

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds app.docker config key (auto-detected via /.dockerenv, overridable
with APP_DOCKER env var). Updates page link and profile dropdown entry
are hidden when docker=true, since self-update is not applicable in
container deployments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace segmented radio group with a compact dropdown matching the
  Owner selector style (label + flux:select)
- Move Getting started buttons below the Save button; make them size="sm"
- Add "These will replace the current template." note next to the buttons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mount() now reads User::where('assign_new_devices', true)->exists() so
any user sees the toggle as on when any other user has enabled it.
Turning off clears assign_new_devices on all users, not just the current
one, so any user can shut off auto-join regardless of who enabled it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DevicePolicy::update() was requiring user_id === auth()->id(), which
returns false for unowned devices (user_id = null). The manage page
already shows unowned devices to all users; the configure page should
follow the same rule.

- DevicePolicy::update(): allow null user_id, matching view()
- configure.blade.php: replace all raw abort_unless(devices->contains)
  checks with $this->authorize() so the policy is the single source of
  truth for all device actions (delete, update, playlist ops, firmware)
- DevicePolicyTest: flip 'cannot update unowned' to 'can update unowned'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Plugin cards on the shared tab were wrapping the title in an <a> that
led to the recipe edit page, resulting in a 403 for non-owners. Now the
card header renders as a plain <div> when the plugin belongs to another
user, leaving only the Install Copy button as the available action.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bnussbau

Copy link
Copy Markdown
Collaborator

Hey there,

are you and @vadimitri colleagues, or are you working on this independently?

A few things to consider:

  • There are too many features bundled into a single PR, which makes it difficult to review. I’d appreciate it if you could split this into multiple, smaller PRs.
  • According to the survey (see Community Survey: How Are You Using LaraPaper? #47), the typical LaraPaper user is running a single-user homelab instance. While multiple users are indirectly supported through the REGISTRATION_ENABLED environment variable, the recommended setup is to keep registration disabled.
  • There are also security implications. With the current architecture, user isolation is not guaranteed. Since Blade templates can execute PHP, a non-admin user could potentially access or manipulate another user’s data.
    Because of this, multi-user support, with roles and shared recipes, should be gated behind a feature flag (e.g. MULTIUSER_ENABLED env var). The security model needs to be hardened before this feature can be considered GA. Otherwise, it could create the false impression that users are properly isolated, giving a false sense of security.
  • Regarding the transform feature: if you plan to implement it, I’d recommend using the transform-v2 branch as a starting point. Also keep the security and isolation of transform code in mind.

@oskardotglobal

Copy link
Copy Markdown
Contributor Author

Hi,

@vadimitri and I are both working on this on behalf of Spark for the Spark x TRMNL hackathon next week; the two PRs just happened because of an internal communication issue. Regarding your points:

  • I will split this into multiple PRs, no problem
  • We can gate the Multiuser feature behind a flag without a problem, maybe even behind an "experimental" flag in the Lab settings. The security model I'm currently aiming for trusts the users a lot anyway (they have to be confirmed by an admin because they are expected to be known/trusted by an admin). Regarding the blade issue, maybe there could be a feature flag to toggle blade for non-admin users?
  • We already have Python, PHP and JS transforms running in prod, using an external Go microservice (see https://github.com/spark-hpi/larapaper-serverless-runner). I'm currently working on having processes in that container namespaces using bubblewrap for extra security.

Regarding your comment in the other PR: for the hackathon, we'll be using our larapaper fork, but I think there is no reason why we couldn't contribute back the features we build. The hackathon is on July 1st, you will probably be able to find pictures on the official trmnl social media channels :)

@bnussbau

bnussbau commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Hey there,

appreciate the contributions, looking forward to your hackathon.

We can gate the Multiuser feature behind a flag without a problem, maybe even behind an "experimental" flag in the Lab settings.

The underlying “toggle” package supports feature flags via environment variables or database-backed UI toggles. Given the security implications, I’d prefer using an environment variable here.

Regarding the blade issue, maybe there could be a feature flag to toggle blade for non-admin users?

Ideally, it would be possible to isolate Blade execution, so that PHP code cannot be executed. If that’s not feasible, then a feature flag would be the fallback.

We already have Python, PHP and JS transforms running in prod, using an external Go microservice (see https://github.com/spark-hpi/larapaper-serverless-runner). I'm currently working on having processes in that container namespaces using bubblewrap for extra security.

That’s interesting, will check out. In this case, it would be great to implement it with an abstraction layer so that both an internal execution engine and an external serverless engine (like yours) can be supported interchangeably.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants