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:
parent
9bcada21dd
commit
1891a8b497
5 changed files with 152 additions and 39 deletions
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue