mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-08-05 13:15:18 +02:00
feat: add Wanderer integration with authentication and management features
This commit is contained in:
parent
2c23b6ca57
commit
9fd06911b2
14 changed files with 462 additions and 26 deletions
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 5.2.2 on 2025-08-04 16:40
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('adventures', '0058_alter_collectioninvite_options_activity'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='activity',
|
||||||
|
options={'verbose_name': 'Activity', 'verbose_name_plural': 'Activities'},
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,10 +1,11 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from allauth.account.decorators import secure_admin_login
|
from allauth.account.decorators import secure_admin_login
|
||||||
|
|
||||||
from .models import ImmichIntegration, StravaToken
|
from .models import ImmichIntegration, StravaToken, WandererIntegration
|
||||||
|
|
||||||
admin.autodiscover()
|
admin.autodiscover()
|
||||||
admin.site.login = secure_admin_login(admin.site.login)
|
admin.site.login = secure_admin_login(admin.site.login)
|
||||||
|
|
||||||
admin.site.register(ImmichIntegration)
|
admin.site.register(ImmichIntegration)
|
||||||
admin.site.register(StravaToken)
|
admin.site.register(StravaToken)
|
||||||
|
admin.site.register(WandererIntegration)
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Generated by Django 5.2.2 on 2025-08-04 16:40
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('integrations', '0003_stravatoken'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WandererIntegration',
|
||||||
|
fields=[
|
||||||
|
('server_url', models.CharField(max_length=255)),
|
||||||
|
('username', models.CharField(max_length=255)),
|
||||||
|
('token', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
('token_expiry', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wanderer_integrations', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Wanderer Integration',
|
||||||
|
'verbose_name_plural': 'Wanderer Integrations',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.2.2 on 2025-08-04 16:55
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('integrations', '0004_wandererintegration'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='wandererintegration',
|
||||||
|
name='token',
|
||||||
|
field=models.CharField(blank=True, max_length=1000, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.2.2 on 2025-08-04 17:03
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('integrations', '0005_alter_wandererintegration_token'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='wandererintegration',
|
||||||
|
name='token',
|
||||||
|
field=models.CharField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -24,3 +24,19 @@ class StravaToken(models.Model):
|
||||||
athlete_id = models.BigIntegerField(null=True, blank=True)
|
athlete_id = models.BigIntegerField(null=True, blank=True)
|
||||||
scope = models.CharField(max_length=255, null=True, blank=True)
|
scope = models.CharField(max_length=255, null=True, blank=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class WandererIntegration(models.Model):
|
||||||
|
server_url = models.CharField(max_length=255)
|
||||||
|
username = models.CharField(max_length=255)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
User, on_delete=models.CASCADE, related_name='wanderer_integrations')
|
||||||
|
token = models.CharField(null=True, blank=True)
|
||||||
|
token_expiry = models.DateTimeField(null=True, blank=True)
|
||||||
|
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.user.username + ' - ' + self.server_url
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Wanderer Integration"
|
||||||
|
verbose_name_plural = "Wanderer Integrations"
|
|
@ -1,7 +1,7 @@
|
||||||
from integrations.views import *
|
from integrations.views import *
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from integrations.views import IntegrationView, StravaIntegrationView
|
from integrations.views import IntegrationView, StravaIntegrationView, WandererIntegrationViewSet
|
||||||
|
|
||||||
# Create the router and register the ViewSet
|
# Create the router and register the ViewSet
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
|
@ -9,6 +9,7 @@ router.register(r'immich', ImmichIntegrationView, basename='immich')
|
||||||
router.register(r'', IntegrationView, basename='integrations')
|
router.register(r'', IntegrationView, basename='integrations')
|
||||||
router.register(r'immich', ImmichIntegrationViewSet, basename='immich_viewset')
|
router.register(r'immich', ImmichIntegrationViewSet, basename='immich_viewset')
|
||||||
router.register(r'strava', StravaIntegrationView, basename='strava')
|
router.register(r'strava', StravaIntegrationView, basename='strava')
|
||||||
|
router.register(r'wanderer', WandererIntegrationViewSet, basename='wanderer')
|
||||||
|
|
||||||
# Include the router URLs
|
# Include the router URLs
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
from .immich_view import ImmichIntegrationView, ImmichIntegrationViewSet
|
from .immich_view import ImmichIntegrationView, ImmichIntegrationViewSet
|
||||||
from .integration_view import IntegrationView
|
from .integration_view import IntegrationView
|
||||||
from .strava_view import StravaIntegrationView
|
from .strava_view import StravaIntegrationView
|
||||||
|
from .wanderer_view import WandererIntegrationViewSet
|
|
@ -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, StravaToken
|
from integrations.models import ImmichIntegration, StravaToken, WandererIntegration
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ class IntegrationView(viewsets.ViewSet):
|
||||||
google_map_integration = settings.GOOGLE_MAPS_API_KEY != ''
|
google_map_integration = settings.GOOGLE_MAPS_API_KEY != ''
|
||||||
strava_integration_global = 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()
|
strava_integration_user = StravaToken.objects.filter(user=request.user).exists()
|
||||||
|
wanderer_integration = WandererIntegration.objects.filter(user=request.user).exists()
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
|
@ -24,7 +25,8 @@ class IntegrationView(viewsets.ViewSet):
|
||||||
'strava': {
|
'strava': {
|
||||||
'global': strava_integration_global,
|
'global': strava_integration_global,
|
||||||
'user': strava_integration_user
|
'user': strava_integration_user
|
||||||
}
|
},
|
||||||
|
'wanderer': wanderer_integration
|
||||||
},
|
},
|
||||||
status=status.HTTP_200_OK
|
status=status.HTTP_200_OK
|
||||||
)
|
)
|
||||||
|
|
141
backend/server/integrations/views/wanderer_view.py
Normal file
141
backend/server/integrations/views/wanderer_view.py
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
# views.py
|
||||||
|
import requests
|
||||||
|
from rest_framework import viewsets, status
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.exceptions import ValidationError, NotFound
|
||||||
|
|
||||||
|
from integrations.models import WandererIntegration
|
||||||
|
from integrations.wanderer_services import get_valid_session, login_to_wanderer, IntegrationError
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
class WandererIntegrationViewSet(viewsets.ViewSet):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def _get_obj(self):
|
||||||
|
try:
|
||||||
|
return WandererIntegration.objects.filter(user=self.request.user).first()
|
||||||
|
except WandererIntegration.DoesNotExist:
|
||||||
|
raise NotFound("Wanderer integration not found.")
|
||||||
|
|
||||||
|
# def list(self, request):
|
||||||
|
# try:
|
||||||
|
# inst = self._get_obj()
|
||||||
|
# except NotFound:
|
||||||
|
# return Response([], status=status.HTTP_200_OK)
|
||||||
|
# return Response({
|
||||||
|
# "id": inst.id,
|
||||||
|
# "server_url": inst.server_url,
|
||||||
|
# "username": inst.username,
|
||||||
|
# "is_connected": bool(inst.token and inst.token_expiry and inst.token_expiry > timezone.now()),
|
||||||
|
# "token_expiry": inst.token_expiry,
|
||||||
|
# })
|
||||||
|
|
||||||
|
def create(self, request):
|
||||||
|
if WandererIntegration.objects.filter(user=request.user).exists():
|
||||||
|
raise ValidationError("Wanderer integration already exists. Use UPDATE instead.")
|
||||||
|
|
||||||
|
server_url = request.data.get("server_url")
|
||||||
|
username = request.data.get("username")
|
||||||
|
password = request.data.get("password")
|
||||||
|
if not server_url or not username or not password:
|
||||||
|
raise ValidationError(
|
||||||
|
"Must provide server_url, username + password in request data."
|
||||||
|
)
|
||||||
|
|
||||||
|
inst = WandererIntegration(
|
||||||
|
user=request.user,
|
||||||
|
server_url=server_url.rstrip("/"),
|
||||||
|
username=username,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
token, expiry = login_to_wanderer(inst, password)
|
||||||
|
except IntegrationError as e:
|
||||||
|
raise ValidationError({"error": str(e)})
|
||||||
|
|
||||||
|
inst.token = token
|
||||||
|
inst.token_expiry = expiry
|
||||||
|
inst.save()
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"message": "Wanderer integration created and authenticated successfully."},
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
def update(self, request, pk=None):
|
||||||
|
inst = self._get_obj()
|
||||||
|
changed = False
|
||||||
|
for field in ("server_url", "username"):
|
||||||
|
if field in request.data and getattr(inst, field) != request.data[field]:
|
||||||
|
setattr(inst, field, request.data[field].rstrip("/") if field=="server_url" else request.data[field])
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
password = request.data.get("password")
|
||||||
|
if not changed and not password:
|
||||||
|
return Response(
|
||||||
|
{"detail": "Nothing updated: send at least one of server_url, username, or password."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# If password provided: re-auth / token renewal
|
||||||
|
if password:
|
||||||
|
try:
|
||||||
|
token, expiry = login_to_wanderer(inst, password)
|
||||||
|
except IntegrationError as e:
|
||||||
|
raise ValidationError({"error": str(e)})
|
||||||
|
inst.token = token
|
||||||
|
inst.token_expiry = expiry
|
||||||
|
|
||||||
|
inst.save()
|
||||||
|
return Response({"message": "Integration updated successfully."})
|
||||||
|
|
||||||
|
@action(detail=False, methods=["post"])
|
||||||
|
def disable(self, request):
|
||||||
|
inst = self._get_obj()
|
||||||
|
inst.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
@action(detail=False, methods=["post"])
|
||||||
|
def refresh(self, request):
|
||||||
|
inst = self._get_obj()
|
||||||
|
|
||||||
|
password = request.data.get("password")
|
||||||
|
try:
|
||||||
|
session = get_valid_session(inst, password_for_reauth=password)
|
||||||
|
except IntegrationError as e:
|
||||||
|
raise ValidationError({"detail": str(e)})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
"token": inst.token,
|
||||||
|
"token_expiry": inst.token_expiry,
|
||||||
|
"is_connected": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
@action(detail=False, methods=["get"], url_path='list')
|
||||||
|
def list_trails(self, request):
|
||||||
|
inst = self._get_obj()
|
||||||
|
|
||||||
|
# Check if we need to prompt for password
|
||||||
|
password = request.query_params.get("password") # Allow password via query param if needed
|
||||||
|
|
||||||
|
try:
|
||||||
|
session = get_valid_session(inst, password_for_reauth=password)
|
||||||
|
except IntegrationError as e:
|
||||||
|
# If session expired and no password provided, give a helpful error
|
||||||
|
if "password is required" in str(e).lower():
|
||||||
|
raise ValidationError({
|
||||||
|
"detail": str(e),
|
||||||
|
"requires_password": True
|
||||||
|
})
|
||||||
|
raise ValidationError({"detail": str(e)})
|
||||||
|
|
||||||
|
url = f"{inst.server_url.rstrip('/')}/api/v1/list"
|
||||||
|
try:
|
||||||
|
response = session.get(url, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
except requests.RequestException as exc:
|
||||||
|
raise ValidationError({"detail": f"Error fetching trails: {exc}"})
|
||||||
|
|
||||||
|
return Response(response.json())
|
111
backend/server/integrations/wanderer_services.py
Normal file
111
backend/server/integrations/wanderer_services.py
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
# wanderer_services.py
|
||||||
|
import requests
|
||||||
|
from datetime import datetime
|
||||||
|
from datetime import timezone as dt_timezone
|
||||||
|
from django.utils import timezone as django_timezone
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .models import WandererIntegration
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class IntegrationError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Use both possible cookie names
|
||||||
|
COOKIE_NAMES = ("pb_auth", "pb-auth")
|
||||||
|
LOGIN_PATH = "/api/v1/auth/login"
|
||||||
|
|
||||||
|
def login_to_wanderer(integration: WandererIntegration, password: str):
|
||||||
|
"""
|
||||||
|
Authenticate with Wanderer and return the auth cookie and expiry.
|
||||||
|
"""
|
||||||
|
url = integration.server_url.rstrip("/") + LOGIN_PATH
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.post(url, json={
|
||||||
|
"username": integration.username,
|
||||||
|
"password": password
|
||||||
|
}, timeout=10)
|
||||||
|
resp.raise_for_status()
|
||||||
|
except requests.RequestException as exc:
|
||||||
|
logger.error("Error connecting to Wanderer login: %s", exc)
|
||||||
|
raise IntegrationError("Could not connect to Wanderer server.")
|
||||||
|
|
||||||
|
# Log only summary (not full token body)
|
||||||
|
logger.debug("Wanderer login status: %s, headers: %s", resp.status_code, resp.headers.get("Set-Cookie"))
|
||||||
|
|
||||||
|
# Extract auth cookie and expiry
|
||||||
|
token = None
|
||||||
|
expiry = None
|
||||||
|
for cookie in resp.cookies:
|
||||||
|
if cookie.name in COOKIE_NAMES:
|
||||||
|
token = cookie.value
|
||||||
|
if cookie.expires:
|
||||||
|
expiry = datetime.fromtimestamp(cookie.expires, tz=dt_timezone.utc)
|
||||||
|
else:
|
||||||
|
# If no expiry set, assume 24 hours from now
|
||||||
|
expiry = django_timezone.now() + django_timezone.timedelta(hours=24)
|
||||||
|
break
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
logger.error("Wanderer login succeeded but no auth cookie in response.")
|
||||||
|
raise IntegrationError("Authentication succeeded, but token cookie not found.")
|
||||||
|
|
||||||
|
logger.info(f"Successfully authenticated with Wanderer. Token expires: {expiry}")
|
||||||
|
return token, expiry
|
||||||
|
|
||||||
|
def get_valid_session(integration: WandererIntegration, password_for_reauth: str = None):
|
||||||
|
"""
|
||||||
|
Get a requests session with valid authentication.
|
||||||
|
Will reuse existing token if valid, or re-authenticate if needed.
|
||||||
|
"""
|
||||||
|
now = django_timezone.now()
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
# Check if we have a valid token
|
||||||
|
if integration.token and integration.token_expiry and integration.token_expiry > now:
|
||||||
|
logger.debug("Using existing valid token")
|
||||||
|
session.cookies.set(COOKIE_NAMES[0], integration.token)
|
||||||
|
return session
|
||||||
|
|
||||||
|
# Token expired or missing - need to re-authenticate
|
||||||
|
if password_for_reauth is None:
|
||||||
|
raise IntegrationError("Session expired; password is required to reconnect.")
|
||||||
|
|
||||||
|
logger.info("Token expired, re-authenticating with Wanderer")
|
||||||
|
token, expiry = login_to_wanderer(integration, password_for_reauth)
|
||||||
|
|
||||||
|
# Update the integration with new token
|
||||||
|
integration.token = token
|
||||||
|
integration.token_expiry = expiry
|
||||||
|
integration.save(update_fields=["token", "token_expiry"])
|
||||||
|
|
||||||
|
# Set the cookie in the session
|
||||||
|
session.cookies.set(COOKIE_NAMES[0], token)
|
||||||
|
return session
|
||||||
|
|
||||||
|
def make_wanderer_request(integration: WandererIntegration, endpoint: str, method: str = "GET", password_for_reauth: str = None, **kwargs):
|
||||||
|
"""
|
||||||
|
Helper function to make authenticated requests to Wanderer API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
integration: WandererIntegration instance
|
||||||
|
endpoint: API endpoint (e.g., '/api/v1/list')
|
||||||
|
method: HTTP method (GET, POST, etc.)
|
||||||
|
password_for_reauth: Password to use if re-authentication is needed
|
||||||
|
**kwargs: Additional arguments to pass to requests method
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
requests.Response object
|
||||||
|
"""
|
||||||
|
session = get_valid_session(integration, password_for_reauth)
|
||||||
|
url = f"{integration.server_url.rstrip('/')}{endpoint}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = getattr(session, method.lower())(url, timeout=10, **kwargs)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response
|
||||||
|
except requests.RequestException as exc:
|
||||||
|
logger.error(f"Error making {method} request to {url}: {exc}")
|
||||||
|
raise IntegrationError(f"Error communicating with Wanderer: {exc}")
|
|
@ -799,5 +799,10 @@
|
||||||
"authorization_error": "Error redirecting to strava authorization URL",
|
"authorization_error": "Error redirecting to strava authorization URL",
|
||||||
"disconnected": "Successfully disconnected from Strava",
|
"disconnected": "Successfully disconnected from Strava",
|
||||||
"disconnect_error": "Error disconnecting from Strava"
|
"disconnect_error": "Error disconnecting from Strava"
|
||||||
|
},
|
||||||
|
"wanderer": {
|
||||||
|
"wanderer_integration_desc": "Connect to Wanderer to easily import and view your trails in locations",
|
||||||
|
"connection_error": "Error connecting to Wanderer",
|
||||||
|
"connected": "Successfully connected to Wanderer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,7 @@ export const load: PageServerLoad = async (event) => {
|
||||||
let googleMapsEnabled = integrations.google_maps as boolean;
|
let googleMapsEnabled = integrations.google_maps as boolean;
|
||||||
let stravaGlobalEnabled = integrations.strava.global as boolean;
|
let stravaGlobalEnabled = integrations.strava.global as boolean;
|
||||||
let stravaUserEnabled = integrations.strava.user as boolean;
|
let stravaUserEnabled = integrations.strava.user as boolean;
|
||||||
|
let wandererEnabled = integrations.wanderer as boolean;
|
||||||
|
|
||||||
let publicUrlFetch = await fetch(`${endpoint}/public-url/`);
|
let publicUrlFetch = await fetch(`${endpoint}/public-url/`);
|
||||||
let publicUrl = '';
|
let publicUrl = '';
|
||||||
|
@ -102,7 +103,8 @@ export const load: PageServerLoad = async (event) => {
|
||||||
socialProviders,
|
socialProviders,
|
||||||
googleMapsEnabled,
|
googleMapsEnabled,
|
||||||
stravaGlobalEnabled,
|
stravaGlobalEnabled,
|
||||||
stravaUserEnabled
|
stravaUserEnabled,
|
||||||
|
wandererEnabled
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
let googleMapsEnabled = data.props.googleMapsEnabled;
|
let googleMapsEnabled = data.props.googleMapsEnabled;
|
||||||
let stravaGlobalEnabled = data.props.stravaGlobalEnabled;
|
let stravaGlobalEnabled = data.props.stravaGlobalEnabled;
|
||||||
let stravaUserEnabled = data.props.stravaUserEnabled;
|
let stravaUserEnabled = data.props.stravaUserEnabled;
|
||||||
|
let wandererEnabled = data.props.wandererEnabled;
|
||||||
let activeSection: string = 'profile';
|
let activeSection: string = 'profile';
|
||||||
|
|
||||||
// Initialize activeSection from URL on mount
|
// Initialize activeSection from URL on mount
|
||||||
|
@ -55,6 +56,12 @@
|
||||||
copy_locally: true
|
copy_locally: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let newWandererIntegration = {
|
||||||
|
server_url: '',
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
};
|
||||||
|
|
||||||
let isMFAModalOpen: boolean = false;
|
let isMFAModalOpen: boolean = false;
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
|
@ -321,6 +328,39 @@
|
||||||
addToast('error', $t('strava.disconnect_error'));
|
addToast('error', $t('strava.disconnect_error'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function wandererDisconnect() {
|
||||||
|
const res = await fetch('/api/integrations/wanderer/disable/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
addToast('success', $t('wanderer.disconnected'));
|
||||||
|
wandererEnabled = false;
|
||||||
|
} else {
|
||||||
|
addToast('error', $t('wanderer.disconnect_error'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wandererConnect() {
|
||||||
|
const res = await fetch('/api/integrations/wanderer/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(newWandererIntegration)
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
addToast('success', $t('wanderer.connected'));
|
||||||
|
wandererEnabled = true;
|
||||||
|
newWandererIntegration = { server_url: '', username: '', password: '' };
|
||||||
|
} else {
|
||||||
|
const data = await res.json();
|
||||||
|
addToast('error', $t('wanderer.connection_error'));
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isMFAModalOpen}
|
{#if isMFAModalOpen}
|
||||||
|
@ -1054,10 +1094,10 @@
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-bold">Wanderer</h3>
|
<h3 class="text-xl font-bold">Wanderer</h3>
|
||||||
<p class="text-sm text-base-content/70">
|
<p class="text-sm text-base-content/70">
|
||||||
{$t('strava.strava_integration_desc')}
|
{$t('wanderer.wanderer_integration_desc')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{#if stravaGlobalEnabled && stravaUserEnabled}
|
{#if wandererEnabled}
|
||||||
<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>
|
||||||
|
@ -1065,25 +1105,56 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content based on integration status -->
|
<!-- Content based on integration status -->
|
||||||
{#if !stravaGlobalEnabled}
|
{#if !wandererEnabled}
|
||||||
<!-- Strava not enabled globally -->
|
<!-- login form with server url username and password -->
|
||||||
<div class="text-center">
|
<div class="space-y-4">
|
||||||
<p class="text-base-content/70 mb-4">
|
<div class="form-control">
|
||||||
{$t('strava.not_enabled') ||
|
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||||
'Strava integration is not enabled on this instance.'}
|
<label class="label">
|
||||||
</p>
|
<span class="label-text font-medium">Server URL</span>
|
||||||
</div>
|
</label>
|
||||||
{:else if !stravaUserEnabled && stravaGlobalEnabled}
|
<input
|
||||||
<!-- Globally enabled but user not connected -->
|
type="url"
|
||||||
<div class="text-center">
|
class="input input-bordered input-primary focus:input-primary"
|
||||||
<button class="btn btn-primary" on:click={stravaAuthorizeRedirect}>
|
placeholder="https://wanderer.example.com"
|
||||||
🔗 {$t('strava.connect_account')}
|
bind:value={newWandererIntegration.server_url}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Username</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered input-primary focus:input-primary"
|
||||||
|
placeholder="Enter your username"
|
||||||
|
bind:value={newWandererIntegration.username}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Password</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="input input-bordered input-primary focus:input-primary"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
bind:value={newWandererIntegration.password}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary w-full" on:click={wandererConnect}>
|
||||||
|
🔗 Connect to Wanderer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{:else if stravaGlobalEnabled && stravaUserEnabled}
|
{:else}
|
||||||
<!-- User connected - show management options -->
|
<!-- User connected - show management options -->
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<button class="btn btn-error" on:click={stravaDisconnect}>
|
<button class="btn btn-error" on:click={wandererDisconnect}>
|
||||||
❌ {$t('strava.disconnect')}
|
❌ {$t('strava.disconnect')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1097,7 +1168,7 @@
|
||||||
📖 {$t('immich.need_help')}
|
📖 {$t('immich.need_help')}
|
||||||
<a
|
<a
|
||||||
class="link link-primary"
|
class="link link-primary"
|
||||||
href="https://adventurelog.app/docs/configuration/strava_integration.html"
|
href="https://adventurelog.app/docs/configuration/wanderer_integration.html"
|
||||||
target="_blank">{$t('navbar.documentation')}</a
|
target="_blank">{$t('navbar.documentation')}</a
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue