feat: ship MVP itinerary optimization, weather, AI key prefs, and MCP tools
This commit is contained in:
@@ -1,11 +1,45 @@
|
||||
from django.contrib import admin
|
||||
from allauth.account.decorators import secure_admin_login
|
||||
from django.utils.html import format_html
|
||||
|
||||
from .models import ImmichIntegration, StravaToken, WandererIntegration
|
||||
from .models import (
|
||||
ImmichIntegration,
|
||||
StravaToken,
|
||||
WandererIntegration,
|
||||
UserAPIKey,
|
||||
UserRecommendationPreferenceProfile,
|
||||
)
|
||||
|
||||
admin.autodiscover()
|
||||
admin.site.login = secure_admin_login(admin.site.login)
|
||||
|
||||
admin.site.register(ImmichIntegration)
|
||||
admin.site.register(StravaToken)
|
||||
admin.site.register(WandererIntegration)
|
||||
admin.site.register(WandererIntegration)
|
||||
|
||||
|
||||
@admin.register(UserAPIKey)
|
||||
class UserAPIKeyAdmin(admin.ModelAdmin):
|
||||
list_display = ("user", "provider", "masked_value", "updated_at")
|
||||
search_fields = ("user__username", "provider")
|
||||
readonly_fields = (
|
||||
"user",
|
||||
"provider",
|
||||
"masked_value",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
exclude = ("encrypted_api_key",)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
@admin.display(description="API key")
|
||||
def masked_value(self, obj):
|
||||
return format_html("<code>{}</code>", obj.masked_api_key)
|
||||
|
||||
|
||||
admin.site.register(UserRecommendationPreferenceProfile)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Generated by Django 5.2.12 on 2026-03-08
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("integrations", "0006_alter_wandererintegration_token"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="UserRecommendationPreferenceProfile",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
("cuisines", models.TextField(blank=True, null=True)),
|
||||
("interests", models.JSONField(blank=True, default=list)),
|
||||
("trip_style", models.CharField(blank=True, max_length=120, null=True)),
|
||||
("notes", models.TextField(blank=True, null=True)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"user",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="recommendation_profile",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="UserAPIKey",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
("provider", models.CharField(max_length=100)),
|
||||
("encrypted_api_key", models.TextField()),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="api_keys",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("user", "provider")},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,23 +1,52 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class EncryptionConfigurationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def get_field_fernet() -> Fernet:
|
||||
key = getattr(settings, "FIELD_ENCRYPTION_KEY", None)
|
||||
if not key:
|
||||
raise EncryptionConfigurationError(
|
||||
"FIELD_ENCRYPTION_KEY is not configured. API key storage is unavailable."
|
||||
)
|
||||
|
||||
key_bytes = key.encode() if isinstance(key, str) else key
|
||||
try:
|
||||
return Fernet(key_bytes)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise EncryptionConfigurationError(
|
||||
"FIELD_ENCRYPTION_KEY is invalid. Provide a valid Fernet key."
|
||||
) from exc
|
||||
|
||||
|
||||
class ImmichIntegration(models.Model):
|
||||
server_url = models.CharField(max_length=255)
|
||||
api_key = models.CharField(max_length=255)
|
||||
user = models.ForeignKey(
|
||||
User, on_delete=models.CASCADE)
|
||||
copy_locally = models.BooleanField(default=True, help_text="Copy image to local storage, instead of just linking to the remote URL.")
|
||||
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
copy_locally = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Copy image to local storage, instead of just linking to the remote URL.",
|
||||
)
|
||||
id = models.UUIDField(
|
||||
default=uuid.uuid4, editable=False, unique=True, primary_key=True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username + ' - ' + self.server_url
|
||||
|
||||
return self.user.username + " - " + self.server_url
|
||||
|
||||
|
||||
class StravaToken(models.Model):
|
||||
user = models.ForeignKey(
|
||||
User, on_delete=models.CASCADE, related_name='strava_tokens')
|
||||
User, on_delete=models.CASCADE, related_name="strava_tokens"
|
||||
)
|
||||
access_token = models.CharField(max_length=255)
|
||||
refresh_token = models.CharField(max_length=255)
|
||||
expires_at = models.BigIntegerField() # Unix timestamp
|
||||
@@ -25,18 +54,73 @@ class StravaToken(models.Model):
|
||||
scope = models.CharField(max_length=255, null=True, blank=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')
|
||||
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)
|
||||
id = models.UUIDField(
|
||||
default=uuid.uuid4, editable=False, unique=True, primary_key=True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username + ' - ' + self.server_url
|
||||
|
||||
return self.user.username + " - " + self.server_url
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Wanderer Integration"
|
||||
verbose_name_plural = "Wanderer Integrations"
|
||||
verbose_name_plural = "Wanderer Integrations"
|
||||
|
||||
|
||||
class UserAPIKey(models.Model):
|
||||
id = models.UUIDField(
|
||||
default=uuid.uuid4, editable=False, unique=True, primary_key=True
|
||||
)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="api_keys")
|
||||
provider = models.CharField(max_length=100)
|
||||
encrypted_api_key = models.TextField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ("user", "provider")
|
||||
|
||||
def set_api_key(self, value: str) -> None:
|
||||
if value is None:
|
||||
raise ValueError("API key cannot be None")
|
||||
fernet = get_field_fernet()
|
||||
self.encrypted_api_key = fernet.encrypt(value.encode()).decode()
|
||||
|
||||
def get_api_key(self) -> str | None:
|
||||
if not self.encrypted_api_key:
|
||||
return None
|
||||
fernet = get_field_fernet()
|
||||
try:
|
||||
return fernet.decrypt(self.encrypted_api_key.encode()).decode()
|
||||
except (InvalidToken, ValueError):
|
||||
return None
|
||||
|
||||
@property
|
||||
def masked_api_key(self) -> str:
|
||||
plain = self.get_api_key() or ""
|
||||
if len(plain) <= 6:
|
||||
return "*" * len(plain)
|
||||
return f"{plain[:3]}{'*' * (len(plain) - 6)}{plain[-3:]}"
|
||||
|
||||
|
||||
class UserRecommendationPreferenceProfile(models.Model):
|
||||
id = models.UUIDField(
|
||||
default=uuid.uuid4, editable=False, unique=True, primary_key=True
|
||||
)
|
||||
user = models.OneToOneField(
|
||||
User, on_delete=models.CASCADE, related_name="recommendation_profile"
|
||||
)
|
||||
cuisines = models.TextField(blank=True, null=True)
|
||||
interests = models.JSONField(default=list, blank=True)
|
||||
trip_style = models.CharField(max_length=120, blank=True, null=True)
|
||||
notes = models.TextField(blank=True, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
@@ -1,13 +1,82 @@
|
||||
from .models import ImmichIntegration
|
||||
from .models import (
|
||||
EncryptionConfigurationError,
|
||||
ImmichIntegration,
|
||||
UserAPIKey,
|
||||
UserRecommendationPreferenceProfile,
|
||||
)
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class ImmichIntegrationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ImmichIntegration
|
||||
fields = '__all__'
|
||||
read_only_fields = ['id', 'user']
|
||||
fields = "__all__"
|
||||
read_only_fields = ["id", "user"]
|
||||
|
||||
def to_representation(self, instance):
|
||||
representation = super().to_representation(instance)
|
||||
representation.pop('user', None)
|
||||
representation.pop("user", None)
|
||||
return representation
|
||||
|
||||
|
||||
class UserAPIKeySerializer(serializers.ModelSerializer):
|
||||
api_key = serializers.CharField(write_only=True, required=True, allow_blank=False)
|
||||
masked_api_key = serializers.CharField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = UserAPIKey
|
||||
fields = [
|
||||
"id",
|
||||
"provider",
|
||||
"api_key",
|
||||
"masked_api_key",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = ["id", "masked_api_key", "created_at", "updated_at"]
|
||||
|
||||
def validate_provider(self, value):
|
||||
return (value or "").strip().lower()
|
||||
|
||||
def create(self, validated_data):
|
||||
api_key = validated_data.pop("api_key")
|
||||
user = self.context["request"].user
|
||||
instance = UserAPIKey(user=user, **validated_data)
|
||||
try:
|
||||
instance.set_api_key(api_key)
|
||||
except EncryptionConfigurationError as exc:
|
||||
raise serializers.ValidationError({"api_key": str(exc)}) from exc
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
api_key = validated_data.pop("api_key", None)
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
if api_key is not None:
|
||||
try:
|
||||
instance.set_api_key(api_key)
|
||||
except EncryptionConfigurationError as exc:
|
||||
raise serializers.ValidationError({"api_key": str(exc)}) from exc
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
def to_representation(self, instance):
|
||||
representation = super().to_representation(instance)
|
||||
representation.pop("api_key", None)
|
||||
return representation
|
||||
|
||||
|
||||
class UserRecommendationPreferenceProfileSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = UserRecommendationPreferenceProfile
|
||||
fields = [
|
||||
"id",
|
||||
"cuisines",
|
||||
"interests",
|
||||
"trip_style",
|
||||
"notes",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
@@ -1,3 +1,53 @@
|
||||
from django.test import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
# Create your tests here.
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import override_settings
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class UserAPIKeyConfigurationTests(APITestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
username="api-key-user",
|
||||
email="apikey@example.com",
|
||||
password="password123",
|
||||
)
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
@override_settings(FIELD_ENCRYPTION_KEY="")
|
||||
def test_api_key_endpoint_missing_encryption_key_is_graceful(self):
|
||||
response = self.client.get("/api/integrations/api-keys/")
|
||||
self.assertEqual(response.status_code, 503)
|
||||
self.assertIn("FIELD_ENCRYPTION_KEY", response.json().get("detail", ""))
|
||||
|
||||
@override_settings(FIELD_ENCRYPTION_KEY="invalid-key")
|
||||
def test_api_key_endpoint_invalid_encryption_key_is_graceful(self):
|
||||
response = self.client.get("/api/integrations/api-keys/")
|
||||
self.assertEqual(response.status_code, 503)
|
||||
self.assertIn("invalid", response.json().get("detail", "").lower())
|
||||
|
||||
@override_settings(FIELD_ENCRYPTION_KEY="")
|
||||
@patch("adventures.views.recommendations_view.requests.get")
|
||||
def test_google_photo_uses_graceful_fallback_when_user_key_unreadable(
|
||||
self, mock_requests_get
|
||||
):
|
||||
from integrations.models import UserAPIKey
|
||||
|
||||
# Legacy/bad row exists but cannot be decrypted due to missing key.
|
||||
UserAPIKey.objects.create(
|
||||
user=self.user,
|
||||
provider="google_maps",
|
||||
encrypted_api_key="not-a-valid-fernet-token",
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
"/api/recommendations/google-photo/?photo_name=places/abc/photos/def"
|
||||
)
|
||||
|
||||
# Should fail gracefully as misconfigured key path, not crash (500).
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn("not configured", response.json().get("error", "").lower())
|
||||
mock_requests_get.assert_not_called()
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
from integrations.views import *
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from integrations.views import IntegrationView, StravaIntegrationView, WandererIntegrationViewSet
|
||||
from integrations.views import (
|
||||
IntegrationView,
|
||||
StravaIntegrationView,
|
||||
WandererIntegrationViewSet,
|
||||
UserAPIKeyViewSet,
|
||||
UserRecommendationPreferenceProfileViewSet,
|
||||
)
|
||||
|
||||
# Create the router and register the ViewSet
|
||||
router = DefaultRouter()
|
||||
router.register(r'immich', ImmichIntegrationView, basename='immich')
|
||||
router.register(r'', IntegrationView, basename='integrations')
|
||||
router.register(r'immich', ImmichIntegrationViewSet, basename='immich_viewset')
|
||||
router.register(r'strava', StravaIntegrationView, basename='strava')
|
||||
router.register(r'wanderer', WandererIntegrationViewSet, basename='wanderer')
|
||||
router.register(r"immich", ImmichIntegrationView, basename="immich")
|
||||
router.register(r"", IntegrationView, basename="integrations")
|
||||
router.register(r"immich", ImmichIntegrationViewSet, basename="immich_viewset")
|
||||
router.register(r"strava", StravaIntegrationView, basename="strava")
|
||||
router.register(r"wanderer", WandererIntegrationViewSet, basename="wanderer")
|
||||
router.register(r"api-keys", UserAPIKeyViewSet, basename="user-api-keys")
|
||||
router.register(
|
||||
r"recommendation-preferences",
|
||||
UserRecommendationPreferenceProfileViewSet,
|
||||
basename="user-recommendation-preferences",
|
||||
)
|
||||
|
||||
# Include the router URLs
|
||||
urlpatterns = [
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from .immich_view import ImmichIntegrationView, ImmichIntegrationViewSet
|
||||
from .integration_view import IntegrationView
|
||||
from .strava_view import StravaIntegrationView
|
||||
from .wanderer_view import WandererIntegrationViewSet
|
||||
from .wanderer_view import WandererIntegrationViewSet
|
||||
from .user_api_key_view import UserAPIKeyViewSet
|
||||
from .recommendation_profile_view import UserRecommendationPreferenceProfileViewSet
|
||||
|
||||
@@ -3,40 +3,77 @@ from rest_framework.response import Response
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django.utils import timezone
|
||||
from integrations.models import ImmichIntegration, StravaToken, WandererIntegration
|
||||
from integrations.models import (
|
||||
EncryptionConfigurationError,
|
||||
ImmichIntegration,
|
||||
StravaToken,
|
||||
WandererIntegration,
|
||||
UserAPIKey,
|
||||
get_field_fernet,
|
||||
)
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class IntegrationView(viewsets.ViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def list(self, request):
|
||||
"""
|
||||
RESTful GET method for listing all integrations.
|
||||
"""
|
||||
immich_integrations = ImmichIntegration.objects.filter(user=request.user)
|
||||
google_map_integration = settings.GOOGLE_MAPS_API_KEY != ''
|
||||
strava_integration_global = settings.STRAVA_CLIENT_ID != '' and settings.STRAVA_CLIENT_SECRET != ''
|
||||
google_map_integration = (
|
||||
settings.GOOGLE_MAPS_API_KEY != ""
|
||||
or UserAPIKey.objects.filter(
|
||||
user=request.user,
|
||||
provider="google_maps",
|
||||
).exists()
|
||||
)
|
||||
strava_integration_global = (
|
||||
settings.STRAVA_CLIENT_ID != "" and settings.STRAVA_CLIENT_SECRET != ""
|
||||
)
|
||||
strava_integration_user = StravaToken.objects.filter(user=request.user).exists()
|
||||
wanderer_integration = WandererIntegration.objects.filter(user=request.user).exists()
|
||||
wanderer_integration = WandererIntegration.objects.filter(
|
||||
user=request.user
|
||||
).exists()
|
||||
is_wanderer_expired = False
|
||||
|
||||
if wanderer_integration:
|
||||
token_expiry = WandererIntegration.objects.filter(user=request.user).first().token_expiry
|
||||
token_expiry = (
|
||||
WandererIntegration.objects.filter(user=request.user)
|
||||
.first()
|
||||
.token_expiry
|
||||
)
|
||||
if token_expiry and token_expiry < timezone.now():
|
||||
is_wanderer_expired = True
|
||||
|
||||
api_key_status = {
|
||||
"enabled": UserAPIKey.objects.filter(user=request.user).exists(),
|
||||
"available": True,
|
||||
"error": None,
|
||||
}
|
||||
try:
|
||||
get_field_fernet()
|
||||
except EncryptionConfigurationError as exc:
|
||||
api_key_status = {
|
||||
"enabled": False,
|
||||
"available": False,
|
||||
"error": str(exc),
|
||||
}
|
||||
|
||||
return Response(
|
||||
{
|
||||
'immich': immich_integrations.exists(),
|
||||
'google_maps': google_map_integration,
|
||||
'strava': {
|
||||
'global': strava_integration_global,
|
||||
'user': strava_integration_user
|
||||
"immich": immich_integrations.exists(),
|
||||
"google_maps": google_map_integration,
|
||||
"api_keys": api_key_status,
|
||||
"strava": {
|
||||
"global": strava_integration_global,
|
||||
"user": strava_integration_user,
|
||||
},
|
||||
"wanderer": {
|
||||
"exists": wanderer_integration,
|
||||
"expired": is_wanderer_expired,
|
||||
},
|
||||
'wanderer': {
|
||||
'exists': wanderer_integration,
|
||||
'expired': is_wanderer_expired
|
||||
}
|
||||
},
|
||||
status=status.HTTP_200_OK
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from integrations.models import UserRecommendationPreferenceProfile
|
||||
from integrations.serializers import UserRecommendationPreferenceProfileSerializer
|
||||
|
||||
|
||||
class UserRecommendationPreferenceProfileViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = UserRecommendationPreferenceProfileSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return UserRecommendationPreferenceProfile.objects.filter(
|
||||
user=self.request.user
|
||||
)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
instance = self.get_queryset().first()
|
||||
if not instance:
|
||||
return Response([], status=status.HTTP_200_OK)
|
||||
serializer = self.get_serializer(instance)
|
||||
return Response([serializer.data], status=status.HTTP_200_OK)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
existing = UserRecommendationPreferenceProfile.objects.filter(
|
||||
user=self.request.user
|
||||
).first()
|
||||
if existing:
|
||||
for field, value in serializer.validated_data.items():
|
||||
setattr(existing, field, value)
|
||||
existing.save()
|
||||
self._upserted_instance = existing
|
||||
return
|
||||
|
||||
self._upserted_instance = serializer.save(user=self.request.user)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
output = self.get_serializer(self._upserted_instance)
|
||||
return Response(output.data, status=status.HTTP_200_OK)
|
||||
33
backend/server/integrations/views/user_api_key_view.py
Normal file
33
backend/server/integrations/views/user_api_key_view.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from integrations.models import (
|
||||
EncryptionConfigurationError,
|
||||
UserAPIKey,
|
||||
get_field_fernet,
|
||||
)
|
||||
from integrations.serializers import UserAPIKeySerializer
|
||||
|
||||
|
||||
class APIKeyConfigurationError(APIException):
|
||||
status_code = 503
|
||||
default_detail = (
|
||||
"API key storage is unavailable due to server encryption configuration."
|
||||
)
|
||||
default_code = "api_key_encryption_unavailable"
|
||||
|
||||
|
||||
class UserAPIKeyViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = UserAPIKeySerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
try:
|
||||
get_field_fernet()
|
||||
except EncryptionConfigurationError as exc:
|
||||
raise APIKeyConfigurationError(detail=str(exc)) from exc
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
return UserAPIKey.objects.filter(user=self.request.user).order_by("provider")
|
||||
Reference in New Issue
Block a user