'integer', 'available_space' => 'integer', 'email_verified_at' => 'datetime', 'unread_notifications_count' => 'integer', ]; protected $appends = ['display_name', 'has_password', 'model_type']; protected bool $billingEnabled = true; protected $gravatarSize; public function preferredLocale() { return $this->language; } public function __construct(array $attributes = []) { parent::__construct($attributes); $this->billingEnabled = (bool) settings('billing.enable'); } public function toArray(bool $showAll = false): array { if ( (!$showAll && !Auth::id()) || (Auth::id() !== $this->id && !Auth::user()?->hasPermission('users.update')) ) { $this->hidden = array_merge($this->hidden, [ 'first_name', 'last_name', 'avatar_url', 'gender', 'email', 'card_brand', 'has_password', 'confirmed', 'stripe_id', 'roles', 'permissions', 'card_last_four', 'created_at', 'updated_at', 'available_space', 'email_verified_at', 'timezone', 'confirmation_code', 'subscriptions', ]); } return parent::toArray(); } public function roles(): BelongsToMany { return $this->belongsToMany(Role::class, 'user_role'); } public function routeNotificationForSlack() { return config('services.slack.webhook_url'); } public function scopeWhereNeedsNotificationFor( Builder $query, string $notifId, ) { return $query->whereHas('notificationSubscriptions', function ( Builder $builder, ) use ($notifId) { if (Str::contains($notifId, '*')) { return $builder->where( 'notif_id', 'like', str_replace('*', '%', $notifId), ); } else { return $builder->where('notif_id', $notifId); } }); } public function notificationSubscriptions(): HasMany { return $this->hasMany(NotificationSubscription::class); } public function entries(array $options = ['owner' => true]): BelongsToMany { $query = $this->morphToMany( FileEntry::class, 'model', 'file_entry_models', 'model_id', 'file_entry_id', ) ->using(FileEntryPivot::class) ->withPivot('owner', 'permissions'); if (Arr::get($options, 'owner')) { $query->wherePivot('owner', true); } return $query ->withTimestamps() ->orderBy('file_entry_models.created_at', 'asc'); } public function activeSessions(): HasMany { return $this->hasMany(ActiveSession::class); } public function lastLogin(): HasOne { return $this->hasOne(ActiveSession::class) ->latest() ->select(['id', 'user_id', 'session_id', 'created_at']); } public function followedUsers(): BelongsToMany { return $this->belongsToMany( User::class, 'follows', 'follower_id', 'followed_id', )->compact(); } public function followers(): BelongsToMany { return $this->belongsToMany( User::class, 'follows', 'followed_id', 'follower_id', )->compact(); } public function social_profiles(): HasMany { return $this->hasMany(SocialProfile::class); } public function bans(): MorphMany { return $this->morphMany(Ban::class, 'bannable'); } /** * Check if user has a password set. */ public function getHasPasswordAttribute(): bool { return isset($this->attributes['password']) && $this->attributes['password']; } protected function password(): Attribute { return Attribute::make( set: function ($value) { if (!$value) { return null; } if (Hash::isHashed($value)) { return $value; } return Hash::make($value); }, ); } protected function availableSpace(): Attribute { return Attribute::make( set: fn($value) => !is_null($value) ? (int) $value : null, ); } protected function otpCodes(): HasMany { return $this->hasMany(OtpCode::class); } public function emailVerificationOtpIsValid(string $code): bool { $otp = $this->otpCodes() ->where('type', OtpCode::TYPE_EMAIL_VERIFICATION) ->first(); if (!$otp || $otp->code !== $code || $otp->isExpired()) { return false; } return true; } protected function emailVerifiedAt(): Attribute { return Attribute::make( set: function ($value) { if ($value === true) { return now(); } elseif ($value === false) { return null; } return $value; }, ); } public function sendEmailVerificationNotification(): void { $otp = OtpCode::createForEmailVerification($this->id); $this->notify(new VerifyEmailWithOtp($otp->code)); } public function markEmailAsVerified(): bool { $this->otpCodes() ->where('type', OtpCode::TYPE_EMAIL_VERIFICATION) ->delete(); return $this->forceFill([ 'email_verified_at' => $this->freshTimestamp(), ])->save(); } public function loadPermissions($force = false): self { if (!$force && $this->relationLoaded('permissions')) { return $this; } $query = Permission::join( 'permissionables', 'permissions.id', 'permissionables.permission_id', ); // Might have a guest user. In this case user ID will be -1, // but we still want to load guest role permissions below if ($this->exists) { $query->where([ 'permissionable_id' => $this->id, 'permissionable_type' => $this->getMorphClass(), ]); } if ($this->roles->pluck('id')->isNotEmpty()) { $query->orWhere(function (Builder $builder) { return $builder ->whereIn('permissionable_id', $this->roles->pluck('id')) ->where( 'permissionable_type', $this->roles->first()->getMorphClass(), ); }); } if ($this->exists && ($plan = $this->getSubscriptionProduct())) { $query->orWhere(function (Builder $builder) use ($plan) { return $builder ->where('permissionable_id', $plan->id) ->where('permissionable_type', $plan->getMorphClass()); }); } $permissions = $query ->select([ 'permissions.id', 'name', 'permissionables.restrictions', 'permissionable_type', ]) ->get() ->sortBy(function ($value) { if ($value['permissionable_type'] === $this->getMorphClass()) { return 1; } elseif ( $value['permissionable_type'] === Product::MODEL_TYPE ) { return 2; } else { return 3; } }) ->groupBy('id') // merge restrictions from all permissions ->map(function (Collection $group) { return $group->reduce(function ( Permission $carry, Permission $permission, ) { return $carry->mergeRestrictions($permission); }, $group[0]); }); $this->setRelation('permissions', $permissions->values()); return $this; } public function getSubscriptionProduct(): ?Product { if (!$this->billingEnabled) { return null; } $subscription = $this->subscriptions->first(); if ($subscription && $subscription->valid()) { return $subscription->product; } else { return Product::where('free', true)->first(); } } public function scopeCompact(Builder $query): Builder { return $query->select( 'users.id', 'users.avatar', 'users.email', 'users.first_name', 'users.last_name', 'users.username', ); } public function sendPasswordResetNotification(mixed $token) { ResetPassword::$createUrlCallback = function ($user, $token) { return url("password/reset/$token"); }; $this->notify(new ResetPassword($token)); } public static function findAdmin(): ?self { return (new static()) ->newQuery() ->whereHas('permissions', function (Builder $query) { $query->where('name', 'admin'); }) ->first(); } public function refreshApiToken($tokenName): string { $this->tokens() ->where('name', $tokenName) ->delete(); $newToken = $this->createToken($tokenName); $this->withAccessToken($newToken->accessToken); return $newToken->plainTextToken; } public function isBanned(): bool { if (!$this->getAttributeValue('banned_at')) { return false; } $bannedUntil = $this->bans->first()->expired_at; return !$bannedUntil || $bannedUntil->isFuture(); } public function resolveRouteBinding($value, $field = null): ?self { if ($value === 'me') { $value = Auth::id(); } return $this->where('id', $value)->firstOrFail(); } public function toSearchableArray(): array { return [ 'id' => $this->id, 'username' => $this->username, 'first_name' => $this->first_name, 'last_name' => $this->last_name, 'email' => $this->email, 'created_at' => $this->created_at->timestamp ?? '_null', 'updated_at' => $this->updated_at->timestamp ?? '_null', ]; } public static function filterableFields(): array { return ['id', 'created_at', 'updated_at']; } public function toNormalizedArray(): array { return [ 'id' => $this->id, 'name' => $this->display_name, 'description' => $this->email, 'image' => $this->avatar, 'model_type' => self::MODEL_TYPE, ]; } public static function getModelTypeAttribute(): string { return self::MODEL_TYPE; } }