fix(settings): fix 404 on /settings for root user on cloud (#7785)

This commit is contained in:
Andras Bacsai
2026-01-02 13:18:07 +01:00
committed by GitHub
11 changed files with 97 additions and 45 deletions

View File

@@ -218,7 +218,10 @@ class TeamController extends Controller
if (is_null($teamId)) { if (is_null($teamId)) {
return invalidTokenResponse(); return invalidTokenResponse();
} }
$team = auth()->user()->currentTeam(); $team = auth()->user()->teams->where('id', $teamId)->first();
if (is_null($team)) {
return response()->json(['message' => 'Team not found.'], 404);
}
return response()->json( return response()->json(
$this->removeSensitiveData($team), $this->removeSensitiveData($team),
@@ -263,7 +266,10 @@ class TeamController extends Controller
if (is_null($teamId)) { if (is_null($teamId)) {
return invalidTokenResponse(); return invalidTokenResponse();
} }
$team = auth()->user()->currentTeam(); $team = auth()->user()->teams->where('id', $teamId)->first();
if (is_null($team)) {
return response()->json(['message' => 'Team not found.'], 404);
}
$team->members->makeHidden([ $team->members->makeHidden([
'pivot', 'pivot',
'email_change_code', 'email_change_code',

View File

@@ -18,14 +18,21 @@ class DecideWhatToDoWithUser
} }
if (auth()?->user()?->currentTeam()) { if (auth()?->user()?->currentTeam()) {
refreshSession(auth()->user()->currentTeam()); refreshSession(auth()->user()->currentTeam());
} elseif (auth()?->user()?->teams?->count() > 0) {
// User's session team is invalid (e.g., removed from team), switch to first available team
refreshSession(auth()->user()->teams->first());
} }
if (! auth()->user() || ! isCloud() || isInstanceAdmin()) { if (! auth()->user() || ! isCloud()) {
if (! isCloud() && showBoarding() && ! in_array($request->path(), allowedPathsForBoardingAccounts())) { if (! isCloud() && showBoarding() && ! in_array($request->path(), allowedPathsForBoardingAccounts())) {
return redirect()->route('onboarding'); return redirect()->route('onboarding');
} }
return $next($request); return $next($request);
} }
// Instance admins can access settings and admin routes regardless of subscription
if (isInstanceAdmin() && ($request->routeIs('settings.*') || $request->path() === 'admin')) {
return $next($request);
}
if (! auth()->user()->hasVerifiedEmail()) { if (! auth()->user()->hasVerifiedEmail()) {
if ($request->path() === 'verify' || in_array($request->path(), allowedPathsForInvalidAccounts()) || $request->routeIs('verify.verify')) { if ($request->path() === 'verify' || in_array($request->path(), allowedPathsForInvalidAccounts()) || $request->routeIs('verify.verify')) {
return $next($request); return $next($request);

View File

@@ -79,8 +79,10 @@ class ActivityMonitor extends Component
$causer_id = data_get($this->activity, 'causer_id'); $causer_id = data_get($this->activity, 'causer_id');
$user = User::find($causer_id); $user = User::find($causer_id);
if ($user) { if ($user) {
$teamId = $user->currentTeam()->id; $teamId = data_get($this->activity, 'properties.team_id')
if (! self::$eventDispatched) { ?? $user->currentTeam()?->id
?? $user->teams->first()?->id;
if ($teamId && ! self::$eventDispatched) {
if (filled($this->eventData)) { if (filled($this->eventData)) {
$this->eventToDispatch::dispatch($teamId, $this->eventData); $this->eventToDispatch::dispatch($teamId, $this->eventData);
} else { } else {

View File

@@ -3,16 +3,12 @@
namespace App\Livewire\Settings; namespace App\Livewire\Settings;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Server;
use App\Rules\ValidIpOrCidr; use App\Rules\ValidIpOrCidr;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
class Advanced extends Component class Advanced extends Component
{ {
#[Validate('required')]
public Server $server;
public InstanceSettings $settings; public InstanceSettings $settings;
#[Validate('boolean')] #[Validate('boolean')]
@@ -44,7 +40,6 @@ class Advanced extends Component
public function rules() public function rules()
{ {
return [ return [
'server' => 'required',
'is_registration_enabled' => 'boolean', 'is_registration_enabled' => 'boolean',
'do_not_track' => 'boolean', 'do_not_track' => 'boolean',
'is_dns_validation_enabled' => 'boolean', 'is_dns_validation_enabled' => 'boolean',
@@ -62,7 +57,6 @@ class Advanced extends Component
if (! isInstanceAdmin()) { if (! isInstanceAdmin()) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$this->server = Server::findOrFail(0);
$this->settings = instanceSettings(); $this->settings = instanceSettings();
$this->custom_dns_servers = $this->settings->custom_dns_servers; $this->custom_dns_servers = $this->settings->custom_dns_servers;
$this->allowed_ips = $this->settings->allowed_ips; $this->allowed_ips = $this->settings->allowed_ips;

View File

@@ -12,7 +12,7 @@ class Index extends Component
{ {
public InstanceSettings $settings; public InstanceSettings $settings;
public Server $server; public ?Server $server = null;
#[Validate('nullable|string|max:255')] #[Validate('nullable|string|max:255')]
public ?string $fqdn = null; public ?string $fqdn = null;
@@ -57,7 +57,9 @@ class Index extends Component
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$this->settings = instanceSettings(); $this->settings = instanceSettings();
$this->server = Server::findOrFail(0); if (! isCloud()) {
$this->server = Server::findOrFail(0);
}
$this->fqdn = $this->settings->fqdn; $this->fqdn = $this->settings->fqdn;
$this->public_port_min = $this->settings->public_port_min; $this->public_port_min = $this->settings->public_port_min;
$this->public_port_max = $this->settings->public_port_max; $this->public_port_max = $this->settings->public_port_max;
@@ -127,7 +129,7 @@ class Index extends Component
$this->validate(); $this->validate();
if ($this->settings->is_dns_validation_enabled && $this->fqdn) { if ($this->settings->is_dns_validation_enabled && $this->fqdn && $this->server) {
if (! validateDNSEntry($this->fqdn, $this->server)) { if (! validateDNSEntry($this->fqdn, $this->server)) {
$this->dispatch('error', "Validating DNS failed.<br><br>Make sure you have added the DNS records correctly.<br><br>{$this->fqdn}->{$this->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help."); $this->dispatch('error', "Validating DNS failed.<br><br>Make sure you have added the DNS records correctly.<br><br>{$this->fqdn}->{$this->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
$error_show = true; $error_show = true;
@@ -151,7 +153,9 @@ class Index extends Component
$this->instantSave(isSave: false); $this->instantSave(isSave: false);
$this->settings->save(); $this->settings->save();
$this->server->setupDynamicProxyConfiguration(); if ($this->server) {
$this->server->setupDynamicProxyConfiguration();
}
if (! $error_show) { if (! $error_show) {
$this->dispatch('success', 'Instance settings updated successfully!'); $this->dispatch('success', 'Instance settings updated successfully!');
} }
@@ -169,6 +173,12 @@ class Index extends Component
return; return;
} }
if (! $this->server) {
$this->dispatch('error', 'Server not available.');
return;
}
$version = $this->dev_helper_version ?: config('constants.coolify.helper_version'); $version = $this->dev_helper_version ?: config('constants.coolify.helper_version');
if (empty($version)) { if (empty($version)) {
$this->dispatch('error', 'Please specify a version to build.'); $this->dispatch('error', 'Please specify a version to build.');

View File

@@ -12,7 +12,7 @@ class Updates extends Component
{ {
public InstanceSettings $settings; public InstanceSettings $settings;
public Server $server; public ?Server $server = null;
#[Validate('string')] #[Validate('string')]
public string $auto_update_frequency; public string $auto_update_frequency;
@@ -25,7 +25,9 @@ class Updates extends Component
public function mount() public function mount()
{ {
$this->server = Server::findOrFail(0); if (! isCloud()) {
$this->server = Server::findOrFail(0);
}
$this->settings = instanceSettings(); $this->settings = instanceSettings();
$this->auto_update_frequency = $this->settings->auto_update_frequency; $this->auto_update_frequency = $this->settings->auto_update_frequency;
@@ -76,7 +78,9 @@ class Updates extends Component
} }
$this->instantSave(); $this->instantSave();
$this->server->setupDynamicProxyConfiguration(); if ($this->server) {
$this->server->setupDynamicProxyConfiguration();
}
} catch (\Exception $e) { } catch (\Exception $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -48,7 +48,7 @@ class InviteLink extends Component
// Prevent privilege escalation: users cannot invite someone with higher privileges // Prevent privilege escalation: users cannot invite someone with higher privileges
$userRole = auth()->user()->role(); $userRole = auth()->user()->role();
if ($userRole === 'member' && in_array($this->role, ['admin', 'owner'])) { if (is_null($userRole) || ($userRole === 'member' && in_array($this->role, ['admin', 'owner']))) {
throw new \Exception('Members cannot invite admins or owners.'); throw new \Exception('Members cannot invite admins or owners.');
} }
if ($userRole === 'admin' && $this->role === 'owner') { if ($userRole === 'admin' && $this->role === 'owner') {

View File

@@ -71,11 +71,11 @@ class Member extends Component
|| Role::from($this->getMemberRole())->gt(auth()->user()->role())) { || Role::from($this->getMemberRole())->gt(auth()->user()->role())) {
throw new \Exception('You are not authorized to perform this action.'); throw new \Exception('You are not authorized to perform this action.');
} }
$teamId = currentTeam()->id;
$this->member->teams()->detach(currentTeam()); $this->member->teams()->detach(currentTeam());
// Clear cache for the removed user - both old and new key formats
Cache::forget("team:{$this->member->id}"); Cache::forget("team:{$this->member->id}");
Cache::remember('team:'.$this->member->id, 3600, function () { Cache::forget("user:{$this->member->id}:team:{$teamId}");
return $this->member->teams()->first();
});
$this->dispatch('reloadWindow'); $this->dispatch('reloadWindow');
} catch (\Exception $e) { } catch (\Exception $e) {
$this->dispatch('error', $e->getMessage()); $this->dispatch('error', $e->getMessage());

View File

@@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Jobs\UpdateStripeCustomerEmailJob;
use App\Notifications\Channels\SendsEmail; use App\Notifications\Channels\SendsEmail;
use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword; use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword;
use App\Traits\DeletesUserSessions; use App\Traits\DeletesUserSessions;
@@ -295,9 +296,10 @@ class User extends Authenticatable implements SendsEmail
public function isInstanceAdmin() public function isInstanceAdmin()
{ {
$found_root_team = Auth::user()->teams->filter(function ($team) { $found_root_team = $this->teams->filter(function ($team) {
if ($team->id == 0) { if ($team->id == 0) {
if (! Auth::user()->isAdmin()) { $role = $team->pivot->role;
if ($role !== 'admin' && $role !== 'owner') {
return false; return false;
} }
@@ -310,32 +312,51 @@ class User extends Authenticatable implements SendsEmail
return $found_root_team->count() > 0; return $found_root_team->count() > 0;
} }
public function currentTeam() public function currentTeam(): ?Team
{ {
return Cache::remember('team:'.Auth::id(), 3600, function () { $sessionTeamId = data_get(session('currentTeam'), 'id');
if (is_null(data_get(session('currentTeam'), 'id')) && Auth::user()->teams->count() > 0) {
return Auth::user()->teams[0];
}
return Team::find(session('currentTeam')->id); if (is_null($sessionTeamId)) {
return null;
}
// Check if user actually belongs to this team
if (! $this->teams->contains('id', $sessionTeamId)) {
session()->forget('currentTeam');
Cache::forget('user:'.$this->id.':team:'.$sessionTeamId);
return null;
}
return Cache::remember('user:'.$this->id.':team:'.$sessionTeamId, 3600, function () use ($sessionTeamId) {
return Team::find($sessionTeamId);
}); });
} }
public function otherTeams() public function role(): ?string
{
return Auth::user()->teams->filter(function ($team) {
return $team->id != currentTeam()->id;
});
}
public function role()
{ {
if (data_get($this, 'pivot')) { if (data_get($this, 'pivot')) {
return $this->pivot->role; return $this->pivot->role;
} }
$user = Auth::user()->teams->where('id', currentTeam()->id)->first();
return data_get($user, 'pivot.role'); $current = $this->currentTeam();
if (is_null($current)) {
return null;
}
$team = $this->teams->where('id', $current->id)->first();
return data_get($team, 'pivot.role');
}
/**
* Get the user's role in a specific team
*/
public function roleInTeam(int $teamId): ?string
{
$team = $this->teams->where('id', $teamId)->first();
return data_get($team, 'pivot.role');
} }
/** /**
@@ -415,9 +436,10 @@ class User extends Authenticatable implements SendsEmail
]); ]);
// For cloud users, dispatch job to update Stripe customer email asynchronously // For cloud users, dispatch job to update Stripe customer email asynchronously
if (isCloud() && $this->currentTeam()->subscription) { $currentTeam = $this->currentTeam();
dispatch(new \App\Jobs\UpdateStripeCustomerEmailJob( if (isCloud() && $currentTeam?->subscription) {
$this->currentTeam(), dispatch(new UpdateStripeCustomerEmailJob(
$currentTeam,
$this->id, $this->id,
$newEmail, $newEmail,
$oldEmail $oldEmail

View File

@@ -182,8 +182,11 @@ function refreshSession(?Team $team = null): void
$team = User::find(Auth::id())->teams->first(); $team = User::find(Auth::id())->teams->first();
} }
} }
// Clear old cache key format for backwards compatibility
Cache::forget('team:'.Auth::id()); Cache::forget('team:'.Auth::id());
Cache::remember('team:'.Auth::id(), 3600, function () use ($team) { // Use new cache key format that includes team ID
Cache::forget('user:'.Auth::id().':team:'.$team->id);
Cache::remember('user:'.Auth::id().':team:'.$team->id, 3600, function () use ($team) {
return $team; return $team;
}); });
session(['currentTeam' => $team]); session(['currentTeam' => $team]);
@@ -384,7 +387,7 @@ function base_url(bool $withPort = true): string
function isSubscribed() function isSubscribed()
{ {
return isSubscriptionActive() || auth()->user()->isInstanceAdmin(); return isSubscriptionActive();
} }
function isProduction(): bool function isProduction(): bool

View File

@@ -13,6 +13,10 @@ function isSubscriptionActive()
if (! $team) { if (! $team) {
return false; return false;
} }
// Root team (id=0) doesn't require subscription
if ($team->id === 0) {
return true;
}
$subscription = $team?->subscription; $subscription = $team?->subscription;
if (is_null($subscription)) { if (is_null($subscription)) {