Skip to content

Conversation

@notAreYouScared
Copy link
Member

Closes #1741

Adds:
Send Test/Reset Password Email actions
Allows admins to set user language/timezone
Allows admins to remove a user's avatar(if set) if they deem it inappropriate
Allows admins to unlink users' sso
Allows admins to ssh/api keys assigned to that user.
Allows admins to see the user activity log.
Before:
image

After:
image
image
image
image

@notAreYouScared notAreYouScared changed the title Charles/issue1741 General Edit User Improvements Oct 7, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 7, 2025

📝 Walkthrough

Walkthrough

Centralizes OAuth linking/unlinking into OAuthService (new linkUser/unlinkUser) and updates controller/UI to use it. Converts User admin form to tabbed layout with external_id, timezone/language, avatar, OAuth unlink, API/SSH keys, and password reset. Adjusts avatar handling and gates ActivityLog IP display by permission.

Changes

Cohort / File(s) Summary
OAuth service & controller
app/Extensions/OAuth/OAuthService.php, app/Http/Controllers/Auth/OAuthController.php
Added linkUser and unlinkUser to OAuthService; removed controller-local linking helper and replaced local linking calls with OAuthService usage in OAuthController.
Admin User UI (tabs & fields)
app/Filament/Admin/Resources/Users/UserResource.php,
app/Filament/Admin/Resources/Users/Pages/EditUser.php
Migrated User form to tabbed layout (account, roles, keys, activity); added external_id, timezone, language, avatar FileUpload, OAuth unlink actions, API/SSH key Repeaters, activity log display; EditUser::handleRecordUpdate now unsets roles and avatar before update.
Profile page avatar & lists
app/Filament/Pages/Auth/EditProfile.php
Replaced hintAction with formatStateUsing for avatar; added deleteUploadedFileUsing handling TemporaryUploadedFile vs stored file; timezone default set to config; added visibility/no-items entries for API and SSH keys.
Activity log IP gating
app/Models/ActivityLog.php
Added public function getIp(): ?string returning IP only when permitted; updated HTML rendering to use getIp() instead of direct $this->ip.
Translations
lang/en/admin/user.php, lang/en/profile.php
Added admin keys: external_id, password_reset, password_reset_sent, password_reset_failed; added profile tabs.keys and no_oauth, no_api_keys, no_ssh_keys.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Provider as OAuth Provider
  participant App as Application
  participant C as OAuthController
  participant S as OAuthService
  participant DB as Database

  User->>App: Start OAuth flow (login or link)
  App->>Provider: Redirect to provider auth
  Provider-->>App: Callback with code
  App->>C: handleCallback(request)
  C->>Provider: fetch OAuth user (Socialite)
  C->>S: linkUser(authenticated User, Schema, OAuthUser)
  S->>DB: persist user's oauth map update
  DB-->>S: persisted
  S-->>C: return refreshed User
  C-->>App: continue flow (redirect/response)
Loading
sequenceDiagram
  autonumber
  actor Admin
  participant UI as Filament UI (EditUser/EditProfile)
  participant S as OAuthService
  participant DB as Database
  participant FS as File Storage

  rect rgba(230,245,255,0.6)
    Admin->>UI: Click "Unlink" for provider
    UI->>S: unlinkUser(current User, Schema)
    S->>DB: remove schema entry from user's oauth map
    DB-->>S: persisted
    S-->>UI: return refreshed User
    UI-->>Admin: show notification
  end

  rect rgba(245,245,245,0.6)
    Admin->>UI: Remove avatar
    alt Temporary upload
      UI->>FS: Delete temp file directly
    else Stored avatar path
      UI->>FS: Delete disk file
    end
    UI->>DB: clear avatar path on user
    DB-->>UI: saved
  end
Loading

Possibly related PRs

Pre-merge checks

❌ Failed checks (3 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Linked Issues Check ⚠️ Warning The pull request implements the external ID field, reset password email actions, language and timezone controls, and OAuth linking UI as specified in issue #1741 but does not include any MFA setup or disable MFA controls that were part of the original requirements. Please add the MFA setup and disable controls outlined in issue #1741 or update the issue if those items are out of scope for this work.
Out of Scope Changes Check ⚠️ Warning In addition to the features requested by issue #1741, the pull request adds API/SSH key management and an activity log tab which were not described in the linked issue’s objectives. Consider moving the API/SSH key and activity log functionality into a separate pull request or updating the issue description to include these additional features.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The title “General Edit User Improvements” is related to the change set but is overly broad and does not clearly communicate the specific enhancements such as external ID fields, OAuth unlinking, API/SSH key and activity log management introduced in this pull request. Consider updating the title to more specifically reflect the primary changes, for example “Add external ID, OAuth unlink, API/SSH key and activity log tabs to Edit User page.”
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed The description clearly lists the new actions and UI enhancements and matches the implemented changes by outlining password email actions, language and timezone fields, avatar removal, SSO unlinking, key management and activity log viewing along with before and after screenshots.

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.

@notAreYouScared notAreYouScared requested a review from Boy132 October 7, 2025 14:23
@notAreYouScared notAreYouScared self-assigned this Oct 7, 2025
@notAreYouScared notAreYouScared added this to the beta26 milestone Oct 7, 2025
@notAreYouScared
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Oct 7, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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: 7

🧹 Nitpick comments (3)
app/Extensions/OAuth/OAuthService.php (1)

56-56: Consider if refresh() is necessary.

Both linkUser (line 56) and unlinkUser (line 69) call $user->refresh() after updating. Since update() modifies the database but doesn't automatically reload all attributes, the refresh() ensures the returned user instance reflects the updated state. However, if the only change is to the oauth attribute and it was explicitly set via update(['oauth' => $oauth]), the refresh may not be strictly necessary unless other model observers or events modify additional attributes.

If you're certain no other attributes are modified during the update (e.g., via model events or observers), you could simplify by removing the refresh() call:

 public function linkUser(User $user, OAuthSchemaInterface $schema, OAuthUser $oauthUser): User
 {
     $oauth = $user->oauth ?? [];
     $oauth[$schema->getId()] = $oauthUser->getId();

     $user->update(['oauth' => $oauth]);

-    return $user->refresh();
+    return $user;
 }

However, keeping refresh() is safer and ensures consistency if there are any hidden side effects from observers or database triggers.

Also applies to: 69-69

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

169-172: Unique validators should ignore the current record

Avoid false “already taken” on edit by making the rule ignorable for the current model.

Based on learnings

-    ->unique()
+    ->unique(ignorable: fn (?User $record) => $record)

...
-    ->unique()
+    ->unique(ignorable: fn (?User $record) => $record)

Please verify if your Filament v4 version already defaults to ignoring the current record; if not, apply this.

Also applies to: 179-183


265-267: Optional: cache timezone options

Building the full timezone list on every render is unnecessary. Cache the options statically/once per request.

-->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
+->options(function () {
+    static $opts;
+    return $opts ??= collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]);
+})
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 69b669e and 40aff61.

📒 Files selected for processing (9)
  • app/Extensions/OAuth/OAuthService.php (2 hunks)
  • app/Filament/Admin/Resources/Users/Pages/EditUser.php (1 hunks)
  • app/Filament/Admin/Resources/Users/UserResource.php (2 hunks)
  • app/Filament/Pages/Auth/EditProfile.php (3 hunks)
  • app/Filament/Server/Resources/Activities/ActivityResource.php (1 hunks)
  • app/Http/Controllers/Auth/OAuthController.php (2 hunks)
  • app/Models/ActivityLog.php (3 hunks)
  • lang/en/admin/user.php (1 hunks)
  • lang/en/profile.php (1 hunks)
🧰 Additional context used
🪛 PHPMD (2.15.0)
app/Filament/Admin/Resources/Users/UserResource.php

311-311: Avoid unused local variables such as '$_'. (undefined)

(UnusedLocalVariable)

🔇 Additional comments (12)
app/Filament/Server/Resources/Activities/ActivityResource.php (1)

