Skip to content

Commit 6eb57d1

Browse files
authored
Add Lodging to Map, Sanitize MD, Optional Password Disable
2 parents d26d3a2 + 6f720a1 commit 6eb57d1

38 files changed

+719
-389
lines changed

backend/server/adventures/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class CustomUserAdmin(UserAdmin):
7171
readonly_fields = ('uuid',)
7272
search_fields = ('username',)
7373
fieldsets = UserAdmin.fieldsets + (
74-
(None, {'fields': ('profile_pic', 'uuid', 'public_profile')}),
74+
(None, {'fields': ('profile_pic', 'uuid', 'public_profile', 'disable_password')}),
7575
)
7676
def image_display(self, obj):
7777
if obj.profile_pic:
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 5.0.8 on 2025-03-17 01:15
2+
3+
import adventures.models
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('adventures', '0023_lodging_delete_hotel'),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name='attachment',
16+
name='file',
17+
field=models.FileField(upload_to=adventures.models.PathAndRename('attachments/'), validators=[adventures.models.validate_file_extension]),
18+
),
19+
]

backend/server/adventures/models.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from collections.abc import Collection
1+
from django.core.exceptions import ValidationError
22
import os
33
from typing import Iterable
44
import uuid
@@ -10,6 +10,13 @@
1010
from django.forms import ValidationError
1111
from django_resized import ResizedImageField
1212

13+
def validate_file_extension(value):
14+
import os
15+
from django.core.exceptions import ValidationError
16+
ext = os.path.splitext(value.name)[1] # [0] returns path+filename
17+
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']
18+
if not ext.lower() in valid_extensions:
19+
raise ValidationError('Unsupported file extension.')
1320

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

backend/server/main/settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,10 @@
227227
"socialaccount_login_error": f"{FRONTEND_URL}/account/provider/callback",
228228
}
229229

230+
AUTHENTICATION_BACKENDS = [
231+
'users.backends.NoPasswordAuthBackend',
232+
]
233+
230234
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
231235
SITE_ID = 1
232236
ACCOUNT_EMAIL_REQUIRED = True

backend/server/main/urls.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from django.urls import include, re_path, path
22
from django.contrib import admin
33
from django.views.generic import RedirectView, TemplateView
4-
from users.views import IsRegistrationDisabled, PublicUserListView, PublicUserDetailView, UserMetadataView, UpdateUserMetadataView, EnabledSocialProvidersView
4+
from users.views import IsRegistrationDisabled, PublicUserListView, PublicUserDetailView, UserMetadataView, UpdateUserMetadataView, EnabledSocialProvidersView, DisablePasswordAuthenticationView
55
from .views import get_csrf_token, get_public_url, serve_protected_media
66
from drf_yasg.views import get_schema_view
77
from drf_yasg import openapi
@@ -29,6 +29,8 @@
2929

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

32+
path('auth/disable-password/', DisablePasswordAuthenticationView.as_view(), name='disable-password-authentication'),
33+
3234
path('csrf/', get_csrf_token, name='get_csrf_token'),
3335
path('public-url/', get_public_url, name='get_public_url'),
3436

backend/server/users/backends.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from django.contrib.auth.backends import ModelBackend
2+
from allauth.socialaccount.models import SocialAccount
3+
4+
class NoPasswordAuthBackend(ModelBackend):
5+
def authenticate(self, request, username=None, password=None, **kwargs):
6+
print("NoPasswordAuthBackend")
7+
# First, attempt normal authentication
8+
user = super().authenticate(request, username=username, password=password, **kwargs)
9+
if user is None:
10+
return None
11+
12+
if SocialAccount.objects.filter(user=user).exists() and user.disable_password:
13+
# If yes, disable login via password
14+
return None
15+
16+
return user
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.0.8 on 2025-03-17 01:15
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('users', '0003_alter_customuser_email'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='customuser',
15+
name='disable_password',
16+
field=models.BooleanField(default=False),
17+
),
18+
]

