feat: add configurable login notification modes (#1813)

This commit is contained in:
Elias Schneider
2026-10-07 20:16:27 +02:00
committed by GitHub
parent 926f5c872d
commit c869cacada
60 changed files with 1466 additions and 641 deletions
+8
View File
@@ -202,6 +202,14 @@
"this_can_be_useful_for_selfsigned_certificates": "This can be useful for self-signed certificates.",
"enabled_emails": "Enabled Emails",
"email_login_notification": "Email Login Notification",
"email_login_notification_description": "Choose when users receive an email after signing in.",
"login_notification_disabled_description": "Do not send sign-in notification emails or use browser recognition cookies.",
"login_notification_always": "Every sign-in",
"login_notification_always_description": "Send an email after every successful sign-in. No browser recognition cookie is used.",
"login_notification_ip_and_user_agent": "New IP address or browser",
"login_notification_ip_and_user_agent_description": "Send an email when the exact combination of IP address and browser User-Agent is not in the user's sign-in history. No browser recognition cookie is used.",
"login_notification_browser_recognition": "Unrecognized browser",
"login_notification_browser_recognition_description": "Send an email unless the browser is recognized by its cookie or a matching IP address and User-Agent in the user's sign-in history.",
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Send an email to the user when they log in from a new device.",
"emai_login_code_requested_by_user": "Email Login Code Requested by User",
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This significantly reduces security as anyone with access to the user's email can gain entry.",
@@ -31,7 +31,7 @@ export type AllAppConfig = AppConfig & {
smtpPassword: string;
smtpTls: 'none' | 'starttls' | 'tls';
smtpSkipCertVerify: boolean;
emailLoginNotificationEnabled: boolean;
emailLoginNotificationMode: 'disabled' | 'always' | 'ipAndUserAgent' | 'browserRecognition';
emailApiKeyExpirationEnabled: boolean;
// LDAP
ldapUrl: string;
@@ -30,6 +30,22 @@
tls: 'TLS'
};
const notificationOptions = $derived({
disabled: { label: m.never(), description: m.login_notification_disabled_description() },
always: {
label: m.login_notification_always(),
description: m.login_notification_always_description()
},
ipAndUserAgent: {
label: m.login_notification_ip_and_user_agent(),
description: m.login_notification_ip_and_user_agent_description()
},
browserRecognition: {
label: m.login_notification_browser_recognition(),
description: m.login_notification_browser_recognition_description()
}
});
let isSendingTestEmail = $state(false);
const formSchema = z
@@ -49,7 +65,12 @@
emailOneTimeAccessAsUnauthenticatedEnabled: z.boolean(),
emailVerificationEnabled: z.boolean(),
emailOneTimeAccessAsAdminEnabled: z.boolean(),
emailLoginNotificationEnabled: z.boolean(),
emailLoginNotificationMode: z.enum([
'disabled',
'always',
'ipAndUserAgent',
'browserRecognition'
]),
emailApiKeyExpirationEnabled: z.boolean()
})
.superRefine((data, ctx) => {
@@ -63,7 +84,6 @@
'emailOneTimeAccessAsUnauthenticatedEnabled',
'emailVerificationEnabled',
'emailOneTimeAccessAsAdminEnabled',
'emailLoginNotificationEnabled',
'emailApiKeyExpirationEnabled'
];
@@ -84,7 +104,8 @@
const anyProvided = requiredSmtpFields.some((f) => !!data[f]);
requireFieldsWhen(anyProvided, m.smtp_field_required_when_other_provided());
const emailEnabled = emailFields.some((f) => data[f]);
const emailEnabled =
data.emailLoginNotificationMode !== 'disabled' || emailFields.some((f) => data[f]);
requireFieldsWhen(emailEnabled, m.smtp_field_required_when_email_enabled());
});
@@ -194,12 +215,37 @@
</div>
<h4 class="mt-10 text-lg font-semibold">{m.enabled_emails()}</h4>
<div class="mt-4 flex flex-col gap-5">
<SwitchWithLabel
id="email-login-notification"
label={m.email_login_notification()}
description={m.send_an_email_to_the_user_when_they_log_in_from_a_new_device()}
bind:checked={$inputs.emailLoginNotificationEnabled.value}
/>
<Field.Field>
<div>
<Field.Label for="email-login-notification">{m.email_login_notification()}</Field.Label>
<Field.Description>{m.email_login_notification_description()}</Field.Description>
</div>
<Select.Root
type="single"
disabled={$appConfigStore.uiConfigDisabled}
value={$inputs.emailLoginNotificationMode.value}
allowDeselect={false}
onValueChange={(value) =>
($inputs.emailLoginNotificationMode.value =
value as AllAppConfig['emailLoginNotificationMode'])}
>
<Select.Trigger id="email-login-notification" class="w-full">
{notificationOptions[$inputs.emailLoginNotificationMode.value].label}
</Select.Trigger>
<Select.Content class="w-[calc(var(--bits-select-anchor-width)+--spacing(3))]">
<Select.Group>
{#each Object.entries(notificationOptions) as [value, option] (value)}
<Select.Item {value} label={option.label}>
<div class="flex flex-col items-start gap-1 whitespace-normal">
<span class="font-medium">{option.label}</span>
<span class="text-muted-foreground text-xs">{option.description}</span>
</div>
</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Root>
</Field.Field>
<SwitchWithLabel
id="email-verification"
label={m.email_verification()}