feat: strava deeplink to mobile
Laravel CI-CD / Tests Unitaires (push) Successful in 21s Details
Laravel CI-CD / Deploy with Kamal (push) Successful in 1m49s Details

This commit is contained in:
Leon Morival 2026-05-20 16:56:42 +02:00
parent eca160d686
commit 3d240eca81
8 changed files with 40 additions and 12 deletions

View File

@ -61,7 +61,6 @@ EXPO_ACCESS_TOKEN=
STRAVA_CLIENT_ID= STRAVA_CLIENT_ID=
STRAVA_CLIENT_SECRET= STRAVA_CLIENT_SECRET=
STRAVA_REDIRECT_URI=
AWS_ACCESS_KEY_ID= AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY= AWS_SECRET_ACCESS_KEY=

View File

@ -50,7 +50,6 @@ jobs:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
STRAVA_CLIENT_ID: ${{ secrets.STRAVA_CLIENT_ID }} STRAVA_CLIENT_ID: ${{ secrets.STRAVA_CLIENT_ID }}
STRAVA_CLIENT_SECRET: ${{ secrets.STRAVA_CLIENT_SECRET }} STRAVA_CLIENT_SECRET: ${{ secrets.STRAVA_CLIENT_SECRET }}
STRAVA_REDIRECT_URI: ${{ secrets.STRAVA_REDIRECT_URI }}
run: | run: |
set -eu set -eu
@ -75,7 +74,6 @@ jobs:
-e SSH_PRIVATE_KEY \ -e SSH_PRIVATE_KEY \
-e STRAVA_CLIENT_ID \ -e STRAVA_CLIENT_ID \
-e STRAVA_CLIENT_SECRET \ -e STRAVA_CLIENT_SECRET \
-e STRAVA_REDIRECT_URI \
--entrypoint /bin/sh \ --entrypoint /bin/sh \
"$KAMAL_IMAGE" -lc ' "$KAMAL_IMAGE" -lc '
set -eu set -eu
@ -115,7 +113,6 @@ jobs:
SSH_PRIVATE_KEY=$SSH_PRIVATE_KEY SSH_PRIVATE_KEY=$SSH_PRIVATE_KEY
STRAVA_CLIENT_ID=$STRAVA_CLIENT_ID STRAVA_CLIENT_ID=$STRAVA_CLIENT_ID
STRAVA_CLIENT_SECRET=$STRAVA_CLIENT_SECRET STRAVA_CLIENT_SECRET=$STRAVA_CLIENT_SECRET
STRAVA_REDIRECT_URI=$STRAVA_REDIRECT_URI
SECRETS SECRETS
chmod 600 .kamal/secrets chmod 600 .kamal/secrets

View File

