null, 'language' => 'en', 'timezone' => 'UTC', 'use_totp' => false, 'totp_secret' => null, 'oauth' => '[]', ]; /** @var array */ public static array $validationRules = [ 'uuid' => ['nullable', 'string', 'size:36', 'unique:users,uuid'], 'email' => ['required', 'email', 'between:1,255', 'unique:users,email'], 'external_id' => ['sometimes', 'nullable', 'string', 'max:255', 'unique:users,external_id'], 'username' => ['required', 'between:1,255', 'unique:users,username'], 'password' => ['sometimes', 'nullable', 'string'], 'language' => ['string'], 'timezone' => ['string'], 'use_totp' => ['boolean'], 'totp_secret' => ['nullable', 'string'], 'oauth' => ['array', 'nullable'], ]; protected function casts(): array { return [ 'use_totp' => 'boolean', 'gravatar' => 'boolean', 'totp_authenticated_at' => 'datetime', 'totp_secret' => 'encrypted', 'oauth' => 'array', ]; } protected static function booted(): void { static::creating(function (self $user) { $user->uuid ??= Str::uuid()->toString(); $user->timezone ??= config('app.timezone'); return true; }); static::saving(function (self $user) { $user->email = mb_strtolower($user->email); }); static::deleting(function (self $user) { throw_if($user->servers()->count() > 0, new DisplayException(trans('exceptions.users.has_servers'))); throw_if(request()->user()?->id === $user->id, new DisplayException(trans('exceptions.users.is_self'))); }); } /** * Implement language verification by overriding Eloquence's gather * rules function. */ public static function getRules(): array { $rules = self::getValidationRules(); $rules['language'][] = new In(array_values(array_filter(ResourceBundle::getLocales(''), fn ($lang) => preg_match('/^[a-z]{2}$/', $lang)))); $rules['timezone'][] = new In(DateTimeZone::listIdentifiers()); $rules['username'][] = new Username(); return $rules; } public function username(): Attribute { return Attribute::make( set: fn (string $value) => mb_strtolower($value), ); } public function email(): Attribute { return Attribute::make( set: fn (string $value) => mb_strtolower($value), ); } /** * Returns all servers that a user owns. * * @return HasMany */ public function servers(): HasMany { return $this->hasMany(Server::class, 'owner_id'); } public function apiKeys(): HasMany { return $this->hasMany(ApiKey::class) ->where('key_type', ApiKey::TYPE_ACCOUNT); } public function recoveryTokens(): HasMany { return $this->hasMany(RecoveryToken::class); } public function sshKeys(): HasMany { return $this->hasMany(UserSSHKey::class); } /** * Returns all the activity logs where this user is the subject — not to * be confused by activity logs where this user is the _actor_. */ public function activity(): MorphToMany { return $this->morphToMany(ActivityLog::class, 'subject', 'activity_log_subjects'); } /** * Returns all the servers that a user can access. * Either because they are an admin or because they are the owner/ a subuser of the server. */ public function accessibleServers(): Builder { if ($this->canned('viewList server')) { return Server::query(); } return $this->directAccessibleServers(); } /** * Returns all the servers that a user can access "directly". * This means either because they are the owner or a subuser of the server. */ public function directAccessibleServers(): Builder { return Server::query() ->select('servers.*') ->leftJoin('subusers', 'subusers.server_id', '=', 'servers.id') ->where(function (Builder $builder) { $builder->where('servers.owner_id', $this->id)->orWhere('subusers.user_id', $this->id); }); } public function subusers(): HasMany { return $this->hasMany(Subuser::class); } public function subServers(): BelongsToMany { return $this->belongsToMany(Server::class, 'subusers'); } protected function checkPermission(Server $server, string $permission = ''): bool { if ($this->canned('update server', $server) || $server->owner_id === $this->id) { return true; } // If the user only has "view" permissions allow viewing the console if ($permission === Permission::ACTION_WEBSOCKET_CONNECT && $this->canned('view server', $server)) { return true; } $subuser = $server->subusers->where('user_id', $this->id)->first(); if (!$subuser || empty($permission)) { return false; } $check = in_array($permission, $subuser->permissions); return $check; } /** * Laravel's policies strictly check for the existence of a real method, * this checks if the ability is one of our permissions and then checks if the user can do it or not * Otherwise it calls the Authorizable trait's parent method * * @param iterable|\BackedEnum|string $abilities * @param array|mixed $arguments */ public function can($abilities, mixed $arguments = []): bool { if (is_string($abilities) && str_contains($abilities, '.')) { [$permission, $key] = str($abilities)->explode('.', 2); if (isset(Permission::permissions()[$permission]['keys'][$key])) { if ($arguments instanceof Server) { return $this->checkPermission($arguments, $abilities); } } } return $this->canned($abilities, $arguments); } public function isLastRootAdmin(): bool { $rootAdmins = User::all()->filter(fn ($user) => $user->isRootAdmin()); return once(fn () => $rootAdmins->count() === 1 && $rootAdmins->first()->is($this)); } public function isRootAdmin(): bool { return $this->hasRole(Role::ROOT_ADMIN); } public function isAdmin(): bool { return $this->isRootAdmin() || ($this->roles()->count() >= 1 && $this->getAllPermissions()->count() >= 1); } public function canAccessPanel(Panel $panel): bool { if ($this->isRootAdmin()) { return true; } if ($panel->getId() === 'admin') { return $this->isAdmin(); } return true; } public function getFilamentName(): string { return $this->username; } public function getFilamentAvatarUrl(): ?string { return 'https://gravatar.com/avatar/' . md5(strtolower($this->email)); } public function canTarget(Model $user): bool { if ($this->isRootAdmin()) { return true; } return $user instanceof User && !$user->isRootAdmin(); } public function getTenants(Panel $panel): array|Collection { return $this->accessibleServers()->get(); } public function canAccessTenant(Model $tenant): bool { if ($tenant instanceof Server) { if ($this->canned('view server', $tenant) || $tenant->owner_id === $this->id) { return true; } $subuser = $tenant->subusers->where('user_id', $this->id)->first(); return $subuser !== null; } return false; } }