From d479ea3d838ed25feceeadf2d90b7fbc8f3c88f6 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Sat, 7 Mar 2026 15:27:16 +0100 Subject: [PATCH 1/9] selects in settings --- .../SettingsEnrollmentPage.tsx | 72 ++++++++++++++++--- .../SettingsInstancePage.tsx | 24 ++++++- .../SettingsVpnStatsPage.tsx | 44 ++++++++++-- web/src/shared/const/numericSelectOptions.ts | 41 +++++++++++ 4 files changed, 167 insertions(+), 14 deletions(-) create mode 100644 web/src/shared/const/numericSelectOptions.ts diff --git a/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx b/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx index 81c2bbb020..e205b0bd60 100644 --- a/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx +++ b/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx @@ -11,6 +11,12 @@ import { Page } from '../../../shared/components/Page/Page'; import { SettingsCard } from '../../../shared/components/SettingsCard/SettingsCard'; import { SettingsHeader } from '../../../shared/components/SettingsHeader/SettingsHeader'; import { SettingsLayout } from '../../../shared/components/SettingsLayout/SettingsLayout'; +import { + createNumericSelectOptions, + formatHourSelectLabel, + formatMinuteSelectLabel, + withNumericFallbackOption, +} from '../../../shared/const/numericSelectOptions'; import { Button } from '../../../shared/defguard-ui/components/Button/Button'; import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { ThemeSpacing } from '../../../shared/defguard-ui/types'; @@ -64,6 +70,16 @@ const formSchema = z.object({ type FormFields = z.infer; +const enrollmentTokenTimeoutBaseOptions = createNumericSelectOptions( + [1, 2, 4, 8, 12, 24, 48, 72, 168], + formatHourSelectLabel, +); + +const enrollmentSessionTimeoutBaseOptions = createNumericSelectOptions( + [5, 10, 15, 30, 45, 60], + formatMinuteSelectLabel, +); + const Content = ({ settings }: { settings: Settings }) => { const { mutateAsync } = useMutation({ mutationFn: api.settings.patchSettings, @@ -85,6 +101,46 @@ const Content = ({ settings }: { settings: Settings }) => { [settings], ); + const enrollmentTokenTimeoutOptions = useMemo( + () => + withNumericFallbackOption( + enrollmentTokenTimeoutBaseOptions, + defaultValues.enrollment_token_timeout_hours, + formatHourSelectLabel, + ), + [defaultValues.enrollment_token_timeout_hours], + ); + + const passwordResetTokenTimeoutOptions = useMemo( + () => + withNumericFallbackOption( + enrollmentTokenTimeoutBaseOptions, + defaultValues.password_reset_token_timeout_hours, + formatHourSelectLabel, + ), + [defaultValues.password_reset_token_timeout_hours], + ); + + const enrollmentSessionTimeoutOptions = useMemo( + () => + withNumericFallbackOption( + enrollmentSessionTimeoutBaseOptions, + defaultValues.enrollment_session_timeout_minutes, + formatMinuteSelectLabel, + ), + [defaultValues.enrollment_session_timeout_minutes], + ); + + const passwordResetSessionTimeoutOptions = useMemo( + () => + withNumericFallbackOption( + enrollmentSessionTimeoutBaseOptions, + defaultValues.password_reset_session_timeout_minutes, + formatMinuteSelectLabel, + ), + [defaultValues.password_reset_session_timeout_minutes], + ); + const form = useAppForm({ defaultValues, validationLogic: formChangeLogic, @@ -109,40 +165,40 @@ const Content = ({ settings }: { settings: Settings }) => { {(field) => ( - )} {(field) => ( - )} {(field) => ( - )} {(field) => ( - )} diff --git a/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx b/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx index c13edd7001..84e40fb1a1 100644 --- a/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx +++ b/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx @@ -11,6 +11,11 @@ import { Page } from '../../../shared/components/Page/Page'; import { SettingsCard } from '../../../shared/components/SettingsCard/SettingsCard'; import { SettingsHeader } from '../../../shared/components/SettingsHeader/SettingsHeader'; import { SettingsLayout } from '../../../shared/components/SettingsLayout/SettingsLayout'; +import { + createNumericSelectOptions, + formatDaySelectLabel, + withNumericFallbackOption, +} from '../../../shared/const/numericSelectOptions'; import { Button } from '../../../shared/defguard-ui/components/Button/Button'; import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { Snackbar } from '../../../shared/defguard-ui/providers/snackbar/snackbar'; @@ -99,6 +104,11 @@ const sessionDurationOptions = [ }, ]; +const authCookieTimeoutBaseOptions = createNumericSelectOptions( + [1, 2, 3, 7, 10, 14, 30], + formatDaySelectLabel, +); + const Content = ({ settings }: { settings: Settings }) => { const { mutateAsync } = useMutation({ mutationFn: api.settings.patchSettings, @@ -128,6 +138,16 @@ const Content = ({ settings }: { settings: Settings }) => { ], ); + const authCookieTimeoutOptions = useMemo( + () => + withNumericFallbackOption( + authCookieTimeoutBaseOptions, + defaultValues.auth_cookie_timeout_days, + formatDaySelectLabel, + ), + [defaultValues.auth_cookie_timeout_days], + ); + const form = useAppForm({ defaultValues, validationLogic: formChangeLogic, @@ -177,10 +197,10 @@ const Content = ({ settings }: { settings: Settings }) => { {(field) => ( - )} diff --git a/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx b/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx index d45f079dd3..ad5a4b7bcd 100644 --- a/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx +++ b/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx @@ -11,6 +11,12 @@ import { Page } from '../../../shared/components/Page/Page'; import { SettingsCard } from '../../../shared/components/SettingsCard/SettingsCard'; import { SettingsHeader } from '../../../shared/components/SettingsHeader/SettingsHeader'; import { SettingsLayout } from '../../../shared/components/SettingsLayout/SettingsLayout'; +import { + createNumericSelectOptions, + formatDaySelectLabel, + formatHourSelectLabel, + withNumericFallbackOption, +} from '../../../shared/const/numericSelectOptions'; import { Button } from '../../../shared/defguard-ui/components/Button/Button'; import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { ThemeSpacing } from '../../../shared/defguard-ui/types'; @@ -63,6 +69,16 @@ const formSchema = z.object({ type FormFields = z.infer; +const statsPurgeFrequencyBaseOptions = createNumericSelectOptions( + [1, 6, 12, 24, 48, 72, 168], + formatHourSelectLabel, +); + +const statsPurgeThresholdBaseOptions = createNumericSelectOptions( + [1, 7, 14, 30, 60, 90, 180, 365], + formatDaySelectLabel, +); + const Content = ({ settings }: { settings: Settings }) => { const { mutateAsync } = useMutation({ mutationFn: api.settings.patchSettings, @@ -80,6 +96,26 @@ const Content = ({ settings }: { settings: Settings }) => { [settings], ); + const statsPurgeFrequencyOptions = useMemo( + () => + withNumericFallbackOption( + statsPurgeFrequencyBaseOptions, + defaultValues.stats_purge_frequency_hours, + formatHourSelectLabel, + ), + [defaultValues.stats_purge_frequency_hours], + ); + + const statsPurgeThresholdOptions = useMemo( + () => + withNumericFallbackOption( + statsPurgeThresholdBaseOptions, + defaultValues.stats_purge_threshold_days, + formatDaySelectLabel, + ), + [defaultValues.stats_purge_threshold_days], + ); + const form = useAppForm({ defaultValues, validationLogic: formChangeLogic, @@ -114,20 +150,20 @@ const Content = ({ settings }: { settings: Settings }) => { {(field) => ( - )} {(field) => ( - )} diff --git a/web/src/shared/const/numericSelectOptions.ts b/web/src/shared/const/numericSelectOptions.ts new file mode 100644 index 0000000000..e46bf138e0 --- /dev/null +++ b/web/src/shared/const/numericSelectOptions.ts @@ -0,0 +1,41 @@ +import type { SelectOption } from '../defguard-ui/components/Select/types'; + +export type NumericSelectOption = SelectOption; + +export const formatDaySelectLabel = (value: number) => + `${value} ${value === 1 ? 'day' : 'days'}`; + +export const formatHourSelectLabel = (value: number) => + `${value} ${value === 1 ? 'hour' : 'hours'}`; + +export const formatMinuteSelectLabel = (value: number) => + `${value} ${value === 1 ? 'minute' : 'minutes'}`; + +export const createNumericSelectOptions = ( + values: readonly number[], + formatLabel: (value: number) => string, +): NumericSelectOption[] => + values.map((value) => ({ + key: value, + label: formatLabel(value), + value, + })); + +export const withNumericFallbackOption = ( + options: readonly NumericSelectOption[], + value: number, + formatLabel: (value: number) => string, +): NumericSelectOption[] => { + if (options.some((option) => option.value === value)) { + return [...options]; + } + + return [ + ...options, + { + key: value, + label: formatLabel(value), + value, + }, + ].sort((a, b) => a.value - b.value); +}; From 1888077d40dd4aed11eadc2ee2bc1328e0a43d85 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Sat, 7 Mar 2026 15:40:23 +0100 Subject: [PATCH 2/9] remove unused auth_cookie_timeout setting --- ...4ec260d08f8feee279c41f73fd8e33aea5aa.json} | 5 +-- ...a24916ddd638337ed9427d94db5b1dc9766a.json} | 26 +++++------ crates/defguard_common/src/config.rs | 9 +--- .../defguard_common/src/db/models/settings.rs | 35 +++++---------- ...0227091211_[2.0.0]_settings_in_db.down.sql | 1 - ...260227091211_[2.0.0]_settings_in_db.up.sql | 1 - web/messages/en/settings.json | 1 - .../SettingsInstancePage.tsx | 43 +++---------------- web/src/shared/api/types.ts | 1 - 9 files changed, 29 insertions(+), 93 deletions(-) rename .sqlx/{query-96917ed09e836086c38396c5778b83ca94ffff8f5f636ef650250befbcd78ab4.json => query-386714aa5e0cbc2d896edf6dcd2f4ec260d08f8feee279c41f73fd8e33aea5aa.json} (87%) rename .sqlx/{query-a043fb7d435267b6c1bd9febbbeb0bc1d29e14daf738f64246f8deea246f2630.json => query-bb022291c5a000545df91eb246c5a24916ddd638337ed9427d94db5b1dc9766a.json} (95%) diff --git a/.sqlx/query-96917ed09e836086c38396c5778b83ca94ffff8f5f636ef650250befbcd78ab4.json b/.sqlx/query-386714aa5e0cbc2d896edf6dcd2f4ec260d08f8feee279c41f73fd8e33aea5aa.json similarity index 87% rename from .sqlx/query-96917ed09e836086c38396c5778b83ca94ffff8f5f636ef650250befbcd78ab4.json rename to .sqlx/query-386714aa5e0cbc2d896edf6dcd2f4ec260d08f8feee279c41f73fd8e33aea5aa.json index 444299927c..2016018b0a 100644 --- a/.sqlx/query-96917ed09e836086c38396c5778b83ca94ffff8f5f636ef650250befbcd78ab4.json +++ b/.sqlx/query-386714aa5e0cbc2d896edf6dcd2f4ec260d08f8feee279c41f73fd8e33aea5aa.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"settings\" SET openid_enabled = $1, wireguard_enabled = $2, webhooks_enabled = $3, worker_enabled = $4, challenge_template = $5, instance_name = $6, main_logo_url = $7, nav_logo_url = $8, smtp_server = $9, smtp_port = $10, smtp_encryption = $11, smtp_user = $12, smtp_password = $13, smtp_sender = $14, enrollment_vpn_step_optional = $15, enrollment_welcome_message = $16, enrollment_welcome_email = $17, enrollment_welcome_email_subject = $18, enrollment_use_welcome_message_as_email = $19, uuid = $20, ldap_url = $21, ldap_bind_username = $22, ldap_bind_password = $23, ldap_group_search_base = $24, ldap_user_search_base = $25, ldap_user_obj_class = $26, ldap_group_obj_class = $27, ldap_username_attr = $28, ldap_groupname_attr = $29, ldap_group_member_attr = $30, ldap_member_attr = $31, ldap_use_starttls = $32, ldap_tls_verify_cert = $33, openid_create_account = $34, license = $35, gateway_disconnect_notifications_enabled = $36, gateway_disconnect_notifications_inactivity_threshold = $37, gateway_disconnect_notifications_reconnect_notification_enabled = $38, ldap_sync_status = $39, ldap_enabled = $40, ldap_sync_enabled = $41, ldap_is_authoritative = $42, ldap_sync_interval = $43, ldap_user_auxiliary_obj_classes = $44, ldap_uses_ad = $45, ldap_user_rdn_attr = $46, ldap_sync_groups = $47, openid_username_handling = $48, ca_key_der = $49, ca_cert_der = $50, ca_expiry = $51, defguard_url = $52, default_admin_group_name = $53, authentication_period_days = $54, mfa_code_timeout_seconds = $55, public_proxy_url = $56, default_admin_id = $57, auth_cookie_timeout_days = $58, secret_key = $59, webauthn_rp_id = $60, disable_stats_purge = $61, stats_purge_frequency_hours = $62, stats_purge_threshold_days = $63, enrollment_token_timeout_hours = $64, password_reset_token_timeout_hours = $65, enrollment_session_timeout_minutes = $66, password_reset_session_timeout_minutes = $67 WHERE id = 1", + "query": "UPDATE \"settings\" SET openid_enabled = $1, wireguard_enabled = $2, webhooks_enabled = $3, worker_enabled = $4, challenge_template = $5, instance_name = $6, main_logo_url = $7, nav_logo_url = $8, smtp_server = $9, smtp_port = $10, smtp_encryption = $11, smtp_user = $12, smtp_password = $13, smtp_sender = $14, enrollment_vpn_step_optional = $15, enrollment_welcome_message = $16, enrollment_welcome_email = $17, enrollment_welcome_email_subject = $18, enrollment_use_welcome_message_as_email = $19, uuid = $20, ldap_url = $21, ldap_bind_username = $22, ldap_bind_password = $23, ldap_group_search_base = $24, ldap_user_search_base = $25, ldap_user_obj_class = $26, ldap_group_obj_class = $27, ldap_username_attr = $28, ldap_groupname_attr = $29, ldap_group_member_attr = $30, ldap_member_attr = $31, ldap_use_starttls = $32, ldap_tls_verify_cert = $33, openid_create_account = $34, license = $35, gateway_disconnect_notifications_enabled = $36, gateway_disconnect_notifications_inactivity_threshold = $37, gateway_disconnect_notifications_reconnect_notification_enabled = $38, ldap_sync_status = $39, ldap_enabled = $40, ldap_sync_enabled = $41, ldap_is_authoritative = $42, ldap_sync_interval = $43, ldap_user_auxiliary_obj_classes = $44, ldap_uses_ad = $45, ldap_user_rdn_attr = $46, ldap_sync_groups = $47, openid_username_handling = $48, ca_key_der = $49, ca_cert_der = $50, ca_expiry = $51, defguard_url = $52, default_admin_group_name = $53, authentication_period_days = $54, mfa_code_timeout_seconds = $55, public_proxy_url = $56, default_admin_id = $57, secret_key = $58, webauthn_rp_id = $59, disable_stats_purge = $60, stats_purge_frequency_hours = $61, stats_purge_threshold_days = $62, enrollment_token_timeout_hours = $63, password_reset_token_timeout_hours = $64, enrollment_session_timeout_minutes = $65, password_reset_session_timeout_minutes = $66 WHERE id = 1", "describe": { "columns": [], "parameters": { @@ -94,7 +94,6 @@ "Int4", "Text", "Int8", - "Int4", "Text", "Text", "Bool", @@ -108,5 +107,5 @@ }, "nullable": [] }, - "hash": "96917ed09e836086c38396c5778b83ca94ffff8f5f636ef650250befbcd78ab4" + "hash": "386714aa5e0cbc2d896edf6dcd2f4ec260d08f8feee279c41f73fd8e33aea5aa" } diff --git a/.sqlx/query-a043fb7d435267b6c1bd9febbbeb0bc1d29e14daf738f64246f8deea246f2630.json b/.sqlx/query-bb022291c5a000545df91eb246c5a24916ddd638337ed9427d94db5b1dc9766a.json similarity index 95% rename from .sqlx/query-a043fb7d435267b6c1bd9febbbeb0bc1d29e14daf738f64246f8deea246f2630.json rename to .sqlx/query-bb022291c5a000545df91eb246c5a24916ddd638337ed9427d94db5b1dc9766a.json index caa5f6b3a7..0e6b201a60 100644 --- a/.sqlx/query-a043fb7d435267b6c1bd9febbbeb0bc1d29e14daf738f64246f8deea246f2630.json +++ b/.sqlx/query-bb022291c5a000545df91eb246c5a24916ddd638337ed9427d94db5b1dc9766a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT openid_enabled, wireguard_enabled, webhooks_enabled, worker_enabled, challenge_template, instance_name, main_logo_url, nav_logo_url, smtp_server, smtp_port, smtp_encryption \"smtp_encryption: _\", smtp_user, smtp_password \"smtp_password?: SecretStringWrapper\", smtp_sender, enrollment_vpn_step_optional, enrollment_welcome_message, enrollment_welcome_email, enrollment_welcome_email_subject, enrollment_use_welcome_message_as_email, uuid, ldap_url, ldap_bind_username, ldap_bind_password \"ldap_bind_password?: SecretStringWrapper\", ldap_group_search_base, ldap_user_search_base, ldap_user_obj_class, ldap_group_obj_class, ldap_username_attr, ldap_groupname_attr, ldap_group_member_attr, ldap_member_attr, openid_create_account, license, gateway_disconnect_notifications_enabled, ldap_use_starttls, ldap_tls_verify_cert, gateway_disconnect_notifications_inactivity_threshold, gateway_disconnect_notifications_reconnect_notification_enabled, ldap_sync_status \"ldap_sync_status: LdapSyncStatus\", ldap_enabled, ldap_sync_enabled, ldap_is_authoritative, ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, ldap_user_rdn_attr, ldap_sync_groups, openid_username_handling \"openid_username_handling: OpenIdUsernameHandling\", ca_key_der, ca_cert_der, ca_expiry, defguard_url, default_admin_group_name, authentication_period_days, mfa_code_timeout_seconds, public_proxy_url, default_admin_id, auth_cookie_timeout_days, secret_key, webauthn_rp_id, disable_stats_purge, stats_purge_frequency_hours, stats_purge_threshold_days, enrollment_token_timeout_hours, password_reset_token_timeout_hours, enrollment_session_timeout_minutes, password_reset_session_timeout_minutes FROM \"settings\" WHERE id = 1", + "query": "SELECT openid_enabled, wireguard_enabled, webhooks_enabled, worker_enabled, challenge_template, instance_name, main_logo_url, nav_logo_url, smtp_server, smtp_port, smtp_encryption \"smtp_encryption: _\", smtp_user, smtp_password \"smtp_password?: SecretStringWrapper\", smtp_sender, enrollment_vpn_step_optional, enrollment_welcome_message, enrollment_welcome_email, enrollment_welcome_email_subject, enrollment_use_welcome_message_as_email, uuid, ldap_url, ldap_bind_username, ldap_bind_password \"ldap_bind_password?: SecretStringWrapper\", ldap_group_search_base, ldap_user_search_base, ldap_user_obj_class, ldap_group_obj_class, ldap_username_attr, ldap_groupname_attr, ldap_group_member_attr, ldap_member_attr, openid_create_account, license, gateway_disconnect_notifications_enabled, ldap_use_starttls, ldap_tls_verify_cert, gateway_disconnect_notifications_inactivity_threshold, gateway_disconnect_notifications_reconnect_notification_enabled, ldap_sync_status \"ldap_sync_status: LdapSyncStatus\", ldap_enabled, ldap_sync_enabled, ldap_is_authoritative, ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, ldap_user_rdn_attr, ldap_sync_groups, openid_username_handling \"openid_username_handling: OpenIdUsernameHandling\", ca_key_der, ca_cert_der, ca_expiry, defguard_url, default_admin_group_name, authentication_period_days, mfa_code_timeout_seconds, public_proxy_url, default_admin_id, secret_key, webauthn_rp_id, disable_stats_purge, stats_purge_frequency_hours, stats_purge_threshold_days, enrollment_token_timeout_hours, password_reset_token_timeout_hours, enrollment_session_timeout_minutes, password_reset_session_timeout_minutes FROM \"settings\" WHERE id = 1", "describe": { "columns": [ { @@ -322,51 +322,46 @@ }, { "ordinal": 57, - "name": "auth_cookie_timeout_days", - "type_info": "Int4" - }, - { - "ordinal": 58, "name": "secret_key", "type_info": "Text" }, { - "ordinal": 59, + "ordinal": 58, "name": "webauthn_rp_id", "type_info": "Text" }, { - "ordinal": 60, + "ordinal": 59, "name": "disable_stats_purge", "type_info": "Bool" }, { - "ordinal": 61, + "ordinal": 60, "name": "stats_purge_frequency_hours", "type_info": "Int4" }, { - "ordinal": 62, + "ordinal": 61, "name": "stats_purge_threshold_days", "type_info": "Int4" }, { - "ordinal": 63, + "ordinal": 62, "name": "enrollment_token_timeout_hours", "type_info": "Int4" }, { - "ordinal": 64, + "ordinal": 63, "name": "password_reset_token_timeout_hours", "type_info": "Int4" }, { - "ordinal": 65, + "ordinal": 64, "name": "enrollment_session_timeout_minutes", "type_info": "Int4" }, { - "ordinal": 66, + "ordinal": 65, "name": "password_reset_session_timeout_minutes", "type_info": "Int4" } @@ -432,7 +427,6 @@ false, false, true, - false, true, true, false, @@ -444,5 +438,5 @@ false ] }, - "hash": "a043fb7d435267b6c1bd9febbbeb0bc1d29e14daf738f64246f8deea246f2630" + "hash": "bb022291c5a000545df91eb246c5a24916ddd638337ed9427d94db5b1dc9766a" } diff --git a/crates/defguard_common/src/config.rs b/crates/defguard_common/src/config.rs index 091e82e3aa..2ad3a5b430 100644 --- a/crates/defguard_common/src/config.rs +++ b/crates/defguard_common/src/config.rs @@ -3,13 +3,13 @@ use std::{net::IpAddr, sync::OnceLock}; use clap::{Args, Parser, Subcommand}; use humantime::Duration; use ipnetwork::IpNetwork; -use openidconnect::{JsonWebKeyId, core::CoreRsaPrivateSigningKey}; +use openidconnect::{core::CoreRsaPrivateSigningKey, JsonWebKeyId}; use reqwest::Url; use rsa::{ - RsaPrivateKey, pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey}, pkcs8::{DecodePrivateKey, LineEnding}, traits::PublicKeyParts, + RsaPrivateKey, }; use secrecy::{ExposeSecret, SecretString}; use serde::Serialize; @@ -37,11 +37,6 @@ pub struct DefGuardConfig { #[arg(long, env = "DEFGUARD_LOG_FILE")] pub log_file: Option, - #[arg(long, env = "DEFGUARD_AUTH_COOKIE_TIMEOUT")] - #[serde(skip_serializing)] - #[deprecated(since = "2.0.0", note = "Use Settings.auth_cookie_timeout instead")] - pub auth_cookie_timeout: Option, - #[arg(long, env = "DEFGUARD_SECRET_KEY")] #[serde(skip_serializing)] #[deprecated(since = "2.0.0", note = "Use Settings.secret_key instead")] diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index 5c757d41b1..936b0575eb 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -180,7 +180,6 @@ pub struct Settings { pub secret_key: Option, pub webauthn_rp_id: Option, pub disable_stats_purge: bool, - auth_cookie_timeout_days: i32, stats_purge_frequency_hours: i32, stats_purge_threshold_days: i32, enrollment_token_timeout_hours: i32, @@ -332,7 +331,7 @@ impl Settings { ca_key_der, ca_cert_der, ca_expiry, defguard_url, \ default_admin_group_name, authentication_period_days, mfa_code_timeout_seconds, \ public_proxy_url, \ - default_admin_id, auth_cookie_timeout_days, secret_key, webauthn_rp_id, disable_stats_purge, \ + default_admin_id, secret_key, webauthn_rp_id, disable_stats_purge, \ stats_purge_frequency_hours, stats_purge_threshold_days, \ enrollment_token_timeout_hours, password_reset_token_timeout_hours, \ enrollment_session_timeout_minutes, password_reset_session_timeout_minutes \ @@ -422,16 +421,15 @@ impl Settings { mfa_code_timeout_seconds = $55, \ public_proxy_url = $56, \ default_admin_id = $57, \ - auth_cookie_timeout_days = $58, \ - secret_key = $59, \ - webauthn_rp_id = $60, \ - disable_stats_purge = $61, \ - stats_purge_frequency_hours = $62, \ - stats_purge_threshold_days = $63, \ - enrollment_token_timeout_hours = $64, \ - password_reset_token_timeout_hours = $65, \ - enrollment_session_timeout_minutes = $66, \ - password_reset_session_timeout_minutes = $67 \ + secret_key = $58, \ + webauthn_rp_id = $59, \ + disable_stats_purge = $60, \ + stats_purge_frequency_hours = $61, \ + stats_purge_threshold_days = $62, \ + enrollment_token_timeout_hours = $63, \ + password_reset_token_timeout_hours = $64, \ + enrollment_session_timeout_minutes = $65, \ + password_reset_session_timeout_minutes = $66 \ WHERE id = 1", self.openid_enabled, self.wireguard_enabled, @@ -490,7 +488,6 @@ impl Settings { self.mfa_code_timeout_seconds, self.public_proxy_url, self.default_admin_id, - self.auth_cookie_timeout_days, self.secret_key, self.webauthn_rp_id, self.disable_stats_purge, @@ -605,11 +602,6 @@ impl Settings { Duration::from_secs(self.authentication_period_days as u64 * 24 * 3600) } - #[must_use] - pub fn auth_cookie_timeout(&self) -> Duration { - Duration::from_secs(self.auth_cookie_timeout_days as u64 * 24 * 3600) - } - #[must_use] pub fn stats_purge_frequency(&self) -> Duration { Duration::from_secs(self.stats_purge_frequency_hours as u64 * 3600) @@ -661,9 +653,6 @@ impl Settings { let hour = minute * 60; let day = hour * 24; - if let Some(auth_cookie_timeout) = config.auth_cookie_timeout { - self.auth_cookie_timeout_days = (auth_cookie_timeout.as_secs() / day) as i32; - } if let Some(secret_key) = &config.secret_key { let secret_key = secret_key.expose_secret(); if let Err(err) = Settings::validate_secret_key(secret_key) { @@ -890,9 +879,6 @@ mod test { }; let mut config = DefGuardConfig::new_test_config(); - config.auth_cookie_timeout = Some(Duration::from(std::time::Duration::from_secs( - 3 * 24 * 3600, - ))); config.secret_key = Some(SecretString::from("a".repeat(64))); config.webauthn_rp_id = Some("rp-from-config".into()); config.enrollment_url = Some(Url::parse("https://proxy.example.com").unwrap()); @@ -917,7 +903,6 @@ mod test { settings.apply_from_config(&config); - assert_eq!(settings.auth_cookie_timeout_days, 3); assert_eq!( settings.secret_key.as_deref(), Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") diff --git a/migrations/20260227091211_[2.0.0]_settings_in_db.down.sql b/migrations/20260227091211_[2.0.0]_settings_in_db.down.sql index c74dfd7d0d..81a6d9b726 100644 --- a/migrations/20260227091211_[2.0.0]_settings_in_db.down.sql +++ b/migrations/20260227091211_[2.0.0]_settings_in_db.down.sql @@ -1,5 +1,4 @@ ALTER TABLE settings - DROP COLUMN auth_cookie_timeout_days, DROP COLUMN secret_key, DROP COLUMN openid_signing_key, DROP COLUMN webauthn_rp_id, diff --git a/migrations/20260227091211_[2.0.0]_settings_in_db.up.sql b/migrations/20260227091211_[2.0.0]_settings_in_db.up.sql index 64858bd93f..1f51954c46 100644 --- a/migrations/20260227091211_[2.0.0]_settings_in_db.up.sql +++ b/migrations/20260227091211_[2.0.0]_settings_in_db.up.sql @@ -1,5 +1,4 @@ ALTER TABLE settings - ADD COLUMN auth_cookie_timeout_days int4 NOT NULL DEFAULT 7, ADD COLUMN secret_key text, ADD COLUMN openid_signing_key text, ADD COLUMN webauthn_rp_id text, diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index e66da76ddd..d7f3e95e79 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -8,7 +8,6 @@ "settings_instance_label_name": "Instance name", "settings_instance_label_public_proxy_url": "Public Edge Component URL", "settings_instance_label_session_duration": "Session duration", - "settings_instance_label_auth_cookie_timeout_days": "Auth cookie timeout (days)", "settings_instance_session_duration_1": "1 day", "settings_instance_session_duration_2": "2 days", "settings_instance_session_duration_3": "3 days", diff --git a/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx b/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx index 84e40fb1a1..d25ccf3ac5 100644 --- a/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx +++ b/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx @@ -11,11 +11,6 @@ import { Page } from '../../../shared/components/Page/Page'; import { SettingsCard } from '../../../shared/components/SettingsCard/SettingsCard'; import { SettingsHeader } from '../../../shared/components/SettingsHeader/SettingsHeader'; import { SettingsLayout } from '../../../shared/components/SettingsLayout/SettingsLayout'; -import { - createNumericSelectOptions, - formatDaySelectLabel, - withNumericFallbackOption, -} from '../../../shared/const/numericSelectOptions'; import { Button } from '../../../shared/defguard-ui/components/Button/Button'; import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { Snackbar } from '../../../shared/defguard-ui/providers/snackbar/snackbar'; @@ -73,7 +68,6 @@ const formSchema = z.object({ }), ) .max(64, m.form_error_max_len({ length: 64 })), - auth_cookie_timeout_days: z.number(m.form_error_required()).int().min(1), public_proxy_url: z .url(m.initial_setup_general_config_error_public_proxy_url_invalid()) .min(1, m.initial_setup_general_config_error_public_proxy_url_required()), @@ -104,11 +98,6 @@ const sessionDurationOptions = [ }, ]; -const authCookieTimeoutBaseOptions = createNumericSelectOptions( - [1, 2, 3, 7, 10, 14, 30], - formatDaySelectLabel, -); - const Content = ({ settings }: { settings: Settings }) => { const { mutateAsync } = useMutation({ mutationFn: api.settings.patchSettings, @@ -124,30 +113,18 @@ const Content = ({ settings }: { settings: Settings }) => { }); const defaultValues = useMemo( - (): FormFields => ({ - instance_name: settings.instance_name ?? '', - auth_cookie_timeout_days: settings.auth_cookie_timeout_days ?? 7, - public_proxy_url: settings.public_proxy_url ?? '', - authentication_period_days: settings.authentication_period_days ?? 7, - }), + (): FormFields => ({ + instance_name: settings.instance_name ?? '', + public_proxy_url: settings.public_proxy_url ?? '', + authentication_period_days: settings.authentication_period_days ?? 7, + }), [ settings.instance_name, settings.public_proxy_url, settings.authentication_period_days, - settings.auth_cookie_timeout_days, ], ); - const authCookieTimeoutOptions = useMemo( - () => - withNumericFallbackOption( - authCookieTimeoutBaseOptions, - defaultValues.auth_cookie_timeout_days, - formatDaySelectLabel, - ), - [defaultValues.auth_cookie_timeout_days], - ); - const form = useAppForm({ defaultValues, validationLogic: formChangeLogic, @@ -194,16 +171,6 @@ const Content = ({ settings }: { settings: Settings }) => { /> )} - - - {(field) => ( - - )} - ({ diff --git a/web/src/shared/api/types.ts b/web/src/shared/api/types.ts index f52540a573..691af31f1b 100644 --- a/web/src/shared/api/types.ts +++ b/web/src/shared/api/types.ts @@ -896,7 +896,6 @@ export interface SettingsGatewayNotifications { } export interface SettingsTimeoutsAndMaintenance { - auth_cookie_timeout_days: number; disable_stats_purge: boolean; stats_purge_frequency_hours: number; stats_purge_threshold_days: number; From 9308f715eddb8366de346650a76d3d71c3575c33 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Sat, 7 Mar 2026 15:56:06 +0100 Subject: [PATCH 3/9] tweak select options --- .../SettingsEnrollmentPage.tsx | 21 +++++++++++-------- .../SettingsInstancePage.tsx | 10 ++++----- .../SettingsVpnStatsPage.tsx | 15 ++++++++----- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx b/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx index e205b0bd60..15d4f417f0 100644 --- a/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx +++ b/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx @@ -12,9 +12,9 @@ import { SettingsCard } from '../../../shared/components/SettingsCard/SettingsCa import { SettingsHeader } from '../../../shared/components/SettingsHeader/SettingsHeader'; import { SettingsLayout } from '../../../shared/components/SettingsLayout/SettingsLayout'; import { - createNumericSelectOptions, formatHourSelectLabel, formatMinuteSelectLabel, + type NumericSelectOption, withNumericFallbackOption, } from '../../../shared/const/numericSelectOptions'; import { Button } from '../../../shared/defguard-ui/components/Button/Button'; @@ -70,15 +70,18 @@ const formSchema = z.object({ type FormFields = z.infer; -const enrollmentTokenTimeoutBaseOptions = createNumericSelectOptions( - [1, 2, 4, 8, 12, 24, 48, 72, 168], - formatHourSelectLabel, -); +const enrollmentTokenTimeoutBaseOptions: NumericSelectOption[] = [ + { key: 1, value: 1, label: '1 hour' }, + { key: 12, value: 12, label: '12 hours' }, + { key: 24, value: 24, label: '1 day' }, + { key: 168, value: 168, label: '1 week' }, +]; -const enrollmentSessionTimeoutBaseOptions = createNumericSelectOptions( - [5, 10, 15, 30, 45, 60], - formatMinuteSelectLabel, -); +const enrollmentSessionTimeoutBaseOptions: NumericSelectOption[] = [ + { key: 10, value: 10, label: '10 minutes' }, + { key: 30, value: 30, label: '30 minutes' }, + { key: 60, value: 60, label: '1 hour' }, +]; const Content = ({ settings }: { settings: Settings }) => { const { mutateAsync } = useMutation({ diff --git a/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx b/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx index d25ccf3ac5..be047c14bb 100644 --- a/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx +++ b/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx @@ -113,11 +113,11 @@ const Content = ({ settings }: { settings: Settings }) => { }); const defaultValues = useMemo( - (): FormFields => ({ - instance_name: settings.instance_name ?? '', - public_proxy_url: settings.public_proxy_url ?? '', - authentication_period_days: settings.authentication_period_days ?? 7, - }), + (): FormFields => ({ + instance_name: settings.instance_name ?? '', + public_proxy_url: settings.public_proxy_url ?? '', + authentication_period_days: settings.authentication_period_days ?? 7, + }), [ settings.instance_name, settings.public_proxy_url, diff --git a/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx b/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx index ad5a4b7bcd..0f899fe200 100644 --- a/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx +++ b/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx @@ -15,6 +15,7 @@ import { createNumericSelectOptions, formatDaySelectLabel, formatHourSelectLabel, + type NumericSelectOption, withNumericFallbackOption, } from '../../../shared/const/numericSelectOptions'; import { Button } from '../../../shared/defguard-ui/components/Button/Button'; @@ -69,13 +70,17 @@ const formSchema = z.object({ type FormFields = z.infer; -const statsPurgeFrequencyBaseOptions = createNumericSelectOptions( - [1, 6, 12, 24, 48, 72, 168], - formatHourSelectLabel, -); +const statsPurgeFrequencyBaseOptions: NumericSelectOption[] = [ + { key: 1, value: 1, label: '1h' }, + { key: 12, value: 12, label: '12h' }, + { key: 24, value: 24, label: '1 day' }, + { key: 48, value: 48, label: '2 days' }, + { key: 168, value: 168, label: '1 week' }, + { key: 720, value: 720, label: '1 month' }, +]; const statsPurgeThresholdBaseOptions = createNumericSelectOptions( - [1, 7, 14, 30, 60, 90, 180, 365], + [1, 7, 14, 30, 90], formatDaySelectLabel, ); From 545805666a402fd2d17a3cb50837fcfbbb917cfe Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 9 Mar 2026 07:41:45 +0100 Subject: [PATCH 4/9] user-friendly settings labels --- .../SettingsEnrollmentPage/SettingsEnrollmentPage.tsx | 8 ++++---- .../SettingsVpnStatsPage/SettingsVpnStatsPage.tsx | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx b/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx index 15d4f417f0..2a62c6973e 100644 --- a/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx +++ b/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx @@ -170,7 +170,7 @@ const Content = ({ settings }: { settings: Settings }) => { {(field) => ( )} @@ -180,7 +180,7 @@ const Content = ({ settings }: { settings: Settings }) => { {(field) => ( )} @@ -190,7 +190,7 @@ const Content = ({ settings }: { settings: Settings }) => { {(field) => ( )} @@ -200,7 +200,7 @@ const Content = ({ settings }: { settings: Settings }) => { {(field) => ( )} diff --git a/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx b/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx index 0f899fe200..d40a74651b 100644 --- a/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx +++ b/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx @@ -157,7 +157,7 @@ const Content = ({ settings }: { settings: Settings }) => { {(field) => ( )} @@ -167,7 +167,7 @@ const Content = ({ settings }: { settings: Settings }) => { {(field) => ( )} From d4d8200ee7f49163ce6d30dce5723d6175e68e43 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 9 Mar 2026 08:57:54 +0100 Subject: [PATCH 5/9] translations --- web/messages/en/settings.json | 33 +++++++-- .../SettingsClientPage/SettingsClientPage.tsx | 4 +- .../SettingsEnrollmentPage.tsx | 73 ++++++++++++------- .../tabs/SettingsGeneralTab.tsx | 33 +++++---- .../SettingsInstancePage.tsx | 29 ++------ .../SettingsVpnStatsPage.tsx | 61 ++++++++++------ 6 files changed, 137 insertions(+), 96 deletions(-) diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index d7f3e95e79..ab83c11a69 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -3,18 +3,37 @@ "settings_page_title": "Settings", "settings_breadcrumb_general": "General", "settings_breadcrumb_instance": "Instance settings", + "settings_breadcrumb_vpn_stats": "VPN stats", + "settings_breadcrumb_enrollment": "Enrollment", + "settings_breadcrumb_client_behavior": "Client behavior", "settings_instance_title": "Instance settings", "settings_instance_subtitle": "Here you can configure general instance parameters.", "settings_instance_label_name": "Instance name", "settings_instance_label_public_proxy_url": "Public Edge Component URL", "settings_instance_label_session_duration": "Session duration", - "settings_instance_session_duration_1": "1 day", - "settings_instance_session_duration_2": "2 days", - "settings_instance_session_duration_3": "3 days", - "settings_instance_session_duration_7": "7 days", - "settings_instance_session_duration_10": "10 days", - "settings_instance_session_duration_14": "14 days", - "settings_instance_session_duration_30": "30 days", + "settings_vpn_stats_title": "VPN stats", + "settings_vpn_stats_subtitle": "Configure statistics purge behavior for VPN data.", + "settings_vpn_stats_toggle_disable_title": "Disable stats purge", + "settings_vpn_stats_label_purge_frequency": "Stats purge frequency", + "settings_vpn_stats_label_purge_threshold": "Stats purge threshold", + "settings_enrollment_title": "Enrollment", + "settings_enrollment_subtitle": "Configure token and session timeouts for enrollment and password reset flows.", + "settings_enrollment_label_token_validity": "Enrollment token validity", + "settings_enrollment_label_password_reset_token_validity": "Password reset token validity", + "settings_enrollment_label_session_expires_in": "Enrollment session expires in", + "settings_enrollment_label_password_reset_session_expires_in": "Password reset session expires in", + "settings_general_section_instance_content": "Configure your instance name and branding settings. Add a logo to personalize the interface and make it easily recognizable to your users.", + "settings_general_section_client_behavior_content": "Manage how users interact with the Defguard client. Control device management permissions, configuration access, and traffic routing options.", + "settings_general_section_vpn_stats_content": "Configure VPN statistics cleanup behavior, including purge frequency and retention threshold.", + "settings_general_section_enrollment_content": "Set enrollment and password reset token and session timeout values.", + "settings_duration_one_day": "1 day", + "settings_duration_days": "{days} days", + "settings_duration_one_hour": "1 hour", + "settings_duration_hours": "{hours} hours", + "settings_duration_one_minute": "1 minute", + "settings_duration_minutes": "{minutes} minutes", + "settings_duration_one_week": "1 week", + "settings_duration_one_month": "1 month", "settings_activity_log_streaming_title": "Activity log streaming", "settings_activity_log_streaming_description": "Monitor and export real-time activity logs from your Defguard instance. Stream events to external systems for auditing, analytics, or security monitoring.", "settings_activity_log_streaming_no_upstreams": "You don't have any activity log upstreams.", diff --git a/web/src/pages/settings/SettingsClientPage/SettingsClientPage.tsx b/web/src/pages/settings/SettingsClientPage/SettingsClientPage.tsx index e45f80f13a..fffe590bfa 100644 --- a/web/src/pages/settings/SettingsClientPage/SettingsClientPage.tsx +++ b/web/src/pages/settings/SettingsClientPage/SettingsClientPage.tsx @@ -32,10 +32,10 @@ import { canUseBusinessFeature } from '../../../shared/utils/license'; const breadcrumbs = [ - General + {m.settings_breadcrumb_general()} , - Client behavior + {m.settings_breadcrumb_client_behavior()} , ]; diff --git a/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx b/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx index 2a62c6973e..ce6b237c7a 100644 --- a/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx +++ b/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx @@ -12,9 +12,7 @@ import { SettingsCard } from '../../../shared/components/SettingsCard/SettingsCa import { SettingsHeader } from '../../../shared/components/SettingsHeader/SettingsHeader'; import { SettingsLayout } from '../../../shared/components/SettingsLayout/SettingsLayout'; import { - formatHourSelectLabel, - formatMinuteSelectLabel, - type NumericSelectOption, + createNumericSelectOptions, withNumericFallbackOption, } from '../../../shared/const/numericSelectOptions'; import { Button } from '../../../shared/defguard-ui/components/Button/Button'; @@ -33,23 +31,23 @@ const breadcrumbs = [ }} key={0} > - General + {m.settings_breadcrumb_general()} , - Enrollment + {m.settings_breadcrumb_enrollment()} , ]; export const SettingsEnrollmentPage = () => { const { data: settings } = useQuery(getSettingsQueryOptions); return ( - + {isPresent(settings) && ( @@ -70,18 +68,39 @@ const formSchema = z.object({ type FormFields = z.infer; -const enrollmentTokenTimeoutBaseOptions: NumericSelectOption[] = [ - { key: 1, value: 1, label: '1 hour' }, - { key: 12, value: 12, label: '12 hours' }, - { key: 24, value: 24, label: '1 day' }, - { key: 168, value: 168, label: '1 week' }, -]; +const formatEnrollmentTokenTimeoutLabel = (value: number) => { + switch (value) { + case 24: + return m.settings_duration_one_day(); + case 168: + return m.settings_duration_one_week(); + case 1: + return m.settings_duration_one_hour(); + default: + return m.settings_duration_hours({ hours: value }); + } +}; -const enrollmentSessionTimeoutBaseOptions: NumericSelectOption[] = [ - { key: 10, value: 10, label: '10 minutes' }, - { key: 30, value: 30, label: '30 minutes' }, - { key: 60, value: 60, label: '1 hour' }, -]; +const enrollmentTokenTimeoutBaseOptions = createNumericSelectOptions( + [1, 12, 24, 168], + formatEnrollmentTokenTimeoutLabel, +); + +const formatEnrollmentSessionTimeoutLabel = (value: number) => { + switch (value) { + case 60: + return m.settings_duration_one_hour(); + case 1: + return m.settings_duration_one_minute(); + default: + return m.settings_duration_minutes({ minutes: value }); + } +}; + +const enrollmentSessionTimeoutBaseOptions = createNumericSelectOptions( + [10, 30, 60], + formatEnrollmentSessionTimeoutLabel, +); const Content = ({ settings }: { settings: Settings }) => { const { mutateAsync } = useMutation({ @@ -109,7 +128,7 @@ const Content = ({ settings }: { settings: Settings }) => { withNumericFallbackOption( enrollmentTokenTimeoutBaseOptions, defaultValues.enrollment_token_timeout_hours, - formatHourSelectLabel, + formatEnrollmentTokenTimeoutLabel, ), [defaultValues.enrollment_token_timeout_hours], ); @@ -119,7 +138,7 @@ const Content = ({ settings }: { settings: Settings }) => { withNumericFallbackOption( enrollmentTokenTimeoutBaseOptions, defaultValues.password_reset_token_timeout_hours, - formatHourSelectLabel, + formatEnrollmentTokenTimeoutLabel, ), [defaultValues.password_reset_token_timeout_hours], ); @@ -129,7 +148,7 @@ const Content = ({ settings }: { settings: Settings }) => { withNumericFallbackOption( enrollmentSessionTimeoutBaseOptions, defaultValues.enrollment_session_timeout_minutes, - formatMinuteSelectLabel, + formatEnrollmentSessionTimeoutLabel, ), [defaultValues.enrollment_session_timeout_minutes], ); @@ -139,7 +158,7 @@ const Content = ({ settings }: { settings: Settings }) => { withNumericFallbackOption( enrollmentSessionTimeoutBaseOptions, defaultValues.password_reset_session_timeout_minutes, - formatMinuteSelectLabel, + formatEnrollmentSessionTimeoutLabel, ), [defaultValues.password_reset_session_timeout_minutes], ); @@ -170,7 +189,7 @@ const Content = ({ settings }: { settings: Settings }) => { {(field) => ( )} @@ -180,7 +199,7 @@ const Content = ({ settings }: { settings: Settings }) => { {(field) => ( )} @@ -190,7 +209,7 @@ const Content = ({ settings }: { settings: Settings }) => { {(field) => ( )} @@ -200,7 +219,7 @@ const Content = ({ settings }: { settings: Settings }) => { {(field) => ( )} diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsGeneralTab.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsGeneralTab.tsx index df69eff1f2..000e5a8111 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsGeneralTab.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsGeneralTab.tsx @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import { Link, useNavigate } from '@tanstack/react-router'; +import { m } from '../../../../paraglide/messages'; import { businessBadgeProps } from '../../../../shared/components/badges/BusinessBadge'; import { SettingsLayout } from '../../../../shared/components/SettingsLayout/SettingsLayout'; import { SectionSelect } from '../../../../shared/defguard-ui/components/SectionSelect/SectionSelect'; @@ -20,8 +21,8 @@ export const SettingsGeneralTab = () => { @@ -39,31 +40,31 @@ export const SettingsGeneralTab = () => { - { - navigate({ to: '/settings/client' }); - }} - /> - + + { + navigate({ to: '/settings/client' }); + }} + /> ); }; diff --git a/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx b/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx index be047c14bb..3b46b5a387 100644 --- a/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx +++ b/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx @@ -11,6 +11,7 @@ import { Page } from '../../../shared/components/Page/Page'; import { SettingsCard } from '../../../shared/components/SettingsCard/SettingsCard'; import { SettingsHeader } from '../../../shared/components/SettingsHeader/SettingsHeader'; import { SettingsLayout } from '../../../shared/components/SettingsLayout/SettingsLayout'; +import { createNumericSelectOptions } from '../../../shared/const/numericSelectOptions'; import { Button } from '../../../shared/defguard-ui/components/Button/Button'; import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { Snackbar } from '../../../shared/defguard-ui/providers/snackbar/snackbar'; @@ -76,27 +77,13 @@ const formSchema = z.object({ type FormFields = z.infer; -const sessionDurationOptions = [ - { key: 1, value: 1, label: m.settings_instance_session_duration_1() }, - { key: 2, value: 2, label: m.settings_instance_session_duration_2() }, - { key: 3, value: 3, label: m.settings_instance_session_duration_3() }, - { key: 7, value: 7, label: m.settings_instance_session_duration_7() }, - { - key: 10, - value: 10, - label: m.settings_instance_session_duration_10(), - }, - { - key: 14, - value: 14, - label: m.settings_instance_session_duration_14(), - }, - { - key: 30, - value: 30, - label: m.settings_instance_session_duration_30(), - }, -]; +const sessionDurationOptions = createNumericSelectOptions( + [1, 2, 3, 7, 10, 14, 30], + (value) => + value === 1 + ? m.settings_duration_one_day() + : m.settings_duration_days({ days: value }), +); const Content = ({ settings }: { settings: Settings }) => { const { mutateAsync } = useMutation({ diff --git a/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx b/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx index d40a74651b..6dd982b0c8 100644 --- a/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx +++ b/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx @@ -13,9 +13,6 @@ import { SettingsHeader } from '../../../shared/components/SettingsHeader/Settin import { SettingsLayout } from '../../../shared/components/SettingsLayout/SettingsLayout'; import { createNumericSelectOptions, - formatDaySelectLabel, - formatHourSelectLabel, - type NumericSelectOption, withNumericFallbackOption, } from '../../../shared/const/numericSelectOptions'; import { Button } from '../../../shared/defguard-ui/components/Button/Button'; @@ -34,23 +31,23 @@ const breadcrumbs = [ }} key={0} > - General + {m.settings_breadcrumb_general()} , - VPN stats + {m.settings_breadcrumb_vpn_stats()} , ]; export const SettingsVpnStatsPage = () => { const { data: settings } = useQuery(getSettingsQueryOptions); return ( - + {isPresent(settings) && ( @@ -70,18 +67,34 @@ const formSchema = z.object({ type FormFields = z.infer; -const statsPurgeFrequencyBaseOptions: NumericSelectOption[] = [ - { key: 1, value: 1, label: '1h' }, - { key: 12, value: 12, label: '12h' }, - { key: 24, value: 24, label: '1 day' }, - { key: 48, value: 48, label: '2 days' }, - { key: 168, value: 168, label: '1 week' }, - { key: 720, value: 720, label: '1 month' }, -]; +const formatStatsPurgeFrequencyLabel = (value: number) => { + switch (value) { + case 24: + return m.settings_duration_one_day(); + case 48: + return m.settings_duration_days({ days: 2 }); + case 168: + return m.settings_duration_one_week(); + case 720: + return m.settings_duration_one_month(); + case 1: + return m.settings_duration_one_hour(); + default: + return m.settings_duration_hours({ hours: value }); + } +}; + +const statsPurgeFrequencyBaseOptions = createNumericSelectOptions( + [1, 12, 24, 48, 168, 720], + formatStatsPurgeFrequencyLabel, +); const statsPurgeThresholdBaseOptions = createNumericSelectOptions( [1, 7, 14, 30, 90], - formatDaySelectLabel, + (value) => + value === 1 + ? m.settings_duration_one_day() + : m.settings_duration_days({ days: value }), ); const Content = ({ settings }: { settings: Settings }) => { @@ -106,7 +119,7 @@ const Content = ({ settings }: { settings: Settings }) => { withNumericFallbackOption( statsPurgeFrequencyBaseOptions, defaultValues.stats_purge_frequency_hours, - formatHourSelectLabel, + formatStatsPurgeFrequencyLabel, ), [defaultValues.stats_purge_frequency_hours], ); @@ -116,7 +129,10 @@ const Content = ({ settings }: { settings: Settings }) => { withNumericFallbackOption( statsPurgeThresholdBaseOptions, defaultValues.stats_purge_threshold_days, - formatDaySelectLabel, + (value) => + value === 1 + ? m.settings_duration_one_day() + : m.settings_duration_days({ days: value }), ), [defaultValues.stats_purge_threshold_days], ); @@ -147,8 +163,7 @@ const Content = ({ settings }: { settings: Settings }) => { {(field) => ( )} @@ -157,7 +172,7 @@ const Content = ({ settings }: { settings: Settings }) => { {(field) => ( )} @@ -167,7 +182,7 @@ const Content = ({ settings }: { settings: Settings }) => { {(field) => ( )} From 97fe79ef86dee0cba47f46dd0888bc30b908ab3d Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 9 Mar 2026 09:07:46 +0100 Subject: [PATCH 6/9] cargo fmt --- crates/defguard_common/src/config.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/defguard_common/src/config.rs b/crates/defguard_common/src/config.rs index 2ad3a5b430..fdd1d779c2 100644 --- a/crates/defguard_common/src/config.rs +++ b/crates/defguard_common/src/config.rs @@ -3,13 +3,13 @@ use std::{net::IpAddr, sync::OnceLock}; use clap::{Args, Parser, Subcommand}; use humantime::Duration; use ipnetwork::IpNetwork; -use openidconnect::{core::CoreRsaPrivateSigningKey, JsonWebKeyId}; +use openidconnect::{JsonWebKeyId, core::CoreRsaPrivateSigningKey}; use reqwest::Url; use rsa::{ + RsaPrivateKey, pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey}, pkcs8::{DecodePrivateKey, LineEnding}, traits::PublicKeyParts, - RsaPrivateKey, }; use secrecy::{ExposeSecret, SecretString}; use serde::Serialize; From 5e54ddf8aeb748a60700d82fb2cf93e2fe816026 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 9 Mar 2026 09:19:09 +0100 Subject: [PATCH 7/9] remove unused overviewPeriodOptions, move numericSelectOptions to utils --- .../SettingsEnrollmentPage.tsx | 8 ++++---- .../SettingsInstancePage.tsx | 20 +++++++++++++++++-- .../SettingsVpnStatsPage.tsx | 8 ++++---- web/src/shared/const/overviewPeriodOptions.ts | 11 ---------- .../{const => utils}/numericSelectOptions.ts | 0 5 files changed, 26 insertions(+), 21 deletions(-) delete mode 100644 web/src/shared/const/overviewPeriodOptions.ts rename web/src/shared/{const => utils}/numericSelectOptions.ts (100%) diff --git a/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx b/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx index ce6b237c7a..c9e2a6d520 100644 --- a/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx +++ b/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx @@ -11,10 +11,6 @@ import { Page } from '../../../shared/components/Page/Page'; import { SettingsCard } from '../../../shared/components/SettingsCard/SettingsCard'; import { SettingsHeader } from '../../../shared/components/SettingsHeader/SettingsHeader'; import { SettingsLayout } from '../../../shared/components/SettingsLayout/SettingsLayout'; -import { - createNumericSelectOptions, - withNumericFallbackOption, -} from '../../../shared/const/numericSelectOptions'; import { Button } from '../../../shared/defguard-ui/components/Button/Button'; import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { ThemeSpacing } from '../../../shared/defguard-ui/types'; @@ -22,6 +18,10 @@ import { isPresent } from '../../../shared/defguard-ui/utils/isPresent'; import { useAppForm } from '../../../shared/form'; import { formChangeLogic } from '../../../shared/formLogic'; import { getSettingsQueryOptions } from '../../../shared/query'; +import { + createNumericSelectOptions, + withNumericFallbackOption, +} from '../../../shared/utils/numericSelectOptions'; const breadcrumbs = [ + value === 1 ? m.settings_duration_one_day() : m.settings_duration_days({ days: value }); + const Content = ({ settings }: { settings: Settings }) => { const { mutateAsync } = useMutation({ mutationFn: api.settings.patchSettings, @@ -112,6 +118,16 @@ const Content = ({ settings }: { settings: Settings }) => { ], ); + const sessionDurationSelectOptions = useMemo( + () => + withNumericFallbackOption( + sessionDurationOptions, + defaultValues.authentication_period_days, + formatSessionDurationLabel, + ), + [defaultValues.authentication_period_days], + ); + const form = useAppForm({ defaultValues, validationLogic: formChangeLogic, @@ -154,7 +170,7 @@ const Content = ({ settings }: { settings: Settings }) => { )} diff --git a/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx b/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx index 6dd982b0c8..3eb969ece8 100644 --- a/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx +++ b/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx @@ -11,10 +11,6 @@ import { Page } from '../../../shared/components/Page/Page'; import { SettingsCard } from '../../../shared/components/SettingsCard/SettingsCard'; import { SettingsHeader } from '../../../shared/components/SettingsHeader/SettingsHeader'; import { SettingsLayout } from '../../../shared/components/SettingsLayout/SettingsLayout'; -import { - createNumericSelectOptions, - withNumericFallbackOption, -} from '../../../shared/const/numericSelectOptions'; import { Button } from '../../../shared/defguard-ui/components/Button/Button'; import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { ThemeSpacing } from '../../../shared/defguard-ui/types'; @@ -22,6 +18,10 @@ import { isPresent } from '../../../shared/defguard-ui/utils/isPresent'; import { useAppForm } from '../../../shared/form'; import { formChangeLogic } from '../../../shared/formLogic'; import { getSettingsQueryOptions } from '../../../shared/query'; +import { + createNumericSelectOptions, + withNumericFallbackOption, +} from '../../../shared/utils/numericSelectOptions'; const breadcrumbs = [ [] = [ - { key: 1, label: '1h period', value: 1 }, - { key: 2, label: '2h period', value: 2 }, - { key: 6, label: '6h period', value: 6 }, - { key: 8, label: '8h period', value: 8 }, - { key: 12, label: '12h period', value: 12 }, - { key: 16, label: '16h period', value: 16 }, - { key: 24, label: '24h period', value: 24 }, -]; diff --git a/web/src/shared/const/numericSelectOptions.ts b/web/src/shared/utils/numericSelectOptions.ts similarity index 100% rename from web/src/shared/const/numericSelectOptions.ts rename to web/src/shared/utils/numericSelectOptions.ts From 1c067e86e22b1dba093ef79012cc2ebf6947ea97 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 9 Mar 2026 09:57:00 +0100 Subject: [PATCH 8/9] simplify createNumericSelectOptions --- .../SettingsEnrollmentPage.tsx | 51 ++++++------------- .../SettingsInstancePage.tsx | 21 ++++---- .../SettingsVpnStatsPage.tsx | 50 +++++++----------- web/src/shared/utils/numericSelectOptions.ts | 34 ++++++------- 4 files changed, 60 insertions(+), 96 deletions(-) diff --git a/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx b/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx index c9e2a6d520..ce3c04c209 100644 --- a/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx +++ b/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx @@ -68,39 +68,18 @@ const formSchema = z.object({ type FormFields = z.infer; -const formatEnrollmentTokenTimeoutLabel = (value: number) => { - switch (value) { - case 24: - return m.settings_duration_one_day(); - case 168: - return m.settings_duration_one_week(); - case 1: - return m.settings_duration_one_hour(); - default: - return m.settings_duration_hours({ hours: value }); - } -}; - -const enrollmentTokenTimeoutBaseOptions = createNumericSelectOptions( - [1, 12, 24, 168], - formatEnrollmentTokenTimeoutLabel, -); - -const formatEnrollmentSessionTimeoutLabel = (value: number) => { - switch (value) { - case 60: - return m.settings_duration_one_hour(); - case 1: - return m.settings_duration_one_minute(); - default: - return m.settings_duration_minutes({ minutes: value }); - } -}; +const enrollmentTokenTimeoutBaseOptions = createNumericSelectOptions({ + 1: m.settings_duration_one_hour(), + 12: m.settings_duration_hours({ hours: 12 }), + 24: m.settings_duration_one_day(), + 168: m.settings_duration_one_week(), +}); -const enrollmentSessionTimeoutBaseOptions = createNumericSelectOptions( - [10, 30, 60], - formatEnrollmentSessionTimeoutLabel, -); +const enrollmentSessionTimeoutBaseOptions = createNumericSelectOptions({ + 10: m.settings_duration_minutes({ minutes: 10 }), + 30: m.settings_duration_minutes({ minutes: 30 }), + 60: m.settings_duration_one_hour(), +}); const Content = ({ settings }: { settings: Settings }) => { const { mutateAsync } = useMutation({ @@ -128,7 +107,7 @@ const Content = ({ settings }: { settings: Settings }) => { withNumericFallbackOption( enrollmentTokenTimeoutBaseOptions, defaultValues.enrollment_token_timeout_hours, - formatEnrollmentTokenTimeoutLabel, + 'hours', ), [defaultValues.enrollment_token_timeout_hours], ); @@ -138,7 +117,7 @@ const Content = ({ settings }: { settings: Settings }) => { withNumericFallbackOption( enrollmentTokenTimeoutBaseOptions, defaultValues.password_reset_token_timeout_hours, - formatEnrollmentTokenTimeoutLabel, + 'hours', ), [defaultValues.password_reset_token_timeout_hours], ); @@ -148,7 +127,7 @@ const Content = ({ settings }: { settings: Settings }) => { withNumericFallbackOption( enrollmentSessionTimeoutBaseOptions, defaultValues.enrollment_session_timeout_minutes, - formatEnrollmentSessionTimeoutLabel, + 'minutes', ), [defaultValues.enrollment_session_timeout_minutes], ); @@ -158,7 +137,7 @@ const Content = ({ settings }: { settings: Settings }) => { withNumericFallbackOption( enrollmentSessionTimeoutBaseOptions, defaultValues.password_reset_session_timeout_minutes, - formatEnrollmentSessionTimeoutLabel, + 'minutes', ), [defaultValues.password_reset_session_timeout_minutes], ); diff --git a/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx b/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx index 48de94e9a4..781370c35f 100644 --- a/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx +++ b/web/src/pages/settings/SettingsInstancePage/SettingsInstancePage.tsx @@ -80,16 +80,17 @@ const formSchema = z.object({ type FormFields = z.infer; -const sessionDurationOptions = createNumericSelectOptions( - [1, 2, 3, 7, 10, 14, 30], - (value) => - value === 1 - ? m.settings_duration_one_day() - : m.settings_duration_days({ days: value }), -); +const sessionDurationOptions = createNumericSelectOptions({ + 1: m.settings_duration_one_day(), + 2: m.settings_duration_days({ days: 2 }), + 3: m.settings_duration_days({ days: 3 }), + 7: m.settings_duration_days({ days: 7 }), + 10: m.settings_duration_days({ days: 10 }), + 14: m.settings_duration_days({ days: 14 }), + 30: m.settings_duration_days({ days: 30 }), +}); -const formatSessionDurationLabel = (value: number) => - value === 1 ? m.settings_duration_one_day() : m.settings_duration_days({ days: value }); +const sessionDurationFallbackUnit = 'days'; const Content = ({ settings }: { settings: Settings }) => { const { mutateAsync } = useMutation({ @@ -123,7 +124,7 @@ const Content = ({ settings }: { settings: Settings }) => { withNumericFallbackOption( sessionDurationOptions, defaultValues.authentication_period_days, - formatSessionDurationLabel, + sessionDurationFallbackUnit, ), [defaultValues.authentication_period_days], ); diff --git a/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx b/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx index 3eb969ece8..2f17b6f308 100644 --- a/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx +++ b/web/src/pages/settings/SettingsVpnStatsPage/SettingsVpnStatsPage.tsx @@ -67,35 +67,22 @@ const formSchema = z.object({ type FormFields = z.infer; -const formatStatsPurgeFrequencyLabel = (value: number) => { - switch (value) { - case 24: - return m.settings_duration_one_day(); - case 48: - return m.settings_duration_days({ days: 2 }); - case 168: - return m.settings_duration_one_week(); - case 720: - return m.settings_duration_one_month(); - case 1: - return m.settings_duration_one_hour(); - default: - return m.settings_duration_hours({ hours: value }); - } -}; - -const statsPurgeFrequencyBaseOptions = createNumericSelectOptions( - [1, 12, 24, 48, 168, 720], - formatStatsPurgeFrequencyLabel, -); +const statsPurgeFrequencyBaseOptions = createNumericSelectOptions({ + 1: m.settings_duration_one_hour(), + 12: m.settings_duration_hours({ hours: 12 }), + 24: m.settings_duration_one_day(), + 48: m.settings_duration_days({ days: 2 }), + 168: m.settings_duration_one_week(), + 720: m.settings_duration_one_month(), +}); -const statsPurgeThresholdBaseOptions = createNumericSelectOptions( - [1, 7, 14, 30, 90], - (value) => - value === 1 - ? m.settings_duration_one_day() - : m.settings_duration_days({ days: value }), -); +const statsPurgeThresholdBaseOptions = createNumericSelectOptions({ + 1: m.settings_duration_one_day(), + 7: m.settings_duration_days({ days: 7 }), + 14: m.settings_duration_days({ days: 14 }), + 30: m.settings_duration_days({ days: 30 }), + 90: m.settings_duration_days({ days: 90 }), +}); const Content = ({ settings }: { settings: Settings }) => { const { mutateAsync } = useMutation({ @@ -119,7 +106,7 @@ const Content = ({ settings }: { settings: Settings }) => { withNumericFallbackOption( statsPurgeFrequencyBaseOptions, defaultValues.stats_purge_frequency_hours, - formatStatsPurgeFrequencyLabel, + 'hours', ), [defaultValues.stats_purge_frequency_hours], ); @@ -129,10 +116,7 @@ const Content = ({ settings }: { settings: Settings }) => { withNumericFallbackOption( statsPurgeThresholdBaseOptions, defaultValues.stats_purge_threshold_days, - (value) => - value === 1 - ? m.settings_duration_one_day() - : m.settings_duration_days({ days: value }), + 'days', ), [defaultValues.stats_purge_threshold_days], ); diff --git a/web/src/shared/utils/numericSelectOptions.ts b/web/src/shared/utils/numericSelectOptions.ts index e46bf138e0..d58e0ed063 100644 --- a/web/src/shared/utils/numericSelectOptions.ts +++ b/web/src/shared/utils/numericSelectOptions.ts @@ -2,39 +2,39 @@ import type { SelectOption } from '../defguard-ui/components/Select/types'; export type NumericSelectOption = SelectOption; -export const formatDaySelectLabel = (value: number) => - `${value} ${value === 1 ? 'day' : 'days'}`; - -export const formatHourSelectLabel = (value: number) => - `${value} ${value === 1 ? 'hour' : 'hours'}`; - -export const formatMinuteSelectLabel = (value: number) => - `${value} ${value === 1 ? 'minute' : 'minutes'}`; +export type NumericSelectOptionMap = Readonly>; export const createNumericSelectOptions = ( - values: readonly number[], - formatLabel: (value: number) => string, + optionMap: NumericSelectOptionMap, ): NumericSelectOption[] => - values.map((value) => ({ - key: value, - label: formatLabel(value), - value, - })); + Object.entries(optionMap) + .map(([value, label]) => { + const numericValue = Number(value); + + return { + key: numericValue, + label, + value: numericValue, + }; + }) + .sort((a, b) => a.value - b.value); export const withNumericFallbackOption = ( options: readonly NumericSelectOption[], value: number, - formatLabel: (value: number) => string, + unit: string, ): NumericSelectOption[] => { if (options.some((option) => option.value === value)) { return [...options]; } + const label = `${value} ${unit}`; + return [ ...options, { key: value, - label: formatLabel(value), + label, value, }, ].sort((a, b) => a.value - b.value); From ada9024f456984eb862db54153168699e68c56a3 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 9 Mar 2026 09:59:20 +0100 Subject: [PATCH 9/9] docstrings --- web/src/shared/utils/numericSelectOptions.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/src/shared/utils/numericSelectOptions.ts b/web/src/shared/utils/numericSelectOptions.ts index d58e0ed063..329c1f428a 100644 --- a/web/src/shared/utils/numericSelectOptions.ts +++ b/web/src/shared/utils/numericSelectOptions.ts @@ -4,6 +4,7 @@ export type NumericSelectOption = SelectOption; export type NumericSelectOptionMap = Readonly>; +/** Builds sorted numeric select options from a value-to-label map. */ export const createNumericSelectOptions = ( optionMap: NumericSelectOptionMap, ): NumericSelectOption[] => @@ -19,6 +20,7 @@ export const createNumericSelectOptions = ( }) .sort((a, b) => a.value - b.value); +/** Appends the current numeric value when it is missing from predefined options. */ export const withNumericFallbackOption = ( options: readonly NumericSelectOption[], value: number,