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\StravaConnection;
|
||||||
use App\Models\WorkoutSessions;
|
use App\Models\WorkoutSessions;
|
||||||
use App\Services\StravaClient;
|
use App\Services\StravaClient;
|
||||||
|
use Illuminate\Http\Client\ConnectionException;
|
||||||
|
use Illuminate\Http\Client\RequestException;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
@ -86,20 +88,47 @@ class StravaController extends Controller
|
||||||
public function status(Request $request): JsonResponse
|
public function status(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$connection = $request->user()->stravaConnection;
|
$connection = $request->user()->stravaConnection;
|
||||||
|
$scopes = $connection?->scopes ?? [];
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'data' => [
|
'data' => [
|
||||||
'connected' => $connection !== null,
|
'connected' => $connection !== null,
|
||||||
'athleteId' => $connection?->athlete_id,
|
'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,
|
'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();
|
return response()->noContent();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,20 @@ class StravaClient
|
||||||
return $connection->refresh();
|
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
|
public function activities(StravaConnection $connection, int $page, int $perPage): array
|
||||||
{
|
{
|
||||||
return Http::baseUrl('https://www.strava.com/api/v3')
|
return Http::baseUrl('https://www.strava.com/api/v3')
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ return [
|
||||||
'refused' => 'Strava connection refused.',
|
'refused' => 'Strava connection refused.',
|
||||||
'missing_connection' => 'Connect Strava before syncing your workouts.',
|
'missing_connection' => 'Connect Strava before syncing your workouts.',
|
||||||
'missing_activity_scope' => 'The Strava connection must allow the activity:read or activity:read_all scope.',
|
'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' => [
|
'validation' => [
|
||||||
'image_required' => 'Add an image to analyze.',
|
'image_required' => 'Add an image to analyze.',
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ return [
|
||||||
'refused' => 'Connexion Strava refusée.',
|
'refused' => 'Connexion Strava refusée.',
|
||||||
'missing_connection' => 'Connecte Strava avant de synchroniser tes workouts.',
|
'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.',
|
'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' => [
|
'validation' => [
|
||||||
'image_required' => 'Ajoute une image à analyser.',
|
'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 () {
|
it('syncs strava activities into workouts', function () {
|
||||||
Http::preventStrayRequests();
|
Http::preventStrayRequests();
|
||||||
Http::fake([
|
Http::fake([
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue