feat: deauthorize strava
This commit is contained in:
parent
2f33919f8b
commit
eda0ffd199
|
|
@ -8,6 +8,8 @@ use App\Http\Requests\StravaSyncRequest;
|
|||
use App\Models\StravaConnection;
|
||||
use App\Models\WorkoutSessions;
|
||||
use App\Services\StravaClient;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
|
@ -86,20 +88,47 @@ class StravaController extends Controller
|
|||
public function status(Request $request): JsonResponse
|
||||
{
|
||||
$connection = $request->user()->stravaConnection;
|
||||
$scopes = $connection?->scopes ?? [];
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'connected' => $connection !== null,
|
||||
'athleteId' => $connection?->athlete_id,
|
||||
'scopes' => $connection?->scopes ?? [],
|
||||
'canReadActivities' => $connection !== null && $this->canReadActivities($scopes),
|
||||
'requestedScopes' => $this->normalizeScopes(config('services.strava.scope', 'read,activity:read')),
|
||||
'scopes' => $scopes,
|
||||
'expiresAt' => $connection?->expires_at,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function disconnect(Request $request): Response
|
||||
public function disconnect(Request $request, StravaClient $strava): Response|JsonResponse
|
||||
{
|
||||
$request->user()->stravaConnection()->delete();
|
||||
$connection = $request->user()->stravaConnection;
|
||||
|
||||
if (! $connection) {
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
try {
|
||||
$strava->deauthorize($connection);
|
||||
} catch (RequestException $exception) {
|
||||
if ($exception->response->status() !== 401) {
|
||||
report($exception);
|
||||
|
||||
return response()->json([
|
||||
'message' => __('api.strava.disconnect_failed'),
|
||||
], 502);
|
||||
}
|
||||
} catch (ConnectionException $exception) {
|
||||
report($exception);
|
||||
|
||||
return response()->json([
|
||||
'message' => __('api.strava.disconnect_failed'),
|
||||
], 502);
|
||||
}
|
||||
|
||||
$connection->delete();
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,20 @@ class StravaClient
|
|||
return $connection->refresh();
|
||||
}
|
||||
|
||||
public function deauthorize(StravaConnection $connection): void
|
||||
{
|
||||
$accessToken = $this->accessToken($connection);
|
||||
|
||||
Http::baseUrl('https://www.strava.com')
|
||||
->timeout(10)
|
||||
->connectTimeout(3)
|
||||
->retry([100, 500, 1000])
|
||||
->post('/oauth/deauthorize?'.http_build_query([
|
||||
'access_token' => $accessToken,
|
||||
], '', '&', PHP_QUERY_RFC3986))
|
||||
->throw();
|
||||
}
|
||||
|
||||
public function activities(StravaConnection $connection, int $page, int $perPage): array
|
||||
{
|
||||
return Http::baseUrl('https://www.strava.com/api/v3')
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ return [
|
|||
'refused' => 'Strava connection refused.',
|
||||
'missing_connection' => 'Connect Strava before syncing your workouts.',
|
||||
'missing_activity_scope' => 'The Strava connection must allow the activity:read or activity:read_all scope.',
|
||||
'disconnect_failed' => 'Unable to revoke Strava access right now. Please try again in a moment.',
|
||||
],
|
||||
'validation' => [
|
||||
'image_required' => 'Add an image to analyze.',
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ return [
|
|||
'refused' => 'Connexion Strava refusée.',
|
||||
'missing_connection' => 'Connecte Strava avant de synchroniser tes workouts.',
|
||||
'missing_activity_scope' => 'La connexion Strava doit autoriser le scope activity:read ou activity:read_all.',
|
||||
'disconnect_failed' => "Impossible de retirer l'accès Strava pour le moment. Réessaie dans quelques instants.",
|
||||
],
|
||||
'validation' => [
|
||||
'image_required' => 'Ajoute une image à analyser.',
|
||||
|
|
|
|||
|
|
@ -96,6 +96,89 @@ it('stores strava tokens from an authorization code', function () {
|
|||
]);
|
||||
});
|
||||
|
||||
it('revokes strava access before deleting the local connection', function () {
|
||||
Http::preventStrayRequests();
|
||||
Http::fake([
|
||||
'https://www.strava.com/oauth/deauthorize*' => Http::response([
|
||||
'access_token' => 'access-token',
|
||||
]),
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
StravaConnection::create([
|
||||
'user_id' => $user->id,
|
||||
'athlete_id' => 123456,
|
||||
'access_token' => 'access-token',
|
||||
'refresh_token' => 'refresh-token',
|
||||
'expires_at' => now()->addHour(),
|
||||
'scopes' => ['read', 'activity:read'],
|
||||
'athlete' => ['id' => 123456],
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$this->deleteJson('/api/strava/connection')->assertNoContent();
|
||||
|
||||
expect($user->fresh()->stravaConnection)->toBeNull();
|
||||
|
||||
Http::assertSent(fn ($request): bool => $request->url() === 'https://www.strava.com/oauth/deauthorize?access_token=access-token');
|
||||
});
|
||||
|
||||
it('deletes the local strava connection when strava already rejects the token', function () {
|
||||
Http::preventStrayRequests();
|
||||
Http::fake([
|
||||
'https://www.strava.com/oauth/deauthorize*' => Http::response([
|
||||
'message' => 'Authorization Error',
|
||||
], 401),
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
StravaConnection::create([
|
||||
'user_id' => $user->id,
|
||||
'athlete_id' => 123456,
|
||||
'access_token' => 'access-token',
|
||||
'refresh_token' => 'refresh-token',
|
||||
'expires_at' => now()->addHour(),
|
||||
'scopes' => ['read', 'activity:read'],
|
||||
'athlete' => ['id' => 123456],
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$this->deleteJson('/api/strava/connection')->assertNoContent();
|
||||
|
||||
expect($user->fresh()->stravaConnection)->toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the local strava connection when strava revocation fails temporarily', function () {
|
||||
Http::preventStrayRequests();
|
||||
Http::fake([
|
||||
'https://www.strava.com/oauth/deauthorize*' => Http::response([
|
||||
'message' => 'temporary failure',
|
||||
], 503),
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
StravaConnection::create([
|
||||
'user_id' => $user->id,
|
||||
'athlete_id' => 123456,
|
||||
'access_token' => 'access-token',
|
||||
'refresh_token' => 'refresh-token',
|
||||
'expires_at' => now()->addHour(),
|
||||
'scopes' => ['read', 'activity:read'],
|
||||
'athlete' => ['id' => 123456],
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$this
|
||||
->deleteJson('/api/strava/connection')
|
||||
->assertStatus(502)
|
||||
->assertJsonPath('message', __('api.strava.disconnect_failed'));
|
||||
|
||||
expect($user->fresh()->stravaConnection)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('syncs strava activities into workouts', function () {
|
||||
Http::preventStrayRequests();
|
||||
Http::fake([
|
||||
|
|
|
|||
Loading…
Reference in New Issue