diff --git a/backend/server/integrations/views/integration_view.py b/backend/server/integrations/views/integration_view.py index 013591d..1ee54ba 100644 --- a/backend/server/integrations/views/integration_view.py +++ b/backend/server/integrations/views/integration_view.py @@ -2,7 +2,7 @@ import os from rest_framework.response import Response from rest_framework import viewsets, status from rest_framework.permissions import IsAuthenticated -from integrations.models import ImmichIntegration +from integrations.models import ImmichIntegration, StravaToken from django.conf import settings @@ -14,13 +14,17 @@ class IntegrationView(viewsets.ViewSet): """ immich_integrations = ImmichIntegration.objects.filter(user=request.user) google_map_integration = settings.GOOGLE_MAPS_API_KEY != '' - strava_integration = settings.STRAVA_CLIENT_ID != '' and settings.STRAVA_CLIENT_SECRET != '' + strava_integration_global = settings.STRAVA_CLIENT_ID != '' and settings.STRAVA_CLIENT_SECRET != '' + strava_integration_user = StravaToken.objects.filter(user=request.user).exists() return Response( { 'immich': immich_integrations.exists(), 'google_maps': google_map_integration, - 'strava': strava_integration + 'strava': { + 'global': strava_integration_global, + 'user': strava_integration_user + } }, status=status.HTTP_200_OK ) diff --git a/backend/server/integrations/views/strava_view.py b/backend/server/integrations/views/strava_view.py index 436b51b..0ce3086 100644 --- a/backend/server/integrations/views/strava_view.py +++ b/backend/server/integrations/views/strava_view.py @@ -5,6 +5,7 @@ from rest_framework.decorators import action import requests import logging import time +from django.shortcuts import redirect from django.conf import settings from integrations.models import StravaToken @@ -87,16 +88,12 @@ class StravaIntegrationView(viewsets.ViewSet): } ) - return Response( - { - 'message': 'Strava access token obtained and saved successfully.', - 'access_token': response_data.get('access_token'), - 'expires_at': response_data.get('expires_at'), - 'refresh_token': response_data.get('refresh_token'), - 'athlete': response_data.get('athlete'), - }, - status=status.HTTP_200_OK - ) + # redirect to frontend url / setttigns + frontend_url = settings.FRONTEND_URL + if not frontend_url.endswith('/'): + frontend_url += '/' + return redirect(f"{frontend_url}settings?strava_authorized=true") + except requests.RequestException as e: logger.error("Error during Strava OAuth token exchange: %s", str(e)) @@ -110,6 +107,28 @@ class StravaIntegrationView(viewsets.ViewSet): status=status.HTTP_502_BAD_GATEWAY ) + @action(detail=False, methods=['post'], url_path='disable') + def disable(self, request): + """ + Disables the Strava integration for the authenticated user by deleting their StravaToken. + """ + strava_token = StravaToken.objects.filter(user=request.user).first() + if not strava_token: + return Response( + { + 'message': 'Strava integration is not enabled for this user.', + 'error': True, + 'code': 'strava.not_enabled' + }, + status=status.HTTP_404_NOT_FOUND + ) + + strava_token.delete() + return Response( + {'message': 'Strava integration disabled successfully.'}, + status=status.HTTP_204_NO_CONTENT + ) + def refresh_strava_token_if_needed(self, user): strava_token = StravaToken.objects.filter(user=user).first() if not strava_token: diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 98f8a8a..55b4a44 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -747,7 +747,8 @@ "network_error": "Network error while connecting to the Immich server. Please check your connection and try again." }, "google_maps": { - "google_maps_integration_desc": "Connect your Google Maps account to get high-quality location search results and recommendations." + "google_maps_integration_desc": "Connect your Google Maps account to get high-quality location search results and recommendations.", + "google_maps_integration_desc_no_staff": "This integration must first be enabled by the admin on this server." }, "recomendations": { "recommendation": "Recommendation", @@ -786,5 +787,17 @@ "invited_on": "Invited on", "no_invites_desc": "Make sure your profile is public so users can invite you.", "by": "by" + }, + "strava": { + "strava_integration_desc": "Connect to Strava to easily import your activties into locations and visits", + "not_configured": "Strava Not Configured", + "admin_setup_required": "The server administrator must enable it globally", + "ready_to_connect": "Ready to Connect", + "connect_account": "Connect Account", + "account_connected": "Account Connected", + "disconnect": "Disconnect", + "authorization_error": "Error redirecting to strava authorization URL", + "disconnected": "Successfully disconnected from Strava", + "disconnect_error": "Error disconnecting from Strava" } } diff --git a/frontend/src/routes/settings/+page.server.ts b/frontend/src/routes/settings/+page.server.ts index 17dd5cc..b1bcdd5 100644 --- a/frontend/src/routes/settings/+page.server.ts +++ b/frontend/src/routes/settings/+page.server.ts @@ -80,7 +80,8 @@ export const load: PageServerLoad = async (event) => { } let integrations = await integrationsFetch.json(); let googleMapsEnabled = integrations.google_maps as boolean; - let stravaEnabled = integrations.strava as boolean; + let stravaGlobalEnabled = integrations.strava.global as boolean; + let stravaUserEnabled = integrations.strava.user as boolean; let publicUrlFetch = await fetch(`${endpoint}/public-url/`); let publicUrl = ''; @@ -100,7 +101,8 @@ export const load: PageServerLoad = async (event) => { publicUrl, socialProviders, googleMapsEnabled, - stravaEnabled + stravaGlobalEnabled, + stravaUserEnabled } }; }; diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index a056174..ed4c6dc 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -25,7 +25,8 @@ let public_url: string = data.props.publicUrl; let immichIntegration = data.props.immichIntegration; let googleMapsEnabled = data.props.googleMapsEnabled; - let stravaEnabled = data.props.stravaEnabled; + let stravaGlobalEnabled = data.props.stravaGlobalEnabled; + let stravaUserEnabled = data.props.stravaUserEnabled; let activeSection: string = 'profile'; let acknowledgeRestoreOverride: boolean = false; @@ -272,6 +273,36 @@ addToast('error', $t('settings.generic_error')); } } + + async function stravaAuthorizeRedirect() { + const res = await fetch('/api/integrations/strava/authorize/', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + if (res.ok) { + const data = await res.json(); + window.location.href = data.auth_url; + } else { + addToast('error', $t('strava.authorization_error')); + } + } + + async function stravaDisconnect() { + const res = await fetch('/api/integrations/strava/disable/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + if (res.ok) { + addToast('success', $t('strava.disconnected')); + stravaUserEnabled = false; + } else { + addToast('error', $t('strava.disconnect_error')); + } + } {#if isMFAModalOpen} @@ -913,43 +944,87 @@
{$t('settings.disconnected')}
{/if} -
-

- 📖 {$t('immich.need_help')} - {$t('navbar.documentation')} -

-
+ {#if user.is_staff || !googleMapsEnabled} +
+ {#if user.is_staff} +

+ 📖 {$t('immich.need_help')} + {$t('navbar.documentation')} +

+ {:else if !googleMapsEnabled} +

+ â„šī¸ {$t('google_maps.google_maps_integration_desc_no_staff')} +

+ {/if} +
+ {/if} +
Strava

Strava

- {$t('google_maps.google_maps_integration_desc')} + {$t('strava.strava_integration_desc')}

- {#if stravaEnabled} + {#if stravaGlobalEnabled && stravaUserEnabled}
{$t('settings.connected')}
{:else}
{$t('settings.disconnected')}
{/if}
-
-

- 📖 {$t('immich.need_help')} - {$t('navbar.documentation')} -

-
+ + + {#if !stravaGlobalEnabled} + +
+

+ {$t('strava.not_enabled') || + 'Strava integration is not enabled on this instance.'} +

+
+ {:else if !stravaUserEnabled && stravaGlobalEnabled} + +
+ +
+ {:else if stravaGlobalEnabled && stravaUserEnabled} + +
+ +
+ {/if} + + + {#if user.is_staff || !stravaGlobalEnabled} +
+ {#if user.is_staff} +

+ 📖 {$t('immich.need_help')} + {$t('navbar.documentation')} +

+ {:else if !stravaGlobalEnabled} +

+ â„šī¸ {$t('google_maps.google_maps_integration_desc_no_staff')} +

+ {/if} +
+ {/if}
{/if}