feat: add local auth settings
Some checks failed
Node.js CI / Test (push) Has been cancelled

This commit is contained in:
thibaud-leclere 2026-05-06 08:52:10 +02:00
parent d4bbde7deb
commit b1e4b648ee
5 changed files with 124 additions and 53 deletions

View file

@ -24,6 +24,9 @@
"email_auth_enabled": "Email authentication is enabled and ready to use.", "email_auth_enabled": "Email authentication is enabled and ready to use.",
"email_auth_smtp_note": "To use email-based authentication, you need to enable and configure SMTP in the SMTP tab first.", "email_auth_smtp_note": "To use email-based authentication, you need to enable and configure SMTP in the SMTP tab first.",
"enable_email_auth": "Enable Email Authentication", "enable_email_auth": "Enable Email Authentication",
"enable_local_auth": "Enable Username and Password",
"local_auth": "Username and Password",
"local_auth_description": "Enable or disable local username and password authentication for your users.",
"smtp_required": "SMTP must be enabled and configured to use email based authentication." "smtp_required": "SMTP must be enabled and configured to use email based authentication."
}, },
"configs": { "configs": {
@ -36,6 +39,7 @@
"description": "Configure authentication providers for your server", "description": "Configure authentication providers for your server",
"email": "Email", "email": "Email",
"github_enterprise": "Enable Github Enterprise", "github_enterprise": "Enable Github Enterprise",
"local": "Username and Password",
"oauth": "OAuth", "oauth": "OAuth",
"oauth_providers": "OAuth Providers", "oauth_providers": "OAuth Providers",
"provider_not_specified": "Please enable at least one authentication provider", "provider_not_specified": "Please enable at least one authentication provider",

View file

@ -1,4 +1,14 @@
{ {
"auth": {
"enable_local_auth": "Activer identifiant et mot de passe",
"local_auth": "Identifiant et mot de passe",
"local_auth_description": "Activer ou désactiver lauthentification locale par identifiant et mot de passe pour les utilisateurs."
},
"configs": {
"auth_providers": {
"local": "Identifiant et mot de passe"
}
},
"onboarding": { "onboarding": {
"local": { "local": {
"confirm_password": "Confirmer le mot de passe", "confirm_password": "Confirmer le mot de passe",

View file

@ -62,6 +62,36 @@
</div> </div>
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab id="local-auth" :label="t('configs.auth_providers.local')">
<div class="pb-8 px-4 grid md:grid-cols-3 gap-4 md:gap-4 pt-8">
<div class="md:col-span-1">
<h3 class="heading">{{ t('auth.local_auth') }}</h3>
<p class="my-1 text-secondaryLight">
{{ t('auth.local_auth_description') }}
</p>
</div>
<div class="sm:px-8 md:col-span-2">
<section>
<h4 class="font-semibold text-secondaryDark">
{{ t('auth.local_auth') }}
</h4>
<div class="space-y-4 py-4">
<div class="flex justify-between">
<HoppSmartToggle
:on="isLocalAuthEnabled"
@change="toggleLocalAuth"
>
{{ t('auth.enable_local_auth') }}
</HoppSmartToggle>
</div>
</div>
</section>
</div>
</div>
</HoppSmartTab>
<HoppSmartTab id="token" :label="t('configs.auth_providers.token.title')"> <HoppSmartTab id="token" :label="t('configs.auth_providers.token.title')">
<div class="pb-8 px-4"> <div class="pb-8 px-4">
<SettingsAuthToken v-model:config="workingConfigs" /> <SettingsAuthToken v-model:config="workingConfigs" />
@ -88,14 +118,14 @@ const emit = defineEmits<{
}>(); }>();
// Auth Sub Tabs // Auth Sub Tabs
type AuthSubTabs = 'auth-providers' | 'email-auth' | 'token'; type AuthSubTabs = 'auth-providers' | 'email-auth' | 'local-auth' | 'token';
const selectedAuthSubTab = ref<AuthSubTabs>('auth-providers'); const selectedAuthSubTab = ref<AuthSubTabs>('auth-providers');
const workingConfigs = useVModel(props, 'config', emit); const workingConfigs = useVModel(props, 'config', emit);
// Check if SMTP is activated but not saved yet. Used to track if SMTP was enabled after the last save. // Check if SMTP is activated but not saved yet. Used to track if SMTP was enabled after the last save.
const isSMTPActivated = computed( const isSMTPActivated = computed(
() => workingConfigs.value?.mailConfigs.enabled ?? false () => workingConfigs.value?.mailConfigs.enabled ?? false,
); );
// Check if Email authentication is enabled // Check if Email authentication is enabled
@ -111,6 +141,15 @@ const toggleEmailAuth = () => {
} }
}; };
const isLocalAuthEnabled = computed(() => {
return workingConfigs.value?.localAuth.enabled ?? false;
});
const toggleLocalAuth = () => {
workingConfigs.value.localAuth.enabled =
!workingConfigs.value.localAuth.enabled;
};
// Check if SMTP is enabled on mount // Check if SMTP is enabled on mount
const isSMTPEnabled = ref(false); const isSMTPEnabled = ref(false);

View file

@ -38,7 +38,7 @@ import { useClientHandler } from './useClientHandler';
const COOKIE_NAME_REGEX = /^[A-Za-z0-9_-]+$/; const COOKIE_NAME_REGEX = /^[A-Za-z0-9_-]+$/;
const OPTIONAL_TOKEN_FIELD_KEYS = new Set( const OPTIONAL_TOKEN_FIELD_KEYS = new Set(
TOKEN_VALIDATION_CONFIGS.filter((cfg) => cfg.optional).map((cfg) => cfg.key) TOKEN_VALIDATION_CONFIGS.filter((cfg) => cfg.optional).map((cfg) => cfg.key),
); );
/** Composable that handles all operations related to server configurations /** Composable that handles all operations related to server configurations
@ -58,10 +58,10 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
InfraConfigsDocument, InfraConfigsDocument,
{ {
configNames: ALL_CONFIGS.flat().map( configNames: ALL_CONFIGS.flat().map(
({ name }) => name ({ name }) => name,
) as InfraConfigEnum[], ) as InfraConfigEnum[],
}, },
(x) => x.infraConfigs (x) => x.infraConfigs,
); );
// Fetching allowed auth providers // Fetching allowed auth providers
@ -73,7 +73,7 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
} = useClientHandler( } = useClientHandler(
AllowedAuthProvidersDocument, AllowedAuthProvidersDocument,
{}, {},
(x) => x.allowedAuthProviders (x) => x.allowedAuthProviders,
); );
// Current and working configs // Current and working configs
@ -133,7 +133,7 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
mailer_smtp_port: getFieldValue(InfraConfigEnum.MailerSmtpPort), mailer_smtp_port: getFieldValue(InfraConfigEnum.MailerSmtpPort),
mailer_smtp_user: getFieldValue(InfraConfigEnum.MailerSmtpUser), mailer_smtp_user: getFieldValue(InfraConfigEnum.MailerSmtpUser),
mailer_smtp_password: getFieldValue( mailer_smtp_password: getFieldValue(
InfraConfigEnum.MailerSmtpPassword InfraConfigEnum.MailerSmtpPassword,
), ),
mailer_smtp_secure: mailer_smtp_secure:
getFieldValue(InfraConfigEnum.MailerSmtpSecure) === 'true', getFieldValue(InfraConfigEnum.MailerSmtpSecure) === 'true',
@ -147,37 +147,42 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
mailer_smtp_auth_type: mailer_smtp_auth_type:
getFieldValue(InfraConfigEnum.MailerSmtpAuthType) || 'login', getFieldValue(InfraConfigEnum.MailerSmtpAuthType) || 'login',
mailer_smtp_oauth2_user: getFieldValue( mailer_smtp_oauth2_user: getFieldValue(
InfraConfigEnum.MailerSmtpOauth2User InfraConfigEnum.MailerSmtpOauth2User,
), ),
mailer_smtp_oauth2_client_id: getFieldValue( mailer_smtp_oauth2_client_id: getFieldValue(
InfraConfigEnum.MailerSmtpOauth2ClientId InfraConfigEnum.MailerSmtpOauth2ClientId,
), ),
mailer_smtp_oauth2_client_secret: getFieldValue( mailer_smtp_oauth2_client_secret: getFieldValue(
InfraConfigEnum.MailerSmtpOauth2ClientSecret InfraConfigEnum.MailerSmtpOauth2ClientSecret,
), ),
mailer_smtp_oauth2_refresh_token: getFieldValue( mailer_smtp_oauth2_refresh_token: getFieldValue(
InfraConfigEnum.MailerSmtpOauth2RefreshToken InfraConfigEnum.MailerSmtpOauth2RefreshToken,
), ),
mailer_smtp_oauth2_access_url: getFieldValue( mailer_smtp_oauth2_access_url: getFieldValue(
InfraConfigEnum.MailerSmtpOauth2AccessUrl InfraConfigEnum.MailerSmtpOauth2AccessUrl,
), ),
}, },
}, },
localAuth: {
name: 'local',
enabled: allowedAuthProviders.value.includes(AuthProvider.Local),
fields: {},
},
tokenConfigs: { tokenConfigs: {
name: 'token', name: 'token',
fields: { fields: {
jwt_secret: getFieldValue(InfraConfigEnum.JwtSecret), jwt_secret: getFieldValue(InfraConfigEnum.JwtSecret),
token_salt_complexity: getFieldValue( token_salt_complexity: getFieldValue(
InfraConfigEnum.TokenSaltComplexity InfraConfigEnum.TokenSaltComplexity,
), ),
magic_link_token_validity: getFieldValue( magic_link_token_validity: getFieldValue(
InfraConfigEnum.MagicLinkTokenValidity InfraConfigEnum.MagicLinkTokenValidity,
), ),
refresh_token_validity: getFieldValue( refresh_token_validity: getFieldValue(
InfraConfigEnum.RefreshTokenValidity InfraConfigEnum.RefreshTokenValidity,
), ),
access_token_validity: getFieldValue( access_token_validity: getFieldValue(
InfraConfigEnum.AccessTokenValidity InfraConfigEnum.AccessTokenValidity,
), ),
session_secret: getFieldValue(InfraConfigEnum.SessionSecret), session_secret: getFieldValue(InfraConfigEnum.SessionSecret),
session_cookie_name: getFieldValue(InfraConfigEnum.SessionCookieName), session_cookie_name: getFieldValue(InfraConfigEnum.SessionCookieName),
@ -186,7 +191,7 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
dataSharingConfigs: { dataSharingConfigs: {
name: 'data_sharing', name: 'data_sharing',
enabled: !!infraConfigs.value.find( enabled: !!infraConfigs.value.find(
(x) => x.name === 'ALLOW_ANALYTICS_COLLECTION' && x.value === 'true' (x) => x.name === 'ALLOW_ANALYTICS_COLLECTION' && x.value === 'true',
), ),
}, },
historyConfig: { historyConfig: {
@ -194,7 +199,7 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
enabled: !!infraConfigs.value.find( enabled: !!infraConfigs.value.find(
(config) => (config) =>
config.name === 'USER_HISTORY_STORE_ENABLED' && config.name === 'USER_HISTORY_STORE_ENABLED' &&
config.value === 'ENABLE' config.value === 'ENABLE',
), ),
}, },
rateLimitConfigs: { rateLimitConfigs: {
@ -208,7 +213,7 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
name: 'mock_server', name: 'mock_server',
fields: { fields: {
mock_server_wildcard_domain: getFieldValue( mock_server_wildcard_domain: getFieldValue(
InfraConfigEnum.MockServerWildcardDomain InfraConfigEnum.MockServerWildcardDomain,
), ),
}, },
}, },
@ -316,8 +321,7 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
return ( return (
section.enabled && section.enabled &&
Object.entries(otherFields).some( Object.entries(otherFields).some(
([key, value]) => ([key, value]) => isFieldEmpty(value) && !excludeKeys.includes(key),
isFieldEmpty(value) && !excludeKeys.includes(key)
) )
); );
} }
@ -327,7 +331,7 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
if (section.name === 'token') { if (section.name === 'token') {
return Object.entries(section.fields).some( return Object.entries(section.fields).some(
([key, value]) => ([key, value]) =>
!OPTIONAL_TOKEN_FIELD_KEYS.has(key) && isFieldNotValid(value) !OPTIONAL_TOKEN_FIELD_KEYS.has(key) && isFieldNotValid(value),
); );
} }
@ -368,20 +372,20 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
(x) => (x) =>
x.key === key && x.key === key &&
key !== 'mailer_smtp_url' && key !== 'mailer_smtp_url' &&
key !== 'mailer_smtp_enabled' key !== 'mailer_smtp_enabled',
); );
} else } else
return MAIL_CONFIGS.some( return MAIL_CONFIGS.some(
(x) => x.key === key && key !== 'mailer_smtp_enabled' (x) => x.key === key && key !== 'mailer_smtp_enabled',
); );
}) }),
); );
// Extract the custom mail config fields // Extract the custom mail config fields
const customMailConfigFields = Object.fromEntries( const customMailConfigFields = Object.fromEntries(
Object.entries(updatedConfigs?.mailConfigs.fields ?? {}).filter(([key]) => Object.entries(updatedConfigs?.mailConfigs.fields ?? {}).filter(([key]) =>
CUSTOM_MAIL_CONFIGS.some((x) => x.key === key) CUSTOM_MAIL_CONFIGS.some((x) => x.key === key),
) ),
); );
// Transforming the working configs back into the format required by the mutations // Transforming the working configs back into the format required by the mutations
@ -441,7 +445,7 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
const executeMutation = async <T, V>( const executeMutation = async <T, V>(
mutation: UseMutationResponse<T>, mutation: UseMutationResponse<T>,
variables: AnyVariables = undefined, variables: AnyVariables = undefined,
errorMessage: string errorMessage: string,
): Promise<boolean> => { ): Promise<boolean> => {
const result = await mutation.executeMutation(variables); const result = await mutation.executeMutation(variables);
@ -460,7 +464,7 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
// Updating the auth provider configurations // Updating the auth provider configurations
const updateAuthProvider = ( const updateAuthProvider = (
updateProviderStatus: UseMutationResponse<EnableAndDisableSsoMutation> updateProviderStatus: UseMutationResponse<EnableAndDisableSsoMutation>,
) => { ) => {
const updatedAllowedAuthProviders: EnableAndDisableSsoArgs[] = [ const updatedAllowedAuthProviders: EnableAndDisableSsoArgs[] = [
{ {
@ -487,6 +491,12 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
? ServiceStatus.Enable ? ServiceStatus.Enable
: ServiceStatus.Disable, : ServiceStatus.Disable,
}, },
{
provider: AuthProvider.Local,
status: updatedConfigs?.localAuth.enabled
? ServiceStatus.Enable
: ServiceStatus.Disable,
},
]; ];
return executeMutation( return executeMutation(
@ -494,13 +504,13 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
{ {
providerInfo: updatedAllowedAuthProviders, providerInfo: updatedAllowedAuthProviders,
}, },
'configs.auth_providers.update_failure' 'configs.auth_providers.update_failure',
); );
}; };
// Updating the infra configurations // Updating the infra configurations
const updateInfraConfigs = ( const updateInfraConfigs = (
updateInfraConfigsMutation: UseMutationResponse<UpdateInfraConfigsMutation> updateInfraConfigsMutation: UseMutationResponse<UpdateInfraConfigsMutation>,
) => { ) => {
const infraConfigs: InfraConfigArgs[] = updatedConfigs const infraConfigs: InfraConfigArgs[] = updatedConfigs
? transformInfraConfigs() ? transformInfraConfigs()
@ -511,23 +521,23 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
{ {
infraConfigs, infraConfigs,
}, },
'configs.update_failure' 'configs.update_failure',
); );
}; };
// Resetting the infra configurations // Resetting the infra configurations
const resetInfraConfigs = ( const resetInfraConfigs = (
resetInfraConfigsMutation: UseMutationResponse<ResetInfraConfigsMutation> resetInfraConfigsMutation: UseMutationResponse<ResetInfraConfigsMutation>,
) => ) =>
executeMutation( executeMutation(
resetInfraConfigsMutation, resetInfraConfigsMutation,
undefined, undefined,
'configs.reset.failure' 'configs.reset.failure',
); );
// Toggle Data Sharing // Toggle Data Sharing
const updateDataSharingConfigs = ( const updateDataSharingConfigs = (
toggleDataSharingMutation: UseMutationResponse<ToggleAnalyticsCollectionMutation> toggleDataSharingMutation: UseMutationResponse<ToggleAnalyticsCollectionMutation>,
) => ) =>
executeMutation( executeMutation(
toggleDataSharingMutation, toggleDataSharingMutation,
@ -536,12 +546,12 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
? ServiceStatus.Enable ? ServiceStatus.Enable
: ServiceStatus.Disable, : ServiceStatus.Disable,
}, },
'configs.data_sharing.update_failure' 'configs.data_sharing.update_failure',
); );
// Toggle SMTP // Toggle SMTP
const toggleSMTPConfigs = ( const toggleSMTPConfigs = (
toggleSMTP: UseMutationResponse<ToggleSmtpMutation> toggleSMTP: UseMutationResponse<ToggleSmtpMutation>,
) => ) =>
executeMutation( executeMutation(
toggleSMTP, toggleSMTP,
@ -550,12 +560,12 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
? ServiceStatus.Enable ? ServiceStatus.Enable
: ServiceStatus.Disable, : ServiceStatus.Disable,
}, },
'configs.mail_configs.toggle_failure' 'configs.mail_configs.toggle_failure',
); );
// Toggle User History Store // Toggle User History Store
const toggleUserHistoryStore = ( const toggleUserHistoryStore = (
toggleUserHistoryStore: UseMutationResponse<ToggleUserHistoryStoreMutation> toggleUserHistoryStore: UseMutationResponse<ToggleUserHistoryStoreMutation>,
) => ) =>
executeMutation( executeMutation(
toggleUserHistoryStore, toggleUserHistoryStore,
@ -564,11 +574,11 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
? ServiceStatus.Enable ? ServiceStatus.Enable
: ServiceStatus.Disable, : ServiceStatus.Disable,
}, },
'configs.user_history_store.toggle_failure' 'configs.user_history_store.toggle_failure',
); );
const updateRateLimitConfigs = ( const updateRateLimitConfigs = (
updateRateLimitMutation: UseMutationResponse<UpdateInfraConfigsMutation> updateRateLimitMutation: UseMutationResponse<UpdateInfraConfigsMutation>,
) => { ) => {
if (!updatedConfigs?.rateLimitConfigs) { if (!updatedConfigs?.rateLimitConfigs) {
toast.error(t('configs.rate_limit.input_validation_error')); toast.error(t('configs.rate_limit.input_validation_error'));
@ -576,10 +586,10 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
} }
const rateLimitTtl = String( const rateLimitTtl = String(
updatedConfigs?.rateLimitConfigs.fields.rate_limit_ttl updatedConfigs?.rateLimitConfigs.fields.rate_limit_ttl,
); );
const rateLimitMax = String( const rateLimitMax = String(
updatedConfigs?.rateLimitConfigs.fields.rate_limit_max updatedConfigs?.rateLimitConfigs.fields.rate_limit_max,
); );
if (isFieldEmpty(rateLimitTtl) || isFieldEmpty(rateLimitMax)) { if (isFieldEmpty(rateLimitTtl) || isFieldEmpty(rateLimitMax)) {
@ -603,12 +613,12 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
{ {
infraConfigs: rateLimitConfigs, infraConfigs: rateLimitConfigs,
}, },
'configs.rate_limit.update_failure' 'configs.rate_limit.update_failure',
); );
}; };
const updateAuthTokenConfigs = ( const updateAuthTokenConfigs = (
updateAuthTokenMutation: UseMutationResponse<UpdateInfraConfigsMutation> updateAuthTokenMutation: UseMutationResponse<UpdateInfraConfigsMutation>,
) => { ) => {
if (!updatedConfigs?.tokenConfigs) { if (!updatedConfigs?.tokenConfigs) {
toast.error(t('configs.auth_providers.token.update_failure')); toast.error(t('configs.auth_providers.token.update_failure'));
@ -617,26 +627,28 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
const jwtSecret = String(updatedConfigs?.tokenConfigs.fields.jwt_secret); const jwtSecret = String(updatedConfigs?.tokenConfigs.fields.jwt_secret);
const tokenSaltComplexity = String( const tokenSaltComplexity = String(
updatedConfigs?.tokenConfigs.fields.token_salt_complexity updatedConfigs?.tokenConfigs.fields.token_salt_complexity,
); );
const magicLinkTokenValidity = String( const magicLinkTokenValidity = String(
updatedConfigs?.tokenConfigs.fields.magic_link_token_validity updatedConfigs?.tokenConfigs.fields.magic_link_token_validity,
); );
const refreshTokenValidity = String( const refreshTokenValidity = String(
updatedConfigs?.tokenConfigs.fields.refresh_token_validity updatedConfigs?.tokenConfigs.fields.refresh_token_validity,
); );
const accessTokenValidity = String( const accessTokenValidity = String(
updatedConfigs?.tokenConfigs.fields.access_token_validity updatedConfigs?.tokenConfigs.fields.access_token_validity,
); );
const sessionSecret = String( const sessionSecret = String(
updatedConfigs?.tokenConfigs.fields.session_secret updatedConfigs?.tokenConfigs.fields.session_secret,
); );
const sessionCookieName = String( const sessionCookieName = String(
updatedConfigs?.tokenConfigs.fields.session_cookie_name || '' updatedConfigs?.tokenConfigs.fields.session_cookie_name || '',
); );
// Validate cookie name: allow empty (falls back to default), else enforce pattern // Validate cookie name: allow empty (falls back to default), else enforce pattern
if (sessionCookieName && !COOKIE_NAME_REGEX.test(sessionCookieName)) { if (sessionCookieName && !COOKIE_NAME_REGEX.test(sessionCookieName)) {
toast.error(t('configs.auth_providers.token.session_cookie_name_invalid')); toast.error(
t('configs.auth_providers.token.session_cookie_name_invalid'),
);
return false; return false;
} }
if ( if (
@ -687,7 +699,7 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
{ {
infraConfigs: authTokenConfigs, infraConfigs: authTokenConfigs,
}, },
'configs.auth_providers.token.update_failure' 'configs.auth_providers.token.update_failure',
); );
}; };

View file

@ -65,6 +65,12 @@ export type ServerConfigs = {
}; };
}; };
localAuth: {
name: string;
enabled: boolean;
fields: Record<string, never>;
};
tokenConfigs: { tokenConfigs: {
name: string; name: string;
fields: { fields: {