feat(api-tokens): warn 7 days before a token expires

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras BacsaiandClaude Opus 5.5 committed 2026-10-30 14:27:58 +01:00
1 parent 5116a93f42
commit 548dec65b7
5 files changed
+30 -9

No files matched your search

+18 -3
View File
@@ -22,17 +22,19 @@ class ApiTokenExpirationWarningJob implements ShouldBeEncrypted, ShouldQueue, Si
public $timeout = 120;
private const WARNING_DAYS = 7;
public function handle(): void
{
PersonalAccessToken::query()
->whereNotNull('expires_at')
->where('expires_at', '>', now())
->where('expires_at', '<=', now()->addDay())
->where('expires_at', '<=', now()->addDays(self::WARNING_DAYS))
->whereNull('api_token_expiration_warning_sent_at')
->where('tokenable_type', User::class)
->chunkById(100, function ($tokens) {
foreach ($tokens as $token) {
if (! $token->team_id) {
if (! $token->team_id || ! $this->isDue($token)) {
continue;
}
@@ -53,7 +55,7 @@ class ApiTokenExpirationWarningJob implements ShouldBeEncrypted, ShouldQueue, Si
->whereKey($token->getKey())
->whereNotNull('expires_at')
->where('expires_at', '>', now())
->where('expires_at', '<=', now()->addDay())
->where('expires_at', '<=', now()->addDays(self::WARNING_DAYS))
->whereNull('api_token_expiration_warning_sent_at')
->update(['api_token_expiration_warning_sent_at' => $warningSentAt]);
@@ -65,4 +67,17 @@ class ApiTokenExpirationWarningJob implements ShouldBeEncrypted, ShouldQueue, Si
}
});
}
/**
* Warn 7 days before expiry. A token that lives 7 days or less gets the warning 24 hours before expiry,
* so it is not warned right after it is created.
*/
private function isDue(PersonalAccessToken $token): bool
{
if ($token->created_at->diffInDays($token->expires_at) > self::WARNING_DAYS) {
return true;
}
return $token->expires_at->lte(now()->addDay());
}
}
@@ -32,7 +32,7 @@ class ApiTokenExpiringNotification extends CustomEmailNotification
public function toMail(): MailMessage
{
$mail = new MailMessage;
$mail->subject("Coolify: API token '{$this->tokenName}' expires in 24 hours");
$mail->subject("Coolify: API token '{$this->tokenName}' expires on {$this->expiresAt}");
$mail->view('emails.api-token-expiring', [
'tokenName' => $this->tokenName,
'expiresAt' => $this->expiresAt,
@@ -5,6 +5,7 @@
'as' => 'span',
'dynamic' => false,
'pulse' => false, // ping the dot for a state that is still in progress
'size' => 'md', // sm fits inline next to 12px table text without growing the row
])
@php
@@ -15,7 +16,12 @@
'error' => 'bg-red-500',
];
$baseClasses = 'inline-flex h-6 max-w-full items-center gap-1.5 whitespace-nowrap rounded-full border border-neutral-200 bg-neutral-100 px-2 text-xs font-medium leading-none text-neutral-700 dark:border-white/[0.12] dark:bg-white/[0.07] dark:text-white';
$sizeClasses = [
'sm' => 'h-4 gap-1 px-1.5 text-[10px]',
'md' => 'h-6 gap-1.5 px-2 text-xs',
];
$baseClasses = 'inline-flex max-w-full items-center whitespace-nowrap rounded-full border border-neutral-200 bg-neutral-100 font-medium leading-none text-neutral-700 dark:border-white/[0.12] dark:bg-white/[0.07] dark:text-white ' . ($sizeClasses[$size] ?? $sizeClasses['md']);
@endphp
@if ($as === 'button')
@@ -234,7 +234,7 @@
<div class="flex min-w-0 items-center gap-1.5">
<span class="truncate text-[12px] font-medium text-black dark:text-fg">{{ $token->name }}</span>
@if ($token->isPaused())
<x-status-badge label="Paused" type="warning" />
<x-status-badge label="Paused" type="warning" size="sm" />
@endif
</div>
<div class="truncate text-[11px] text-neutral-500 dark:text-fg-dim">
@@ -266,7 +266,7 @@
@if (!$token->expires_at)
Never
@elseif ($token->expires_at->isPast())
<x-status-badge label="Expired" type="error" />
<x-status-badge label="Expired" type="error" size="sm" />
@else
{{ $token->expires_at->format('Y-m-d') }}
@endif
@@ -106,8 +106,8 @@ describe('ApiTokenExpirationWarningJob', function () {
Notification::assertSentToTimes($this->team, ApiTokenExpiringNotification::class, 2);
});
test('skips tokens expiring more than 24h out', function () {
createTokenExpiring($this->user, $this->team, Carbon::now()->addDays(3));
test('skips tokens expiring more than 7 days out', function () {
createTokenExpiring($this->user, $this->team, Carbon::now()->addDays(8));
(new ApiTokenExpirationWarningJob)->handle();