coolify/app/Traits/HasNotificationSettings.php
Andras Bacsai 90ddbb3572 feat(security): support expiration on API tokens with warning notifications
Add optional expiration to personal API tokens. Users pick a duration
(1/7/30/60/90 days or Never) at creation time. Expired tokens are
rejected by Sanctum, pruned hourly by sanctum:prune-expired, and a
team notification fires ~24h before expiry so owners can rotate
before API calls start failing.

- ApiTokens Livewire component stores expires_at from expiresInDays
- Rework issued-tokens UI from card grid to table (matches other views)
- New ApiTokenExpirationWarningJob scheduled hourly (idempotent via RateLimiter)
- New ApiTokenExpiringNotification (email/discord/telegram/slack/pushover)
- api_token_expiring added to alwaysSendEvents so users cannot silence
  expiry warnings from the per-event notification toggle UI
- sanctum:prune-expired cadence moved from daily to hourly

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 14:28:38 +02:00

99 lines
2.8 KiB
PHP

<?php
namespace App\Traits;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\PushoverChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Channels\WebhookChannel;
use Illuminate\Database\Eloquent\Model;
trait HasNotificationSettings
{
protected $alwaysSendEvents = [
'server_force_enabled',
'server_force_disabled',
'general',
'test',
'ssl_certificate_renewal',
'hetzner_deletion_failure',
'api_token_expiring',
];
/**
* Get settings model for specific channel
*/
public function getNotificationSettings(string $channel): ?Model
{
return match ($channel) {
'email' => $this->emailNotificationSettings,
'discord' => $this->discordNotificationSettings,
'telegram' => $this->telegramNotificationSettings,
'slack' => $this->slackNotificationSettings,
'pushover' => $this->pushoverNotificationSettings,
'webhook' => $this->webhookNotificationSettings,
default => null,
};
}
/**
* Check if a notification channel is enabled
*/
public function isNotificationEnabled(string $channel): bool
{
$settings = $this->getNotificationSettings($channel);
return $settings?->isEnabled() ?? false;
}
/**
* Check if a specific notification type is enabled for a channel
*/
public function isNotificationTypeEnabled(string $channel, string $event): bool
{
$settings = $this->getNotificationSettings($channel);
if (! $settings || ! $this->isNotificationEnabled($channel)) {
return false;
}
if (in_array($event, $this->alwaysSendEvents)) {
return true;
}
$settingKey = "{$event}_{$channel}_notifications";
return (bool) $settings->$settingKey;
}
/**
* Get all enabled notification channels for an event
*/
public function getEnabledChannels(string $event): array
{
$channels = [];
$channelMap = [
'email' => EmailChannel::class,
'discord' => DiscordChannel::class,
'telegram' => TelegramChannel::class,
'slack' => SlackChannel::class,
'pushover' => PushoverChannel::class,
'webhook' => WebhookChannel::class,
];
if ($event === 'general') {
unset($channelMap['email']);
}
foreach ($channelMap as $channel => $channelClass) {
if ($this->isNotificationEnabled($channel) && $this->isNotificationTypeEnabled($channel, $event)) {
$channels[] = $channelClass;
}
}
return $channels;
}
}