1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-19 21:09:37 +02:00

feat: add social authentication support with enabled providers endpoint and UI integration

This commit is contained in:
Sean Morley 2025-01-06 14:25:57 -05:00
parent 4fdc16da58
commit 128c33d9a1
7 changed files with 83 additions and 12 deletions

View file

@ -42,6 +42,7 @@ INSTALLED_APPS = (
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.sites', 'django.contrib.sites',
"allauth_ui",
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
'allauth', 'allauth',
@ -49,8 +50,8 @@ INSTALLED_APPS = (
'allauth.mfa', 'allauth.mfa',
'allauth.headless', 'allauth.headless',
'allauth.socialaccount', 'allauth.socialaccount',
# "widget_tweaks", 'allauth.socialaccount.providers.github',
# "slippers", 'allauth.socialaccount.providers.openid_connect',
'drf_yasg', 'drf_yasg',
'corsheaders', 'corsheaders',
'adventures', 'adventures',
@ -58,6 +59,9 @@ INSTALLED_APPS = (
'users', 'users',
'integrations', 'integrations',
'django.contrib.gis', 'django.contrib.gis',
'widget_tweaks',
'slippers',
) )
MIDDLEWARE = ( MIDDLEWARE = (
@ -75,6 +79,8 @@ MIDDLEWARE = (
# disable verifications for new users # disable verifications for new users
ACCOUNT_EMAIL_VERIFICATION = 'none' ACCOUNT_EMAIL_VERIFICATION = 'none'
ALLAUTH_UI_THEME = "night"
CACHES = { CACHES = {
'default': { 'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
@ -120,7 +126,7 @@ USE_L10N = True
USE_TZ = True USE_TZ = True
SESSION_COOKIE_SAMESITE = None
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/ # https://docs.djangoproject.com/en/1.7/howto/static-files/
@ -143,6 +149,7 @@ STORAGES = {
} }
} }
SILENCED_SYSTEM_CHECKS = ["slippers.E001"]
TEMPLATES = [ TEMPLATES = [
{ {
@ -175,6 +182,9 @@ SESSION_SAVE_EVERY_REQUEST = True
FRONTEND_URL = getenv('FRONTEND_URL', 'http://localhost:3000') FRONTEND_URL = getenv('FRONTEND_URL', 'http://localhost:3000')
# Set login redirect URL to the frontend
LOGIN_REDIRECT_URL = FRONTEND_URL
HEADLESS_FRONTEND_URLS = { HEADLESS_FRONTEND_URLS = {
"account_confirm_email": f"{FRONTEND_URL}/user/verify-email/{{key}}", "account_confirm_email": f"{FRONTEND_URL}/user/verify-email/{{key}}",
"account_reset_password": f"{FRONTEND_URL}/user/reset-password", "account_reset_password": f"{FRONTEND_URL}/user/reset-password",

View file

@ -3,7 +3,7 @@ from django.contrib import admin
from django.views.generic import RedirectView, TemplateView from django.views.generic import RedirectView, TemplateView
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static 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 .views import get_csrf_token
from drf_yasg.views import get_schema_view 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/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('csrf/', get_csrf_token, name='get_csrf_token'),
path('', TemplateView.as_view(template_name='home.html')), path('', TemplateView.as_view(template_name='home.html')),

View file

@ -13,8 +13,8 @@ django-geojson
setuptools setuptools
gunicorn==23.0.0 gunicorn==23.0.0
qrcode==8.0 qrcode==8.0
# slippers==0.6.2 slippers==0.6.2
# django-allauth-ui==1.5.1 django-allauth-ui==1.5.1
# django-widget-tweaks==1.5.0 django-widget-tweaks==1.5.0
django-ical==1.9.2 django-ical==1.9.2
icalendar==6.1.0 icalendar==6.1.0

View file

@ -1,3 +1,4 @@
from os import getenv
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
@ -9,6 +10,7 @@ from django.conf import settings
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from .serializers import CustomUserDetailsSerializer as PublicUserSerializer from .serializers import CustomUserDetailsSerializer as PublicUserSerializer
from allauth.socialaccount.models import SocialApp
User = get_user_model() User = get_user_model()
@ -120,4 +122,31 @@ class UpdateUserMetadataView(APIView):
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 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)

View file

@ -295,7 +295,8 @@
"email_required": "Email is required", "email_required": "Email is required",
"new_password": "New Password (6+ characters)", "new_password": "New Password (6+ characters)",
"both_passwords_required": "Both passwords are required", "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": { "users": {
"no_users_found": "No users found with public profiles." "no_users_found": "No users found with public profiles."

View file

@ -4,6 +4,7 @@ import type { Actions, PageServerLoad, RouteParams } from './$types';
import { getRandomBackground, getRandomQuote } from '$lib'; import { getRandomBackground, getRandomQuote } from '$lib';
import { fetchCSRFToken } from '$lib/index.server'; import { fetchCSRFToken } from '$lib/index.server';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
if (event.locals.user) { if (event.locals.user) {
@ -12,10 +13,17 @@ export const load: PageServerLoad = async (event) => {
const quote = getRandomQuote(); const quote = getRandomQuote();
const background = getRandomBackground(); 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 { return {
props: { props: {
quote, quote,
background background,
socialProviders
} }
}; };
} }

View file

@ -9,14 +9,19 @@
let isImageInfoModalOpen: boolean = false; 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 { page } from '$app/stores';
import ImageInfoModal from '$lib/components/ImageInfoModal.svelte'; import ImageInfoModal from '$lib/components/ImageInfoModal.svelte';
import type { Background } from '$lib/types.js'; 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: '' };
</script> </script>
{#if isImageInfoModalOpen} {#if isImageInfoModalOpen}
@ -62,6 +67,22 @@
{/if} {/if}
<button class="py-2 px-4 btn btn-primary mr-2">{$t('auth.login')}</button> <button class="py-2 px-4 btn btn-primary mr-2">{$t('auth.login')}</button>
{#if socialProviders.length > 0}
<div class="divider text-center text-sm my-4">{$t('auth.or_3rd_party')}</div>
<div class="flex justify-center">
{#each socialProviders as provider}
<a href={provider.url} class="btn btn-primary mr-2 flex items-center">
{#if provider.provider === 'github'}
<GitHub class="w-4 h-4 mr-2" />
{:else if provider.provider === 'openid_connect'}
<OpenIdConnect class="w-4 h-4 mr-2" />
{/if}
{provider.name}
</a>
{/each}
</div>
{/if}
<div class="flex justify-between mt-4"> <div class="flex justify-between mt-4">
<p><a href="/signup" class="underline">{$t('auth.signup')}</a></p> <p><a href="/signup" class="underline">{$t('auth.signup')}</a></p>
<p> <p>