sanitizeIdentifier(Arr::get($payload, 'device_id')); $contentEncoding = (string) ($payload['content_encoding'] ?? Arr::get($payload, 'encoding', 'aes128gcm')); $language = (string) ($payload['language'] ?? null); $userAgent = (string) ($payload['user_agent'] ?? null); $expiresAt = $this->normalizeExpiration(Arr::get($payload, 'expiration_time')); $endpointHash = hash('sha256', $endpoint); $data = [ 'tenant_id' => $tenant->id, 'user_id' => $user?->id, 'device_id' => $deviceId, 'public_key' => $publicKey, 'auth_token' => $authToken, 'content_encoding' => $contentEncoding ?: 'aes128gcm', 'status' => 'active', 'expires_at' => $expiresAt, 'last_seen_at' => now(), 'language' => $language !== '' ? substr($language, 0, 12) : null, 'user_agent' => $userAgent !== '' ? substr($userAgent, 0, 255) : null, 'failure_count' => 0, ]; $subscription = TenantAdminPushSubscription::query() ->where('endpoint_hash', $endpointHash) ->first(); if ($subscription) { $subscription->fill($data); $subscription->status = 'active'; $subscription->endpoint = $endpoint; $subscription->save(); return $subscription; } return TenantAdminPushSubscription::create(array_merge($data, [ 'endpoint' => $endpoint, 'endpoint_hash' => $endpointHash, ])); } public function revoke(Tenant $tenant, string $endpoint, ?User $user = null): bool { $hash = hash('sha256', (string) $endpoint); $subscription = TenantAdminPushSubscription::query() ->where('tenant_id', $tenant->id) ->when($user, fn ($query) => $query->where('user_id', $user->id)) ->where(function ($query) use ($hash, $endpoint) { $query->where('endpoint_hash', $hash) ->orWhere('endpoint', $endpoint); }) ->first(); if (! $subscription) { return false; } $subscription->update([ 'status' => 'revoked', 'last_failed_at' => now(), ]); return true; } public function markFailed(TenantAdminPushSubscription $subscription, ?string $message = null): void { $subscription->fill([ 'last_failed_at' => now(), 'failure_count' => min(65535, $subscription->failure_count + 1), ]); if ($subscription->failure_count >= 3) { $subscription->status = 'revoked'; } $meta = $subscription->meta ?? []; if ($message) { $meta['last_error'] = substr($message, 0, 255); } $subscription->meta = $meta; $subscription->save(); } public function markDelivered(TenantAdminPushSubscription $subscription): void { $subscription->fill([ 'last_notified_at' => now(), 'last_failed_at' => null, 'failure_count' => 0, ])->save(); } private function sanitizeIdentifier(?string $value): ?string { if ($value === null) { return null; } $sanitized = preg_replace('/[^A-Za-z0-9 _\-]/', '', $value) ?? ''; $sanitized = trim(mb_substr($sanitized, 0, 120)); return $sanitized === '' ? null : $sanitized; } private function normalizeExpiration(mixed $value): ?CarbonImmutable { if ($value === null || $value === '') { return null; } if (is_numeric($value)) { $seconds = (int) round(((float) $value) / 1000); return CarbonImmutable::createFromTimestampUTC($seconds); } try { return CarbonImmutable::parse((string) $value); } catch (\Throwable) { return null; } } }