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:
parent
4fdc16da58
commit
128c33d9a1
7 changed files with 83 additions and 12 deletions
|
@ -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",
|
||||||
|
|
|
@ -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')),
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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."
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue