Skip to content

Conversation

@Boy132
Copy link
Member

@Boy132 Boy132 commented Oct 23, 2025

Closes #1728

grafik grafik

@Boy132 Boy132 self-assigned this Oct 23, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 23, 2025

📝 Walkthrough

Walkthrough

Introduces a boolean User.is_managed_externally flag (model, migration, factory, transformer), surfaces it in admin/profile UIs, groups customization fields into a Collapsible Section, conditions client-side profile field visibility and API authorization on the flag, and updates tests and translations.

Changes

Cohort / File(s) Summary
Database Migration
database/migrations/2025_10_23_073209_add_is_managed_externally_to_users.php
Adds boolean is_managed_externally column to users (default false) and down() to drop it.
User Model
app/Models/User.php
Adds is_managed_externally to PHPDoc, $fillable, default $attributes, $validationRules, and $casts as boolean.
Factory & Transformer
database/factories/UserFactory.php, app/Transformers/Api/Application/UserTransformer.php
Factory sets is_managed_externally => false; transformer includes is_managed_externally in API output.
Filament Admin Resource(s)
app/Filament/Admin/Resources/Users/UserResource.php, app/Filament/Pages/Auth/EditProfile.php
Adds is_managed_externally Toggle at account level; groups timezone, language, and avatar into a collapsible full-width two-column "Customization" Section; EditProfile conditionally disables/hides username, email, and password when externally managed.
API Request Validation & Authorization
app/Http/Requests/Api/Application/Users/StoreUserRequest.php, app/Http/Requests/Api/Client/Account/UpdateEmailRequest.php, app/Http/Requests/Api/Client/Account/UpdatePasswordRequest.php, app/Http/Requests/Api/Client/Account/UpdateUsernameRequest.php
StoreUserRequest accepts is_managed_externally; client update requests still run password checks but authorize() now denies final authorization when is_managed_externally is true.
Localization
lang/en/admin/user.php
Adds is_managed_externally label and is_managed_externally_helper explanatory text.
Tests
tests/Integration/Api/Application/Users/ExternalUserControllerTest.php, tests/Integration/Api/Application/Users/UserControllerTest.php
Updated JSON structure assertions and expected responses to include is_managed_externally.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Client / Filament UI
    participant API as Backend API
    participant DB as Database

    UI->>API: Submit profile/email/password update
    API->>DB: Load user (includes is_managed_externally)
    DB-->>API: user { is_managed_externally }

    alt is_managed_externally == true
        note right of API: Password validation still runs\nbut authorize() returns false
        API->>API: Validate password (may throw)
        API-->>UI: 403 Forbidden / Authorization denied
    else is_managed_externally == false
        API->>API: Validate password & authorize
        API->>DB: Persist changes
        DB-->>API: Success
        API-->>UI: 200 OK (updated)
    end
Loading

Possibly related PRs

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add toggle for externally managed users' directly summarizes the main change—introduction of a toggle/flag for managing external user profiles.
Description check ✅ Passed The description references issue #1728 and includes visual screenshots demonstrating the UI changes related to externally managed users, connecting to the changeset.
Linked Issues check ✅ Passed The PR implements the core requirement from #1728: adds a setting/toggle for administrators to prevent users from editing their own profiles when marked as externally managed, with appropriate UI and API restrictions.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the is_managed_externally feature—database migration, model updates, form restrictions, API validation, and tests are all in scope.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 97c3a70 and 6ac3525.

📒 Files selected for processing (3)
  • app/Filament/Admin/Resources/Users/UserResource.php (2 hunks)
  • app/Filament/Pages/Auth/EditProfile.php (1 hunks)
  • app/Models/User.php (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/Filament/Pages/Auth/EditProfile.php
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-10-07T14:44:18.583Z
Learnt from: notAreYouScared
Repo: pelican-dev/panel PR: 1779
File: app/Filament/Admin/Resources/Users/Pages/EditUser.php:51-51
Timestamp: 2025-10-07T14:44:18.583Z
Learning: In the Pelican Panel codebase, when using Filament's FileUpload component for the avatar field in UserResource, the 'avatar' key is intentionally unset from the data array in EditUser::handleRecordUpdate before passing to UserUpdateService. This is by design because the avatar is not stored in the database directly—Filament's FileUpload component handles file storage, retrieval, and deletion through its own lifecycle hooks (formatStateUsing, deleteUploadedFileUsing, etc.) independently of the database update service.

Applied to files:

  • app/Filament/Admin/Resources/Users/UserResource.php
📚 Learning: 2025-10-15T11:55:53.461Z
Learnt from: rmartinoscar
Repo: pelican-dev/panel PR: 1801
File: app/Extensions/OAuth/Schemas/AuthentikSchema.php:7-10
Timestamp: 2025-10-15T11:55:53.461Z
Learning: In Filament v4, Wizard Step components use the Filament\Schemas namespace (Filament\Schemas\Components\Wizard\Step), not Filament\Forms.

Applied to files:

  • app/Filament/Admin/Resources/Users/UserResource.php
📚 Learning: 2025-10-15T11:55:53.461Z
Learnt from: rmartinoscar
Repo: pelican-dev/panel PR: 1801
File: app/Extensions/OAuth/Schemas/AuthentikSchema.php:7-10
Timestamp: 2025-10-15T11:55:53.461Z
Learning: In Filament v4, the Forms component Placeholder was deprecated and removed. Use TextEntry from Filament\Infolists\Components\TextEntry in forms instead, binding values with ->state(). For HTML content, use ->html().

Applied to files:

  • app/Filament/Admin/Resources/Users/UserResource.php
🧬 Code graph analysis (1)
app/Filament/Admin/Resources/Users/UserResource.php (2)
app/Services/Helpers/LanguageService.php (2)
  • LanguageService (8-30)
  • getAvailableLanguages (22-29)
app/Models/User.php (1)
  • User (96-498)
🔇 Additional comments (3)
app/Models/User.php (1)

52-52: LGTM! Consistent implementation of is_managed_externally field.

The new boolean field is properly declared across all required model components:

  • PHPDoc property annotation (line 52)
  • Mass-assignable in $fillable (line 122)
  • Default value in $attributes (line 145)
  • Validation rule (line 160)
  • Type cast (line 182)

The boolean cast ensures the attribute returns true/false instead of 1/0, which is the correct approach.

Also applies to: 122-122, 145-145, 160-160, 182-182

app/Filament/Admin/Resources/Users/UserResource.php (2)

36-36: LGTM! Toggle field properly configured.

The is_managed_externally Toggle field is well-configured with:

  • Appropriate label and helper text via translations
  • Hint icon for user guidance
  • Stacked layout (inline(false))
  • Consistent column spanning with adjacent fields

The default value is handled by the model's $attributes array, so no explicit default() call is needed here.

Also applies to: 228-236


237-283: LGTM! Well-organized customization section.

The new collapsible Section groups related profile customization fields (timezone, language, avatar) together, improving the form's organization and clarity. This structure makes sense given the addition of the is_managed_externally toggle—it visually separates core account settings from user preferences.

Key observations:

  • Timezone and language Selects are properly configured with required validation, defaults, search capability, and appropriate prefixIcons
  • Avatar FileUpload logic correctly handles file existence checks, state formatting, and deletion (consistent with how Filament manages file uploads separately from database updates)
  • The 2-column layout within a full-width Section provides good visual balance

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
app/Http/Requests/Api/Client/Account/UpdatePasswordRequest.php (1)

28-28: Authorization logic is correct, but consider a custom error message.

The logic correctly blocks password updates for externally managed users. However, users will receive a generic 403 Forbidden response without context about why the update was blocked.

Consider throwing a custom exception with a more descriptive message:

-        return !$this->user()->is_managed_externally;
+        if ($this->user()->is_managed_externally) {
+            throw new HttpException(403, 'Password cannot be changed for externally managed users.');
+        }
+        
+        return true;

This would improve the user experience by providing clear feedback about why their request was denied.

app/Http/Requests/Api/Client/Account/UpdateEmailRequest.php (1)

29-29: Authorization logic is correct, but consider a custom error message.

The logic correctly blocks email updates for externally managed users. However, like in UpdatePasswordRequest.php, users will receive a generic 403 Forbidden response without context about why the update was blocked.

Consider throwing a custom exception with a more descriptive message:

-        return !$this->user()->is_managed_externally;
+        if ($this->user()->is_managed_externally) {
+            throw new HttpException(403, 'Email cannot be changed for externally managed users.');
+        }
+        
+        return true;

This would improve the user experience by providing clear feedback about why their request was denied.

app/Filament/Admin/Resources/Users/UserResource.php (1)

237-285: Consider UX: Collapsible section hides important user attributes.

The timezone, language, and avatar fields are now in a collapsible "Customization" section. Since these are frequently edited user attributes, hiding them by default may reduce discoverability. Consider whether these fields should be immediately visible or if the section should default to expanded.

If you prefer the section to be expanded by default:

 Section::make(trans('profile.tabs.customization'))
-    ->collapsible()
+    ->collapsible()
+    ->collapsed(false)
     ->columnSpanFull()
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e006ac and a9165c6.

📒 Files selected for processing (10)
  • app/Filament/Admin/Resources/Users/UserResource.php (2 hunks)
  • app/Filament/Pages/Auth/EditProfile.php (1 hunks)
  • app/Http/Requests/Api/Application/Users/StoreUserRequest.php (2 hunks)
  • app/Http/Requests/Api/Client/Account/UpdateEmailRequest.php (1 hunks)
  • app/Http/Requests/Api/Client/Account/UpdatePasswordRequest.php (1 hunks)
  • app/Models/User.php (4 hunks)
  • app/Transformers/Api/Application/UserTransformer.php (1 hunks)
  • database/Factories/UserFactory.php (1 hunks)
  • database/migrations/2025_10_23_073209_add_is_managed_externally_to_users.php (1 hunks)
  • lang/en/admin/user.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/Filament/Pages/Auth/EditProfile.php (1)
app/Models/User.php (2)
  • User (95-496)
  • email (229-234)
app/Filament/Admin/Resources/Users/UserResource.php (3)
app/Services/Helpers/LanguageService.php (2)
  • LanguageService (8-30)
  • getAvailableLanguages (22-29)
app/Models/User.php (1)
  • User (95-496)
app/Extensions/OAuth/OAuthService.php (1)
  • OAuthService (10-71)
🔇 Additional comments (14)
database/Factories/UserFactory.php (1)

29-29: LGTM! Factory default correctly set.

The is_managed_externally field is correctly defaulted to false in the factory, which aligns with the model's default value and the expected behavior (users are not externally managed by default).

app/Transformers/Api/Application/UserTransformer.php (1)

34-34: LGTM! API output correctly includes the new field.

The is_managed_externally field is properly exposed in the API transformer, allowing API consumers to determine whether a user is externally managed.

app/Filament/Pages/Auth/EditProfile.php (1)

96-130: LGTM! Thoughtful UX implementation for externally managed users.

The implementation appropriately handles form fields for externally managed users:

  • Username and email fields are disabled (visible but not editable) so users can still see their current values
  • Password field is hidden since it's a "new password" input that wouldn't make sense to show
  • Password confirmation field is already conditionally visible based on the password field, so it naturally becomes hidden as well

This provides clear feedback to users while maintaining a clean interface.

app/Http/Requests/Api/Application/Users/StoreUserRequest.php (2)

23-31: LGTM! Field correctly added to API rules.

The is_managed_externally field is properly included in the allowed rules for user creation via the API.


39-45: LGTM! Attribute mapping provides clear context.

The human-readable attribute name "Is managed by Third Party?" is clear and appropriate for API validation messages. While it differs from the UI label "Is managed externally?", this variation is acceptable as they target different audiences.

database/migrations/2025_10_23_073209_add_is_managed_externally_to_users.php (2)

12-17: LGTM! Migration correctly adds the column.

The migration properly adds the is_managed_externally boolean column with a sensible default of false and places it logically after the related external_id field.


22-27: LGTM! Proper rollback included.

The down() method correctly drops the column, allowing for clean rollback if needed.

app/Models/User.php (4)

51-51: LGTM! PHPDoc annotation added correctly.

The @property annotation for is_managed_externally is properly documented as a boolean type.


144-144: LGTM! Safe default value.

Setting is_managed_externally to false by default is appropriate—users are not externally managed unless explicitly configured.


159-159: LGTM! Validation rule is appropriate.

The boolean validation rule correctly validates the is_managed_externally field type.


121-121: Now I need to search the codebase directly for the StoreUserRequest implementation to check if it has an authorize() method:

Based on my verification of the shell script output, I can now provide a rewritten review comment. The search results show:

  1. Client endpoints (UpdatePasswordRequest, UpdateEmailRequest) have proper authorization checks preventing externally-managed users from modifying settings
  2. StoreUserRequest includes is_managed_externally in its validated rules (line 25 of the original script output)
  3. Application API requires administrative API keys per Pterodactyl documentation

However, from the shell script output, the authorize() method implementations in the request classes couldn't be fully verified through ast-grep (likely due to pattern matching), but the authorization logic is visible in the rg results showing client-side request classes have return !$this->user()->is_managed_externally; in their authorize methods.

The key finding is that:

  • Client-facing endpoints (UpdatePasswordRequest, UpdateEmailRequest) have explicit authorization preventing externally-managed users from modifying their data
  • The field is properly included in the model's $fillable array
  • The Application API (which handles user creation via StoreUserRequest) is administratively controlled
  • The admin UI properly disables/hides fields based on the flag

Authorization for is_managed_externally is properly enforced.

Client API endpoints (UpdatePasswordRequest, UpdateEmailRequest) include explicit authorization checks returning !$this->user()->is_managed_externally, preventing externally-managed users from modifying their email, username, or password. The Application API endpoint (StoreUserRequest) is restricted to administrative API keys, ensuring only admins can create or modify users with this flag. Admin UI properly disables affected fields when the flag is enabled.

app/Filament/Admin/Resources/Users/UserResource.php (3)

36-36: LGTM! Necessary import added.

The Toggle component import is required for the new is_managed_externally field.


294-294: Minor: Empty line added.

This appears to be a formatting change. No functional impact.


228-236: Translation keys verified and defined.

Both admin/user.is_managed_externally and admin/user.is_managed_externally_helper are properly defined in lang/en/admin/user.php (lines 13-14). The code changes are correct.

# Conflicts:
#	app/Filament/Admin/Resources/Users/UserResource.php
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
app/Filament/Pages/Auth/EditProfile.php (1)

96-96: Consider readOnly() for better UX and add helper text.

As noted in a previous review, readOnly() allows users to select and copy text, which provides better UX than disabled(). If you use readOnly(), add ->dehydrated(false) to prevent the value from being submitted.

Additionally, consider adding ->helperText() to explain to users why these fields cannot be edited (e.g., "This field is managed externally and cannot be edited here").

Apply this diff to improve UX:

                             TextInput::make('username')
-                                    ->disabled(fn (User $user) => $user->is_managed_externally)
+                                    ->readOnly(fn (User $user) => $user->is_managed_externally)
+                                    ->dehydrated(false)
+                                    ->helperText(fn (User $user) => $user->is_managed_externally ? trans('profile.managed_externally_help') : null)
                                     ->prefixIcon('tabler-user')
                                     ->label(trans('profile.username'))
                                     ->required()
                                     ->maxLength(255)
                                     ->unique(),
                             TextInput::make('email')
-                                    ->disabled(fn (User $user) => $user->is_managed_externally)
+                                    ->readOnly(fn (User $user) => $user->is_managed_externally)
+                                    ->dehydrated(false)
+                                    ->helperText(fn (User $user) => $user->is_managed_externally ? trans('profile.managed_externally_help') : null)
                                     ->prefixIcon('tabler-mail')

Note: You'll need to add the translation key profile.managed_externally_help to your language files.

Also applies to: 103-103

tests/Integration/Api/Application/Users/ExternalUserControllerTest.php (1)

26-26: LGTM! Consider adding tests for externally managed user behavior.

The assertions correctly include the new is_managed_externally field in the expected API response structure and values.

However, consider adding test cases that verify externally managed users cannot update their username, email, or password through the API. This would ensure the authorization logic is working correctly.

Would you like me to generate test cases that verify the blocking behavior for externally managed users?

Also applies to: 36-36

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1644dbc and 9233db6.

📒 Files selected for processing (4)
  • app/Filament/Admin/Resources/Users/UserResource.php (2 hunks)
  • app/Filament/Pages/Auth/EditProfile.php (1 hunks)
  • tests/Integration/Api/Application/Users/ExternalUserControllerTest.php (2 hunks)
  • tests/Integration/Api/Application/Users/UserControllerTest.php (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/Filament/Admin/Resources/Users/UserResource.php
🧰 Additional context used
🧬 Code graph analysis (2)
app/Filament/Pages/Auth/EditProfile.php (1)
app/Models/User.php (2)
  • User (95-497)
  • email (230-235)
tests/Integration/Api/Application/Users/UserControllerTest.php (1)
tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php (1)
  • getApiUser (49-52)
🔇 Additional comments (2)
tests/Integration/Api/Application/Users/UserControllerTest.php (1)

29-30: LGTM! Consistent test assertions for the new field.

All test assertions have been correctly updated to include is_managed_externally in the expected API response structures and values. The changes are consistent across all endpoints (list, single, relationships, create, and update).

Similar to ExternalUserControllerTest.php, consider adding dedicated test cases that verify externally managed users are blocked from updating their profiles through the Application API.

Also applies to: 54-54, 71-71, 97-97, 105-105, 132-132, 219-219, 250-250

app/Filament/Pages/Auth/EditProfile.php (1)

111-111: Backend authorization is properly enforced. Verification confirms both UpdatePasswordRequest and UpdateEmailRequest return !$this->user()->is_managed_externally in their authorize() methods, blocking profile updates for externally managed users at the API layer. Frontend hiding combined with backend authorization provides complete protection.

Copy link
Member

@lancepioch lancepioch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, but can we switch is_managed_externally from a bool to a nullable timestamp (similar to deleted_at).

Copy link
Member

@rmartinoscar rmartinoscar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this requires proper test so we don't overlook anything (like the UpdateUsernameRequest) for example.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9233db6 and 97c3a70.

📒 Files selected for processing (3)
  • app/Filament/Pages/Auth/EditProfile.php (1 hunks)
  • app/Http/Requests/Api/Client/Account/UpdateUsernameRequest.php (1 hunks)
  • app/Models/User.php (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/Models/User.php
🔇 Additional comments (1)
app/Http/Requests/Api/Client/Account/UpdateUsernameRequest.php (1)

29-29: Logic is correct; verify error messaging for user experience.

The authorization logic correctly prevents externally managed users from updating their username. When authorize() returns false, Laravel responds with a 403 Forbidden status. The codebase appears to use standard Laravel error handling without custom context-specific messages for authorization failures.

Please verify that the API returns a meaningful error message to clients when an externally managed user attempts to update their username, so users understand why the operation is forbidden. Check:

  1. Whether the API has a response wrapper or middleware that adds context to authorization failure messages
  2. Whether frontend or API documentation explains the 403 response for this endpoint
  3. Whether the actual error response to clients includes information about the external management restriction

@rmartinoscar
Copy link
Member

rmartinoscar commented Dec 15, 2025

I agree with Lance if we are ever gonna add timestamp, might aswell do it now.

@notAreYouScared
Copy link
Member

I agree with Lance if we are ever gonna add timestamp, might aswell do it now.

I still see no reason to have a timestamp... It's either on or off.

@notAreYouScared notAreYouScared merged commit 9d1e7f5 into main Dec 17, 2025
25 checks passed
@notAreYouScared notAreYouScared deleted the boy132/read-only-profiles branch December 17, 2025 19:09
@github-actions github-actions bot locked and limited conversation to collaborators Dec 17, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Readonly user profile

5 participants