Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/server/adventures/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class CustomUserAdmin(UserAdmin):
readonly_fields = ('uuid',)
search_fields = ('username',)
fieldsets = UserAdmin.fieldsets + (
(None, {'fields': ('profile_pic', 'uuid', 'public_profile')}),
(None, {'fields': ('profile_pic', 'uuid', 'public_profile', 'disable_password')}),
)
def image_display(self, obj):
if obj.profile_pic:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 5.0.8 on 2025-03-17 01:15

import adventures.models
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('adventures', '0023_lodging_delete_hotel'),
]

operations = [
migrations.AlterField(
model_name='attachment',
name='file',
field=models.FileField(upload_to=adventures.models.PathAndRename('attachments/'), validators=[adventures.models.validate_file_extension]),
),
]
11 changes: 9 additions & 2 deletions backend/server/adventures/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from collections.abc import Collection
from django.core.exceptions import ValidationError
import os
from typing import Iterable
import uuid
Expand All @@ -10,6 +10,13 @@
from django.forms import ValidationError
from django_resized import ResizedImageField

def validate_file_extension(value):
import os
from django.core.exceptions import ValidationError
ext = os.path.splitext(value.name)[1] # [0] returns path+filename
valid_extensions = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.mp4', '.mov', '.avi', '.mkv', '.mp3', '.wav', '.flac', '.ogg', '.m4a', '.wma', '.aac', '.opus', '.zip', '.rar', '.7z', '.tar', '.gz', '.bz2', '.xz', '.zst', '.lz4', '.lzma', '.lzo', '.z', '.tar.gz', '.tar.bz2', '.tar.xz', '.tar.zst', '.tar.lz4', '.tar.lzma', '.tar.lzo', '.tar.z', 'gpx', 'md', 'pdf']
if not ext.lower() in valid_extensions:
raise ValidationError('Unsupported file extension.')

ADVENTURE_TYPES = [
('general', 'General 🌍'),
Expand Down Expand Up @@ -306,7 +313,7 @@ class Attachment(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
file = models.FileField(upload_to=PathAndRename('attachments/'))
file = models.FileField(upload_to=PathAndRename('attachments/'),validators=[validate_file_extension])
adventure = models.ForeignKey(Adventure, related_name='attachments', on_delete=models.CASCADE)
name = models.CharField(max_length=200, null=True, blank=True)

Expand Down
4 changes: 4 additions & 0 deletions backend/server/main/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@
"socialaccount_login_error": f"{FRONTEND_URL}/account/provider/callback",
}

AUTHENTICATION_BACKENDS = [
'users.backends.NoPasswordAuthBackend',
]

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
SITE_ID = 1
ACCOUNT_EMAIL_REQUIRED = True
Expand Down
4 changes: 3 additions & 1 deletion backend/server/main/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.urls import include, re_path, path
from django.contrib import admin
from django.views.generic import RedirectView, TemplateView
from users.views import IsRegistrationDisabled, PublicUserListView, PublicUserDetailView, UserMetadataView, UpdateUserMetadataView, EnabledSocialProvidersView
from users.views import IsRegistrationDisabled, PublicUserListView, PublicUserDetailView, UserMetadataView, UpdateUserMetadataView, EnabledSocialProvidersView, DisablePasswordAuthenticationView
from .views import get_csrf_token, get_public_url, serve_protected_media
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
Expand Down Expand Up @@ -29,6 +29,8 @@

path('auth/social-providers/', EnabledSocialProvidersView.as_view(), name='enabled-social-providers'),

path('auth/disable-password/', DisablePasswordAuthenticationView.as_view(), name='disable-password-authentication'),

path('csrf/', get_csrf_token, name='get_csrf_token'),
path('public-url/', get_public_url, name='get_public_url'),

Expand Down
16 changes: 16 additions & 0 deletions backend/server/users/backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.contrib.auth.backends import ModelBackend
from allauth.socialaccount.models import SocialAccount

class NoPasswordAuthBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
print("NoPasswordAuthBackend")
# First, attempt normal authentication
user = super().authenticate(request, username=username, password=password, **kwargs)
if user is None:
return None

if SocialAccount.objects.filter(user=user).exists() and user.disable_password:
# If yes, disable login via password
return None

return user
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.0.8 on 2025-03-17 01:15

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('users', '0003_alter_customuser_email'),
]

