Skip to content

Add changes from upstream#2293

Open
Boy132 wants to merge 16 commits intomainfrom
boy132/upstream-changes
Open

Add changes from upstream#2293
Boy132 wants to merge 16 commits intomainfrom
boy132/upstream-changes

Conversation

Boy132 and others added 9 commits April 9, 2026 11:01
…ckups/transfers

Co-authored-by: DaneEveritt <dane@daneeveritt.com>
… deletion

Co-authored-by: DaneEveritt <dane@daneeveritt.com>
Co-authored-by: DaneEveritt <dane@daneeveritt.com>
Co-authored-by: DaneEveritt <dane@daneeveritt.com>
Co-authored-by: danny6167 <danielb@purpleflaghosting.com>
Co-authored-by: MrSoulPenguin <28676680+MrSoulPenguin@users.noreply.github.com>
Co-authored-by: DaneEveritt <dane@daneeveritt.com>
Co-authored-by: DaneEveritt <dane@daneeveritt.com>
@Boy132 Boy132 self-assigned this Apr 9, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9d264d1b-eed5-4def-9996-05c422e61f20

📥 Commits

Reviewing files that changed from the base of the PR and between a463277 and 3dd6d04.

📒 Files selected for processing (1)
  • tests/Integration/Jobs/RevokeSftpAccessJobTest.php
✅ Files skipped from review due to trivial changes (1)
  • tests/Integration/Jobs/RevokeSftpAccessJobTest.php

📝 Walkthrough

Walkthrough

This PR introduces asynchronous SFTP access revocation triggered by user deletion or password changes via a new RevokeSftpAccessJob. It replaces synchronous daemon calls with async job dispatch across multiple services, refactors remote API authorization to use inline node checks instead of a form request class, adds email update rate limiting and security headers, and updates activity logging patterns.

Changes

Cohort / File(s) Summary
User Events & Deletion Flow
app/Events/User/Deleting, app/Events/User/PasswordChanged, app/Models/User.php, app/Listeners/RevocationListener.php, app/Services/Users/UserUpdateService.php
Added user deletion and password-change events that dispatch revocation listeners. Listener queries accessible node servers and dispatches RevokeSftpAccessJob for each node. Password event conditionally dispatched when password key exists in update data.
SFTP Revocation Job
app/Jobs/RevokeSftpAccessJob.php, tests/Integration/Jobs/RevokeSftpAccessJobTest.php
New async job to revoke SFTP access for users across Server or Node targets. Implements unique job IDs, connection-error backoff with exponential scaling, and max 3 retry attempts. Tested with unique ID formatting and connection failure handling.
Service Async Refactoring
app/Services/Servers/DetailsModificationService.php, app/Services/Subusers/SubuserDeletionService.php, app/Services/Subusers/SubuserUpdateService.php
Replaced synchronous DaemonServerRepository calls with async RevokeSftpAccessJob dispatch. Removed exception handling for connection failures; now jobs handle retries. Activities log revocation intent with job dispatch inside transaction closure.
Daemon Repository Updates
app/Repositories/Daemon/DaemonServerRepository.php
Modified deauthorize() to conditionally send servers array: populated when targeting specific server, empty when targeting all node servers. Updated docblock to reflect dual targeting modes.
Remote API Authorization Refactoring
app/Http/Controllers/Api/Remote/Servers/ServerContainersController.php, app/Http/Controllers/Api/Remote/Servers/ServerDetailsController.php, app/Http/Controllers/Api/Remote/Servers/ServerInstallController.php, app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php, app/Http/Requests/Api/Remote/ServerRequest.php, app/Http/Requests/Api/Remote/InstallationDataRequest.php
Removed ServerRequest form request class entirely. Replaced with inline node authorization checks in controllers using $request->attributes->get('node'). failure()/success() endpoints enforce node matching (new/old node for failure, new node only for success). InstallationDataRequest now extends FormRequest directly with authorize() = true.
Client Account Management
app/Http/Controllers/Api/Client/AccountController.php, routes/api-client.php, tests/Integration/Api/Client/AccountControllerTest.php
Added EMAIL_UPDATE_THROTTLE constant (24 hours) with per-user rate limiting on email updates. Email change only hits throttler when email differs (case-insensitive). Password update wrapped in activity transaction with RevokeSftpAccessJob dispatch assertions in tests across multi-node scenarios.
Backup & Database Operations
app/Http/Controllers/Api/Client/Servers/BackupController.php, app/Http/Controllers/Api/Client/Servers/DatabaseController.php, app/Http/Controllers/Api/Client/Servers/StartupController.php
Added lockForUpdate()->count() to backup/database row-lock queries ensuring pessimistic locks execute within transactions. Modified database password rotation to execute inside activity transaction closure. Adjusted activity logging comparisons to use pre-update values.
Backup Remote Access
app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php, app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php
Updated forbidden-access error messages and added node-ownership validation in restore() to verify requesting node matches backup's server node.
Queue Job Base Class
app/Jobs/Job.php, app/Jobs/Schedule/RunTaskJob.php
Removed Job abstract base class entirely. RunTaskJob now implements ShouldQueue directly and uses Queueable trait instead of extending removed base class.
Security & Middleware
app/Http/Middleware/SetSecurityHeaders.php, app/Providers/Filament/PanelProvider.php, bootstrap/app.php
New middleware SetSecurityHeaders sets X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy headers only when not already present. Registered in both Filament panel and web middleware stacks.
Allocation Pessimistic Locking
app/Services/Allocations/FindAssignableAllocationService.php
Added lockForUpdate() to allocation selection and re-selection queries, preventing concurrent modifications between read and update()/refresh() operations.
Configuration & Models
config/http.php, app/Models/ServerTransfer.php, database/Factories/ServerTransferFactory.php
Updated API rate limit defaults from 120/240 to 256/256. Added HasFactory trait to ServerTransfer model. New factory provides default attributes for transfer records (allocations as arrays, successful as null, archived as false).
Subuser Test Updates
tests/Integration/Api/Client/Server/Subuser/DeleteSubuserTest.php, tests/Integration/Api/Client/Server/Subuser/SubuserAuthorizationTest.php, tests/Integration/Api/Client/Server/Subuser/UpdateSubuserTest.php
Replaced DaemonServerRepository mocks with Bus::fake([RevokeSftpAccessJob::class]) to assert job dispatch instead of direct daemon calls. Verify job dispatched with correct user UUID and target server/node.
User Deletion & Server Transfer Tests
tests/Integration/Services/Users/UserDeletionServiceTest.php, tests/Integration/Api/Remote/ServerTransferControllerTest.php
New user deletion test verifies RevokeSftpAccessJob dispatch for users with subusers. Server transfer test validates node-based authorization for success/failure endpoints with forbidden/allowed scenarios per node relationship.
Remote Routes
routes/api-remote.php
Removed GET routes for transfer/failure and transfer/success; only POST routes remain for those actions.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant App as Application
    participant Event as Event System
    participant Listener as RevocationListener
    participant Queue as Job Queue
    participant Daemon as Daemon

    User->>App: Delete User / Change Password
    App->>App: Save Model Changes
    App->>Event: Dispatch Deleting/PasswordChanged Event
    Event->>Listener: Call Handle
    Listener->>Listener: Query User's Accessible Nodes
    loop For Each Node (chunked)
        Listener->>Queue: Dispatch RevokeSftpAccessJob<br/>(user uuid, target node)
    end
    Queue-->>Daemon: Async: Execute deauthorize<br/>(retry on connection error)
    Daemon->>Daemon: Revoke SFTP Access