82-82: LGTM! Centralized IP permission check.

The refactor to always call $activityLog->getIp() is correct. The permission check is now encapsulated within the ActivityLog::getIp() method (lines 166-169 in app/Models/ActivityLog.php), which returns null when the user lacks the 'seeIps activityLog' permission. This improves code organization by centralizing the authorization logic.

app/Models/ActivityLog.php (2)

166-169: LGTM! Permission-based IP accessor.

The new getIp() method correctly encapsulates the permission check for viewing activity log IPs. Returning null when the user lacks permission is the appropriate behavior, as it prevents IP exposure while allowing the calling code to handle the absence of data gracefully.


183-184: LGTM! Safe IP formatting with fallback.

The IP formatting logic correctly handles the null case from getIp() by converting it to an empty string. The ternary operator ensures that when the IP is not visible (null), no separator is appended, resulting in a clean display.

app/Http/Controllers/Auth/OAuthController.php (2)

59-59: LGTM! Centralized OAuth linking.

The refactor to delegate OAuth user linking to OAuthService::linkUser() improves code organization by centralizing OAuth management logic. This eliminates duplication and ensures consistent behavior across the application.


86-86: LGTM! Consistent OAuth service usage.

The delegation to $this->oauthService->linkUser() for handling missing users is consistent with the approach used in the callback method (line 59). This ensures uniform OAuth linking behavior throughout the controller.

lang/en/admin/user.php (1)

12-12: LGTM! New translation keys for user management features.

The added translation keys support the new features mentioned in the PR objectives:

  • external_id for displaying OAuth external identifiers
  • password_reset, password_reset_sent, and password_reset_failed for the password reset functionality

The keys are clear, descriptive, and follow the existing naming conventions.

Also applies to: 19-21

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

154-168: LGTM! Improved avatar file handling.

The updated avatar file upload handling correctly:

  1. Uses formatStateUsing to populate the field with the existing avatar path when present (lines 154-159)
  2. Implements deleteUploadedFileUsing to handle both temporary uploads (TemporaryUploadedFile) and stored files (lines 160-168)

This provides proper lifecycle management for avatar files, ensuring temporary uploads are cleaned up and existing avatars can be removed.


188-190: LGTM! Centralized OAuth unlinking.

The refactor to use $this->oauthService->unlinkUser() (line 190) aligns with the centralized OAuth management approach introduced in this PR. This eliminates manual array manipulation and ensures consistent behavior with other OAuth operations throughout the application.

lang/en/profile.php (1)

11-11: 'keys' translation is required. It’s used as the parent tab label grouping both API and SSH keys in the profile UI, so it isn’t redundant.

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

391-396: Double subject in Activity chain — confirm API semantics

Calling ->subject($user) then ->subject($apiKey/$sshKey) likely overwrites the first. If your Activity facade supports only a single subject, consider using actor/subject consistently and move the other value into ->property().

Do you want me to adjust these calls to your facade’s canonical pattern?

Also applies to: 429-434


460-463: Confirm Repeater relationship usage

relationship(null, ...) relies on implicit naming. Ensure this binds to the intended relationship on User (e.g., activity or activityLogs) and works with the v4 Schemas API. If not, pass the explicit relationship name.

Based on learnings


285-304: Avatar assumes .png storage

Visibility/formatStateUsing/delete logic is hardcoded to {id}.png on the public disk. Confirm the avatar pipeline always normalizes to PNG; otherwise deletions may miss JPEG/WebP.

If formats differ, I can update this to resolve the actual stored path and delete variants.

Removed helpertext, admins don't need to see it...
Remove 2fa section
Added requires confirm on unlinking
@notAreYouScared notAreYouScared marked this pull request as ready for review October 7, 2025 20:34
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

♻️ Duplicate comments (4)
app/Filament/Admin/Resources/Users/UserResource.php (4)

339-339: Invalid query builder method whereNot.

Laravel's Eloquent query builder doesn't provide a whereNot() method. This will cause a runtime error.

Apply this diff to use a valid query method:

