From 128c33d9a18f19f2db65a3f96170f583ab97c74c Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Mon, 6 Jan 2025 14:25:57 -0500 Subject: [PATCH] feat: add social authentication support with enabled providers endpoint and UI integration --- backend/server/main/settings.py | 16 +++++++++--- backend/server/main/urls.py | 4 ++- backend/server/requirements.txt | 6 ++--- backend/server/users/views.py | 31 ++++++++++++++++++++++- frontend/src/locales/en.json | 3 ++- frontend/src/routes/login/+page.server.ts | 10 +++++++- frontend/src/routes/login/+page.svelte | 25 ++++++++++++++++-- 7 files changed, 83 insertions(+), 12 deletions(-) diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index 5acecb7..c135d79 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -42,6 +42,7 @@ INSTALLED_APPS = ( 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.sites', + "allauth_ui", 'rest_framework', 'rest_framework.authtoken', 'allauth', @@ -49,8 +50,8 @@ INSTALLED_APPS = ( 'allauth.mfa', 'allauth.headless', 'allauth.socialaccount', - # "widget_tweaks", - # "slippers", + 'allauth.socialaccount.providers.github', + 'allauth.socialaccount.providers.openid_connect', 'drf_yasg', 'corsheaders', 'adventures', @@ -58,6 +59,9 @@ INSTALLED_APPS = ( 'users', 'integrations', 'django.contrib.gis', + 'widget_tweaks', + 'slippers', + ) MIDDLEWARE = ( @@ -75,6 +79,8 @@ MIDDLEWARE = ( # disable verifications for new users ACCOUNT_EMAIL_VERIFICATION = 'none' +ALLAUTH_UI_THEME = "night" + CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', @@ -120,7 +126,7 @@ USE_L10N = True USE_TZ = True - +SESSION_COOKIE_SAMESITE = None # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.7/howto/static-files/ @@ -143,6 +149,7 @@ STORAGES = { } } +SILENCED_SYSTEM_CHECKS = ["slippers.E001"] TEMPLATES = [ { @@ -175,6 +182,9 @@ SESSION_SAVE_EVERY_REQUEST = True FRONTEND_URL = getenv('FRONTEND_URL', 'http://localhost:3000') +# Set login redirect URL to the frontend +LOGIN_REDIRECT_URL = FRONTEND_URL + HEADLESS_FRONTEND_URLS = { "account_confirm_email": f"{FRONTEND_URL}/user/verify-email/{{key}}", "account_reset_password": f"{FRONTEND_URL}/user/reset-password", diff --git a/backend/server/main/urls.py b/backend/server/main/urls.py index ab1e084..9cf7532 100644 --- a/backend/server/main/urls.py +++ b/backend/server/main/urls.py @@ -3,7 +3,7 @@ from django.contrib import admin from django.views.generic import RedirectView, TemplateView from django.conf import settings from django.conf.urls.static import static -from users.views import IsRegistrationDisabled, PublicUserListView, PublicUserDetailView, UserMetadataView, UpdateUserMetadataView +from users.views import IsRegistrationDisabled, PublicUserListView, PublicUserDetailView, UserMetadataView, UpdateUserMetadataView, EnabledSocialProvidersView from .views import get_csrf_token from drf_yasg.views import get_schema_view @@ -27,6 +27,8 @@ urlpatterns = [ path('auth/user-metadata/', UserMetadataView.as_view(), name='user-metadata'), + path('auth/social-providers/', EnabledSocialProvidersView.as_view(), name='enabled-social-providers'), + path('csrf/', get_csrf_token, name='get_csrf_token'), path('', TemplateView.as_view(template_name='home.html')), diff --git a/backend/server/requirements.txt b/backend/server/requirements.txt index bae189f..228249f 100644 --- a/backend/server/requirements.txt +++ b/backend/server/requirements.txt @@ -13,8 +13,8 @@ django-geojson setuptools gunicorn==23.0.0 qrcode==8.0 -# slippers==0.6.2 -# django-allauth-ui==1.5.1 -# django-widget-tweaks==1.5.0 +slippers==0.6.2 +django-allauth-ui==1.5.1 +django-widget-tweaks==1.5.0 django-ical==1.9.2 icalendar==6.1.0 \ No newline at end of file diff --git a/backend/server/users/views.py b/backend/server/users/views.py index 109d04b..b03760e 100644 --- a/backend/server/users/views.py +++ b/backend/server/users/views.py @@ -1,3 +1,4 @@ +from os import getenv from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status @@ -9,6 +10,7 @@ from django.conf import settings from django.shortcuts import get_object_or_404 from django.contrib.auth import get_user_model from .serializers import CustomUserDetailsSerializer as PublicUserSerializer +from allauth.socialaccount.models import SocialApp User = get_user_model() @@ -120,4 +122,31 @@ class UpdateUserMetadataView(APIView): if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class EnabledSocialProvidersView(APIView): + """ + Get enabled social providers for social authentication. This is used to determine which buttons to show on the frontend. Also returns a URL for each to start the authentication flow. + """ + + @swagger_auto_schema( + responses={ + 200: openapi.Response('Enabled social providers'), + 400: 'Bad Request' + }, + operation_description="Get enabled social providers." + ) + def get(self, request): + social_providers = SocialApp.objects.filter(sites=settings.SITE_ID) + providers = [] + for provider in social_providers: + if provider.provider == 'openid_connect': + new_provider = f'oidc/{provider.client_id}' + else: + new_provider = provider.provider + providers.append({ + 'provider': provider.provider, + 'url': f"{getenv('PUBLIC_URL')}/accounts/{new_provider}/login/", + 'name': provider.name + }) + return Response(providers, status=status.HTTP_200_OK) \ No newline at end of file diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index a7e668e..050ae37 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -295,7 +295,8 @@ "email_required": "Email is required", "new_password": "New Password (6+ characters)", "both_passwords_required": "Both passwords are required", - "reset_failed": "Failed to reset password" + "reset_failed": "Failed to reset password", + "or_3rd_party": "Or login with a third-party service" }, "users": { "no_users_found": "No users found with public profiles." diff --git a/frontend/src/routes/login/+page.server.ts b/frontend/src/routes/login/+page.server.ts index adb0905..a57865c 100644 --- a/frontend/src/routes/login/+page.server.ts +++ b/frontend/src/routes/login/+page.server.ts @@ -4,6 +4,7 @@ import type { Actions, PageServerLoad, RouteParams } from './$types'; import { getRandomBackground, getRandomQuote } from '$lib'; import { fetchCSRFToken } from '$lib/index.server'; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; +const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; export const load: PageServerLoad = async (event) => { if (event.locals.user) { @@ -12,10 +13,17 @@ export const load: PageServerLoad = async (event) => { const quote = getRandomQuote(); const background = getRandomBackground(); + let socialProviderFetch = await event.fetch(`${serverEndpoint}/auth/social-providers/`); + if (!socialProviderFetch.ok) { + return fail(500, { message: 'settings.social_providers_error' }); + } + let socialProviders = await socialProviderFetch.json(); + return { props: { quote, - background + background, + socialProviders } }; } diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte index 3bf6c98..d6ba6b7 100644 --- a/frontend/src/routes/login/+page.svelte +++ b/frontend/src/routes/login/+page.svelte @@ -9,14 +9,19 @@ let isImageInfoModalOpen: boolean = false; + let socialProviders = data.props?.socialProviders ?? []; + + import GitHub from '~icons/mdi/github'; + import OpenIdConnect from '~icons/mdi/openid'; + import { page } from '$app/stores'; import ImageInfoModal from '$lib/components/ImageInfoModal.svelte'; import type { Background } from '$lib/types.js'; - let quote: { quote: string; author: string } = data.props.quote; + let quote: { quote: string; author: string } = data.props?.quote ?? { quote: '', author: '' }; - let background: Background = data.props.background; + let background: Background = data.props?.background ?? { url: '' }; {#if isImageInfoModalOpen} @@ -62,6 +67,22 @@ {/if} + {#if socialProviders.length > 0} +
{$t('auth.or_3rd_party')}
+
+ {#each socialProviders as provider} + + {#if provider.provider === 'github'} + + {:else if provider.provider === 'openid_connect'} + + {/if} + {provider.name} + + {/each} +
+ {/if} +

{$t('auth.signup')}