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_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_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."
},
"configs": {
@ -36,6 +39,7 @@
"description": "Configure authentication providers for your server",
"email": "Email",
"github_enterprise": "Enable Github Enterprise",
"local": "Username and Password",
"oauth": "OAuth",
"oauth_providers": "OAuth Providers",
"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": {
"local": {
"confirm_password": "Confirmer le mot de passe",

View file

@ -62,6 +62,36 @@
</div>
</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')">
<div class="pb-8 px-4">
<SettingsAuthToken v-model:config="workingConfigs" />
@ -88,14 +118,14 @@ const emit = defineEmits<{
}>();
// 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 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.
const isSMTPActivated = computed(
() => workingConfigs.value?.mailConfigs.enabled ?? false
() => workingConfigs.value?.mailConfigs.enabled ?? false,
);
// 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
const isSMTPEnabled = ref(false);

View file

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