From 95e5d658e7ab86899509ae8b67a5a179cb9590f6 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 23 Mar 2026 09:59:15 +0100 Subject: [PATCH 01/33] enrollment settings model and migration --- ...3a83fe7ed47aecf0a79e48d508d87464a38e.json} | 186 +++++++++++++----- ...1cfe82dcdd65a86bf82629533e380594fd6a0.json | 162 +++++++++++++++ ...2d41284e6629883f647462029872c9fad2bfb.json | 110 ----------- .../defguard_common/src/db/models/settings.rs | 172 +++++++++++----- ...83929_[2.0.0]_enrollment_settings.down.sql | 12 ++ ...3083929_[2.0.0]_enrollment_settings.up.sql | 21 ++ 6 files changed, 459 insertions(+), 204 deletions(-) rename .sqlx/{query-dab137a626956fe0a0f2fbfc17c45075372f6963ff73760a53f843eaf5ebed4a.json => query-2c604cff80e910a3b8e95f461d9e3a83fe7ed47aecf0a79e48d508d87464a38e.json} (70%) create mode 100644 .sqlx/query-6649a414fbececa2324a151e9ce1cfe82dcdd65a86bf82629533e380594fd6a0.json delete mode 100644 .sqlx/query-89698ecaa251e056770bb90827d2d41284e6629883f647462029872c9fad2bfb.json create mode 100644 migrations/20260323083929_[2.0.0]_enrollment_settings.down.sql create mode 100644 migrations/20260323083929_[2.0.0]_enrollment_settings.up.sql diff --git a/.sqlx/query-dab137a626956fe0a0f2fbfc17c45075372f6963ff73760a53f843eaf5ebed4a.json b/.sqlx/query-2c604cff80e910a3b8e95f461d9e3a83fe7ed47aecf0a79e48d508d87464a38e.json similarity index 70% rename from .sqlx/query-dab137a626956fe0a0f2fbfc17c45075372f6963ff73760a53f843eaf5ebed4a.json rename to .sqlx/query-2c604cff80e910a3b8e95f461d9e3a83fe7ed47aecf0a79e48d508d87464a38e.json index ce20619c2c..e4c4a6a04c 100644 --- a/.sqlx/query-dab137a626956fe0a0f2fbfc17c45075372f6963ff73760a53f843eaf5ebed4a.json +++ b/.sqlx/query-2c604cff80e910a3b8e95f461d9e3a83fe7ed47aecf0a79e48d508d87464a38e.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, secret_key, enable_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, enrollment_admin_email_mode \"enrollment_admin_email_mode: EnrollmentAdminEmailMode\", enrollment_admin_custom_email, enrollment_show_reset_password, enrollment_show_welcome_message, enrollment_send_welcome_email, enrollment_windows_release_channel \"enrollment_windows_release_channel: EnrollmentReleaseChannel\", enrollment_linux_release_channel \"enrollment_linux_release_channel: EnrollmentReleaseChannel\", enrollment_macos_release_channel \"enrollment_macos_release_channel: EnrollmentReleaseChannel\", 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, enable_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": [ { @@ -111,101 +111,185 @@ }, { "ordinal": 19, + "name": "enrollment_admin_email_mode: EnrollmentAdminEmailMode", + "type_info": { + "Custom": { + "name": "enrollment_admin_email_mode", + "kind": { + "Enum": [ + "initiating_admin", + "hidden", + "custom_email" + ] + } + } + } + }, + { + "ordinal": 20, + "name": "enrollment_admin_custom_email", + "type_info": "Text" + }, + { + "ordinal": 21, + "name": "enrollment_show_reset_password", + "type_info": "Bool" + }, + { + "ordinal": 22, + "name": "enrollment_show_welcome_message", + "type_info": "Bool" + }, + { + "ordinal": 23, + "name": "enrollment_send_welcome_email", + "type_info": "Bool" + }, + { + "ordinal": 24, + "name": "enrollment_windows_release_channel: EnrollmentReleaseChannel", + "type_info": { + "Custom": { + "name": "enrollment_release_channel", + "kind": { + "Enum": [ + "stable", + "beta", + "alpha" + ] + } + } + } + }, + { + "ordinal": 25, + "name": "enrollment_linux_release_channel: EnrollmentReleaseChannel", + "type_info": { + "Custom": { + "name": "enrollment_release_channel", + "kind": { + "Enum": [ + "stable", + "beta", + "alpha" + ] + } + } + } + }, + { + "ordinal": 26, + "name": "enrollment_macos_release_channel: EnrollmentReleaseChannel", + "type_info": { + "Custom": { + "name": "enrollment_release_channel", + "kind": { + "Enum": [ + "stable", + "beta", + "alpha" + ] + } + } + } + }, + { + "ordinal": 27, "name": "uuid", "type_info": "Uuid" }, { - "ordinal": 20, + "ordinal": 28, "name": "ldap_url", "type_info": "Text" }, { - "ordinal": 21, + "ordinal": 29, "name": "ldap_bind_username", "type_info": "Text" }, { - "ordinal": 22, + "ordinal": 30, "name": "ldap_bind_password?: SecretStringWrapper", "type_info": "Text" }, { - "ordinal": 23, + "ordinal": 31, "name": "ldap_group_search_base", "type_info": "Text" }, { - "ordinal": 24, + "ordinal": 32, "name": "ldap_user_search_base", "type_info": "Text" }, { - "ordinal": 25, + "ordinal": 33, "name": "ldap_user_obj_class", "type_info": "Text" }, { - "ordinal": 26, + "ordinal": 34, "name": "ldap_group_obj_class", "type_info": "Text" }, { - "ordinal": 27, + "ordinal": 35, "name": "ldap_username_attr", "type_info": "Text" }, { - "ordinal": 28, + "ordinal": 36, "name": "ldap_groupname_attr", "type_info": "Text" }, { - "ordinal": 29, + "ordinal": 37, "name": "ldap_group_member_attr", "type_info": "Text" }, { - "ordinal": 30, + "ordinal": 38, "name": "ldap_member_attr", "type_info": "Text" }, { - "ordinal": 31, + "ordinal": 39, "name": "openid_create_account", "type_info": "Bool" }, { - "ordinal": 32, + "ordinal": 40, "name": "license", "type_info": "Text" }, { - "ordinal": 33, + "ordinal": 41, "name": "gateway_disconnect_notifications_enabled", "type_info": "Bool" }, { - "ordinal": 34, + "ordinal": 42, "name": "ldap_use_starttls", "type_info": "Bool" }, { - "ordinal": 35, + "ordinal": 43, "name": "ldap_tls_verify_cert", "type_info": "Bool" }, { - "ordinal": 36, + "ordinal": 44, "name": "gateway_disconnect_notifications_inactivity_threshold", "type_info": "Int4" }, { - "ordinal": 37, + "ordinal": 45, "name": "gateway_disconnect_notifications_reconnect_notification_enabled", "type_info": "Bool" }, { - "ordinal": 38, + "ordinal": 46, "name": "ldap_sync_status: LdapSyncStatus", "type_info": { "Custom": { @@ -220,47 +304,47 @@ } }, { - "ordinal": 39, + "ordinal": 47, "name": "ldap_enabled", "type_info": "Bool" }, { - "ordinal": 40, + "ordinal": 48, "name": "ldap_sync_enabled", "type_info": "Bool" }, { - "ordinal": 41, + "ordinal": 49, "name": "ldap_is_authoritative", "type_info": "Bool" }, { - "ordinal": 42, + "ordinal": 50, "name": "ldap_sync_interval", "type_info": "Int4" }, { - "ordinal": 43, + "ordinal": 51, "name": "ldap_user_auxiliary_obj_classes", "type_info": "TextArray" }, { - "ordinal": 44, + "ordinal": 52, "name": "ldap_uses_ad", "type_info": "Bool" }, { - "ordinal": 45, + "ordinal": 53, "name": "ldap_user_rdn_attr", "type_info": "Text" }, { - "ordinal": 46, + "ordinal": 54, "name": "ldap_sync_groups", "type_info": "TextArray" }, { - "ordinal": 47, + "ordinal": 55, "name": "openid_username_handling: OpenIdUsernameHandling", "type_info": { "Custom": { @@ -276,87 +360,87 @@ } }, { - "ordinal": 48, + "ordinal": 56, "name": "ca_key_der", "type_info": "Bytea" }, { - "ordinal": 49, + "ordinal": 57, "name": "ca_cert_der", "type_info": "Bytea" }, { - "ordinal": 50, + "ordinal": 58, "name": "ca_expiry", "type_info": "Timestamp" }, { - "ordinal": 51, + "ordinal": 59, "name": "defguard_url", "type_info": "Text" }, { - "ordinal": 52, + "ordinal": 60, "name": "default_admin_group_name", "type_info": "Text" }, { - "ordinal": 53, + "ordinal": 61, "name": "authentication_period_days", "type_info": "Int4" }, { - "ordinal": 54, + "ordinal": 62, "name": "mfa_code_timeout_seconds", "type_info": "Int4" }, { - "ordinal": 55, + "ordinal": 63, "name": "public_proxy_url", "type_info": "Text" }, { - "ordinal": 56, + "ordinal": 64, "name": "default_admin_id", "type_info": "Int8" }, { - "ordinal": 57, + "ordinal": 65, "name": "secret_key", "type_info": "Text" }, { - "ordinal": 58, + "ordinal": 66, "name": "enable_stats_purge", "type_info": "Bool" }, { - "ordinal": 59, + "ordinal": 67, "name": "stats_purge_frequency_hours", "type_info": "Int4" }, { - "ordinal": 60, + "ordinal": 68, "name": "stats_purge_threshold_days", "type_info": "Int4" }, { - "ordinal": 61, + "ordinal": 69, "name": "enrollment_token_timeout_hours", "type_info": "Int4" }, { - "ordinal": 62, + "ordinal": 70, "name": "password_reset_token_timeout_hours", "type_info": "Int4" }, { - "ordinal": 63, + "ordinal": 71, "name": "enrollment_session_timeout_minutes", "type_info": "Int4" }, { - "ordinal": 64, + "ordinal": 72, "name": "password_reset_session_timeout_minutes", "type_info": "Int4" } @@ -386,6 +470,14 @@ false, false, true, + false, + false, + false, + false, + false, + false, + false, + true, true, true, true, @@ -432,5 +524,5 @@ false ] }, - "hash": "dab137a626956fe0a0f2fbfc17c45075372f6963ff73760a53f843eaf5ebed4a" + "hash": "2c604cff80e910a3b8e95f461d9e3a83fe7ed47aecf0a79e48d508d87464a38e" } diff --git a/.sqlx/query-6649a414fbececa2324a151e9ce1cfe82dcdd65a86bf82629533e380594fd6a0.json b/.sqlx/query-6649a414fbececa2324a151e9ce1cfe82dcdd65a86bf82629533e380594fd6a0.json new file mode 100644 index 0000000000..65857f9751 --- /dev/null +++ b/.sqlx/query-6649a414fbececa2324a151e9ce1cfe82dcdd65a86bf82629533e380594fd6a0.json @@ -0,0 +1,162 @@ +{ + "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, enrollment_admin_email_mode = $20, enrollment_admin_custom_email = $21, enrollment_show_reset_password = $22, enrollment_show_welcome_message = $23, enrollment_send_welcome_email = $24, enrollment_windows_release_channel = $25, enrollment_linux_release_channel = $26, enrollment_macos_release_channel = $27, uuid = $28, ldap_url = $29, ldap_bind_username = $30, ldap_bind_password = $31, ldap_group_search_base = $32, ldap_user_search_base = $33, ldap_user_obj_class = $34, ldap_group_obj_class = $35, ldap_username_attr = $36, ldap_groupname_attr = $37, ldap_group_member_attr = $38, ldap_member_attr = $39, ldap_use_starttls = $40, ldap_tls_verify_cert = $41, openid_create_account = $42, license = $43, gateway_disconnect_notifications_enabled = $44, gateway_disconnect_notifications_inactivity_threshold = $45, gateway_disconnect_notifications_reconnect_notification_enabled = $46, ldap_sync_status = $47, ldap_enabled = $48, ldap_sync_enabled = $49, ldap_is_authoritative = $50, ldap_sync_interval = $51, ldap_user_auxiliary_obj_classes = $52, ldap_uses_ad = $53, ldap_user_rdn_attr = $54, ldap_sync_groups = $55, openid_username_handling = $56, ca_key_der = $57, ca_cert_der = $58, ca_expiry = $59, defguard_url = $60, default_admin_group_name = $61, authentication_period_days = $62, mfa_code_timeout_seconds = $63, public_proxy_url = $64, default_admin_id = $65, secret_key = $66, enable_stats_purge = $67, stats_purge_frequency_hours = $68, stats_purge_threshold_days = $69, enrollment_token_timeout_hours = $70, password_reset_token_timeout_hours = $71, enrollment_session_timeout_minutes = $72, password_reset_session_timeout_minutes = $73 WHERE id = 1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Bool", + "Bool", + "Bool", + "Bool", + "Text", + "Text", + "Text", + "Text", + "Text", + "Int4", + { + "Custom": { + "name": "smtp_encryption", + "kind": { + "Enum": [ + "none", + "starttls", + "implicittls" + ] + } + } + }, + "Text", + "Text", + "Text", + "Bool", + "Text", + "Text", + "Text", + "Bool", + { + "Custom": { + "name": "enrollment_admin_email_mode", + "kind": { + "Enum": [ + "initiating_admin", + "hidden", + "custom_email" + ] + } + } + }, + "Text", + "Bool", + "Bool", + "Bool", + { + "Custom": { + "name": "enrollment_release_channel", + "kind": { + "Enum": [ + "stable", + "beta", + "alpha" + ] + } + } + }, + { + "Custom": { + "name": "enrollment_release_channel", + "kind": { + "Enum": [ + "stable", + "beta", + "alpha" + ] + } + } + }, + { + "Custom": { + "name": "enrollment_release_channel", + "kind": { + "Enum": [ + "stable", + "beta", + "alpha" + ] + } + } + }, + "Uuid", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Bool", + "Bool", + "Bool", + "Text", + "Bool", + "Int4", + "Bool", + { + "Custom": { + "name": "ldap_sync_status", + "kind": { + "Enum": [ + "insync", + "outofsync" + ] + } + } + }, + "Bool", + "Bool", + "Bool", + "Int4", + "TextArray", + "Bool", + "Text", + "TextArray", + { + "Custom": { + "name": "openid_username_handling", + "kind": { + "Enum": [ + "remove_forbidden", + "replace_forbidden", + "prune_email_domain" + ] + } + } + }, + "Bytea", + "Bytea", + "Timestamp", + "Text", + "Text", + "Int4", + "Int4", + "Text", + "Int8", + "Text", + "Bool", + "Int4", + "Int4", + "Int4", + "Int4", + "Int4", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "6649a414fbececa2324a151e9ce1cfe82dcdd65a86bf82629533e380594fd6a0" +} diff --git a/.sqlx/query-89698ecaa251e056770bb90827d2d41284e6629883f647462029872c9fad2bfb.json b/.sqlx/query-89698ecaa251e056770bb90827d2d41284e6629883f647462029872c9fad2bfb.json deleted file mode 100644 index af565cdd2e..0000000000 --- a/.sqlx/query-89698ecaa251e056770bb90827d2d41284e6629883f647462029872c9fad2bfb.json +++ /dev/null @@ -1,110 +0,0 @@ -{ - "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, secret_key = $58, enable_stats_purge = $59, stats_purge_frequency_hours = $60, stats_purge_threshold_days = $61, enrollment_token_timeout_hours = $62, password_reset_token_timeout_hours = $63, enrollment_session_timeout_minutes = $64, password_reset_session_timeout_minutes = $65 WHERE id = 1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Bool", - "Bool", - "Bool", - "Bool", - "Text", - "Text", - "Text", - "Text", - "Text", - "Int4", - { - "Custom": { - "name": "smtp_encryption", - "kind": { - "Enum": [ - "none", - "starttls", - "implicittls" - ] - } - } - }, - "Text", - "Text", - "Text", - "Bool", - "Text", - "Text", - "Text", - "Bool", - "Uuid", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Bool", - "Bool", - "Bool", - "Text", - "Bool", - "Int4", - "Bool", - { - "Custom": { - "name": "ldap_sync_status", - "kind": { - "Enum": [ - "insync", - "outofsync" - ] - } - } - }, - "Bool", - "Bool", - "Bool", - "Int4", - "TextArray", - "Bool", - "Text", - "TextArray", - { - "Custom": { - "name": "openid_username_handling", - "kind": { - "Enum": [ - "remove_forbidden", - "replace_forbidden", - "prune_email_domain" - ] - } - } - }, - "Bytea", - "Bytea", - "Timestamp", - "Text", - "Text", - "Int4", - "Int4", - "Text", - "Int8", - "Text", - "Bool", - "Int4", - "Int4", - "Int4", - "Int4", - "Int4", - "Int4" - ] - }, - "nullable": [] - }, - "hash": "89698ecaa251e056770bb90827d2d41284e6629883f647462029872c9fad2bfb" -} diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index c83048f292..56c66c5337 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -125,6 +125,24 @@ impl LdapSyncStatus { } } +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Serialize, PartialEq, Type)] +#[sqlx(type_name = "enrollment_admin_email_mode", rename_all = "snake_case")] +pub enum EnrollmentAdminEmailMode { + #[default] + InitiatingAdmin, + Hidden, + CustomEmail, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Serialize, PartialEq, Type)] +#[sqlx(type_name = "enrollment_release_channel", rename_all = "lowercase")] +pub enum EnrollmentReleaseChannel { + #[default] + Stable, + Beta, + Alpha, +} + #[derive(Clone, Deserialize, PartialEq, Patch, Serialize, Default)] #[patch(attribute(derive(Deserialize, Serialize, Debug)))] pub struct Settings { @@ -152,6 +170,14 @@ pub struct Settings { pub enrollment_welcome_email: Option, pub enrollment_welcome_email_subject: Option, pub enrollment_use_welcome_message_as_email: bool, + pub enrollment_admin_email_mode: EnrollmentAdminEmailMode, + pub enrollment_admin_custom_email: Option, + pub enrollment_show_reset_password: bool, + pub enrollment_show_welcome_message: bool, + pub enrollment_send_welcome_email: bool, + pub enrollment_windows_release_channel: EnrollmentReleaseChannel, + pub enrollment_linux_release_channel: EnrollmentReleaseChannel, + pub enrollment_macos_release_channel: EnrollmentReleaseChannel, // Instance UUID needed for desktop client #[serde(skip)] pub uuid: Uuid, @@ -246,6 +272,35 @@ impl fmt::Debug for Settings { "enrollment_use_welcome_message_as_email", &self.enrollment_use_welcome_message_as_email, ) + .field("enrollment_admin_email_mode", &self.enrollment_admin_email_mode) + .field( + "enrollment_admin_custom_email", + &self.enrollment_admin_custom_email, + ) + .field( + "enrollment_show_reset_password", + &self.enrollment_show_reset_password, + ) + .field( + "enrollment_show_welcome_message", + &self.enrollment_show_welcome_message, + ) + .field( + "enrollment_send_welcome_email", + &self.enrollment_send_welcome_email, + ) + .field( + "enrollment_windows_release_channel", + &self.enrollment_windows_release_channel, + ) + .field( + "enrollment_linux_release_channel", + &self.enrollment_linux_release_channel, + ) + .field( + "enrollment_macos_release_channel", + &self.enrollment_macos_release_channel, + ) .field("uuid", &self.uuid) .field("ldap_url", &self.ldap_url) .field("ldap_bind_username", &self.ldap_bind_username) @@ -393,7 +448,14 @@ impl Settings { 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, \ + enrollment_use_welcome_message_as_email, \ + enrollment_admin_email_mode \"enrollment_admin_email_mode: EnrollmentAdminEmailMode\", \ + enrollment_admin_custom_email, enrollment_show_reset_password, \ + enrollment_show_welcome_message, enrollment_send_welcome_email, \ + enrollment_windows_release_channel \"enrollment_windows_release_channel: EnrollmentReleaseChannel\", \ + enrollment_linux_release_channel \"enrollment_linux_release_channel: EnrollmentReleaseChannel\", \ + enrollment_macos_release_channel \"enrollment_macos_release_channel: EnrollmentReleaseChannel\", \ + 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, \ @@ -463,52 +525,60 @@ impl Settings { 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, \ - enable_stats_purge = $59, \ - stats_purge_frequency_hours = $60, \ - stats_purge_threshold_days = $61, \ - enrollment_token_timeout_hours = $62, \ - password_reset_token_timeout_hours = $63, \ - enrollment_session_timeout_minutes = $64, \ - password_reset_session_timeout_minutes = $65 \ + enrollment_admin_email_mode = $20, \ + enrollment_admin_custom_email = $21, \ + enrollment_show_reset_password = $22, \ + enrollment_show_welcome_message = $23, \ + enrollment_send_welcome_email = $24, \ + enrollment_windows_release_channel = $25, \ + enrollment_linux_release_channel = $26, \ + enrollment_macos_release_channel = $27, \ + uuid = $28, \ + ldap_url = $29, \ + ldap_bind_username = $30, \ + ldap_bind_password = $31, \ + ldap_group_search_base = $32, \ + ldap_user_search_base = $33, \ + ldap_user_obj_class = $34, \ + ldap_group_obj_class = $35, \ + ldap_username_attr = $36, \ + ldap_groupname_attr = $37, \ + ldap_group_member_attr = $38, \ + ldap_member_attr = $39, \ + ldap_use_starttls = $40, \ + ldap_tls_verify_cert = $41, \ + openid_create_account = $42, \ + license = $43, \ + gateway_disconnect_notifications_enabled = $44, \ + gateway_disconnect_notifications_inactivity_threshold = $45, \ + gateway_disconnect_notifications_reconnect_notification_enabled = $46, \ + ldap_sync_status = $47, \ + ldap_enabled = $48, \ + ldap_sync_enabled = $49, \ + ldap_is_authoritative = $50, \ + ldap_sync_interval = $51, \ + ldap_user_auxiliary_obj_classes = $52, \ + ldap_uses_ad = $53, \ + ldap_user_rdn_attr = $54, \ + ldap_sync_groups = $55, \ + openid_username_handling = $56, \ + ca_key_der = $57, \ + ca_cert_der = $58, \ + ca_expiry = $59, \ + defguard_url = $60, \ + default_admin_group_name = $61, \ + authentication_period_days = $62, \ + mfa_code_timeout_seconds = $63, \ + public_proxy_url = $64, \ + default_admin_id = $65, \ + secret_key = $66, \ + enable_stats_purge = $67, \ + stats_purge_frequency_hours = $68, \ + stats_purge_threshold_days = $69, \ + enrollment_token_timeout_hours = $70, \ + password_reset_token_timeout_hours = $71, \ + enrollment_session_timeout_minutes = $72, \ + password_reset_session_timeout_minutes = $73 \ WHERE id = 1", self.openid_enabled, self.wireguard_enabled, @@ -529,6 +599,14 @@ impl Settings { self.enrollment_welcome_email, self.enrollment_welcome_email_subject, self.enrollment_use_welcome_message_as_email, + &self.enrollment_admin_email_mode as &EnrollmentAdminEmailMode, + self.enrollment_admin_custom_email, + self.enrollment_show_reset_password, + self.enrollment_show_welcome_message, + self.enrollment_send_welcome_email, + &self.enrollment_windows_release_channel as &EnrollmentReleaseChannel, + &self.enrollment_linux_release_channel as &EnrollmentReleaseChannel, + &self.enrollment_macos_release_channel as &EnrollmentReleaseChannel, self.uuid, self.ldap_url, self.ldap_bind_username, diff --git a/migrations/20260323083929_[2.0.0]_enrollment_settings.down.sql b/migrations/20260323083929_[2.0.0]_enrollment_settings.down.sql new file mode 100644 index 0000000000..5c55bf3994 --- /dev/null +++ b/migrations/20260323083929_[2.0.0]_enrollment_settings.down.sql @@ -0,0 +1,12 @@ +ALTER TABLE settings + DROP COLUMN enrollment_macos_release_channel, + DROP COLUMN enrollment_linux_release_channel, + DROP COLUMN enrollment_windows_release_channel, + DROP COLUMN enrollment_send_welcome_email, + DROP COLUMN enrollment_show_welcome_message, + DROP COLUMN enrollment_show_reset_password, + DROP COLUMN enrollment_admin_custom_email, + DROP COLUMN enrollment_admin_email_mode; + +DROP TYPE enrollment_release_channel; +DROP TYPE enrollment_admin_email_mode; diff --git a/migrations/20260323083929_[2.0.0]_enrollment_settings.up.sql b/migrations/20260323083929_[2.0.0]_enrollment_settings.up.sql new file mode 100644 index 0000000000..db53c74e06 --- /dev/null +++ b/migrations/20260323083929_[2.0.0]_enrollment_settings.up.sql @@ -0,0 +1,21 @@ +CREATE TYPE enrollment_admin_email_mode AS ENUM ( + 'initiating_admin', + 'hidden', + 'custom_email' +); + +CREATE TYPE enrollment_release_channel AS ENUM ( + 'stable', + 'beta', + 'alpha' +); + +ALTER TABLE settings + ADD COLUMN enrollment_admin_email_mode enrollment_admin_email_mode NOT NULL DEFAULT 'initiating_admin', + ADD COLUMN enrollment_admin_custom_email TEXT NULL, + ADD COLUMN enrollment_show_reset_password BOOLEAN NOT NULL DEFAULT TRUE, + ADD COLUMN enrollment_show_welcome_message BOOLEAN NOT NULL DEFAULT TRUE, + ADD COLUMN enrollment_send_welcome_email BOOLEAN NOT NULL DEFAULT TRUE, + ADD COLUMN enrollment_windows_release_channel enrollment_release_channel NOT NULL DEFAULT 'stable', + ADD COLUMN enrollment_linux_release_channel enrollment_release_channel NOT NULL DEFAULT 'stable', + ADD COLUMN enrollment_macos_release_channel enrollment_release_channel NOT NULL DEFAULT 'stable'; From b92eb6ffcaa6d563f1ff323d9697e7d21507fc15 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 23 Mar 2026 10:41:56 +0100 Subject: [PATCH 02/33] "Enrollment" nav item & initial page --- web/messages/en/components.json | 1 + web/messages/en/settings.json | 7 ++ .../pages/EnrollmentPage/EnrollmentPage.tsx | 78 +++++++++++++++++++ web/src/routeTree.gen.ts | 22 ++++++ .../_authorized/_default/enrollment.tsx | 6 ++ web/src/shared/api/types.ts | 26 +++++++ .../components/Navigation/Navigation.tsx | 6 ++ 7 files changed, 146 insertions(+) create mode 100644 web/src/pages/EnrollmentPage/EnrollmentPage.tsx create mode 100644 web/src/routes/_authorized/_default/enrollment.tsx diff --git a/web/messages/en/components.json b/web/messages/en/components.json index 5237298d31..fc1ea93279 100644 --- a/web/messages/en/components.json +++ b/web/messages/en/components.json @@ -20,6 +20,7 @@ "cmp_nav_item_aliases": "Aliases", "cmp_nav_item_users": "Users", "cmp_nav_item_groups": "Groups", + "cmp_nav_item_enrollment": "Enrollment", "cmp_nav_item_network_devices": "Network Devices", "cmp_nav_item_openid": "OpenID Apps", "cmp_nav_item_webhooks": "Webhooks", diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index 82c14c4351..13d75d91cc 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -27,6 +27,13 @@ "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_enrollment_page_title": "Enrollment", + "settings_enrollment_page_subtitle": "Configure enrollment settings to control how users join your instance and manage the enrollment flow.", + "settings_enrollment_tab_general": "General", + "settings_enrollment_tab_message_templates": "Message templates", + "settings_enrollment_general_title": "Enrollment settings", + "settings_enrollment_message_templates_title": "Welcome message", + "settings_enrollment_message_templates_subtitle": "This message will be shown at the end of enrollment process. Enrollment is a process by which a new employee will be able to activate their new account, create a password and configure a VPN device.", "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_enrollment_content": "Configure enrollment settings to define how users are invited, registered, and onboarded into your instance.", diff --git a/web/src/pages/EnrollmentPage/EnrollmentPage.tsx b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx new file mode 100644 index 0000000000..760ab47396 --- /dev/null +++ b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx @@ -0,0 +1,78 @@ +import { useMemo, useState } from 'react'; +import { m } from '../../paraglide/messages'; +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 { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox'; +import { Tabs } from '../../shared/defguard-ui/components/Tabs/Tabs'; +import type { TabsItem } from '../../shared/defguard-ui/components/Tabs/types'; +import { ThemeSpacing } from '../../shared/defguard-ui/types'; + +const EnrollmentPageTab = { + General: 'general', + MessageTemplates: 'message-templates', +} as const; + +type EnrollmentTabValue = + (typeof EnrollmentPageTab)[keyof typeof EnrollmentPageTab]; + +export const EnrollmentPage = () => { + const [activeTab, setActiveTab] = + useState(EnrollmentPageTab.General); + + const tabs = useMemo( + (): TabsItem[] => [ + { + title: m.settings_enrollment_tab_general(), + active: activeTab === EnrollmentPageTab.General, + onClick: () => { + setActiveTab(EnrollmentPageTab.General); + }, + }, + { + title: m.settings_enrollment_tab_message_templates(), + active: activeTab === EnrollmentPageTab.MessageTemplates, + onClick: () => { + setActiveTab(EnrollmentPageTab.MessageTemplates); + }, + }, + ], + [activeTab], + ); + + return ( + + + + + + {activeTab === EnrollmentPageTab.General && ( + + )} + {activeTab === EnrollmentPageTab.MessageTemplates && ( + + )} + + + {activeTab === EnrollmentPageTab.General && } + {activeTab === EnrollmentPageTab.MessageTemplates && ( + + )} + + + + ); +}; + +const EmptyTabContent = ({ tab }: { tab: EnrollmentTabValue }) => { + return
; +}; diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index 443c5dd0e8..0f13eed0d5 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -40,6 +40,7 @@ import { Route as AuthorizedDefaultUsersRouteImport } from './routes/_authorized import { Route as AuthorizedDefaultOpenidRouteImport } from './routes/_authorized/_default/openid' import { Route as AuthorizedDefaultNetworkDevicesRouteImport } from './routes/_authorized/_default/network-devices' import { Route as AuthorizedDefaultGroupsRouteImport } from './routes/_authorized/_default/groups' +import { Route as AuthorizedDefaultEnrollmentRouteImport } from './routes/_authorized/_default/enrollment' import { Route as AuthorizedDefaultEdgesRouteImport } from './routes/_authorized/_default/edges' import { Route as AuthorizedDefaultActivityRouteImport } from './routes/_authorized/_default/activity' import { Route as AuthorizedDefaultVpnOverviewIndexRouteImport } from './routes/_authorized/_default/vpn-overview/index' @@ -227,6 +228,12 @@ const AuthorizedDefaultGroupsRoute = AuthorizedDefaultGroupsRouteImport.update({ path: '/groups', getParentRoute: () => AuthorizedDefaultRoute, } as any) +const AuthorizedDefaultEnrollmentRoute = + AuthorizedDefaultEnrollmentRouteImport.update({ + id: '/enrollment', + path: '/enrollment', + getParentRoute: () => AuthorizedDefaultRoute, + } as any) const AuthorizedDefaultEdgesRoute = AuthorizedDefaultEdgesRouteImport.update({ id: '/edges', path: '/edges', @@ -407,6 +414,7 @@ export interface FileRoutesByFullPath { '/auth/': typeof AuthIndexRoute '/activity': typeof AuthorizedDefaultActivityRoute '/edges': typeof AuthorizedDefaultEdgesRoute + '/enrollment': typeof AuthorizedDefaultEnrollmentRoute '/groups': typeof AuthorizedDefaultGroupsRoute '/network-devices': typeof AuthorizedDefaultNetworkDevicesRoute '/openid': typeof AuthorizedDefaultOpenidRoute @@ -464,6 +472,7 @@ export interface FileRoutesByTo { '/auth': typeof AuthIndexRoute '/activity': typeof AuthorizedDefaultActivityRoute '/edges': typeof AuthorizedDefaultEdgesRoute + '/enrollment': typeof AuthorizedDefaultEnrollmentRoute '/groups': typeof AuthorizedDefaultGroupsRoute '/network-devices': typeof AuthorizedDefaultNetworkDevicesRoute '/openid': typeof AuthorizedDefaultOpenidRoute @@ -525,6 +534,7 @@ export interface FileRoutesById { '/auth/': typeof AuthIndexRoute '/_authorized/_default/activity': typeof AuthorizedDefaultActivityRoute '/_authorized/_default/edges': typeof AuthorizedDefaultEdgesRoute + '/_authorized/_default/enrollment': typeof AuthorizedDefaultEnrollmentRoute '/_authorized/_default/groups': typeof AuthorizedDefaultGroupsRoute '/_authorized/_default/network-devices': typeof AuthorizedDefaultNetworkDevicesRoute '/_authorized/_default/openid': typeof AuthorizedDefaultOpenidRoute @@ -585,6 +595,7 @@ export interface FileRouteTypes { | '/auth/' | '/activity' | '/edges' + | '/enrollment' | '/groups' | '/network-devices' | '/openid' @@ -642,6 +653,7 @@ export interface FileRouteTypes { | '/auth' | '/activity' | '/edges' + | '/enrollment' | '/groups' | '/network-devices' | '/openid' @@ -702,6 +714,7 @@ export interface FileRouteTypes { | '/auth/' | '/_authorized/_default/activity' | '/_authorized/_default/edges' + | '/_authorized/_default/enrollment' | '/_authorized/_default/groups' | '/_authorized/_default/network-devices' | '/_authorized/_default/openid' @@ -977,6 +990,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthorizedDefaultGroupsRouteImport parentRoute: typeof AuthorizedDefaultRoute } + '/_authorized/_default/enrollment': { + id: '/_authorized/_default/enrollment' + path: '/enrollment' + fullPath: '/enrollment' + preLoaderRoute: typeof AuthorizedDefaultEnrollmentRouteImport + parentRoute: typeof AuthorizedDefaultRoute + } '/_authorized/_default/edges': { id: '/_authorized/_default/edges' path: '/edges' @@ -1172,6 +1192,7 @@ declare module '@tanstack/react-router' { interface AuthorizedDefaultRouteChildren { AuthorizedDefaultActivityRoute: typeof AuthorizedDefaultActivityRoute AuthorizedDefaultEdgesRoute: typeof AuthorizedDefaultEdgesRoute + AuthorizedDefaultEnrollmentRoute: typeof AuthorizedDefaultEnrollmentRoute AuthorizedDefaultGroupsRoute: typeof AuthorizedDefaultGroupsRoute AuthorizedDefaultNetworkDevicesRoute: typeof AuthorizedDefaultNetworkDevicesRoute AuthorizedDefaultOpenidRoute: typeof AuthorizedDefaultOpenidRoute @@ -1207,6 +1228,7 @@ interface AuthorizedDefaultRouteChildren { const AuthorizedDefaultRouteChildren: AuthorizedDefaultRouteChildren = { AuthorizedDefaultActivityRoute: AuthorizedDefaultActivityRoute, AuthorizedDefaultEdgesRoute: AuthorizedDefaultEdgesRoute, + AuthorizedDefaultEnrollmentRoute: AuthorizedDefaultEnrollmentRoute, AuthorizedDefaultGroupsRoute: AuthorizedDefaultGroupsRoute, AuthorizedDefaultNetworkDevicesRoute: AuthorizedDefaultNetworkDevicesRoute, AuthorizedDefaultOpenidRoute: AuthorizedDefaultOpenidRoute, diff --git a/web/src/routes/_authorized/_default/enrollment.tsx b/web/src/routes/_authorized/_default/enrollment.tsx new file mode 100644 index 0000000000..cb56cc1ce4 --- /dev/null +++ b/web/src/routes/_authorized/_default/enrollment.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { EnrollmentPage } from '../../../pages/EnrollmentPage/EnrollmentPage'; + +export const Route = createFileRoute('/_authorized/_default/enrollment')({ + component: EnrollmentPage, +}); diff --git a/web/src/shared/api/types.ts b/web/src/shared/api/types.ts index 662a701163..e3976e7bba 100644 --- a/web/src/shared/api/types.ts +++ b/web/src/shared/api/types.ts @@ -869,6 +869,24 @@ export const SmtpEncryption = { export type SmtpEncryptionValue = (typeof SmtpEncryption)[keyof typeof SmtpEncryption]; +export const EnrollmentAdminEmailMode = { + InitiatingAdmin: 'InitiatingAdmin', + Hidden: 'Hidden', + CustomEmail: 'CustomEmail', +} as const; + +export type EnrollmentAdminEmailModeValue = + (typeof EnrollmentAdminEmailMode)[keyof typeof EnrollmentAdminEmailMode]; + +export const EnrollmentReleaseChannel = { + Stable: 'Stable', + Beta: 'Beta', + Alpha: 'Alpha', +} as const; + +export type EnrollmentReleaseChannelValue = + (typeof EnrollmentReleaseChannel)[keyof typeof EnrollmentReleaseChannel]; + export interface SettingsSMTP { smtp_encryption: SmtpEncryptionValue; smtp_server: string | null; @@ -884,6 +902,14 @@ export interface SettingsEnrollment { enrollment_welcome_email: string; enrollment_welcome_email_subject: string; enrollment_use_welcome_message_as_email: boolean; + enrollment_admin_email_mode: EnrollmentAdminEmailModeValue; + enrollment_admin_custom_email: string | null; + enrollment_show_reset_password: boolean; + enrollment_show_welcome_message: boolean; + enrollment_send_welcome_email: boolean; + enrollment_windows_release_channel: EnrollmentReleaseChannelValue; + enrollment_linux_release_channel: EnrollmentReleaseChannelValue; + enrollment_macos_release_channel: EnrollmentReleaseChannelValue; } export interface SettingsModules { diff --git a/web/src/shared/components/Navigation/Navigation.tsx b/web/src/shared/components/Navigation/Navigation.tsx index bf0eb46b74..010657fd05 100644 --- a/web/src/shared/components/Navigation/Navigation.tsx +++ b/web/src/shared/components/Navigation/Navigation.tsx @@ -81,6 +81,12 @@ const navigationConfig: NavGroupProps[] = [ link: '/groups', testId: 'groups', }, + { + id: 'enrollment', + icon: 'enrollment', + label: m.cmp_nav_item_enrollment(), + link: '/enrollment', + }, ], }, { From d35ed57b27912ef9a6d7db0774da26a9d580e00a Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 23 Mar 2026 11:11:54 +0100 Subject: [PATCH 03/33] enrollment settings general tab --- web/messages/en/settings.json | 25 +- .../pages/EnrollmentPage/EnrollmentPage.tsx | 375 ++++++++++++++++-- .../SettingsEnrollmentPage.tsx | 59 +-- .../tabs/SettingsGeneralTab.tsx | 4 +- 4 files changed, 383 insertions(+), 80 deletions(-) diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index 13d75d91cc..872b471183 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -3,7 +3,7 @@ "settings_page_title": "Settings", "settings_breadcrumb_general": "General", "settings_breadcrumb_instance": "Instance settings", - "settings_breadcrumb_enrollment": "Enrollment", + "settings_breadcrumb_password_reset": "Password reset", "settings_breadcrumb_client_behavior": "Client behavior", "settings_instance_title": "Instance settings", "settings_instance_subtitle": "Here you can configure general instance parameters.", @@ -21,8 +21,8 @@ "settings_vpn_stats_toggle_title": "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_password_reset_title": "Password reset", + "settings_password_reset_subtitle": "Configure token and session timeouts for 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", @@ -34,7 +34,26 @@ "settings_enrollment_general_title": "Enrollment settings", "settings_enrollment_message_templates_title": "Welcome message", "settings_enrollment_message_templates_subtitle": "This message will be shown at the end of enrollment process. Enrollment is a process by which a new employee will be able to activate their new account, create a password and configure a VPN device.", + "settings_enrollment_section_general_title": "General", + "settings_enrollment_section_general_description": "Choose whether the administrator email should be shown during enrollment and how it should be presented to the end user.", + "settings_enrollment_admin_email_mode_initiating_admin": "Use the email address of the administrator who initiated the enrollment", + "settings_enrollment_admin_email_mode_hidden": "Do not show admin email", + "settings_enrollment_admin_email_mode_custom": "Use custom email address", + "settings_enrollment_admin_email_custom_label": "Email", + "settings_enrollment_section_versions_title": "Version control", + "settings_enrollment_section_versions_description": "Defguard Desktop downloads during enrollment can be pinned to a release channel for each supported operating system.", + "settings_enrollment_windows_channel_label": "Windows", + "settings_enrollment_linux_channel_label": "Linux", + "settings_enrollment_macos_channel_label": "Mac OS", + "settings_enrollment_release_channel_stable": "Product version released", + "settings_enrollment_release_channel_beta": "Testing (Beta)", + "settings_enrollment_release_channel_alpha": "Quick fixes (Alpha)", + "settings_enrollment_section_duration_title": "Enrollment session duration", + "settings_enrollment_section_duration_description": "Configure how long enrollment links stay valid and how long an enrollment session can remain active before the user needs to start again.", + "settings_enrollment_toggle_reset_password_title": "Display the \"Reset password\" option on the Enrollment Service home screen.", + "settings_enrollment_toggle_reset_password_description": "In case your instance uses only an external SSO provider, Defguard's internal password reset is not needed, as authentication is handled externally.", "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_password_reset_content": "Configure password reset token and session timeouts for self-service password recovery.", "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_enrollment_content": "Configure enrollment settings to define how users are invited, registered, and onboarded into your instance.", "settings_duration_one_day": "1 day", diff --git a/web/src/pages/EnrollmentPage/EnrollmentPage.tsx b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx index 760ab47396..9507d6ecce 100644 --- a/web/src/pages/EnrollmentPage/EnrollmentPage.tsx +++ b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx @@ -1,25 +1,97 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; +import z from 'zod'; import { m } from '../../paraglide/messages'; +import api from '../../shared/api/api'; +import { + EnrollmentAdminEmailMode, + EnrollmentReleaseChannel, + type EnrollmentReleaseChannelValue, + type Settings, +} from '../../shared/api/types'; +import { Controls } from '../../shared/components/Controls/Controls'; 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 { Button } from '../../shared/defguard-ui/components/Button/Button'; +import { Divider } from '../../shared/defguard-ui/components/Divider/Divider'; +import { Fold } from '../../shared/defguard-ui/components/Fold/Fold'; +import { MarkedSection } from '../../shared/defguard-ui/components/MarkedSection/MarkedSection'; +import { MarkedSectionHeader } from '../../shared/defguard-ui/components/MarkedSectionHeader/MarkedSectionHeader'; +import type { SelectOption } from '../../shared/defguard-ui/components/Select/types'; import { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox'; import { Tabs } from '../../shared/defguard-ui/components/Tabs/Tabs'; import type { TabsItem } from '../../shared/defguard-ui/components/Tabs/types'; +import { Snackbar } from '../../shared/defguard-ui/providers/snackbar/snackbar'; import { ThemeSpacing } from '../../shared/defguard-ui/types'; +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 EnrollmentPageTab = { General: 'general', MessageTemplates: 'message-templates', } as const; -type EnrollmentTabValue = - (typeof EnrollmentPageTab)[keyof typeof EnrollmentPageTab]; +type EnrollmentTabValue = (typeof EnrollmentPageTab)[keyof typeof EnrollmentPageTab]; + +const adminEmailModeOptions = [ + { + value: EnrollmentAdminEmailMode.InitiatingAdmin, + title: m.settings_enrollment_admin_email_mode_initiating_admin(), + }, + { + value: EnrollmentAdminEmailMode.Hidden, + title: m.settings_enrollment_admin_email_mode_hidden(), + }, + { + value: EnrollmentAdminEmailMode.CustomEmail, + title: m.settings_enrollment_admin_email_mode_custom(), + }, +] as const; + +const releaseChannelOptions: SelectOption[] = [ + { + key: EnrollmentReleaseChannel.Stable, + value: EnrollmentReleaseChannel.Stable, + label: m.settings_enrollment_release_channel_stable(), + }, + { + key: EnrollmentReleaseChannel.Beta, + value: EnrollmentReleaseChannel.Beta, + label: m.settings_enrollment_release_channel_beta(), + }, + { + key: EnrollmentReleaseChannel.Alpha, + value: EnrollmentReleaseChannel.Alpha, + label: m.settings_enrollment_release_channel_alpha(), + }, +]; + +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: m.settings_duration_minutes({ minutes: 10 }), + 30: m.settings_duration_minutes({ minutes: 30 }), + 60: m.settings_duration_one_hour(), +}); export const EnrollmentPage = () => { - const [activeTab, setActiveTab] = - useState(EnrollmentPageTab.General); + const [activeTab, setActiveTab] = useState( + EnrollmentPageTab.General, + ); + const { data: settings } = useQuery(getSettingsQueryOptions); const tabs = useMemo( (): TabsItem[] => [ @@ -48,31 +120,288 @@ export const EnrollmentPage = () => { {activeTab === EnrollmentPageTab.General && ( - + <> + + + {isPresent(settings) && ( + + + + )} + )} {activeTab === EnrollmentPageTab.MessageTemplates && ( - + <> + + + +
+ + )} - - - {activeTab === EnrollmentPageTab.General && } - {activeTab === EnrollmentPageTab.MessageTemplates && ( - - )} - ); }; -const EmptyTabContent = ({ tab }: { tab: EnrollmentTabValue }) => { - return
; +const generalTabFormSchema = z + .object({ + enrollment_admin_email_mode: z.enum(EnrollmentAdminEmailMode), + enrollment_admin_custom_email: z.string(), + enrollment_windows_release_channel: z.enum(EnrollmentReleaseChannel), + enrollment_linux_release_channel: z.enum(EnrollmentReleaseChannel), + enrollment_macos_release_channel: z.enum(EnrollmentReleaseChannel), + enrollment_token_timeout_hours: z.number(m.form_error_required()).int().min(1), + enrollment_session_timeout_minutes: z.number(m.form_error_required()).int().min(1), + enrollment_show_reset_password: z.boolean(), + }) + .superRefine((values, ctx) => { + if (values.enrollment_admin_email_mode === EnrollmentAdminEmailMode.CustomEmail) { + const result = z + .email(m.form_error_email()) + .min(1, m.form_error_required()) + .safeParse(values.enrollment_admin_custom_email); + if (!result.success) { + ctx.addIssue({ + code: 'custom', + path: ['enrollment_admin_custom_email'], + message: result.error.message, + }); + } + } + }); + +type GeneralTabFormFields = z.infer; + +const GeneralTabContent = ({ settings }: { settings: Settings }) => { + const { mutateAsync } = useMutation({ + mutationFn: api.settings.patchSettings, + meta: { + invalidate: ['settings'], + }, + onSuccess: () => { + Snackbar.default(m.settings_msg_saved()); + }, + onError: () => { + Snackbar.error(m.settings_msg_save_failed()); + }, + }); + + const defaultValues = useMemo( + (): GeneralTabFormFields => ({ + enrollment_admin_email_mode: + settings.enrollment_admin_email_mode ?? EnrollmentAdminEmailMode.InitiatingAdmin, + enrollment_admin_custom_email: settings.enrollment_admin_custom_email ?? '', + enrollment_windows_release_channel: + settings.enrollment_windows_release_channel ?? EnrollmentReleaseChannel.Stable, + enrollment_linux_release_channel: + settings.enrollment_linux_release_channel ?? EnrollmentReleaseChannel.Stable, + enrollment_macos_release_channel: + settings.enrollment_macos_release_channel ?? EnrollmentReleaseChannel.Stable, + enrollment_token_timeout_hours: settings.enrollment_token_timeout_hours ?? 24, + enrollment_session_timeout_minutes: + settings.enrollment_session_timeout_minutes ?? 10, + enrollment_show_reset_password: settings.enrollment_show_reset_password ?? true, + }), + [settings], + ); + + const enrollmentTokenTimeoutOptions = useMemo( + () => + withNumericFallbackOption( + enrollmentTokenTimeoutBaseOptions, + defaultValues.enrollment_token_timeout_hours, + 'hours', + ), + [defaultValues.enrollment_token_timeout_hours], + ); + + const enrollmentSessionTimeoutOptions = useMemo( + () => + withNumericFallbackOption( + enrollmentSessionTimeoutBaseOptions, + defaultValues.enrollment_session_timeout_minutes, + 'minutes', + ), + [defaultValues.enrollment_session_timeout_minutes], + ); + + const form = useAppForm({ + defaultValues, + validationLogic: formChangeLogic, + validators: { + onSubmit: generalTabFormSchema, + onChange: generalTabFormSchema, + }, + onSubmit: async ({ value }) => { + await mutateAsync({ + ...value, + enrollment_admin_custom_email: + value.enrollment_admin_email_mode === EnrollmentAdminEmailMode.CustomEmail + ? value.enrollment_admin_custom_email + : null, + }); + form.reset(value); + }, + }); + + return ( +
{ + e.stopPropagation(); + e.preventDefault(); + form.handleSubmit(); + }} + > + + + + {adminEmailModeOptions.map((option, index) => ( +
+ {index > 0 && } + + {(field) => ( + + {option.value === EnrollmentAdminEmailMode.CustomEmail && ( + + state.values.enrollment_admin_email_mode === + EnrollmentAdminEmailMode.CustomEmail + } + > + {(isCustomEmailMode) => ( + + + + {(field) => ( + + )} + + + )} + + )} + + )} + +
+ ))} +
+ + + + + {(field) => ( + + )} + + + + {(field) => ( + + )} + + + + {(field) => ( + + )} + + + + + + + {(field) => ( + + )} + + + + {(field) => ( + + )} + + + + {(field) => ( + + )} + + +
+ ({ + isDefault: state.isDefaultValue || state.isPristine, + isSubmitting: state.isSubmitting, + canSubmit: state.canSubmit, + })} + > + {({ isDefault, isSubmitting, canSubmit }) => ( + +
+
+
+ )} +
+
+ ); }; diff --git a/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx b/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx index ab4994b25f..62facdab35 100644 --- a/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx +++ b/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx @@ -35,7 +35,7 @@ const breadcrumbs = [ {m.settings_breadcrumb_general()} , - {m.settings_breadcrumb_enrollment()} + {m.settings_breadcrumb_password_reset()} , ]; @@ -47,8 +47,8 @@ export const SettingsEnrollmentPage = () => { {isPresent(settings) && ( @@ -61,22 +61,20 @@ export const SettingsEnrollmentPage = () => { }; const formSchema = z.object({ - enrollment_token_timeout_hours: z.number(m.form_error_required()).int().min(1), password_reset_token_timeout_hours: z.number(m.form_error_required()).int().min(1), - enrollment_session_timeout_minutes: z.number(m.form_error_required()).int().min(1), password_reset_session_timeout_minutes: z.number(m.form_error_required()).int().min(1), }); type FormFields = z.infer; -const enrollmentTokenTimeoutBaseOptions = createNumericSelectOptions({ +const passwordResetTokenTimeoutBaseOptions = 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({ +const passwordResetSessionTimeoutBaseOptions = createNumericSelectOptions({ 10: m.settings_duration_minutes({ minutes: 10 }), 30: m.settings_duration_minutes({ minutes: 30 }), 60: m.settings_duration_one_hour(), @@ -98,51 +96,28 @@ const Content = ({ settings }: { settings: Settings }) => { const defaultValues = useMemo( (): FormFields => ({ - enrollment_token_timeout_hours: settings.enrollment_token_timeout_hours ?? 24, password_reset_token_timeout_hours: settings.password_reset_token_timeout_hours ?? 24, - enrollment_session_timeout_minutes: - settings.enrollment_session_timeout_minutes ?? 10, password_reset_session_timeout_minutes: settings.password_reset_session_timeout_minutes ?? 10, }), [settings], ); - const enrollmentTokenTimeoutOptions = useMemo( - () => - withNumericFallbackOption( - enrollmentTokenTimeoutBaseOptions, - defaultValues.enrollment_token_timeout_hours, - 'hours', - ), - [defaultValues.enrollment_token_timeout_hours], - ); - const passwordResetTokenTimeoutOptions = useMemo( () => withNumericFallbackOption( - enrollmentTokenTimeoutBaseOptions, + passwordResetTokenTimeoutBaseOptions, defaultValues.password_reset_token_timeout_hours, 'hours', ), [defaultValues.password_reset_token_timeout_hours], ); - const enrollmentSessionTimeoutOptions = useMemo( - () => - withNumericFallbackOption( - enrollmentSessionTimeoutBaseOptions, - defaultValues.enrollment_session_timeout_minutes, - 'minutes', - ), - [defaultValues.enrollment_session_timeout_minutes], - ); - const passwordResetSessionTimeoutOptions = useMemo( () => withNumericFallbackOption( - enrollmentSessionTimeoutBaseOptions, + passwordResetSessionTimeoutBaseOptions, defaultValues.password_reset_session_timeout_minutes, 'minutes', ), @@ -171,16 +146,6 @@ const Content = ({ settings }: { settings: Settings }) => { }} > - - {(field) => ( - - )} - - {(field) => ( { )} - - {(field) => ( - - )} - - {(field) => ( { From 95e22035f75974fd45fdafd0fc459baa28476035 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 23 Mar 2026 11:52:39 +0100 Subject: [PATCH 04/33] fix "General" section --- web/messages/en/settings.json | 9 +++++---- .../pages/EnrollmentPage/EnrollmentPage.tsx | 19 ++++++++++++++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index 872b471183..f8861df660 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -35,10 +35,11 @@ "settings_enrollment_message_templates_title": "Welcome message", "settings_enrollment_message_templates_subtitle": "This message will be shown at the end of enrollment process. Enrollment is a process by which a new employee will be able to activate their new account, create a password and configure a VPN device.", "settings_enrollment_section_general_title": "General", - "settings_enrollment_section_general_description": "Choose whether the administrator email should be shown during enrollment and how it should be presented to the end user.", - "settings_enrollment_admin_email_mode_initiating_admin": "Use the email address of the administrator who initiated the enrollment", - "settings_enrollment_admin_email_mode_hidden": "Do not show admin email", - "settings_enrollment_admin_email_mode_custom": "Use custom email address", + "settings_enrollment_section_admin_email_title": "Show administrator email in Enrollment", + "settings_enrollment_section_admin_email_description": "Configure whether the administrator email address is displayed in the final step of enrollment, allowing you to show or hide contact information as needed.", + "settings_enrollment_admin_email_mode_initiating_admin": "Show the email address of the administrator who initiated the enrollment", + "settings_enrollment_admin_email_mode_hidden": "Do not show admin contact", + "settings_enrollment_admin_email_mode_custom": "Show a specified address", "settings_enrollment_admin_email_custom_label": "Email", "settings_enrollment_section_versions_title": "Version control", "settings_enrollment_section_versions_description": "Defguard Desktop downloads during enrollment can be pinned to a release channel for each supported operating system.", diff --git a/web/src/pages/EnrollmentPage/EnrollmentPage.tsx b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx index 9507d6ecce..6681bc3432 100644 --- a/web/src/pages/EnrollmentPage/EnrollmentPage.tsx +++ b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx @@ -14,6 +14,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 { AppText } from '../../shared/defguard-ui/components/AppText/AppText'; import { Button } from '../../shared/defguard-ui/components/Button/Button'; import { Divider } from '../../shared/defguard-ui/components/Divider/Divider'; import { Fold } from '../../shared/defguard-ui/components/Fold/Fold'; @@ -24,7 +25,7 @@ import { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox' import { Tabs } from '../../shared/defguard-ui/components/Tabs/Tabs'; import type { TabsItem } from '../../shared/defguard-ui/components/Tabs/types'; import { Snackbar } from '../../shared/defguard-ui/providers/snackbar/snackbar'; -import { ThemeSpacing } from '../../shared/defguard-ui/types'; +import { TextStyle, ThemeSpacing, ThemeVariable } from '../../shared/defguard-ui/types'; import { isPresent } from '../../shared/defguard-ui/utils/isPresent'; import { useAppForm } from '../../shared/form'; import { formChangeLogic } from '../../shared/formLogic'; @@ -263,9 +264,10 @@ const GeneralTabContent = ({ settings }: { settings: Settings }) => { > + {adminEmailModeOptions.map((option, index) => (
@@ -405,3 +407,14 @@ const GeneralTabContent = ({ settings }: { settings: Settings }) => { ); }; + +const SectionTitle = ({ title }: { title: string }) => { + return ( + <> + + {title} + + + + ); +}; From 096f37f909d186c7cd703eb41c8708a19fec8820 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 23 Mar 2026 13:50:02 +0100 Subject: [PATCH 05/33] fix "Version control" section --- web/messages/en/settings.json | 9 +- .../pages/EnrollmentPage/EnrollmentPage.tsx | 93 +++++++++++++------ .../FormSelectMultiple/FormSelectMultiple.tsx | 2 +- web/src/shared/form-context.tsx | 4 + web/src/shared/form.tsx | 6 +- 5 files changed, 77 insertions(+), 37 deletions(-) create mode 100644 web/src/shared/form-context.tsx diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index f8861df660..69fc259c34 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -25,7 +25,7 @@ "settings_password_reset_subtitle": "Configure token and session timeouts for 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_session_expires_in": "Enrollment session expiration", "settings_enrollment_label_password_reset_session_expires_in": "Password reset session expires in", "settings_enrollment_page_title": "Enrollment", "settings_enrollment_page_subtitle": "Configure enrollment settings to control how users join your instance and manage the enrollment flow.", @@ -42,15 +42,16 @@ "settings_enrollment_admin_email_mode_custom": "Show a specified address", "settings_enrollment_admin_email_custom_label": "Email", "settings_enrollment_section_versions_title": "Version control", - "settings_enrollment_section_versions_description": "Defguard Desktop downloads during enrollment can be pinned to a release channel for each supported operating system.", + "settings_enrollment_section_versions_subtitle": "Download Version Overrides by OS", + "settings_enrollment_section_versions_description": "Define which application version is offered to users on each operating system, including alpha or test builds for urgent fixes. Version overrides set here apply by default and remain active until modified or removed.", "settings_enrollment_windows_channel_label": "Windows", "settings_enrollment_linux_channel_label": "Linux", "settings_enrollment_macos_channel_label": "Mac OS", - "settings_enrollment_release_channel_stable": "Product version released", + "settings_enrollment_release_channel_stable": "Product version (stable)", "settings_enrollment_release_channel_beta": "Testing (Beta)", "settings_enrollment_release_channel_alpha": "Quick fixes (Alpha)", "settings_enrollment_section_duration_title": "Enrollment session duration", - "settings_enrollment_section_duration_description": "Configure how long enrollment links stay valid and how long an enrollment session can remain active before the user needs to start again.", + "settings_enrollment_section_duration_description": "Configure the expiration time of the unique token sent to a newly added user. This token is used to activate the account, and in this section administrators can control how long the token remains valid.", "settings_enrollment_toggle_reset_password_title": "Display the \"Reset password\" option on the Enrollment Service home screen.", "settings_enrollment_toggle_reset_password_description": "In case your instance uses only an external SSO provider, Defguard's internal password reset is not needed, as authentication is handled externally.", "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.", diff --git a/web/src/pages/EnrollmentPage/EnrollmentPage.tsx b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx index 6681bc3432..a05602cf67 100644 --- a/web/src/pages/EnrollmentPage/EnrollmentPage.tsx +++ b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx @@ -1,5 +1,5 @@ import { useMutation, useQuery } from '@tanstack/react-query'; -import { useMemo, useState } from 'react'; +import { type ReactNode, useMemo, useState } from 'react'; import z from 'zod'; import { m } from '../../paraglide/messages'; import api from '../../shared/api/api'; @@ -18,6 +18,8 @@ import { AppText } from '../../shared/defguard-ui/components/AppText/AppText'; import { Button } from '../../shared/defguard-ui/components/Button/Button'; import { Divider } from '../../shared/defguard-ui/components/Divider/Divider'; import { Fold } from '../../shared/defguard-ui/components/Fold/Fold'; +import type { IconKindValue } from '../../shared/defguard-ui/components/Icon'; +import { Icon } from '../../shared/defguard-ui/components/Icon'; import { MarkedSection } from '../../shared/defguard-ui/components/MarkedSection/MarkedSection'; import { MarkedSectionHeader } from '../../shared/defguard-ui/components/MarkedSectionHeader/MarkedSectionHeader'; import type { SelectOption } from '../../shared/defguard-ui/components/Select/types'; @@ -309,39 +311,37 @@ const GeneralTabContent = ({ settings }: { settings: Settings }) => { + - - {(field) => ( - - )} - + + + {(field) => } + + - - {(field) => ( - - )} - + + + {(field) => } + + - - {(field) => ( - - )} - + + + {(field) => } + + @@ -418,3 +418,38 @@ const SectionTitle = ({ title }: { title: string }) => { ); }; + +const EnrollmentVersionControlRow = ({ + icon, + label, + children, +}: { + icon: IconKindValue; + label: string; + children: ReactNode; +}) => { + return ( +
+
+ + + {label} + +
+ {children} +
+ ); +}; diff --git a/web/src/shared/components/FormSelectMultiple/FormSelectMultiple.tsx b/web/src/shared/components/FormSelectMultiple/FormSelectMultiple.tsx index 62abd1176c..576ed7fa47 100644 --- a/web/src/shared/components/FormSelectMultiple/FormSelectMultiple.tsx +++ b/web/src/shared/components/FormSelectMultiple/FormSelectMultiple.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react'; import { useFormFieldError } from '../../defguard-ui/hooks/useFormFieldError'; -import { useFieldContext } from '../../form'; +import { useFieldContext } from '../../form-context'; import type { SelectionKey } from '../SelectionSection/type'; import { SelectMultiple } from '../SelectMultiple/SelectMultiple'; import type { SelectMultipleProps } from '../SelectMultiple/types'; diff --git a/web/src/shared/form-context.tsx b/web/src/shared/form-context.tsx new file mode 100644 index 0000000000..a6705eabe1 --- /dev/null +++ b/web/src/shared/form-context.tsx @@ -0,0 +1,4 @@ +import { createFormHookContexts } from '@tanstack/react-form'; + +export const { fieldContext, formContext, useFieldContext, useFormContext } = + createFormHookContexts(); diff --git a/web/src/shared/form.tsx b/web/src/shared/form.tsx index c382bd48ca..3e6d048fff 100644 --- a/web/src/shared/form.tsx +++ b/web/src/shared/form.tsx @@ -1,4 +1,4 @@ -import { createFormHook, createFormHookContexts } from '@tanstack/react-form'; +import { createFormHook } from '@tanstack/react-form'; import { FormSelectMultiple } from './components/FormSelectMultiple/FormSelectMultiple'; import { FormUploadField } from './components/FormUploadField/FormUploadField'; import { FormCheckbox } from './defguard-ui/components/form/FormCheckbox/FormCheckbox'; @@ -11,9 +11,9 @@ import { FormSubmitButton } from './defguard-ui/components/form/FormSubmitButton import { FormSuggestedIPInput } from './defguard-ui/components/form/FormSuggestedIPInput/FormSuggestedIPInput'; import { FormTextarea } from './defguard-ui/components/form/FormTextarea/FormTextarea'; import { FormToggle } from './defguard-ui/components/form/FormToggle/FormToggle'; +import { fieldContext, formContext } from './form-context'; -export const { fieldContext, formContext, useFieldContext, useFormContext } = - createFormHookContexts(); +export { useFieldContext, useFormContext } from './form-context'; export const { useAppForm, withFieldGroup, withForm } = createFormHook({ fieldContext, From 5062477422e2555a387273dcd24db027364161a0 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 23 Mar 2026 14:14:24 +0100 Subject: [PATCH 06/33] implement "Message templates" tab --- web/messages/en/settings.json | 10 + .../pages/EnrollmentPage/EnrollmentPage.tsx | 264 +++++++++++++++++- web/src/pages/EnrollmentPage/style.scss | 55 ++++ 3 files changed, 326 insertions(+), 3 deletions(-) create mode 100644 web/src/pages/EnrollmentPage/style.scss diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index 69fc259c34..7a56c244dc 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -34,6 +34,16 @@ "settings_enrollment_general_title": "Enrollment settings", "settings_enrollment_message_templates_title": "Welcome message", "settings_enrollment_message_templates_subtitle": "This message will be shown at the end of enrollment process. Enrollment is a process by which a new employee will be able to activate their new account, create a password and configure a VPN device.", + "settings_enrollment_template_display_message_title": "Display welcome message", + "settings_enrollment_template_display_message_description": "This information will be displayed for user in service once enrollment is completed. We advise to insert links and explain next steps briefly.", + "settings_enrollment_template_message_label": "Welcome message", + "settings_enrollment_template_send_email_title": "Send welcome email", + "settings_enrollment_template_send_email_description": "This information will be sent to user email once enrollment is completed. We advise to insert links and explain next steps briefly.", + "settings_enrollment_template_email_subject_label": "Email subject", + "settings_enrollment_template_same_as_message": "Same as welcome message", + "settings_enrollment_template_same_as_message_banner": "Your email will contain the same text as the welcome message from the input above.", + "settings_enrollment_template_email_label": "Email message", + "settings_enrollment_template_help_title": "Tricks to format your message", "settings_enrollment_section_general_title": "General", "settings_enrollment_section_admin_email_title": "Show administrator email in Enrollment", "settings_enrollment_section_admin_email_description": "Configure whether the administrator email address is displayed in the final step of enrollment, allowing you to show or hide contact information as needed.", diff --git a/web/src/pages/EnrollmentPage/EnrollmentPage.tsx b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx index a05602cf67..83ee72d15e 100644 --- a/web/src/pages/EnrollmentPage/EnrollmentPage.tsx +++ b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx @@ -1,6 +1,7 @@ import { useMutation, useQuery } from '@tanstack/react-query'; import { type ReactNode, useMemo, useState } from 'react'; import z from 'zod'; +import './style.scss'; import { m } from '../../paraglide/messages'; import api from '../../shared/api/api'; import { @@ -90,6 +91,30 @@ const enrollmentSessionTimeoutBaseOptions = createNumericSelectOptions({ 60: m.settings_duration_one_hour(), }); +const messageTemplatesHelpVariables = `{{ first_name }} - newly created user first name +{{ last_name }} - newly created user last name +{{ username }} - newly created user username/login +{{ admin_first_name }} - first name of the administrator who initiated the enrollment process +{{ admin_last_name }} - last name of the administrator who initiated the enrollment process +{{ admin_phone }} - phone number of the administrator who initiated the enrollment process +{{ admin_email }} - email of the administrator who initiated the enrollment process +{{ defguard_url }} - internal Defguard URL (your Defguard instance address)`; + +const messageTemplatesHelpMarkdown = [ + '#, ##, ### - Create headings.', + '*text* - Italic text.', + '**text** - Bold text.', + '***text*** - Bold and italic.', + '> text - Blockquote.', + '- item or 1. item - Lists (unordered or ordered).', + '`code` - Inline code.', + '```code``` - Code block.', + '*** - Horizontal line.', + '[text](url) - Link.', + '| and --- - Create tables.', + '\\ - Escape special characters.', +].join('\n'); + export const EnrollmentPage = () => { const [activeTab, setActiveTab] = useState( EnrollmentPageTab.General, @@ -145,9 +170,7 @@ export const EnrollmentPage = () => { subtitle={m.settings_enrollment_message_templates_subtitle()} /> - -
- + {isPresent(settings) && } )} @@ -184,6 +207,17 @@ const generalTabFormSchema = z type GeneralTabFormFields = z.infer; +const messageTemplatesFormSchema = z.object({ + enrollment_show_welcome_message: z.boolean(), + enrollment_welcome_message: z.string(), + enrollment_send_welcome_email: z.boolean(), + enrollment_welcome_email_subject: z.string().min(1, m.form_error_required()), + enrollment_use_welcome_message_as_email: z.boolean(), + enrollment_welcome_email: z.string(), +}); + +type MessageTemplatesFormFields = z.infer; + const GeneralTabContent = ({ settings }: { settings: Settings }) => { const { mutateAsync } = useMutation({ mutationFn: api.settings.patchSettings, @@ -408,6 +442,200 @@ const GeneralTabContent = ({ settings }: { settings: Settings }) => { ); }; +const MessageTemplatesTabContent = ({ settings }: { settings: Settings }) => { + const { mutateAsync } = useMutation({ + mutationFn: api.settings.patchSettings, + meta: { + invalidate: ['settings'], + }, + onSuccess: () => { + Snackbar.default(m.settings_msg_saved()); + }, + onError: () => { + Snackbar.error(m.settings_msg_save_failed()); + }, + }); + + const defaultValues = useMemo( + (): MessageTemplatesFormFields => ({ + enrollment_show_welcome_message: settings.enrollment_show_welcome_message ?? true, + enrollment_welcome_message: settings.enrollment_welcome_message ?? '', + enrollment_send_welcome_email: settings.enrollment_send_welcome_email ?? true, + enrollment_welcome_email_subject: settings.enrollment_welcome_email_subject ?? '', + enrollment_use_welcome_message_as_email: + settings.enrollment_use_welcome_message_as_email ?? true, + enrollment_welcome_email: settings.enrollment_welcome_email ?? '', + }), + [settings], + ); + + const form = useAppForm({ + defaultValues, + validationLogic: formChangeLogic, + validators: { + onSubmit: messageTemplatesFormSchema, + onChange: messageTemplatesFormSchema, + }, + onSubmit: async ({ value }) => { + await mutateAsync(value); + form.reset(value); + }, + }); + + return ( +
+ +
{ + e.stopPropagation(); + e.preventDefault(); + form.handleSubmit(); + }} + > + + + + {(field) => ( + + )} + + state.values.enrollment_show_welcome_message} + > + {(showWelcomeMessage) => ( + + + + {(field) => ( + + )} + + + )} + + + + + + {(field) => ( + + )} + + state.values.enrollment_send_welcome_email} + > + {(sendWelcomeEmail) => ( + + + + {(field) => ( + + )} + + +
+ + {(field) => ( + + )} + +
+ + + state.values.enrollment_use_welcome_message_as_email + } + > + {(sameAsWelcomeMessage) => ( + <> + +
+ +

+ {m.settings_enrollment_template_same_as_message_banner()} +

+
+
+ + + + {(field) => ( + + )} + + + + )} +
+
+ )} +
+
+
+ + + + ({ + isDefault: state.isDefaultValue || state.isPristine, + isSubmitting: state.isSubmitting, + canSubmit: state.canSubmit, + })} + > + {({ isDefault, isSubmitting, canSubmit }) => ( + +
+
+
+ )} +
+ +
+ +
+ ); +}; + const SectionTitle = ({ title }: { title: string }) => { return ( <> @@ -453,3 +681,33 @@ const EnrollmentVersionControlRow = ({
); }; + +const MessageTemplatesHelpPanel = () => { + return ( +
+
+ + + {m.settings_enrollment_template_help_title()} + +
+
+ + {messageTemplatesHelpVariables} + + + + {messageTemplatesHelpMarkdown} + +
+
+ ); +}; diff --git a/web/src/pages/EnrollmentPage/style.scss b/web/src/pages/EnrollmentPage/style.scss new file mode 100644 index 0000000000..baa4478f03 --- /dev/null +++ b/web/src/pages/EnrollmentPage/style.scss @@ -0,0 +1,55 @@ +#enrollment-page { + .message-templates-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) 373px; + gap: var(--spacing-4xl); + align-items: start; + } + + .message-templates-sidebar { + display: flex; + flex-flow: column; + row-gap: var(--spacing-lg); + + .sidebar-header { + display: flex; + align-items: center; + gap: var(--spacing-md); + } + + .sidebar-panel { + border: var(--border-1) solid var(--border-disabled); + border-radius: var(--radius-lg); + background-color: var(--bg-default); + padding: var(--spacing-lg); + display: flex; + flex-flow: column; + row-gap: var(--spacing-lg); + } + + .sidebar-copy { + white-space: pre-line; + } + } + + .message-templates-checkbox { + display: inline-flex; + } + + .message-templates-success-banner { + display: grid; + grid-template-columns: 20px 1fr; + column-gap: var(--spacing-md); + align-items: center; + background-color: var(--fg-success-muted); + border-radius: var(--radius-lg); + padding: var(--spacing-sm) var(--spacing-lg); + box-sizing: border-box; + min-height: 36px; + + .copy { + font: var(--t-body-xs-500); + color: var(--fg-success); + } + } +} From a5fbf1d71596c22a67bdc7b61e6f852d2db5824f Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 24 Mar 2026 07:09:42 +0100 Subject: [PATCH 07/33] move password reset settings to "instance" section --- web/messages/en/settings.json | 4 +- .../SettingsEnrollmentPage.tsx | 194 ------------------ .../tabs/SettingsGeneralTab.tsx | 8 - .../SettingsInstancePage.tsx | 67 ++++++ web/src/routeTree.gen.ts | 23 --- .../_default/settings/enrollment.tsx | 6 - 6 files changed, 70 insertions(+), 232 deletions(-) delete mode 100644 web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx delete mode 100644 web/src/routes/_authorized/_default/settings/enrollment.tsx diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index 7a56c244dc..6d38568963 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -11,6 +11,8 @@ "settings_instance_section_core_description": "Configure the primary instance identity, URLs and session lifetime used across the platform.", "settings_instance_section_data_retention": "VPN stats retention", "settings_instance_section_data_retention_description": "Control if VPN statistics are purged automatically and how long they are kept.", + "settings_instance_section_password_title": "Password settings", + "settings_instance_section_password_description": "Set the token expiration time when a password is changed in the system.", "settings_instance_error_invalid_url": "Invalid URL", "settings_instance_error_invalid_host": "Defguard URL must use a hostname, not an IP address", "settings_instance_error_defguard_url_required": "Defguard URL is required", @@ -18,6 +20,7 @@ "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_password_reset_session_expiration": "Password reset session expiration", "settings_vpn_stats_toggle_title": "Stats purge", "settings_vpn_stats_label_purge_frequency": "Stats purge frequency", "settings_vpn_stats_label_purge_threshold": "Stats purge threshold", @@ -65,7 +68,6 @@ "settings_enrollment_toggle_reset_password_title": "Display the \"Reset password\" option on the Enrollment Service home screen.", "settings_enrollment_toggle_reset_password_description": "In case your instance uses only an external SSO provider, Defguard's internal password reset is not needed, as authentication is handled externally.", "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_password_reset_content": "Configure password reset token and session timeouts for self-service password recovery.", "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_enrollment_content": "Configure enrollment settings to define how users are invited, registered, and onboarded into your instance.", "settings_duration_one_day": "1 day", diff --git a/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx b/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx deleted file mode 100644 index 62facdab35..0000000000 --- a/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import { useMutation, useQuery } from '@tanstack/react-query'; -import { Link } from '@tanstack/react-router'; -import { useMemo } from 'react'; -import z from 'zod'; -import { m } from '../../../paraglide/messages'; -import api from '../../../shared/api/api'; -import type { Settings } from '../../../shared/api/types'; -import { Breadcrumbs } from '../../../shared/components/Breadcrumbs/Breadcrumbs'; -import { Controls } from '../../../shared/components/Controls/Controls'; -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 { 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'; -import { ThemeSpacing } from '../../../shared/defguard-ui/types'; -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 = [ - - {m.settings_breadcrumb_general()} - , - - {m.settings_breadcrumb_password_reset()} - , -]; - -export const SettingsEnrollmentPage = () => { - const { data: settings } = useQuery(getSettingsQueryOptions); - return ( - - - - - {isPresent(settings) && ( - - - - )} - - - ); -}; - -const formSchema = z.object({ - password_reset_token_timeout_hours: z.number(m.form_error_required()).int().min(1), - password_reset_session_timeout_minutes: z.number(m.form_error_required()).int().min(1), -}); - -type FormFields = z.infer; - -const passwordResetTokenTimeoutBaseOptions = 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 passwordResetSessionTimeoutBaseOptions = 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({ - mutationFn: api.settings.patchSettings, - meta: { - invalidate: ['settings'], - }, - onSuccess: () => { - Snackbar.default(m.settings_msg_saved()); - }, - onError: () => { - Snackbar.error(m.settings_msg_save_failed()); - }, - }); - - const defaultValues = useMemo( - (): FormFields => ({ - password_reset_token_timeout_hours: - settings.password_reset_token_timeout_hours ?? 24, - password_reset_session_timeout_minutes: - settings.password_reset_session_timeout_minutes ?? 10, - }), - [settings], - ); - - const passwordResetTokenTimeoutOptions = useMemo( - () => - withNumericFallbackOption( - passwordResetTokenTimeoutBaseOptions, - defaultValues.password_reset_token_timeout_hours, - 'hours', - ), - [defaultValues.password_reset_token_timeout_hours], - ); - - const passwordResetSessionTimeoutOptions = useMemo( - () => - withNumericFallbackOption( - passwordResetSessionTimeoutBaseOptions, - defaultValues.password_reset_session_timeout_minutes, - 'minutes', - ), - [defaultValues.password_reset_session_timeout_minutes], - ); - - const form = useAppForm({ - defaultValues, - validationLogic: formChangeLogic, - validators: { - onSubmit: formSchema, - onChange: formSchema, - }, - onSubmit: async ({ value }) => { - await mutateAsync(value); - form.reset(value); - }, - }); - - return ( -
{ - e.stopPropagation(); - e.preventDefault(); - form.handleSubmit(); - }} - > - - - {(field) => ( - - )} - - - - {(field) => ( - - )} - - - ({ - isDefault: s.isDefaultValue || s.isPristine, - isSubmitting: s.isSubmitting, - })} - > - {({ isDefault, isSubmitting }) => ( - -
-
-
- )} -
-
- ); -}; diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsGeneralTab.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsGeneralTab.tsx index 3878579812..a0d2a3a747 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsGeneralTab.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsGeneralTab.tsx @@ -23,14 +23,6 @@ export const SettingsGeneralTab = () => { /> - - - - ; @@ -124,6 +126,19 @@ const statsPurgeThresholdOptions = createNumericSelectOptions({ 90: m.settings_duration_days({ days: 90 }), }); +const passwordResetTokenTimeoutBaseOptions = 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 passwordResetSessionTimeoutBaseOptions = 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({ mutationFn: api.settings.patchSettings, @@ -147,6 +162,10 @@ const Content = ({ settings }: { settings: Settings }) => { enable_stats_purge: settings.enable_stats_purge ?? true, stats_purge_frequency_hours: settings.stats_purge_frequency_hours ?? 24, stats_purge_threshold_days: settings.stats_purge_threshold_days ?? 30, + password_reset_token_timeout_hours: + settings.password_reset_token_timeout_hours ?? 24, + password_reset_session_timeout_minutes: + settings.password_reset_session_timeout_minutes ?? 10, }), [ settings.defguard_url, @@ -156,6 +175,8 @@ const Content = ({ settings }: { settings: Settings }) => { settings.enable_stats_purge, settings.stats_purge_frequency_hours, settings.stats_purge_threshold_days, + settings.password_reset_token_timeout_hours, + settings.password_reset_session_timeout_minutes, ], ); @@ -189,6 +210,26 @@ const Content = ({ settings }: { settings: Settings }) => { [defaultValues.stats_purge_threshold_days], ); + const passwordResetTokenTimeoutOptions = useMemo( + () => + withNumericFallbackOption( + passwordResetTokenTimeoutBaseOptions, + defaultValues.password_reset_token_timeout_hours, + 'hours', + ), + [defaultValues.password_reset_token_timeout_hours], + ); + + const passwordResetSessionTimeoutOptions = useMemo( + () => + withNumericFallbackOption( + passwordResetSessionTimeoutBaseOptions, + defaultValues.password_reset_session_timeout_minutes, + 'minutes', + ), + [defaultValues.password_reset_session_timeout_minutes], + ); + const form = useAppForm({ defaultValues, validationLogic: formChangeLogic, @@ -292,6 +333,32 @@ const Content = ({ settings }: { settings: Settings }) => { )}
+ + + + + {(field) => ( + + )} + + + + {(field) => ( + + )} + + ({ diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index 0f13eed0d5..02d8132372 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -53,7 +53,6 @@ import { Route as AuthorizedDefaultSettingsOpenidRouteImport } from './routes/_a import { Route as AuthorizedDefaultSettingsLdapRouteImport } from './routes/_authorized/_default/settings/ldap' import { Route as AuthorizedDefaultSettingsInstanceRouteImport } from './routes/_authorized/_default/settings/instance' import { Route as AuthorizedDefaultSettingsGatewayNotificationsRouteImport } from './routes/_authorized/_default/settings/gateway-notifications' -import { Route as AuthorizedDefaultSettingsEnrollmentRouteImport } from './routes/_authorized/_default/settings/enrollment' import { Route as AuthorizedDefaultSettingsEditOpenidRouteImport } from './routes/_authorized/_default/settings/edit-openid' import { Route as AuthorizedDefaultSettingsClientRouteImport } from './routes/_authorized/_default/settings/client' import { Route as AuthorizedDefaultAclRulesRouteImport } from './routes/_authorized/_default/acl/rules' @@ -305,12 +304,6 @@ const AuthorizedDefaultSettingsGatewayNotificationsRoute = path: '/settings/gateway-notifications', getParentRoute: () => AuthorizedDefaultRoute, } as any) -const AuthorizedDefaultSettingsEnrollmentRoute = - AuthorizedDefaultSettingsEnrollmentRouteImport.update({ - id: '/settings/enrollment', - path: '/settings/enrollment', - getParentRoute: () => AuthorizedDefaultRoute, - } as any) const AuthorizedDefaultSettingsEditOpenidRoute = AuthorizedDefaultSettingsEditOpenidRouteImport.update({ id: '/settings/edit-openid', @@ -440,7 +433,6 @@ export interface FileRoutesByFullPath { '/acl/rules': typeof AuthorizedDefaultAclRulesRoute '/settings/client': typeof AuthorizedDefaultSettingsClientRoute '/settings/edit-openid': typeof AuthorizedDefaultSettingsEditOpenidRoute - '/settings/enrollment': typeof AuthorizedDefaultSettingsEnrollmentRoute '/settings/gateway-notifications': typeof AuthorizedDefaultSettingsGatewayNotificationsRoute '/settings/instance': typeof AuthorizedDefaultSettingsInstanceRoute '/settings/ldap': typeof AuthorizedDefaultSettingsLdapRoute @@ -498,7 +490,6 @@ export interface FileRoutesByTo { '/acl/rules': typeof AuthorizedDefaultAclRulesRoute '/settings/client': typeof AuthorizedDefaultSettingsClientRoute '/settings/edit-openid': typeof AuthorizedDefaultSettingsEditOpenidRoute - '/settings/enrollment': typeof AuthorizedDefaultSettingsEnrollmentRoute '/settings/gateway-notifications': typeof AuthorizedDefaultSettingsGatewayNotificationsRoute '/settings/instance': typeof AuthorizedDefaultSettingsInstanceRoute '/settings/ldap': typeof AuthorizedDefaultSettingsLdapRoute @@ -560,7 +551,6 @@ export interface FileRoutesById { '/_authorized/_default/acl/rules': typeof AuthorizedDefaultAclRulesRoute '/_authorized/_default/settings/client': typeof AuthorizedDefaultSettingsClientRoute '/_authorized/_default/settings/edit-openid': typeof AuthorizedDefaultSettingsEditOpenidRoute - '/_authorized/_default/settings/enrollment': typeof AuthorizedDefaultSettingsEnrollmentRoute '/_authorized/_default/settings/gateway-notifications': typeof AuthorizedDefaultSettingsGatewayNotificationsRoute '/_authorized/_default/settings/instance': typeof AuthorizedDefaultSettingsInstanceRoute '/_authorized/_default/settings/ldap': typeof AuthorizedDefaultSettingsLdapRoute @@ -621,7 +611,6 @@ export interface FileRouteTypes { | '/acl/rules' | '/settings/client' | '/settings/edit-openid' - | '/settings/enrollment' | '/settings/gateway-notifications' | '/settings/instance' | '/settings/ldap' @@ -679,7 +668,6 @@ export interface FileRouteTypes { | '/acl/rules' | '/settings/client' | '/settings/edit-openid' - | '/settings/enrollment' | '/settings/gateway-notifications' | '/settings/instance' | '/settings/ldap' @@ -740,7 +728,6 @@ export interface FileRouteTypes { | '/_authorized/_default/acl/rules' | '/_authorized/_default/settings/client' | '/_authorized/_default/settings/edit-openid' - | '/_authorized/_default/settings/enrollment' | '/_authorized/_default/settings/gateway-notifications' | '/_authorized/_default/settings/instance' | '/_authorized/_default/settings/ldap' @@ -1081,13 +1068,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthorizedDefaultSettingsGatewayNotificationsRouteImport parentRoute: typeof AuthorizedDefaultRoute } - '/_authorized/_default/settings/enrollment': { - id: '/_authorized/_default/settings/enrollment' - path: '/settings/enrollment' - fullPath: '/settings/enrollment' - preLoaderRoute: typeof AuthorizedDefaultSettingsEnrollmentRouteImport - parentRoute: typeof AuthorizedDefaultRoute - } '/_authorized/_default/settings/edit-openid': { id: '/_authorized/_default/settings/edit-openid' path: '/settings/edit-openid' @@ -1209,7 +1189,6 @@ interface AuthorizedDefaultRouteChildren { AuthorizedDefaultAclRulesRoute: typeof AuthorizedDefaultAclRulesRoute AuthorizedDefaultSettingsClientRoute: typeof AuthorizedDefaultSettingsClientRoute AuthorizedDefaultSettingsEditOpenidRoute: typeof AuthorizedDefaultSettingsEditOpenidRoute - AuthorizedDefaultSettingsEnrollmentRoute: typeof AuthorizedDefaultSettingsEnrollmentRoute AuthorizedDefaultSettingsGatewayNotificationsRoute: typeof AuthorizedDefaultSettingsGatewayNotificationsRoute AuthorizedDefaultSettingsInstanceRoute: typeof AuthorizedDefaultSettingsInstanceRoute AuthorizedDefaultSettingsLdapRoute: typeof AuthorizedDefaultSettingsLdapRoute @@ -1248,8 +1227,6 @@ const AuthorizedDefaultRouteChildren: AuthorizedDefaultRouteChildren = { AuthorizedDefaultSettingsClientRoute: AuthorizedDefaultSettingsClientRoute, AuthorizedDefaultSettingsEditOpenidRoute: AuthorizedDefaultSettingsEditOpenidRoute, - AuthorizedDefaultSettingsEnrollmentRoute: - AuthorizedDefaultSettingsEnrollmentRoute, AuthorizedDefaultSettingsGatewayNotificationsRoute: AuthorizedDefaultSettingsGatewayNotificationsRoute, AuthorizedDefaultSettingsInstanceRoute: diff --git a/web/src/routes/_authorized/_default/settings/enrollment.tsx b/web/src/routes/_authorized/_default/settings/enrollment.tsx deleted file mode 100644 index cc7fb8c77c..0000000000 --- a/web/src/routes/_authorized/_default/settings/enrollment.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { SettingsEnrollmentPage } from '../../../../pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage'; - -export const Route = createFileRoute('/_authorized/_default/settings/enrollment')({ - component: SettingsEnrollmentPage, -}); From 4bc356e2c49e195c4a551584f004266f0b073fdd Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 24 Mar 2026 07:37:06 +0100 Subject: [PATCH 08/33] update forms to conform to new limited scope --- web/messages/en/settings.json | 20 -- .../pages/EnrollmentPage/EnrollmentPage.tsx | 259 +----------------- web/src/shared/api/types.ts | 25 -- 3 files changed, 13 insertions(+), 291 deletions(-) diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index 6d38568963..e49554c4f7 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -37,8 +37,6 @@ "settings_enrollment_general_title": "Enrollment settings", "settings_enrollment_message_templates_title": "Welcome message", "settings_enrollment_message_templates_subtitle": "This message will be shown at the end of enrollment process. Enrollment is a process by which a new employee will be able to activate their new account, create a password and configure a VPN device.", - "settings_enrollment_template_display_message_title": "Display welcome message", - "settings_enrollment_template_display_message_description": "This information will be displayed for user in service once enrollment is completed. We advise to insert links and explain next steps briefly.", "settings_enrollment_template_message_label": "Welcome message", "settings_enrollment_template_send_email_title": "Send welcome email", "settings_enrollment_template_send_email_description": "This information will be sent to user email once enrollment is completed. We advise to insert links and explain next steps briefly.", @@ -47,26 +45,8 @@ "settings_enrollment_template_same_as_message_banner": "Your email will contain the same text as the welcome message from the input above.", "settings_enrollment_template_email_label": "Email message", "settings_enrollment_template_help_title": "Tricks to format your message", - "settings_enrollment_section_general_title": "General", - "settings_enrollment_section_admin_email_title": "Show administrator email in Enrollment", - "settings_enrollment_section_admin_email_description": "Configure whether the administrator email address is displayed in the final step of enrollment, allowing you to show or hide contact information as needed.", - "settings_enrollment_admin_email_mode_initiating_admin": "Show the email address of the administrator who initiated the enrollment", - "settings_enrollment_admin_email_mode_hidden": "Do not show admin contact", - "settings_enrollment_admin_email_mode_custom": "Show a specified address", - "settings_enrollment_admin_email_custom_label": "Email", - "settings_enrollment_section_versions_title": "Version control", - "settings_enrollment_section_versions_subtitle": "Download Version Overrides by OS", - "settings_enrollment_section_versions_description": "Define which application version is offered to users on each operating system, including alpha or test builds for urgent fixes. Version overrides set here apply by default and remain active until modified or removed.", - "settings_enrollment_windows_channel_label": "Windows", - "settings_enrollment_linux_channel_label": "Linux", - "settings_enrollment_macos_channel_label": "Mac OS", - "settings_enrollment_release_channel_stable": "Product version (stable)", - "settings_enrollment_release_channel_beta": "Testing (Beta)", - "settings_enrollment_release_channel_alpha": "Quick fixes (Alpha)", "settings_enrollment_section_duration_title": "Enrollment session duration", "settings_enrollment_section_duration_description": "Configure the expiration time of the unique token sent to a newly added user. This token is used to activate the account, and in this section administrators can control how long the token remains valid.", - "settings_enrollment_toggle_reset_password_title": "Display the \"Reset password\" option on the Enrollment Service home screen.", - "settings_enrollment_toggle_reset_password_description": "In case your instance uses only an external SSO provider, Defguard's internal password reset is not needed, as authentication is handled externally.", "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_enrollment_content": "Configure enrollment settings to define how users are invited, registered, and onboarded into your instance.", diff --git a/web/src/pages/EnrollmentPage/EnrollmentPage.tsx b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx index 83ee72d15e..3ba7f967ec 100644 --- a/web/src/pages/EnrollmentPage/EnrollmentPage.tsx +++ b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx @@ -1,15 +1,10 @@ import { useMutation, useQuery } from '@tanstack/react-query'; -import { type ReactNode, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import z from 'zod'; import './style.scss'; import { m } from '../../paraglide/messages'; import api from '../../shared/api/api'; -import { - EnrollmentAdminEmailMode, - EnrollmentReleaseChannel, - type EnrollmentReleaseChannelValue, - type Settings, -} from '../../shared/api/types'; +import type { Settings } from '../../shared/api/types'; import { Controls } from '../../shared/components/Controls/Controls'; import { Page } from '../../shared/components/Page/Page'; import { SettingsCard } from '../../shared/components/SettingsCard/SettingsCard'; @@ -19,11 +14,9 @@ import { AppText } from '../../shared/defguard-ui/components/AppText/AppText'; import { Button } from '../../shared/defguard-ui/components/Button/Button'; import { Divider } from '../../shared/defguard-ui/components/Divider/Divider'; import { Fold } from '../../shared/defguard-ui/components/Fold/Fold'; -import type { IconKindValue } from '../../shared/defguard-ui/components/Icon'; import { Icon } from '../../shared/defguard-ui/components/Icon'; import { MarkedSection } from '../../shared/defguard-ui/components/MarkedSection/MarkedSection'; import { MarkedSectionHeader } from '../../shared/defguard-ui/components/MarkedSectionHeader/MarkedSectionHeader'; -import type { SelectOption } from '../../shared/defguard-ui/components/Select/types'; import { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox'; import { Tabs } from '../../shared/defguard-ui/components/Tabs/Tabs'; import type { TabsItem } from '../../shared/defguard-ui/components/Tabs/types'; @@ -45,39 +38,6 @@ const EnrollmentPageTab = { type EnrollmentTabValue = (typeof EnrollmentPageTab)[keyof typeof EnrollmentPageTab]; -const adminEmailModeOptions = [ - { - value: EnrollmentAdminEmailMode.InitiatingAdmin, - title: m.settings_enrollment_admin_email_mode_initiating_admin(), - }, - { - value: EnrollmentAdminEmailMode.Hidden, - title: m.settings_enrollment_admin_email_mode_hidden(), - }, - { - value: EnrollmentAdminEmailMode.CustomEmail, - title: m.settings_enrollment_admin_email_mode_custom(), - }, -] as const; - -const releaseChannelOptions: SelectOption[] = [ - { - key: EnrollmentReleaseChannel.Stable, - value: EnrollmentReleaseChannel.Stable, - label: m.settings_enrollment_release_channel_stable(), - }, - { - key: EnrollmentReleaseChannel.Beta, - value: EnrollmentReleaseChannel.Beta, - label: m.settings_enrollment_release_channel_beta(), - }, - { - key: EnrollmentReleaseChannel.Alpha, - value: EnrollmentReleaseChannel.Alpha, - label: m.settings_enrollment_release_channel_alpha(), - }, -]; - const enrollmentTokenTimeoutBaseOptions = createNumericSelectOptions({ 1: m.settings_duration_one_hour(), 12: m.settings_duration_hours({ hours: 12 }), @@ -178,37 +138,14 @@ export const EnrollmentPage = () => { ); }; -const generalTabFormSchema = z - .object({ - enrollment_admin_email_mode: z.enum(EnrollmentAdminEmailMode), - enrollment_admin_custom_email: z.string(), - enrollment_windows_release_channel: z.enum(EnrollmentReleaseChannel), - enrollment_linux_release_channel: z.enum(EnrollmentReleaseChannel), - enrollment_macos_release_channel: z.enum(EnrollmentReleaseChannel), - enrollment_token_timeout_hours: z.number(m.form_error_required()).int().min(1), - enrollment_session_timeout_minutes: z.number(m.form_error_required()).int().min(1), - enrollment_show_reset_password: z.boolean(), - }) - .superRefine((values, ctx) => { - if (values.enrollment_admin_email_mode === EnrollmentAdminEmailMode.CustomEmail) { - const result = z - .email(m.form_error_email()) - .min(1, m.form_error_required()) - .safeParse(values.enrollment_admin_custom_email); - if (!result.success) { - ctx.addIssue({ - code: 'custom', - path: ['enrollment_admin_custom_email'], - message: result.error.message, - }); - } - } - }); +const generalTabFormSchema = z.object({ + enrollment_token_timeout_hours: z.number(m.form_error_required()).int().min(1), + enrollment_session_timeout_minutes: z.number(m.form_error_required()).int().min(1), +}); type GeneralTabFormFields = z.infer; const messageTemplatesFormSchema = z.object({ - enrollment_show_welcome_message: z.boolean(), enrollment_welcome_message: z.string(), enrollment_send_welcome_email: z.boolean(), enrollment_welcome_email_subject: z.string().min(1, m.form_error_required()), @@ -234,19 +171,9 @@ const GeneralTabContent = ({ settings }: { settings: Settings }) => { const defaultValues = useMemo( (): GeneralTabFormFields => ({ - enrollment_admin_email_mode: - settings.enrollment_admin_email_mode ?? EnrollmentAdminEmailMode.InitiatingAdmin, - enrollment_admin_custom_email: settings.enrollment_admin_custom_email ?? '', - enrollment_windows_release_channel: - settings.enrollment_windows_release_channel ?? EnrollmentReleaseChannel.Stable, - enrollment_linux_release_channel: - settings.enrollment_linux_release_channel ?? EnrollmentReleaseChannel.Stable, - enrollment_macos_release_channel: - settings.enrollment_macos_release_channel ?? EnrollmentReleaseChannel.Stable, enrollment_token_timeout_hours: settings.enrollment_token_timeout_hours ?? 24, enrollment_session_timeout_minutes: settings.enrollment_session_timeout_minutes ?? 10, - enrollment_show_reset_password: settings.enrollment_show_reset_password ?? true, }), [settings], ); @@ -279,13 +206,7 @@ const GeneralTabContent = ({ settings }: { settings: Settings }) => { onChange: generalTabFormSchema, }, onSubmit: async ({ value }) => { - await mutateAsync({ - ...value, - enrollment_admin_custom_email: - value.enrollment_admin_email_mode === EnrollmentAdminEmailMode.CustomEmail - ? value.enrollment_admin_custom_email - : null, - }); + await mutateAsync(value); form.reset(value); }, }); @@ -300,85 +221,6 @@ const GeneralTabContent = ({ settings }: { settings: Settings }) => { > - - - {adminEmailModeOptions.map((option, index) => ( -
- {index > 0 && } - - {(field) => ( - - {option.value === EnrollmentAdminEmailMode.CustomEmail && ( - - state.values.enrollment_admin_email_mode === - EnrollmentAdminEmailMode.CustomEmail - } - > - {(isCustomEmailMode) => ( - - - - {(field) => ( - - )} - - - )} - - )} - - )} - -
- ))} -
- - - - - - - {(field) => } - - - - - - {(field) => } - - - - - - {(field) => } - - - - - { /> )} - - - {(field) => ( - - )} -
{ const defaultValues = useMemo( (): MessageTemplatesFormFields => ({ - enrollment_show_welcome_message: settings.enrollment_show_welcome_message ?? true, enrollment_welcome_message: settings.enrollment_welcome_message ?? '', enrollment_send_welcome_email: settings.enrollment_send_welcome_email ?? true, enrollment_welcome_email_subject: settings.enrollment_welcome_email_subject ?? '', @@ -497,34 +328,16 @@ const MessageTemplatesTabContent = ({ settings }: { settings: Settings }) => { > - + {(field) => ( - )} - state.values.enrollment_show_welcome_message} - > - {(showWelcomeMessage) => ( - - - - {(field) => ( - - )} - - - )} - @@ -636,52 +449,6 @@ const MessageTemplatesTabContent = ({ settings }: { settings: Settings }) => { ); }; -const SectionTitle = ({ title }: { title: string }) => { - return ( - <> - - {title} - - - - ); -}; - -const EnrollmentVersionControlRow = ({ - icon, - label, - children, -}: { - icon: IconKindValue; - label: string; - children: ReactNode; -}) => { - return ( -
-
- - - {label} - -
- {children} -
- ); -}; - const MessageTemplatesHelpPanel = () => { return (
diff --git a/web/src/shared/api/types.ts b/web/src/shared/api/types.ts index e3976e7bba..273ffc384a 100644 --- a/web/src/shared/api/types.ts +++ b/web/src/shared/api/types.ts @@ -869,24 +869,6 @@ export const SmtpEncryption = { export type SmtpEncryptionValue = (typeof SmtpEncryption)[keyof typeof SmtpEncryption]; -export const EnrollmentAdminEmailMode = { - InitiatingAdmin: 'InitiatingAdmin', - Hidden: 'Hidden', - CustomEmail: 'CustomEmail', -} as const; - -export type EnrollmentAdminEmailModeValue = - (typeof EnrollmentAdminEmailMode)[keyof typeof EnrollmentAdminEmailMode]; - -export const EnrollmentReleaseChannel = { - Stable: 'Stable', - Beta: 'Beta', - Alpha: 'Alpha', -} as const; - -export type EnrollmentReleaseChannelValue = - (typeof EnrollmentReleaseChannel)[keyof typeof EnrollmentReleaseChannel]; - export interface SettingsSMTP { smtp_encryption: SmtpEncryptionValue; smtp_server: string | null; @@ -902,14 +884,7 @@ export interface SettingsEnrollment { enrollment_welcome_email: string; enrollment_welcome_email_subject: string; enrollment_use_welcome_message_as_email: boolean; - enrollment_admin_email_mode: EnrollmentAdminEmailModeValue; - enrollment_admin_custom_email: string | null; - enrollment_show_reset_password: boolean; - enrollment_show_welcome_message: boolean; enrollment_send_welcome_email: boolean; - enrollment_windows_release_channel: EnrollmentReleaseChannelValue; - enrollment_linux_release_channel: EnrollmentReleaseChannelValue; - enrollment_macos_release_channel: EnrollmentReleaseChannelValue; } export interface SettingsModules { From 4008655bcb3d2431eba9c2b6f2fceca44e3cf712 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 24 Mar 2026 07:53:41 +0100 Subject: [PATCH 09/33] update backend & migrations to conform to new limited scope --- ...30ef98e0902176cef9733468eefb094b37638.json | 111 +++++++++++ ...c58bcb3c9a964b3e3286d5f3b5b0366836e4.json} | 182 +++++------------- ...1cfe82dcdd65a86bf82629533e380594fd6a0.json | 162 ---------------- .../defguard_common/src/db/models/settings.rs | 166 +++++----------- ...83929_[2.0.0]_enrollment_settings.down.sql | 12 +- ...3083929_[2.0.0]_enrollment_settings.up.sql | 21 +- 6 files changed, 209 insertions(+), 445 deletions(-) create mode 100644 .sqlx/query-0c5875b67c6a8bd35091f17fca130ef98e0902176cef9733468eefb094b37638.json rename .sqlx/{query-2c604cff80e910a3b8e95f461d9e3a83fe7ed47aecf0a79e48d508d87464a38e.json => query-37bce7224352d3313ca950f344ddc58bcb3c9a964b3e3286d5f3b5b0366836e4.json} (71%) delete mode 100644 .sqlx/query-6649a414fbececa2324a151e9ce1cfe82dcdd65a86bf82629533e380594fd6a0.json diff --git a/.sqlx/query-0c5875b67c6a8bd35091f17fca130ef98e0902176cef9733468eefb094b37638.json b/.sqlx/query-0c5875b67c6a8bd35091f17fca130ef98e0902176cef9733468eefb094b37638.json new file mode 100644 index 0000000000..6fa706ec05 --- /dev/null +++ b/.sqlx/query-0c5875b67c6a8bd35091f17fca130ef98e0902176cef9733468eefb094b37638.json @@ -0,0 +1,111 @@ +{ + "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, enrollment_send_welcome_email = $20, uuid = $21, ldap_url = $22, ldap_bind_username = $23, ldap_bind_password = $24, ldap_group_search_base = $25, ldap_user_search_base = $26, ldap_user_obj_class = $27, ldap_group_obj_class = $28, ldap_username_attr = $29, ldap_groupname_attr = $30, ldap_group_member_attr = $31, ldap_member_attr = $32, ldap_use_starttls = $33, ldap_tls_verify_cert = $34, openid_create_account = $35, license = $36, gateway_disconnect_notifications_enabled = $37, gateway_disconnect_notifications_inactivity_threshold = $38, gateway_disconnect_notifications_reconnect_notification_enabled = $39, ldap_sync_status = $40, ldap_enabled = $41, ldap_sync_enabled = $42, ldap_is_authoritative = $43, ldap_sync_interval = $44, ldap_user_auxiliary_obj_classes = $45, ldap_uses_ad = $46, ldap_user_rdn_attr = $47, ldap_sync_groups = $48, openid_username_handling = $49, ca_key_der = $50, ca_cert_der = $51, ca_expiry = $52, defguard_url = $53, default_admin_group_name = $54, authentication_period_days = $55, mfa_code_timeout_seconds = $56, public_proxy_url = $57, default_admin_id = $58, secret_key = $59, enable_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": { + "Left": [ + "Bool", + "Bool", + "Bool", + "Bool", + "Text", + "Text", + "Text", + "Text", + "Text", + "Int4", + { + "Custom": { + "name": "smtp_encryption", + "kind": { + "Enum": [ + "none", + "starttls", + "implicittls" + ] + } + } + }, + "Text", + "Text", + "Text", + "Bool", + "Text", + "Text", + "Text", + "Bool", + "Bool", + "Uuid", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Bool", + "Bool", + "Bool", + "Text", + "Bool", + "Int4", + "Bool", + { + "Custom": { + "name": "ldap_sync_status", + "kind": { + "Enum": [ + "insync", + "outofsync" + ] + } + } + }, + "Bool", + "Bool", + "Bool", + "Int4", + "TextArray", + "Bool", + "Text", + "TextArray", + { + "Custom": { + "name": "openid_username_handling", + "kind": { + "Enum": [ + "remove_forbidden", + "replace_forbidden", + "prune_email_domain" + ] + } + } + }, + "Bytea", + "Bytea", + "Timestamp", + "Text", + "Text", + "Int4", + "Int4", + "Text", + "Int8", + "Text", + "Bool", + "Int4", + "Int4", + "Int4", + "Int4", + "Int4", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "0c5875b67c6a8bd35091f17fca130ef98e0902176cef9733468eefb094b37638" +} diff --git a/.sqlx/query-2c604cff80e910a3b8e95f461d9e3a83fe7ed47aecf0a79e48d508d87464a38e.json b/.sqlx/query-37bce7224352d3313ca950f344ddc58bcb3c9a964b3e3286d5f3b5b0366836e4.json similarity index 71% rename from .sqlx/query-2c604cff80e910a3b8e95f461d9e3a83fe7ed47aecf0a79e48d508d87464a38e.json rename to .sqlx/query-37bce7224352d3313ca950f344ddc58bcb3c9a964b3e3286d5f3b5b0366836e4.json index e4c4a6a04c..7febb0d9b9 100644 --- a/.sqlx/query-2c604cff80e910a3b8e95f461d9e3a83fe7ed47aecf0a79e48d508d87464a38e.json +++ b/.sqlx/query-37bce7224352d3313ca950f344ddc58bcb3c9a964b3e3286d5f3b5b0366836e4.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, enrollment_admin_email_mode \"enrollment_admin_email_mode: EnrollmentAdminEmailMode\", enrollment_admin_custom_email, enrollment_show_reset_password, enrollment_show_welcome_message, enrollment_send_welcome_email, enrollment_windows_release_channel \"enrollment_windows_release_channel: EnrollmentReleaseChannel\", enrollment_linux_release_channel \"enrollment_linux_release_channel: EnrollmentReleaseChannel\", enrollment_macos_release_channel \"enrollment_macos_release_channel: EnrollmentReleaseChannel\", 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, enable_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, enrollment_send_welcome_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, enable_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": [ { @@ -111,185 +111,106 @@ }, { "ordinal": 19, - "name": "enrollment_admin_email_mode: EnrollmentAdminEmailMode", - "type_info": { - "Custom": { - "name": "enrollment_admin_email_mode", - "kind": { - "Enum": [ - "initiating_admin", - "hidden", - "custom_email" - ] - } - } - } - }, - { - "ordinal": 20, - "name": "enrollment_admin_custom_email", - "type_info": "Text" - }, - { - "ordinal": 21, - "name": "enrollment_show_reset_password", - "type_info": "Bool" - }, - { - "ordinal": 22, - "name": "enrollment_show_welcome_message", - "type_info": "Bool" - }, - { - "ordinal": 23, "name": "enrollment_send_welcome_email", "type_info": "Bool" }, { - "ordinal": 24, - "name": "enrollment_windows_release_channel: EnrollmentReleaseChannel", - "type_info": { - "Custom": { - "name": "enrollment_release_channel", - "kind": { - "Enum": [ - "stable", - "beta", - "alpha" - ] - } - } - } - }, - { - "ordinal": 25, - "name": "enrollment_linux_release_channel: EnrollmentReleaseChannel", - "type_info": { - "Custom": { - "name": "enrollment_release_channel", - "kind": { - "Enum": [ - "stable", - "beta", - "alpha" - ] - } - } - } - }, - { - "ordinal": 26, - "name": "enrollment_macos_release_channel: EnrollmentReleaseChannel", - "type_info": { - "Custom": { - "name": "enrollment_release_channel", - "kind": { - "Enum": [ - "stable", - "beta", - "alpha" - ] - } - } - } - }, - { - "ordinal": 27, + "ordinal": 20, "name": "uuid", "type_info": "Uuid" }, { - "ordinal": 28, + "ordinal": 21, "name": "ldap_url", "type_info": "Text" }, { - "ordinal": 29, + "ordinal": 22, "name": "ldap_bind_username", "type_info": "Text" }, { - "ordinal": 30, + "ordinal": 23, "name": "ldap_bind_password?: SecretStringWrapper", "type_info": "Text" }, { - "ordinal": 31, + "ordinal": 24, "name": "ldap_group_search_base", "type_info": "Text" }, { - "ordinal": 32, + "ordinal": 25, "name": "ldap_user_search_base", "type_info": "Text" }, { - "ordinal": 33, + "ordinal": 26, "name": "ldap_user_obj_class", "type_info": "Text" }, { - "ordinal": 34, + "ordinal": 27, "name": "ldap_group_obj_class", "type_info": "Text" }, { - "ordinal": 35, + "ordinal": 28, "name": "ldap_username_attr", "type_info": "Text" }, { - "ordinal": 36, + "ordinal": 29, "name": "ldap_groupname_attr", "type_info": "Text" }, { - "ordinal": 37, + "ordinal": 30, "name": "ldap_group_member_attr", "type_info": "Text" }, { - "ordinal": 38, + "ordinal": 31, "name": "ldap_member_attr", "type_info": "Text" }, { - "ordinal": 39, + "ordinal": 32, "name": "openid_create_account", "type_info": "Bool" }, { - "ordinal": 40, + "ordinal": 33, "name": "license", "type_info": "Text" }, { - "ordinal": 41, + "ordinal": 34, "name": "gateway_disconnect_notifications_enabled", "type_info": "Bool" }, { - "ordinal": 42, + "ordinal": 35, "name": "ldap_use_starttls", "type_info": "Bool" }, { - "ordinal": 43, + "ordinal": 36, "name": "ldap_tls_verify_cert", "type_info": "Bool" }, { - "ordinal": 44, + "ordinal": 37, "name": "gateway_disconnect_notifications_inactivity_threshold", "type_info": "Int4" }, { - "ordinal": 45, + "ordinal": 38, "name": "gateway_disconnect_notifications_reconnect_notification_enabled", "type_info": "Bool" }, { - "ordinal": 46, + "ordinal": 39, "name": "ldap_sync_status: LdapSyncStatus", "type_info": { "Custom": { @@ -304,47 +225,47 @@ } }, { - "ordinal": 47, + "ordinal": 40, "name": "ldap_enabled", "type_info": "Bool" }, { - "ordinal": 48, + "ordinal": 41, "name": "ldap_sync_enabled", "type_info": "Bool" }, { - "ordinal": 49, + "ordinal": 42, "name": "ldap_is_authoritative", "type_info": "Bool" }, { - "ordinal": 50, + "ordinal": 43, "name": "ldap_sync_interval", "type_info": "Int4" }, { - "ordinal": 51, + "ordinal": 44, "name": "ldap_user_auxiliary_obj_classes", "type_info": "TextArray" }, { - "ordinal": 52, + "ordinal": 45, "name": "ldap_uses_ad", "type_info": "Bool" }, { - "ordinal": 53, + "ordinal": 46, "name": "ldap_user_rdn_attr", "type_info": "Text" }, { - "ordinal": 54, + "ordinal": 47, "name": "ldap_sync_groups", "type_info": "TextArray" }, { - "ordinal": 55, + "ordinal": 48, "name": "openid_username_handling: OpenIdUsernameHandling", "type_info": { "Custom": { @@ -360,87 +281,87 @@ } }, { - "ordinal": 56, + "ordinal": 49, "name": "ca_key_der", "type_info": "Bytea" }, { - "ordinal": 57, + "ordinal": 50, "name": "ca_cert_der", "type_info": "Bytea" }, { - "ordinal": 58, + "ordinal": 51, "name": "ca_expiry", "type_info": "Timestamp" }, { - "ordinal": 59, + "ordinal": 52, "name": "defguard_url", "type_info": "Text" }, { - "ordinal": 60, + "ordinal": 53, "name": "default_admin_group_name", "type_info": "Text" }, { - "ordinal": 61, + "ordinal": 54, "name": "authentication_period_days", "type_info": "Int4" }, { - "ordinal": 62, + "ordinal": 55, "name": "mfa_code_timeout_seconds", "type_info": "Int4" }, { - "ordinal": 63, + "ordinal": 56, "name": "public_proxy_url", "type_info": "Text" }, { - "ordinal": 64, + "ordinal": 57, "name": "default_admin_id", "type_info": "Int8" }, { - "ordinal": 65, + "ordinal": 58, "name": "secret_key", "type_info": "Text" }, { - "ordinal": 66, + "ordinal": 59, "name": "enable_stats_purge", "type_info": "Bool" }, { - "ordinal": 67, + "ordinal": 60, "name": "stats_purge_frequency_hours", "type_info": "Int4" }, { - "ordinal": 68, + "ordinal": 61, "name": "stats_purge_threshold_days", "type_info": "Int4" }, { - "ordinal": 69, + "ordinal": 62, "name": "enrollment_token_timeout_hours", "type_info": "Int4" }, { - "ordinal": 70, + "ordinal": 63, "name": "password_reset_token_timeout_hours", "type_info": "Int4" }, { - "ordinal": 71, + "ordinal": 64, "name": "enrollment_session_timeout_minutes", "type_info": "Int4" }, { - "ordinal": 72, + "ordinal": 65, "name": "password_reset_session_timeout_minutes", "type_info": "Int4" } @@ -469,13 +390,6 @@ true, false, false, - true, - false, - false, - false, - false, - false, - false, false, true, true, @@ -524,5 +438,5 @@ false ] }, - "hash": "2c604cff80e910a3b8e95f461d9e3a83fe7ed47aecf0a79e48d508d87464a38e" + "hash": "37bce7224352d3313ca950f344ddc58bcb3c9a964b3e3286d5f3b5b0366836e4" } diff --git a/.sqlx/query-6649a414fbececa2324a151e9ce1cfe82dcdd65a86bf82629533e380594fd6a0.json b/.sqlx/query-6649a414fbececa2324a151e9ce1cfe82dcdd65a86bf82629533e380594fd6a0.json deleted file mode 100644 index 65857f9751..0000000000 --- a/.sqlx/query-6649a414fbececa2324a151e9ce1cfe82dcdd65a86bf82629533e380594fd6a0.json +++ /dev/null @@ -1,162 +0,0 @@ -{ - "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, enrollment_admin_email_mode = $20, enrollment_admin_custom_email = $21, enrollment_show_reset_password = $22, enrollment_show_welcome_message = $23, enrollment_send_welcome_email = $24, enrollment_windows_release_channel = $25, enrollment_linux_release_channel = $26, enrollment_macos_release_channel = $27, uuid = $28, ldap_url = $29, ldap_bind_username = $30, ldap_bind_password = $31, ldap_group_search_base = $32, ldap_user_search_base = $33, ldap_user_obj_class = $34, ldap_group_obj_class = $35, ldap_username_attr = $36, ldap_groupname_attr = $37, ldap_group_member_attr = $38, ldap_member_attr = $39, ldap_use_starttls = $40, ldap_tls_verify_cert = $41, openid_create_account = $42, license = $43, gateway_disconnect_notifications_enabled = $44, gateway_disconnect_notifications_inactivity_threshold = $45, gateway_disconnect_notifications_reconnect_notification_enabled = $46, ldap_sync_status = $47, ldap_enabled = $48, ldap_sync_enabled = $49, ldap_is_authoritative = $50, ldap_sync_interval = $51, ldap_user_auxiliary_obj_classes = $52, ldap_uses_ad = $53, ldap_user_rdn_attr = $54, ldap_sync_groups = $55, openid_username_handling = $56, ca_key_der = $57, ca_cert_der = $58, ca_expiry = $59, defguard_url = $60, default_admin_group_name = $61, authentication_period_days = $62, mfa_code_timeout_seconds = $63, public_proxy_url = $64, default_admin_id = $65, secret_key = $66, enable_stats_purge = $67, stats_purge_frequency_hours = $68, stats_purge_threshold_days = $69, enrollment_token_timeout_hours = $70, password_reset_token_timeout_hours = $71, enrollment_session_timeout_minutes = $72, password_reset_session_timeout_minutes = $73 WHERE id = 1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Bool", - "Bool", - "Bool", - "Bool", - "Text", - "Text", - "Text", - "Text", - "Text", - "Int4", - { - "Custom": { - "name": "smtp_encryption", - "kind": { - "Enum": [ - "none", - "starttls", - "implicittls" - ] - } - } - }, - "Text", - "Text", - "Text", - "Bool", - "Text", - "Text", - "Text", - "Bool", - { - "Custom": { - "name": "enrollment_admin_email_mode", - "kind": { - "Enum": [ - "initiating_admin", - "hidden", - "custom_email" - ] - } - } - }, - "Text", - "Bool", - "Bool", - "Bool", - { - "Custom": { - "name": "enrollment_release_channel", - "kind": { - "Enum": [ - "stable", - "beta", - "alpha" - ] - } - } - }, - { - "Custom": { - "name": "enrollment_release_channel", - "kind": { - "Enum": [ - "stable", - "beta", - "alpha" - ] - } - } - }, - { - "Custom": { - "name": "enrollment_release_channel", - "kind": { - "Enum": [ - "stable", - "beta", - "alpha" - ] - } - } - }, - "Uuid", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Bool", - "Bool", - "Bool", - "Text", - "Bool", - "Int4", - "Bool", - { - "Custom": { - "name": "ldap_sync_status", - "kind": { - "Enum": [ - "insync", - "outofsync" - ] - } - } - }, - "Bool", - "Bool", - "Bool", - "Int4", - "TextArray", - "Bool", - "Text", - "TextArray", - { - "Custom": { - "name": "openid_username_handling", - "kind": { - "Enum": [ - "remove_forbidden", - "replace_forbidden", - "prune_email_domain" - ] - } - } - }, - "Bytea", - "Bytea", - "Timestamp", - "Text", - "Text", - "Int4", - "Int4", - "Text", - "Int8", - "Text", - "Bool", - "Int4", - "Int4", - "Int4", - "Int4", - "Int4", - "Int4" - ] - }, - "nullable": [] - }, - "hash": "6649a414fbececa2324a151e9ce1cfe82dcdd65a86bf82629533e380594fd6a0" -} diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index 56c66c5337..f226ae0066 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -125,24 +125,6 @@ impl LdapSyncStatus { } } -#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Serialize, PartialEq, Type)] -#[sqlx(type_name = "enrollment_admin_email_mode", rename_all = "snake_case")] -pub enum EnrollmentAdminEmailMode { - #[default] - InitiatingAdmin, - Hidden, - CustomEmail, -} - -#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Serialize, PartialEq, Type)] -#[sqlx(type_name = "enrollment_release_channel", rename_all = "lowercase")] -pub enum EnrollmentReleaseChannel { - #[default] - Stable, - Beta, - Alpha, -} - #[derive(Clone, Deserialize, PartialEq, Patch, Serialize, Default)] #[patch(attribute(derive(Deserialize, Serialize, Debug)))] pub struct Settings { @@ -170,14 +152,7 @@ pub struct Settings { pub enrollment_welcome_email: Option, pub enrollment_welcome_email_subject: Option, pub enrollment_use_welcome_message_as_email: bool, - pub enrollment_admin_email_mode: EnrollmentAdminEmailMode, - pub enrollment_admin_custom_email: Option, - pub enrollment_show_reset_password: bool, - pub enrollment_show_welcome_message: bool, pub enrollment_send_welcome_email: bool, - pub enrollment_windows_release_channel: EnrollmentReleaseChannel, - pub enrollment_linux_release_channel: EnrollmentReleaseChannel, - pub enrollment_macos_release_channel: EnrollmentReleaseChannel, // Instance UUID needed for desktop client #[serde(skip)] pub uuid: Uuid, @@ -272,35 +247,10 @@ impl fmt::Debug for Settings { "enrollment_use_welcome_message_as_email", &self.enrollment_use_welcome_message_as_email, ) - .field("enrollment_admin_email_mode", &self.enrollment_admin_email_mode) - .field( - "enrollment_admin_custom_email", - &self.enrollment_admin_custom_email, - ) - .field( - "enrollment_show_reset_password", - &self.enrollment_show_reset_password, - ) - .field( - "enrollment_show_welcome_message", - &self.enrollment_show_welcome_message, - ) .field( "enrollment_send_welcome_email", &self.enrollment_send_welcome_email, ) - .field( - "enrollment_windows_release_channel", - &self.enrollment_windows_release_channel, - ) - .field( - "enrollment_linux_release_channel", - &self.enrollment_linux_release_channel, - ) - .field( - "enrollment_macos_release_channel", - &self.enrollment_macos_release_channel, - ) .field("uuid", &self.uuid) .field("ldap_url", &self.ldap_url) .field("ldap_bind_username", &self.ldap_bind_username) @@ -448,13 +398,7 @@ impl Settings { 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, \ - enrollment_admin_email_mode \"enrollment_admin_email_mode: EnrollmentAdminEmailMode\", \ - enrollment_admin_custom_email, enrollment_show_reset_password, \ - enrollment_show_welcome_message, enrollment_send_welcome_email, \ - enrollment_windows_release_channel \"enrollment_windows_release_channel: EnrollmentReleaseChannel\", \ - enrollment_linux_release_channel \"enrollment_linux_release_channel: EnrollmentReleaseChannel\", \ - enrollment_macos_release_channel \"enrollment_macos_release_channel: EnrollmentReleaseChannel\", \ + enrollment_use_welcome_message_as_email, enrollment_send_welcome_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, \ @@ -525,60 +469,53 @@ impl Settings { enrollment_welcome_email = $17, \ enrollment_welcome_email_subject = $18, \ enrollment_use_welcome_message_as_email = $19, \ - enrollment_admin_email_mode = $20, \ - enrollment_admin_custom_email = $21, \ - enrollment_show_reset_password = $22, \ - enrollment_show_welcome_message = $23, \ - enrollment_send_welcome_email = $24, \ - enrollment_windows_release_channel = $25, \ - enrollment_linux_release_channel = $26, \ - enrollment_macos_release_channel = $27, \ - uuid = $28, \ - ldap_url = $29, \ - ldap_bind_username = $30, \ - ldap_bind_password = $31, \ - ldap_group_search_base = $32, \ - ldap_user_search_base = $33, \ - ldap_user_obj_class = $34, \ - ldap_group_obj_class = $35, \ - ldap_username_attr = $36, \ - ldap_groupname_attr = $37, \ - ldap_group_member_attr = $38, \ - ldap_member_attr = $39, \ - ldap_use_starttls = $40, \ - ldap_tls_verify_cert = $41, \ - openid_create_account = $42, \ - license = $43, \ - gateway_disconnect_notifications_enabled = $44, \ - gateway_disconnect_notifications_inactivity_threshold = $45, \ - gateway_disconnect_notifications_reconnect_notification_enabled = $46, \ - ldap_sync_status = $47, \ - ldap_enabled = $48, \ - ldap_sync_enabled = $49, \ - ldap_is_authoritative = $50, \ - ldap_sync_interval = $51, \ - ldap_user_auxiliary_obj_classes = $52, \ - ldap_uses_ad = $53, \ - ldap_user_rdn_attr = $54, \ - ldap_sync_groups = $55, \ - openid_username_handling = $56, \ - ca_key_der = $57, \ - ca_cert_der = $58, \ - ca_expiry = $59, \ - defguard_url = $60, \ - default_admin_group_name = $61, \ - authentication_period_days = $62, \ - mfa_code_timeout_seconds = $63, \ - public_proxy_url = $64, \ - default_admin_id = $65, \ - secret_key = $66, \ - enable_stats_purge = $67, \ - stats_purge_frequency_hours = $68, \ - stats_purge_threshold_days = $69, \ - enrollment_token_timeout_hours = $70, \ - password_reset_token_timeout_hours = $71, \ - enrollment_session_timeout_minutes = $72, \ - password_reset_session_timeout_minutes = $73 \ + enrollment_send_welcome_email = $20, \ + uuid = $21, \ + ldap_url = $22, \ + ldap_bind_username = $23, \ + ldap_bind_password = $24, \ + ldap_group_search_base = $25, \ + ldap_user_search_base = $26, \ + ldap_user_obj_class = $27, \ + ldap_group_obj_class = $28, \ + ldap_username_attr = $29, \ + ldap_groupname_attr = $30, \ + ldap_group_member_attr = $31, \ + ldap_member_attr = $32, \ + ldap_use_starttls = $33, \ + ldap_tls_verify_cert = $34, \ + openid_create_account = $35, \ + license = $36, \ + gateway_disconnect_notifications_enabled = $37, \ + gateway_disconnect_notifications_inactivity_threshold = $38, \ + gateway_disconnect_notifications_reconnect_notification_enabled = $39, \ + ldap_sync_status = $40, \ + ldap_enabled = $41, \ + ldap_sync_enabled = $42, \ + ldap_is_authoritative = $43, \ + ldap_sync_interval = $44, \ + ldap_user_auxiliary_obj_classes = $45, \ + ldap_uses_ad = $46, \ + ldap_user_rdn_attr = $47, \ + ldap_sync_groups = $48, \ + openid_username_handling = $49, \ + ca_key_der = $50, \ + ca_cert_der = $51, \ + ca_expiry = $52, \ + defguard_url = $53, \ + default_admin_group_name = $54, \ + authentication_period_days = $55, \ + mfa_code_timeout_seconds = $56, \ + public_proxy_url = $57, \ + default_admin_id = $58, \ + secret_key = $59, \ + enable_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, @@ -599,14 +536,7 @@ impl Settings { self.enrollment_welcome_email, self.enrollment_welcome_email_subject, self.enrollment_use_welcome_message_as_email, - &self.enrollment_admin_email_mode as &EnrollmentAdminEmailMode, - self.enrollment_admin_custom_email, - self.enrollment_show_reset_password, - self.enrollment_show_welcome_message, self.enrollment_send_welcome_email, - &self.enrollment_windows_release_channel as &EnrollmentReleaseChannel, - &self.enrollment_linux_release_channel as &EnrollmentReleaseChannel, - &self.enrollment_macos_release_channel as &EnrollmentReleaseChannel, self.uuid, self.ldap_url, self.ldap_bind_username, diff --git a/migrations/20260323083929_[2.0.0]_enrollment_settings.down.sql b/migrations/20260323083929_[2.0.0]_enrollment_settings.down.sql index 5c55bf3994..ffe792ce73 100644 --- a/migrations/20260323083929_[2.0.0]_enrollment_settings.down.sql +++ b/migrations/20260323083929_[2.0.0]_enrollment_settings.down.sql @@ -1,12 +1,2 @@ ALTER TABLE settings - DROP COLUMN enrollment_macos_release_channel, - DROP COLUMN enrollment_linux_release_channel, - DROP COLUMN enrollment_windows_release_channel, - DROP COLUMN enrollment_send_welcome_email, - DROP COLUMN enrollment_show_welcome_message, - DROP COLUMN enrollment_show_reset_password, - DROP COLUMN enrollment_admin_custom_email, - DROP COLUMN enrollment_admin_email_mode; - -DROP TYPE enrollment_release_channel; -DROP TYPE enrollment_admin_email_mode; + DROP COLUMN enrollment_send_welcome_email; diff --git a/migrations/20260323083929_[2.0.0]_enrollment_settings.up.sql b/migrations/20260323083929_[2.0.0]_enrollment_settings.up.sql index db53c74e06..ebe6934a7b 100644 --- a/migrations/20260323083929_[2.0.0]_enrollment_settings.up.sql +++ b/migrations/20260323083929_[2.0.0]_enrollment_settings.up.sql @@ -1,21 +1,2 @@ -CREATE TYPE enrollment_admin_email_mode AS ENUM ( - 'initiating_admin', - 'hidden', - 'custom_email' -); - -CREATE TYPE enrollment_release_channel AS ENUM ( - 'stable', - 'beta', - 'alpha' -); - ALTER TABLE settings - ADD COLUMN enrollment_admin_email_mode enrollment_admin_email_mode NOT NULL DEFAULT 'initiating_admin', - ADD COLUMN enrollment_admin_custom_email TEXT NULL, - ADD COLUMN enrollment_show_reset_password BOOLEAN NOT NULL DEFAULT TRUE, - ADD COLUMN enrollment_show_welcome_message BOOLEAN NOT NULL DEFAULT TRUE, - ADD COLUMN enrollment_send_welcome_email BOOLEAN NOT NULL DEFAULT TRUE, - ADD COLUMN enrollment_windows_release_channel enrollment_release_channel NOT NULL DEFAULT 'stable', - ADD COLUMN enrollment_linux_release_channel enrollment_release_channel NOT NULL DEFAULT 'stable', - ADD COLUMN enrollment_macos_release_channel enrollment_release_channel NOT NULL DEFAULT 'stable'; + ADD COLUMN enrollment_send_welcome_email BOOLEAN NOT NULL DEFAULT TRUE; From 7d7a8f3e0ebbe180a54ab7c8cf346be3e7a051f7 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 24 Mar 2026 08:07:06 +0100 Subject: [PATCH 10/33] implement welcome email gating mechanism --- .../src/servers/enrollment.rs | 100 ++++++++++++++++-- 1 file changed, 89 insertions(+), 11 deletions(-) diff --git a/crates/defguard_proxy_manager/src/servers/enrollment.rs b/crates/defguard_proxy_manager/src/servers/enrollment.rs index fcf44e7979..2472cab39f 100644 --- a/crates/defguard_proxy_manager/src/servers/enrollment.rs +++ b/crates/defguard_proxy_manager/src/servers/enrollment.rs @@ -351,6 +351,31 @@ impl EnrollmentServer { Ok(()) } + async fn send_welcome_email_if_enabled( + &self, + enrollment: &Token, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + user: &User, + settings: &Settings, + ip_address: &str, + device_info: Option<&str>, + ) -> Result<(), Status> { + if !settings.enrollment_send_welcome_email { + info!( + "Skipping enrollment welcome email for user {} because it is disabled in settings", + user.username + ); + return Ok(()); + } + + debug!("Try to send welcome email..."); + enrollment + .send_welcome_email(transaction, user, settings, ip_address, device_info) + .await?; + + Ok(()) + } + #[instrument(skip_all)] pub(crate) async fn activate_user( &self, @@ -423,17 +448,15 @@ impl EnrollmentServer { let settings = Settings::get_current_settings(); debug!("Settings successfully retrieved."); - // send welcome email - debug!("Try to send welcome email..."); - enrollment - .send_welcome_email( - &mut transaction, - &user, - &settings, - &ip_address, - device_info.as_deref(), - ) - .await?; + self.send_welcome_email_if_enabled( + &enrollment, + &mut transaction, + &user, + &settings, + &ip_address, + device_info.as_deref(), + ) + .await?; // send success notification to admin debug!( @@ -1105,6 +1128,61 @@ mod test { }; use defguard_core::db::models::enrollment::{ENROLLMENT_TOKEN_TYPE, Token}; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; + use tokio::sync::{broadcast, mpsc::unbounded_channel}; + + use super::EnrollmentServer; + + #[sqlx::test] + async fn test_send_welcome_email_if_disabled_skips_mail( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + + let user = User::new( + "test_user_disabled_mail", + None, + "Test", + "User", + "user-disabled-mail@test.com", + None, + ) + .save(&pool) + .await + .unwrap(); + + let token = Token::new( + user.id, + None, + Some(user.email.clone()), + 10, + Some(ENROLLMENT_TOKEN_TYPE.to_string()), + ); + + Settings::initialize_runtime_defaults(&pool).await.unwrap(); + initialize_current_settings(&pool).await.unwrap(); + + let mut settings = Settings::get_current_settings(); + settings.enrollment_send_welcome_email = false; + + let (wireguard_tx, _) = broadcast::channel(1); + let (bidi_event_tx, _) = unbounded_channel(); + let server = EnrollmentServer::new(pool.clone(), wireguard_tx, bidi_event_tx); + + let mut transaction = pool.begin().await.unwrap(); + let result = server + .send_welcome_email_if_enabled( + &token, + &mut transaction, + &user, + &settings, + "127.0.0.1", + None, + ) + .await; + + assert!(result.is_ok()); + } #[ignore] #[sqlx::test] From e92d4d45a0521e0e2a99e82bcdc3e6e463a9e685 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 24 Mar 2026 08:17:34 +0100 Subject: [PATCH 11/33] remove unused translations --- web/messages/en/settings.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index e49554c4f7..6bbec9ec17 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -24,12 +24,9 @@ "settings_vpn_stats_toggle_title": "Stats purge", "settings_vpn_stats_label_purge_frequency": "Stats purge frequency", "settings_vpn_stats_label_purge_threshold": "Stats purge threshold", - "settings_password_reset_title": "Password reset", - "settings_password_reset_subtitle": "Configure token and session timeouts for 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 expiration", - "settings_enrollment_label_password_reset_session_expires_in": "Password reset session expires in", "settings_enrollment_page_title": "Enrollment", "settings_enrollment_page_subtitle": "Configure enrollment settings to control how users join your instance and manage the enrollment flow.", "settings_enrollment_tab_general": "General", @@ -49,7 +46,6 @@ "settings_enrollment_section_duration_description": "Configure the expiration time of the unique token sent to a newly added user. This token is used to activate the account, and in this section administrators can control how long the token remains valid.", "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_enrollment_content": "Configure enrollment settings to define how users are invited, registered, and onboarded into your instance.", "settings_duration_one_day": "1 day", "settings_duration_days": "{days} days", "settings_duration_one_hour": "1 hour", From 27ba0af68cae2ee996fa618349054255782d050f Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 24 Mar 2026 08:33:15 +0100 Subject: [PATCH 12/33] fix enrollment form structure --- .../pages/EnrollmentPage/EnrollmentPage.tsx | 123 +++++++++--------- 1 file changed, 61 insertions(+), 62 deletions(-) diff --git a/web/src/pages/EnrollmentPage/EnrollmentPage.tsx b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx index 3ba7f967ec..2272f707d9 100644 --- a/web/src/pages/EnrollmentPage/EnrollmentPage.tsx +++ b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx @@ -105,7 +105,6 @@ export const EnrollmentPage = () => { - {activeTab === EnrollmentPageTab.General && ( <> @@ -116,9 +115,7 @@ export const EnrollmentPage = () => { /> {isPresent(settings) && ( - - - + )} )} @@ -212,65 +209,67 @@ const GeneralTabContent = ({ settings }: { settings: Settings }) => { }); return ( -
{ - e.stopPropagation(); - e.preventDefault(); - form.handleSubmit(); - }} - > - - - - - {(field) => ( - - )} - - - - {(field) => ( - - )} - - - - ({ - isDefault: state.isDefaultValue || state.isPristine, - isSubmitting: state.isSubmitting, - canSubmit: state.canSubmit, - })} + + { + e.stopPropagation(); + e.preventDefault(); + form.handleSubmit(); + }} > - {({ isDefault, isSubmitting, canSubmit }) => ( - -
-
-
- )} -
-
+ + + + + {(field) => ( + + )} + + + + {(field) => ( + + )} + + + + ({ + isDefault: state.isDefaultValue || state.isPristine, + isSubmitting: state.isSubmitting, + canSubmit: state.canSubmit, + })} + > + {({ isDefault, isSubmitting, canSubmit }) => ( + +
+
+
+ )} +
+ + ); }; From bddb2e53e47191480e4cb4c56ee69e76ed5862c9 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 24 Mar 2026 09:14:59 +0100 Subject: [PATCH 13/33] fix message templates form layout --- web/messages/en/settings.json | 2 + .../pages/EnrollmentPage/EnrollmentPage.tsx | 165 ++++++++++-------- web/src/pages/EnrollmentPage/style.scss | 44 ++++- 3 files changed, 135 insertions(+), 76 deletions(-) diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index 6bbec9ec17..6350a448bc 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -34,6 +34,8 @@ "settings_enrollment_general_title": "Enrollment settings", "settings_enrollment_message_templates_title": "Welcome message", "settings_enrollment_message_templates_subtitle": "This message will be shown at the end of enrollment process. Enrollment is a process by which a new employee will be able to activate their new account, create a password and configure a VPN device.", + "settings_enrollment_template_display_message_title": "Welcome message", + "settings_enrollment_template_display_message_description": "This information will be displayed for user in service once enrollment is completed. We advise to insert links and explain next steps briefly.", "settings_enrollment_template_message_label": "Welcome message", "settings_enrollment_template_send_email_title": "Send welcome email", "settings_enrollment_template_send_email_description": "This information will be sent to user email once enrollment is completed. We advise to insert links and explain next steps briefly.", diff --git a/web/src/pages/EnrollmentPage/EnrollmentPage.tsx b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx index 2272f707d9..06c5155ea0 100644 --- a/web/src/pages/EnrollmentPage/EnrollmentPage.tsx +++ b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx @@ -114,9 +114,7 @@ export const EnrollmentPage = () => { subtitle={m.settings_enrollment_page_subtitle()} /> - {isPresent(settings) && ( - - )} + {isPresent(settings) && } )} {activeTab === EnrollmentPageTab.MessageTemplates && ( @@ -326,93 +324,112 @@ const MessageTemplatesTabContent = ({ settings }: { settings: Settings }) => { }} > - - - {(field) => ( - - )} - - - - +
+
+
+
+ + {m.settings_enrollment_template_display_message_title()} + + + {m.settings_enrollment_template_display_message_description()} + +
+ + + {(field) => ( + + )} + +
+
+
+ +
+
{(field) => ( - )} - - state.values.enrollment_send_welcome_email} - > - {(sendWelcomeEmail) => ( - - - - {(field) => ( - - )} - - -
- - {(field) => ( - - )} - -
- + > - state.values.enrollment_use_welcome_message_as_email - } + selector={(state) => state.values.enrollment_send_welcome_email} > - {(sameAsWelcomeMessage) => ( - <> - -
- ( + + + + {(field) => ( + -

- {m.settings_enrollment_template_same_as_message_banner()} -

-
-
- - - + )} + + +
+ {(field) => ( - )} - - +
+ + + state.values.enrollment_use_welcome_message_as_email + } + > + {(sameAsWelcomeMessage) => ( + <> + +
+ +

+ {m.settings_enrollment_template_same_as_message_banner()} +

+
+
+ + + + {(field) => ( + + )} + + + + )} +
+
)}
-
+ )} -
- + +
diff --git a/web/src/pages/EnrollmentPage/style.scss b/web/src/pages/EnrollmentPage/style.scss index baa4478f03..083db9ac30 100644 --- a/web/src/pages/EnrollmentPage/style.scss +++ b/web/src/pages/EnrollmentPage/style.scss @@ -6,6 +6,46 @@ align-items: start; } + .message-template-section { + display: flex; + flex-flow: column; + } + + .message-template-section-offset { + display: grid; + grid-template-columns: 36px minmax(0, 1fr); + column-gap: var(--spacing-lg); + } + + .message-template-offset-spacer { + width: 36px; + } + + .message-template-offset-content { + min-width: 0; + } + + .message-template-offset-divider { + padding-left: calc(36px + var(--spacing-lg)); + } + + .message-template-static-header { + display: flex; + flex-flow: column; + row-gap: var(--spacing-xs); + } + + .message-template-toggle { + .grid { + grid-template-columns: 36px 1fr; + column-gap: var(--spacing-lg); + } + + .content { + max-width: 780px; + } + } + .message-templates-sidebar { display: flex; flex-flow: column; @@ -18,9 +58,9 @@ } .sidebar-panel { - border: var(--border-1) solid var(--border-disabled); + border: none; border-radius: var(--radius-lg); - background-color: var(--bg-default); + background-color: var(--bg-emphasis); padding: var(--spacing-lg); display: flex; flex-flow: column; From c3f70464775b26e9140bf7745e5b606fff730a42 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 24 Mar 2026 10:55:23 +0100 Subject: [PATCH 14/33] fix narrow viewport display --- web/src/pages/EnrollmentPage/style.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/pages/EnrollmentPage/style.scss b/web/src/pages/EnrollmentPage/style.scss index 083db9ac30..704f2a146f 100644 --- a/web/src/pages/EnrollmentPage/style.scss +++ b/web/src/pages/EnrollmentPage/style.scss @@ -1,9 +1,10 @@ #enrollment-page { .message-templates-layout { display: grid; - grid-template-columns: minmax(0, 1fr) 373px; + grid-template-columns: minmax(720px, 1fr) 373px; gap: var(--spacing-4xl); align-items: start; + min-width: calc(720px + 373px + var(--spacing-4xl)); } .message-template-section { From 964283002a3bc02c3b5d7ab8d1dbb7e5d9a9e7b0 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 24 Mar 2026 11:05:56 +0100 Subject: [PATCH 15/33] markdown helper box tweaks --- .../pages/EnrollmentPage/EnrollmentPage.tsx | 92 ++++++++++++------- web/src/pages/EnrollmentPage/style.scss | 29 +++++- 2 files changed, 84 insertions(+), 37 deletions(-) diff --git a/web/src/pages/EnrollmentPage/EnrollmentPage.tsx b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx index 06c5155ea0..2c00c81783 100644 --- a/web/src/pages/EnrollmentPage/EnrollmentPage.tsx +++ b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx @@ -51,29 +51,43 @@ const enrollmentSessionTimeoutBaseOptions = createNumericSelectOptions({ 60: m.settings_duration_one_hour(), }); -const messageTemplatesHelpVariables = `{{ first_name }} - newly created user first name -{{ last_name }} - newly created user last name -{{ username }} - newly created user username/login -{{ admin_first_name }} - first name of the administrator who initiated the enrollment process -{{ admin_last_name }} - last name of the administrator who initiated the enrollment process -{{ admin_phone }} - phone number of the administrator who initiated the enrollment process -{{ admin_email }} - email of the administrator who initiated the enrollment process -{{ defguard_url }} - internal Defguard URL (your Defguard instance address)`; +const messageTemplatesHelpVariables = [ + ['{{ first_name }}', 'newly created user first name'], + ['{{ last_name }}', 'newly created user last name'], + ['{{ username }}', 'newly created user username/login'], + [ + '{{ admin_first_name }}', + 'first name of the administrator who initiated the enrollment process', + ], + [ + '{{ admin_last_name }}', + 'last name of the administrator who initiated the enrollment process', + ], + [ + '{{ admin_phone }}', + 'phone number of the administrator who initiated the enrollment process', + ], + [ + '{{ admin_email }}', + 'email of the administrator who initiated the enrollment process', + ], + ['{{ defguard_url }}', 'internal Defguard URL (your Defguard instance address)'], +] as const; const messageTemplatesHelpMarkdown = [ - '#, ##, ### - Create headings.', - '*text* - Italic text.', - '**text** - Bold text.', - '***text*** - Bold and italic.', - '> text - Blockquote.', - '- item or 1. item - Lists (unordered or ordered).', - '`code` - Inline code.', - '```code``` - Code block.', - '*** - Horizontal line.', - '[text](url) - Link.', - '| and --- - Create tables.', - '\\ - Escape special characters.', -].join('\n'); + ['#, ##, ###', 'Create headings.', 'medium'], + ['*text*', 'Italic text.'], + ['**text**', 'Bold text.'], + ['***text***', 'Bold and italic.'], + ['> text', 'Blockquote.'], + ['- item or 1. item', 'Lists (unordered or ordered).'], + ['`code`', 'Inline code.'], + ['```code```', 'Code block.'], + ['***', 'Horizontal line.'], + ['[text](url)', 'Link.'], + ['| and ---', 'Create tables.'], + ['\\', 'Escape special characters.'], +] as const; export const EnrollmentPage = () => { const [activeTab, setActiveTab] = useState( @@ -475,21 +489,29 @@ const MessageTemplatesHelpPanel = () => {
- - {messageTemplatesHelpVariables} - +
    + {messageTemplatesHelpVariables.map(([token, description]) => ( +
  • + {token} + - + {description} +
  • + ))} +
- - {messageTemplatesHelpMarkdown} - +
    + {messageTemplatesHelpMarkdown.map(([token, description, weight]) => ( +
  • + + {token} + + - + {description} +
  • + ))} +
); diff --git a/web/src/pages/EnrollmentPage/style.scss b/web/src/pages/EnrollmentPage/style.scss index 704f2a146f..28aca0fad2 100644 --- a/web/src/pages/EnrollmentPage/style.scss +++ b/web/src/pages/EnrollmentPage/style.scss @@ -68,8 +68,33 @@ row-gap: var(--spacing-lg); } - .sidebar-copy { - white-space: pre-line; + .sidebar-list { + list-style: disc; + padding-left: 18px; + margin: 0; + display: flex; + flex-flow: column; + row-gap: var(--spacing-xs); + font: var(--t-body-sm-400); + color: var(--fg-faded); + + li { + line-height: 20px; + } + } + + .sidebar-token { + font: var(--t-body-sm-600); + color: var(--fg-faded); + } + + .sidebar-token-medium { + font: var(--t-body-sm-500); + color: var(--fg-faded); + } + + .sidebar-separator { + color: var(--fg-faded); } } From fb22f651a67a55c2750a64aae6c73c18b030723a Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 24 Mar 2026 11:52:43 +0100 Subject: [PATCH 16/33] add log message --- crates/defguard_proxy_manager/src/servers/enrollment.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/defguard_proxy_manager/src/servers/enrollment.rs b/crates/defguard_proxy_manager/src/servers/enrollment.rs index 2472cab39f..edc7a27e04 100644 --- a/crates/defguard_proxy_manager/src/servers/enrollment.rs +++ b/crates/defguard_proxy_manager/src/servers/enrollment.rs @@ -372,6 +372,7 @@ impl EnrollmentServer { enrollment .send_welcome_email(transaction, user, settings, ip_address, device_info) .await?; + info!("Welcome email sent to {} at {}", user.username, user.email); Ok(()) } From cfb801a995203f8c706e7260bef81869678ce807 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 24 Mar 2026 11:59:43 +0100 Subject: [PATCH 17/33] refactor EnrollmentPage into separate tab components --- web/messages/en/settings.json | 2 +- .../pages/EnrollmentPage/EnrollmentPage.tsx | 476 +----------------- .../pages/EnrollmentPage/tabs/GeneralTab.tsx | 181 +++++++ .../tabs/MessageTemplatesTab.tsx | 322 ++++++++++++ 4 files changed, 509 insertions(+), 472 deletions(-) create mode 100644 web/src/pages/EnrollmentPage/tabs/GeneralTab.tsx create mode 100644 web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index 6350a448bc..5a266510c4 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -34,7 +34,7 @@ "settings_enrollment_general_title": "Enrollment settings", "settings_enrollment_message_templates_title": "Welcome message", "settings_enrollment_message_templates_subtitle": "This message will be shown at the end of enrollment process. Enrollment is a process by which a new employee will be able to activate their new account, create a password and configure a VPN device.", - "settings_enrollment_template_display_message_title": "Welcome message", + "settings_enrollment_template_display_message_title": "Display welcome message", "settings_enrollment_template_display_message_description": "This information will be displayed for user in service once enrollment is completed. We advise to insert links and explain next steps briefly.", "settings_enrollment_template_message_label": "Welcome message", "settings_enrollment_template_send_email_title": "Send welcome email", diff --git a/web/src/pages/EnrollmentPage/EnrollmentPage.tsx b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx index 2c00c81783..85fb8c7688 100644 --- a/web/src/pages/EnrollmentPage/EnrollmentPage.tsx +++ b/web/src/pages/EnrollmentPage/EnrollmentPage.tsx @@ -1,35 +1,13 @@ -import { useMutation, useQuery } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; -import z from 'zod'; import './style.scss'; import { m } from '../../paraglide/messages'; -import api from '../../shared/api/api'; -import type { Settings } from '../../shared/api/types'; -import { Controls } from '../../shared/components/Controls/Controls'; 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 { AppText } from '../../shared/defguard-ui/components/AppText/AppText'; -import { Button } from '../../shared/defguard-ui/components/Button/Button'; -import { Divider } from '../../shared/defguard-ui/components/Divider/Divider'; -import { Fold } from '../../shared/defguard-ui/components/Fold/Fold'; -import { Icon } from '../../shared/defguard-ui/components/Icon'; -import { MarkedSection } from '../../shared/defguard-ui/components/MarkedSection/MarkedSection'; -import { MarkedSectionHeader } from '../../shared/defguard-ui/components/MarkedSectionHeader/MarkedSectionHeader'; import { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox'; import { Tabs } from '../../shared/defguard-ui/components/Tabs/Tabs'; import type { TabsItem } from '../../shared/defguard-ui/components/Tabs/types'; -import { Snackbar } from '../../shared/defguard-ui/providers/snackbar/snackbar'; -import { TextStyle, ThemeSpacing, ThemeVariable } from '../../shared/defguard-ui/types'; -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'; +import { ThemeSpacing } from '../../shared/defguard-ui/types'; +import { GeneralTab } from './tabs/GeneralTab'; +import { MessageTemplatesTab } from './tabs/MessageTemplatesTab'; const EnrollmentPageTab = { General: 'general', @@ -38,62 +16,10 @@ const EnrollmentPageTab = { type EnrollmentTabValue = (typeof EnrollmentPageTab)[keyof typeof EnrollmentPageTab]; -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: m.settings_duration_minutes({ minutes: 10 }), - 30: m.settings_duration_minutes({ minutes: 30 }), - 60: m.settings_duration_one_hour(), -}); - -const messageTemplatesHelpVariables = [ - ['{{ first_name }}', 'newly created user first name'], - ['{{ last_name }}', 'newly created user last name'], - ['{{ username }}', 'newly created user username/login'], - [ - '{{ admin_first_name }}', - 'first name of the administrator who initiated the enrollment process', - ], - [ - '{{ admin_last_name }}', - 'last name of the administrator who initiated the enrollment process', - ], - [ - '{{ admin_phone }}', - 'phone number of the administrator who initiated the enrollment process', - ], - [ - '{{ admin_email }}', - 'email of the administrator who initiated the enrollment process', - ], - ['{{ defguard_url }}', 'internal Defguard URL (your Defguard instance address)'], -] as const; - -const messageTemplatesHelpMarkdown = [ - ['#, ##, ###', 'Create headings.', 'medium'], - ['*text*', 'Italic text.'], - ['**text**', 'Bold text.'], - ['***text***', 'Bold and italic.'], - ['> text', 'Blockquote.'], - ['- item or 1. item', 'Lists (unordered or ordered).'], - ['`code`', 'Inline code.'], - ['```code```', 'Code block.'], - ['***', 'Horizontal line.'], - ['[text](url)', 'Link.'], - ['| and ---', 'Create tables.'], - ['\\', 'Escape special characters.'], -] as const; - export const EnrollmentPage = () => { const [activeTab, setActiveTab] = useState( EnrollmentPageTab.General, ); - const { data: settings } = useQuery(getSettingsQueryOptions); const tabs = useMemo( (): TabsItem[] => [ @@ -119,400 +45,8 @@ export const EnrollmentPage = () => { - - {activeTab === EnrollmentPageTab.General && ( - <> - - - {isPresent(settings) && } - - )} - {activeTab === EnrollmentPageTab.MessageTemplates && ( - <> - - - {isPresent(settings) && } - - )} - + {activeTab === EnrollmentPageTab.General && } + {activeTab === EnrollmentPageTab.MessageTemplates && } ); }; - -const generalTabFormSchema = z.object({ - enrollment_token_timeout_hours: z.number(m.form_error_required()).int().min(1), - enrollment_session_timeout_minutes: z.number(m.form_error_required()).int().min(1), -}); - -type GeneralTabFormFields = z.infer; - -const messageTemplatesFormSchema = z.object({ - enrollment_welcome_message: z.string(), - enrollment_send_welcome_email: z.boolean(), - enrollment_welcome_email_subject: z.string().min(1, m.form_error_required()), - enrollment_use_welcome_message_as_email: z.boolean(), - enrollment_welcome_email: z.string(), -}); - -type MessageTemplatesFormFields = z.infer; - -const GeneralTabContent = ({ settings }: { settings: Settings }) => { - const { mutateAsync } = useMutation({ - mutationFn: api.settings.patchSettings, - meta: { - invalidate: ['settings'], - }, - onSuccess: () => { - Snackbar.default(m.settings_msg_saved()); - }, - onError: () => { - Snackbar.error(m.settings_msg_save_failed()); - }, - }); - - const defaultValues = useMemo( - (): GeneralTabFormFields => ({ - enrollment_token_timeout_hours: settings.enrollment_token_timeout_hours ?? 24, - enrollment_session_timeout_minutes: - settings.enrollment_session_timeout_minutes ?? 10, - }), - [settings], - ); - - const enrollmentTokenTimeoutOptions = useMemo( - () => - withNumericFallbackOption( - enrollmentTokenTimeoutBaseOptions, - defaultValues.enrollment_token_timeout_hours, - 'hours', - ), - [defaultValues.enrollment_token_timeout_hours], - ); - - const enrollmentSessionTimeoutOptions = useMemo( - () => - withNumericFallbackOption( - enrollmentSessionTimeoutBaseOptions, - defaultValues.enrollment_session_timeout_minutes, - 'minutes', - ), - [defaultValues.enrollment_session_timeout_minutes], - ); - - const form = useAppForm({ - defaultValues, - validationLogic: formChangeLogic, - validators: { - onSubmit: generalTabFormSchema, - onChange: generalTabFormSchema, - }, - onSubmit: async ({ value }) => { - await mutateAsync(value); - form.reset(value); - }, - }); - - return ( - -
{ - e.stopPropagation(); - e.preventDefault(); - form.handleSubmit(); - }} - > - - - - - {(field) => ( - - )} - - - - {(field) => ( - - )} - - - - ({ - isDefault: state.isDefaultValue || state.isPristine, - isSubmitting: state.isSubmitting, - canSubmit: state.canSubmit, - })} - > - {({ isDefault, isSubmitting, canSubmit }) => ( - -
-
-
- )} -
-
-
- ); -}; - -const MessageTemplatesTabContent = ({ settings }: { settings: Settings }) => { - const { mutateAsync } = useMutation({ - mutationFn: api.settings.patchSettings, - meta: { - invalidate: ['settings'], - }, - onSuccess: () => { - Snackbar.default(m.settings_msg_saved()); - }, - onError: () => { - Snackbar.error(m.settings_msg_save_failed()); - }, - }); - - const defaultValues = useMemo( - (): MessageTemplatesFormFields => ({ - enrollment_welcome_message: settings.enrollment_welcome_message ?? '', - enrollment_send_welcome_email: settings.enrollment_send_welcome_email ?? true, - enrollment_welcome_email_subject: settings.enrollment_welcome_email_subject ?? '', - enrollment_use_welcome_message_as_email: - settings.enrollment_use_welcome_message_as_email ?? true, - enrollment_welcome_email: settings.enrollment_welcome_email ?? '', - }), - [settings], - ); - - const form = useAppForm({ - defaultValues, - validationLogic: formChangeLogic, - validators: { - onSubmit: messageTemplatesFormSchema, - onChange: messageTemplatesFormSchema, - }, - onSubmit: async ({ value }) => { - await mutateAsync(value); - form.reset(value); - }, - }); - - return ( -
- -
{ - e.stopPropagation(); - e.preventDefault(); - form.handleSubmit(); - }} - > - -
-
-
-
- - {m.settings_enrollment_template_display_message_title()} - - - {m.settings_enrollment_template_display_message_description()} - -
- - - {(field) => ( - - )} - -
-
-
- -
-
- - {(field) => ( - - state.values.enrollment_send_welcome_email} - > - {(sendWelcomeEmail) => ( - - - - {(field) => ( - - )} - - -
- - {(field) => ( - - )} - -
- - - state.values.enrollment_use_welcome_message_as_email - } - > - {(sameAsWelcomeMessage) => ( - <> - -
- -

- {m.settings_enrollment_template_same_as_message_banner()} -

-
-
- - - - {(field) => ( - - )} - - - - )} -
-
- )} -
-
- )} -
-
- - - - - ({ - isDefault: state.isDefaultValue || state.isPristine, - isSubmitting: state.isSubmitting, - canSubmit: state.canSubmit, - })} - > - {({ isDefault, isSubmitting, canSubmit }) => ( - -
-
-
- )} -
- - - -
- ); -}; - -const MessageTemplatesHelpPanel = () => { - return ( -
-
- - - {m.settings_enrollment_template_help_title()} - -
-
-
    - {messageTemplatesHelpVariables.map(([token, description]) => ( -
  • - {token} - - - {description} -
  • - ))} -
- -
    - {messageTemplatesHelpMarkdown.map(([token, description, weight]) => ( -
  • - - {token} - - - - {description} -
  • - ))} -
-
-
- ); -}; diff --git a/web/src/pages/EnrollmentPage/tabs/GeneralTab.tsx b/web/src/pages/EnrollmentPage/tabs/GeneralTab.tsx new file mode 100644 index 0000000000..efc81240ba --- /dev/null +++ b/web/src/pages/EnrollmentPage/tabs/GeneralTab.tsx @@ -0,0 +1,181 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; +import z from 'zod'; +import { m } from '../../../paraglide/messages'; +import api from '../../../shared/api/api'; +import type { Settings } from '../../../shared/api/types'; +import { Controls } from '../../../shared/components/Controls/Controls'; +import { SettingsCard } from '../../../shared/components/SettingsCard/SettingsCard'; +import { SettingsHeader } from '../../../shared/components/SettingsHeader/SettingsHeader'; +import { SettingsLayout } from '../../../shared/components/SettingsLayout/SettingsLayout'; +import { Button } from '../../../shared/defguard-ui/components/Button/Button'; +import { MarkedSection } from '../../../shared/defguard-ui/components/MarkedSection/MarkedSection'; +import { MarkedSectionHeader } from '../../../shared/defguard-ui/components/MarkedSectionHeader/MarkedSectionHeader'; +import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedBox'; +import { Snackbar } from '../../../shared/defguard-ui/providers/snackbar/snackbar'; +import { ThemeSpacing } from '../../../shared/defguard-ui/types'; +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 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: m.settings_duration_minutes({ minutes: 10 }), + 30: m.settings_duration_minutes({ minutes: 30 }), + 60: m.settings_duration_one_hour(), +}); + +const generalTabFormSchema = z.object({ + enrollment_token_timeout_hours: z.number(m.form_error_required()).int().min(1), + enrollment_session_timeout_minutes: z.number(m.form_error_required()).int().min(1), +}); + +type GeneralTabFormFields = z.infer; + +export const GeneralTab = () => { + const { data: settings } = useQuery(getSettingsQueryOptions); + + return ( + + + + {isPresent(settings) && } + + ); +}; + +const GeneralTabContent = ({ settings }: { settings: Settings }) => { + const { mutateAsync } = useMutation({ + mutationFn: api.settings.patchSettings, + meta: { + invalidate: ['settings'], + }, + onSuccess: () => { + Snackbar.default(m.settings_msg_saved()); + }, + onError: () => { + Snackbar.error(m.settings_msg_save_failed()); + }, + }); + + const defaultValues = useMemo( + (): GeneralTabFormFields => ({ + enrollment_token_timeout_hours: settings.enrollment_token_timeout_hours ?? 24, + enrollment_session_timeout_minutes: + settings.enrollment_session_timeout_minutes ?? 10, + }), + [settings], + ); + + const enrollmentTokenTimeoutOptions = useMemo( + () => + withNumericFallbackOption( + enrollmentTokenTimeoutBaseOptions, + defaultValues.enrollment_token_timeout_hours, + 'hours', + ), + [defaultValues.enrollment_token_timeout_hours], + ); + + const enrollmentSessionTimeoutOptions = useMemo( + () => + withNumericFallbackOption( + enrollmentSessionTimeoutBaseOptions, + defaultValues.enrollment_session_timeout_minutes, + 'minutes', + ), + [defaultValues.enrollment_session_timeout_minutes], + ); + + const form = useAppForm({ + defaultValues, + validationLogic: formChangeLogic, + validators: { + onSubmit: generalTabFormSchema, + onChange: generalTabFormSchema, + }, + onSubmit: async ({ value }) => { + await mutateAsync(value); + form.reset(value); + }, + }); + + return ( + +
{ + e.stopPropagation(); + e.preventDefault(); + form.handleSubmit(); + }} + > + + + + + {(field) => ( + + )} + + + + {(field) => ( + + )} + + + + ({ + isDefault: state.isDefaultValue || state.isPristine, + isSubmitting: state.isSubmitting, + canSubmit: state.canSubmit, + })} + > + {({ isDefault, isSubmitting, canSubmit }) => ( + +
+
+
+ )} +
+
+
+ ); +}; diff --git a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx new file mode 100644 index 0000000000..4a744ac881 --- /dev/null +++ b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx @@ -0,0 +1,322 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; +import z from 'zod'; +import { m } from '../../../paraglide/messages'; +import api from '../../../shared/api/api'; +import type { Settings } from '../../../shared/api/types'; +import { Controls } from '../../../shared/components/Controls/Controls'; +import { SettingsCard } from '../../../shared/components/SettingsCard/SettingsCard'; +import { SettingsHeader } from '../../../shared/components/SettingsHeader/SettingsHeader'; +import { AppText } from '../../../shared/defguard-ui/components/AppText/AppText'; +import { Button } from '../../../shared/defguard-ui/components/Button/Button'; +import { Divider } from '../../../shared/defguard-ui/components/Divider/Divider'; +import { Fold } from '../../../shared/defguard-ui/components/Fold/Fold'; +import { Icon } from '../../../shared/defguard-ui/components/Icon'; +import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedBox'; +import { Snackbar } from '../../../shared/defguard-ui/providers/snackbar/snackbar'; +import { + TextStyle, + ThemeSpacing, + ThemeVariable, +} from '../../../shared/defguard-ui/types'; +import { isPresent } from '../../../shared/defguard-ui/utils/isPresent'; +import { useAppForm } from '../../../shared/form'; +import { formChangeLogic } from '../../../shared/formLogic'; +import { getSettingsQueryOptions } from '../../../shared/query'; + +const messageTemplatesHelpVariables = [ + ['{{ first_name }}', 'newly created user first name'], + ['{{ last_name }}', 'newly created user last name'], + ['{{ username }}', 'newly created user username/login'], + [ + '{{ admin_first_name }}', + 'first name of the administrator who initiated the enrollment process', + ], + [ + '{{ admin_last_name }}', + 'last name of the administrator who initiated the enrollment process', + ], + [ + '{{ admin_phone }}', + 'phone number of the administrator who initiated the enrollment process', + ], + [ + '{{ admin_email }}', + 'email of the administrator who initiated the enrollment process', + ], + ['{{ defguard_url }}', 'internal Defguard URL (your Defguard instance address)'], +] as const; + +const messageTemplatesHelpMarkdown = [ + ['#, ##, ###', 'Create headings.', 'medium'], + ['*text*', 'Italic text.'], + ['**text**', 'Bold text.'], + ['***text***', 'Bold and italic.'], + ['> text', 'Blockquote.'], + ['- item or 1. item', 'Lists (unordered or ordered).'], + ['`code`', 'Inline code.'], + ['```code```', 'Code block.'], + ['***', 'Horizontal line.'], + ['[text](url)', 'Link.'], + ['| and ---', 'Create tables.'], + ['\\', 'Escape special characters.'], +] as const; + +const messageTemplatesFormSchema = z.object({ + enrollment_welcome_message: z.string(), + enrollment_send_welcome_email: z.boolean(), + enrollment_welcome_email_subject: z.string().min(1, m.form_error_required()), + enrollment_use_welcome_message_as_email: z.boolean(), + enrollment_welcome_email: z.string(), +}); + +type MessageTemplatesFormFields = z.infer; + +export const MessageTemplatesTab = () => { + const { data: settings } = useQuery(getSettingsQueryOptions); + + if (!isPresent(settings)) { + return null; + } + + return ; +}; + +const MessageTemplatesTabContent = ({ settings }: { settings: Settings }) => { + const { mutateAsync } = useMutation({ + mutationFn: api.settings.patchSettings, + meta: { + invalidate: ['settings'], + }, + onSuccess: () => { + Snackbar.default(m.settings_msg_saved()); + }, + onError: () => { + Snackbar.error(m.settings_msg_save_failed()); + }, + }); + + const defaultValues = useMemo( + (): MessageTemplatesFormFields => ({ + enrollment_welcome_message: settings.enrollment_welcome_message ?? '', + enrollment_send_welcome_email: settings.enrollment_send_welcome_email ?? true, + enrollment_welcome_email_subject: settings.enrollment_welcome_email_subject ?? '', + enrollment_use_welcome_message_as_email: + settings.enrollment_use_welcome_message_as_email ?? true, + enrollment_welcome_email: settings.enrollment_welcome_email ?? '', + }), + [settings], + ); + + const form = useAppForm({ + defaultValues, + validationLogic: formChangeLogic, + validators: { + onSubmit: messageTemplatesFormSchema, + onChange: messageTemplatesFormSchema, + }, + onSubmit: async ({ value }) => { + await mutateAsync(value); + form.reset(value); + }, + }); + + return ( +
+
+ + +
{ + e.stopPropagation(); + e.preventDefault(); + form.handleSubmit(); + }} + > + +
+
+
+
+ + {m.settings_enrollment_template_display_message_title()} + + + {m.settings_enrollment_template_display_message_description()} + +
+ + + {(field) => ( + + )} + +
+
+
+ +
+
+ + {(field) => ( + + state.values.enrollment_send_welcome_email} + > + {(sendWelcomeEmail) => ( + + + + {(field) => ( + + )} + + +
+ + {(field) => ( + + )} + +
+ + + state.values.enrollment_use_welcome_message_as_email + } + > + {(sameAsWelcomeMessage) => ( + <> + +
+ +

+ {m.settings_enrollment_template_same_as_message_banner()} +

+
+
+ + + + {(field) => ( + + )} + + + + )} +
+
+ )} +
+
+ )} +
+
+ + + + + ({ + isDefault: state.isDefaultValue || state.isPristine, + isSubmitting: state.isSubmitting, + canSubmit: state.canSubmit, + })} + > + {({ isDefault, isSubmitting, canSubmit }) => ( + +
+
+
+ )} +
+ + +
+ +
+ ); +}; + +const MessageTemplatesHelpPanel = () => { + return ( +
+
+ + + {m.settings_enrollment_template_help_title()} + +
+
+
    + {messageTemplatesHelpVariables.map(([token, description]) => ( +
  • + {token} + - + {description} +
  • + ))} +
+ +
    + {messageTemplatesHelpMarkdown.map(([token, description, weight]) => ( +
  • + + {token} + + - + {description} +
  • + ))} +
+
+
+ ); +}; From ac3ef77953b441a0e69892a27bf133fa6b70ee61 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 24 Mar 2026 12:39:40 +0100 Subject: [PATCH 18/33] fix messages width after refactoring --- web/src/pages/EnrollmentPage/style.scss | 12 + .../tabs/MessageTemplatesTab.tsx | 304 +++++++++--------- 2 files changed, 167 insertions(+), 149 deletions(-) diff --git a/web/src/pages/EnrollmentPage/style.scss b/web/src/pages/EnrollmentPage/style.scss index 28aca0fad2..34a2808af3 100644 --- a/web/src/pages/EnrollmentPage/style.scss +++ b/web/src/pages/EnrollmentPage/style.scss @@ -1,12 +1,24 @@ #enrollment-page { + .message-templates-content { + grid-column: 1 / 12; + } + .message-templates-layout { display: grid; grid-template-columns: minmax(720px, 1fr) 373px; gap: var(--spacing-4xl); align-items: start; + width: 100%; + max-width: calc(1000px + 373px + var(--spacing-4xl)); min-width: calc(720px + 373px + var(--spacing-4xl)); } + .message-templates-left { + display: flex; + flex-flow: column; + min-width: 0; + } + .message-template-section { display: flex; flex-flow: column; diff --git a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx index 4a744ac881..84b0f29617 100644 --- a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx +++ b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx @@ -7,6 +7,7 @@ import type { Settings } from '../../../shared/api/types'; import { Controls } from '../../../shared/components/Controls/Controls'; import { SettingsCard } from '../../../shared/components/SettingsCard/SettingsCard'; import { SettingsHeader } from '../../../shared/components/SettingsHeader/SettingsHeader'; +import { SettingsLayout } from '../../../shared/components/SettingsLayout/SettingsLayout'; import { AppText } from '../../../shared/defguard-ui/components/AppText/AppText'; import { Button } from '../../../shared/defguard-ui/components/Button/Button'; import { Divider } from '../../../shared/defguard-ui/components/Divider/Divider'; @@ -122,164 +123,169 @@ const MessageTemplatesTabContent = ({ settings }: { settings: Settings }) => { }); return ( -
-
- - -
{ - e.stopPropagation(); - e.preventDefault(); - form.handleSubmit(); - }} - > - -
-
-
-
- - {m.settings_enrollment_template_display_message_title()} - - - {m.settings_enrollment_template_display_message_description()} - + +
+
+ + + { + e.stopPropagation(); + e.preventDefault(); + form.handleSubmit(); + }} + > + +
+
+
+
+ + {m.settings_enrollment_template_display_message_title()} + + + {m.settings_enrollment_template_display_message_description()} + +
+ + + {(field) => ( + + )} +
- - - {(field) => ( - - )} -
-
-
- -
-
- - {(field) => ( - - state.values.enrollment_send_welcome_email} +
+ +
+
+ + {(field) => ( + - {(sendWelcomeEmail) => ( - - - - {(field) => ( - - )} - - -
- + state.values.enrollment_send_welcome_email} + > + {(sendWelcomeEmail) => ( + + + {(field) => ( - )} -
- - - state.values.enrollment_use_welcome_message_as_email - } - > - {(sameAsWelcomeMessage) => ( - <> - -
- -

- {m.settings_enrollment_template_same_as_message_banner()} -

-
-
- - - - {(field) => ( - +
+ + {(field) => ( + + )} + +
+ + + state.values.enrollment_use_welcome_message_as_email + } + > + {(sameAsWelcomeMessage) => ( + <> + +
+ - )} - - - - )} - - - )} - - - )} - -
- - - - - ({ - isDefault: state.isDefaultValue || state.isPristine, - isSubmitting: state.isSubmitting, - canSubmit: state.canSubmit, - })} - > - {({ isDefault, isSubmitting, canSubmit }) => ( - -
-
-
- )} -
- - +

+ {m.settings_enrollment_template_same_as_message_banner()} +

+
+ + + + + {(field) => ( + + )} + + + + )} +
+ + )} + +
+ )} +
+
+
+ + + + ({ + isDefault: state.isDefaultValue || state.isPristine, + isSubmitting: state.isSubmitting, + canSubmit: state.canSubmit, + })} + > + {({ isDefault, isSubmitting, canSubmit }) => ( + +
+
+
+ )} +
+ +
+
+
- -
+ ); }; From 75fafa7a167e597350fdfddfed5ea569a351ff3f Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 24 Mar 2026 12:43:02 +0100 Subject: [PATCH 19/33] move "Tricks" box messages to translations --- web/messages/en/settings.json | 20 +++++++ .../tabs/MessageTemplatesTab.tsx | 52 +++++++------------ 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index 5a266510c4..53d93a1e43 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -44,6 +44,26 @@ "settings_enrollment_template_same_as_message_banner": "Your email will contain the same text as the welcome message from the input above.", "settings_enrollment_template_email_label": "Email message", "settings_enrollment_template_help_title": "Tricks to format your message", + "settings_enrollment_template_help_first_name": "newly created user first name", + "settings_enrollment_template_help_last_name": "newly created user last name", + "settings_enrollment_template_help_username": "newly created user username/login", + "settings_enrollment_template_help_admin_first_name": "first name of the administrator who initiated the enrollment process", + "settings_enrollment_template_help_admin_last_name": "last name of the administrator who initiated the enrollment process", + "settings_enrollment_template_help_admin_phone": "phone number of the administrator who initiated the enrollment process", + "settings_enrollment_template_help_admin_email": "email of the administrator who initiated the enrollment process", + "settings_enrollment_template_help_defguard_url": "internal Defguard URL (your Defguard instance address)", + "settings_enrollment_template_help_markdown_headings": "Create headings.", + "settings_enrollment_template_help_markdown_italic": "Italic text.", + "settings_enrollment_template_help_markdown_bold": "Bold text.", + "settings_enrollment_template_help_markdown_bold_italic": "Bold and italic.", + "settings_enrollment_template_help_markdown_blockquote": "Blockquote.", + "settings_enrollment_template_help_markdown_lists": "Lists (unordered or ordered).", + "settings_enrollment_template_help_markdown_inline_code": "Inline code.", + "settings_enrollment_template_help_markdown_code_block": "Code block.", + "settings_enrollment_template_help_markdown_horizontal_line": "Horizontal line.", + "settings_enrollment_template_help_markdown_link": "Link.", + "settings_enrollment_template_help_markdown_tables": "Create tables.", + "settings_enrollment_template_help_markdown_escape": "Escape special characters.", "settings_enrollment_section_duration_title": "Enrollment session duration", "settings_enrollment_section_duration_description": "Configure the expiration time of the unique token sent to a newly added user. This token is used to activate the account, and in this section administrators can control how long the token remains valid.", "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.", diff --git a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx index 84b0f29617..ce32f3366f 100644 --- a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx +++ b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx @@ -26,41 +26,29 @@ import { formChangeLogic } from '../../../shared/formLogic'; import { getSettingsQueryOptions } from '../../../shared/query'; const messageTemplatesHelpVariables = [ - ['{{ first_name }}', 'newly created user first name'], - ['{{ last_name }}', 'newly created user last name'], - ['{{ username }}', 'newly created user username/login'], - [ - '{{ admin_first_name }}', - 'first name of the administrator who initiated the enrollment process', - ], - [ - '{{ admin_last_name }}', - 'last name of the administrator who initiated the enrollment process', - ], - [ - '{{ admin_phone }}', - 'phone number of the administrator who initiated the enrollment process', - ], - [ - '{{ admin_email }}', - 'email of the administrator who initiated the enrollment process', - ], - ['{{ defguard_url }}', 'internal Defguard URL (your Defguard instance address)'], + ['{{ first_name }}', m.settings_enrollment_template_help_first_name()], + ['{{ last_name }}', m.settings_enrollment_template_help_last_name()], + ['{{ username }}', m.settings_enrollment_template_help_username()], + ['{{ admin_first_name }}', m.settings_enrollment_template_help_admin_first_name()], + ['{{ admin_last_name }}', m.settings_enrollment_template_help_admin_last_name()], + ['{{ admin_phone }}', m.settings_enrollment_template_help_admin_phone()], + ['{{ admin_email }}', m.settings_enrollment_template_help_admin_email()], + ['{{ defguard_url }}', m.settings_enrollment_template_help_defguard_url()], ] as const; const messageTemplatesHelpMarkdown = [ - ['#, ##, ###', 'Create headings.', 'medium'], - ['*text*', 'Italic text.'], - ['**text**', 'Bold text.'], - ['***text***', 'Bold and italic.'], - ['> text', 'Blockquote.'], - ['- item or 1. item', 'Lists (unordered or ordered).'], - ['`code`', 'Inline code.'], - ['```code```', 'Code block.'], - ['***', 'Horizontal line.'], - ['[text](url)', 'Link.'], - ['| and ---', 'Create tables.'], - ['\\', 'Escape special characters.'], + ['#, ##, ###', m.settings_enrollment_template_help_markdown_headings(), 'medium'], + ['*text*', m.settings_enrollment_template_help_markdown_italic()], + ['**text**', m.settings_enrollment_template_help_markdown_bold()], + ['***text***', m.settings_enrollment_template_help_markdown_bold_italic()], + ['> text', m.settings_enrollment_template_help_markdown_blockquote()], + ['- item or 1. item', m.settings_enrollment_template_help_markdown_lists()], + ['`code`', m.settings_enrollment_template_help_markdown_inline_code()], + ['```code```', m.settings_enrollment_template_help_markdown_code_block()], + ['***', m.settings_enrollment_template_help_markdown_horizontal_line()], + ['[text](url)', m.settings_enrollment_template_help_markdown_link()], + ['| and ---', m.settings_enrollment_template_help_markdown_tables()], + ['\\', m.settings_enrollment_template_help_markdown_escape()], ] as const; const messageTemplatesFormSchema = z.object({ From 0905699c3274aa0550f2d0d93644e3a3bab432c9 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 24 Mar 2026 13:03:44 +0100 Subject: [PATCH 20/33] fix enrollment nav item icon --- web/src/shared/components/Navigation/Navigation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/shared/components/Navigation/Navigation.tsx b/web/src/shared/components/Navigation/Navigation.tsx index 8c2686fbcb..756793ed93 100644 --- a/web/src/shared/components/Navigation/Navigation.tsx +++ b/web/src/shared/components/Navigation/Navigation.tsx @@ -83,7 +83,7 @@ const navigationConfig: NavGroupProps[] = [ }, { id: 'enrollment', - icon: 'enrollment', + icon: 'key', label: m.cmp_nav_item_enrollment(), link: '/enrollment', }, From 917f344dcdbd6e4b3ce33a7b794de6b7379acff8 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 25 Mar 2026 06:47:58 +0100 Subject: [PATCH 21/33] take &mut PgConnection as argument --- crates/defguard_proxy_manager/src/servers/enrollment.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/defguard_proxy_manager/src/servers/enrollment.rs b/crates/defguard_proxy_manager/src/servers/enrollment.rs index 0565fb0323..41645886f2 100644 --- a/crates/defguard_proxy_manager/src/servers/enrollment.rs +++ b/crates/defguard_proxy_manager/src/servers/enrollment.rs @@ -39,7 +39,7 @@ use defguard_proto::proxy::{ EnrollmentStartRequest, EnrollmentStartResponse, ExistingDevice, InitialUserInfo, MfaMethod, NewDevice, RegisterMobileAuthRequest, }; -use sqlx::{PgPool, query_scalar}; +use sqlx::{PgConnection, PgPool, query_scalar}; use tokio::sync::{ broadcast::Sender, mpsc::{UnboundedSender, error::SendError}, @@ -354,7 +354,7 @@ impl EnrollmentServer { async fn send_welcome_email_if_enabled( &self, enrollment: &Token, - transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + conn: &mut PgConnection, user: &User, ip_address: &str, device_info: Option<&str>, @@ -370,7 +370,7 @@ impl EnrollmentServer { debug!("Try to send welcome email..."); enrollment - .send_welcome_email(transaction, user, ip_address, device_info) + .send_welcome_email(conn, user, ip_address, device_info) .await?; info!("Welcome email sent to {} at {}", user.username, user.email); From 8b3f09b62c04fbe8718c965e1e45b35d71115fc7 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 25 Mar 2026 08:18:35 +0100 Subject: [PATCH 22/33] use FormInteractiveBlock for the "Display welcome message" section --- .../tabs/MessageTemplatesTab.tsx | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx index ce32f3366f..eba41f25d0 100644 --- a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx +++ b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx @@ -52,6 +52,7 @@ const messageTemplatesHelpMarkdown = [ ] as const; const messageTemplatesFormSchema = z.object({ + enrollment_display_welcome_message: z.boolean(), enrollment_welcome_message: z.string(), enrollment_send_welcome_email: z.boolean(), enrollment_welcome_email_subject: z.string().min(1, m.form_error_required()), @@ -87,6 +88,7 @@ const MessageTemplatesTabContent = ({ settings }: { settings: Settings }) => { const defaultValues = useMemo( (): MessageTemplatesFormFields => ({ + enrollment_display_welcome_message: true, enrollment_welcome_message: settings.enrollment_welcome_message ?? '', enrollment_send_welcome_email: settings.enrollment_send_welcome_email ?? true, enrollment_welcome_email_subject: settings.enrollment_welcome_email_subject ?? '', @@ -105,7 +107,9 @@ const MessageTemplatesTabContent = ({ settings }: { settings: Settings }) => { onChange: messageTemplatesFormSchema, }, onSubmit: async ({ value }) => { - await mutateAsync(value); + const { enrollment_display_welcome_message: _displayWelcomeMessage, ...payload } = + value; + await mutateAsync(payload); form.reset(value); }, }); @@ -131,35 +135,39 @@ const MessageTemplatesTabContent = ({ settings }: { settings: Settings }) => { }} > -
-
-
-
- - {m.settings_enrollment_template_display_message_title()} - - + + {(field) => ( + - {m.settings_enrollment_template_display_message_description()} - -
- - - {(field) => ( - - )} - -
+ + state.values.enrollment_display_welcome_message + } + > + {() => ( + <> + + + {(field) => ( + + )} + + + )} + + + )} +
From c30403fc9880e3e97cac4c0bd6ce7b03b00a1b8b Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 25 Mar 2026 08:35:11 +0100 Subject: [PATCH 23/33] cleanup unused css & classes --- web/src/pages/EnrollmentPage/style.scss | 115 ------------------ .../tabs/MessageTemplatesTab.tsx | 21 ++-- 2 files changed, 7 insertions(+), 129 deletions(-) diff --git a/web/src/pages/EnrollmentPage/style.scss b/web/src/pages/EnrollmentPage/style.scss index 34a2808af3..3f3e6c6406 100644 --- a/web/src/pages/EnrollmentPage/style.scss +++ b/web/src/pages/EnrollmentPage/style.scss @@ -1,119 +1,4 @@ #enrollment-page { - .message-templates-content { - grid-column: 1 / 12; - } - - .message-templates-layout { - display: grid; - grid-template-columns: minmax(720px, 1fr) 373px; - gap: var(--spacing-4xl); - align-items: start; - width: 100%; - max-width: calc(1000px + 373px + var(--spacing-4xl)); - min-width: calc(720px + 373px + var(--spacing-4xl)); - } - - .message-templates-left { - display: flex; - flex-flow: column; - min-width: 0; - } - - .message-template-section { - display: flex; - flex-flow: column; - } - - .message-template-section-offset { - display: grid; - grid-template-columns: 36px minmax(0, 1fr); - column-gap: var(--spacing-lg); - } - - .message-template-offset-spacer { - width: 36px; - } - - .message-template-offset-content { - min-width: 0; - } - - .message-template-offset-divider { - padding-left: calc(36px + var(--spacing-lg)); - } - - .message-template-static-header { - display: flex; - flex-flow: column; - row-gap: var(--spacing-xs); - } - - .message-template-toggle { - .grid { - grid-template-columns: 36px 1fr; - column-gap: var(--spacing-lg); - } - - .content { - max-width: 780px; - } - } - - .message-templates-sidebar { - display: flex; - flex-flow: column; - row-gap: var(--spacing-lg); - - .sidebar-header { - display: flex; - align-items: center; - gap: var(--spacing-md); - } - - .sidebar-panel { - border: none; - border-radius: var(--radius-lg); - background-color: var(--bg-emphasis); - padding: var(--spacing-lg); - display: flex; - flex-flow: column; - row-gap: var(--spacing-lg); - } - - .sidebar-list { - list-style: disc; - padding-left: 18px; - margin: 0; - display: flex; - flex-flow: column; - row-gap: var(--spacing-xs); - font: var(--t-body-sm-400); - color: var(--fg-faded); - - li { - line-height: 20px; - } - } - - .sidebar-token { - font: var(--t-body-sm-600); - color: var(--fg-faded); - } - - .sidebar-token-medium { - font: var(--t-body-sm-500); - color: var(--fg-faded); - } - - .sidebar-separator { - color: var(--fg-faded); - } - } - - .message-templates-checkbox { - display: inline-flex; - } - .message-templates-success-banner { display: grid; grid-template-columns: 20px 1fr; diff --git a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx index eba41f25d0..1d086969ad 100644 --- a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx +++ b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx @@ -115,12 +115,9 @@ const MessageTemplatesTabContent = ({ settings }: { settings: Settings }) => { }); return ( - -
-
+ +
+
{ }} > -
+
{(field) => ( { )}
-
- -
-
+ +
{(field) => ( { )} -
+
{(field) => ( Date: Wed, 25 Mar 2026 09:08:28 +0100 Subject: [PATCH 24/33] use static interactive block variant --- web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx | 2 +- web/src/shared/defguard-ui | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx index 1d086969ad..9d123842f6 100644 --- a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx +++ b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx @@ -136,7 +136,7 @@ const MessageTemplatesTabContent = ({ settings }: { settings: Settings }) => { {(field) => ( diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index e77b9152d9..adc34b7238 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit e77b9152d960c7ce25c2ed06504f4e94238dfaaa +Subproject commit adc34b7238cd46276883bb844a1d022aed5d5038 From 4b5f47dbe69f28585049da3375918128e4d138a3 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 25 Mar 2026 09:13:20 +0100 Subject: [PATCH 25/33] remove unneccesary subscriptions --- .../tabs/MessageTemplatesTab.tsx | 159 ++++++++---------- 1 file changed, 71 insertions(+), 88 deletions(-) diff --git a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx index 9d123842f6..662a53b917 100644 --- a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx +++ b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx @@ -140,27 +140,17 @@ const MessageTemplatesTabContent = ({ settings }: { settings: Settings }) => { title={m.settings_enrollment_template_display_message_title()} content={m.settings_enrollment_template_display_message_description()} > - - state.values.enrollment_display_welcome_message - } - > - {() => ( - <> - - - {(field) => ( - - )} - - + + + {(field) => ( + )} - + )} @@ -168,77 +158,70 @@ const MessageTemplatesTabContent = ({ settings }: { settings: Settings }) => {
- {(field) => ( - - state.values.enrollment_send_welcome_email} + {(field) => { + const sendWelcomeEmail = Boolean(field.state.value); + + return ( + - {(sendWelcomeEmail) => ( - - - - {(field) => ( - - )} - - -
- - {(field) => ( - - )} - -
- - - state.values.enrollment_use_welcome_message_as_email - } - > - {(sameAsWelcomeMessage) => ( - <> - -
- -

- {m.settings_enrollment_template_same_as_message_banner()} -

-
-
- + + + + {(field) => ( + + )} + + +
+ + {(field) => { + const sameAsWelcomeMessage = Boolean(field.state.value); + + return ( + <> + - - {(field) => ( - +
+ - )} - - - - )} - - - )} - - - )} +

+ {m.settings_enrollment_template_same_as_message_banner()} +

+
+ + + + {(field) => ( + + )} + + + + ); + }} +
+
+
+
+ ); + }}
From e0e12f1ddab0eff8673a018694e0c7b8269a72a9 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 25 Mar 2026 09:33:30 +0100 Subject: [PATCH 26/33] fix the divider --- web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx index 662a53b917..e19e358d88 100644 --- a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx +++ b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx @@ -151,11 +151,11 @@ const MessageTemplatesTabContent = ({ settings }: { settings: Settings }) => { /> )} + )}
-
{(field) => { From 90aa72b42393643cc15cfd53af75027efc921248 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 25 Mar 2026 09:35:08 +0100 Subject: [PATCH 27/33] fix header icon --- web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx index e19e358d88..2bacd0bb4a 100644 --- a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx +++ b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx @@ -119,7 +119,7 @@ const MessageTemplatesTabContent = ({ settings }: { settings: Settings }) => {
From 7dee7c19fd51a93f4c8ed3dde6829944e6277068 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 25 Mar 2026 10:28:29 +0100 Subject: [PATCH 28/33] SettingsLayout takes suggestion prop, Suggestion component --- .../tabs/MessageTemplatesTab.tsx | 78 +++++++++---------- .../SettingsLayout/SettingsLayout.tsx | 20 +++-- .../components/SettingsLayout/style.scss | 16 ++++ .../components/Suggestion/Suggestion.tsx | 25 ++++++ .../shared/components/Suggestion/style.scss | 19 +++++ 5 files changed, 109 insertions(+), 49 deletions(-) create mode 100644 web/src/shared/components/Suggestion/Suggestion.tsx create mode 100644 web/src/shared/components/Suggestion/style.scss diff --git a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx index 2bacd0bb4a..19c1d88e0e 100644 --- a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx +++ b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx @@ -8,18 +8,14 @@ import { Controls } from '../../../shared/components/Controls/Controls'; import { SettingsCard } from '../../../shared/components/SettingsCard/SettingsCard'; import { SettingsHeader } from '../../../shared/components/SettingsHeader/SettingsHeader'; import { SettingsLayout } from '../../../shared/components/SettingsLayout/SettingsLayout'; -import { AppText } from '../../../shared/defguard-ui/components/AppText/AppText'; +import { Suggestion } from '../../../shared/components/Suggestion/Suggestion'; import { Button } from '../../../shared/defguard-ui/components/Button/Button'; import { Divider } from '../../../shared/defguard-ui/components/Divider/Divider'; import { Fold } from '../../../shared/defguard-ui/components/Fold/Fold'; import { Icon } from '../../../shared/defguard-ui/components/Icon'; import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { Snackbar } from '../../../shared/defguard-ui/providers/snackbar/snackbar'; -import { - TextStyle, - ThemeSpacing, - ThemeVariable, -} from '../../../shared/defguard-ui/types'; +import { ThemeSpacing, ThemeVariable } from '../../../shared/defguard-ui/types'; import { isPresent } from '../../../shared/defguard-ui/utils/isPresent'; import { useAppForm } from '../../../shared/form'; import { formChangeLogic } from '../../../shared/formLogic'; @@ -115,7 +111,7 @@ const MessageTemplatesTabContent = ({ settings }: { settings: Settings }) => { }); return ( - + }>
{
-
); }; -const MessageTemplatesHelpPanel = () => { +const MessageTemplatesSuggestion = () => { return ( -
-
- - - {m.settings_enrollment_template_help_title()} - -
-
-
    - {messageTemplatesHelpVariables.map(([token, description]) => ( -
  • - {token} - - - {description} -
  • - ))} -
- -
    - {messageTemplatesHelpMarkdown.map(([token, description, weight]) => ( -
  • - - {token} - - - - {description} -
  • - ))} -
-
-
+ +
    + {messageTemplatesHelpVariables.map(([token, description]) => ( +
  • + {token} + - + {description} +
  • + ))} +
+ +
    + {messageTemplatesHelpMarkdown.map(([token, description, weight]) => ( +
  • + + {token} + + - + {description} +
  • + ))} +
+ + } + /> ); }; diff --git a/web/src/shared/components/SettingsLayout/SettingsLayout.tsx b/web/src/shared/components/SettingsLayout/SettingsLayout.tsx index 4dbeb0f6a1..d4c61952c8 100644 --- a/web/src/shared/components/SettingsLayout/SettingsLayout.tsx +++ b/web/src/shared/components/SettingsLayout/SettingsLayout.tsx @@ -1,19 +1,25 @@ -import type { HTMLProps, PropsWithChildren } from 'react'; +import type { HTMLProps, PropsWithChildren, ReactNode } from 'react'; import './style.scss'; import clsx from 'clsx'; import { LayoutGrid } from '../LayoutGrid/LayoutGrid'; -export const SettingsLayout = ({ - children, - className, - ...props -}: PropsWithChildren & HTMLProps) => { +type Props = HTMLProps & + PropsWithChildren & { + suggestion?: ReactNode; + }; + +export const SettingsLayout = ({ children, className, suggestion, ...props }: Props) => { return ( -
+
{children}
+ {suggestion &&
{suggestion}
}
); diff --git a/web/src/shared/components/SettingsLayout/style.scss b/web/src/shared/components/SettingsLayout/style.scss index 83dac2a49f..55116d1a21 100644 --- a/web/src/shared/components/SettingsLayout/style.scss +++ b/web/src/shared/components/SettingsLayout/style.scss @@ -7,5 +7,21 @@ grid-column: 1 / 7; grid-row: 1; } + + & > .suggestion-content { + grid-row: 1; + } + } + + &.with-suggestion { + & > .layout-grid { + & > .main-content { + grid-column: 1 / 10; + } + + & > .suggestion-content { + grid-column: 10 / 13; + } + } } } diff --git a/web/src/shared/components/Suggestion/Suggestion.tsx b/web/src/shared/components/Suggestion/Suggestion.tsx new file mode 100644 index 0000000000..c45aa6b516 --- /dev/null +++ b/web/src/shared/components/Suggestion/Suggestion.tsx @@ -0,0 +1,25 @@ +import type { HTMLProps, ReactNode } from 'react'; +import './style.scss'; +import clsx from 'clsx'; +import { AppText } from '../../defguard-ui/components/AppText/AppText'; +import { Icon } from '../../defguard-ui/components/Icon'; +import { TextStyle, ThemeVariable } from '../../defguard-ui/types'; + +type Props = Omit, 'content' | 'title'> & { + title: string; + content: ReactNode; +}; + +export const Suggestion = ({ title, content, className, ...props }: Props) => { + return ( +
+
+ + + {title} + +
+
{content}
+
+ ); +}; diff --git a/web/src/shared/components/Suggestion/style.scss b/web/src/shared/components/Suggestion/style.scss new file mode 100644 index 0000000000..e1967e4df4 --- /dev/null +++ b/web/src/shared/components/Suggestion/style.scss @@ -0,0 +1,19 @@ +.suggestion { + display: flex; + flex-flow: column; + row-gap: var(--spacing-lg); + + .suggestion-header { + display: grid; + grid-template-columns: 20px 1fr; + align-items: center; + column-gap: var(--spacing-sm); + } + + .suggestion-panel { + background-color: var(--bg-neutral-faded); + border-radius: var(--radius-lg); + padding: var(--spacing-lg); + box-sizing: border-box; + } +} From 52137d53348dfc5262c661f6b654156b66615a81 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 25 Mar 2026 10:35:52 +0100 Subject: [PATCH 29/33] remove the gap --- .../shared/components/SettingsLayout/style.scss | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/web/src/shared/components/SettingsLayout/style.scss b/web/src/shared/components/SettingsLayout/style.scss index 55116d1a21..cd0e18f2fe 100644 --- a/web/src/shared/components/SettingsLayout/style.scss +++ b/web/src/shared/components/SettingsLayout/style.scss @@ -10,18 +10,10 @@ & > .suggestion-content { grid-row: 1; - } - } - - &.with-suggestion { - & > .layout-grid { - & > .main-content { - grid-column: 1 / 10; - } - - & > .suggestion-content { - grid-column: 10 / 13; - } + grid-column: 7 / 10; + justify-self: start; + width: 100%; + max-width: 373px; } } } From 83c24305232737847cc0524a900d156ee420259d Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 25 Mar 2026 10:52:56 +0100 Subject: [PATCH 30/33] fix suggestion box styling --- web/src/pages/EnrollmentPage/style.scss | 26 +++++++++++++++++++ .../SettingsLayout/SettingsLayout.tsx | 4 +-- .../components/SettingsLayout/style.scss | 4 +-- .../shared/components/Suggestion/style.scss | 2 +- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/web/src/pages/EnrollmentPage/style.scss b/web/src/pages/EnrollmentPage/style.scss index 3f3e6c6406..ad2fd5b99f 100644 --- a/web/src/pages/EnrollmentPage/style.scss +++ b/web/src/pages/EnrollmentPage/style.scss @@ -1,4 +1,30 @@ #enrollment-page { + .sidebar-list { + display: flex; + flex-flow: column; + row-gap: var(--spacing-sm); + list-style: disc; + padding-left: 20px; + color: var(--fg-faded); + font: var(--t-body-sm-400); + + li { + color: var(--fg-faded); + font: var(--t-body-sm-400); + line-height: 20px; + } + } + + .sidebar-token, + .sidebar-token-medium { + color: var(--fg-faded); + font: var(--t-body-sm-600); + } + + .sidebar-separator { + color: var(--fg-faded); + } + .message-templates-success-banner { display: grid; grid-template-columns: 20px 1fr; diff --git a/web/src/shared/components/SettingsLayout/SettingsLayout.tsx b/web/src/shared/components/SettingsLayout/SettingsLayout.tsx index d4c61952c8..cb2168c174 100644 --- a/web/src/shared/components/SettingsLayout/SettingsLayout.tsx +++ b/web/src/shared/components/SettingsLayout/SettingsLayout.tsx @@ -16,10 +16,10 @@ export const SettingsLayout = ({ children, className, suggestion, ...props }: Pr })} > -
+
{children}
- {suggestion &&
{suggestion}
} + {suggestion &&
{suggestion}
}
); diff --git a/web/src/shared/components/SettingsLayout/style.scss b/web/src/shared/components/SettingsLayout/style.scss index cd0e18f2fe..cb3232e1ad 100644 --- a/web/src/shared/components/SettingsLayout/style.scss +++ b/web/src/shared/components/SettingsLayout/style.scss @@ -3,12 +3,12 @@ box-sizing: border-box; padding: var(--spacing-3xl) var(--spacing-xl); - & > .main-content { + & > .main { grid-column: 1 / 7; grid-row: 1; } - & > .suggestion-content { + & > .helpers { grid-row: 1; grid-column: 7 / 10; justify-self: start; diff --git a/web/src/shared/components/Suggestion/style.scss b/web/src/shared/components/Suggestion/style.scss index e1967e4df4..71a56aae0b 100644 --- a/web/src/shared/components/Suggestion/style.scss +++ b/web/src/shared/components/Suggestion/style.scss @@ -11,7 +11,7 @@ } .suggestion-panel { - background-color: var(--bg-neutral-faded); + background-color: var(--bg-disabled); border-radius: var(--radius-lg); padding: var(--spacing-lg); box-sizing: border-box; From 8aa350039fe3dab956b3584d470f03187947a653 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 25 Mar 2026 11:04:17 +0100 Subject: [PATCH 31/33] rename "static" variant to "empty" --- web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx | 2 +- web/src/shared/defguard-ui | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx index 19c1d88e0e..b8ecd5aee9 100644 --- a/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx +++ b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx @@ -132,7 +132,7 @@ const MessageTemplatesTabContent = ({ settings }: { settings: Settings }) => { {(field) => ( diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index adc34b7238..0ce3d2e004 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit adc34b7238cd46276883bb844a1d022aed5d5038 +Subproject commit 0ce3d2e004a062653734b4addebace600ba21211 From c3995ce3873a4e01b169f3b62b224aa7ea401bd4 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 25 Mar 2026 11:06:29 +0100 Subject: [PATCH 32/33] update defguard-ui dependency --- web/src/shared/defguard-ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index 0ce3d2e004..a9177dfb6e 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit 0ce3d2e004a062653734b4addebace600ba21211 +Subproject commit a9177dfb6eec895bb3e20e91990abf35e440cd56 From 8a476de23678da357759618622ebe8fc6139c802 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 25 Mar 2026 11:14:03 +0100 Subject: [PATCH 33/33] fix the test --- crates/defguard_proxy_manager/src/servers/enrollment.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/defguard_proxy_manager/src/servers/enrollment.rs b/crates/defguard_proxy_manager/src/servers/enrollment.rs index 41645886f2..41e79d10a3 100644 --- a/crates/defguard_proxy_manager/src/servers/enrollment.rs +++ b/crates/defguard_proxy_manager/src/servers/enrollment.rs @@ -1140,7 +1140,10 @@ pub async fn new_polling_token(pool: &PgPool, device: &Device) -> Result