@ -8,6 +8,7 @@ use App\Http\Requests\StravaSyncRequest;
use App\Models\WorkoutSessions; use App\Models\WorkoutSessions;
use App\Services\StravaClient; use App\Services\StravaClient;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
@ -29,6 +30,18 @@ class StravaController extends Controller
]); ]);
} }
public function redirectToMobileApp(Request $request): RedirectResponse
{
$params = collect($request->only(['code', 'error', 'scope', 'state']))
->filter(fn ($value): bool => $value !== null && $value !== '')
->all();
$query = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
$url = 'dailymeal://strava/callback'.($query ? "?{$query}" : '');
return redirect()->away($url);
}
public function callback(StravaCallbackRequest $request, StravaClient $strava): JsonResponse public function callback(StravaCallbackRequest $request, StravaClient $strava): JsonResponse
{ {
$data = $request->validated(); $data = $request->validated();

View File

@ -15,15 +15,14 @@ class StravaClient
public function authorizationUrl(string $state): string public function authorizationUrl(string $state): string
{ {
$clientId = config('services.strava.client_id'); $clientId = config('services.strava.client_id');
$redirectUri = config('services.strava.redirect_uri');
if (! $clientId || ! $redirectUri) { if (! $clientId) {
throw new RuntimeException('Strava is not configured.'); throw new RuntimeException('Strava is not configured.');
} }
return self::AUTHORIZE_URL.'?'.http_build_query([ return self::AUTHORIZE_URL.'?'.http_build_query([
'client_id' => $clientId, 'client_id' => $clientId,
'redirect_uri' => $redirectUri, 'redirect_uri' => $this->redirectUri(),
'response_type' => 'code', 'response_type' => 'code',
'approval_prompt' => 'auto', 'approval_prompt' => 'auto',
'scope' => config('services.strava.scope', 'read,activity:read'), 'scope' => config('services.strava.scope', 'read,activity:read'),
@ -97,4 +96,9 @@ class StravaClient
->connectTimeout(3) ->connectTimeout(3)
->retry([100, 500, 1000]); ->retry([100, 500, 1000]);
} }
private function redirectUri(): string
{
return rtrim((string) config('app.url'), '/').'/strava/callback';
}
} }

View File

@ -78,7 +78,6 @@ env:
- MEILISEARCH_KEY - MEILISEARCH_KEY
- STRAVA_CLIENT_ID - STRAVA_CLIENT_ID
- STRAVA_CLIENT_SECRET - STRAVA_CLIENT_SECRET
- STRAVA_REDIRECT_URI
volumes: volumes:
- bemeal_storage_data:/var/www/html/storage/app - bemeal_storage_data:/var/www/html/storage/app

View File

@ -29,7 +29,6 @@ return [
'strava' => [ 'strava' => [
'client_id' => env('STRAVA_CLIENT_ID'), 'client_id' => env('STRAVA_CLIENT_ID'),
'client_secret' => env('STRAVA_CLIENT_SECRET'), 'client_secret' => env('STRAVA_CLIENT_SECRET'),
'redirect_uri' => env('STRAVA_REDIRECT_URI'),
'scope' => env('STRAVA_SCOPE', 'read,activity:read'), 'scope' => env('STRAVA_SCOPE', 'read,activity:read'),
], ],

View File

@ -1 +1,6 @@
<?php <?php
use App\Http\Controllers\StravaController;
use Illuminate\Support\Facades\Route;
Route::get('strava/callback', [StravaController::class, 'redirectToMobileApp'])->name('strava.web-callback');

View File

@ -10,13 +10,13 @@ use Laravel\Sanctum\Sanctum;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
beforeEach(function () { beforeEach(function () {
config()->set('app.url', 'https://bemeal.leonmorival.com');
config()->set('services.strava.client_id', '248440'); config()->set('services.strava.client_id', '248440');
config()->set('services.strava.client_secret', 'strava-secret'); config()->set('services.strava.client_secret', 'strava-secret');
config()->set('services.strava.redirect_uri', 'dailymeal://strava/callback');
config()->set('services.strava.scope', 'read,activity:read'); config()->set('services.strava.scope', 'read,activity:read');
}); });
it('returns a strava authorization url for the mobile app', function () { it('returns a strava authorization url for the api callback', function () {
Sanctum::actingAs(User::factory()->create()); Sanctum::actingAs(User::factory()->create());
$response = $this->getJson('/api/strava/authorization-url'); $response = $this->getJson('/api/strava/authorization-url');
@ -30,13 +30,25 @@ it('returns a strava authorization url for the mobile app', function () {
parse_str((string) parse_url($authorizationUrl, PHP_URL_QUERY), $query); parse_str((string) parse_url($authorizationUrl, PHP_URL_QUERY), $query);
expect($query['client_id'])->toBe('248440'); expect($query['client_id'])->toBe('248440');
expect($query['redirect_uri'])->toBe('dailymeal://strava/callback'); expect($query['redirect_uri'])->toBe('https://bemeal.leonmorival.com/strava/callback');
expect($query['response_type'])->toBe('code'); expect($query['response_type'])->toBe('code');
expect($query['approval_prompt'])->toBe('auto'); expect($query['approval_prompt'])->toBe('auto');
expect($query['scope'])->toBe('read,activity:read'); expect($query['scope'])->toBe('read,activity:read');
expect($query['state'])->not->toBeEmpty(); expect($query['state'])->not->toBeEmpty();
}); });
it('redirects the strava web callback to the mobile app', function () {
$query = http_build_query([
'code' => 'authorization-code',
'scope' => 'read activity:read',
'state' => 'oauth-state',
], '', '&', PHP_QUERY_RFC3986);
$this
->get("/strava/callback?{$query}")
->assertRedirect("dailymeal://strava/callback?{$query}");
});
it('stores strava tokens from an authorization code', function () { it('stores strava tokens from an authorization code', function () {
Http::preventStrayRequests(); Http::preventStrayRequests();
Http::fake([ Http::fake([