-->relationship('roles', 'name', fn (Builder $query) => $query->whereNot('id', Role::getRootAdmin()->id))
+->relationship('roles', 'name', fn (Builder $query) => $query->where('id', '!=', Role::getRootAdmin()->id))

Alternatively, use whereKeyNot:

-->relationship('roles', 'name', fn (Builder $query) => $query->whereNot('id', Role::getRootAdmin()->id))
+->relationship('roles', 'name', fn (Builder $query) => $query->whereKeyNot(Role::getRootAdmin()->id))

414-424: Fix null dereference in SSH key delete action.

Same issue as with API keys: UserSSHKey::find() can return null, and calling ->exists() on null will throw an error.

Apply this diff to fix the null check:

 $sshKey = UserSSHKey::find($key['id'] ?? null);
-if ($sshKey->exists()) {
+if ($sshKey) {
     $sshKey->delete();
 
     Activity::event('user:ssh-key.delete')
         ->actor(auth()->user())
         ->subject($user)
         ->subject($sshKey)
         ->property('fingerprint', $sshKey->fingerprint)
         ->log();
 }

376-387: Fix null dereference in API key delete action.

ApiKey::find() can return null, and calling ->exists() on null will throw an error. Additionally, exists is a property on Eloquent models, not a method.

Apply this diff to fix the null check:

 $apiKey = ApiKey::find($key['id'] ?? null);
-if ($apiKey->exists()) {
+if ($apiKey) {
     $apiKey->delete();
 
     Activity::event('user:api-key.delete')
         ->actor(auth()->user())
         ->subject($user)
         ->subject($apiKey)
         ->property('identifier', $apiKey->identifier)
         ->log();
 }

307-331: Fix variable shadowing, early return, and missing refresh.

This code has several issues that were previously flagged but remain unaddressed:

  1. Variable shadowing: Line 308 overwrites the loop variable $schema (which holds the schema key) with the schema object, making the key inaccessible.
  2. Early return aborts action building: Line 310's return exits the entire closure, preventing subsequent OAuth providers from being processed. Should be continue.
  3. Missing refresh before form refill: Line 322 refills the form without refreshing the user model, potentially showing stale data.

Apply this diff to fix all issues:

-foreach ($user->oauth as $schema => $_) {
-    $schema = $oauthService->get($schema);
-    if (!$schema) {
-        return;
+foreach (array_keys($user->oauth ?? []) as $schemaKey) {
+    $provider = $oauthService->get($schemaKey);
+    if (!$provider) {
+        continue;
     }
 
-    $id = $schema->getId();
-    $name = $schema->getName();
+    $id = $provider->getId();
+    $name = $provider->getName();
     $actions[] = Action::make("oauth_$id")
         ->label(trans('profile.unlink', ['name' => $name]))
         ->icon('tabler-unlink')
         ->requiresConfirmation()
-        ->color(Color::hex($schema->getHexColor()))
-        ->action(function ($livewire) use ($oauthService, $user, $name, $schema) {
-            $oauthService->unlinkUser($user, $schema);
+        ->color(Color::hex($provider->getHexColor()))
+        ->action(function ($livewire) use ($oauthService, $user, $name, $provider) {
+            $oauthService->unlinkUser($user, $provider);
+            $user->refresh();
             $livewire->form->fill($user->attributesToArray());
             Notification::make()
                 ->title(trans('profile.unlinked', ['name' => $name]))
                 ->success()
                 ->send();
         });
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 40aff61 and 321cf3a.

📒 Files selected for processing (2)
  • app/Extensions/OAuth/OAuthService.php (2 hunks)
  • app/Filament/Admin/Resources/Users/UserResource.php (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/Filament/Admin/Resources/Users/UserResource.php (8)
app/Extensions/OAuth/OAuthService.php (3)
  • OAuthService (10-71)
  • get (21-24)
  • unlinkUser (59-70)
app/Facades/Activity.php (1)
  • Activity (8-14)
app/Models/ActivityLog.php (4)
  • ActivityLog (55-265)
  • apiKey (106-109)
  • actor (93-96)
  • htmlable (171-197)
app/Models/ApiKey.php (1)
  • ApiKey (56-234)
app/Models/User.php (1)
  • User (94-492)
app/Models/UserSSHKey.php (1)
  • UserSSHKey (43-70)
app/Notifications/MailTested.php (1)
  • MailTested (9-28)
app/Services/Helpers/LanguageService.php (2)
  • LanguageService (8-30)
  • getAvailableLanguages (22-29)
app/Extensions/OAuth/OAuthService.php (2)
app/Models/User.php (1)
  • User (94-492)
app/Extensions/OAuth/OAuthSchemaInterface.php (1)
  • getId (10-10)
🪛 PHPMD (2.15.0)
app/Filament/Admin/Resources/Users/UserResource.php

307-307: Avoid unused local variables such as '$_'. (undefined)

(UnusedLocalVariable)

🔇 Additional comments (1)
app/Extensions/OAuth/OAuthService.php (1)

49-70: LGTM! Null handling correctly implemented.

Both linkUser and unlinkUser methods now correctly handle the potential null value of $user->oauth using the null coalescing operator (?? []). The implementations safely update the OAuth associations and return refreshed user instances.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/Filament/Pages/Auth/EditProfile.php (1)

188-204: Critical: undefined variable in OAuth unlink closure.

The closure references $updateService at line 193, but it's neither injected as a parameter nor captured in the use() clause. This will cause a fatal error when the unlink action is triggered.

Apply this diff to fix using the existing OAuthService:

-                                        ->action(function () use ($id, $name, $schema, $unlink) {
+                                        ->action(function (OAuthService $oauthService) use ($id, $name, $schema, $unlink) {
                                             if ($unlink) {
-                                                $oauth = user()?->oauth;
-                                                unset($oauth[$id]);
-
-                                                $updateService->handle(user(), ['oauth' => $oauth]);
+                                                $oauthService->unlinkUser(user(), $schema);
 
                                                 $this->fillForm();
 
                                                 Notification::make()
                                                     ->title(trans('profile.unlinked', ['name' => $name]))
                                                     ->success()
                                                     ->send();
                                             } else {
                                                 redirect(Socialite::with($id)->redirect()->getTargetUrl());
                                             }
                                         });
♻️ Duplicate comments (5)
app/Filament/Admin/Resources/Users/UserResource.php (5)

339-339: Verify Laravel version for whereNot() compatibility.

The whereNot() method was introduced in Laravel 10. Ensure your project is running Laravel 10 or higher, otherwise this will cause a fatal error.

Run the following script to check the Laravel version:

#!/bin/bash
# Check Laravel version
echo "=== Checking Laravel version ==="
cat composer.json | jq -r '.require["laravel/framework"]'

echo -e "\n=== Checking composer.lock for installed version ==="
cat composer.lock | jq -r '.packages[] | select(.name == "laravel/framework") | .version'

If the version is below 10, apply this diff:

-                                    ->relationship('roles', 'name', fn (Builder $query) => $query->whereNot('id', Role::getRootAdmin()->id))
+                                    ->relationship('roles', 'name', fn (Builder $query) => $query->where('id', '!=', Role::getRootAdmin()->id))

307-331: Fix variable shadowing, early return, and add user refresh.

Three issues remain from previous reviews:

  1. Line 307: Loop variable $schema is shadowed when reassigned on line 308. Use a different variable name like $schemaKey for the loop.
  2. Line 310: return; exits the entire function, preventing subsequent OAuth providers from being processed. Use continue; instead.
  3. Line 322: After unlinking, refresh the user model before refilling the form to ensure fresh data.

Apply this diff:

-                                        foreach ($user->oauth as $schema => $_) {
-                                            $schema = $oauthService->get($schema);
+                                        foreach (array_keys($user->oauth ?? []) as $schemaKey) {
+                                            $provider = $oauthService->get($schemaKey);
-                                            if (!$schema) {
-                                                return;
+                                            if (!$provider) {
+                                                continue;
                                             }
 
-                                            $id = $schema->getId();
-                                            $name = $schema->getName();
+                                            $id = $provider->getId();
+                                            $name = $provider->getName();
                                             $actions[] = Action::make("oauth_$id")
                                                 ->label(trans('profile.unlink', ['name' => $name]))
                                                 ->icon('tabler-unlink')
                                                 ->requiresConfirmation()
-                                                ->color(Color::hex($schema->getHexColor()))
-                                                ->action(function ($livewire) use ($oauthService, $user, $name, $schema) {
-                                                    $oauthService->unlinkUser($user, $schema);
+                                                ->color(Color::hex($provider->getHexColor()))
+                                                ->action(function ($livewire) use ($oauthService, $user, $name, $provider) {
+                                                    $oauthService->unlinkUser($user, $provider);
+                                                    $user->refresh();
                                                     $livewire->form->fill($user->attributesToArray());
                                                     Notification::make()
                                                         ->title(trans('profile.unlinked', ['name' => $name]))
                                                         ->success()
                                                         ->send();
                                                 });
                                         }

376-387: Fix null pointer dereference in API key deletion.

ApiKey::find() returns null when the record is not found. Calling ->exists() on null will throw a fatal error. Additionally, Eloquent models use the $model->exists property, not ->exists() method.

Apply this diff:

                                                     $apiKey = ApiKey::find($key['id'] ?? null);
-                                                    if ($apiKey->exists()) {
+                                                    if ($apiKey) {
                                                         $apiKey->delete();
 
                                                         Activity::event('user:api-key.delete')
                                                             ->actor(auth()->user())
                                                             ->subject($user)
                                                             ->subject($apiKey)
                                                             ->property('identifier', $apiKey->identifier)
                                                             ->log();
                                                     }

414-424: Fix null pointer dereference in SSH key deletion.

Same issue as the API key deletion: UserSSHKey::find() can return null, and calling ->exists() on null will throw a fatal error.

Apply this diff:

                                                     $sshKey = UserSSHKey::find($key['id'] ?? null);
-                                                    if ($sshKey->exists()) {
+                                                    if ($sshKey) {
                                                         $sshKey->delete();
 
                                                         Activity::event('user:ssh-key.delete')
                                                             ->actor(auth()->user())
                                                             ->subject($user)
                                                             ->subject($sshKey)
                                                             ->property('fingerprint', $sshKey->fingerprint)
                                                             ->log();
                                                     }

449-451: Specify relationship name for activity repeater.

Using null as the relationship name prevents proper binding to the User model's activity() relationship. This should reference the actual relationship name.

Apply this diff:

-                                    ->relationship(null, function (Builder $query) {
+                                    ->relationship('activity', function (Builder $query) {
                                         $query->orderBy('timestamp', 'desc');
                                     })
🧹 Nitpick comments (1)
app/Filament/Admin/Resources/Users/UserResource.php (1)

245-251: Consider adding helper text for external_id field.

The external_id field would benefit from helper text or a hint explaining its purpose (e.g., for external system integrations or OAuth provider IDs).

Example:

 TextInput::make('external_id')
     ->label(trans('admin/user.external_id'))
+    ->helperText('Identifier from external systems or OAuth providers')
     ->columnSpan([
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 321cf3a and 09f3010.

📒 Files selected for processing (3)
  • app/Filament/Admin/Resources/Users/Pages/EditUser.php (1 hunks)
  • app/Filament/Admin/Resources/Users/UserResource.php (2 hunks)
  • app/Filament/Pages/Auth/EditProfile.php (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/Filament/Admin/Resources/Users/Pages/EditUser.php
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-07T14:44:18.554Z
Learnt from: notAreYouScared
PR: pelican-dev/panel#1779
File: app/Filament/Admin/Resources/Users/Pages/EditUser.php:51-51
Timestamp: 2025-10-07T14:44:18.554Z
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/Pages/Auth/EditProfile.php
🧬 Code graph analysis (1)
app/Filament/Admin/Resources/Users/UserResource.php (9)
app/Extensions/OAuth/OAuthService.php (3)
  • OAuthService (10-71)
  • get (21-24)
  • unlinkUser (59-70)
app/Facades/Activity.php (1)
  • Activity (8-14)
app/Models/ActivityLog.php (4)
  • ActivityLog (55-265)
  • apiKey (106-109)
  • actor (93-96)
  • htmlable (171-197)
app/Models/ApiKey.php (1)
  • ApiKey (56-234)
app/Models/User.php (2)
  • User (94-492)
  • email (225-230)
app/Models/UserSSHKey.php (1)
  • UserSSHKey (43-70)
app/Notifications/MailTested.php (1)
  • MailTested (9-28)
app/Services/Helpers/LanguageService.php (2)
  • LanguageService (8-30)
  • getAvailableLanguages (22-29)
app/Filament/Pages/Auth/EditProfile.php (1)
  • form (83-533)
🪛 PHPMD (2.15.0)
app/Filament/Admin/Resources/Users/UserResource.php

307-307: Avoid unused local variables such as '$_'. (undefined)

(UnusedLocalVariable)

🔇 Additional comments (5)
app/Filament/Pages/Auth/EditProfile.php (1)

154-168: LGTM! Avatar lifecycle handling is correct.

The implementation properly manages avatar files through FileUpload's lifecycle hooks without direct database interaction, correctly distinguishing between temporary uploads and persisted disk files.

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

181-203: LGTM! Mail configuration check is correct.

The test mail action now correctly uses config('mail.default', 'log') to check the Laravel mail driver configuration. The action properly handles exceptions and provides appropriate user feedback.


213-244: LGTM! Password reset implementation is solid.

The password reset action correctly integrates with Laravel's Password broker and Filament's reset password flow. Error handling and user notifications are properly implemented.


252-280: LGTM! Timezone and language fields are well-implemented.

The timezone and language selects properly use Laravel's DateTimeZone and the LanguageService, with appropriate defaults and searchability. The non-native select provides better UX.


281-300: LGTM! Avatar field implementation is correct.

The avatar field properly manages the file lifecycle through Filament's FileUpload component, with correct handling of temporary uploads and persisted files. The visibility condition appropriately checks for file existence.

@notAreYouScared notAreYouScared requested a review from Boy132 October 8, 2025 00:52
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

♻️ Duplicate comments (3)
app/Filament/Admin/Resources/Users/UserResource.php (3)

412-413: Fix null dereference in SSH key delete action.

UserSSHKey::find() returns null if the key doesn't exist. Calling ->exists() on null will throw an error.

Apply this diff:

 $sshKey = UserSSHKey::find($key['id'] ?? null);
-if ($sshKey->exists()) {
+if ($sshKey) {
     $sshKey->delete();

455-456: Fix null relationship name in activity repeater.

Using null as the relationship name doesn't bind to the User model's activity relation. This prevents the repeater from loading activity records properly.

Apply this diff:

-->relationship(null, function (Builder $query) {
+->relationship('activity', function (Builder $query) {
     $query->orderBy('timestamp', 'desc');
 })

289-310: Fix variable shadowing, early return, and use refreshed user (resolves PHPMD warning).

Three issues in the OAuth unlink logic:

  1. Variable shadowing: Loop variable $schema is overwritten at line 290, making code confusing.
  2. Early return: Line 292 exits the entire schema function, preventing subsequent OAuth providers from rendering. Use continue instead.
  3. Missing refresh: unlinkUser() returns a refreshed user (see OAuthService.php), but the code doesn't use the return value before refilling the form.

Apply this diff to fix all three issues:

-foreach ($user->oauth as $schema => $_) {
-    $schema = $oauthService->get($schema);
-    if (!$schema) {
-        return;
+foreach (array_keys($user->oauth ?? []) as $providerId) {
+    $provider = $oauthService->get($providerId);
+    if (!$provider) {
+        continue;
     }
 
-    $id = $schema->getId();
-    $name = $schema->getName();
+    $id = $provider->getId();
+    $name = $provider->getName();
     $actions[] = Action::make("oauth_$id")
         ->label(trans('profile.unlink', ['name' => $name]))
         ->icon('tabler-unlink')
         ->requiresConfirmation()
-        ->color(Color::hex($schema->getHexColor()))
-        ->action(function ($livewire) use ($oauthService, $user, $name, $schema) {
-            $oauthService->unlinkUser($user, $schema);
-            $livewire->form->fill($user->attributesToArray());
+        ->color(Color::hex($provider->getHexColor()))
+        ->action(function ($livewire) use ($oauthService, $user, $name, $provider) {
+            $user = $oauthService->unlinkUser($user, $provider);
+            $livewire->form->fill($user->attributesToArray());
             Notification::make()
                 ->title(trans('profile.unlinked', ['name' => $name]))
                 ->success()
                 ->send();
         });
 }
🧹 Nitpick comments (1)
app/Filament/Admin/Resources/Users/UserResource.php (1)

369-370: Simplify null check in API key delete action.

Using ?->exists() is safe but redundant. find() returns null if the record doesn't exist, so checking if ($apiKey) is clearer and more idiomatic.

Apply this diff:

 $apiKey = ApiKey::find($key['id'] ?? null);
-if ($apiKey?->exists()) {
+if ($apiKey) {
     $apiKey->delete();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9acb01f and cdc1e15.

📒 Files selected for processing (3)
  • app/Filament/Admin/Resources/Users/UserResource.php (2 hunks)
  • app/Filament/Pages/Auth/EditProfile.php (5 hunks)
  • lang/en/profile.php (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • lang/en/profile.php
  • app/Filament/Pages/Auth/EditProfile.php
🧰 Additional context used
🧬 Code graph analysis (1)
app/Filament/Admin/Resources/Users/UserResource.php (9)
app/Extensions/OAuth/OAuthService.php (3)
  • OAuthService (10-71)
  • get (21-24)
  • unlinkUser (59-70)
app/Facades/Activity.php (1)
  • Activity (8-14)
app/Filament/Admin/Resources/Users/Pages/EditUser.php (1)
  • EditUser (16-55)
app/Models/ActivityLog.php (4)
  • ActivityLog (55-265)
  • apiKey (106-109)
  • actor (93-96)
  • htmlable (171-197)
app/Models/ApiKey.php (1)
  • ApiKey (56-234)
app/Models/User.php (4)
  • User (94-492)
  • email (225-230)
  • apiKeys (242-246)
  • sshKeys (248-251)
app/Models/UserSSHKey.php (1)
  • UserSSHKey (43-70)
app/Services/Helpers/LanguageService.php (2)
  • LanguageService (8-30)
  • getAvailableLanguages (22-29)
app/Services/Activity/ActivityLogService.php (3)
  • event (48-53)
  • property (109-117)
  • log (136-155)
🪛 PHPMD (2.15.0)
app/Filament/Admin/Resources/Users/UserResource.php

289-289: Avoid unused local variables such as '$_'. (undefined)

(UnusedLocalVariable)

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

191-191: LGTM! Previous review concerns addressed.

Excellent improvements addressing past review feedback:

  • ✅ Correct mail config usage: config('mail.default') instead of config('MAIL_MAILER')
  • ✅ Proper timezone default: config('app.timezone', 'UTC')
  • ✅ Avatar field safely handles user creation scenario
  • ✅ OAuth unlink actions include confirmation prompts
  • ✅ Placeholders added for empty states (OAuth, API keys, SSH keys) for UI consistency

Also applies to: 236-236, 257-257, 300-300, 312-318


329-329: No changes needed—Laravel v12.32.5 supports whereNot().

@notAreYouScared notAreYouScared merged commit dbe4bdd into main Oct 8, 2025
25 checks passed
@notAreYouScared notAreYouScared deleted the charles/issue1741 branch October 8, 2025 09:04
@github-actions github-actions bot locked and limited conversation to collaborators Oct 8, 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.

Improve EditUser page

4 participants