From c8f875550a5145023f2074d53e27f254d44b02ce Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Fri, 3 Jul 2026 13:11:36 -0300 Subject: [PATCH] Replace generic token-TTL constant with per-platform Platform::defaultTokenTtlSeconds() The single LONG_LIVED_TOKEN_TTL_SECONDS constant (Meta 60-day) plus a loose inline 7200 for X made it unclear which networks each value applied to. Express the fallback TTL as a per-platform match method instead, matching how the enum already exposes every other per-network value, so the network->value mapping is visible in one place: X 2h, Instagram/Threads 60d, everyone else null (they always return expires_in). Behavior is unchanged. --- app/Enums/SocialAccount/Platform.php | 29 ++++++++++++++----- .../Controllers/Auth/InstagramController.php | 2 +- .../Controllers/Auth/ThreadsController.php | 2 +- app/Services/Social/ConnectionVerifier.php | 6 ++-- tests/Unit/Enums/PlatformTest.php | 18 ++++++++++++ 5 files changed, 45 insertions(+), 12 deletions(-) diff --git a/app/Enums/SocialAccount/Platform.php b/app/Enums/SocialAccount/Platform.php index 808f865b..afcd02f3 100644 --- a/app/Enums/SocialAccount/Platform.php +++ b/app/Enums/SocialAccount/Platform.php @@ -23,13 +23,6 @@ enum Platform: string case Telegram = 'telegram'; case Discord = 'discord'; - /** - * Fallback lifetime, in seconds, for Meta long-lived tokens (Instagram and - * Threads — see extendsAccessTokenOnRefresh) when the provider response - * omits expires_in. Meta issues these tokens for 60 days. - */ - public const LONG_LIVED_TOKEN_TTL_SECONDS = 5184000; - /** * The social network this platform belongs to. Variants that represent the * same network (LinkedIn profile vs. company page, Instagram standalone vs. @@ -300,6 +293,28 @@ public static function accessTokenExtendingPlatformValues(): array )); } + /** + * The token lifetime, in seconds, to assume when the provider's OAuth + * response omits expires_in. Each value is that network's own documented + * default: + * + * - X: a 2-hour access token. + * - Instagram / Threads: Meta's 60-day long-lived token. + * + * Networks that always return expires_in (LinkedIn, TikTok, YouTube, + * Pinterest), whose refresh sets a fixed lifetime directly (Bluesky), or + * whose tokens never expire (Facebook, Mastodon, Telegram, Discord) have no + * fallback here and return null. + */ + public function defaultTokenTtlSeconds(): ?int + { + return match ($this) { + self::X => 7200, + self::Instagram, self::Threads => 5184000, + default => null, + }; + } + public function queue(): string { return 'social-'.$this->value; diff --git a/app/Http/Controllers/Auth/InstagramController.php b/app/Http/Controllers/Auth/InstagramController.php index af48218c..f8a0c054 100644 --- a/app/Http/Controllers/Auth/InstagramController.php +++ b/app/Http/Controllers/Auth/InstagramController.php @@ -69,7 +69,7 @@ public function callback(Request $request): InertiaResponse $avatarPath = $socialUser->getAvatar() ? uploadFromUrl($socialUser->getAvatar()) : null; // Calculate token expiration (long-lived tokens last 60 days) - $expiresIn = $socialUser->expiresIn ?? SocialPlatform::LONG_LIVED_TOKEN_TTL_SECONDS; + $expiresIn = $socialUser->expiresIn ?? $this->platform->defaultTokenTtlSeconds(); $tokenExpiresAt = now()->addSeconds($expiresIn); $workspace->socialAccounts()->updateOrCreate( diff --git a/app/Http/Controllers/Auth/ThreadsController.php b/app/Http/Controllers/Auth/ThreadsController.php index 059b173e..955009ea 100644 --- a/app/Http/Controllers/Auth/ThreadsController.php +++ b/app/Http/Controllers/Auth/ThreadsController.php @@ -117,7 +117,7 @@ public function callback(Request $request): InertiaResponse $longLivedData = $longLivedResponse->json(); $longLivedToken = $longLivedData['access_token'] ?? $shortLivedToken; - $expiresIn = $longLivedData['expires_in'] ?? SocialPlatform::LONG_LIVED_TOKEN_TTL_SECONDS; + $expiresIn = $longLivedData['expires_in'] ?? $this->platform->defaultTokenTtlSeconds(); // Fetch user profile $profileResponse = Http::get(config('trypost.platforms.threads.graph_api')."/{$userId}", [ diff --git a/app/Services/Social/ConnectionVerifier.php b/app/Services/Social/ConnectionVerifier.php index 547b78d6..071d554e 100644 --- a/app/Services/Social/ConnectionVerifier.php +++ b/app/Services/Social/ConnectionVerifier.php @@ -181,7 +181,7 @@ private function refreshXToken(SocialAccount $account): void $account->update([ 'access_token' => data_get($data, 'access_token'), 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), - 'token_expires_at' => now()->addSeconds(data_get($data, 'expires_in', 7200)), + 'token_expires_at' => now()->addSeconds(data_get($data, 'expires_in', $account->platform->defaultTokenTtlSeconds())), ]); $account->refresh(); @@ -328,7 +328,7 @@ private function refreshThreadsToken(SocialAccount $account): void $account->update([ 'access_token' => $newToken, 'refresh_token' => $newToken, - 'token_expires_at' => now()->addSeconds(data_get($data, 'expires_in', Platform::LONG_LIVED_TOKEN_TTL_SECONDS)), + 'token_expires_at' => now()->addSeconds(data_get($data, 'expires_in', $account->platform->defaultTokenTtlSeconds())), ]); $account->refresh(); @@ -350,7 +350,7 @@ private function refreshInstagramToken(SocialAccount $account): void $account->update([ 'access_token' => $newToken, 'refresh_token' => $newToken, - 'token_expires_at' => now()->addSeconds(data_get($data, 'expires_in', Platform::LONG_LIVED_TOKEN_TTL_SECONDS)), + 'token_expires_at' => now()->addSeconds(data_get($data, 'expires_in', $account->platform->defaultTokenTtlSeconds())), ]); $account->refresh(); diff --git a/tests/Unit/Enums/PlatformTest.php b/tests/Unit/Enums/PlatformTest.php index 5c4f94d1..340d302f 100644 --- a/tests/Unit/Enums/PlatformTest.php +++ b/tests/Unit/Enums/PlatformTest.php @@ -85,6 +85,24 @@ expect(Platform::Pinterest->supportsTextOnly())->toBeFalse(); }); +test('platform exposes the correct default token TTL fallback', function () { + // X access tokens live 2 hours. + expect(Platform::X->defaultTokenTtlSeconds())->toBe(7200); + + // Instagram and Threads use Meta's 60-day long-lived token. + expect(Platform::Instagram->defaultTokenTtlSeconds())->toBe(5184000); + expect(Platform::Threads->defaultTokenTtlSeconds())->toBe(5184000); + + // Networks that always return expires_in, set a fixed lifetime directly, or + // never expire have no fallback here. + expect(Platform::LinkedIn->defaultTokenTtlSeconds())->toBeNull(); + expect(Platform::TikTok->defaultTokenTtlSeconds())->toBeNull(); + expect(Platform::YouTube->defaultTokenTtlSeconds())->toBeNull(); + expect(Platform::Pinterest->defaultTokenTtlSeconds())->toBeNull(); + expect(Platform::Bluesky->defaultTokenTtlSeconds())->toBeNull(); + expect(Platform::Facebook->defaultTokenTtlSeconds())->toBeNull(); +}); + test('platform is enabled by default', function () { expect(Platform::LinkedIn->isEnabled())->toBeTrue(); expect(Platform::Instagram->isEnabled())->toBeTrue();