From 2fbdc9ccea90879f6a4d5df66f09fde42d5d97e1 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sun, 14 Dec 2025 16:51:19 -0500 Subject: [PATCH] Fixes #511 --- backend/server/.env.example | 2 + backend/server/main/settings.py | 2 + backend/server/users/backends.py | 5 + backend/server/users/views.py | 3 +- .../configuration/advanced_configuration.md | 7 +- frontend/src/routes/+page.server.ts | 2 +- frontend/src/routes/login/+page.server.ts | 23 +++ frontend/src/routes/login/+page.svelte | 192 ++++++++++-------- 8 files changed, 149 insertions(+), 87 deletions(-) diff --git a/backend/server/.env.example b/backend/server/.env.example index a2792ef6..1d8fcff7 100644 --- a/backend/server/.env.example +++ b/backend/server/.env.example @@ -29,6 +29,8 @@ EMAIL_BACKEND='console' # ACCOUNT_EMAIL_VERIFICATION='none' # 'none', 'optional', 'mandatory' # You can change this as needed for your environment +# FORCE_SOCIALACCOUNT_LOGIN=False # When true, only social login is allowed (no password login) and the login page will show only social providers or redirect directly to the first provider if only one is configured. + # ------------------- # # For Developers to start a Demo Database diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index 22d81006..935fc822 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -259,6 +259,8 @@ SOCIALACCOUNT_EMAIL_AUTHENTICATION = True SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = True # Auto-link by email SOCIALACCOUNT_AUTO_SIGNUP = True # Allow auto-signup post adapter checks +FORCE_SOCIALACCOUNT_LOGIN = getenv('FORCE_SOCIALACCOUNT_LOGIN', 'false').lower() == 'true' # When true, only social login is allowed (no password login) and the login page will show only social providers or redirect directly to the first provider if only one is configured. + if getenv('EMAIL_BACKEND', 'console') == 'console': EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' else: diff --git a/backend/server/users/backends.py b/backend/server/users/backends.py index 8c985ce7..fbcce68b 100644 --- a/backend/server/users/backends.py +++ b/backend/server/users/backends.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.contrib.auth.backends import ModelBackend from allauth.socialaccount.models import SocialAccount from allauth.account.auth_backends import AuthenticationBackend as AllauthBackend @@ -7,6 +8,10 @@ User = get_user_model() class NoPasswordAuthBackend(ModelBackend): def authenticate(self, request, username=None, password=None, **kwargs): + # Block all password-based logins when social-only mode is enforced + if getattr(settings, "FORCE_SOCIALACCOUNT_LOGIN", False) and password: + return None + # Handle allauth-specific authentication (like email login) allauth_backend = AllauthBackend() allauth_user = allauth_backend.authenticate(request, username=username, password=password, **kwargs) diff --git a/backend/server/users/views.py b/backend/server/users/views.py index 5fb7d2a2..2b5548f6 100644 --- a/backend/server/users/views.py +++ b/backend/server/users/views.py @@ -171,7 +171,8 @@ class EnabledSocialProvidersView(APIView): providers.append({ 'provider': provider.provider, 'url': f"{getenv('PUBLIC_URL')}/accounts/{new_provider}/login/", - 'name': provider.name + 'name': provider.name, + 'usage_required': settings.FORCE_SOCIALACCOUNT_LOGIN }) return Response(providers, status=status.HTTP_200_OK) diff --git a/documentation/docs/configuration/advanced_configuration.md b/documentation/docs/configuration/advanced_configuration.md index c4d8bd64..903d5296 100644 --- a/documentation/docs/configuration/advanced_configuration.md +++ b/documentation/docs/configuration/advanced_configuration.md @@ -2,6 +2,7 @@ In addition to the primary configuration variables listed above, there are several optional environment variables that can be set to further customize your AdventureLog instance. These variables are not required for a basic setup but can enhance functionality and security. -| Name | Required | Description | Default Value | -| ---------------------------- | -------- | ------------------------------------------------------------------------------------------ | ------------- | -| `ACCOUNT_EMAIL_VERIFICATION` | No | Enable email verification for new accounts. Options are `none`, `optional`, or `mandatory` | `none` | +| Name | Required | Description | Default Value | +| ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | +| `ACCOUNT_EMAIL_VERIFICATION` | No | Enable email verification for new accounts. Options are `none`, `optional`, or `mandatory` | `none` | +| `FORCE_SOCIALACCOUNT_LOGIN` | No | When set to `True`, only social login is allowed (no password login). The login page will show only social providers or redirect directly to the first provider if only one is configured. | `False` | diff --git a/frontend/src/routes/+page.server.ts b/frontend/src/routes/+page.server.ts index 6d5b21d7..6182a1a0 100644 --- a/frontend/src/routes/+page.server.ts +++ b/frontend/src/routes/+page.server.ts @@ -78,7 +78,7 @@ export const actions: Actions = { }); if (res.status === 401) { - return redirect(302, '/login'); + return redirect(302, '/'); } else { return redirect(302, '/'); } diff --git a/frontend/src/routes/login/+page.server.ts b/frontend/src/routes/login/+page.server.ts index ba58204e..69105306 100644 --- a/frontend/src/routes/login/+page.server.ts +++ b/frontend/src/routes/login/+page.server.ts @@ -20,6 +20,29 @@ export const load: PageServerLoad = async (event) => { } let socialProviders = await socialProviderFetch.json(); + // Determine if social auth usage is required. The API returns `usage_required` + // either for all providers or none — so check the first provider if present. + const usageRequired = + socialProviders && socialProviders.length > 0 ? !!socialProviders[0].usage_required : false; + + // If usage is required and there's exactly one social provider, redirect straight + // to that provider's URL. If multiple providers and usage is required, instruct + // the client to render social-only UI. + if (usageRequired) { + if (socialProviders.length === 1) { + return redirect(302, socialProviders[0].url); + } else if (socialProviders.length > 1) { + return { + props: { + quote, + background, + socialProviders, + socialOnly: true + } + }; + } + } + return { props: { quote, diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte index 11f79977..0dad5df4 100644 --- a/frontend/src/routes/login/+page.svelte +++ b/frontend/src/routes/login/+page.svelte @@ -12,6 +12,8 @@ let socialProviders = data.props?.socialProviders ?? []; + let socialOnly: boolean = data.props?.socialOnly ?? false; + import GitHub from '~icons/mdi/github'; import OpenIdConnect from '~icons/mdi/openid'; @@ -78,83 +80,12 @@

{$t('auth.login')}

- +
-
- -
- - -
- - -
- - -
- - - {#if $page.form?.mfa_required} -
- - -
- {/if} - - -
- -
- - - {#if ($page.form?.message && $page.form?.message.length > 1) || $page.form?.type === 'error'} -
- {$t($page.form.message) || $t('auth.login_error')} -
- {/if} - - + {#if socialOnly} {#if socialProviders.length > 0} -
{$t('auth.or_3rd_party')}
-
+
{$t('auth.or_3rd_party')}
{#each socialProviders as provider} {/if} - - + {:else} +
+ +
+ + +
+ + +
+ + +
+ + + {#if $page.form?.mfa_required} +
+ + +
+ {/if} + + +
+ +
+ + + {#if ($page.form?.message && $page.form?.message.length > 1) || $page.form?.type === 'error'} +
+ {$t($page.form.message) || $t('auth.login_error')} +
+ {/if} + + + {#if socialProviders.length > 0} +
{$t('auth.or_3rd_party')}
+ + + {/if} + + + +
+ {/if}