1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-08-05 05:05:17 +02:00

feat: enhance Strava integration with user-specific settings and management options; update localization strings

This commit is contained in:
Sean Morley 2025-08-02 09:56:02 -04:00
parent 9bcada21dd
commit 1891a8b497
5 changed files with 152 additions and 39 deletions

View file

@ -2,7 +2,7 @@ import os
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import viewsets, status from rest_framework import viewsets, status
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from integrations.models import ImmichIntegration from integrations.models import ImmichIntegration, StravaToken
from django.conf import settings from django.conf import settings
@ -14,13 +14,17 @@ class IntegrationView(viewsets.ViewSet):
""" """
immich_integrations = ImmichIntegration.objects.filter(user=request.user) immich_integrations = ImmichIntegration.objects.filter(user=request.user)
google_map_integration = settings.GOOGLE_MAPS_API_KEY != '' 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( return Response(
{ {
'immich': immich_integrations.exists(), 'immich': immich_integrations.exists(),
'google_maps': google_map_integration, 'google_maps': google_map_integration,
'strava': strava_integration 'strava': {
'global': strava_integration_global,
'user': strava_integration_user
}
}, },
status=status.HTTP_200_OK status=status.HTTP_200_OK
) )

View file

@ -5,6 +5,7 @@ from rest_framework.decorators import action
import requests import requests
import logging import logging
import time import time
from django.shortcuts import redirect
from django.conf import settings from django.conf import settings
from integrations.models import StravaToken from integrations.models import StravaToken
@ -87,16 +88,12 @@ class StravaIntegrationView(viewsets.ViewSet):
} }
) )
return Response( # redirect to frontend url / setttigns
{ frontend_url = settings.FRONTEND_URL
'message': 'Strava access token obtained and saved successfully.', if not frontend_url.endswith('/'):
'access_token': response_data.get('access_token'), frontend_url += '/'
'expires_at': response_data.get('expires_at'), return redirect(f"{frontend_url}settings?strava_authorized=true")
'refresh_token': response_data.get('refresh_token'),
'athlete': response_data.get('athlete'),
},
status=status.HTTP_200_OK
)
except requests.RequestException as e: except requests.RequestException as e:
logger.error("Error during Strava OAuth token exchange: %s", str(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 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): def refresh_strava_token_if_needed(self, user):
strava_token = StravaToken.objects.filter(user=user).first() strava_token = StravaToken.objects.filter(user=user).first()
if not strava_token: if not strava_token:

View file

@ -747,7 +747,8 @@
"network_error": "Network error while connecting to the Immich server. Please check your connection and try again." "network_error": "Network error while connecting to the Immich server. Please check your connection and try again."
}, },
"google_maps": { "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": { "recomendations": {
"recommendation": "Recommendation", "recommendation": "Recommendation",
@ -786,5 +787,17 @@
"invited_on": "Invited on", "invited_on": "Invited on",
"no_invites_desc": "Make sure your profile is public so users can invite you.", "no_invites_desc": "Make sure your profile is public so users can invite you.",
"by": "by" "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"
} }
} }

View file

@ -80,7 +80,8 @@ export const load: PageServerLoad = async (event) => {
} }
let integrations = await integrationsFetch.json(); let integrations = await integrationsFetch.json();
let googleMapsEnabled = integrations.google_maps as boolean; 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 publicUrlFetch = await fetch(`${endpoint}/public-url/`);
let publicUrl = ''; let publicUrl = '';
@ -100,7 +101,8 @@ export const load: PageServerLoad = async (event) => {
publicUrl, publicUrl,
socialProviders, socialProviders,
googleMapsEnabled, googleMapsEnabled,
stravaEnabled stravaGlobalEnabled,
stravaUserEnabled
} }
}; };
}; };

View file

