feat(auth): added configuration to disable auth by email (#394)

This commit is contained in:
Corentin Thomasset
2025-07-02 13:36:19 +02:00
committed by GitHub
parent aad36f3252
commit f28d8245bf
16 changed files with 97 additions and 30 deletions

View File

@@ -0,0 +1,6 @@
---
"@papra/app-client": patch
"@papra/app-server": patch
---
Added the possibility to disable login via email, to support sso-only auth

View File

@@ -71,6 +71,9 @@ auth.legal-links.description: Indem Sie fortfahren, bestätigen Sie, dass Sie di
auth.legal-links.terms: Nutzungsbedingungen
auth.legal-links.privacy: Datenschutzrichtlinie
auth.no-auth-provider.title: Kein Authentifizierungsanbieter
auth.no-auth-provider.description: Es gibt keine Authentifizierungsanbieter auf dieser Papra-Instanz. Bitte kontaktieren Sie den Administrator dieser Instanz, um sie zu aktivieren.
# User settings
user.settings.title: Benutzereinstellungen

View File

@@ -71,6 +71,9 @@ auth.legal-links.description: By continuing, you acknowledge that you understand
auth.legal-links.terms: Terms of Service
auth.legal-links.privacy: Privacy Policy
auth.no-auth-provider.title: No authentication provider
auth.no-auth-provider.description: There are no authentication providers enabled on this instance of Papra. Please contact the administrator of this instance to enable them.
# User settings
user.settings.title: User settings

View File

@@ -71,6 +71,9 @@ auth.legal-links.description: En continuant, vous reconnaissez que vous comprene
auth.legal-links.terms: Conditions d'utilisation
auth.legal-links.privacy: Politique de confidentialité
auth.no-auth-provider.title: Aucun fournisseur d'authentification
auth.no-auth-provider.description: Il n'y a pas de fournisseurs d'authentification activés sur cette instance de Papra. Veuillez contacter l'administrateur de cette instance pour les activer.
# User settings
user.settings.title: Paramètres de l'utilisateur

View File

@@ -71,6 +71,9 @@ auth.legal-links.description: Ao continuar, você reconhece que leu e concorda c
auth.legal-links.terms: Termos de Serviço
auth.legal-links.privacy: Política de Privacidade
auth.no-auth-provider.title: Nenhum provedor de autenticação
auth.no-auth-provider.description: Não há provedores de autenticação habilitados nesta instância do Papra. Por favor, entre em contato com o administrador desta instância para habilitá-los.
# User settings
user.settings.title: Configurações do usuário

View File

@@ -0,0 +1,17 @@
import type { Component } from 'solid-js';
import { useI18n } from '@/modules/i18n/i18n.provider';
export const NoAuthProviderWarning: Component = () => {
const { t } = useI18n();
return (
<div class="flex items-center justify-center h-full p-6 sm:pb-32">
<div class="max-w-sm w-full">
<h1 class="text-lg font-bold">{t('auth.no-auth-provider.title')}</h1>
<p class="text-muted-foreground mt-1 mb-4">
{t('auth.no-auth-provider.description')}
</p>
</div>
</div>
);
};

View File

@@ -14,6 +14,7 @@ import { AuthLayout } from '../../ui/layouts/auth-layout.component';
import { getEnabledSsoProviderConfigs, isEmailVerificationRequiredError } from '../auth.models';
import { authWithProvider, signIn } from '../auth.services';
import { AuthLegalLinks } from '../components/legal-links.component';
import { NoAuthProviderWarning } from '../components/no-auth-provider';
import { SsoProviderButton } from '../components/sso-provider-button.component';
export const EmailLoginForm: Component = () => {
@@ -105,7 +106,7 @@ export const LoginPage: Component = () => {
const { config } = useConfig();
const { t } = useI18n();
const [getShowEmailLogin, setShowEmailLogin] = createSignal(false);
const [getShowEmailLoginForm, setShowEmailLoginForm] = createSignal(false);
const loginWithProvider = async (provider: SsoProviderConfig) => {
await authWithProvider({ provider, config });
@@ -113,6 +114,10 @@ export const LoginPage: Component = () => {
const getHasSsoProviders = () => getEnabledSsoProviderConfigs({ config }).length > 0;
if (!config.auth.providers.email.isEnabled && !getHasSsoProviders()) {
return <AuthLayout><NoAuthProviderWarning /></AuthLayout>;
}
return (
<AuthLayout>
<div class="flex items-center justify-center h-full p-6 sm:pb-32">
@@ -120,17 +125,22 @@ export const LoginPage: Component = () => {
<h1 class="text-xl font-bold">{t('auth.login.title')}</h1>
<p class="text-muted-foreground mt-1 mb-4">{t('auth.login.description')}</p>
{getShowEmailLogin() || !getHasSsoProviders()
? <EmailLoginForm />
: (
<Button onClick={() => setShowEmailLogin(true)} class="w-full">
<div class="i-tabler-mail mr-2 size-4.5" />
{t('auth.login.login-with-provider', { provider: 'Email' })}
</Button>
)}
<Show when={config.auth.providers.email.isEnabled}>
{getShowEmailLoginForm() || !getHasSsoProviders()
? <EmailLoginForm />
: (
<Button onClick={() => setShowEmailLoginForm(true)} class="w-full">
<div class="i-tabler-mail mr-2 size-4.5" />
{t('auth.login.login-with-provider', { provider: 'Email' })}
</Button>
)}
</Show>
<Show when={config.auth.providers.email.isEnabled && getHasSsoProviders()}>
<Separator class="my-4" />
</Show>
<Show when={getHasSsoProviders()}>
<Separator class="my-4" />
<div class="flex flex-col gap-2">
<For each={getEnabledSsoProviderConfigs({ config })}>

View File

@@ -13,6 +13,7 @@ import { AuthLayout } from '../../ui/layouts/auth-layout.component';
import { getEnabledSsoProviderConfigs } from '../auth.models';
import { authWithProvider, signUp } from '../auth.services';
import { AuthLegalLinks } from '../components/legal-links.component';
import { NoAuthProviderWarning } from '../components/no-auth-provider';
import { SsoProviderButton } from '../components/sso-provider-button.component';
export const EmailRegisterForm: Component = () => {
@@ -139,6 +140,10 @@ export const RegisterPage: Component = () => {
const getHasSsoProviders = () => getEnabledSsoProviderConfigs({ config }).length > 0;
if (!config.auth.providers.email.isEnabled && !getHasSsoProviders()) {
return <AuthLayout><NoAuthProviderWarning /></AuthLayout>;
}
return (
<AuthLayout>
<div class="flex items-center justify-center h-full p-6 sm:pb-32">
@@ -150,17 +155,22 @@ export const RegisterPage: Component = () => {
{t('auth.register.description')}
</p>
{getShowEmailRegister() || !getHasSsoProviders()
? <EmailRegisterForm />
: (
<Button onClick={() => setShowEmailRegister(true)} class="w-full">
<div class="i-tabler-mail mr-2 size-4.5" />
{t('auth.register.register-with-email')}
</Button>
)}
<Show when={config.auth.providers.email.isEnabled}>
{getShowEmailRegister() || !getHasSsoProviders()
? <EmailRegisterForm />
: (
<Button onClick={() => setShowEmailRegister(true)} class="w-full">
<div class="i-tabler-mail mr-2 size-4.5" />
{t('auth.register.register-with-email')}
</Button>
)}
</Show>
<Show when={config.auth.providers.email.isEnabled && getHasSsoProviders()}>
<Separator class="my-4" />
</Show>
<Show when={getHasSsoProviders()}>
<Separator class="my-4" />
<div class="flex flex-col gap-2">
<For each={getEnabledSsoProviderConfigs({ config })}>

View File

@@ -58,7 +58,7 @@ export const RequestPasswordResetPage: Component = () => {
const navigate = useNavigate();
onMount(() => {
if (!config.auth.isPasswordResetEnabled) {
if (!config.auth.isPasswordResetEnabled || !config.auth.providers.email.isEnabled) {
navigate('/login');
}
});

View File

@@ -62,7 +62,7 @@ export const ResetPasswordPage: Component = () => {
const navigate = useNavigate();
onMount(() => {
if (!config.auth.isPasswordResetEnabled) {
if (!config.auth.isPasswordResetEnabled || !config.auth.providers.email.isEnabled) {
navigate('/login');
}
});

View File

@@ -16,6 +16,7 @@ export const buildTimeConfig = {
isEmailVerificationRequired: asBoolean(import.meta.env.VITE_AUTH_IS_EMAIL_VERIFICATION_REQUIRED, true),
showLegalLinksOnAuthPage: asBoolean(import.meta.env.VITE_AUTH_SHOW_LEGAL_LINKS_ON_AUTH_PAGE, false),
providers: {
email: { isEnabled: asBoolean(import.meta.env.VITE_AUTH_PROVIDERS_EMAIL_IS_ENABLED, true) },
github: { isEnabled: asBoolean(import.meta.env.VITE_AUTH_PROVIDERS_GITHUB_IS_ENABLED, false) },
google: { isEnabled: asBoolean(import.meta.env.VITE_AUTH_PROVIDERS_GOOGLE_IS_ENABLED, false) },
customs: [] as {
@@ -35,7 +36,6 @@ export const buildTimeConfig = {
},
intakeEmails: {
isEnabled: asBoolean(import.meta.env.VITE_INTAKE_EMAILS_IS_ENABLED, false),
emailGenerationDomain: asString(import.meta.env.VITE_INTAKE_EMAILS_EMAIL_GENERATION_DOMAIN),
},
isSubscriptionsEnabled: asBoolean(import.meta.env.VITE_IS_SUBSCRIPTIONS_ENABLED, false),
} as const;

View File

@@ -68,6 +68,8 @@ export type LocaleKeys =
| 'auth.legal-links.description'
| 'auth.legal-links.terms'
| 'auth.legal-links.privacy'
| 'auth.no-auth-provider.title'
| 'auth.no-auth-provider.description'
| 'user.settings.title'
| 'user.settings.description'
| 'user.settings.email.title'

View File

@@ -56,6 +56,14 @@ export const authConfig = {
env: 'AUTH_SHOW_LEGAL_LINKS',
},
providers: {
email: {
isEnabled: {
doc: 'Whether email/password authentication is enabled',
schema: booleanishSchema,
default: true,
env: 'AUTH_PROVIDERS_EMAIL_IS_ENABLED',
},
},
github: {
isEnabled: {
doc: 'Whether Github OAuth is enabled',

View File

@@ -42,7 +42,7 @@ export function getAuth({
},
},
emailAndPassword: {
enabled: true,
enabled: config.auth.providers.email.isEnabled,
requireEmailVerification: config.auth.isEmailVerificationRequired,
sendResetPassword: config.auth.isPasswordResetEnabled
? authEmailsServices.sendPasswordResetEmail

View File

@@ -1,6 +1,8 @@
import type { DeepPartial } from '@corentinth/chisels';
import type { Config } from './config.types';
import { describe, expect, test } from 'vitest';
import { getPublicConfig } from './config.models';
import { overrideConfig } from './config.test-utils';
describe('config models', () => {
describe('getPublicConfig', () => {
@@ -12,10 +14,10 @@ describe('config models', () => {
- auth.providers.*.isEnabled Wether a oauth provider is enabled
- documents.deletedExpirationDelayInDays The delay in days before a deleted document is permanently deleted
- intakeEmails.isEnabled Whether intake emails are enabled
- intakeEmails.emailGenerationDomain The domain to use when generating email addresses for intake emails
- auth.providers.email.isEnabled Whether email/password authentication is enabled
Any other config should not be exposed.`, () => {
const config = {
const config = overrideConfig({
foo: 'bar',
auth: {
bar: 'baz',
@@ -38,9 +40,8 @@ describe('config models', () => {
},
intakeEmails: {
isEnabled: true,
emailGenerationDomain: 'papra.email',
},
} as unknown as Config;
} as DeepPartial<Config>);
expect(getPublicConfig({ config })).to.eql({
publicConfig: {
@@ -57,7 +58,9 @@ describe('config models', () => {
isEnabled: false,
},
customs: [],
email: {
isEnabled: true,
},
},
},
documents: {
@@ -65,7 +68,6 @@ describe('config models', () => {
},
intakeEmails: {
isEnabled: true,
emailGenerationDomain: 'papra.email',
},
},
});

View File

@@ -9,11 +9,11 @@ export function getPublicConfig({ config }: { config: Config }) {
'auth.isPasswordResetEnabled',
'auth.isRegistrationEnabled',
'auth.showLegalLinksOnAuthPage',
'auth.providers.email.isEnabled',
'auth.providers.github.isEnabled',
'auth.providers.google.isEnabled',
'documents.deletedDocumentsRetentionDays',
'intakeEmails.isEnabled',
'intakeEmails.emailGenerationDomain',
]),
{
auth: {