'array', 'expires_at' => 'datetime', 'revoked_at' => 'datetime', 'usage_limit' => 'integer', 'usage_count' => 'integer', ]; protected $hidden = [ 'token_encrypted', 'token_hash', ]; protected $appends = [ 'token_preview', ]; public function event(): BelongsTo { return $this->belongsTo(Event::class); } public function creator(): BelongsTo { 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) { return false; } if ($this->expires_at !== null && $this->expires_at->isPast()) { return false; } if ($this->usage_limit !== null && $this->usage_count >= $this->usage_limit) { return false; } 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); } }