diff --git a/.sqlx/query-89698ecaa251e056770bb90827d2d41284e6629883f647462029872c9fad2bfb.json b/.sqlx/query-0c5875b67c6a8bd35091f17fca130ef98e0902176cef9733468eefb094b37638.json similarity index 61% rename from .sqlx/query-89698ecaa251e056770bb90827d2d41284e6629883f647462029872c9fad2bfb.json rename to .sqlx/query-0c5875b67c6a8bd35091f17fca130ef98e0902176cef9733468eefb094b37638.json index af565cdd2e..6fa706ec05 100644 --- a/.sqlx/query-89698ecaa251e056770bb90827d2d41284e6629883f647462029872c9fad2bfb.json +++ b/.sqlx/query-0c5875b67c6a8bd35091f17fca130ef98e0902176cef9733468eefb094b37638.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"settings\" SET openid_enabled = $1, wireguard_enabled = $2, webhooks_enabled = $3, worker_enabled = $4, challenge_template = $5, instance_name = $6, main_logo_url = $7, nav_logo_url = $8, smtp_server = $9, smtp_port = $10, smtp_encryption = $11, smtp_user = $12, smtp_password = $13, smtp_sender = $14, enrollment_vpn_step_optional = $15, enrollment_welcome_message = $16, enrollment_welcome_email = $17, enrollment_welcome_email_subject = $18, enrollment_use_welcome_message_as_email = $19, uuid = $20, ldap_url = $21, ldap_bind_username = $22, ldap_bind_password = $23, ldap_group_search_base = $24, ldap_user_search_base = $25, ldap_user_obj_class = $26, ldap_group_obj_class = $27, ldap_username_attr = $28, ldap_groupname_attr = $29, ldap_group_member_attr = $30, ldap_member_attr = $31, ldap_use_starttls = $32, ldap_tls_verify_cert = $33, openid_create_account = $34, license = $35, gateway_disconnect_notifications_enabled = $36, gateway_disconnect_notifications_inactivity_threshold = $37, gateway_disconnect_notifications_reconnect_notification_enabled = $38, ldap_sync_status = $39, ldap_enabled = $40, ldap_sync_enabled = $41, ldap_is_authoritative = $42, ldap_sync_interval = $43, ldap_user_auxiliary_obj_classes = $44, ldap_uses_ad = $45, ldap_user_rdn_attr = $46, ldap_sync_groups = $47, openid_username_handling = $48, ca_key_der = $49, ca_cert_der = $50, ca_expiry = $51, defguard_url = $52, default_admin_group_name = $53, authentication_period_days = $54, mfa_code_timeout_seconds = $55, public_proxy_url = $56, default_admin_id = $57, 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", + "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": { @@ -35,6 +35,7 @@ "Text", "Text", "Bool", + "Bool", "Uuid", "Text", "Text", @@ -106,5 +107,5 @@ }, "nullable": [] }, - "hash": "89698ecaa251e056770bb90827d2d41284e6629883f647462029872c9fad2bfb" + "hash": "0c5875b67c6a8bd35091f17fca130ef98e0902176cef9733468eefb094b37638" } diff --git a/.sqlx/query-dab137a626956fe0a0f2fbfc17c45075372f6963ff73760a53f843eaf5ebed4a.json b/.sqlx/query-37bce7224352d3313ca950f344ddc58bcb3c9a964b3e3286d5f3b5b0366836e4.json similarity index 86% rename from .sqlx/query-dab137a626956fe0a0f2fbfc17c45075372f6963ff73760a53f843eaf5ebed4a.json rename to .sqlx/query-37bce7224352d3313ca950f344ddc58bcb3c9a964b3e3286d5f3b5b0366836e4.json index ce20619c2c..7febb0d9b9 100644 --- a/.sqlx/query-dab137a626956fe0a0f2fbfc17c45075372f6963ff73760a53f843eaf5ebed4a.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, 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,101 +111,106 @@ }, { "ordinal": 19, + "name": "enrollment_send_welcome_email", + "type_info": "Bool" + }, + { + "ordinal": 20, "name": "uuid", "type_info": "Uuid" }, { - "ordinal": 20, + "ordinal": 21, "name": "ldap_url", "type_info": "Text" }, { - "ordinal": 21, + "ordinal": 22, "name": "ldap_bind_username", "type_info": "Text" }, { - "ordinal": 22, + "ordinal": 23, "name": "ldap_bind_password?: SecretStringWrapper", "type_info": "Text" }, { - "ordinal": 23, + "ordinal": 24, "name": "ldap_group_search_base", "type_info": "Text" }, { - "ordinal": 24, + "ordinal": 25, "name": "ldap_user_search_base", "type_info": "Text" }, { - "ordinal": 25, + "ordinal": 26, "name": "ldap_user_obj_class", "type_info": "Text" }, { - "ordinal": 26, + "ordinal": 27, "name": "ldap_group_obj_class", "type_info": "Text" }, { - "ordinal": 27, + "ordinal": 28, "name": "ldap_username_attr", "type_info": "Text" }, { - "ordinal": 28, + "ordinal": 29, "name": "ldap_groupname_attr", "type_info": "Text" }, { - "ordinal": 29, + "ordinal": 30, "name": "ldap_group_member_attr", "type_info": "Text" }, { - "ordinal": 30, + "ordinal": 31, "name": "ldap_member_attr", "type_info": "Text" }, { - "ordinal": 31, + "ordinal": 32, "name": "openid_create_account", "type_info": "Bool" }, { - "ordinal": 32, + "ordinal": 33, "name": "license", "type_info": "Text" }, { - "ordinal": 33, + "ordinal": 34, "name": "gateway_disconnect_notifications_enabled", "type_info": "Bool" }, { - "ordinal": 34, + "ordinal": 35, "name": "ldap_use_starttls", "type_info": "Bool" }, { - "ordinal": 35, + "ordinal": 36, "name": "ldap_tls_verify_cert", "type_info": "Bool" }, { - "ordinal": 36, + "ordinal": 37, "name": "gateway_disconnect_notifications_inactivity_threshold", "type_info": "Int4" }, { - "ordinal": 37, + "ordinal": 38, "name": "gateway_disconnect_notifications_reconnect_notification_enabled", "type_info": "Bool" }, { - "ordinal": 38, + "ordinal": 39, "name": "ldap_sync_status: LdapSyncStatus", "type_info": { "Custom": { @@ -220,47 +225,47 @@ } }, { - "ordinal": 39, + "ordinal": 40, "name": "ldap_enabled", "type_info": "Bool" }, { - "ordinal": 40, + "ordinal": 41, "name": "ldap_sync_enabled", "type_info": "Bool" }, { - "ordinal": 41, + "ordinal": 42, "name": "ldap_is_authoritative", "type_info": "Bool" }, { - "ordinal": 42, + "ordinal": 43, "name": "ldap_sync_interval", "type_info": "Int4" }, { - "ordinal": 43, + "ordinal": 44, "name": "ldap_user_auxiliary_obj_classes", "type_info": "TextArray" }, { - "ordinal": 44, + "ordinal": 45, "name": "ldap_uses_ad", "type_info": "Bool" }, { - "ordinal": 45, + "ordinal": 46, "name": "ldap_user_rdn_attr", "type_info": "Text" }, { - "ordinal": 46, + "ordinal": 47, "name": "ldap_sync_groups", "type_info": "TextArray" }, { - "ordinal": 47, + "ordinal": 48, "name": "openid_username_handling: OpenIdUsernameHandling", "type_info": { "Custom": { @@ -276,87 +281,87 @@ } }, { - "ordinal": 48, + "ordinal": 49, "name": "ca_key_der", "type_info": "Bytea" }, { - "ordinal": 49, + "ordinal": 50, "name": "ca_cert_der", "type_info": "Bytea" }, { - "ordinal": 50, + "ordinal": 51, "name": "ca_expiry", "type_info": "Timestamp" }, { - "ordinal": 51, + "ordinal": 52, "name": "defguard_url", "type_info": "Text" }, { - "ordinal": 52, + "ordinal": 53, "name": "default_admin_group_name", "type_info": "Text" }, { - "ordinal": 53, + "ordinal": 54, "name": "authentication_period_days", "type_info": "Int4" }, { - "ordinal": 54, + "ordinal": 55, "name": "mfa_code_timeout_seconds", "type_info": "Int4" }, { - "ordinal": 55, + "ordinal": 56, "name": "public_proxy_url", "type_info": "Text" }, { - "ordinal": 56, + "ordinal": 57, "name": "default_admin_id", "type_info": "Int8" }, { - "ordinal": 57, + "ordinal": 58, "name": "secret_key", "type_info": "Text" }, { - "ordinal": 58, + "ordinal": 59, "name": "enable_stats_purge", "type_info": "Bool" }, { - "ordinal": 59, + "ordinal": 60, "name": "stats_purge_frequency_hours", "type_info": "Int4" }, { - "ordinal": 60, + "ordinal": 61, "name": "stats_purge_threshold_days", "type_info": "Int4" }, { - "ordinal": 61, + "ordinal": 62, "name": "enrollment_token_timeout_hours", "type_info": "Int4" }, { - "ordinal": 62, + "ordinal": 63, "name": "password_reset_token_timeout_hours", "type_info": "Int4" }, { - "ordinal": 63, + "ordinal": 64, "name": "enrollment_session_timeout_minutes", "type_info": "Int4" }, { - "ordinal": 64, + "ordinal": 65, "name": "password_reset_session_timeout_minutes", "type_info": "Int4" } @@ -385,6 +390,7 @@ true, false, false, + false, true, true, true, @@ -432,5 +438,5 @@ false ] }, - "hash": "dab137a626956fe0a0f2fbfc17c45075372f6963ff73760a53f843eaf5ebed4a" + "hash": "37bce7224352d3313ca950f344ddc58bcb3c9a964b3e3286d5f3b5b0366836e4" } diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index 8a7175d839..ea288e0863 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -152,6 +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_send_welcome_email: bool, // Instance UUID needed for desktop client #[serde(skip)] pub uuid: Uuid, @@ -246,6 +247,10 @@ impl fmt::Debug for Settings { "enrollment_use_welcome_message_as_email", &self.enrollment_use_welcome_message_as_email, ) + .field( + "enrollment_send_welcome_email", + &self.enrollment_send_welcome_email, + ) .field("uuid", &self.uuid) .field("ldap_url", &self.ldap_url) .field("ldap_bind_username", &self.ldap_bind_username) @@ -393,7 +398,8 @@ 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_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, \ @@ -463,52 +469,53 @@ 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_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, @@ -529,6 +536,7 @@ impl Settings { self.enrollment_welcome_email, self.enrollment_welcome_email_subject, self.enrollment_use_welcome_message_as_email, + self.enrollment_send_welcome_email, self.uuid, self.ldap_url, self.ldap_bind_username, diff --git a/crates/defguard_proxy_manager/src/servers/enrollment.rs b/crates/defguard_proxy_manager/src/servers/enrollment.rs index 3725e2d87b..41e79d10a3 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}, @@ -351,6 +351,32 @@ impl EnrollmentServer { Ok(()) } + async fn send_welcome_email_if_enabled( + &self, + enrollment: &Token, + conn: &mut PgConnection, + user: &User, + ip_address: &str, + device_info: Option<&str>, + ) -> Result<(), Status> { + let settings = Settings::get_current_settings(); + 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(conn, user, ip_address, device_info) + .await?; + info!("Welcome email sent to {} at {}", user.username, user.email); + + Ok(()) + } + #[instrument(skip_all)] pub(crate) async fn activate_user( &self, @@ -419,11 +445,14 @@ impl EnrollmentServer { debug!("Updating user details ended with success."); let _ = update_counts(&mut *transaction).await; - // send welcome email - debug!("Try to send welcome email..."); - enrollment - .send_welcome_email(&mut transaction, &user, &ip_address, device_info.as_deref()) - .await?; + self.send_welcome_email_if_enabled( + &enrollment, + &mut transaction, + &user, + &ip_address, + device_info.as_deref(), + ) + .await?; // send success notification to admin debug!( @@ -1107,3 +1136,65 @@ pub async fn new_polling_token(pool: &PgPool, device: &Device) -> Result { + 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 && } + + ); +}; diff --git a/web/src/pages/EnrollmentPage/style.scss b/web/src/pages/EnrollmentPage/style.scss new file mode 100644 index 0000000000..ad2fd5b99f --- /dev/null +++ b/web/src/pages/EnrollmentPage/style.scss @@ -0,0 +1,44 @@ +#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; + 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); + } + } +} 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..b8ecd5aee9 --- /dev/null +++ b/web/src/pages/EnrollmentPage/tabs/MessageTemplatesTab.tsx @@ -0,0 +1,294 @@ +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 { 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 { 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 }}', 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 = [ + ['#, ##, ###', 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({ + 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()), + 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_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 ?? '', + 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 }) => { + const { enrollment_display_welcome_message: _displayWelcomeMessage, ...payload } = + value; + await mutateAsync(payload); + form.reset(value); + }, + }); + + return ( + }> +
+
+ + +
{ + e.stopPropagation(); + e.preventDefault(); + form.handleSubmit(); + }} + > + +
+ + {(field) => ( + + + + {(field) => ( + + )} + + + + )} + +
+
+ + {(field) => { + const sendWelcomeEmail = Boolean(field.state.value); + + return ( + + + + + {(field) => ( + + )} + + +
+ + {(field) => { + const sameAsWelcomeMessage = Boolean(field.state.value); + + return ( + <> + + + +
+ +

+ {m.settings_enrollment_template_same_as_message_banner()} +

+
+
+ + + {(field) => ( + + )} + + + + ); + }} +
+
+
+
+ ); + }} +
+
+
+ + + + ({ + isDefault: state.isDefaultValue || state.isPristine, + isSubmitting: state.isSubmitting, + canSubmit: state.canSubmit, + })} + > + {({ isDefault, isSubmitting, canSubmit }) => ( + +
+
+
+ )} +
+ +
+
+
+
+ ); +}; + +const MessageTemplatesSuggestion = () => { + return ( + +
    + {messageTemplatesHelpVariables.map(([token, description]) => ( +
  • + {token} + - + {description} +
  • + ))} +
+ +
    + {messageTemplatesHelpMarkdown.map(([token, description, weight]) => ( +
  • + + {token} + + - + {description} +
  • + ))} +
+ + } + /> + ); +}; diff --git a/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx b/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx deleted file mode 100644 index ab4994b25f..0000000000 --- a/web/src/pages/settings/SettingsEnrollmentPage/SettingsEnrollmentPage.tsx +++ /dev/null @@ -1,239 +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_enrollment()} - , -]; - -export const SettingsEnrollmentPage = () => { - const { data: settings } = useQuery(getSettingsQueryOptions); - return ( - - - - - {isPresent(settings) && ( - - - - )} - - - ); -}; - -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({ - 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 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 => ({ - 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, - 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, - 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) => ( - - )} - - - - {(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 51318a9884..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 443c5dd0e8..02d8132372 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' @@ -52,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' @@ -227,6 +227,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', @@ -298,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', @@ -407,6 +407,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 @@ -432,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 @@ -464,6 +464,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 @@ -489,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 @@ -525,6 +525,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 @@ -550,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 @@ -585,6 +585,7 @@ export interface FileRouteTypes { | '/auth/' | '/activity' | '/edges' + | '/enrollment' | '/groups' | '/network-devices' | '/openid' @@ -610,7 +611,6 @@ export interface FileRouteTypes { | '/acl/rules' | '/settings/client' | '/settings/edit-openid' - | '/settings/enrollment' | '/settings/gateway-notifications' | '/settings/instance' | '/settings/ldap' @@ -642,6 +642,7 @@ export interface FileRouteTypes { | '/auth' | '/activity' | '/edges' + | '/enrollment' | '/groups' | '/network-devices' | '/openid' @@ -667,7 +668,6 @@ export interface FileRouteTypes { | '/acl/rules' | '/settings/client' | '/settings/edit-openid' - | '/settings/enrollment' | '/settings/gateway-notifications' | '/settings/instance' | '/settings/ldap' @@ -702,6 +702,7 @@ export interface FileRouteTypes { | '/auth/' | '/_authorized/_default/activity' | '/_authorized/_default/edges' + | '/_authorized/_default/enrollment' | '/_authorized/_default/groups' | '/_authorized/_default/network-devices' | '/_authorized/_default/openid' @@ -727,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' @@ -977,6 +977,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' @@ -1061,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' @@ -1172,6 +1172,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 @@ -1188,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 @@ -1207,6 +1207,7 @@ interface AuthorizedDefaultRouteChildren { const AuthorizedDefaultRouteChildren: AuthorizedDefaultRouteChildren = { AuthorizedDefaultActivityRoute: AuthorizedDefaultActivityRoute, AuthorizedDefaultEdgesRoute: AuthorizedDefaultEdgesRoute, + AuthorizedDefaultEnrollmentRoute: AuthorizedDefaultEnrollmentRoute, AuthorizedDefaultGroupsRoute: AuthorizedDefaultGroupsRoute, AuthorizedDefaultNetworkDevicesRoute: AuthorizedDefaultNetworkDevicesRoute, AuthorizedDefaultOpenidRoute: AuthorizedDefaultOpenidRoute, @@ -1226,8 +1227,6 @@ const AuthorizedDefaultRouteChildren: AuthorizedDefaultRouteChildren = { AuthorizedDefaultSettingsClientRoute: AuthorizedDefaultSettingsClientRoute, AuthorizedDefaultSettingsEditOpenidRoute: AuthorizedDefaultSettingsEditOpenidRoute, - AuthorizedDefaultSettingsEnrollmentRoute: - AuthorizedDefaultSettingsEnrollmentRoute, AuthorizedDefaultSettingsGatewayNotificationsRoute: AuthorizedDefaultSettingsGatewayNotificationsRoute, AuthorizedDefaultSettingsInstanceRoute: 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/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, -}); diff --git a/web/src/shared/api/types.ts b/web/src/shared/api/types.ts index 662a701163..273ffc384a 100644 --- a/web/src/shared/api/types.ts +++ b/web/src/shared/api/types.ts @@ -884,6 +884,7 @@ export interface SettingsEnrollment { enrollment_welcome_email: string; enrollment_welcome_email_subject: string; enrollment_use_welcome_message_as_email: boolean; + enrollment_send_welcome_email: boolean; } export interface SettingsModules { 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/components/Navigation/Navigation.tsx b/web/src/shared/components/Navigation/Navigation.tsx index f7cc59a092..756793ed93 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: 'key', + label: m.cmp_nav_item_enrollment(), + link: '/enrollment', + }, ], }, { diff --git a/web/src/shared/components/SettingsLayout/SettingsLayout.tsx b/web/src/shared/components/SettingsLayout/SettingsLayout.tsx index 4dbeb0f6a1..cb2168c174 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..cb3232e1ad 100644 --- a/web/src/shared/components/SettingsLayout/style.scss +++ b/web/src/shared/components/SettingsLayout/style.scss @@ -3,9 +3,17 @@ box-sizing: border-box; padding: var(--spacing-3xl) var(--spacing-xl); - & > .main-content { + & > .main { grid-column: 1 / 7; grid-row: 1; } + + & > .helpers { + grid-row: 1; + grid-column: 7 / 10; + justify-self: start; + width: 100%; + max-width: 373px; + } } } 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..71a56aae0b --- /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-disabled); + border-radius: var(--radius-lg); + padding: var(--spacing-lg); + box-sizing: border-box; + } +} diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index e77b9152d9..a9177dfb6e 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit e77b9152d960c7ce25c2ed06504f4e94238dfaaa +Subproject commit a9177dfb6eec895bb3e20e91990abf35e440cd56 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,