trypost/app/Jobs/RefreshSocialToken.php

52 lines
1.7 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Exceptions\PlatformUnavailableException;
fix(social): proactive token refresh actually refreshes (not just verifies) Three orthogonal fixes that together close the gap where social tokens were silently aging out without ever being refreshed, then dying at the provider when the refresh_token also got revoked. The original failure mode: a user's X token expired because the hourly proactive-refresh cron's smart `verify()` skip-logic kept saying 'token still works, no need to refresh', and once the token actually expired, the cron's WHERE clause excluded it from future runs. By the time anyone noticed, the refresh_token at X was also gone. (C) ConnectionVerifier: rename private `refreshTokenIfNeeded` → public `refreshToken`. Callers that want the smart 'try access_token first' behavior keep using `verify()`. Callers that want a proactive refresh (the cron) call `refreshToken` directly. (B) RefreshExpiringTokens command: drop the `where('token_expires_at', '>', now())` filter. Already-expired tokens now get a last-chance refresh attempt before the refresh_token also dies at the provider. Status filter (`Connected`) still excludes accounts already marked TokenExpired. (D) RefreshSocialToken job: switch from `verify()` to `refreshToken()`, and on `TokenExpiredException` call `markAsTokenExpired` so the user is notified immediately. The lock + transition detection in markAsTokenExpired prevents notification spam if subsequent cron passes also fail. Tests: - 3 new tests for RefreshSocialToken (calls refreshToken not verify, marks TokenExpired on TokenExpiredException, logs warning on other errors) - Updated RefreshExpiringTokens test to assert already-expired tokens are now dispatched (was previously asserted as 'should NOT')
2026-05-12 22:36:35 +00:00
use App\Exceptions\TokenExpiredException;
use App\Models\SocialAccount;
use App\Services\Social\ConnectionVerifier;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
fix(social): proactive token refresh actually refreshes (not just verifies) Three orthogonal fixes that together close the gap where social tokens were silently aging out without ever being refreshed, then dying at the provider when the refresh_token also got revoked. The original failure mode: a user's X token expired because the hourly proactive-refresh cron's smart `verify()` skip-logic kept saying 'token still works, no need to refresh', and once the token actually expired, the cron's WHERE clause excluded it from future runs. By the time anyone noticed, the refresh_token at X was also gone. (C) ConnectionVerifier: rename private `refreshTokenIfNeeded` → public `refreshToken`. Callers that want the smart 'try access_token first' behavior keep using `verify()`. Callers that want a proactive refresh (the cron) call `refreshToken` directly. (B) RefreshExpiringTokens command: drop the `where('token_expires_at', '>', now())` filter. Already-expired tokens now get a last-chance refresh attempt before the refresh_token also dies at the provider. Status filter (`Connected`) still excludes accounts already marked TokenExpired. (D) RefreshSocialToken job: switch from `verify()` to `refreshToken()`, and on `TokenExpiredException` call `markAsTokenExpired` so the user is notified immediately. The lock + transition detection in markAsTokenExpired prevents notification spam if subsequent cron passes also fail. Tests: - 3 new tests for RefreshSocialToken (calls refreshToken not verify, marks TokenExpired on TokenExpiredException, logs warning on other errors) - Updated RefreshExpiringTokens test to assert already-expired tokens are now dispatched (was previously asserted as 'should NOT')
2026-05-12 22:36:35 +00:00
use Throwable;
class RefreshSocialToken implements ShouldQueue
{
use Queueable;
public int $tries = 1;
public function __construct(public SocialAccount $account) {}
public function handle(ConnectionVerifier $verifier): void
{
try {
if ($this->account->platform->extendsAccessTokenOnRefresh()) {
// Instagram/Threads extend the long-lived token itself and
// can't be refreshed once expired, so extend it while it's
// still valid instead of waiting for it to fail.
$verifier->refreshToken($this->account);
} else {
$verifier->verify($this->account);
}
} catch (PlatformUnavailableException $e) {
Log::warning('Token refresh skipped: platform unavailable', [
'account_id' => $this->account->id,
'platform' => $this->account->platform->value,
'error' => $e->getMessage(),
]);
fix(social): proactive token refresh actually refreshes (not just verifies) Three orthogonal fixes that together close the gap where social tokens were silently aging out without ever being refreshed, then dying at the provider when the refresh_token also got revoked. The original failure mode: a user's X token expired because the hourly proactive-refresh cron's smart `verify()` skip-logic kept saying 'token still works, no need to refresh', and once the token actually expired, the cron's WHERE clause excluded it from future runs. By the time anyone noticed, the refresh_token at X was also gone. (C) ConnectionVerifier: rename private `refreshTokenIfNeeded` → public `refreshToken`. Callers that want the smart 'try access_token first' behavior keep using `verify()`. Callers that want a proactive refresh (the cron) call `refreshToken` directly. (B) RefreshExpiringTokens command: drop the `where('token_expires_at', '>', now())` filter. Already-expired tokens now get a last-chance refresh attempt before the refresh_token also dies at the provider. Status filter (`Connected`) still excludes accounts already marked TokenExpired. (D) RefreshSocialToken job: switch from `verify()` to `refreshToken()`, and on `TokenExpiredException` call `markAsTokenExpired` so the user is notified immediately. The lock + transition detection in markAsTokenExpired prevents notification spam if subsequent cron passes also fail. Tests: - 3 new tests for RefreshSocialToken (calls refreshToken not verify, marks TokenExpired on TokenExpiredException, logs warning on other errors) - Updated RefreshExpiringTokens test to assert already-expired tokens are now dispatched (was previously asserted as 'should NOT')
2026-05-12 22:36:35 +00:00
} catch (TokenExpiredException $e) {
$this->account->markAsTokenExpired($e->getMessage());
} catch (Throwable $e) {
Log::warning('Proactive token refresh failed', [
'account_id' => $this->account->id,
'platform' => $this->account->platform->value,
'error' => $e->getMessage(),
]);
}
}
}