operations = [
migrations.AddField(
model_name='customuser',
name='disable_password',
field=models.BooleanField(default=False),
),
]
1 change: 1 addition & 0 deletions backend/server/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class CustomUser(AbstractUser):
profile_pic = ResizedImageField(force_format="WEBP", quality=75, null=True, blank=True, upload_to='profile-pics/')
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
public_profile = models.BooleanField(default=False)
disable_password = models.BooleanField(default=False)

def __str__(self):
return self.username
10 changes: 6 additions & 4 deletions backend/server/users/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@ class Meta:
extra_fields.append('date_joined')
if hasattr(UserModel, 'is_staff'):
extra_fields.append('is_staff')
if hasattr(UserModel, 'disable_password'):
extra_fields.append('disable_password')

fields = ['pk', *extra_fields]
read_only_fields = ('email', 'date_joined', 'is_staff', 'is_superuser', 'is_active', 'pk')
read_only_fields = ('email', 'date_joined', 'is_staff', 'is_superuser', 'is_active', 'pk', 'disable_password')

def handle_public_profile_change(self, instance, validated_data):
"""
Expand Down Expand Up @@ -94,8 +96,8 @@ class CustomUserDetailsSerializer(UserDetailsSerializer):

class Meta(UserDetailsSerializer.Meta):
model = CustomUser
fields = UserDetailsSerializer.Meta.fields + ['profile_pic', 'uuid', 'public_profile', 'has_password']
read_only_fields = UserDetailsSerializer.Meta.read_only_fields + ('uuid', 'has_password')
fields = UserDetailsSerializer.Meta.fields + ['profile_pic', 'uuid', 'public_profile', 'has_password', 'disable_password']
read_only_fields = UserDetailsSerializer.Meta.read_only_fields + ('uuid', 'has_password', 'disable_password')

@staticmethod
def get_has_password(instance):
Expand All @@ -120,5 +122,5 @@ def to_representation(self, instance):
representation.pop('pk', None)
# Remove the email field
representation.pop('email', None)

return representation
35 changes: 34 additions & 1 deletion backend/server/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from allauth.socialaccount.models import SocialApp
from adventures.serializers import AdventureSerializer, CollectionSerializer
from adventures.models import Adventure, Collection
from allauth.socialaccount.models import SocialAccount

User = get_user_model()

Expand Down Expand Up @@ -71,6 +72,7 @@ def get(self, request):
# for every user, remove the field has_password
for user in serializer.data:
user.pop('has_password', None)
user.pop('disable_password', None)
return Response(serializer.data, status=status.HTTP_200_OK)

class PublicUserDetailView(APIView):
Expand Down Expand Up @@ -171,4 +173,35 @@ def get(self, request):
'url': f"{getenv('PUBLIC_URL')}/accounts/{new_provider}/login/",
'name': provider.name
})
return Response(providers, status=status.HTTP_200_OK)
return Response(providers, status=status.HTTP_200_OK)


class DisablePasswordAuthenticationView(APIView):
"""
Disable password authentication for a user. This is used when a user signs up with a social provider.
"""

# Allows the user to set the disable_password field to True if they have a social account linked
permission_classes = [IsAuthenticated]

@swagger_auto_schema(
responses={
200: openapi.Response('Password authentication disabled'),
400: 'Bad Request'
},
operation_description="Disable password authentication."
)
def post(self, request):
user = request.user
if SocialAccount.objects.filter(user=user).exists():
user.disable_password = True
user.save()
return Response({"detail": "Password authentication disabled."}, status=status.HTTP_200_OK)
return Response({"detail": "No social account linked."}, status=status.HTTP_400_BAD_REQUEST)

def delete(self, request):
user = request.user
user.disable_password = False
user.save()
return Response({"detail": "Password authentication enabled."}, status=status.HTTP_200_OK)

8 changes: 6 additions & 2 deletions backend/server/worldtravel/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,23 @@ class Meta:

class RegionSerializer(serializers.ModelSerializer):
num_cities = serializers.SerializerMethodField()
country_name = serializers.CharField(source='country.name', read_only=True)
class Meta:
model = Region
fields = '__all__'
read_only_fields = ['id', 'name', 'country', 'longitude', 'latitude', 'num_cities']
read_only_fields = ['id', 'name', 'country', 'longitude', 'latitude', 'num_cities', 'country_name']

def get_num_cities(self, obj):
return City.objects.filter(region=obj).count()

class CitySerializer(serializers.ModelSerializer):
region_name = serializers.CharField(source='region.name', read_only=True)
country_name = serializers.CharField(source='region.country.name', read_only=True
)
class Meta:
model = City
fields = '__all__'
read_only_fields = ['id', 'name', 'region', 'longitude', 'latitude']
read_only_fields = ['id', 'name', 'region', 'longitude', 'latitude', 'region_name', 'country_name']

class VisitedRegionSerializer(CustomModelSerializer):
longitude = serializers.DecimalField(source='region.longitude', max_digits=9, decimal_places=6, read_only=True)
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"dependencies": {
"@lukulent/svelte-umami": "^0.0.3",
"@mapbox/togeojson": "^0.16.2",
"dompurify": "^3.2.4",
"emoji-picker-element": "^1.26.0",
"gsap": "^3.12.7",
"marked": "^15.0.4",
Expand Down
16 changes: 16 additions & 0 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/src/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ declare global {
uuid: string;
public_profile: boolean;
has_password: boolean;
disable_password: boolean;
} | null;
locale: string;
}
Expand Down
53 changes: 52 additions & 1 deletion frontend/src/lib/components/AdventureModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,57 @@

let categories: Category[] = [];

const allowedFileTypes = [
'.pdf',
'.doc',
'.docx',
'.xls',
'.xlsx',
'.ppt',
'.pptx',
'.txt',
'.png',
'.jpg',
'.jpeg',
'.gif',
'.webp',
'.mp4',
'.mov',
'.avi',
'.mkv',
'.mp3',
'.wav',
'.flac',
'.ogg',
'.m4a',
'.wma',
'.aac',
'.opus',
'.zip',
'.rar',
'.7z',
'.tar',
'.gz',
'.bz2',
'.xz',
'.zst',
'.lz4',
'.lzma',
'.lzo',
'.z',
'.tar.gz',
'.tar.bz2',
'.tar.xz',
'.tar.zst',
'.tar.lz4',
'.tar.lzma',
'.tar.lzo',
'.tar.z',
'gpx',
'md',
'pdf'
];

export let initialLatLng: { lat: number; lng: number } | null = null; // Used to pass the location from the map selection to the modal

let fileInput: HTMLInputElement;
Expand Down Expand Up @@ -783,7 +834,7 @@
type="file"
id="fileInput"
class="file-input file-input-bordered w-full max-w-xs"
accept="image/*,video/*,audio/*,application/pdf,.gpx"
accept={allowedFileTypes.join(',')}
on:change={handleFileChange}
/>

Expand Down
6 changes: 5 additions & 1 deletion frontend/src/lib/components/CityCard.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { addToast } from '$lib/toasts';
import type { City } from '$lib/types';
import { createEventDispatcher } from 'svelte';
Expand Down Expand Up @@ -45,7 +46,10 @@
<div class="card-body">
<h2 class="card-title overflow-ellipsis">{city.name}</h2>
<div class="flex flex-wrap gap-2">
<div class="badge badge-neutral-300">{city.id}</div>
<div class="badge badge-primary">
{city.region_name}, {city.country_name}
</div>
<div class="badge badge-neutral-300">{city.region}</div>
</div>
<div class="card-actions justify-end">
{#if !visited}
Expand Down
Loading
Loading