diff --git a/app/Console/Commands/ClearGlobalSearchCache.php b/app/Console/Commands/ClearGlobalSearchCache.php index a368b0badc8..e4853ccec54 100644 --- a/app/Console/Commands/ClearGlobalSearchCache.php +++ b/app/Console/Commands/ClearGlobalSearchCache.php @@ -39,7 +39,14 @@ public function handle(): int return Command::FAILURE; } - $teamId = auth()->user()->currentTeam()->id; + $user = auth()->user(); + $teamId = $user?->currentTeam()?->id; + + if (! $teamId) { + $this->error('Current user has no team assigned. Use --team=ID or --all option.'); + + return Command::FAILURE; + } return $this->clearTeamCache($teamId); } diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php index e12d83542c4..0bac5250c5d 100644 --- a/app/Http/Controllers/Api/TeamController.php +++ b/app/Http/Controllers/Api/TeamController.php @@ -212,13 +212,17 @@ public function members_by_id(Request $request) ), ] )] - public function current_team(Request $request) + public function current_team(Request $request): \Illuminate\Http\JsonResponse { $teamId = getTeamIdFromToken(); if (is_null($teamId)) { return invalidTokenResponse(); } - $team = auth()->user()->currentTeam(); + $user = auth()->user(); + $team = $user?->currentTeam(); + if (is_null($team)) { + return response()->json(['message' => 'No team assigned to user.'], 404); + } return response()->json( $this->removeSensitiveData($team), @@ -263,7 +267,11 @@ public function current_team_members(Request $request) if (is_null($teamId)) { return invalidTokenResponse(); } - $team = auth()->user()->currentTeam(); + $user = auth()->user(); + $team = $user?->currentTeam(); + if (is_null($team)) { + return response()->json(['message' => 'No team assigned to user.'], 404); + } $team->members->makeHidden([ 'pivot', 'email_change_code', diff --git a/app/Http/Controllers/MagicController.php b/app/Http/Controllers/MagicController.php index 59c9b8b94e1..3ac2a7e8df8 100644 --- a/app/Http/Controllers/MagicController.php +++ b/app/Http/Controllers/MagicController.php @@ -46,9 +46,16 @@ public function environments() public function newProject() { + $team = currentTeam(); + if (! $team) { + return response()->json([ + 'message' => 'No team assigned to user.', + ], 404); + } + $project = Project::firstOrCreate( ['name' => request()->query('name') ?? generate_random_name()], - ['team_id' => currentTeam()->id] + ['team_id' => $team->id] ); return response()->json([ @@ -70,13 +77,20 @@ public function newEnvironment() public function newTeam() { + $user = auth()->user(); + if (! $user) { + return response()->json([ + 'message' => 'Unauthenticated.', + ], 401); + } + $team = Team::create( [ 'name' => request()->query('name') ?? generate_random_name(), 'personal_team' => false, ], ); - auth()->user()->teams()->attach($team, ['role' => 'admin']); + $user->teams()->attach($team, ['role' => 'admin']); refreshSession(); return redirect(request()->header('Referer')); diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php index e1dd678ff88..7af70a563ff 100644 --- a/app/Livewire/GlobalSearch.php +++ b/app/Livewire/GlobalSearch.php @@ -248,12 +248,23 @@ private function canCreateResource(string $type): bool private function loadSearchableItems() { // Try to get from Redis cache first - $cacheKey = self::getCacheKey(auth()->user()->currentTeam()->id); + $user = auth()->user(); + if (! $user) { + $this->allSearchableItems = []; + + return; + } + $team = $user->currentTeam(); + if (! $team) { + $this->allSearchableItems = []; + + return; + } + $cacheKey = self::getCacheKey($team->id); - $this->allSearchableItems = Cache::remember($cacheKey, 300, function () { + $this->allSearchableItems = Cache::remember($cacheKey, 300, function () use ($team) { ray()->showQueries(); $items = collect(); - $team = auth()->user()->currentTeam(); // Get all applications $applications = Application::ownedByCurrentTeam() @@ -1232,7 +1243,12 @@ public function loadProjects() { $this->loadingProjects = true; $user = auth()->user(); - $team = $user->currentTeam(); + $team = $user?->currentTeam(); + if (! $team) { + $this->loadingProjects = false; + + return $this->dispatch('error', message: 'No team assigned to user'); + } $projects = Project::where('team_id', $team->id)->get(); if ($projects->isEmpty()) { diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php index b914fbd9458..e5c5ebc0a47 100644 --- a/app/Livewire/Notifications/Discord.php +++ b/app/Livewire/Notifications/Discord.php @@ -71,7 +71,11 @@ class Discord extends Component public function mount() { try { - $this->team = auth()->user()->currentTeam(); + $user = auth()->user(); + $this->team = $user?->currentTeam(); + if (! $this->team) { + return handleError(new \Exception('Team not found.'), $this); + } $this->settings = $this->team->discordNotificationSettings; $this->authorize('view', $this->settings); $this->syncData(); diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index 847f1076568..c26fd63067c 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -113,12 +113,19 @@ class Email extends Component public function mount() { try { - $this->team = auth()->user()->currentTeam(); - $this->emails = auth()->user()->email; + $user = auth()->user(); + if (! $user) { + return handleError(new \Exception('User not authenticated.'), $this); + } + $this->team = $user->currentTeam(); + if (! $this->team) { + return handleError(new \Exception('Team not found.'), $this); + } + $this->emails = $user->email; $this->settings = $this->team->emailNotificationSettings; $this->authorize('view', $this->settings); $this->syncData(); - $this->testEmailAddress = auth()->user()->email; + $this->testEmailAddress = $user->email; } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Notifications/Pushover.php b/app/Livewire/Notifications/Pushover.php index d79eea87be0..2f0afdbc546 100644 --- a/app/Livewire/Notifications/Pushover.php +++ b/app/Livewire/Notifications/Pushover.php @@ -76,7 +76,11 @@ class Pushover extends Component public function mount() { try { - $this->team = auth()->user()->currentTeam(); + $user = auth()->user(); + $this->team = $user?->currentTeam(); + if (! $this->team) { + return handleError(new \Exception('Team not found.'), $this); + } $this->settings = $this->team->pushoverNotificationSettings; $this->authorize('view', $this->settings); $this->syncData(); diff --git a/app/Livewire/Notifications/Slack.php b/app/Livewire/Notifications/Slack.php index fa8c97ae90b..ce79dccc4f3 100644 --- a/app/Livewire/Notifications/Slack.php +++ b/app/Livewire/Notifications/Slack.php @@ -73,7 +73,11 @@ class Slack extends Component public function mount() { try { - $this->team = auth()->user()->currentTeam(); + $user = auth()->user(); + $this->team = $user?->currentTeam(); + if (! $this->team) { + return handleError(new \Exception('Team not found.'), $this); + } $this->settings = $this->team->slackNotificationSettings; $this->authorize('view', $this->settings); $this->syncData(); diff --git a/app/Livewire/Notifications/Telegram.php b/app/Livewire/Notifications/Telegram.php index fc3966cf6c0..cb1d7eef2da 100644 --- a/app/Livewire/Notifications/Telegram.php +++ b/app/Livewire/Notifications/Telegram.php @@ -118,7 +118,11 @@ class Telegram extends Component public function mount() { try { - $this->team = auth()->user()->currentTeam(); + $user = auth()->user(); + $this->team = $user?->currentTeam(); + if (! $this->team) { + return handleError(new \Exception('Team not found.'), $this); + } $this->settings = $this->team->telegramNotificationSettings; $this->authorize('view', $this->settings); $this->syncData(); diff --git a/app/Livewire/Notifications/Webhook.php b/app/Livewire/Notifications/Webhook.php index 8af70c6eb79..ecf794edfc2 100644 --- a/app/Livewire/Notifications/Webhook.php +++ b/app/Livewire/Notifications/Webhook.php @@ -68,7 +68,11 @@ class Webhook extends Component public function mount() { try { - $this->team = auth()->user()->currentTeam(); + $user = auth()->user(); + $this->team = $user?->currentTeam(); + if (! $this->team) { + return handleError(new \Exception('Team not found.'), $this); + } $this->settings = $this->team->webhookNotificationSettings; $this->authorize('view', $this->settings); $this->syncData(); diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index ca48e9b167e..05e44545f70 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -61,10 +61,17 @@ public function mount() if (isInstanceAdmin() === false) { return redirect()->route('dashboard'); } + $user = auth()->user(); + if (! $user) { + return redirect()->route('login'); + } $this->settings = instanceSettings(); $this->syncData(); - $this->team = auth()->user()->currentTeam(); - $this->testEmailAddress = auth()->user()->email; + $this->team = $user->currentTeam(); + if (! $this->team) { + return redirect()->route('dashboard'); + } + $this->testEmailAddress = $user->email; } public function syncData(bool $toModel = false) diff --git a/app/Models/User.php b/app/Models/User.php index f04b6fa7703..a99563cc836 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -220,6 +220,34 @@ public function teams() return $this->belongsToMany(Team::class)->withPivot('role'); } + public function organizations() + { + return $this->belongsToMany(Organization::class, 'organization_users') + ->using(OrganizationUser::class) + ->withPivot('role', 'permissions', 'is_active') + ->withTimestamps(); + } + + public function currentOrganization() + { + return $this->belongsTo(Organization::class, 'current_organization_id'); + } + + public function canPerformAction($action, $resource = null) + { + $organization = $this->currentOrganization; + if (! $organization) { + return false; + } + + return $organization->canUserPerformAction($this, $action, $resource); + } + + public function hasLicenseFeature($feature) + { + return $this->currentOrganization?->activeLicense?->hasFeature($feature) ?? false; + } + public function changelogReads() { return $this->hasMany(UserChangelogRead::class); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 1b23247faf7..95131947c0e 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -175,8 +175,9 @@ function showBoarding(): bool function refreshSession(?Team $team = null): void { if (! $team) { - if (Auth::user()->currentTeam()) { - $team = Team::find(Auth::user()->currentTeam()->id); + $currentTeam = Auth::user()?->currentTeam(); + if ($currentTeam) { + $team = Team::find($currentTeam->id); } else { $team = User::find(Auth::id())->teams->first(); } @@ -3139,6 +3140,204 @@ function parseDockerfileInterval(string $something) return $seconds; } +/** + * Check if the current organization has a valid license for a specific feature + */ +function hasLicenseFeature(string $feature): bool +{ + $user = Auth::user(); + if (! $user) { + return false; + } + + $organization = $user->currentOrganization ?? $user->organizations()->first(); + if (! $organization) { + return false; + } + + return $organization->hasFeature($feature); +} + +/** + * Check if the current organization can provision a specific resource type + */ +function canProvisionResource(string $resourceType): bool +{ + $user = Auth::user(); + if (! $user) { + return false; + } + + $organization = $user->currentOrganization ?? $user->organizations()->first(); + if (! $organization) { + return false; + } + + $license = $organization->activeLicense; + if (! $license) { + return false; + } + + // Check feature availability + $featureMap = [ + 'servers' => 'server_management', + 'applications' => 'application_deployment', + 'domains' => 'domain_management', + 'cloud_providers' => 'cloud_provisioning', + ]; + + $requiredFeature = $featureMap[$resourceType] ?? null; + if ($requiredFeature && ! $license->hasFeature($requiredFeature)) { + return false; + } + + // Check usage limits + return $organization->isWithinLimits(); +} + +/** + * Get the current organization's license tier + */ +function getCurrentLicenseTier(): ?string +{ + $user = Auth::user(); + if (! $user) { + return null; + } + + $organization = $user->currentOrganization ?? $user->organizations()->first(); + if (! $organization) { + return null; + } + + return $organization->activeLicense?->license_tier; +} + +/** + * Check if a deployment option is available in the current license + */ +function isDeploymentOptionAvailable(string $option): bool +{ + $licenseTier = getCurrentLicenseTier(); + if (! $licenseTier) { + return false; + } + + $tierOptions = [ + 'basic' => [ + 'docker_deployment', + 'basic_monitoring', + 'manual_scaling', + ], + 'professional' => [ + 'docker_deployment', + 'basic_monitoring', + 'manual_scaling', + 'advanced_monitoring', + 'blue_green_deployment', + 'auto_scaling', + 'backup_management', + 'force_rebuild', + 'instant_deployment', + ], + 'enterprise' => [ + 'docker_deployment', + 'basic_monitoring', + 'manual_scaling', + 'advanced_monitoring', + 'blue_green_deployment', + 'auto_scaling', + 'backup_management', + 'force_rebuild', + 'instant_deployment', + 'multi_region_deployment', + 'advanced_security', + 'compliance_reporting', + 'custom_integrations', + 'canary_deployment', + 'rollback_automation', + ], + ]; + + $availableOptions = $tierOptions[$licenseTier] ?? []; + + return in_array($option, $availableOptions); +} + +/** + * Get license-based resource limits for the current organization + */ +function getResourceLimits(): array +{ + $user = Auth::user(); + if (! $user) { + return []; + } + + $organization = $user->currentOrganization ?? $user->organizations()->first(); + if (! $organization) { + return []; + } + + $license = $organization->activeLicense; + if (! $license) { + return []; + } + + $usage = $organization->getUsageMetrics(); + $limits = $license->limits ?? []; + + $resourceLimits = []; + foreach (['servers', 'applications', 'domains', 'cloud_providers'] as $resource) { + $limit = $limits[$resource] ?? null; + $current = $usage[$resource] ?? 0; + + $resourceLimits[$resource] = [ + 'current' => $current, + 'limit' => $limit, + 'unlimited' => $limit === null, + 'remaining' => $limit ? max(0, $limit - $current) : null, + 'percentage_used' => $limit ? round(($current / $limit) * 100, 2) : 0, + 'near_limit' => $limit ? ($current / $limit) >= 0.8 : false, + ]; + } + + return $resourceLimits; +} + +/** + * Validate license before performing resource provisioning actions + */ +function validateLicenseForAction(string $action, ?string $resourceType = null): ?array +{ + $user = Auth::user(); + if (! $user) { + return ['error' => 'Authentication required']; + } + + $organization = $user->currentOrganization ?? $user->organizations()->first(); + if (! $organization) { + return ['error' => 'No organization context found']; + } + + $license = $organization->activeLicense; + if (! $license) { + return ['error' => 'Valid license required for this action']; + } + + // Validate license status + if (! $license->isValid()) { + return ['error' => 'License is not valid or has expired']; + } + + // Check resource-specific limits + if ($resourceType && ! canProvisionResource($resourceType)) { + return ['error' => "Cannot provision {$resourceType}: limit exceeded or feature not available"]; + } + + return null; // License is valid +} + function addPreviewDeploymentSuffix(string $name, int $pull_request_id = 0): string { return ($pull_request_id === 0) ? $name : $name.'-pr-'.$pull_request_id;