states, and pulls data from the authenticated /api/v1/tenant/packages endpoint.
(resources/js/admin/pages/EventFormPage.tsx, resources/js/admin/api.ts)
- Harden tenant-admin auth flow: prevent PKCE state loss, scope out StrictMode double-processing, add SPA
routes for /event-admin/login and /event-admin/logout, and tighten token/session clearing semantics (resources/js/admin/auth/{context,tokens}.tsx, resources/js/admin/pages/{AuthCallbackPage,LogoutPage}.tsx,
resources/js/admin/router.tsx, routes/web.php)
121 lines
3.0 KiB
PHP
121 lines
3.0 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Http\Request;
|
|
|
|
class RefreshToken extends Model
|
|
{
|
|
public $incrementing = false;
|
|
protected $keyType = 'string';
|
|
public $timestamps = false;
|
|
|
|
protected $table = 'refresh_tokens';
|
|
|
|
protected $guarded = [];
|
|
|
|
protected $fillable = [
|
|
'id',
|
|
'tenant_id',
|
|
'client_id',
|
|
'token',
|
|
'access_token',
|
|
'expires_at',
|
|
'last_used_at',
|
|
'scope',
|
|
'ip_address',
|
|
'user_agent',
|
|
'revoked_at',
|
|
'revoked_reason',
|
|
];
|
|
|
|
protected $casts = [
|
|
'expires_at' => 'datetime',
|
|
'last_used_at' => 'datetime',
|
|
'revoked_at' => 'datetime',
|
|
'created_at' => 'datetime',
|
|
];
|
|
|
|
public function tenant(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Tenant::class);
|
|
}
|
|
|
|
public function audits(): HasMany
|
|
{
|
|
return $this->hasMany(RefreshTokenAudit::class);
|
|
}
|
|
|
|
public function revoke(?string $reason = null, ?int $performedBy = null, ?Request $request = null, array $context = []): bool
|
|
{
|
|
$result = $this->update([
|
|
'revoked_at' => now(),
|
|
'revoked_reason' => $reason,
|
|
]);
|
|
|
|
$event = match ($reason) {
|
|
'rotated' => 'rotated',
|
|
'ip_mismatch' => 'ip_mismatch',
|
|
'expired' => 'expired',
|
|
'invalid_secret' => 'invalid_secret',
|
|
'tenant_missing' => 'tenant_missing',
|
|
'max_active_limit' => 'max_active_limit',
|
|
default => 'revoked',
|
|
};
|
|
|
|
$this->recordAudit(
|
|
$event,
|
|
array_merge([
|
|
'reason' => $reason,
|
|
], $context),
|
|
$performedBy,
|
|
$request
|
|
);
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function isActive(): bool
|
|
{
|
|
if ($this->revoked_at !== null) {
|
|
return false;
|
|
}
|
|
|
|
return $this->expires_at === null || $this->expires_at->isFuture();
|
|
}
|
|
|
|
public function scopeActive($query)
|
|
{
|
|
return $query
|
|
->whereNull('revoked_at')
|
|
->where(function ($inner) {
|
|
$inner->whereNull('expires_at')
|
|
->orWhere('expires_at', '>', now());
|
|
});
|
|
}
|
|
|
|
public function scopeForTenant($query, string $tenantId)
|
|
{
|
|
return $query->where('tenant_id', $tenantId);
|
|
}
|
|
|
|
public function recordAudit(string $event, array $context = [], ?int $performedBy = null, ?Request $request = null): void
|
|
{
|
|
$request ??= request();
|
|
|
|
$this->audits()->create([
|
|
'tenant_id' => $this->tenant_id,
|
|
'client_id' => $this->client_id,
|
|
'event' => $event,
|
|
'context' => $context ?: null,
|
|
'ip_address' => $request?->ip(),
|
|
'user_agent' => $request?->userAgent(),
|
|
'performed_by' => $performedBy,
|
|
'created_at' => now(),
|
|
]);
|
|
}
|
|
}
|