1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-08-04 20:55:19 +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 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
)

View file

@ -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:

View file

@ -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"
}
}

View file

@ -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
}
};
};

View file

@ -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'));
}
}
</script>
{#if isMFAModalOpen}
@ -913,43 +944,87 @@
<div class="badge badge-error ml-auto">{$t('settings.disconnected')}</div>
{/if}
</div>
<div class="mt-4 p-4 bg-info/10 rounded-lg">
<p class="text-sm">
📖 {$t('immich.need_help')}
<a
class="link link-primary"
href="https://adventurelog.app/docs/configuration/google_maps_integration.html"
target="_blank">{$t('navbar.documentation')}</a
>
</p>
</div>
{#if user.is_staff || !googleMapsEnabled}
<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/google_maps_integration.html"
target="_blank">{$t('navbar.documentation')}</a
>
</p>
{:else if !googleMapsEnabled}
<p class="text-sm">
{$t('google_maps.google_maps_integration_desc_no_staff')}
</p>
{/if}
</div>
{/if}
</div>
<!-- Strava Integration Section -->
<div class="p-6 bg-base-200 rounded-xl">
<div class="flex items-center gap-4 mb-4">
<img src={StravaLogo} alt="Strava" class="w-8 h-8 rounded-md" />
<div>
<h3 class="text-xl font-bold">Strava</h3>
<p class="text-sm text-base-content/70">
{$t('google_maps.google_maps_integration_desc')}
{$t('strava.strava_integration_desc')}
</p>
</div>
{#if stravaEnabled}
{#if stravaGlobalEnabled && stravaUserEnabled}
<div class="badge badge-success ml-auto">{$t('settings.connected')}</div>
{:else}
<div class="badge badge-error ml-auto">{$t('settings.disconnected')}</div>
{/if}
</div>
<div class="mt-4 p-4 bg-info/10 rounded-lg">
<p class="text-sm">
📖 {$t('immich.need_help')}
<a
class="link link-primary"
href="https://adventurelog.app/docs/configuration/google_maps_integration.html"
target="_blank">{$t('navbar.documentation')}</a
>
</p>
</div>
<!-- Content based on integration status -->
{#if !stravaGlobalEnabled}
<!-- Strava not enabled globally -->
<div class="text-center">
<p class="text-base-content/70 mb-4">
{$t('strava.not_enabled') ||
'Strava integration is not enabled on this instance.'}
</p>
</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>
{/if}