1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-08-05 05:05:17 +02:00

feat: Implement disable password authentication for users with social accounts

This commit is contained in:
Sean Morley 2025-03-16 21:49:00 -04:00
parent 189cd0ee69
commit a38828eb45
14 changed files with 184 additions and 17 deletions

View file

@ -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:

View file

@ -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]),
),
]

View file

@ -227,6 +227,10 @@ HEADLESS_FRONTEND_URLS = {
"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

View file

@ -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
@ -29,6 +29,8 @@ urlpatterns = [
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'),

View file

@ -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

View file

@ -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),
),
]

View file

@ -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

View file

@ -64,9 +64,11 @@ class UserDetailsSerializer(serializers.ModelSerializer):
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):
"""
@ -95,7 +97,7 @@ 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')
read_only_fields = UserDetailsSerializer.Meta.read_only_fields + ('uuid', 'has_password', 'disable_password')
@staticmethod
def get_has_password(instance):

View file

@ -13,6 +13,7 @@ from .serializers import CustomUserDetailsSerializer as PublicUserSerializer
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()
@ -171,4 +172,35 @@ class EnabledSocialProvidersView(APIView):
'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)