Loading

Possibly Related PRs

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.03% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Add changes from upstream' is vague and generic, using non-descriptive terminology that doesn't convey meaningful information about the substantial changes in the changeset. Consider a more specific title that summarizes the main changes, such as 'Add async SFTP revocation and security improvements' or 'Implement event-driven user deletion and access revocation'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description consists solely of links to upstream commits without providing context about what changes are being imported or why they are important to this codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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
Copy Markdown

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

Caution

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

⚠️ Outside diff range comments (3)
app/Services/Databases/DatabaseManagementService.php (1)

145-155: ⚠️ Potential issue | 🔴 Critical

Critical: sharedLock() without WHERE clause updates all database records.

Calling $database->sharedLock()->update([...]) on an Eloquent Model delegates via __call() to a fresh query Builder with no WHERE clause. This is equivalent to Database::sharedLock()->update([...]), which updates ALL database records in the table to the same password instead of just the intended record.

Use:

Database::where('id', $database->id)->sharedLock()->update([
    'password' => $password,
]);

Or simply $database->update([...]) since you're already in a transaction.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Services/Databases/DatabaseManagementService.php` around lines 145 - 155,
Inside the transaction in DatabaseManagementService (the closure using $database
and $password), replace the unsafe call to
$database->sharedLock()->update([...]) with a per-model update — for example
call $database->update([...]) — so only the intended record has its password
changed; keep the rest of the chain
($database->dropUser()->createUser()->assignUserToDatabase()->flushPrivileges())
intact.
app/Services/Subusers/SubuserDeletionService.php (1)

20-26: ⚠️ Potential issue | 🟠 Major

Job dispatch inside database transaction may cause inconsistency on rollback.

If the transaction fails and rolls back after RevokeSftpAccessJob::dispatch() is called, the job will still be queued and executed, potentially revoking SFTP access for a subuser that wasn't actually deleted. Consider dispatching the job after the transaction commits.

Proposed fix using afterCommit
         $log->transaction(function ($instance) use ($server, $subuser) {
             $subuser->delete();

             event(new SubUserRemoved($subuser->server, $subuser->user));
-
-            RevokeSftpAccessJob::dispatch($subuser->user->uuid, $server);
         });
+
+        RevokeSftpAccessJob::dispatch($subuser->user->uuid, $server);

Alternatively, you can use Laravel's afterCommit() method on the job:

RevokeSftpAccessJob::dispatch($subuser->user->uuid, $server)->afterCommit();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Services/Subusers/SubuserDeletionService.php` around lines 20 - 26, The
job dispatch is happening inside the DB transaction closure ($log->transaction)
which can lead to SFTP revocation running even if the transaction rolls back;
update SubuserDeletionService to either move the RevokeSftpAccessJob dispatch
call out of the $log->transaction closure to run after the transaction
completes, or call ->afterCommit() on the job
(RevokeSftpAccessJob::dispatch(...)->afterCommit()) so the job only queues once
the deletion (and event dispatch of SubUserRemoved) is committed.
app/Services/Allocations/FindAssignableAllocationService.php (1)

58-79: ⚠️ Potential issue | 🔴 Critical

lockForUpdate() without a transaction leaves the allocation assignment vulnerable to race conditions.

The pessimistic lock is released immediately after ->first() completes at line 67. The subsequent ->update() call at line 77 executes outside the lock, allowing concurrent requests to select the same unassigned allocation and both attempt assignment. This pattern exists in both the initial query (lines 58–67) and in createNewAllocation() (lines 128–133).

Wrap the entire find-or-create-and-assign sequence in a DB::transaction() block. Within the transaction, move lockForUpdate() immediately before the ->first() call, and replace the update() method with direct property assignment followed by save(). This ensures the lock remains active through the assignment operation and aligns with the allocation update pattern used in ServerCreationService.

🔒 Suggested shape
+use Illuminate\Support\Facades\DB;
+
 public function handle(Server $server): Allocation
 {
     if (!config('panel.client_features.allocations.enabled')) {
         throw new AutoAllocationNotEnabledException();
     }
@@
-    /** `@var` Allocation|null $allocation */
-    $allocation = Allocation::withoutGlobalScopes()
-        ->lockForUpdate()
-        ->where('node_id', $server->node_id)
-        ->when($server->allocation, function ($query) use ($server) {
-            $query->where('ip', $server->allocation->ip);
-        })
-        ->whereBetween('port', [$start, $end])
-        ->whereNull('server_id')
-        ->inRandomOrder()
-        ->first();
-
-    if (!$createNew && !$allocation) {
-        throw new NoAutoAllocationSpaceAvailableException();
-    }
-
-    $allocation ??= $this->createNewAllocation($server, $start, $end);
-
-    $allocation->update(['server_id' => $server->id]);
-
-    return $allocation->refresh();
+    return DB::transaction(function () use ($server, $createNew, $start, $end) {
+        /** `@var` Allocation|null $allocation */
+        $allocation = Allocation::withoutGlobalScopes()
+            ->where('node_id', $server->node_id)
+            ->when($server->allocation, function ($query) use ($server) {
+                $query->where('ip', $server->allocation->ip);
+            })
+            ->whereBetween('port', [$start, $end])
+            ->whereNull('server_id')
+            ->lockForUpdate()
+            ->inRandomOrder()
+            ->first();
+
+        if (!$createNew && !$allocation) {
+            throw new NoAutoAllocationSpaceAvailableException();
+        }
+
+        $allocation ??= $this->createNewAllocation($server, $start, $end);
+        $allocation->server_id = $server->id;
+        $allocation->save();
+
+        return $allocation->refresh();
+    });
 }

Also applies to: lines 128–133 in createNewAllocation().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Services/Allocations/FindAssignableAllocationService.php` around lines 58
- 79, Wrap the entire find-or-create-and-assign flow in a DB::transaction()
inside FindAssignableAllocationService so the lock persists through assignment:
perform the Allocation::withoutGlobalScopes()->lockForUpdate() query and call
->first() inside the transaction, and if none found either call
createNewAllocation($server, $start, $end) from inside the transaction (also
wrapped with lockForUpdate() there) or create the allocation record inside the
same transaction; replace the post-query $allocation->update([...]) call with
direct property assignment ($allocation->server_id = $server->id) followed by
$allocation->save() so the assignment happens while the row lock is held. Ensure
createNewAllocation()’s allocation creation/assignment (the code around lines
where it currently uses update()) is moved into the same DB::transaction() or
also wrapped similarly.
🧹 Nitpick comments (6)
app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php (1)

100-103: Prefer direct node_id comparison in restore() for consistency and simpler authorization.

Current logic works, but matching index() style avoids an extra relation dereference and keeps the check uniform.

