Fix tenant event form package selector so it no longer renders empty-value options, handles loading/empty
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)
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use App\Models\EventJoinTokenEvent;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class EventJoinToken extends Model
|
||||
{
|
||||
@@ -11,7 +15,9 @@ class EventJoinToken extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'event_id',
|
||||
'token',
|
||||
'token_hash',
|
||||
'token_encrypted',
|
||||
'token_preview',
|
||||
'label',
|
||||
'usage_limit',
|
||||
'usage_count',
|
||||
@@ -29,6 +35,15 @@ class EventJoinToken extends Model
|
||||
'usage_count' => 'integer',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'token_encrypted',
|
||||
'token_hash',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'token_preview',
|
||||
];
|
||||
|
||||
public function event(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Event::class);
|
||||
@@ -39,6 +54,11 @@ class EventJoinToken extends Model
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function analytics(): HasMany
|
||||
{
|
||||
return $this->hasMany(EventJoinTokenEvent::class);
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
if ($this->revoked_at !== null) {
|
||||
@@ -55,4 +75,64 @@ class EventJoinToken extends Model
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getTokenAttribute(?string $value): ?string
|
||||
{
|
||||
$encrypted = $this->attributes['token_encrypted'] ?? null;
|
||||
|
||||
if (! empty($encrypted)) {
|
||||
try {
|
||||
return Crypt::decryptString($encrypted);
|
||||
} catch (\Throwable $e) {
|
||||
try {
|
||||
return Crypt::decrypt($encrypted);
|
||||
} catch (\Throwable $e) {
|
||||
// Fall back to stored hash if both decrypt strategies fail.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function setTokenAttribute(?string $value): void
|
||||
{
|
||||
if ($value === null) {
|
||||
$this->attributes['token'] = null;
|
||||
$this->attributes['token_hash'] = null;
|
||||
$this->attributes['token_encrypted'] = null;
|
||||
$this->attributes['token_preview'] = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$hash = hash('sha256', $value);
|
||||
|
||||
$this->attributes['token'] = $hash;
|
||||
$this->attributes['token_hash'] = $hash;
|
||||
$this->attributes['token_encrypted'] = Crypt::encryptString($value);
|
||||
$this->attributes['token_preview'] = $this->buildPreview($value);
|
||||
}
|
||||
|
||||
public function getTokenPreviewAttribute(?string $value): ?string
|
||||
{
|
||||
if ($value) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$token = $this->token;
|
||||
|
||||
return $token ? $this->buildPreview($token) : null;
|
||||
}
|
||||
|
||||
private function buildPreview(string $token): string
|
||||
{
|
||||
$length = strlen($token);
|
||||
|
||||
if ($length <= 10) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
return substr($token, 0, 6).'…'.substr($token, -4);
|
||||
}
|
||||
}
|
||||
|
||||
50
app/Models/EventJoinTokenEvent.php
Normal file
50
app/Models/EventJoinTokenEvent.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class EventJoinTokenEvent extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'event_join_token_id',
|
||||
'event_id',
|
||||
'tenant_id',
|
||||
'token_hash',
|
||||
'token_preview',
|
||||
'event_type',
|
||||
'route',
|
||||
'http_method',
|
||||
'http_status',
|
||||
'device_id',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
'context',
|
||||
'occurred_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'context' => 'array',
|
||||
'occurred_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function joinToken(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EventJoinToken::class, 'event_join_token_id');
|
||||
}
|
||||
|
||||
public function event(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Event::class);
|
||||
}
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,24 @@ class Package extends Model
|
||||
return $this->type === 'reseller';
|
||||
}
|
||||
|
||||
public function getNameForLocale(?string $locale = null): string
|
||||
{
|
||||
$locale = $locale ?: app()->getLocale();
|
||||
$translations = $this->name_translations ?? [];
|
||||
|
||||
if (!empty($translations[$locale])) {
|
||||
return $translations[$locale];
|
||||
}
|
||||
|
||||
foreach (['en', 'de'] as $fallback) {
|
||||
if ($locale !== $fallback && !empty($translations[$fallback])) {
|
||||
return $translations[$fallback];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->name ?? '';
|
||||
}
|
||||
|
||||
public function getLimitsAttribute(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -20,6 +20,12 @@ class Photo extends Model
|
||||
protected $casts = [
|
||||
'is_featured' => 'boolean',
|
||||
'metadata' => 'array',
|
||||
'security_meta' => 'array',
|
||||
'security_scanned_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'security_scan_status' => 'pending',
|
||||
];
|
||||
|
||||
public function mediaAsset(): BelongsTo
|
||||
|
||||
@@ -4,6 +4,8 @@ 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
|
||||
{
|
||||
@@ -12,9 +14,9 @@ class RefreshToken extends Model
|
||||
public $timestamps = false;
|
||||
|
||||
protected $table = 'refresh_tokens';
|
||||
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'tenant_id',
|
||||
@@ -22,40 +24,97 @@ class RefreshToken extends Model
|
||||
'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 revoke(): bool
|
||||
|
||||
public function audits(): HasMany
|
||||
{
|
||||
return $this->update(['revoked_at' => now()]);
|
||||
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
|
||||
{
|
||||
return $this->revoked_at === null && $this->expires_at > now();
|
||||
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('expires_at', '>', now());
|
||||
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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
46
app/Models/RefreshTokenAudit.php
Normal file
46
app/Models/RefreshTokenAudit.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class RefreshTokenAudit extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'refresh_token_id',
|
||||
'tenant_id',
|
||||
'client_id',
|
||||
'event',
|
||||
'context',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
'performed_by',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $casts = [
|
||||
'context' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function refreshToken(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(RefreshToken::class);
|
||||
}
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
|
||||
public function performedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'performed_by');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user