This commit is contained in:
Sean Morley
2025-12-14 16:51:19 -05:00
parent 1eff5fd82b
commit 2fbdc9ccea
8 changed files with 149 additions and 87 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -78,7 +78,7 @@ export const actions: Actions = {
});
if (res.status === 401) {
return redirect(302, '/login');
return redirect(302, '/');
} else {
return redirect(302, '/');
}

View File

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

View File

@@ -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 @@
<h2 class="text-4xl font-bold text-base-content mb-2">{$t('auth.login')}</h2>
</div>
<!-- Form -->
<!-- Form / Social Only -->
<div class="max-w-sm mx-auto w-full">
<form method="post" use:enhance={handleEnhanceSubmit} class="space-y-4">
<!-- Username -->
<div class="form-control">
<label class="label" for="username">
<span class="label-text font-medium">{$t('auth.username')}</span>
</label>
<input
name="username"
id="username"
type="text"
class="input input-bordered w-full focus:input-primary"
placeholder={$t('auth.enter_username')}
autocomplete="username"
/>
</div>
<!-- Password -->
<div class="form-control">
<label class="label" for="password">
<span class="label-text font-medium">{$t('auth.password')}</span>
</label>
<input
type="password"
name="password"
id="password"
class="input input-bordered w-full focus:input-primary"
placeholder={$t('auth.enter_password')}
autocomplete="current-password"
/>
</div>
<!-- TOTP -->
{#if $page.form?.mfa_required}
<div class="form-control">
<label class="label" for="totp">
<span class="label-text font-medium">{$t('auth.totp')}</span>
</label>
<input
type="text"
name="totp"
id="totp"
inputmode="numeric"
pattern="[0-9]*"
autocomplete="one-time-code"
class="input input-bordered w-full focus:input-primary"
placeholder="000000"
maxlength="6"
/>
</div>
{/if}
<!-- Submit Button -->
<div class="form-control mt-6">
<button type="submit" class="btn btn-primary w-full" disabled={isSubmitting}>
{#if isSubmitting}
<span class="loading loading-spinner"></span>
<span class="ml-2">{$t('auth.logging_in')}...</span>
{:else}
{$t('auth.login')}
{/if}
</button>
</div>
<!-- Error Message -->
{#if ($page.form?.message && $page.form?.message.length > 1) || $page.form?.type === 'error'}
<div class="alert alert-error mt-4">
<span>{$t($page.form.message) || $t('auth.login_error')}</span>
</div>
{/if}
<!-- Social Login -->
{#if socialOnly}
{#if socialProviders.length > 0}
<div class="divider text-sm">{$t('auth.or_3rd_party')}</div>
<div class="space-y-2">
<div class="divider text-sm">{$t('auth.or_3rd_party')}</div>
{#each socialProviders as provider}
<a
href={provider.url}
@@ -171,16 +102,113 @@
</div>
{/if}
<!-- Footer Links -->
<div class="flex justify-between text-sm mt-6 pt-4 border-t border-base-300">
<a href="/signup" class="link link-primary">
{$t('auth.signup')}
</a>
<a href="/user/reset-password" class="link link-primary">
{$t('auth.forgot_password')}
</a>
<a href="/signup" class="link link-primary">{$t('auth.signup')}</a>
<a href="/user/reset-password" class="link link-primary"
>{$t('auth.forgot_password')}</a
>
</div>
</form>
{:else}
<form method="post" use:enhance={handleEnhanceSubmit} class="space-y-4">
<!-- Username -->
<div class="form-control">
<label class="label" for="username">
<span class="label-text font-medium">{$t('auth.username')}</span>
</label>
<input
name="username"
id="username"
type="text"
class="input input-bordered w-full focus:input-primary"
placeholder={$t('auth.enter_username')}
autocomplete="username"
/>
</div>
<!-- Password -->
<div class="form-control">
<label class="label" for="password">
<span class="label-text font-medium">{$t('auth.password')}</span>
</label>
<input
type="password"
name="password"
id="password"
class="input input-bordered w-full focus:input-primary"
placeholder={$t('auth.enter_password')}
autocomplete="current-password"
/>
</div>
<!-- TOTP -->
{#if $page.form?.mfa_required}
<div class="form-control">
<label class="label" for="totp">
<span class="label-text font-medium">{$t('auth.totp')}</span>
</label>
<input
type="text"
name="totp"
id="totp"
inputmode="numeric"
pattern="[0-9]*"
autocomplete="one-time-code"
class="input input-bordered w-full focus:input-primary"
placeholder="000000"
maxlength="6"
/>
</div>
{/if}
<!-- Submit Button -->
<div class="form-control mt-6">
<button type="submit" class="btn btn-primary w-full" disabled={isSubmitting}>
{#if isSubmitting}
<span class="loading loading-spinner"></span>
<span class="ml-2">{$t('auth.logging_in')}...</span>
{:else}
{$t('auth.login')}
{/if}
</button>
</div>
<!-- Error Message -->
{#if ($page.form?.message && $page.form?.message.length > 1) || $page.form?.type === 'error'}
<div class="alert alert-error mt-4">
<span>{$t($page.form.message) || $t('auth.login_error')}</span>
</div>
{/if}
<!-- Social Login -->
{#if socialProviders.length > 0}
<div class="divider text-sm">{$t('auth.or_3rd_party')}</div>
<div class="space-y-2">
{#each socialProviders as provider}
<a
href={provider.url}
class="btn btn-outline w-full flex items-center gap-2"
>
{#if provider.provider === 'github'}
<GitHub class="w-4 h-4" />
{:else if provider.provider === 'openid_connect'}
<OpenIdConnect class="w-4 h-4" />
{/if}
Continue with {provider.name}
</a>
{/each}
</div>
{/if}
<!-- Footer Links -->
<div class="flex justify-between text-sm mt-6 pt-4 border-t border-base-300">
<a href="/signup" class="link link-primary">{$t('auth.signup')}</a>
<a href="/user/reset-password" class="link link-primary"
>{$t('auth.forgot_password')}</a
>
</div>
</form>
{/if}
</div>
</div>