♻️ Suggested refactor
-        $node = $request->attributes->get('node');
-        if (!$model->server->node->is($node)) {
+        /** `@var` Node $node */
+        $node = $request->attributes->get('node');
+        if ($model->server->node_id !== $node->id) {
             throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php` around
lines 100 - 103, The authorization in restore() currently compares
model->server->node via is(), add consistency with index() by comparing node IDs
directly: retrieve $node from $request->attributes, compare
$model->server->node_id !== $node->id (or cast both to int) and throw the same
HttpForbiddenException when they differ; update the check in the restore()
method to use this direct node_id comparison instead of ->is().
app/Http/Middleware/SetSecurityHeaders.php (1)

20-25: Use a class constant for immutable security headers.

This map is static configuration and is never mutated, so a constant is cleaner and safer than a mutable static property.

♻️ Proposed refactor
-    protected static array $headers = [
+    private const HEADERS = [
         'X-Frame-Options' => 'DENY',
         'X-Content-Type-Options' => 'nosniff',
         'X-XSS-Protection' => '1; mode=block',
         'Referrer-Policy' => 'no-referrer-when-downgrade',
     ];
@@
-        foreach (static::$headers as $key => $value) {
+        foreach (self::HEADERS as $key => $value) {
             if (!$response->headers->has($key)) {
                 $response->headers->set($key, $value);
             }
         }

Also applies to: 38-38

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Http/Middleware/SetSecurityHeaders.php` around lines 20 - 25, Replace the
mutable protected static array $headers in class SetSecurityHeaders with a class
constant (e.g. protected const HEADERS = [...]) and update all usages
(self::$headers or static::$headers) to reference the constant (self::HEADERS or
static::HEADERS); do the same refactor for the other static array referenced
around the same file (the one at the other occurrence) so both immutable header
maps become class constants to reflect immutability.
config/http.php (1)

14-20: Rate limits significantly increased.

The client rate limit doubled (120→256) and application rate limit increased (240→256). Per the PR objectives, this is a temporary bump for problematic endpoints. Consider adding a comment noting this is temporary, or tracking with a TODO/issue so it can be revisited once the underlying issues are addressed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/http.php` around lines 14 - 20, The rate limits in the 'rate_limit'
config (keys 'client' and 'application' / env vars APP_API_CLIENT_RATELIMIT and
APP_API_APPLICATION_RATELIMIT) were temporarily bumped; add a short inline
comment or TODO next to these entries explaining this is a temporary increase
and reference an issue/ticket ID (or create one) so the change is revisited when
the underlying endpoint problems are fixed; ensure the comment mentions which
env vars control the values and the original limits for context.
app/Events/User/PasswordChanged.php (1)

8-12: Consider adding SerializesModels trait for consistency.

The sibling Deleting event uses SerializesModels, but this event does not. While Dispatchable handles event dispatching, SerializesModels ensures proper model serialization when the event is queued. If this event is handled by a queued listener, the User model should serialize correctly.

♻️ Suggested change for consistency
 use App\Models\User;
 use Illuminate\Foundation\Events\Dispatchable;
+use Illuminate\Queue\SerializesModels;

 final class PasswordChanged
 {
     use Dispatchable;
+    use SerializesModels;

     public function __construct(public readonly User $user) {}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Events/User/PasswordChanged.php` around lines 8 - 12, Add the
SerializesModels trait to the PasswordChanged event so the User model serializes
correctly when queued; update the class to use both Dispatchable and
SerializesModels (i.e., add SerializesModels to the use clause in class
PasswordChanged) and ensure the SerializesModels symbol is imported so the
existing constructor public readonly User $user continues to work with queued
listeners.
app/Jobs/RevokeSftpAccessJob.php (1)

23-25: Clarify the interaction between $tries, $maxExceptions, and manual release().

With $tries = 3 and $maxExceptions = 1:

  • $maxExceptions = 1 means the job fails permanently after 1 uncaught exception
  • Since ConnectionException is caught and the job is released, it won't count toward $maxExceptions
  • However, each release() still counts toward $tries

After 3 connection failures (releases), the job will be marked as failed without ever logging an exception. Consider whether this silent failure behavior is intentional, or if you want to log/alert when max retries are exhausted.

Optional: Add a failed() method to handle exhausted retries
+    public function failed(?\Throwable $exception): void
+    {
+        // Log or alert when SFTP revocation ultimately fails
+        \Log::warning("Failed to revoke SFTP access for user {$this->user} on {$this->target->uuid} after {$this->tries} attempts");
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Jobs/RevokeSftpAccessJob.php` around lines 23 - 25, The job's current
configuration (public int $tries = 3; public int $maxExceptions = 1;) plus the
use of release() on ConnectionException means releases consume $tries and can
silently exhaust retries without any exception being logged; in
RevokeSftpAccessJob, either adjust $tries/$maxExceptions to reflect desired
behavior or implement a public function failed(Throwable $exception = null) that
logs/alerts when retries are exhausted (include context like job id and
ConnectionException details) so you get visibility when release()-based retries
hit the $tries limit rather than silently failing; reference the class
RevokeSftpAccessJob, the release() calls for ConnectionException, and
add/implement failed() to surface the exhausted-retries condition.
tests/Integration/Api/Remote/ServerTransferControllerTest.php (1)

37-102: Consider extracting bearer token header creation into a helper.

The repeated "Bearer {$node->daemon_token_id}.{$node->daemon_token}" logic appears in multiple tests; a private helper would reduce duplication and future drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Integration/Api/Remote/ServerTransferControllerTest.php` around lines
37 - 102, Tests in ServerTransferControllerTest duplicate the Authorization
header string construction ("Bearer
{$node->daemon_token_id}.{$node->daemon_token}") across multiple withHeader
calls; add a private helper on the test class (e.g. authHeaderFor(Node $node):
array or authHeaderValue(Node $node): string) that returns the properly
formatted header and update all usages of withHeader('Authorization', "Bearer
...") to call that helper (refer to withHeader calls in
test_success_status_update_cannot_be_sent_from_old_node,
test_success_status_update_cannot_be_sent_from_unauthorized_node,
test_failure_status_update_cannot_be_sent_from_unauthorized_node, and other
tests using $oldNode/$newNode/$node and their daemon_token_id/daemon_token).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Listeners/AuthenticationListener.php`:
- Line 20: The AuthenticationListener's method name "login" isn't wired into
Laravel's event system, so update event registration: add explicit mappings in
EventServiceProvider::$listen that map Illuminate\Auth\Events\Login and
Illuminate\Auth\Events\Failed to App\Listeners\AuthenticationListener@login (or
implement Illuminate\Contracts\Events\DispatcherSubscriber and define
subscribe() in AuthenticationListener, or rename the listener method to handle()
and register the listener class for the events) so the AuthenticationListener
methods are actually invoked when auth events fire.

In `@app/Services/Servers/DetailsModificationService.php`:
- Around line 48-50: The code revokes SFTP for the new owner because after
DetailsModificationService::forceFill() updates owner_id the $server->user
relation points to the new owner; capture the old owner before mutating and
dispatch RevokeSftpAccessJob with that old owner's identifier instead.
Concretely, read and store the previous owner id or uuid (e.g. $oldOwnerId =
$server->owner_id or $oldUserUuid = $server->user->uuid) before calling
forceFill(), then after detecting the change (the existing if ($server->owner_id
!== $owner) logic) call RevokeSftpAccessJob::dispatch(...) with the stored
$oldUserUuid/$oldOwnerId so the previous owner's SFTP is revoked rather than the
new owner's.

In `@app/Services/Subusers/SubuserUpdateService.php`:
- Around line 39-45: The RevokeSftpAccessJob is being dispatched inside the
$log->transaction closure in SubuserUpdateService which risks revoking SFTP
access if the transaction later rolls back; move the job dispatch so it runs
only after a successful commit (either call
RevokeSftpAccessJob::dispatch(...)->afterCommit() from inside the transaction or
defer the dispatch to after the $log->transaction call), ensuring the
$subuser->update(['permissions' => $cleanedPermissions]) is committed before
revocation is triggered.

In `@database/Factories/ServerTransferFactory.php`:
- Around line 22-23: The ServerTransferFactory::definition() method is missing
the explicit array return type; update the method signature for definition() in
class ServerTransferFactory to declare a return type of array (i.e., change the
signature to include : array) so it matches the other factories and enforces the
same typing consistency across the codebase.

In `@tests/Integration/Api/Client/Server/Subuser/DeleteSubuserTest.php`:
- Around line 48-50: The test is using Bus::assertDispatchedTimes() with a
closure (incorrect API) and risks false positives because the same job is
dispatched twice; replace the Bus::assertDispatchedTimes(...) checks with
Bus::assertDispatched(RevokeSftpAccessJob::class, fn($job) => $job->user ===
$subuser->uuid && $job->target->is($server)) and reset the bus fake between the
two DELETE operations (e.g., call Bus::fake() after the first DELETE or before
the second assertion) so the second assertion only sees the second dispatch;
update both the assertion around RevokeSftpAccessJob and the duplicate at the
later location accordingly.

In `@tests/Integration/Api/Client/Server/Subuser/UpdateSubuserTest.php`:
- Around line 62-64: Replace the incorrect Bus::assertDispatchedTimes(...) call
with Bus::assertDispatched(...) using a closure to filter by the job payload:
locate the assertion referencing RevokeSftpAccessJob and change it to
Bus::assertDispatched(function (RevokeSftpAccessJob $job) use ($server,
$subuser) { return $job->user === $subuser->user->uuid &&
$job->target->is($server); }); Make the same replacement for the second
occurrence around the RevokeSftpAccessJob assertion at the later lines
(105-107).

In `@tests/Integration/Jobs/RevokeSftpAccessJobTest` .php:
- Line 1: The test file name contains an extra space before the .php extension
which breaks PHPUnit discovery; rename the file from "RevokeSftpAccessJobTest
.php" to "RevokeSftpAccessJobTest.php" and update any references/imports or
test-suite entries if present so the class RevokeSftpAccessJobTest is located by
PHPUnit's *Test.php pattern.

In `@tests/Integration/Services/Users/UserDeletionServiceTest.php`:
- Around line 25-32: The test uses
$this->expectException(DisplayException::class) which aborts execution when the
exception is thrown, making the subsequent assertions
($this->assertModelExists($server->user) and
Bus::assertNotDispatched(RevokeSftpAccessJob::class)) unreachable; replace the
expectException pattern with an explicit try/catch around the
$server->user->delete() call (or manually fail if no exception) so you can catch
DisplayException and then run the post-exception assertions inside the catch
block, ensuring DisplayException is asserted and that assertModelExists and
Bus::assertNotDispatched still execute.

---

Outside diff comments:
In `@app/Services/Allocations/FindAssignableAllocationService.php`:
- Around line 58-79: Wrap the entire find-or-create-and-assign flow in a
DB::transaction() inside FindAssignableAllocationService so the lock persists
through assignment: perform the
Allocation::withoutGlobalScopes()->lockForUpdate() query and call ->first()
inside the transaction, and if none found either call
createNewAllocation($server, $start, $end) from inside the transaction (also
wrapped with lockForUpdate() there) or create the allocation record inside the
same transaction; replace the post-query $allocation->update([...]) call with
direct property assignment ($allocation->server_id = $server->id) followed by
$allocation->save() so the assignment happens while the row lock is held. Ensure
createNewAllocation()’s allocation creation/assignment (the code around lines
where it currently uses update()) is moved into the same DB::transaction() or
also wrapped similarly.

In `@app/Services/Databases/DatabaseManagementService.php`:
- Around line 145-155: Inside the transaction in DatabaseManagementService (the
closure using $database and $password), replace the unsafe call to
$database->sharedLock()->update([...]) with a per-model update — for example
call $database->update([...]) — so only the intended record has its password
changed; keep the rest of the chain
($database->dropUser()->createUser()->assignUserToDatabase()->flushPrivileges())
intact.

In `@app/Services/Subusers/SubuserDeletionService.php`:
- Around line 20-26: The job dispatch is happening inside the DB transaction
closure ($log->transaction) which can lead to SFTP revocation running even if
the transaction rolls back; update SubuserDeletionService to either move the
RevokeSftpAccessJob dispatch call out of the $log->transaction closure to run
after the transaction completes, or call ->afterCommit() on the job
(RevokeSftpAccessJob::dispatch(...)->afterCommit()) so the job only queues once
the deletion (and event dispatch of SubUserRemoved) is committed.

---

Nitpick comments:
In `@app/Events/User/PasswordChanged.php`:
- Around line 8-12: Add the SerializesModels trait to the PasswordChanged event
so the User model serializes correctly when queued; update the class to use both
Dispatchable and SerializesModels (i.e., add SerializesModels to the use clause
in class PasswordChanged) and ensure the SerializesModels symbol is imported so
the existing constructor public readonly User $user continues to work with
queued listeners.

In `@app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php`:
- Around line 100-103: The authorization in restore() currently compares
model->server->node via is(), add consistency with index() by comparing node IDs
directly: retrieve $node from $request->attributes, compare
$model->server->node_id !== $node->id (or cast both to int) and throw the same
HttpForbiddenException when they differ; update the check in the restore()
method to use this direct node_id comparison instead of ->is().

In `@app/Http/Middleware/SetSecurityHeaders.php`:
- Around line 20-25: Replace the mutable protected static array $headers in
class SetSecurityHeaders with a class constant (e.g. protected const HEADERS =
[...]) and update all usages (self::$headers or static::$headers) to reference
the constant (self::HEADERS or static::HEADERS); do the same refactor for the
other static array referenced around the same file (the one at the other
occurrence) so both immutable header maps become class constants to reflect
immutability.

In `@app/Jobs/RevokeSftpAccessJob.php`:
- Around line 23-25: The job's current configuration (public int $tries = 3;
public int $maxExceptions = 1;) plus the use of release() on ConnectionException
means releases consume $tries and can silently exhaust retries without any
exception being logged; in RevokeSftpAccessJob, either adjust
$tries/$maxExceptions to reflect desired behavior or implement a public function
failed(Throwable $exception = null) that logs/alerts when retries are exhausted
(include context like job id and ConnectionException details) so you get
visibility when release()-based retries hit the $tries limit rather than
silently failing; reference the class RevokeSftpAccessJob, the release() calls
for ConnectionException, and add/implement failed() to surface the
exhausted-retries condition.

In `@config/http.php`:
- Around line 14-20: The rate limits in the 'rate_limit' config (keys 'client'
and 'application' / env vars APP_API_CLIENT_RATELIMIT and
APP_API_APPLICATION_RATELIMIT) were temporarily bumped; add a short inline
comment or TODO next to these entries explaining this is a temporary increase
and reference an issue/ticket ID (or create one) so the change is revisited when
the underlying endpoint problems are fixed; ensure the comment mentions which
env vars control the values and the original limits for context.

In `@tests/Integration/Api/Remote/ServerTransferControllerTest.php`:
- Around line 37-102: Tests in ServerTransferControllerTest duplicate the
Authorization header string construction ("Bearer
{$node->daemon_token_id}.{$node->daemon_token}") across multiple withHeader
calls; add a private helper on the test class (e.g. authHeaderFor(Node $node):
array or authHeaderValue(Node $node): string) that returns the properly
formatted header and update all usages of withHeader('Authorization', "Bearer
...") to call that helper (refer to withHeader calls in
test_success_status_update_cannot_be_sent_from_old_node,
test_success_status_update_cannot_be_sent_from_unauthorized_node,
test_failure_status_update_cannot_be_sent_from_unauthorized_node, and other
tests using $oldNode/$newNode/$node and their daemon_token_id/daemon_token).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 773f00d5-d95e-4be9-8e42-742872db96af

📥 Commits

Reviewing files that changed from the base of the PR and between e01d9f2 and 77e94e3.

📒 Files selected for processing (38)
  • app/Events/User/Deleting.php
  • app/Events/User/PasswordChanged.php
  • app/Http/Controllers/Api/Client/AccountController.php
  • app/Http/Controllers/Api/Client/Servers/BackupController.php
  • app/Http/Controllers/Api/Client/Servers/DatabaseController.php
  • app/Http/Controllers/Api/Client/Servers/StartupController.php
  • app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php
  • app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php
  • app/Http/Middleware/SetSecurityHeaders.php
  • app/Jobs/Job.php
  • app/Jobs/RevokeSftpAccessJob.php
  • app/Jobs/Schedule/RunTaskJob.php
  • app/Listeners/Auth/PasswordResetListener.php
  • app/Listeners/AuthenticationListener.php
  • app/Listeners/RevocationListener.php
  • app/Listeners/TwoFactorListener.php
  • app/Models/ServerTransfer.php
  • app/Models/User.php
  • app/Providers/Filament/PanelProvider.php
  • app/Repositories/Daemon/DaemonServerRepository.php
  • app/Services/Allocations/FindAssignableAllocationService.php
  • app/Services/Databases/DatabaseManagementService.php
  • app/Services/Servers/DetailsModificationService.php
  • app/Services/Subusers/SubuserDeletionService.php
  • app/Services/Subusers/SubuserUpdateService.php
  • app/Services/Users/UserUpdateService.php
  • bootstrap/app.php
  • config/http.php
  • database/Factories/ServerTransferFactory.php
  • routes/api-client.php
  • routes/api-remote.php
  • tests/Integration/Api/Client/AccountControllerTest.php
  • tests/Integration/Api/Client/Server/Subuser/DeleteSubuserTest.php
  • tests/Integration/Api/Client/Server/Subuser/SubuserAuthorizationTest.php
  • tests/Integration/Api/Client/Server/Subuser/UpdateSubuserTest.php
  • tests/Integration/Api/Remote/ServerTransferControllerTest.php
  • tests/Integration/Jobs/RevokeSftpAccessJobTest .php
  • tests/Integration/Services/Users/UserDeletionServiceTest.php
💤 Files with no reviewable changes (3)
  • app/Jobs/Job.php
  • app/Listeners/Auth/PasswordResetListener.php
  • routes/api-remote.php

Comment thread app/Listeners/AuthenticationListener.php Outdated
Comment thread app/Services/Servers/DetailsModificationService.php Outdated
Comment thread app/Services/Subusers/SubuserUpdateService.php
Comment thread database/Factories/ServerTransferFactory.php Outdated
Comment thread tests/Integration/Api/Client/Server/Subuser/DeleteSubuserTest.php Outdated
Comment thread tests/Integration/Api/Client/Server/Subuser/UpdateSubuserTest.php Outdated
Comment thread tests/Integration/Jobs/RevokeSftpAccessJobTest.php
Comment thread tests/Integration/Services/Users/UserDeletionServiceTest.php
Copy link
Copy Markdown

@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.

Caution

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

⚠️ Outside diff range comments (1)
app/Http/Requests/Api/Remote/InstallationDataRequest.php (1)

5-13: ⚠️ Potential issue | 🟠 Major

rules() will not run when extending Illuminate\Http\Request

On Line 7, this class extends Illuminate\Http\Request, but the rules() method on Line 12 is auto-applied only through FormRequest resolution. This allows successful/reinstall to bypass validation in ServerInstallController::store() (line 49, 53).

Proposed fix
-use Illuminate\Http\Request;
+use Illuminate\Foundation\Http\FormRequest;
 
-class InstallationDataRequest extends Request
+class InstallationDataRequest extends FormRequest
 {
+    public function authorize(): bool
+    {
+        return true;
+    }
+
     /**
      * `@return` array<string, string|string[]>
      */
     public function rules(): array
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Http/Requests/Api/Remote/InstallationDataRequest.php` around lines 5 -
13, The InstallationDataRequest class currently extends Illuminate\Http\Request
so its rules() aren't applied; change the base class to
Illuminate\Foundation\Http\FormRequest (or import and extend FormRequest) so the
rules() method is used by Laravel's validation pipeline; update the class
declaration in InstallationDataRequest and ensure any custom authorize() or
validation logic is preserved/added to the FormRequest implementation so
ServerInstallController::store()'s successful/reinstall flows run validation.
🧹 Nitpick comments (2)
app/Http/Controllers/Api/Remote/Servers/ServerInstallController.php (1)

21-34: Authorization check should precede relationship loading.

The $egg = $server->egg assignment on line 23 triggers a database query before the authorization check on lines 25-27. If the request is unauthorized, this query is wasted.

♻️ Proposed reordering
 public function index(Request $request, Server $server): JsonResponse
 {
-    $egg = $server->egg;
-
     if (!$server->node->is($request->attributes->get('node'))) {
         throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
     }

+    $egg = $server->egg;
+
     return new JsonResponse([
         'container_image' => $egg->copy_script_container,
         'entrypoint' => $egg->copy_script_entry,
         'script' => $egg->copy_script_install,
     ]);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Http/Controllers/Api/Remote/Servers/ServerInstallController.php` around
lines 21 - 34, Move the authorization check to before loading related models to
avoid unnecessary queries: in the ServerInstallController::index method, perform
the node permission check (the if (!
$server->node->is($request->attributes->get('node'))) throw
HttpForbiddenException) before accessing $server->egg, then only assign $egg =
$server->egg and return the JsonResponse if the check passes.
app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php (1)

42-49: Slight inconsistency with other controllers using Assert::isInstanceOf.

This controller uses Assert::isInstanceOf for node validation while ServerContainersController and ServerInstallController directly use the node from attributes without assertion. While Assert::isInstanceOf provides defense-in-depth, it throws InvalidArgumentException rather than an HTTP exception if the middleware somehow fails to set the node attribute.

Given that DaemonAuthenticate middleware guarantees the node attribute is set (context snippet 1), this is a minor inconsistency rather than a bug. The authorization logic itself is correct—allowing either node to report transfer failure is appropriate since both nodes are aware of the transfer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php` around
lines 42 - 49, The code in ServerTransferController uses
Assert::isInstanceOf($node = $request->attributes->get('node'), Node::class)
which is inconsistent with ServerContainersController and
ServerInstallController; remove the Assert::isInstanceOf check and directly
retrieve the node from $request->attributes->get('node') (trusting
DaemonAuthenticate middleware) and keep the subsequent authorization logic that
compares $node->is($transfer->newNode) and $node->is($transfer->oldNode); this
aligns behavior with other controllers and avoids throwing an
InvalidArgumentException from Assert::isInstanceOf.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@app/Http/Requests/Api/Remote/InstallationDataRequest.php`:
- Around line 5-13: The InstallationDataRequest class currently extends
Illuminate\Http\Request so its rules() aren't applied; change the base class to
Illuminate\Foundation\Http\FormRequest (or import and extend FormRequest) so the
rules() method is used by Laravel's validation pipeline; update the class
declaration in InstallationDataRequest and ensure any custom authorize() or
validation logic is preserved/added to the FormRequest implementation so
ServerInstallController::store()'s successful/reinstall flows run validation.

---

Nitpick comments:
In `@app/Http/Controllers/Api/Remote/Servers/ServerInstallController.php`:
- Around line 21-34: Move the authorization check to before loading related
models to avoid unnecessary queries: in the ServerInstallController::index
method, perform the node permission check (the if (!
$server->node->is($request->attributes->get('node'))) throw
HttpForbiddenException) before accessing $server->egg, then only assign $egg =
$server->egg and return the JsonResponse if the check passes.

In `@app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php`:
- Around line 42-49: The code in ServerTransferController uses
Assert::isInstanceOf($node = $request->attributes->get('node'), Node::class)
which is inconsistent with ServerContainersController and
ServerInstallController; remove the Assert::isInstanceOf check and directly
retrieve the node from $request->attributes->get('node') (trusting
DaemonAuthenticate middleware) and keep the subsequent authorization logic that
compares $node->is($transfer->newNode) and $node->is($transfer->oldNode); this
aligns behavior with other controllers and avoids throwing an
InvalidArgumentException from Assert::isInstanceOf.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d2a128e2-78da-4203-8c3f-28547690f3e4

📥 Commits

Reviewing files that changed from the base of the PR and between 77e94e3 and e8d5ec3.

📒 Files selected for processing (8)
  • app/Http/Controllers/Api/Remote/Servers/ServerContainersController.php
  • app/Http/Controllers/Api/Remote/Servers/ServerDetailsController.php
  • app/Http/Controllers/Api/Remote/Servers/ServerInstallController.php
  • app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php
  • app/Http/Requests/Api/Remote/InstallationDataRequest.php
  • app/Http/Requests/Api/Remote/ServerRequest.php
  • app/Listeners/Auth/PasswordResetListener.php
  • app/Listeners/RevocationListener.php
💤 Files with no reviewable changes (1)
  • app/Http/Requests/Api/Remote/ServerRequest.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/Listeners/Auth/PasswordResetListener.php
  • app/Listeners/RevocationListener.php

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
app/Http/Controllers/Api/Remote/Servers/ServerInstallController.php (1)

23-25: Consider extracting node authorization into a private helper.

The same guard logic is duplicated in two methods; centralizing it will reduce future drift.

♻️ Optional refactor
 class ServerInstallController extends Controller
 {
+    private function ensureNodeCanAccessServer(Request $request, Server $server): void
+    {
+        if (!$server->node->is($request->attributes->get('node'))) {
+            throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
+        }
+    }
+
     public function index(Request $request, Server $server): JsonResponse
     {
-        if (!$server->node->is($request->attributes->get('node'))) {
-            throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
-        }
+        $this->ensureNodeCanAccessServer($request, $server);
 
         $egg = $server->egg;
@@
     public function store(InstallationDataRequest $request, Server $server): JsonResponse
     {
         $status = null;
 
-        if (!$server->node->is($request->attributes->get('node'))) {
-            throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
-        }
+        $this->ensureNodeCanAccessServer($request, $server);

Also applies to: 45-47

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Http/Controllers/Api/Remote/Servers/ServerInstallController.php` around
lines 23 - 25, Extract the duplicated node-authorization guard into a private
helper on ServerInstallController (e.g., private function
assertNodeAuthorized(Server $server, Request $request) or ensureNodeMatches),
move the existing check (if
(!$server->node->is($request->attributes->get('node'))) throw new
HttpForbiddenException(...)) into that helper, and replace the two inline guards
in the controller methods with a call to this helper; ensure the helper uses the
same exception message and types so behavior remains identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/Http/Controllers/Api/Remote/Servers/ServerInstallController.php`:
- Around line 23-25: Extract the duplicated node-authorization guard into a
private helper on ServerInstallController (e.g., private function
assertNodeAuthorized(Server $server, Request $request) or ensureNodeMatches),
move the existing check (if
(!$server->node->is($request->attributes->get('node'))) throw new
HttpForbiddenException(...)) into that helper, and replace the two inline guards
in the controller methods with a call to this helper; ensure the helper uses the
same exception message and types so behavior remains identical.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: da17e9ba-8d26-47bc-b4fb-c8b84ab27d74

📥 Commits

Reviewing files that changed from the base of the PR and between 8d86944 and a463277.

📒 Files selected for processing (3)
  • app/Http/Controllers/Api/Remote/Servers/ServerInstallController.php
  • app/Http/Requests/Api/Remote/InstallationDataRequest.php
  • tests/Integration/Jobs/RevokeSftpAccessJobTest.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/Http/Requests/Api/Remote/InstallationDataRequest.php
  • tests/Integration/Jobs/RevokeSftpAccessJobTest.php

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.

1 participant