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