Files
landing-api/app/Models/User.php
Josep Piguillem f93775e549 Issue machine tokens with limited permissions and an expiry
A token acts as the person who made it, within the permissions it was given
and those they still hold, checked on every request. Permission checks now go
through one gate rule, since spatie's answered first and never asked the
token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 09:21:37 +02:00

150 lines
4.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Models;
use Database\Factories\UserFactory;
use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasAvatar;
use Filament\Models\Contracts\HasName;
use Filament\Panel;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Spatie\Permission\Traits\HasRoles;
/**
* A person who signed in through the Pireon identity server.
*
* Identity (name, email, active flag) is owned upstream and refreshed on every
* login. Authorisation is owned here: the roles and permissions attached by
* spatie/laravel-permission are the only thing that grants access.
*/
#[Fillable(['name', 'email', 'avatar_url', 'locale', 'is_active', 'oauth_id', 'remote_roles', 'last_login_at'])]
#[Hidden(['password', 'remember_token'])]
class User extends Authenticatable implements FilamentUser, HasAvatar, HasName
{
/** @use HasFactory<UserFactory> */
use HasFactory, HasRoles, Notifiable;
/**
* Pin spatie's permission lookups to a single guard.
*
* The same person reaches this app through two guards — `web` in the panel,
* `api` with a bearer token — but there is only one permission catalogue.
* Without this, an API request would look for permissions registered under
* the `api` guard, find none, and deny everything.
*/
protected string $guard_name = 'web';
/**
* The machine token this request came in with, when it did. Not stored:
* it describes the request, not the person.
*/
protected ?ApiToken $accessToken = null;
public function withAccessToken(?ApiToken $token): static
{
$this->accessToken = $token;
return $this;
}
public function currentAccessToken(): ?ApiToken
{
return $this->accessToken;
}
/**
* @return HasMany<ApiToken, $this>
*/
public function apiTokens(): HasMany
{
return $this->hasMany(ApiToken::class);
}
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'last_login_at' => 'datetime',
'password' => 'hashed',
'is_active' => 'boolean',
'remote_roles' => 'array',
];
}
/**
* @return HasMany<Page, $this>
*/
public function pages(): HasMany
{
return $this->hasMany(Page::class, 'author_id');
}
/**
* Every browser this account is currently signed in from, as Laravel's
* own database session driver already knows it.
*
* @return HasMany<UserSession, $this>
*/
public function sessions(): HasMany
{
return $this->hasMany(UserSession::class, 'user_id');
}
/**
* Gate for the Filament panel.
*
* Two independent conditions must hold: the identity server still considers
* the account usable, and this app has granted the account a role carrying
* the `panel.access` permission.
*/
public function canAccessPanel(Panel $panel): bool
{
return $this->is_active && $this->can('panel.access');
}
public function getFilamentName(): string
{
return $this->name;
}
public function getFilamentAvatarUrl(): ?string
{
return $this->avatar_url;
}
/**
* Preferred UI locale, falling back to the application default when the
* user has never picked one or picked one we no longer support.
*/
public function preferredLocale(): string
{
$supported = array_keys(config('locales.supported'));
return in_array($this->locale, $supported, true)
? $this->locale
: (string) config('locales.default');
}
/**
* @param Builder<$this> $query
* @return Builder<$this>
*/
public function scopeActive(Builder $query): Builder
{
return $query->where('is_active', true);
}
}