@ -25,7 +25,8 @@
let public_url: string = data.props.publicUrl; let public_url: string = data.props.publicUrl;
let immichIntegration = data.props.immichIntegration; let immichIntegration = data.props.immichIntegration;
let googleMapsEnabled = data.props.googleMapsEnabled; 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 activeSection: string = 'profile';
let acknowledgeRestoreOverride: boolean = false; let acknowledgeRestoreOverride: boolean = false;
@ -272,6 +273,36 @@
addToast('error', $t('settings.generic_error')); 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'));
}
}
</script> </script>
{#if isMFAModalOpen} {#if isMFAModalOpen}
@ -913,43 +944,87 @@
<div class="badge badge-error ml-auto">{$t('settings.disconnected')}</div> <div class="badge badge-error ml-auto">{$t('settings.disconnected')}</div>
{/if} {/if}
</div> </div>
<div class="mt-4 p-4 bg-info/10 rounded-lg"> {#if user.is_staff || !googleMapsEnabled}
<p class="text-sm"> <div class="mt-4 p-4 bg-info/10 rounded-lg">
📖 {$t('immich.need_help')} {#if user.is_staff}
<a <p class="text-sm">
class="link link-primary" 📖 {$t('immich.need_help')}
href="https://adventurelog.app/docs/configuration/google_maps_integration.html" <a
target="_blank">{$t('navbar.documentation')}</a class="link link-primary"
> href="https://adventurelog.app/docs/configuration/google_maps_integration.html"
</p> target="_blank">{$t('navbar.documentation')}</a
</div> >
</p>
{:else if !googleMapsEnabled}
<p class="text-sm">
{$t('google_maps.google_maps_integration_desc_no_staff')}
</p>
{/if}
</div>
{/if}
</div> </div>
<!-- Strava Integration Section -->
<div class="p-6 bg-base-200 rounded-xl"> <div class="p-6 bg-base-200 rounded-xl">
<div class="flex items-center gap-4 mb-4"> <div class="flex items-center gap-4 mb-4">
<img src={StravaLogo} alt="Strava" class="w-8 h-8 rounded-md" /> <img src={StravaLogo} alt="Strava" class="w-8 h-8 rounded-md" />
<div> <div>
<h3 class="text-xl font-bold">Strava</h3> <h3 class="text-xl font-bold">Strava</h3>
<p class="text-sm text-base-content/70"> <p class="text-sm text-base-content/70">
{$t('google_maps.google_maps_integration_desc')} {$t('strava.strava_integration_desc')}
</p> </p>
</div> </div>
{#if stravaEnabled} {#if stravaGlobalEnabled && stravaUserEnabled}
<div class="badge badge-success ml-auto">{$t('settings.connected')}</div> <div class="badge badge-success ml-auto">{$t('settings.connected')}</div>
{:else} {:else}
<div class="badge badge-error ml-auto">{$t('settings.disconnected')}</div> <div class="badge badge-error ml-auto">{$t('settings.disconnected')}</div>
{/if} {/if}
</div> </div>
<div class="mt-4 p-4 bg-info/10 rounded-lg">
<p class="text-sm"> <!-- Content based on integration status -->
📖 {$t('immich.need_help')} {#if !stravaGlobalEnabled}
<a <!-- Strava not enabled globally -->
class="link link-primary" <div class="text-center">
href="https://adventurelog.app/docs/configuration/google_maps_integration.html" <p class="text-base-content/70 mb-4">
target="_blank">{$t('navbar.documentation')}</a {$t('strava.not_enabled') ||
> 'Strava integration is not enabled on this instance.'}
</p> </p>
</div> </div>
{:else if !stravaUserEnabled && stravaGlobalEnabled}
<!-- Globally enabled but user not connected -->
<div class="text-center">
<button class="btn btn-primary" on:click={stravaAuthorizeRedirect}>
🔗 {$t('strava.connect_account')}
</button>
</div>
{:else if stravaGlobalEnabled && stravaUserEnabled}
<!-- User connected - show management options -->
<div class="text-center">
<button class="btn btn-error" on:click={stravaDisconnect}>
{$t('strava.disconnect')}
</button>
</div>
{/if}
<!-- Help documentation link -->
{#if user.is_staff || !stravaGlobalEnabled}
<div class="mt-4 p-4 bg-info/10 rounded-lg">
{#if user.is_staff}
<p class="text-sm">
📖 {$t('immich.need_help')}
<a
class="link link-primary"
href="https://adventurelog.app/docs/configuration/strava_integration.html"
target="_blank">{$t('navbar.documentation')}</a
>
</p>
{:else if !stravaGlobalEnabled}
<p class="text-sm">
{$t('google_maps.google_maps_integration_desc_no_staff')}
</p>
{/if}
</div>
{/if}
</div> </div>
</div> </div>
{/if} {/if}