backend/server/users/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class CustomUser(AbstractUser):
88
profile_pic = ResizedImageField(force_format="WEBP", quality=75, null=True, blank=True, upload_to='profile-pics/')
99
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
1010
public_profile = models.BooleanField(default=False)
11+
disable_password = models.BooleanField(default=False)
1112

1213
def __str__(self):
1314
return self.username

backend/server/users/serializers.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,11 @@ class Meta:
6464
extra_fields.append('date_joined')
6565
if hasattr(UserModel, 'is_staff'):
6666
extra_fields.append('is_staff')
67+
if hasattr(UserModel, 'disable_password'):
68+
extra_fields.append('disable_password')
6769

6870
fields = ['pk', *extra_fields]
69-
read_only_fields = ('email', 'date_joined', 'is_staff', 'is_superuser', 'is_active', 'pk')
71+
read_only_fields = ('email', 'date_joined', 'is_staff', 'is_superuser', 'is_active', 'pk', 'disable_password')
7072

7173
def handle_public_profile_change(self, instance, validated_data):
7274
"""
@@ -94,8 +96,8 @@ class CustomUserDetailsSerializer(UserDetailsSerializer):
9496

9597
class Meta(UserDetailsSerializer.Meta):
9698
model = CustomUser
97-
fields = UserDetailsSerializer.Meta.fields + ['profile_pic', 'uuid', 'public_profile', 'has_password']
98-
read_only_fields = UserDetailsSerializer.Meta.read_only_fields + ('uuid', 'has_password')
99+
fields = UserDetailsSerializer.Meta.fields + ['profile_pic', 'uuid', 'public_profile', 'has_password', 'disable_password']
100+
read_only_fields = UserDetailsSerializer.Meta.read_only_fields + ('uuid', 'has_password', 'disable_password')
99101

100102
@staticmethod
101103
def get_has_password(instance):
@@ -120,5 +122,5 @@ def to_representation(self, instance):
120122
representation.pop('pk', None)
121123
# Remove the email field
122124
representation.pop('email', None)
123-
125+
124126
return representation

backend/server/users/views.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from allauth.socialaccount.models import SocialApp
1414
from adventures.serializers import AdventureSerializer, CollectionSerializer
1515
from adventures.models import Adventure, Collection
16+
from allauth.socialaccount.models import SocialAccount
1617

1718
User = get_user_model()
1819

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

7678
class PublicUserDetailView(APIView):
@@ -171,4 +173,35 @@ def get(self, request):
171173
'url': f"{getenv('PUBLIC_URL')}/accounts/{new_provider}/login/",
172174
'name': provider.name
173175
})
174-
return Response(providers, status=status.HTTP_200_OK)
176+
return Response(providers, status=status.HTTP_200_OK)
177+
178+
179+
class DisablePasswordAuthenticationView(APIView):
180+
"""
181+
Disable password authentication for a user. This is used when a user signs up with a social provider.
182+
"""
183+
184+
# Allows the user to set the disable_password field to True if they have a social account linked
185+
permission_classes = [IsAuthenticated]
186+
187+
@swagger_auto_schema(
188+
responses={
189+
200: openapi.Response('Password authentication disabled'),
190+
400: 'Bad Request'
191+
},
192+
operation_description="Disable password authentication."
193+
)
194+
def post(self, request):
195+
user = request.user
196+
if SocialAccount.objects.filter(user=user).exists():
197+
user.disable_password = True
198+
user.save()
199+
return Response({"detail": "Password authentication disabled."}, status=status.HTTP_200_OK)
200+
return Response({"detail": "No social account linked."}, status=status.HTTP_400_BAD_REQUEST)
201+
202+
def delete(self, request):
203+
user = request.user
204+
user.disable_password = False
205+
user.save()
206+
return Response({"detail": "Password authentication enabled."}, status=status.HTTP_200_OK)
207+

0 commit comments

Comments
 (0)