diff --git a/.sqlx/query-ec7e1e23a50c0fc4a31f17c084c9b05dee47c63913d10a5a048bedafbbc1f9dd.json b/.sqlx/query-01f5f2906c76d16f544617963f1342eb0b718a23b6d8629f24eeeed4504b2b20.json similarity index 82% rename from .sqlx/query-ec7e1e23a50c0fc4a31f17c084c9b05dee47c63913d10a5a048bedafbbc1f9dd.json rename to .sqlx/query-01f5f2906c76d16f544617963f1342eb0b718a23b6d8629f24eeeed4504b2b20.json index bb33f41173..b6add23ae9 100644 --- a/.sqlx/query-ec7e1e23a50c0fc4a31f17c084c9b05dee47c63913d10a5a048bedafbbc1f9dd.json +++ b/.sqlx/query-01f5f2906c76d16f544617963f1342eb0b718a23b6d8629f24eeeed4504b2b20.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, 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, defguard_url = $50, default_admin_group_name = $51, authentication_period_days = $52, mfa_code_timeout_seconds = $53, public_proxy_url = $54, default_admin_id = $55, secret_key = $56, enable_stats_purge = $57, stats_purge_frequency_hours = $58, stats_purge_threshold_days = $59, enrollment_token_timeout_hours = $60, password_reset_token_timeout_hours = $61, enrollment_session_timeout_minutes = $62, password_reset_session_timeout_minutes = $63 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, ldap_remote_enrollment_enabled = $49, ldap_remote_enrollment_send_invite = $50, openid_username_handling = $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": { @@ -74,6 +74,8 @@ "Bool", "Text", "TextArray", + "Bool", + "Bool", { "Custom": { "name": "openid_username_handling", @@ -104,5 +106,5 @@ }, "nullable": [] }, - "hash": "ec7e1e23a50c0fc4a31f17c084c9b05dee47c63913d10a5a048bedafbbc1f9dd" + "hash": "01f5f2906c76d16f544617963f1342eb0b718a23b6d8629f24eeeed4504b2b20" } diff --git a/.sqlx/query-4b7289fe59a0e9553fa0bfd9e9da2dbd2cda17952cf77ac3d743a4fa85d37482.json b/.sqlx/query-093a7426c1d5df52f5e282a296fa2e1f4559c6ef703fc2a2343cbab1058963c9.json similarity index 90% rename from .sqlx/query-4b7289fe59a0e9553fa0bfd9e9da2dbd2cda17952cf77ac3d743a4fa85d37482.json rename to .sqlx/query-093a7426c1d5df52f5e282a296fa2e1f4559c6ef703fc2a2343cbab1058963c9.json index 872ad26651..e81a8ae962 100644 --- a/.sqlx/query-4b7289fe59a0e9553fa0bfd9e9da2dbd2cda17952cf77ac3d743a4fa85d37482.json +++ b/.sqlx/query-093a7426c1d5df52f5e282a296fa2e1f4559c6ef703fc2a2343cbab1058963c9.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" WHERE openid_sub = $1", + "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed,enrollment_pending FROM \"user\" WHERE openid_sub = $1", "describe": { "columns": [ { @@ -117,6 +117,11 @@ }, { "ordinal": 20, + "name": "ldap_remote_enrollment_completed", + "type_info": "Bool" + }, + { + "ordinal": 21, "name": "enrollment_pending", "type_info": "Bool" } @@ -147,8 +152,9 @@ false, true, true, + false, false ] }, - "hash": "4b7289fe59a0e9553fa0bfd9e9da2dbd2cda17952cf77ac3d743a4fa85d37482" + "hash": "093a7426c1d5df52f5e282a296fa2e1f4559c6ef703fc2a2343cbab1058963c9" } diff --git a/.sqlx/query-84b5185c8c297717e9935cd2521bbbb2ed2bf453b32cb1c22b227d8541a63392.json b/.sqlx/query-212a211fca991b0a58e4cde25571edc03e2e3b9970079b7903ac3def6efff79a.json similarity index 85% rename from .sqlx/query-84b5185c8c297717e9935cd2521bbbb2ed2bf453b32cb1c22b227d8541a63392.json rename to .sqlx/query-212a211fca991b0a58e4cde25571edc03e2e3b9970079b7903ac3def6efff79a.json index 95c30504ab..8481fcd594 100644 --- a/.sqlx/query-84b5185c8c297717e9935cd2521bbbb2ed2bf453b32cb1c22b227d8541a63392.json +++ b/.sqlx/query-212a211fca991b0a58e4cde25571edc03e2e3b9970079b7903ac3def6efff79a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"from_ldap\",\"ldap_pass_randomized\",\"ldap_rdn\",\"ldap_user_path\",\"openid_sub\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\" \"mfa_method: _\",\"recovery_codes\" \"recovery_codes: _\",\"enrollment_pending\" FROM \"user\" WHERE id = $1", + "query": "SELECT id, \"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"from_ldap\",\"ldap_pass_randomized\",\"ldap_rdn\",\"ldap_user_path\",\"ldap_remote_enrollment_completed\",\"openid_sub\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\" \"mfa_method: _\",\"recovery_codes\" \"recovery_codes: _\",\"enrollment_pending\" FROM \"user\" LIMIT $1 OFFSET $2", "describe": { "columns": [ { @@ -70,31 +70,36 @@ }, { "ordinal": 13, + "name": "ldap_remote_enrollment_completed", + "type_info": "Bool" + }, + { + "ordinal": 14, "name": "openid_sub", "type_info": "Text" }, { - "ordinal": 14, + "ordinal": 15, "name": "totp_enabled", "type_info": "Bool" }, { - "ordinal": 15, + "ordinal": 16, "name": "email_mfa_enabled", "type_info": "Bool" }, { - "ordinal": 16, + "ordinal": 17, "name": "totp_secret", "type_info": "Bytea" }, { - "ordinal": 17, + "ordinal": 18, "name": "email_mfa_secret", "type_info": "Bytea" }, { - "ordinal": 18, + "ordinal": 19, "name": "mfa_method: _", "type_info": { "Custom": { @@ -111,18 +116,19 @@ } }, { - "ordinal": 19, + "ordinal": 20, "name": "recovery_codes: _", "type_info": "TextArray" }, { - "ordinal": 20, + "ordinal": 21, "name": "enrollment_pending", "type_info": "Bool" } ], "parameters": { "Left": [ + "Int8", "Int8" ] }, @@ -140,6 +146,7 @@ false, true, true, + false, true, false, false, @@ -150,5 +157,5 @@ false ] }, - "hash": "84b5185c8c297717e9935cd2521bbbb2ed2bf453b32cb1c22b227d8541a63392" + "hash": "212a211fca991b0a58e4cde25571edc03e2e3b9970079b7903ac3def6efff79a" } diff --git a/.sqlx/query-0dcbd4cfa8f34d5fdf303988c606d7304ddfbb1fdbb4e513c92bf7059ba228c7.json b/.sqlx/query-21d29424c3a8df807ea526da51f5d2753ad992eedaadd215e9058b10b245581e.json similarity index 87% rename from .sqlx/query-0dcbd4cfa8f34d5fdf303988c606d7304ddfbb1fdbb4e513c92bf7059ba228c7.json rename to .sqlx/query-21d29424c3a8df807ea526da51f5d2753ad992eedaadd215e9058b10b245581e.json index db9a09bd80..98185b0c2f 100644 --- a/.sqlx/query-0dcbd4cfa8f34d5fdf303988c606d7304ddfbb1fdbb4e513c92bf7059ba228c7.json +++ b/.sqlx/query-21d29424c3a8df807ea526da51f5d2753ad992eedaadd215e9058b10b245581e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT u.id, u.username, u.password_hash, u.last_name, u.first_name, u.email, u.phone, u.mfa_enabled, u.totp_enabled, u.email_mfa_enabled, u.totp_secret, u.email_mfa_secret, u.mfa_method \"mfa_method: _\", u.recovery_codes, u.is_active, u.openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" u WHERE EXISTS (SELECT 1 FROM group_user gu LEFT JOIN \"group\" g ON gu.group_id = g.id WHERE is_admin AND user_id = u.id) AND u.is_active", + "query": "SELECT u.id, u.username, u.password_hash, u.last_name, u.first_name, u.email, u.phone, u.mfa_enabled, u.totp_enabled, u.email_mfa_enabled, u.totp_secret, u.email_mfa_secret, u.mfa_method \"mfa_method: _\", u.recovery_codes, u.is_active, u.openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending FROM \"user\" u WHERE EXISTS (SELECT 1 FROM group_user gu LEFT JOIN \"group\" g ON gu.group_id = g.id WHERE is_admin AND user_id = u.id) AND u.is_active", "describe": { "columns": [ { @@ -117,6 +117,11 @@ }, { "ordinal": 20, + "name": "ldap_remote_enrollment_completed", + "type_info": "Bool" + }, + { + "ordinal": 21, "name": "enrollment_pending", "type_info": "Bool" } @@ -145,8 +150,9 @@ false, true, true, + false, false ] }, - "hash": "0dcbd4cfa8f34d5fdf303988c606d7304ddfbb1fdbb4e513c92bf7059ba228c7" + "hash": "21d29424c3a8df807ea526da51f5d2753ad992eedaadd215e9058b10b245581e" } diff --git a/.sqlx/query-523933d20a62a730c3eb881814200ed1b03ab9dbe8de5368321466b0256d028c.json b/.sqlx/query-2540640625dbc36da2632daf756fa43cccaec4da1b6317696f892f878aadd96d.json similarity index 71% rename from .sqlx/query-523933d20a62a730c3eb881814200ed1b03ab9dbe8de5368321466b0256d028c.json rename to .sqlx/query-2540640625dbc36da2632daf756fa43cccaec4da1b6317696f892f878aadd96d.json index cf84d03729..29a6b68de1 100644 --- a/.sqlx/query-523933d20a62a730c3eb881814200ed1b03ab9dbe8de5368321466b0256d028c.json +++ b/.sqlx/query-2540640625dbc36da2632daf756fa43cccaec4da1b6317696f892f878aadd96d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"user\" (\"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"from_ldap\",\"ldap_pass_randomized\",\"ldap_rdn\",\"ldap_user_path\",\"openid_sub\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\",\"recovery_codes\",\"enrollment_pending\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20) RETURNING id", + "query": "INSERT INTO \"user\" (\"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"from_ldap\",\"ldap_pass_randomized\",\"ldap_rdn\",\"ldap_user_path\",\"ldap_remote_enrollment_completed\",\"openid_sub\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\",\"recovery_codes\",\"enrollment_pending\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21) RETURNING id", "describe": { "columns": [ { @@ -23,6 +23,7 @@ "Bool", "Text", "Text", + "Bool", "Text", "Bool", "Bool", @@ -49,5 +50,5 @@ false ] }, - "hash": "523933d20a62a730c3eb881814200ed1b03ab9dbe8de5368321466b0256d028c" + "hash": "2540640625dbc36da2632daf756fa43cccaec4da1b6317696f892f878aadd96d" } diff --git a/.sqlx/query-9a96f2f262ba86ab421f5f3b08984707951915fdac31ecc117dfbdbd0434366d.json b/.sqlx/query-25c223aecb861460fae25048225bc6005e322fbd1a8208a29a4ad662e58045c0.json similarity index 90% rename from .sqlx/query-9a96f2f262ba86ab421f5f3b08984707951915fdac31ecc117dfbdbd0434366d.json rename to .sqlx/query-25c223aecb861460fae25048225bc6005e322fbd1a8208a29a4ad662e58045c0.json index bb0ffd7dcf..e007ea16df 100644 --- a/.sqlx/query-9a96f2f262ba86ab421f5f3b08984707951915fdac31ecc117dfbdbd0434366d.json +++ b/.sqlx/query-25c223aecb861460fae25048225bc6005e322fbd1a8208a29a4ad662e58045c0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" WHERE id = ANY($1)", + "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending FROM \"user\" WHERE id = ANY($1)", "describe": { "columns": [ { @@ -117,6 +117,11 @@ }, { "ordinal": 20, + "name": "ldap_remote_enrollment_completed", + "type_info": "Bool" + }, + { + "ordinal": 21, "name": "enrollment_pending", "type_info": "Bool" } @@ -147,8 +152,9 @@ false, true, true, + false, false ] }, - "hash": "9a96f2f262ba86ab421f5f3b08984707951915fdac31ecc117dfbdbd0434366d" + "hash": "25c223aecb861460fae25048225bc6005e322fbd1a8208a29a4ad662e58045c0" } diff --git a/.sqlx/query-ab17ac33cd462975a6e8975d4a8cc6700f42030c507917ef9fb3acf3523a802b.json b/.sqlx/query-33bbbc2b7a47acce0d333a71f0edf12111864c122a9f2e1d539a80551350013c.json similarity index 90% rename from .sqlx/query-ab17ac33cd462975a6e8975d4a8cc6700f42030c507917ef9fb3acf3523a802b.json rename to .sqlx/query-33bbbc2b7a47acce0d333a71f0edf12111864c122a9f2e1d539a80551350013c.json index f5424ec86f..32e811cebf 100644 --- a/.sqlx/query-ab17ac33cd462975a6e8975d4a8cc6700f42030c507917ef9fb3acf3523a802b.json +++ b/.sqlx/query-33bbbc2b7a47acce0d333a71f0edf12111864c122a9f2e1d539a80551350013c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" WHERE is_active", + "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending FROM \"user\" WHERE is_active", "describe": { "columns": [ { @@ -117,6 +117,11 @@ }, { "ordinal": 20, + "name": "ldap_remote_enrollment_completed", + "type_info": "Bool" + }, + { + "ordinal": 21, "name": "enrollment_pending", "type_info": "Bool" } @@ -145,8 +150,9 @@ false, true, true, + false, false ] }, - "hash": "ab17ac33cd462975a6e8975d4a8cc6700f42030c507917ef9fb3acf3523a802b" + "hash": "33bbbc2b7a47acce0d333a71f0edf12111864c122a9f2e1d539a80551350013c" } diff --git a/.sqlx/query-3a6870df1845033d891e88530acb40d355f476044a7ce5a20dd55d4abbb6321a.json b/.sqlx/query-3a6870df1845033d891e88530acb40d355f476044a7ce5a20dd55d4abbb6321a.json new file mode 100644 index 0000000000..156ced9da0 --- /dev/null +++ b/.sqlx/query-3a6870df1845033d891e88530acb40d355f476044a7ce5a20dd55d4abbb6321a.json @@ -0,0 +1,160 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending FROM \"user\" WHERE email = ANY($1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "password_hash", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "last_name", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "first_name", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "email", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "phone", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "mfa_enabled", + "type_info": "Bool" + }, + { + "ordinal": 8, + "name": "totp_enabled", + "type_info": "Bool" + }, + { + "ordinal": 9, + "name": "email_mfa_enabled", + "type_info": "Bool" + }, + { + "ordinal": 10, + "name": "totp_secret", + "type_info": "Bytea" + }, + { + "ordinal": 11, + "name": "email_mfa_secret", + "type_info": "Bytea" + }, + { + "ordinal": 12, + "name": "mfa_method: _", + "type_info": { + "Custom": { + "name": "mfa_method", + "kind": { + "Enum": [ + "none", + "one_time_password", + "webauthn", + "email" + ] + } + } + } + }, + { + "ordinal": 13, + "name": "recovery_codes", + "type_info": "TextArray" + }, + { + "ordinal": 14, + "name": "is_active", + "type_info": "Bool" + }, + { + "ordinal": 15, + "name": "openid_sub", + "type_info": "Text" + }, + { + "ordinal": 16, + "name": "from_ldap", + "type_info": "Bool" + }, + { + "ordinal": 17, + "name": "ldap_pass_randomized", + "type_info": "Bool" + }, + { + "ordinal": 18, + "name": "ldap_rdn", + "type_info": "Text" + }, + { + "ordinal": 19, + "name": "ldap_user_path", + "type_info": "Text" + }, + { + "ordinal": 20, + "name": "ldap_remote_enrollment_completed", + "type_info": "Bool" + }, + { + "ordinal": 21, + "name": "enrollment_pending", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "TextArray" + ] + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + true, + false, + false, + false, + true, + true, + false, + false, + false, + true, + false, + false, + true, + true, + false, + false + ] + }, + "hash": "3a6870df1845033d891e88530acb40d355f476044a7ce5a20dd55d4abbb6321a" +} diff --git a/.sqlx/query-eb84d653ce75cd9124e26eeb147a8f11aecbf5ef4296d00ba3f4e9b9c64165da.json b/.sqlx/query-6788faaac53f42dbf2bf68025e1338be91f5e0190b75e9ab8838df76e5168fb3.json similarity index 86% rename from .sqlx/query-eb84d653ce75cd9124e26eeb147a8f11aecbf5ef4296d00ba3f4e9b9c64165da.json rename to .sqlx/query-6788faaac53f42dbf2bf68025e1338be91f5e0190b75e9ab8838df76e5168fb3.json index bfe4d536fe..075749e9ac 100644 --- a/.sqlx/query-eb84d653ce75cd9124e26eeb147a8f11aecbf5ef4296d00ba3f4e9b9c64165da.json +++ b/.sqlx/query-6788faaac53f42dbf2bf68025e1338be91f5e0190b75e9ab8838df76e5168fb3.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"from_ldap\",\"ldap_pass_randomized\",\"ldap_rdn\",\"ldap_user_path\",\"openid_sub\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\" \"mfa_method: _\",\"recovery_codes\" \"recovery_codes: _\",\"enrollment_pending\" FROM \"user\" LIMIT $1 OFFSET $2", + "query": "SELECT id, \"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"from_ldap\",\"ldap_pass_randomized\",\"ldap_rdn\",\"ldap_user_path\",\"ldap_remote_enrollment_completed\",\"openid_sub\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\" \"mfa_method: _\",\"recovery_codes\" \"recovery_codes: _\",\"enrollment_pending\" FROM \"user\" WHERE id = $1", "describe": { "columns": [ { @@ -70,31 +70,36 @@ }, { "ordinal": 13, + "name": "ldap_remote_enrollment_completed", + "type_info": "Bool" + }, + { + "ordinal": 14, "name": "openid_sub", "type_info": "Text" }, { - "ordinal": 14, + "ordinal": 15, "name": "totp_enabled", "type_info": "Bool" }, { - "ordinal": 15, + "ordinal": 16, "name": "email_mfa_enabled", "type_info": "Bool" }, { - "ordinal": 16, + "ordinal": 17, "name": "totp_secret", "type_info": "Bytea" }, { - "ordinal": 17, + "ordinal": 18, "name": "email_mfa_secret", "type_info": "Bytea" }, { - "ordinal": 18, + "ordinal": 19, "name": "mfa_method: _", "type_info": { "Custom": { @@ -111,19 +116,18 @@ } }, { - "ordinal": 19, + "ordinal": 20, "name": "recovery_codes: _", "type_info": "TextArray" }, { - "ordinal": 20, + "ordinal": 21, "name": "enrollment_pending", "type_info": "Bool" } ], "parameters": { "Left": [ - "Int8", "Int8" ] }, @@ -141,6 +145,7 @@ false, true, true, + false, true, false, false, @@ -151,5 +156,5 @@ false ] }, - "hash": "eb84d653ce75cd9124e26eeb147a8f11aecbf5ef4296d00ba3f4e9b9c64165da" + "hash": "6788faaac53f42dbf2bf68025e1338be91f5e0190b75e9ab8838df76e5168fb3" } diff --git a/.sqlx/query-37214c3279207b0d6e33b46726f35ed13ec41c1209e82412c4f7a70d39b21aca.json b/.sqlx/query-76553e0ae70872ba36db324a5e2b19f0b841e426d876eba72ae5c4f5b5b54513.json similarity index 89% rename from .sqlx/query-37214c3279207b0d6e33b46726f35ed13ec41c1209e82412c4f7a70d39b21aca.json rename to .sqlx/query-76553e0ae70872ba36db324a5e2b19f0b841e426d876eba72ae5c4f5b5b54513.json index 0de478df57..0793719fb8 100644 --- a/.sqlx/query-37214c3279207b0d6e33b46726f35ed13ec41c1209e82412c4f7a70d39b21aca.json +++ b/.sqlx/query-76553e0ae70872ba36db324a5e2b19f0b841e426d876eba72ae5c4f5b5b54513.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" WHERE ldap_user_path IS NULL\n ", + "query": "\n SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending FROM \"user\" WHERE ldap_user_path IS NULL\n ", "describe": { "columns": [ { @@ -117,6 +117,11 @@ }, { "ordinal": 20, + "name": "ldap_remote_enrollment_completed", + "type_info": "Bool" + }, + { + "ordinal": 21, "name": "enrollment_pending", "type_info": "Bool" } @@ -145,8 +150,9 @@ false, true, true, + false, false ] }, - "hash": "37214c3279207b0d6e33b46726f35ed13ec41c1209e82412c4f7a70d39b21aca" + "hash": "76553e0ae70872ba36db324a5e2b19f0b841e426d876eba72ae5c4f5b5b54513" } diff --git a/.sqlx/query-f3568532db86448be0fcdc2bcdea805d0b5b6a222ed4036da7880c57c9790091.json b/.sqlx/query-7f7f2da3cabcf74dcf9aa1f4711ee99229d9197e9a1cf2cdefe154ba275dad8c.json similarity index 86% rename from .sqlx/query-f3568532db86448be0fcdc2bcdea805d0b5b6a222ed4036da7880c57c9790091.json rename to .sqlx/query-7f7f2da3cabcf74dcf9aa1f4711ee99229d9197e9a1cf2cdefe154ba275dad8c.json index 01a2303485..90f0a3b214 100644 --- a/.sqlx/query-f3568532db86448be0fcdc2bcdea805d0b5b6a222ed4036da7880c57c9790091.json +++ b/.sqlx/query-7f7f2da3cabcf74dcf9aa1f4711ee99229d9197e9a1cf2cdefe154ba275dad8c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"from_ldap\",\"ldap_pass_randomized\",\"ldap_rdn\",\"ldap_user_path\",\"openid_sub\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\" \"mfa_method: _\",\"recovery_codes\" \"recovery_codes: _\",\"enrollment_pending\" FROM \"user\"", + "query": "SELECT id, \"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"from_ldap\",\"ldap_pass_randomized\",\"ldap_rdn\",\"ldap_user_path\",\"ldap_remote_enrollment_completed\",\"openid_sub\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\" \"mfa_method: _\",\"recovery_codes\" \"recovery_codes: _\",\"enrollment_pending\" FROM \"user\"", "describe": { "columns": [ { @@ -70,31 +70,36 @@ }, { "ordinal": 13, + "name": "ldap_remote_enrollment_completed", + "type_info": "Bool" + }, + { + "ordinal": 14, "name": "openid_sub", "type_info": "Text" }, { - "ordinal": 14, + "ordinal": 15, "name": "totp_enabled", "type_info": "Bool" }, { - "ordinal": 15, + "ordinal": 16, "name": "email_mfa_enabled", "type_info": "Bool" }, { - "ordinal": 16, + "ordinal": 17, "name": "totp_secret", "type_info": "Bytea" }, { - "ordinal": 17, + "ordinal": 18, "name": "email_mfa_secret", "type_info": "Bytea" }, { - "ordinal": 18, + "ordinal": 19, "name": "mfa_method: _", "type_info": { "Custom": { @@ -111,12 +116,12 @@ } }, { - "ordinal": 19, + "ordinal": 20, "name": "recovery_codes: _", "type_info": "TextArray" }, { - "ordinal": 20, + "ordinal": 21, "name": "enrollment_pending", "type_info": "Bool" } @@ -138,6 +143,7 @@ false, true, true, + false, true, false, false, @@ -148,5 +154,5 @@ false ] }, - "hash": "f3568532db86448be0fcdc2bcdea805d0b5b6a222ed4036da7880c57c9790091" + "hash": "7f7f2da3cabcf74dcf9aa1f4711ee99229d9197e9a1cf2cdefe154ba275dad8c" } diff --git a/.sqlx/query-fd671557afabd523045f73a2f32e876821bd29b49c096d798754857b22089c21.json b/.sqlx/query-898fd61e48f35b9befabec12c7b71d0c45b63dad212faea2a7b15b05161e1b12.json similarity index 90% rename from .sqlx/query-fd671557afabd523045f73a2f32e876821bd29b49c096d798754857b22089c21.json rename to .sqlx/query-898fd61e48f35b9befabec12c7b71d0c45b63dad212faea2a7b15b05161e1b12.json index 5280946e04..24d5834c4f 100644 --- a/.sqlx/query-fd671557afabd523045f73a2f32e876821bd29b49c096d798754857b22089c21.json +++ b/.sqlx/query-898fd61e48f35b9befabec12c7b71d0c45b63dad212faea2a7b15b05161e1b12.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" WHERE username = $1", + "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending FROM \"user\" WHERE username = $1", "describe": { "columns": [ { @@ -117,6 +117,11 @@ }, { "ordinal": 20, + "name": "ldap_remote_enrollment_completed", + "type_info": "Bool" + }, + { + "ordinal": 21, "name": "enrollment_pending", "type_info": "Bool" } @@ -147,8 +152,9 @@ false, true, true, + false, false ] }, - "hash": "fd671557afabd523045f73a2f32e876821bd29b49c096d798754857b22089c21" + "hash": "898fd61e48f35b9befabec12c7b71d0c45b63dad212faea2a7b15b05161e1b12" } diff --git a/.sqlx/query-d1595e6cfd8947354669341ac26ac5d84efa6746a9cff3e28db6d8be28e7f69c.json b/.sqlx/query-91d9845411ad48a9847aa65c675ef1f6ff2537571d41b17e12c3379e261faafe.json similarity index 90% rename from .sqlx/query-d1595e6cfd8947354669341ac26ac5d84efa6746a9cff3e28db6d8be28e7f69c.json rename to .sqlx/query-91d9845411ad48a9847aa65c675ef1f6ff2537571d41b17e12c3379e261faafe.json index c2515c8b66..61d57f3cad 100644 --- a/.sqlx/query-d1595e6cfd8947354669341ac26ac5d84efa6746a9cff3e28db6d8be28e7f69c.json +++ b/.sqlx/query-91d9845411ad48a9847aa65c675ef1f6ff2537571d41b17e12c3379e261faafe.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" WHERE id = $1", + "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending FROM \"user\" WHERE id = $1", "describe": { "columns": [ { @@ -117,6 +117,11 @@ }, { "ordinal": 20, + "name": "ldap_remote_enrollment_completed", + "type_info": "Bool" + }, + { + "ordinal": 21, "name": "enrollment_pending", "type_info": "Bool" } @@ -147,8 +152,9 @@ false, true, true, + false, false ] }, - "hash": "d1595e6cfd8947354669341ac26ac5d84efa6746a9cff3e28db6d8be28e7f69c" + "hash": "91d9845411ad48a9847aa65c675ef1f6ff2537571d41b17e12c3379e261faafe" } diff --git a/.sqlx/query-5b9e2982325e824daab097a76e29c23d7e5d4ebac61f87d98c441a12e14e8774.json b/.sqlx/query-94d375808a70533f9e2c09eff7ef7a02214cb9a1cb0b90e5b87304a6cdd866d8.json similarity index 88% rename from .sqlx/query-5b9e2982325e824daab097a76e29c23d7e5d4ebac61f87d98c441a12e14e8774.json rename to .sqlx/query-94d375808a70533f9e2c09eff7ef7a02214cb9a1cb0b90e5b87304a6cdd866d8.json index 48a9989af2..137d8f49d2 100644 --- a/.sqlx/query-5b9e2982325e824daab097a76e29c23d7e5d4ebac61f87d98c441a12e14e8774.json +++ b/.sqlx/query-94d375808a70533f9e2c09eff7ef7a02214cb9a1cb0b90e5b87304a6cdd866d8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT \"user\".id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" JOIN group_user ON \"user\".id = group_user.user_id WHERE group_user.group_id = $1", + "query": "SELECT \"user\".id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending FROM \"user\" JOIN group_user ON \"user\".id = group_user.user_id WHERE group_user.group_id = $1", "describe": { "columns": [ { @@ -117,6 +117,11 @@ }, { "ordinal": 20, + "name": "ldap_remote_enrollment_completed", + "type_info": "Bool" + }, + { + "ordinal": 21, "name": "enrollment_pending", "type_info": "Bool" } @@ -147,8 +152,9 @@ false, true, true, + false, false ] }, - "hash": "5b9e2982325e824daab097a76e29c23d7e5d4ebac61f87d98c441a12e14e8774" + "hash": "94d375808a70533f9e2c09eff7ef7a02214cb9a1cb0b90e5b87304a6cdd866d8" } diff --git a/.sqlx/query-b855a2bc8e31ac52f8e23f783652116227f55516d17150bd4f370d21f2e1a46b.json b/.sqlx/query-a5efa87acd3b331ae142a5f0b4f3daf814b72b1c2c7b22d96a2ebf870cf388dd.json similarity index 73% rename from .sqlx/query-b855a2bc8e31ac52f8e23f783652116227f55516d17150bd4f370d21f2e1a46b.json rename to .sqlx/query-a5efa87acd3b331ae142a5f0b4f3daf814b72b1c2c7b22d96a2ebf870cf388dd.json index 22086c1e09..1f6ac09cef 100644 --- a/.sqlx/query-b855a2bc8e31ac52f8e23f783652116227f55516d17150bd4f370d21f2e1a46b.json +++ b/.sqlx/query-a5efa87acd3b331ae142a5f0b4f3daf814b72b1c2c7b22d96a2ebf870cf388dd.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"user\" SET \"username\" = $2,\"password_hash\" = $3,\"last_name\" = $4,\"first_name\" = $5,\"email\" = $6,\"phone\" = $7,\"mfa_enabled\" = $8,\"is_active\" = $9,\"from_ldap\" = $10,\"ldap_pass_randomized\" = $11,\"ldap_rdn\" = $12,\"ldap_user_path\" = $13,\"openid_sub\" = $14,\"totp_enabled\" = $15,\"email_mfa_enabled\" = $16,\"totp_secret\" = $17,\"email_mfa_secret\" = $18,\"mfa_method\" = $19,\"recovery_codes\" = $20,\"enrollment_pending\" = $21 WHERE id = $1", + "query": "UPDATE \"user\" SET \"username\" = $2,\"password_hash\" = $3,\"last_name\" = $4,\"first_name\" = $5,\"email\" = $6,\"phone\" = $7,\"mfa_enabled\" = $8,\"is_active\" = $9,\"from_ldap\" = $10,\"ldap_pass_randomized\" = $11,\"ldap_rdn\" = $12,\"ldap_user_path\" = $13,\"ldap_remote_enrollment_completed\" = $14,\"openid_sub\" = $15,\"totp_enabled\" = $16,\"email_mfa_enabled\" = $17,\"totp_secret\" = $18,\"email_mfa_secret\" = $19,\"mfa_method\" = $20,\"recovery_codes\" = $21,\"enrollment_pending\" = $22 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -18,6 +18,7 @@ "Bool", "Text", "Text", + "Bool", "Text", "Bool", "Bool", @@ -42,5 +43,5 @@ }, "nullable": [] }, - "hash": "b855a2bc8e31ac52f8e23f783652116227f55516d17150bd4f370d21f2e1a46b" + "hash": "a5efa87acd3b331ae142a5f0b4f3daf814b72b1c2c7b22d96a2ebf870cf388dd" } diff --git a/.sqlx/query-1151dfec098a686f3d15154de87b701cc13c06dacb8981cba46c9d5a1fcc69e7.json b/.sqlx/query-b8ca90c8d5135c7020ba287bfdab3e2dfeb6e14bfae355fe5216feeb21796883.json similarity index 88% rename from .sqlx/query-1151dfec098a686f3d15154de87b701cc13c06dacb8981cba46c9d5a1fcc69e7.json rename to .sqlx/query-b8ca90c8d5135c7020ba287bfdab3e2dfeb6e14bfae355fe5216feeb21796883.json index b439c1b326..309f6b8166 100644 --- a/.sqlx/query-1151dfec098a686f3d15154de87b701cc13c06dacb8981cba46c9d5a1fcc69e7.json +++ b/.sqlx/query-b8ca90c8d5135c7020ba287bfdab3e2dfeb6e14bfae355fe5216feeb21796883.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT u.id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM aclruleuser r JOIN \"user\" u ON u.id = r.user_id WHERE r.rule_id = $1 AND NOT r.allow AND u.is_active", + "query": "SELECT u.id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending FROM aclruleuser r JOIN \"user\" u ON u.id = r.user_id WHERE r.rule_id = $1 AND NOT r.allow AND u.is_active", "describe": { "columns": [ { @@ -117,6 +117,11 @@ }, { "ordinal": 20, + "name": "ldap_remote_enrollment_completed", + "type_info": "Bool" + }, + { + "ordinal": 21, "name": "enrollment_pending", "type_info": "Bool" } @@ -147,8 +152,9 @@ false, true, true, + false, false ] }, - "hash": "1151dfec098a686f3d15154de87b701cc13c06dacb8981cba46c9d5a1fcc69e7" + "hash": "b8ca90c8d5135c7020ba287bfdab3e2dfeb6e14bfae355fe5216feeb21796883" } diff --git a/.sqlx/query-e5a4b8671069997f4ae7fc38c33ffbae9869524f2ecf68ee407ab9ac4fae4cb4.json b/.sqlx/query-bd8af5f0863b7f96407000b87253a8b5bdaf9555cdd212dc2975828387262ff2.json similarity index 88% rename from .sqlx/query-e5a4b8671069997f4ae7fc38c33ffbae9869524f2ecf68ee407ab9ac4fae4cb4.json rename to .sqlx/query-bd8af5f0863b7f96407000b87253a8b5bdaf9555cdd212dc2975828387262ff2.json index aeb15fac8e..323f67f9e6 100644 --- a/.sqlx/query-e5a4b8671069997f4ae7fc38c33ffbae9869524f2ecf68ee407ab9ac4fae4cb4.json +++ b/.sqlx/query-bd8af5f0863b7f96407000b87253a8b5bdaf9555cdd212dc2975828387262ff2.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" u JOIN group_user gu ON u.id=gu.user_id WHERE u.is_active=true AND gu.group_id=ANY($1)", + "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending FROM \"user\" u JOIN group_user gu ON u.id=gu.user_id WHERE u.is_active=true AND gu.group_id=ANY($1)", "describe": { "columns": [ { @@ -117,6 +117,11 @@ }, { "ordinal": 20, + "name": "ldap_remote_enrollment_completed", + "type_info": "Bool" + }, + { + "ordinal": 21, "name": "enrollment_pending", "type_info": "Bool" } @@ -147,8 +152,9 @@ false, true, true, + false, false ] }, - "hash": "e5a4b8671069997f4ae7fc38c33ffbae9869524f2ecf68ee407ab9ac4fae4cb4" + "hash": "bd8af5f0863b7f96407000b87253a8b5bdaf9555cdd212dc2975828387262ff2" } diff --git a/.sqlx/query-afbebcf9d6784d63227376d154a35a002501d911b81f03ae5f969e69a9a4ffa5.json b/.sqlx/query-cb7d346e5385fe780f0a2e0fb333779aa50b25176fe9e7799db2d701b47ea7f0.json similarity index 90% rename from .sqlx/query-afbebcf9d6784d63227376d154a35a002501d911b81f03ae5f969e69a9a4ffa5.json rename to .sqlx/query-cb7d346e5385fe780f0a2e0fb333779aa50b25176fe9e7799db2d701b47ea7f0.json index 0b95519c07..c81ce9a035 100644 --- a/.sqlx/query-afbebcf9d6784d63227376d154a35a002501d911b81f03ae5f969e69a9a4ffa5.json +++ b/.sqlx/query-cb7d346e5385fe780f0a2e0fb333779aa50b25176fe9e7799db2d701b47ea7f0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT u.id, u.username, u.password_hash, u.last_name, u.first_name, u.email, u.phone, u.mfa_enabled, u.totp_enabled, u.email_mfa_enabled, u.totp_secret, u.email_mfa_secret, u.mfa_method \"mfa_method: _\", u.recovery_codes, u.is_active, u.openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" u JOIN \"device\" d ON u.id = d.user_id WHERE d.id = $1", + "query": "SELECT u.id, u.username, u.password_hash, u.last_name, u.first_name, u.email, u.phone, u.mfa_enabled, u.totp_enabled, u.email_mfa_enabled, u.totp_secret, u.email_mfa_secret, u.mfa_method \"mfa_method: _\", u.recovery_codes, u.is_active, u.openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending FROM \"user\" u JOIN \"device\" d ON u.id = d.user_id WHERE d.id = $1", "describe": { "columns": [ { @@ -117,6 +117,11 @@ }, { "ordinal": 20, + "name": "ldap_remote_enrollment_completed", + "type_info": "Bool" + }, + { + "ordinal": 21, "name": "enrollment_pending", "type_info": "Bool" } @@ -147,8 +152,9 @@ false, true, true, + false, false ] }, - "hash": "afbebcf9d6784d63227376d154a35a002501d911b81f03ae5f969e69a9a4ffa5" + "hash": "cb7d346e5385fe780f0a2e0fb333779aa50b25176fe9e7799db2d701b47ea7f0" } diff --git a/.sqlx/query-eabe841f211c8fab042d3dbb0166610188ce73aa66e76726aeaedfbb4ecf3290.json b/.sqlx/query-ce08d19023028c4797c2ca33ff76824b882f17f5891bb512deff13a2c26069bc.json similarity index 88% rename from .sqlx/query-eabe841f211c8fab042d3dbb0166610188ce73aa66e76726aeaedfbb4ecf3290.json rename to .sqlx/query-ce08d19023028c4797c2ca33ff76824b882f17f5891bb512deff13a2c26069bc.json index 18047abbe1..3b569125bd 100644 --- a/.sqlx/query-eabe841f211c8fab042d3dbb0166610188ce73aa66e76726aeaedfbb4ecf3290.json +++ b/.sqlx/query-ce08d19023028c4797c2ca33ff76824b882f17f5891bb512deff13a2c26069bc.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT u.id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM aclruleuser r JOIN \"user\" u ON u.id = r.user_id WHERE r.rule_id = $1 AND r.allow AND u.is_active", + "query": "SELECT u.id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending FROM aclruleuser r JOIN \"user\" u ON u.id = r.user_id WHERE r.rule_id = $1 AND r.allow AND u.is_active", "describe": { "columns": [ { @@ -117,6 +117,11 @@ }, { "ordinal": 20, + "name": "ldap_remote_enrollment_completed", + "type_info": "Bool" + }, + { + "ordinal": 21, "name": "enrollment_pending", "type_info": "Bool" } @@ -147,8 +152,9 @@ false, true, true, + false, false ] }, - "hash": "eabe841f211c8fab042d3dbb0166610188ce73aa66e76726aeaedfbb4ecf3290" + "hash": "ce08d19023028c4797c2ca33ff76824b882f17f5891bb512deff13a2c26069bc" } diff --git a/.sqlx/query-98d0dc6568f54ebee7efaf5ee6965afbee229e80bbaecc1b321c9e309c70409c.json b/.sqlx/query-dc8062f91f1ae908ec3e81cf0195a76f38faf1a26df3ff4c045a8a9bbd7f73de.json similarity index 86% rename from .sqlx/query-98d0dc6568f54ebee7efaf5ee6965afbee229e80bbaecc1b321c9e309c70409c.json rename to .sqlx/query-dc8062f91f1ae908ec3e81cf0195a76f38faf1a26df3ff4c045a8a9bbd7f73de.json index 3ba7d6e933..fc82447453 100644 --- a/.sqlx/query-98d0dc6568f54ebee7efaf5ee6965afbee229e80bbaecc1b321c9e309c70409c.json +++ b/.sqlx/query-dc8062f91f1ae908ec3e81cf0195a76f38faf1a26df3ff4c045a8a9bbd7f73de.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT \"user\".id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" INNER JOIN \"group_user\" ON \"user\".id = \"group_user\".user_id INNER JOIN \"group\" ON \"group_user\".group_id = \"group\".id WHERE \"group\".name = $1", + "query": "SELECT \"user\".id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending FROM \"user\" INNER JOIN \"group_user\" ON \"user\".id = \"group_user\".user_id INNER JOIN \"group\" ON \"group_user\".group_id = \"group\".id WHERE \"group\".name = $1", "describe": { "columns": [ { @@ -117,6 +117,11 @@ }, { "ordinal": 20, + "name": "ldap_remote_enrollment_completed", + "type_info": "Bool" + }, + { + "ordinal": 21, "name": "enrollment_pending", "type_info": "Bool" } @@ -147,8 +152,9 @@ false, true, true, + false, false ] }, - "hash": "98d0dc6568f54ebee7efaf5ee6965afbee229e80bbaecc1b321c9e309c70409c" + "hash": "dc8062f91f1ae908ec3e81cf0195a76f38faf1a26df3ff4c045a8a9bbd7f73de" } diff --git a/.sqlx/query-c973c8ca54523b237b7b3b4c95d9603a13056d395734ba2399cd11c146f4d473.json b/.sqlx/query-e20bb4d7529adc50b620a5a96bdccea9f2f51f0f45e1833555263cb776daf0d0.json similarity index 90% rename from .sqlx/query-c973c8ca54523b237b7b3b4c95d9603a13056d395734ba2399cd11c146f4d473.json rename to .sqlx/query-e20bb4d7529adc50b620a5a96bdccea9f2f51f0f45e1833555263cb776daf0d0.json index 6e0608e1d0..e29f0eff37 100644 --- a/.sqlx/query-c973c8ca54523b237b7b3b4c95d9603a13056d395734ba2399cd11c146f4d473.json +++ b/.sqlx/query-e20bb4d7529adc50b620a5a96bdccea9f2f51f0f45e1833555263cb776daf0d0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" WHERE email ILIKE $1", + "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending FROM \"user\" WHERE email ILIKE $1", "describe": { "columns": [ { @@ -117,6 +117,11 @@ }, { "ordinal": 20, + "name": "ldap_remote_enrollment_completed", + "type_info": "Bool" + }, + { + "ordinal": 21, "name": "enrollment_pending", "type_info": "Bool" } @@ -147,8 +152,9 @@ false, true, true, + false, false ] }, - "hash": "c973c8ca54523b237b7b3b4c95d9603a13056d395734ba2399cd11c146f4d473" + "hash": "e20bb4d7529adc50b620a5a96bdccea9f2f51f0f45e1833555263cb776daf0d0" } diff --git a/.sqlx/query-1a382c085f293eae11e2ea23a82fe7bf65e8bb18b6a430a898bfeb4ceb443cbc.json b/.sqlx/query-fa5d5b618ddd5c71b09d1b22eec063b8e4b4cbaf79fee6e28f7703352588b8ab.json similarity index 91% rename from .sqlx/query-1a382c085f293eae11e2ea23a82fe7bf65e8bb18b6a430a898bfeb4ceb443cbc.json rename to .sqlx/query-fa5d5b618ddd5c71b09d1b22eec063b8e4b4cbaf79fee6e28f7703352588b8ab.json index 9db9da6aed..4285c6ab83 100644 --- a/.sqlx/query-1a382c085f293eae11e2ea23a82fe7bf65e8bb18b6a430a898bfeb4ceb443cbc.json +++ b/.sqlx/query-fa5d5b618ddd5c71b09d1b22eec063b8e4b4cbaf79fee6e28f7703352588b8ab.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_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\", 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, ldap_remote_enrollment_enabled, ldap_remote_enrollment_send_invite, openid_username_handling \"openid_username_handling: OpenIdUsernameHandling\", 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": [ { @@ -266,6 +266,16 @@ }, { "ordinal": 48, + "name": "ldap_remote_enrollment_enabled", + "type_info": "Bool" + }, + { + "ordinal": 49, + "name": "ldap_remote_enrollment_send_invite", + "type_info": "Bool" + }, + { + "ordinal": 50, "name": "openid_username_handling: OpenIdUsernameHandling", "type_info": { "Custom": { @@ -281,72 +291,72 @@ } }, { - "ordinal": 49, + "ordinal": 51, "name": "defguard_url", "type_info": "Text" }, { - "ordinal": 50, + "ordinal": 52, "name": "default_admin_group_name", "type_info": "Text" }, { - "ordinal": 51, + "ordinal": 53, "name": "authentication_period_days", "type_info": "Int4" }, { - "ordinal": 52, + "ordinal": 54, "name": "mfa_code_timeout_seconds", "type_info": "Int4" }, { - "ordinal": 53, + "ordinal": 55, "name": "public_proxy_url", "type_info": "Text" }, { - "ordinal": 54, + "ordinal": 56, "name": "default_admin_id", "type_info": "Int8" }, { - "ordinal": 55, + "ordinal": 57, "name": "secret_key", "type_info": "Text" }, { - "ordinal": 56, + "ordinal": 58, "name": "enable_stats_purge", "type_info": "Bool" }, { - "ordinal": 57, + "ordinal": 59, "name": "stats_purge_frequency_hours", "type_info": "Int4" }, { - "ordinal": 58, + "ordinal": 60, "name": "stats_purge_threshold_days", "type_info": "Int4" }, { - "ordinal": 59, + "ordinal": 61, "name": "enrollment_token_timeout_hours", "type_info": "Int4" }, { - "ordinal": 60, + "ordinal": 62, "name": "password_reset_token_timeout_hours", "type_info": "Int4" }, { - "ordinal": 61, + "ordinal": 63, "name": "enrollment_session_timeout_minutes", "type_info": "Int4" }, { - "ordinal": 62, + "ordinal": 64, "name": "password_reset_session_timeout_minutes", "type_info": "Int4" } @@ -409,6 +419,8 @@ false, false, false, + false, + false, true, true, false, @@ -420,5 +432,5 @@ false ] }, - "hash": "1a382c085f293eae11e2ea23a82fe7bf65e8bb18b6a430a898bfeb4ceb443cbc" + "hash": "fa5d5b618ddd5c71b09d1b22eec063b8e4b4cbaf79fee6e28f7703352588b8ab" } diff --git a/Cargo.lock b/Cargo.lock index b545effdb3..61a8e310bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,9 +175,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" dependencies = [ "rustversion", ] @@ -748,9 +748,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.58" +version = "1.2.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" dependencies = [ "find-msvc-tools", "jobserver", @@ -2149,9 +2149,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fax" @@ -2550,7 +2550,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.13.0", + "indexmap 2.13.1", "slab", "tokio", "tokio-util", @@ -3064,9 +3064,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" dependencies = [ "equivalent", "hashbrown 0.16.1", @@ -3289,9 +3289,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "lettre" -version = "0.11.20" +version = "0.11.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471816f3e24b85e820dee02cde962379ea1a669e5242f19c61bcbcffedf4c4fb" +checksum = "dabda5859ee7c06b995b9d1165aa52c39110e079ef609db97178d86aeb051fa7" dependencies = [ "async-trait", "base64 0.22.1", @@ -3372,9 +3372,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.25" +version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" +checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" dependencies = [ "cc", "libc", @@ -3590,7 +3590,7 @@ dependencies = [ "enum-as-inner", "enum_dispatch", "htmlparser", - "indexmap 2.13.0", + "indexmap 2.13.1", "itertools 0.14.0", "rustc-hash", "serde", @@ -4318,7 +4318,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", "hashbrown 0.15.5", - "indexmap 2.13.0", + "indexmap 2.13.1", ] [[package]] @@ -5394,9 +5394,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", "serde_core", @@ -5469,7 +5469,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" dependencies = [ "form_urlencoded", - "indexmap 2.13.0", + "indexmap 2.13.1", "itoa", "ryu", "serde_core", @@ -5510,9 +5510,9 @@ dependencies = [ [[package]] name = "serde_qs" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c742cd44662647326f86b514eadcc227fff4ce684dbbdaf1943f758d5ea058c" +checksum = "c2316d01592c3382277c5062105510e35e0a6bfb2851e30028485f7af8cf1240" dependencies = [ "itoa", "percent-encoding", @@ -5542,7 +5542,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.13.0", + "indexmap 2.13.1", "schemars 0.9.0", "schemars 1.2.1", "serde_core", @@ -5569,7 +5569,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.13.1", "itoa", "ryu", "serde", @@ -5828,7 +5828,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap 2.13.0", + "indexmap 2.13.1", "ipnetwork", "log", "memchr", @@ -6367,9 +6367,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" dependencies = [ "bytes", "libc", @@ -6383,9 +6383,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -6448,11 +6448,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.10+spec-1.1.0" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82418ca169e235e6c399a84e395ab6debeb3bc90edc959bf0f48647c6a32d1b" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.13.1", "toml_datetime", "toml_parser", "winnow", @@ -6571,7 +6571,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap 2.13.0", + "indexmap 2.13.1", "pin-project-lite", "slab", "sync_wrapper", @@ -6846,7 +6846,7 @@ version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.13.1", "serde", "serde_json", "utoipa-gen", @@ -7090,7 +7090,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.13.0", + "indexmap 2.13.1", "wasm-encoder", "wasmparser", ] @@ -7116,7 +7116,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags 2.11.0", "hashbrown 0.15.5", - "indexmap 2.13.0", + "indexmap 2.13.1", "semver", ] @@ -7616,7 +7616,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap 2.13.0", + "indexmap 2.13.1", "prettyplease", "syn", "wasm-metadata", @@ -7647,7 +7647,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags 2.11.0", - "indexmap 2.13.0", + "indexmap 2.13.1", "log", "serde", "serde_derive", @@ -7666,7 +7666,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.13.0", + "indexmap 2.13.1", "log", "semver", "serde", @@ -7678,9 +7678,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wyz" @@ -7880,7 +7880,7 @@ dependencies = [ "arbitrary", "crc32fast", "flate2", - "indexmap 2.13.0", + "indexmap 2.13.1", "memchr", "zopfli", ] diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs index 0fb36007ec..31a30f0e8f 100644 --- a/crates/defguard_common/src/db/models/device.rs +++ b/crates/defguard_common/src/db/models/device.rs @@ -1117,7 +1117,7 @@ impl Device { "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, \ totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending \ FROM \"user\" WHERE id = $1", self.user_id ) diff --git a/crates/defguard_common/src/db/models/group.rs b/crates/defguard_common/src/db/models/group.rs index a341b40251..eb81daad3a 100644 --- a/crates/defguard_common/src/db/models/group.rs +++ b/crates/defguard_common/src/db/models/group.rs @@ -83,7 +83,7 @@ impl Group { "SELECT \"user\".id, username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending \ FROM \"user\" \ JOIN group_user ON \"user\".id = group_user.user_id \ WHERE group_user.group_id = $1", diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index e5d32f3aaa..9cf57bdd86 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -50,6 +50,14 @@ pub async fn update_current_settings<'e, E: sqlx::PgExecutor<'e>>( pub enum SettingsValidationError { #[error("Cannot enable gateway disconnect notifications. SMTP is not configured")] CannotEnableGatewayNotifications, + #[error("Cannot enable remote enrollment for LDAP. LDAP and SMTP must both be configured")] + CannotEnableLdapRemoteEnrollment, + #[error( + "Cannot enable automatic invites for LDAP remote enrollment. LDAP remote enrollment is not enabled" + )] + CannotEnableLdapRemoteEnrollmentInvite, + #[error("Cannot enable LDAP. Required LDAP fields are not configured")] + CannotEnableLdap, #[error("Invalid defguard_url `{0}`, url has to be a domain, not IP")] InvalidDefguardUrl(String), } @@ -182,6 +190,8 @@ pub struct Settings { // The attribute which is used to map LDAP usernames to Defguard usernames pub ldap_user_rdn_attr: Option, pub ldap_sync_groups: Vec, + pub ldap_remote_enrollment_enabled: bool, + pub ldap_remote_enrollment_send_invite: bool, // Whether to create a new account when users try to log in with external OpenID pub openid_create_account: bool, pub openid_username_handling: OpenIdUsernameHandling, @@ -405,7 +415,7 @@ impl Settings { 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, \ + ldap_user_rdn_attr, ldap_sync_groups, ldap_remote_enrollment_enabled, ldap_remote_enrollment_send_invite, \ openid_username_handling \"openid_username_handling: OpenIdUsernameHandling\", \ defguard_url, \ default_admin_group_name, authentication_period_days, mfa_code_timeout_seconds, \ @@ -429,6 +439,7 @@ impl Settings { } self.build_webauthn() .map_err(|_| SettingsValidationError::InvalidDefguardUrl(self.defguard_url.clone()))?; + // Check if gateway disconnect notifications can be enabled, since it requires SMTP to be // configured. if self.gateway_disconnect_notifications_enabled && !self.smtp_configured() { @@ -436,6 +447,28 @@ impl Settings { return Err(SettingsValidationError::CannotEnableGatewayNotifications); } + // Check if LDAP can be enabled + if self.ldap_enabled && !self.ldap_configured() { + warn!("Cannot enable LDAP. Required fields are not configured."); + return Err(SettingsValidationError::CannotEnableLdap); + } + + // Check if LDAP remote enrollment can be enabled + if self.ldap_remote_enrollment_enabled && !self.smtp_configured() { + warn!("Cannot enable remote enrollment for LDAP. SMTP is not configured."); + return Err(SettingsValidationError::CannotEnableLdapRemoteEnrollment); + } + if self.ldap_remote_enrollment_enabled && !self.ldap_configured() { + warn!("Cannot enable remote enrollment for LDAP. LDAP is not configured."); + return Err(SettingsValidationError::CannotEnableLdapRemoteEnrollment); + } + if self.ldap_remote_enrollment_send_invite && !self.ldap_remote_enrollment_enabled { + warn!( + "Cannot enable automatic invites for LDAP remote enrollment. LDAP remote enrollment is not enabled" + ); + return Err(SettingsValidationError::CannotEnableLdapRemoteEnrollmentInvite); + } + Ok(()) } @@ -493,21 +526,23 @@ impl Settings { ldap_uses_ad = $46, \ ldap_user_rdn_attr = $47, \ ldap_sync_groups = $48, \ - openid_username_handling = $49, \ - defguard_url = $50, \ - default_admin_group_name = $51, \ - authentication_period_days = $52, \ - mfa_code_timeout_seconds = $53, \ - public_proxy_url = $54, \ - default_admin_id = $55, \ - secret_key = $56, \ - enable_stats_purge = $57, \ - stats_purge_frequency_hours = $58, \ - stats_purge_threshold_days = $59, \ - enrollment_token_timeout_hours = $60, \ - password_reset_token_timeout_hours = $61, \ - enrollment_session_timeout_minutes = $62, \ - password_reset_session_timeout_minutes = $63 \ + ldap_remote_enrollment_enabled = $49, \ + ldap_remote_enrollment_send_invite = $50, \ + openid_username_handling = $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", self.openid_enabled, self.wireguard_enabled, @@ -557,6 +592,8 @@ impl Settings { self.ldap_uses_ad, self.ldap_user_rdn_attr, &self.ldap_sync_groups as &Vec, + self.ldap_remote_enrollment_enabled, + self.ldap_remote_enrollment_send_invite, &self.openid_username_handling as &OpenIdUsernameHandling, self.defguard_url, self.default_admin_group_name, @@ -639,6 +676,27 @@ impl Settings { && self.smtp_sender != Some(String::new()) } + /// Check if all required LDAP options are configured. + /// + /// Meant to be used to check if LDAP integration can be enabled. + #[must_use] + pub fn ldap_configured(&self) -> bool { + let non_empty = |opt: &Option| opt.as_deref().is_some_and(|s| !s.is_empty()); + self.ldap_url + .as_deref() + .is_some_and(|s| Url::parse(s).is_ok()) + && non_empty(&self.ldap_bind_username) + && self.ldap_bind_password.is_some() // just check the presence, don't expose the secret + && non_empty(&self.ldap_username_attr) + && non_empty(&self.ldap_user_search_base) + && non_empty(&self.ldap_user_obj_class) + && non_empty(&self.ldap_member_attr) + && non_empty(&self.ldap_groupname_attr) + && non_empty(&self.ldap_group_obj_class) + && non_empty(&self.ldap_group_member_attr) + && non_empty(&self.ldap_group_search_base) + } + #[must_use] pub fn ldap_using_username_as_rdn(&self) -> bool { self.ldap_user_rdn_attr diff --git a/crates/defguard_common/src/db/models/user.rs b/crates/defguard_common/src/db/models/user.rs index 7f78c31100..2d83925527 100644 --- a/crates/defguard_common/src/db/models/user.rs +++ b/crates/defguard_common/src/db/models/user.rs @@ -118,6 +118,9 @@ pub struct User { pub ldap_rdn: Option, /// Rest of the user's DN pub ldap_user_path: Option, + /// Marks whether LDAP user has completed enrollment + /// Only relevant if `Settings::ldap_remote_enrollment_enabled` is set to `true` + pub ldap_remote_enrollment_completed: bool, /// The user's sub claim returned by the OpenID provider. Also indicates whether the user has /// used OpenID to log in. // FIXME: must be unique @@ -154,6 +157,7 @@ impl fmt::Debug for User { ldap_pass_randomized, ldap_rdn, ldap_user_path, + ldap_remote_enrollment_completed, openid_sub, totp_enabled, email_mfa_enabled, @@ -177,6 +181,10 @@ impl fmt::Debug for User { .field("ldap_pass_randomized", ldap_pass_randomized) .field("ldap_rdn", ldap_rdn) .field("ldap_user_path", ldap_user_path) // sensitive data + .field( + "ldap_remote_enrollment_completed", + ldap_remote_enrollment_completed, + ) .field("openid_sub", openid_sub) .field("totp_enabled", totp_enabled) .field("email_mfa_enabled", email_mfa_enabled) @@ -233,6 +241,7 @@ impl User { ldap_pass_randomized: false, ldap_rdn: Some(username), ldap_user_path: None, + ldap_remote_enrollment_completed: false, enrollment_pending: false, } } @@ -275,12 +284,27 @@ impl User { /// A user is treated as enrolled if: /// - The `enrollment_pending` flag is **not** set, i.e. enrollment was not requested by an /// administrator (https://github.com/DefGuard/client/issues/647). - /// - They either have a password configured, have authenticated via an external OIDC provider - /// or were synced from LDAP. + /// - They either have a password configured, have authenticated via an external OIDC provider, + /// or were synced from LDAP (subject to the `ldap_remote_enrollment_enabled` setting). #[must_use] pub fn is_enrolled(&self) -> bool { - !self.enrollment_pending - && (self.password_hash.is_some() || self.openid_sub.is_some() || self.from_ldap) + if self.enrollment_pending { + return false; + } + if self.from_ldap { + let settings = Settings::get_current_settings(); + if settings.ldap_remote_enrollment_enabled { + // When LDAP remote enrollment is enabled, an LDAP user is only considered + // enrolled after they have completed the self-enrollment process. + return self.ldap_remote_enrollment_completed; + } + // Feature disabled: all LDAP-synced users are implicitly enrolled (legacy behaviour). + return true; + } + if self.password_hash.is_some() || self.openid_sub.is_some() { + return true; + } + false } #[must_use] @@ -623,7 +647,7 @@ impl User { "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, \ totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, \ - ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ + ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending \ FROM \"user\" \ WHERE is_active" ) @@ -642,7 +666,7 @@ impl User { phone, mfa_enabled, totp_enabled, totp_secret, \ email_mfa_enabled, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending \ FROM \"user\" \ INNER JOIN \"group_user\" ON \"user\".id = \"group_user\".user_id \ INNER JOIN \"group\" ON \"group_user\".group_id = \"group\".id \ @@ -790,7 +814,7 @@ impl User { "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, \ totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending \ FROM \"user\" WHERE username = $1", username ) @@ -807,7 +831,7 @@ impl User { "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, \ totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, \ - ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ + ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending \ FROM \"user\" WHERE email ILIKE $1", email ) @@ -835,14 +859,16 @@ impl User { where E: PgExecutor<'e>, { - query_as( + let emails: Vec = emails.iter().map(ToString::to_string).collect(); + query_as!( + Self, "SELECT id, username, password_hash, last_name, first_name, email, phone, \ mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, \ - mfa_method, recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, \ - ldap_rdn, ldap_user_path, enrollment_pending \ + mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, \ + ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending \ FROM \"user\" WHERE email = ANY($1)", + &emails ) - .bind(emails) .fetch_all(executor) .await } @@ -856,7 +882,7 @@ impl User { "SELECT id, username, password_hash, last_name, first_name, email, phone, \ mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed,enrollment_pending \ FROM \"user\" WHERE openid_sub = $1", sub ) @@ -1080,7 +1106,7 @@ impl User { "SELECT u.id, u.username, u.password_hash, u.last_name, u.first_name, u.email, \ u.phone, u.mfa_enabled, u.totp_enabled, u.email_mfa_enabled, \ u.totp_secret, u.email_mfa_secret, u.mfa_method \"mfa_method: _\", u.recovery_codes, \ - u.is_active, u.openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, \ + u.is_active, u.openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, \ enrollment_pending \ FROM \"user\" u \ JOIN \"device\" d ON u.id = d.user_id \ @@ -1101,7 +1127,7 @@ impl User { "SELECT id, username, password_hash, last_name, first_name, email, phone, \ mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, \ mfa_method, recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, \ - ldap_rdn, ldap_user_path, enrollment_pending \ + ldap_rdn, ldap_user_path, enrollment_pending, ldap_remote_enrollment_completed \ FROM \"user\" WHERE email NOT IN (SELECT * FROM UNNEST($1::TEXT[]))", ) .bind(user_emails) @@ -1134,7 +1160,7 @@ impl User { u.phone, u.mfa_enabled, u.totp_enabled, u.email_mfa_enabled, \ u.totp_secret, u.email_mfa_secret, u.mfa_method \"mfa_method: _\", u.recovery_codes, \ u.is_active, u.openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending \ FROM \"user\" u \ WHERE EXISTS (SELECT 1 FROM group_user gu LEFT JOIN \"group\" g ON gu.group_id = g.id \ WHERE is_admin AND user_id = u.id) AND u.is_active" @@ -1184,6 +1210,7 @@ impl Distribution> for Standard { ldap_pass_randomized: false, ldap_rdn: None, ldap_user_path: None, + ldap_remote_enrollment_completed: false, enrollment_pending: false, } } @@ -1224,6 +1251,7 @@ impl Distribution> for Standard { ldap_pass_randomized: false, ldap_rdn: None, ldap_user_path: None, + ldap_remote_enrollment_completed: false, enrollment_pending: false, } } @@ -1231,12 +1259,18 @@ impl Distribution> for Standard { #[cfg(test)] mod test { + use std::str::FromStr; + use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; use crate::{ config::{DefGuardConfig, SERVER_CONFIG}, - db::{models::settings::initialize_current_settings, setup_pool}, + db::{ + models::settings::{initialize_current_settings, update_current_settings}, + setup_pool, + }, + secret::SecretStringWrapper, }; #[sqlx::test] @@ -1587,6 +1621,9 @@ mod test { #[sqlx::test] async fn test_user_is_enrolled(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; + // Populate the settings cache so that `is_enrolled()` can call + // `Settings::get_current_settings()` without panicking. + initialize_current_settings(&pool).await.unwrap(); let user = User::new( "test", Some("31071980"), @@ -1638,5 +1675,63 @@ mod test { user.from_ldap = true; user.save(&pool).await.unwrap(); assert!(!user.is_enrolled()); + + // Feature disabled (default), ldap_remote_enrollment_completed=false + // → LDAP user is still enrolled (legacy behaviour). + user.enrollment_pending = false; + user.password_hash = None; + user.openid_sub = None; + user.from_ldap = true; + user.ldap_remote_enrollment_completed = false; + user.save(&pool).await.unwrap(); + assert!( + user.is_enrolled(), + "LDAP user should be enrolled when remote enrollment is disabled (legacy)" + ); + + // Feature enabled, ldap_remote_enrollment_completed=false + // → LDAP user is NOT yet enrolled. + let mut settings = Settings::get_current_settings(); + settings.smtp_server = Some("smtp.example.com".into()); + settings.smtp_port = Some(587); + settings.smtp_sender = Some("noreply@example.com".into()); + settings.ldap_url = Some("ldap://localhost".into()); + settings.ldap_bind_username = Some("cn=admin,dc=example,dc=com".into()); + settings.ldap_bind_password = Some(SecretStringWrapper::from_str("secret").unwrap()); + settings.ldap_username_attr = Some("uid".into()); + settings.ldap_user_search_base = Some("ou=users,dc=example,dc=com".into()); + settings.ldap_user_obj_class = Some("inetOrgPerson".into()); + settings.ldap_member_attr = Some("memberUid".into()); + settings.ldap_groupname_attr = Some("cn".into()); + settings.ldap_group_obj_class = Some("posixGroup".into()); + settings.ldap_group_member_attr = Some("memberUid".into()); + settings.ldap_group_search_base = Some("ou=groups,dc=example,dc=com".into()); + settings.ldap_remote_enrollment_enabled = true; + update_current_settings(&pool, settings).await.unwrap(); + // user fields unchanged from the previous case — only the setting changed + assert!( + !user.is_enrolled(), + "LDAP user should not be enrolled when remote enrollment is enabled but not completed" + ); + + // Feature enabled, ldap_remote_enrollment_completed=true + // → LDAP user IS enrolled. + user.ldap_remote_enrollment_completed = true; + user.save(&pool).await.unwrap(); + assert!( + user.is_enrolled(), + "LDAP user should be enrolled when remote enrollment is enabled and completed" + ); + + // Non-LDAP user with a password while the feature is enabled + // → still enrolled (feature must not affect non-LDAP users). + user.from_ldap = false; + user.ldap_remote_enrollment_completed = false; + user.password_hash = Some(hash_password("31071980").unwrap()); + user.save(&pool).await.unwrap(); + assert!( + user.is_enrolled(), + "non-LDAP user with a password should be enrolled regardless of the remote enrollment setting" + ); } } diff --git a/crates/defguard_core/src/enrollment_management.rs b/crates/defguard_core/src/enrollment_management.rs index 90f96d8d57..04758b0647 100644 --- a/crates/defguard_core/src/enrollment_management.rs +++ b/crates/defguard_core/src/enrollment_management.rs @@ -1,4 +1,7 @@ -use defguard_common::db::{Id, models::user::User}; +use defguard_common::db::{ + Id, + models::{Settings, user::User}, +}; use defguard_mail::templates::{desktop_start_mail, new_account_mail}; use reqwest::Url; use sqlx::{PgConnection, PgExecutor}; @@ -6,7 +9,7 @@ use sqlx::{PgConnection, PgExecutor}; use crate::db::models::enrollment::{ENROLLMENT_TOKEN_TYPE, Token, TokenError}; /// Start user enrollment process -/// This creates a new enrollment token valid for 24h +/// This creates a new enrollment token valid for the duration specified by `token_timeout_seconds` /// and optionally sends enrollment email notification to user pub async fn start_user_enrollment( user: &mut User, @@ -17,35 +20,23 @@ pub async fn start_user_enrollment( enrollment_service_url: Url, send_user_notification: bool, ) -> Result { - info!( - "User {} started a new enrollment process for user {}.", - admin.username, user.username - ); - debug!( - "Notify user by mail about the enrollment process: {}", - send_user_notification - ); - debug!("Check if {} has a password.", user.username); - if user.has_password() { - debug!( - "User {} that you want to start enrollment process for already has a password.", - user.username - ); + info!("User {admin} started a new enrollment process for user {user}."); + debug!("Notify user by mail about the enrollment process: {send_user_notification}"); + debug!("Check if {user} is enrolled."); + if user.is_enrolled() { + debug!("User {user} that you want to start enrollment process for is already enrolled."); return Err(TokenError::AlreadyActive); } - debug!("Verify that {} is an active user.", user.username); + debug!("Verify that {user} is an active user."); if !user.is_active { - warn!( - "Can't create enrollment token for disabled user {}", - user.username - ); + warn!("Can't create enrollment token for disabled user {user}"); return Err(TokenError::UserDisabled); } clear_unused_enrollment_tokens(user, &mut *conn).await?; - debug!("Create a new enrollment token for user {}.", user.username); + debug!("Create a new enrollment token for user {user}."); let enrollment = Token::new( user.id, Some(admin.id), @@ -53,11 +44,11 @@ pub async fn start_user_enrollment( token_timeout_seconds, Some(ENROLLMENT_TOKEN_TYPE.to_string()), ); - debug!("Saving a new enrollment token..."); + debug!("Saving a new enrollment token for user {user}"); enrollment.save(&mut *conn).await?; debug!( - "Saved a new enrollment token with id {} for user {}.", - enrollment.id, user.username + "Saved a new enrollment token with ID {} for user {user}.", + enrollment.id ); // Mark the user with enrollment-pending flag. @@ -67,10 +58,7 @@ pub async fn start_user_enrollment( if send_user_notification { if let Some(email) = email { - debug!( - "Sending an enrollment mail for user {} to {email}.", - user.username - ); + debug!("Sending an enrollment mail for user {user} to {email}."); let base_message_context = enrollment.get_welcome_message_context(&mut *conn).await?; let result = new_account_mail( &email, @@ -82,10 +70,7 @@ pub async fn start_user_enrollment( .await; match result { Ok(()) => { - info!( - "Sent enrollment start mail for user {} to {email}", - user.username - ); + info!("Sent enrollment start mail for user {user} to {email}"); } Err(err) => { error!("Error sending mail: {err}"); @@ -94,10 +79,7 @@ pub async fn start_user_enrollment( } } } - info!( - "New enrollment token has been generated for {}.", - user.username - ); + info!("New enrollment token has been generated for {user}."); Ok(enrollment.id) } @@ -193,13 +175,72 @@ pub async fn start_desktop_configuration( } // Remove unused tokens when triggering user enrollment -pub async fn clear_unused_enrollment_tokens<'e, E>( +pub async fn clear_unused_enrollment_tokens<'e, E: PgExecutor<'e>>( user: &User, executor: E, -) -> Result<(), TokenError> -where - E: PgExecutor<'e>, -{ +) -> Result<(), TokenError> { info!("Removing unused tokens for user {}.", user.username); Token::delete_unused_user_tokens(executor, user.id).await } + +/// Sends an enrollment invitation to a newly-created LDAP user when both +/// `ldap_remote_enrollment_enabled` and `ldap_remote_enrollment_send_invite` settings are enabled. +/// +/// Errors are logged and swallowed — this must not disrupt the caller's flow. +pub async fn try_send_ldap_enrollment_invite(user: &mut User, conn: &mut PgConnection) { + let settings = Settings::get_current_settings(); + if !settings.ldap_remote_enrollment_enabled || !settings.ldap_remote_enrollment_send_invite { + return; + } + + let admins = match User::find_admins(&mut *conn).await { + Ok(a) => a, + Err(err) => { + error!( + "Failed to fetch admins while sending LDAP enrollment invite for user {user}: {err}", + ); + return; + } + }; + + // Find first active admin + // This is required because the desktop client expects `AdminInfo` to be available + // and this action is triggered automatically, not by a specific admin + let Some(admin) = admins.into_iter().next() else { + error!( + "No active admin found; skipping enrollment invite for LDAP user {}", + user.username + ); + return; + }; + + let enrollment_service_url = match settings.proxy_public_url() { + Ok(url) => url, + Err(err) => { + error!( + "Failed to get enrollment service URL while sending LDAP enrollment invite for \ + user {user}: {err}", + ); + return; + } + }; + + let token_timeout_seconds = settings.enrollment_token_timeout().as_secs(); + + if let Err(err) = start_user_enrollment( + user, + &mut *conn, + &admin, + Some(user.email.clone()), + token_timeout_seconds, + enrollment_service_url, + true, + ) + .await + { + error!( + "Failed to send LDAP enrollment invite for user {}: {err}", + user.username + ); + } +} diff --git a/crates/defguard_core/src/enterprise/db/models/acl.rs b/crates/defguard_core/src/enterprise/db/models/acl.rs index 938a2b647b..e0bba7542a 100644 --- a/crates/defguard_core/src/enterprise/db/models/acl.rs +++ b/crates/defguard_core/src/enterprise/db/models/acl.rs @@ -1033,7 +1033,7 @@ impl AclRule { "SELECT u.id, username, password_hash, last_name, first_name, email, phone, \ mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, \ - ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ + ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending \ FROM aclruleuser r \ JOIN \"user\" u \ ON u.id = r.user_id \ @@ -1056,7 +1056,7 @@ impl AclRule { "SELECT u.id, username, password_hash, last_name, first_name, email, phone, \ mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, \ - ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ + ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending \ FROM aclruleuser r \ JOIN \"user\" u \ ON u.id = r.user_id \ @@ -1262,7 +1262,7 @@ impl AclRuleInfo { "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, \ totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending \ FROM \"user\" u \ JOIN group_user gu ON u.id=gu.user_id \ WHERE u.is_active=true AND gu.group_id=ANY($1)", @@ -1319,7 +1319,7 @@ impl AclRuleInfo { phone, mfa_enabled, totp_enabled, totp_secret, \ email_mfa_enabled, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending \ FROM \"user\" u \ JOIN group_user gu ON u.id=gu.user_id \ WHERE u.is_active=true AND gu.group_id=ANY($1)", diff --git a/crates/defguard_core/src/enterprise/ldap/model.rs b/crates/defguard_core/src/enterprise/ldap/model.rs index 77a9cc61e4..0ead7b3821 100644 --- a/crates/defguard_core/src/enterprise/ldap/model.rs +++ b/crates/defguard_core/src/enterprise/ldap/model.rs @@ -273,7 +273,7 @@ where SELECT id, username, password_hash, last_name, first_name, email, phone, \ mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending \ FROM \"user\" WHERE ldap_user_path IS NULL ", ) diff --git a/crates/defguard_core/src/enterprise/ldap/sync.rs b/crates/defguard_core/src/enterprise/ldap/sync.rs index edf315203d..073a9da66f 100644 --- a/crates/defguard_core/src/enterprise/ldap/sync.rs +++ b/crates/defguard_core/src/enterprise/ldap/sync.rs @@ -82,6 +82,7 @@ use sqlx::{PgConnection, PgPool}; use super::{LDAPConfig, error::LdapError}; use crate::{ + enrollment_management::try_send_ldap_enrollment_invite, enterprise::{ ldap::model::{ get_users_without_ldap_path, ldap_sync_allowed_for_user, update_from_ldap_user, @@ -856,6 +857,7 @@ impl super::LDAPConnection { } } + let mut new_users = Vec::new(); for user in changes.add_defguard { debug!("Adding user {} to Defguard", user.username); if let Some(defguard_user) = @@ -893,13 +895,22 @@ impl super::LDAPConnection { } continue; } - user.save(&mut *transaction).await?; + let saved_user = user.save(&mut *transaction).await?; + new_users.push(saved_user); user_count += 1; } } transaction.commit().await?; + // attempt to send enrollment invites after the original DB transaction is commited + // and users actually exist in DB + let mut transaction = pool.begin().await?; + for mut user in new_users { + try_send_ldap_enrollment_invite(&mut user, &mut transaction).await; + } + transaction.commit().await?; + update_counts(pool).await?; for user in changes.delete_ldap { diff --git a/crates/defguard_core/src/enterprise/ldap/tests.rs b/crates/defguard_core/src/enterprise/ldap/tests.rs index 9a6634e8a9..af0a081154 100644 --- a/crates/defguard_core/src/enterprise/ldap/tests.rs +++ b/crates/defguard_core/src/enterprise/ldap/tests.rs @@ -1,6 +1,12 @@ -use std::collections::HashMap; +use std::{collections::HashMap, str::FromStr}; -use defguard_common::db::{models::settings::initialize_current_settings, setup_pool}; +use defguard_common::{ + db::{ + models::{group::Permission, settings::initialize_current_settings}, + setup_pool, + }, + secret::SecretStringWrapper, +}; use ldap3::SearchEntry; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; @@ -14,7 +20,9 @@ use super::{ *, }; use crate::{ + db::models::enrollment::Token, enterprise::{ + ldap::utils::login_through_ldap_with_connection, license::{License, LicenseTier, SupportType, set_cached_license}, limits::get_counts, }, @@ -41,6 +49,38 @@ fn make_test_user( user } +/// Save a user to Defguard and make them an active admin. +async fn make_test_admin(pool: &sqlx::PgPool, username: &str) -> User { + let user = make_test_user(username, None, None) + .save(pool) + .await + .unwrap(); + let group = Group::new("admins").save(pool).await.unwrap(); + group + .set_permission(pool, Permission::IsAdmin, true) + .await + .unwrap(); + user.add_to_group(pool, &group).await.unwrap(); + user +} + +fn configure_smtp_and_ldap(settings: &mut Settings) { + settings.smtp_server = Some("smtp.example.com".into()); + settings.smtp_port = Some(587); + settings.smtp_sender = Some("noreply@example.com".into()); + settings.ldap_url = Some("ldap://localhost".into()); + settings.ldap_bind_username = Some("cn=admin,dc=example,dc=com".into()); + settings.ldap_bind_password = Some(SecretStringWrapper::from_str("secret").unwrap()); + settings.ldap_username_attr = Some("uid".into()); + settings.ldap_user_search_base = Some("ou=users,dc=example,dc=com".into()); + settings.ldap_user_obj_class = Some("inetOrgPerson".into()); + settings.ldap_member_attr = Some("memberUid".into()); + settings.ldap_groupname_attr = Some("cn".into()); + settings.ldap_group_obj_class = Some("posixGroup".into()); + settings.ldap_group_member_attr = Some("memberUid".into()); + settings.ldap_group_search_base = Some("ou=groups,dc=example,dc=com".into()); +} + fn set_test_license_business() { let license = License { customer_id: "0c4dcb5400544d47ad8617fcdf2704cb".into(), @@ -3395,3 +3435,324 @@ async fn test_ldap_sync_allowed_all_conditions_false(_: PgPoolOptions, options: let result = ldap_sync_allowed_for_user(&user, &pool).await.unwrap(); assert!(!result); } + +/// When both `ldap_remote_enrollment_enabled` and `ldap_remote_enrollment_send_invite` are +/// disabled (the default), syncing new LDAP users must NOT create any enrollment tokens. +#[sqlx::test] +async fn test_sync_does_not_send_invite_when_flags_disabled( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + let _ = initialize_current_settings(&pool).await; + + // Create an admin so find_admins() would have something to return — we want to prove + // the early-return on the flag guard, not the no-admin guard. + make_test_admin(&pool, "sync_admin_nodisabled").await; + + let mut ldap_conn = LDAPConnection::create().await.unwrap(); + let config = ldap_conn.config.clone(); + + let mut ldap_user = make_test_user("sync_invite_disabled_user", None, None); + ldap_user.ldap_rdn = Some("sync_invite_disabled_user".into()); + ldap_user.ldap_user_path = Some("ou=users,dc=example,dc=com".into()); + ldap_conn + .test_client_mut() + .add_test_user(&ldap_user, &config); + + ldap_conn.sync(&pool, false).await.unwrap(); + + // User must be saved to Defguard. + let saved = User::find_by_username(&pool, "sync_invite_disabled_user") + .await + .unwrap(); + assert!(saved.is_some(), "User should have been synced to Defguard"); + + // No enrollment token should have been created. + let tokens = Token::fetch_all(&pool).await.unwrap(); + assert!( + tokens.is_empty(), + "Expected no enrollment token when invite flags are disabled, got {tokens:?}" + ); +} + +/// When only `ldap_remote_enrollment_enabled` is on but `ldap_remote_enrollment_send_invite` +/// is off, syncing new LDAP users must NOT create enrollment tokens. +#[sqlx::test] +async fn test_sync_invite_skipped_when_send_invite_flag_disabled( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + let _ = initialize_current_settings(&pool).await; + + let mut settings = Settings::get_current_settings(); + configure_smtp_and_ldap(&mut settings); + settings.ldap_remote_enrollment_enabled = true; + settings.ldap_remote_enrollment_send_invite = false; + update_current_settings(&pool, settings).await.unwrap(); + + make_test_admin(&pool, "sync_admin_sendoff").await; + + let mut ldap_conn = LDAPConnection::create().await.unwrap(); + let config = ldap_conn.config.clone(); + + let mut ldap_user = make_test_user("sync_invite_sendoff_user", None, None); + ldap_user.ldap_rdn = Some("sync_invite_sendoff_user".into()); + ldap_user.ldap_user_path = Some("ou=users,dc=example,dc=com".into()); + ldap_conn + .test_client_mut() + .add_test_user(&ldap_user, &config); + + ldap_conn.sync(&pool, false).await.unwrap(); + + let saved = User::find_by_username(&pool, "sync_invite_sendoff_user") + .await + .unwrap(); + assert!(saved.is_some(), "User should have been synced to Defguard"); + + let tokens = Token::fetch_all(&pool).await.unwrap(); + assert!( + tokens.is_empty(), + "Expected no enrollment token when send_invite flag is disabled, got {tokens:?}" + ); +} + +/// When both `ldap_remote_enrollment_enabled` and `ldap_remote_enrollment_send_invite` are on, +/// syncing a new LDAP user must create an enrollment token and set `enrollment_pending = true`. +/// +/// SMTP is configured in settings but no real SMTP server is reachable, so `new_account_mail` +/// will fail — but the token and flag are persisted before the mail attempt, so the DB side +/// effects are still observable. +#[sqlx::test] +async fn test_sync_sends_invite_when_flags_enabled(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let _ = initialize_current_settings(&pool).await; + + let mut settings = Settings::get_current_settings(); + configure_smtp_and_ldap(&mut settings); + settings.ldap_remote_enrollment_enabled = true; + settings.ldap_remote_enrollment_send_invite = true; + // Provide a valid proxy URL so proxy_public_url() succeeds. + settings.public_proxy_url = "http://proxy.example.com".into(); + update_current_settings(&pool, settings).await.unwrap(); + + make_test_admin(&pool, "sync_admin_invite").await; + + let mut ldap_conn = LDAPConnection::create().await.unwrap(); + let config = ldap_conn.config.clone(); + + let mut ldap_user = make_test_user("sync_invite_user", None, None); + ldap_user.ldap_rdn = Some("sync_invite_user".into()); + ldap_user.ldap_user_path = Some("ou=users,dc=example,dc=com".into()); + ldap_conn + .test_client_mut() + .add_test_user(&ldap_user, &config); + + ldap_conn.sync(&pool, false).await.unwrap(); + + let saved = User::find_by_username(&pool, "sync_invite_user") + .await + .unwrap() + .expect("User should have been synced to Defguard"); + + assert!( + saved.enrollment_pending, + "enrollment_pending should be true after invite is sent" + ); + + let tokens = Token::fetch_all(&pool).await.unwrap(); + assert_eq!( + tokens.len(), + 1, + "Expected exactly one enrollment token, got {tokens:?}" + ); + assert_eq!( + tokens[0].user_id, saved.id, + "Token should belong to the synced user" + ); + + // Second sync: user already exists in Defguard — must NOT create a second token. + ldap_conn.sync(&pool, false).await.unwrap(); + + let tokens = Token::fetch_all(&pool).await.unwrap(); + assert_eq!( + tokens.len(), + 1, + "Expected still exactly one enrollment token after second sync, got {tokens:?}" + ); +} + +/// When both invite flags are on but there are no active admins in Defguard, the sync must +/// succeed and the user must be saved — the invite is silently skipped with a logged error. +#[sqlx::test] +async fn test_sync_invite_skipped_when_no_admin_exists( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + let _ = initialize_current_settings(&pool).await; + + let mut settings = Settings::get_current_settings(); + configure_smtp_and_ldap(&mut settings); + settings.ldap_remote_enrollment_enabled = true; + settings.ldap_remote_enrollment_send_invite = true; + settings.public_proxy_url = "http://proxy.example.com".into(); + update_current_settings(&pool, settings).await.unwrap(); + + // Deliberately do NOT create any admin user. + + let mut ldap_conn = LDAPConnection::create().await.unwrap(); + let config = ldap_conn.config.clone(); + + let mut ldap_user = make_test_user("sync_invite_noadmin_user", None, None); + ldap_user.ldap_rdn = Some("sync_invite_noadmin_user".into()); + ldap_user.ldap_user_path = Some("ou=users,dc=example,dc=com".into()); + ldap_conn + .test_client_mut() + .add_test_user(&ldap_user, &config); + + // Sync must succeed even with no admins. + ldap_conn.sync(&pool, false).await.unwrap(); + + // User must still be created. + let saved = User::find_by_username(&pool, "sync_invite_noadmin_user") + .await + .unwrap(); + assert!( + saved.is_some(), + "User should have been synced to Defguard even when invite was skipped" + ); + + // No token should have been created. + let tokens = Token::fetch_all(&pool).await.unwrap(); + assert!( + tokens.is_empty(), + "Expected no enrollment token when no admin exists, got {tokens:?}" + ); +} + +/// When both invite flags are on and a user logs in through LDAP for the first time (not yet +/// in Defguard), an enrollment token must be created and `enrollment_pending` set to `true`. +#[sqlx::test] +async fn test_ldap_login_sends_invite_when_flags_enabled( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + let _ = initialize_current_settings(&pool).await; + + let mut settings = Settings::get_current_settings(); + configure_smtp_and_ldap(&mut settings); + settings.ldap_remote_enrollment_enabled = true; + settings.ldap_remote_enrollment_send_invite = true; + settings.public_proxy_url = "http://proxy.example.com".into(); + update_current_settings(&pool, settings).await.unwrap(); + + make_test_admin(&pool, "login_admin_invite").await; + + let mut ldap_conn = super::LDAPConnection::create().await.unwrap(); + let config = ldap_conn.config.clone(); + + let mut ldap_user = make_test_user("login_invite_user", None, None); + ldap_user.ldap_rdn = Some("login_invite_user".into()); + ldap_user.ldap_user_path = Some("ou=users,dc=example,dc=com".into()); + ldap_conn + .test_client_mut() + .add_test_user(&ldap_user, &config); + + let result = + login_through_ldap_with_connection(&pool, &mut ldap_conn, "login_invite_user", PASSWORD) + .await; + assert!(result.is_ok(), "LDAP login should succeed: {result:?}"); + + let saved = User::find_by_username(&pool, "login_invite_user") + .await + .unwrap() + .expect("User should have been created in Defguard"); + + assert!( + saved.enrollment_pending, + "enrollment_pending should be true after login invite" + ); + + let tokens = Token::fetch_all(&pool).await.unwrap(); + assert_eq!( + tokens.len(), + 1, + "Expected exactly one enrollment token after first LDAP login, got {tokens:?}" + ); + assert_eq!( + tokens[0].user_id, saved.id, + "Token should belong to the logged-in user" + ); + + // Second login: user now exists in Defguard — must NOT create a second token. + let result = + login_through_ldap_with_connection(&pool, &mut ldap_conn, "login_invite_user", PASSWORD) + .await; + assert!( + result.is_ok(), + "Second LDAP login should succeed: {result:?}" + ); + + let tokens = Token::fetch_all(&pool).await.unwrap(); + assert_eq!( + tokens.len(), + 1, + "Expected still exactly one enrollment token after second login, got {tokens:?}" + ); +} + +/// When both invite flags are on but the LDAP user already exists in Defguard (returning user), +/// no additional enrollment token must be created. +#[sqlx::test] +async fn test_ldap_login_does_not_send_invite_for_existing_user( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + let _ = initialize_current_settings(&pool).await; + + let mut settings = Settings::get_current_settings(); + configure_smtp_and_ldap(&mut settings); + settings.ldap_remote_enrollment_enabled = true; + settings.ldap_remote_enrollment_send_invite = true; + settings.public_proxy_url = "http://proxy.example.com".into(); + update_current_settings(&pool, settings).await.unwrap(); + + make_test_admin(&pool, "login_admin_existing").await; + + let mut ldap_conn = super::LDAPConnection::create().await.unwrap(); + let config = ldap_conn.config.clone(); + + // Pre-create the user in Defguard (simulates a returning user). + let mut existing = make_test_user("login_existing_user", None, None); + existing.from_ldap = true; + existing.ldap_rdn = Some("login_existing_user".into()); + existing.ldap_user_path = Some("ou=users,dc=example,dc=com".into()); + existing.clone().save(&pool).await.unwrap(); + + ldap_conn + .test_client_mut() + .add_test_user(&existing, &config); + + let result = super::utils::login_through_ldap_with_connection( + &pool, + &mut ldap_conn, + "login_existing_user", + PASSWORD, + ) + .await; + assert!( + result.is_ok(), + "LDAP login for existing user should succeed: {result:?}" + ); + + // No enrollment token should be created for a returning user. + let tokens = Token::fetch_all(&pool).await.unwrap(); + assert!( + tokens.is_empty(), + "Expected no enrollment token for a returning LDAP user, got {tokens:?}" + ); +} diff --git a/crates/defguard_core/src/enterprise/ldap/utils.rs b/crates/defguard_core/src/enterprise/ldap/utils.rs index 3a31fd137d..9d38d468f0 100644 --- a/crates/defguard_core/src/enterprise/ldap/utils.rs +++ b/crates/defguard_core/src/enterprise/ldap/utils.rs @@ -10,10 +10,13 @@ use defguard_common::db::{ use sqlx::PgPool; use super::{LDAPConnection, error::LdapError}; -use crate::enterprise::{ - ldap::{model::ldap_sync_allowed_for_user, with_ldap_status}, - license::get_cached_license, - limits::get_counts, +use crate::{ + enrollment_management::try_send_ldap_enrollment_invite, + enterprise::{ + ldap::{model::ldap_sync_allowed_for_user, with_ldap_status}, + license::get_cached_license, + limits::get_counts, + }, }; fn reached_user_license_limit() -> Option<(u32, u32)> { @@ -46,6 +49,8 @@ pub(crate) async fn login_through_ldap_with_connection( password: &str, ) -> Result, LdapError> { debug!("Logging in user {username} through LDAP"); + let mut transaction = pool.begin().await?; + let mut ldap_user = ldap_connection .get_user_by_credentials(username, password) .await?; @@ -59,16 +64,16 @@ pub(crate) async fn login_through_ldap_with_connection( debug!("User {ldap_user} logged in through LDAP"); // The user is logging in through LDAP, so we can infer that there are no other login options // (Defguard password), so we should mark them as from_ldap. - let user = if let Some(mut defguard_user) = - User::find_by_username(pool, &ldap_user.username).await? + let (mut user, is_new_user) = if let Some(mut defguard_user) = + User::find_by_username(&mut *transaction, &ldap_user.username).await? { debug!( "User {defguard_user} already exists in Defguard, marking them as coming from LDAP and \ proceeding with login" ); defguard_user.from_ldap = true; - defguard_user.save(pool).await?; - defguard_user + defguard_user.save(&mut *transaction).await?; + (defguard_user, false) } else { debug!( "User {ldap_user} doesn't exist in Defguard, creating them first based on LDAP data" @@ -84,9 +89,20 @@ pub(crate) async fn login_through_ldap_with_connection( } ldap_user.from_ldap = true; - ldap_user.save(pool).await? + (ldap_user.save(&mut *transaction).await?, true) }; + transaction.commit().await?; + + // Attempt to send enrollment invite after the original DB transaction is committed, + // so that the user row is visible to the new transaction inside try_send_ldap_enrollment_invite. + // Only send for newly-created users — returning users must not receive a second invite. + if is_new_user { + let mut transaction = pool.begin().await?; + try_send_ldap_enrollment_invite(&mut user, &mut transaction).await; + transaction.commit().await?; + } + Ok(user) } diff --git a/crates/defguard_core/src/error.rs b/crates/defguard_core/src/error.rs index 6d00bfcbae..1acbd75d2c 100644 --- a/crates/defguard_core/src/error.rs +++ b/crates/defguard_core/src/error.rs @@ -180,9 +180,10 @@ impl From for WebError { impl From for WebError { fn from(err: SettingsValidationError) -> Self { match err { - SettingsValidationError::CannotEnableGatewayNotifications => { - Self::BadRequest(err.to_string()) - } + SettingsValidationError::CannotEnableGatewayNotifications + | SettingsValidationError::CannotEnableLdapRemoteEnrollment + | SettingsValidationError::CannotEnableLdapRemoteEnrollmentInvite + | SettingsValidationError::CannotEnableLdap => Self::BadRequest(err.to_string()), SettingsValidationError::InvalidDefguardUrl(_) => Self::BadRequest(err.to_string()), } } diff --git a/crates/defguard_core/src/handlers/group.rs b/crates/defguard_core/src/handlers/group.rs index 51ac4e435e..87d1fa89a0 100644 --- a/crates/defguard_core/src/handlers/group.rs +++ b/crates/defguard_core/src/handlers/group.rs @@ -71,7 +71,7 @@ pub(crate) async fn bulk_assign_to_groups( "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, \ totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending \ FROM \"user\" WHERE id = ANY($1)", &data.users ) diff --git a/crates/defguard_core/src/handlers/settings.rs b/crates/defguard_core/src/handlers/settings.rs index bb9b5686a0..5af53c323a 100644 --- a/crates/defguard_core/src/handlers/settings.rs +++ b/crates/defguard_core/src/handlers/settings.rs @@ -130,18 +130,17 @@ pub async fn patch_settings( let before = settings.clone(); let license = data.license.clone(); + // update LDAP sync status if relevant settings have been changed if let Some(ldap_enabled) = data.ldap_enabled { if !ldap_enabled { settings.ldap_sync_status = LdapSyncStatus::OutOfSync; } } - if let Some(ldap_authority) = data.ldap_is_authoritative { if settings.ldap_is_authoritative != ldap_authority { settings.ldap_sync_status = LdapSyncStatus::OutOfSync; } } - if let Some(ldap_sync_groups) = &data.ldap_sync_groups { if &settings.ldap_sync_groups != ldap_sync_groups { settings.ldap_sync_status = LdapSyncStatus::OutOfSync; @@ -150,6 +149,7 @@ pub async fn patch_settings( settings.apply(data); settings.validate()?; + // clone for event let after = settings.clone(); update_current_settings(&appstate.pool, settings).await?; diff --git a/crates/defguard_core/tests/integration/api/common/mod.rs b/crates/defguard_core/tests/integration/api/common/mod.rs index a1886a0f15..499cb46a4e 100644 --- a/crates/defguard_core/tests/integration/api/common/mod.rs +++ b/crates/defguard_core/tests/integration/api/common/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod client; use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, + str::FromStr, sync::{Arc, Mutex}, }; @@ -14,6 +15,7 @@ use defguard_common::{ Id, models::{Device, Settings, User, WireguardNetwork, settings::initialize_current_settings}, }, + secret::SecretStringWrapper, }; use defguard_core::{ auth::failed_login::FailedLoginMap, @@ -243,3 +245,25 @@ pub(crate) async fn get_db_location(pool: &PgPool, location_id: Id) -> Wireguard pub(crate) async fn get_db_device(pool: &PgPool, device_id: Id) -> Device { Device::find_by_id(pool, device_id).await.unwrap().unwrap() } + +/// Set minimal SMTP fields on a [`Settings`] so that `smtp_configured()` returns `true`. +pub(crate) fn configure_smtp(settings: &mut Settings) { + settings.smtp_server = Some("smtp.example.com".into()); + settings.smtp_port = Some(587); + settings.smtp_sender = Some("noreply@example.com".into()); +} + +/// Set minimal LDAP fields on a [`Settings`] so that `ldap_configured()` returns `true`. +pub(crate) fn configure_ldap(settings: &mut Settings) { + settings.ldap_url = Some("ldap://localhost".into()); + settings.ldap_bind_username = Some("cn=admin,dc=example,dc=com".into()); + settings.ldap_bind_password = Some(SecretStringWrapper::from_str("secret").unwrap()); + settings.ldap_username_attr = Some("uid".into()); + settings.ldap_user_search_base = Some("ou=users,dc=example,dc=com".into()); + settings.ldap_user_obj_class = Some("inetOrgPerson".into()); + settings.ldap_member_attr = Some("memberUid".into()); + settings.ldap_groupname_attr = Some("cn".into()); + settings.ldap_group_obj_class = Some("posixGroup".into()); + settings.ldap_group_member_attr = Some("memberUid".into()); + settings.ldap_group_search_base = Some("ou=groups,dc=example,dc=com".into()); +} diff --git a/crates/defguard_core/tests/integration/api/enrollment.rs b/crates/defguard_core/tests/integration/api/enrollment.rs index 1997d38135..a1be4c3ba6 100644 --- a/crates/defguard_core/tests/integration/api/enrollment.rs +++ b/crates/defguard_core/tests/integration/api/enrollment.rs @@ -1,5 +1,5 @@ use chrono::Duration; -use defguard_common::db::models::User; +use defguard_common::db::models::{Settings, User, settings::update_current_settings}; use defguard_core::{ db::models::enrollment::Token, handlers::{AddUserData, Auth}, @@ -9,7 +9,9 @@ use serde::Deserialize; use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use super::common::{fetch_user_details, make_client_with_db, setup_pool}; +use super::common::{ + configure_ldap, configure_smtp, fetch_user_details, make_client_with_db, setup_pool, +}; #[sqlx::test] async fn test_initialize_enrollment(_: PgPoolOptions, options: PgConnectOptions) { @@ -368,3 +370,97 @@ async fn test_enrollment_pending_unset_for_desktop_client( assert!(!user.enrollment_pending); assert!(user.is_enrolled()); } + +/// An LDAP-synced user (from_ldap=true, ldap_remote_enrollment_completed=false) is +/// listed as **enrolled** by the API when `ldap_remote_enrollment_enabled` is disabled +/// (the default / legacy behaviour). +#[sqlx::test] +async fn test_ldap_user_enrolled_via_api_when_remote_enrollment_disabled( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + let (client, pool) = make_client_with_db(pool).await; + + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // Create user without a password (simulates an LDAP-synced account). + let new_user = AddUserData { + username: "ldapuser_b1".into(), + last_name: "User".into(), + first_name: "Ldap".into(), + email: "ldapuser_b1@example.com".into(), + phone: None, + password: None, + }; + let response = client.post("/api/v1/user").json(&new_user).send().await; + assert_eq!(response.status(), StatusCode::CREATED); + + // Mark the user as LDAP-sourced directly in the DB (the API doesn't expose from_ldap). + let mut user = User::find_by_username(&pool, &new_user.username) + .await + .unwrap() + .unwrap(); + user.from_ldap = true; + user.ldap_remote_enrollment_completed = false; + user.save(&pool).await.unwrap(); + + // ldap_remote_enrollment_enabled is false by default — no settings change needed. + let details = fetch_user_details(&client, &new_user.username).await; + assert!( + details.user.enrolled, + "LDAP user should be reported as enrolled by the API when remote enrollment is disabled" + ); +} + +/// An LDAP-synced user (from_ldap=true, ldap_remote_enrollment_completed=false) is +/// listed as **not enrolled** by the API when `ldap_remote_enrollment_enabled` is enabled. +#[sqlx::test] +async fn test_ldap_user_not_enrolled_via_api_when_remote_enrollment_enabled( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + let (client, pool) = make_client_with_db(pool).await; + + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // Create user without a password (simulates an LDAP-synced account). + let new_user = AddUserData { + username: "ldapuser_b2".into(), + last_name: "User".into(), + first_name: "Ldap".into(), + email: "ldapuser_b2@example.com".into(), + phone: None, + password: None, + }; + let response = client.post("/api/v1/user").json(&new_user).send().await; + assert_eq!(response.status(), StatusCode::CREATED); + + // Mark the user as LDAP-sourced directly in the DB. + let mut user = User::find_by_username(&pool, &new_user.username) + .await + .unwrap() + .unwrap(); + user.from_ldap = true; + user.ldap_remote_enrollment_completed = false; + user.save(&pool).await.unwrap(); + + // Enable LDAP remote enrollment directly in the DB, bypassing HTTP API + // validation (which would require LDAP and SMTP to be fully configured). + let mut settings = Settings::get_current_settings(); + configure_smtp(&mut settings); + configure_ldap(&mut settings); + settings.ldap_remote_enrollment_enabled = true; + update_current_settings(&pool, settings).await.unwrap(); + + let details = fetch_user_details(&client, &new_user.username).await; + assert!( + !details.user.enrolled, + "LDAP user should not be reported as enrolled by the API when remote enrollment is enabled but not completed" + ); +} diff --git a/crates/defguard_core/tests/integration/api/settings.rs b/crates/defguard_core/tests/integration/api/settings.rs index a55de9a152..a2272be788 100644 --- a/crates/defguard_core/tests/integration/api/settings.rs +++ b/crates/defguard_core/tests/integration/api/settings.rs @@ -1,4 +1,7 @@ -use defguard_common::db::models::{Settings, settings::SettingsPatch}; +use defguard_common::db::models::{ + Settings, + settings::{SettingsPatch, update_current_settings}, +}; use defguard_core::handlers::Auth; use reqwest::StatusCode; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; @@ -44,3 +47,171 @@ async fn test_settings(_: PgPoolOptions, options: PgConnectOptions) { let new_settings: Settings = response.json().await; assert!(new_settings.wireguard_enabled); } + +// JSON fragment containing all required LDAP fields except ldap_url (add that at the call site). +const VALID_LDAP_FIELDS_NO_URL: &str = r#" + "ldap_bind_username": "cn=admin,dc=example,dc=com", + "ldap_bind_password": "secret", + "ldap_username_attr": "uid", + "ldap_user_search_base": "ou=users,dc=example,dc=com", + "ldap_user_obj_class": "inetOrgPerson", + "ldap_member_attr": "memberUid", + "ldap_groupname_attr": "cn", + "ldap_group_obj_class": "posixGroup", + "ldap_group_member_attr": "memberUid", + "ldap_group_search_base": "ou=groups,dc=example,dc=com" +"#; + +const VALID_LDAP_URL: &str = r#""ldap_url": "ldap://localhost""#; + +#[sqlx::test] +async fn test_ldap_settings_validation(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let (client, _client_state) = make_test_client(pool).await; + + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // enabling LDAP without any fields configured must fail + let patch: SettingsPatch = serde_json::from_str(r#"{ "ldap_enabled": true }"#).unwrap(); + let response = client.patch("/api/v1/settings").json(&patch).send().await; + assert_eq!( + response.status(), + StatusCode::BAD_REQUEST, + "enabling LDAP without configured fields should return 400" + ); + + // enabling LDAP with an invalid URL must fail even when all other fields are present + let patch: SettingsPatch = serde_json::from_str(&format!( + r#"{{ {VALID_LDAP_FIELDS_NO_URL}, "ldap_url": "not-a-url", "ldap_enabled": true }}"# + )) + .unwrap(); + let response = client.patch("/api/v1/settings").json(&patch).send().await; + assert_eq!( + response.status(), + StatusCode::BAD_REQUEST, + "enabling LDAP with an invalid URL should return 400" + ); + + // enabling LDAP with all required fields filled and a valid URL must succeed + let patch: SettingsPatch = serde_json::from_str(&format!( + r#"{{ {VALID_LDAP_FIELDS_NO_URL}, {VALID_LDAP_URL}, "ldap_enabled": true }}"# + )) + .unwrap(); + let response = client.patch("/api/v1/settings").json(&patch).send().await; + assert_eq!( + response.status(), + StatusCode::OK, + "enabling LDAP with all required fields should return 200" + ); +} + +#[sqlx::test] +async fn test_ldap_remote_enrollment_validation(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let (client, client_state) = make_test_client(pool.clone()).await; + + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // enabling remote enrollment without LDAP configured must fail + let patch: SettingsPatch = + serde_json::from_str(r#"{ "ldap_remote_enrollment_enabled": true }"#).unwrap(); + let response = client.patch("/api/v1/settings").json(&patch).send().await; + assert_eq!( + response.status(), + StatusCode::BAD_REQUEST, + "enabling remote enrollment without LDAP configured should return 400" + ); + + // configure LDAP fields (without SMTP) + let patch: SettingsPatch = serde_json::from_str(&format!( + r#"{{ {VALID_LDAP_FIELDS_NO_URL}, {VALID_LDAP_URL} }}"# + )) + .unwrap(); + let response = client.patch("/api/v1/settings").json(&patch).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // enabling remote enrollment with LDAP configured but no SMTP must still fail + let patch: SettingsPatch = + serde_json::from_str(r#"{ "ldap_remote_enrollment_enabled": true }"#).unwrap(); + let response = client.patch("/api/v1/settings").json(&patch).send().await; + assert_eq!( + response.status(), + StatusCode::BAD_REQUEST, + "enabling remote enrollment without SMTP configured should return 400" + ); + + // configure SMTP via direct DB mutation (same pattern used for test setup in auth tests) + let mut settings = Settings::get_current_settings(); + settings.smtp_server = Some("smtp.example.com".into()); + settings.smtp_port = Some(587); + settings.smtp_sender = Some("noreply@example.com".into()); + update_current_settings(&client_state.pool, settings) + .await + .unwrap(); + + // enabling remote enrollment with both LDAP and SMTP configured must succeed + let patch: SettingsPatch = + serde_json::from_str(r#"{ "ldap_remote_enrollment_enabled": true }"#).unwrap(); + let response = client.patch("/api/v1/settings").json(&patch).send().await; + assert_eq!( + response.status(), + StatusCode::OK, + "enabling remote enrollment with LDAP and SMTP configured should return 200" + ); + // verify the flag was actually persisted to the database (not just held in memory) + let from_db = Settings::get(&pool).await.unwrap().unwrap(); + assert!( + from_db.ldap_remote_enrollment_enabled, + "ldap_remote_enrollment_enabled must be persisted to DB after enabling" + ); + + // enabling send_invite while remote enrollment is disabled must fail + // (use a fresh settings state: disable enrollment first) + let patch: SettingsPatch = + serde_json::from_str(r#"{ "ldap_remote_enrollment_enabled": false }"#).unwrap(); + let response = client.patch("/api/v1/settings").json(&patch).send().await; + assert_eq!(response.status(), StatusCode::OK); + let from_db = Settings::get(&pool).await.unwrap().unwrap(); + assert!( + !from_db.ldap_remote_enrollment_enabled, + "disabling ldap_remote_enrollment_enabled must be persisted to DB" + ); + + let patch: SettingsPatch = + serde_json::from_str(r#"{ "ldap_remote_enrollment_send_invite": true }"#).unwrap(); + let response = client.patch("/api/v1/settings").json(&patch).send().await; + assert_eq!( + response.status(), + StatusCode::BAD_REQUEST, + "enabling send_invite without remote enrollment enabled should return 400" + ); + + // re-enable remote enrollment, then enabling send_invite must succeed + let patch: SettingsPatch = + serde_json::from_str(r#"{ "ldap_remote_enrollment_enabled": true }"#).unwrap(); + let response = client.patch("/api/v1/settings").json(&patch).send().await; + assert_eq!(response.status(), StatusCode::OK); + + let patch: SettingsPatch = + serde_json::from_str(r#"{ "ldap_remote_enrollment_send_invite": true }"#).unwrap(); + let response = client.patch("/api/v1/settings").json(&patch).send().await; + assert_eq!( + response.status(), + StatusCode::OK, + "enabling send_invite with remote enrollment enabled should return 200" + ); + // verify both flags were persisted to the database + let from_db = Settings::get(&pool).await.unwrap().unwrap(); + assert!( + from_db.ldap_remote_enrollment_enabled, + "ldap_remote_enrollment_enabled must still be true in DB" + ); + assert!( + from_db.ldap_remote_enrollment_send_invite, + "ldap_remote_enrollment_send_invite must be persisted to DB after enabling" + ); +} diff --git a/crates/defguard_event_logger/src/message.rs b/crates/defguard_event_logger/src/message.rs index 29012fdb32..5a2075ebe5 100644 --- a/crates/defguard_event_logger/src/message.rs +++ b/crates/defguard_event_logger/src/message.rs @@ -371,6 +371,7 @@ pub enum VpnEvent { } /// Represents activity log events related to user enrollment process +#[allow(clippy::large_enum_variant)] pub enum EnrollmentEvent { EnrollmentStarted, EnrollmentDeviceAdded { device: Device }, diff --git a/crates/defguard_proxy_manager/src/servers/enrollment.rs b/crates/defguard_proxy_manager/src/servers/enrollment.rs index 41e79d10a3..bbbc27f5f0 100644 --- a/crates/defguard_proxy_manager/src/servers/enrollment.rs +++ b/crates/defguard_proxy_manager/src/servers/enrollment.rs @@ -478,6 +478,12 @@ impl EnrollmentServer { // Unset the enrollment-pending flag (https://github.com/DefGuard/client/issues/647). user.enrollment_pending = false; + + // If this is an LDAP user completing remote self-enrollment, mark it as done so that + // `is_enrolled()` returns `true` when `ldap_remote_enrollment_enabled` is set. + if user.from_ldap { + user.ldap_remote_enrollment_completed = true; + } user.save(&mut *transaction).await.map_err(|err| { error!( "Failed to unset enrollment_pending flag for user {}: {err}", diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/enrollment.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/enrollment.rs index af34429d3d..c42f98cfe2 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/enrollment.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/enrollment.rs @@ -1,5 +1,6 @@ use defguard_common::db::models::{ - Device, User, biometric_auth::BiometricAuth, polling_token::PollingToken, + Device, Settings, User, biometric_auth::BiometricAuth, polling_token::PollingToken, + settings::update_current_settings, }; use defguard_core::{ events::{BidiStreamEventType, EnrollmentEvent}, @@ -10,13 +11,14 @@ use defguard_proto::proxy::{ core_response, }; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; +use tokio::time::timeout; use super::support::{ STRONG_PASSWORD, assert_device_config_response, assert_error_response, - complete_proxy_handshake, create_enrollment_token, create_network, create_polling_token, - create_user, create_user_with_device, make_device_info, send_activate_user, - send_code_mfa_setup_finish, send_code_mfa_setup_start, start_enrollment_session, - totp_code_from_base32_secret, + complete_proxy_handshake, configure_ldap, configure_smtp, create_enrollment_token, + create_network, create_polling_token, create_user, create_user_with_device, make_device_info, + send_activate_user, send_code_mfa_setup_finish, send_code_mfa_setup_start, + start_enrollment_session, totp_code_from_base32_secret, }; use crate::tests::common::{HandlerTestContext, TEST_TIMEOUT}; @@ -139,7 +141,7 @@ async fn test_activate_user_happy_path(_: PgPoolOptions, options: PgConnectOptio start_enrollment_session(&mut context, &token.id).await; // `start_enrollment_session` emits an `EnrollmentStarted` bidi event; drain // it so that the subsequent assertion only sees `EnrollmentCompleted`. - let _ = tokio::time::timeout(TEST_TIMEOUT, context.bidi_events_rx.recv()).await; + let _ = timeout(TEST_TIMEOUT, context.bidi_events_rx.recv()).await; let response = send_activate_user(&mut context, &token.id, STRONG_PASSWORD, None).await; @@ -164,7 +166,7 @@ async fn test_activate_user_happy_path(_: PgPoolOptions, options: PgConnectOptio ); // A BidiStreamEvent::Enrollment(EnrollmentCompleted) must have been emitted. - let event = tokio::time::timeout(TEST_TIMEOUT, context.bidi_events_rx.recv()) + let event = timeout(TEST_TIMEOUT, context.bidi_events_rx.recv()) .await .expect("timed out waiting for BidiStreamEvent") .expect("bidi_events_rx closed"); @@ -226,7 +228,7 @@ async fn test_activate_user_already_activated_returns_error( _ => panic!("expected Empty on first activation"), } // Consume the EnrollmentCompleted bidi event so the channel doesn't fill. - let _ = tokio::time::timeout(TEST_TIMEOUT, context.bidi_events_rx.recv()).await; + let _ = timeout(TEST_TIMEOUT, context.bidi_events_rx.recv()).await; // Create a fresh enrollment token (old one is now used), start a new session. let token2 = create_enrollment_token(&context.pool, user.id, Some(user.id)).await; @@ -277,7 +279,7 @@ async fn test_new_device_sends_gateway_device_created_event( let _response = context.mock_proxy_mut().recv_outbound().await; // Check that a DeviceCreated event was broadcast. - let event = tokio::time::timeout(TEST_TIMEOUT, gateway_rx.recv()) + let event = timeout(TEST_TIMEOUT, gateway_rx.recv()) .await .expect("timed out waiting for GatewayEvent::DeviceCreated") .expect("gateway event channel closed"); @@ -737,3 +739,135 @@ async fn test_code_mfa_setup_unsupported_method_returns_error( context.finish().await.expect_server_finished().await; } + +/// After a non-LDAP user completes the enrollment flow via the proxy handler, +/// `is_enrolled()` must return `true` and `ldap_remote_enrollment_completed` must +/// remain `false` (the flag is only set for LDAP users). +#[sqlx::test] +async fn test_activate_user_sets_is_enrolled(_: PgPoolOptions, options: PgConnectOptions) { + let mut context = HandlerTestContext::new(options).await; + complete_proxy_handshake(&mut context).await; + + let user = create_user(&context.pool).await; + let token = create_enrollment_token(&context.pool, user.id, Some(user.id)).await; + start_enrollment_session(&mut context, &token.id).await; + // Drain the EnrollmentStarted bidi event so the next assertion only sees EnrollmentCompleted. + let _ = timeout(TEST_TIMEOUT, context.bidi_events_rx.recv()).await; + + send_activate_user(&mut context, &token.id, STRONG_PASSWORD, None).await; + + let updated = User::find_by_username(&context.pool, &user.username) + .await + .expect("db query failed") + .expect("user not found"); + assert!( + updated.is_enrolled(), + "non-LDAP user must be enrolled after completing activation" + ); + assert!( + !updated.ldap_remote_enrollment_completed, + "ldap_remote_enrollment_completed must remain false for non-LDAP users" + ); + + context.finish().await.expect_server_finished().await; +} + +/// When `ldap_remote_enrollment_enabled` is set, an LDAP user who completes +/// the enrollment flow must have `ldap_remote_enrollment_completed` set to `true` +/// and `is_enrolled()` must return `true` afterwards. +#[sqlx::test] +async fn test_activate_ldap_user_sets_ldap_remote_enrollment_completed( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let mut context = HandlerTestContext::new(options).await; + complete_proxy_handshake(&mut context).await; + + // Enable LDAP remote enrollment in settings (bypasses HTTP validation). + let mut settings = Settings::get_current_settings(); + configure_smtp(&mut settings); + configure_ldap(&mut settings); + settings.ldap_remote_enrollment_enabled = true; + update_current_settings(&context.pool, settings) + .await + .unwrap(); + + // Create user then mark them as LDAP-sourced in the DB. + let mut user = create_user(&context.pool).await; + user.from_ldap = true; + user.save(&context.pool) + .await + .expect("failed to save LDAP user"); + + // Before activation the LDAP user must NOT be enrolled. + assert!( + !user.is_enrolled(), + "LDAP user should not be enrolled before completing remote enrollment" + ); + + let token = create_enrollment_token(&context.pool, user.id, Some(user.id)).await; + start_enrollment_session(&mut context, &token.id).await; + // Drain EnrollmentStarted. + let _ = timeout(TEST_TIMEOUT, context.bidi_events_rx.recv()).await; + + send_activate_user(&mut context, &token.id, STRONG_PASSWORD, None).await; + + let updated = User::find_by_username(&context.pool, &user.username) + .await + .expect("db query failed") + .expect("user not found"); + assert!( + updated.ldap_remote_enrollment_completed, + "ldap_remote_enrollment_completed must be set to true after LDAP user completes enrollment" + ); + assert!( + updated.is_enrolled(), + "LDAP user must be enrolled after completing the remote enrollment process" + ); + + context.finish().await.expect_server_finished().await; +} + +/// When `ldap_remote_enrollment_enabled` is set, a non-LDAP user who completes +/// activation must NOT have `ldap_remote_enrollment_completed` set — the flag is +/// LDAP-specific and must remain `false`. +#[sqlx::test] +async fn test_activate_non_ldap_user_does_not_set_ldap_remote_enrollment_completed( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let mut context = HandlerTestContext::new(options).await; + complete_proxy_handshake(&mut context).await; + + // Enable LDAP remote enrollment in settings (bypasses HTTP validation). + let mut settings = Settings::get_current_settings(); + configure_smtp(&mut settings); + configure_ldap(&mut settings); + settings.ldap_remote_enrollment_enabled = true; + update_current_settings(&context.pool, settings) + .await + .unwrap(); + + // Non-LDAP user (from_ldap stays false, the default). + let user = create_user(&context.pool).await; + let token = create_enrollment_token(&context.pool, user.id, Some(user.id)).await; + start_enrollment_session(&mut context, &token.id).await; + let _ = timeout(TEST_TIMEOUT, context.bidi_events_rx.recv()).await; + + send_activate_user(&mut context, &token.id, STRONG_PASSWORD, None).await; + + let updated = User::find_by_username(&context.pool, &user.username) + .await + .expect("db query failed") + .expect("user not found"); + assert!( + !updated.ldap_remote_enrollment_completed, + "ldap_remote_enrollment_completed must not be set for non-LDAP users even when the feature is enabled" + ); + assert!( + updated.is_enrolled(), + "non-LDAP user with password must be enrolled after activation" + ); + + context.finish().await.expect_server_finished().await; +} diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs index 09f1082c1f..0eb7bf7dc7 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs @@ -1,18 +1,22 @@ use std::{ mem::discriminant, + str::FromStr, sync::atomic::{AtomicU16, AtomicU64, Ordering}, time::{Duration, SystemTime}, }; -use defguard_common::db::{ - Id, NoId, - models::{ - Device, DeviceType, User, WireguardNetwork, - polling_token::PollingToken, - settings::{Settings, update_current_settings}, - vpn_client_session::VpnClientSession, - wireguard::{LocationMfaMode, ServiceLocationMode}, +use defguard_common::{ + db::{ + Id, NoId, + models::{ + Device, DeviceType, User, WireguardNetwork, + polling_token::PollingToken, + settings::{Settings, update_current_settings}, + vpn_client_session::VpnClientSession, + wireguard::{LocationMfaMode, ServiceLocationMode}, + }, }, + secret::SecretStringWrapper, }; use defguard_core::{ db::models::enrollment::{ENROLLMENT_TOKEN_TYPE, PASSWORD_RESET_TOKEN_TYPE, Token}, @@ -828,3 +832,25 @@ pub(crate) async fn send_code_mfa_setup_finish( }); context.mock_proxy_mut().recv_outbound().await } + +/// Set minimal SMTP fields on a [`Settings`] so that `smtp_configured()` returns `true`. +pub(crate) fn configure_smtp(settings: &mut Settings) { + settings.smtp_server = Some("smtp.example.com".into()); + settings.smtp_port = Some(587); + settings.smtp_sender = Some("noreply@example.com".into()); +} + +/// Set minimal LDAP fields on a [`Settings`] so that `ldap_configured()` returns `true`. +pub(crate) fn configure_ldap(settings: &mut Settings) { + settings.ldap_url = Some("ldap://localhost".into()); + settings.ldap_bind_username = Some("cn=admin,dc=example,dc=com".into()); + settings.ldap_bind_password = Some(SecretStringWrapper::from_str("secret").unwrap()); + settings.ldap_username_attr = Some("uid".into()); + settings.ldap_user_search_base = Some("ou=users,dc=example,dc=com".into()); + settings.ldap_user_obj_class = Some("inetOrgPerson".into()); + settings.ldap_member_attr = Some("memberUid".into()); + settings.ldap_groupname_attr = Some("cn".into()); + settings.ldap_group_obj_class = Some("posixGroup".into()); + settings.ldap_group_member_attr = Some("memberUid".into()); + settings.ldap_group_search_base = Some("ou=groups,dc=example,dc=com".into()); +} diff --git a/flake.lock b/flake.lock index e01bd8a050..d2092c1303 100644 --- a/flake.lock +++ b/flake.lock @@ -32,11 +32,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1775036866, - "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=", + "lastModified": 1775423009, + "narHash": "sha256-vPKLpjhIVWdDrfiUM8atW6YkIggCEKdSAlJPzzhkQlw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", + "rev": "68d8aa3d661f0e6bd5862291b5bb263b2a6595c9", "type": "github" }, "original": { @@ -74,11 +74,11 @@ ] }, "locked": { - "lastModified": 1775099554, - "narHash": "sha256-3xBsGnGDLOFtnPZ1D3j2LU19wpAlYefRKTlkv648rU0=", + "lastModified": 1775617983, + "narHash": "sha256-2NWGA/I4j/qlx6qbg86QvJiK1/GyH9gnf0hFiARWVwE=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "8d6387ed6d8e6e6672fd3ed4b61b59d44b124d99", + "rev": "d98b91b1feae7ef07fa2ccb3aa3f83f11abfae54", "type": "github" }, "original": { diff --git a/migrations/20260402133638_[2.0.0]_add_ldap_secure_enrollment.down.sql b/migrations/20260402133638_[2.0.0]_add_ldap_secure_enrollment.down.sql new file mode 100644 index 0000000000..e5fb454f33 --- /dev/null +++ b/migrations/20260402133638_[2.0.0]_add_ldap_secure_enrollment.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE settings DROP COLUMN ldap_remote_enrollment_enabled; +ALTER TABLE settings DROP COLUMN ldap_remote_enrollment_send_invite; + +ALTER TABLE "user" DROP COLUMN ldap_remote_enrollment_completed; diff --git a/migrations/20260402133638_[2.0.0]_add_ldap_secure_enrollment.up.sql b/migrations/20260402133638_[2.0.0]_add_ldap_secure_enrollment.up.sql new file mode 100644 index 0000000000..08fd49e33b --- /dev/null +++ b/migrations/20260402133638_[2.0.0]_add_ldap_secure_enrollment.up.sql @@ -0,0 +1,6 @@ +ALTER TABLE settings ADD COLUMN ldap_remote_enrollment_enabled bool NOT NULL DEFAULT false; +ALTER TABLE settings ADD COLUMN ldap_remote_enrollment_send_invite bool NOT NULL DEFAULT false; + +ALTER TABLE "user" ADD COLUMN ldap_remote_enrollment_completed BOOLEAN NOT NULL DEFAULT false; +-- set to true for all existing LDAP users +UPDATE "user" SET ldap_remote_enrollment_completed = true WHERE from_ldap = true; diff --git a/web/messages/en/common.json b/web/messages/en/common.json index 6b6ac2ee9f..7976ee4fa0 100644 --- a/web/messages/en/common.json +++ b/web/messages/en/common.json @@ -54,6 +54,7 @@ "controls_add_destination": "Add destination", "controls_save": "Save", "controls_save_changes": "Save changes", + "controls_save_changes_anyway": "Save changes anyway", "controls_sign_in": "Sign in", "controls_enable": "Enable", "controls_disable": "Disable", diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index 3ed93a83cf..fbb9297585 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -221,7 +221,8 @@ "settings_ldap_label_group_search_base": "Group search base", "settings_ldap_helper_group_search_base": "", "settings_ldap_section_sync_title": "LDAP synchronization", - "settings_ldap_section_sync_description": "Control how Defguard synchronizes users with LDAP.", + "settings_ldap_section_sync_description": "Control how Defguard synchronizes users with LDAP — disable sync, import users from LDAP, or keep both systems updated automatically.", + "settings_ldap_section_sync_warning": "To enable LDAP synchronization, please fill out the form above first.", "settings_ldap_sync_one_way_title": "One-way synchronization", "settings_ldap_sync_one_way_content": "Changes made to users in Defguard are propagated to your LDAP directory.", "settings_ldap_sync_two_way_title": "Two-way synchronization", @@ -237,6 +238,14 @@ "settings_ldap_toggle_enable_integration": "Enable LDAP integration", "settings_ldap_button_test_connection": "Test connection", "settings_ldap_test_connection_tooltip": "To test connection please fill the form and save the changes first.", + "settings_ldap_section_remote_enrollment_title": "Secure remote enrollment", + "settings_ldap_section_remote_enrollment_description": "Control how Defguard synchronizes users with LDAP — disable sync, import users from LDAP, or keep both systems updated automatically.", + "settings_ldap_label_remote_enrollment_enabled": "Enable Secure Remote Enrollment", + "settings_ldap_helper_remote_enrollment_enabled": "When Defguard is integrated with AD/LDAP, users can log in with their directory credentials and complete remote enrollment (including MFA) via the desktop client. When this switch is enabled, newly created users can be Enrolled.", + "settings_ldap_label_remote_enrollment_send_invite": "Automatically invite new AD/LDAP users to enroll", + "settings_ldap_helper_remote_enrollment_send_invite": "When users are synced via AD/LDAP, Defguard can automatically send an enrollment email for device setup and MFA. If disabled, enrollment must be started manually by an admin.", + "settings_ldap_remote_enrollment_warning_no_sync_no_smtp": "Please activate LDAP synchronization and set up the SMTP server first to enable this feature.", + "settings_ldap_remote_enrollment_warning_no_smtp": "Please configure the SMTP server to enable this feature.", "settings_client_subtitle": "Manage user permissions and configuration options for device control, WireGuard setup, and VPN routing.", "settings_client_section_permissions_title": "Permissions", "settings_client_permissions_description_title": "Client Configuration Permissions", diff --git a/web/package.json b/web/package.json index 17611b6727..4ca6441630 100644 --- a/web/package.json +++ b/web/package.json @@ -18,13 +18,13 @@ "dependencies": { "@axa-ch/react-polymorphic-types": "^1.4.1", "@floating-ui/react": "^0.27.19", - "@inlang/paraglide-js": "^2.15.1", + "@inlang/paraglide-js": "^2.15.2", "@react-hook/resize-observer": "^2.0.2", "@shortercode/webzip": "1.1.1-0", "@stablelib/base64": "^2.0.1", "@stablelib/x25519": "^2.0.1", "@tanstack/react-form": "^1.28.6", - "@tanstack/react-query": "^5.96.1", + "@tanstack/react-query": "^5.96.2", "@tanstack/react-router": "^1.168.10", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.23", @@ -57,30 +57,30 @@ "@biomejs/biome": "2.4.7", "@inlang/paraglide-js": "2.15.0", "@tanstack/devtools-vite": "^0.6.0", - "@tanstack/react-devtools": "^0.10.1", - "@tanstack/react-query-devtools": "^5.96.1", + "@tanstack/react-devtools": "^0.10.2", + "@tanstack/react-query-devtools": "^5.96.2", "@tanstack/react-router-devtools": "^1.166.11", "@tanstack/router-plugin": "^1.167.12", "@types/byte-size": "^8.1.2", "@types/humanize-duration": "^3.27.4", "@types/lodash-es": "^4.17.12", - "@types/node": "^25.5.0", + "@types/node": "^25.5.2", "@types/qs": "^6.15.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", - "@vitest/ui": "^4.1.2", + "@vitest/ui": "^4.1.3", "autoprefixer": "^10.4.27", "globals": "^17.4.0", "prettier": "^3.8.1", - "sass": "^1.98.0", + "sass": "^1.99.0", "sharp": "^0.34.5", "stylelint": "^17.6.0", "stylelint-config-standard-scss": "^17.0.0", "stylelint-scss": "^7.0.0", "typescript": "~5.9.3", - "vite": "^8.0.3", + "vite": "^8.0.7", "vite-plugin-image-optimizer": "^2.0.3", - "vitest": "^4.1.2" + "vitest": "^4.1.3" } } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 3d3a26db9d..092f5c3024 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: ^0.27.19 version: 0.27.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@inlang/paraglide-js': - specifier: ^2.15.1 - version: 2.15.1 + specifier: ^2.15.2 + version: 2.15.2 '@react-hook/resize-observer': specifier: ^2.0.2 version: 2.0.2(react@19.2.4) @@ -33,8 +33,8 @@ importers: specifier: ^1.28.6 version: 1.28.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-query': - specifier: ^5.96.1 - version: 5.96.1(react@19.2.4) + specifier: ^5.96.2 + version: 5.96.2(react@19.2.4) '@tanstack/react-router': specifier: ^1.168.10 version: 1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -122,19 +122,19 @@ importers: version: 2.4.7 '@tanstack/devtools-vite': specifier: ^0.6.0 - version: 0.6.0(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0)) + version: 0.6.0(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) '@tanstack/react-devtools': - specifier: ^0.10.1 - version: 0.10.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.9) + specifier: ^0.10.2 + version: 0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.9) '@tanstack/react-query-devtools': - specifier: ^5.96.1 - version: 5.96.1(@tanstack/react-query@5.96.1(react@19.2.4))(react@19.2.4) + specifier: ^5.96.2 + version: 5.96.2(@tanstack/react-query@5.96.2(react@19.2.4))(react@19.2.4) '@tanstack/react-router-devtools': specifier: ^1.166.11 version: 1.166.11(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.168.9)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/router-plugin': specifier: ^1.167.12 - version: 1.167.12(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0)) + version: 1.167.12(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) '@types/byte-size': specifier: ^8.1.2 version: 8.1.2 @@ -145,8 +145,8 @@ importers: specifier: ^4.17.12 version: 4.17.12 '@types/node': - specifier: ^25.5.0 - version: 25.5.0 + specifier: ^25.5.2 + version: 25.5.2 '@types/qs': specifier: ^6.15.0 version: 6.15.0 @@ -158,13 +158,13 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0)) + version: 6.0.1(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) '@vitest/ui': - specifier: ^4.1.2 - version: 4.1.2(vitest@4.1.2) + specifier: ^4.1.3 + version: 4.1.3(vitest@4.1.3) autoprefixer: specifier: ^10.4.27 - version: 10.4.27(postcss@8.5.8) + version: 10.4.27(postcss@8.5.9) globals: specifier: ^17.4.0 version: 17.4.0 @@ -172,8 +172,8 @@ importers: specifier: ^3.8.1 version: 3.8.1 sass: - specifier: ^1.98.0 - version: 1.98.0 + specifier: ^1.99.0 + version: 1.99.0 sharp: specifier: ^0.34.5 version: 0.34.5 @@ -182,7 +182,7 @@ importers: version: 17.6.0(typescript@5.9.3) stylelint-config-standard-scss: specifier: ^17.0.0 - version: 17.0.0(postcss@8.5.8)(stylelint@17.6.0(typescript@5.9.3)) + version: 17.0.0(postcss@8.5.9)(stylelint@17.6.0(typescript@5.9.3)) stylelint-scss: specifier: ^7.0.0 version: 7.0.0(stylelint@17.6.0(typescript@5.9.3)) @@ -190,14 +190,14 @@ importers: specifier: ~5.9.3 version: 5.9.3 vite: - specifier: ^8.0.3 - version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0) + specifier: ^8.0.7 + version: 8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) vite-plugin-image-optimizer: specifier: ^2.0.3 - version: 2.0.3(sharp@0.34.5)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0)) + version: 2.0.3(sharp@0.34.5)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) vitest: - specifier: ^4.1.2 - version: 4.1.2(@types/node@25.5.0)(@vitest/ui@4.1.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0)) + specifier: ^4.1.3 + version: 4.1.3(@types/node@25.5.2)(@vitest/ui@4.1.3)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) packages: @@ -312,24 +312,28 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] '@biomejs/cli-linux-arm64@2.4.7': resolution: {integrity: sha512-om6FugwmibzfP/6ALj5WRDVSND4H2G9X0nkI1HZpp2ySf9lW2j0X68oQSaHEnls6666oy4KDsc5RFjT4m0kV0w==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-x64-musl@2.4.7': resolution: {integrity: sha512-00kx4YrBMU8374zd2wHuRV5wseh0rom5HqRND+vDldJPrWwQw+mzd/d8byI9hPx926CG+vWzq6AeiT7Yi5y59g==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] '@biomejs/cli-linux-x64@2.4.7': resolution: {integrity: sha512-bV8/uo2Tj+gumnk4sUdkerWyCPRabaZdv88IpbmDWARQQoA/Q0YaqPz1a+LSEDIL7OfrnPi9Hq1Llz4ZIGyIQQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] '@biomejs/cli-win32-arm64@2.4.7': resolution: {integrity: sha512-hOUHBMlFCvDhu3WCq6vaBoG0dp0LkWxSEnEEsxxXvOa9TfT6ZBnbh72A/xBM7CBYB7WgwqboetzFEVDnMxelyw==} @@ -399,161 +403,164 @@ packages: '@emnapi/runtime@1.9.1': resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + '@emnapi/wasi-threads@1.2.0': resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} - '@esbuild/aix-ppc64@0.27.5': - resolution: {integrity: sha512-nGsF/4C7uzUj+Nj/4J+Zt0bYQ6bz33Phz8Lb2N80Mti1HjGclTJdXZ+9APC4kLvONbjxN1zfvYNd8FEcbBK/MQ==} + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.5': - resolution: {integrity: sha512-Oeghq+XFgh1pUGd1YKs4DDoxzxkoUkvko+T/IVKwlghKLvvjbGFB3ek8VEDBmNvqhwuL0CQS3cExdzpmUyIrgA==} + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.5': - resolution: {integrity: sha512-Cv781jd0Rfj/paoNrul1/r4G0HLvuFKYh7C9uHZ2Pl8YXstzvCyyeWENTFR9qFnRzNMCjXmsulZuvosDg10Mog==} + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.5': - resolution: {integrity: sha512-nQD7lspbzerlmtNOxYMFAGmhxgzn8Z7m9jgFkh6kpkjsAhZee1w8tJW3ZlW+N9iRePz0oPUDrYrXidCPSImD0Q==} + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.5': - resolution: {integrity: sha512-I+Ya/MgC6rr8oRWGRDF3BXDfP8K1BVUggHqN6VI2lUZLdDi1IM1v2cy0e3lCPbP+pVcK3Tv8cgUhHse1kaNZZw==} + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.5': - resolution: {integrity: sha512-MCjQUtC8wWJn/pIPM7vQaO69BFgwPD1jriEdqwTCKzWjGgkMbcg+M5HzrOhPhuYe1AJjXlHmD142KQf+jnYj8A==} + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.5': - resolution: {integrity: sha512-X6xVS+goSH0UelYXnuf4GHLwpOdc8rgK/zai+dKzBMnncw7BTQIwquOodE7EKvY2UVUetSqyAfyZC1D+oqLQtg==} + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.5': - resolution: {integrity: sha512-233X1FGo3a8x1ekLB6XT69LfZ83vqz+9z3TSEQCTYfMNY880A97nr81KbPcAMl9rmOFp11wO0dP+eB18KU/Ucg==} + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.5': - resolution: {integrity: sha512-euKkilsNOv7x/M1NKsx5znyprbpsRFIzTV6lWziqJch7yWYayfLtZzDxDTl+LSQDJYAjd9TVb/Kt5UKIrj2e4A==} + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.5': - resolution: {integrity: sha512-0wkVrYHG4sdCCN/bcwQ7yYMXACkaHc3UFeaEOwSVW6e5RycMageYAFv+JS2bKLwHyeKVUvtoVH+5/RHq0fgeFw==} + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.5': - resolution: {integrity: sha512-hVRQX4+P3MS36NxOy24v/Cdsimy/5HYePw+tmPqnNN1fxV0bPrFWR6TMqwXPwoTM2VzbkA+4lbHWUKDd5ZDA/w==} + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.5': - resolution: {integrity: sha512-mKqqRuOPALI8nDzhOBmIS0INvZOOFGGg5n1osGIXAx8oersceEbKd4t1ACNTHM3sJBXGFAlEgqM+svzjPot+ZQ==} + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.5': - resolution: {integrity: sha512-EE/QXH9IyaAj1qeuIV5+/GZkBTipgGO782Ff7Um3vPS9cvLhJJeATy4Ggxikz2inZ46KByamMn6GqtqyVjhenA==} + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.5': - resolution: {integrity: sha512-0V2iF1RGxBf1b7/BjurA5jfkl7PtySjom1r6xOK2q9KWw/XCpAdtB6KNMO+9xx69yYfSCRR9FE0TyKfHA2eQMw==} + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.5': - resolution: {integrity: sha512-rYxThBx6G9HN6tFNuvB/vykeLi4VDsm5hE5pVwzqbAjZEARQrWu3noZSfbEnPZ/CRXP3271GyFk/49up2W190g==} + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.5': - resolution: {integrity: sha512-uEP2q/4qgd8goEUc4QIdU/1P2NmEtZ/zX5u3OpLlCGhJIuBIv0s0wr7TB2nBrd3/A5XIdEkkS5ZLF0ULuvaaYQ==} + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.5': - resolution: {integrity: sha512-+Gq47Wqq6PLOOZuBzVSII2//9yyHNKZLuwfzCemqexqOQCSz0zy0O26kIzyp9EMNMK+nZ0tFHBZrCeVUuMs/ew==} + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.5': - resolution: {integrity: sha512-3F/5EG8VHfN/I+W5cO1/SV2H9Q/5r7vcHabMnBqhHK2lTWOh3F8vixNzo8lqxrlmBtZVFpW8pmITHnq54+Tq4g==} + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.5': - resolution: {integrity: sha512-28t+Sj3CPN8vkMOlZotOmDgilQwVvxWZl7b8rxpn73Tt/gCnvrHxQUMng4uu3itdFvrtba/1nHejvxqz8xgEMA==} + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.5': - resolution: {integrity: sha512-Doz/hKtiuVAi9hMsBMpwBANhIZc8l238U2Onko3t2xUp8xtM0ZKdDYHMnm/qPFVthY8KtxkXaocwmMh6VolzMA==} + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.5': - resolution: {integrity: sha512-WfGVaa1oz5A7+ZFPkERIbIhKT4olvGl1tyzTRaB5yoZRLqC0KwaO95FeZtOdQj/oKkjW57KcVF944m62/0GYtA==} + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.5': - resolution: {integrity: sha512-Xh+VRuh6OMh3uJ0JkCjI57l+DVe7VRGBYymen8rFPnTVgATBwA6nmToxM2OwTlSvrnWpPKkrQUj93+K9huYC6A==} + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.5': - resolution: {integrity: sha512-aC1gpJkkaUADHuAdQfuVTnqVUTLqqUNhAvEwHwVWcnVVZvNlDPGA0UveZsfXJJ9T6k9Po4eHi3c02gbdwO3g6w==} + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.5': - resolution: {integrity: sha512-0UNx2aavV0fk6UpZcwXFLztA2r/k9jTUa7OW7SAea1VYUhkug99MW1uZeXEnPn5+cHOd0n8myQay6TlFnBR07w==} + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.5': - resolution: {integrity: sha512-5nlJ3AeJWCTSzR7AEqVjT/faWyqKU86kCi1lLmxVqmNR+j4HrYdns+eTGjS/vmrzCIe8inGQckUadvS0+JkKdQ==} + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.5': - resolution: {integrity: sha512-PWypQR+d4FLfkhBIV+/kHsUELAnMpx1bRvvsn3p+/sAERbnCzFrtDRG2Xw5n+2zPxBK2+iaP+vetsRl4Ti7WgA==} + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -609,89 +616,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -716,8 +739,8 @@ packages: cpu: [x64] os: [win32] - '@inlang/paraglide-js@2.15.1': - resolution: {integrity: sha512-7wWKbLWwLx1dkkYz55TnVp+39atKXf7rnlHnL8adSmM73UaAdB9fXDzo24GHSY/6FPGFKSkgHdT2qyJv2whWsA==} + '@inlang/paraglide-js@2.15.2': + resolution: {integrity: sha512-1S2jBvc8jzJAZFRf3gKu3Z2+9zQRhvIzALEE4vvWDNIoiiOn0vF3cJHf3xFqgfN/JY5IVS//zQsvAT0jWXH69g==} hasBin: true '@inlang/recommend-sherlock@0.2.1': @@ -759,8 +782,8 @@ packages: '@lix-js/server-protocol-schema@0.1.1': resolution: {integrity: sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ==} - '@napi-rs/wasm-runtime@1.1.2': - resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==} + '@napi-rs/wasm-runtime@1.1.3': + resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 @@ -777,8 +800,8 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@oxc-project/types@0.122.0': - resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + '@oxc-project/types@0.123.0': + resolution: {integrity: sha512-YtECP/y8Mj1lSHiUWGSRzy/C6teUKlS87dEfuVKT09LgQbUsBW1rNg+MiJ4buGu3yuADV60gbIvo9/HplA56Ew==} '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} @@ -809,36 +832,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.6': resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} @@ -891,97 +920,103 @@ packages: react-redux: optional: true - '@rolldown/binding-android-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + '@rolldown/binding-android-arm64@1.0.0-rc.13': + resolution: {integrity: sha512-5ZiiecKH2DXAVJTNN13gNMUcCDg4Jy8ZjbXEsPnqa248wgOVeYRX0iqXXD5Jz4bI9BFHgKsI2qmyJynstbmr+g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.13': + resolution: {integrity: sha512-tz/v/8G77seu8zAB3A5sK3UFoOl06zcshEzhUO62sAEtrEuW/H1CcyoupOrD+NbQJytYgA4CppXPzlrmp4JZKA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.12': - resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + '@rolldown/binding-darwin-x64@1.0.0-rc.13': + resolution: {integrity: sha512-8DakphqOz8JrMYWTJmWA+vDJxut6LijZ8Xcdc4flOlAhU7PNVwo2MaWBF9iXjJAPo5rC/IxEFZDhJ3GC7NHvug==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.12': - resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.13': + resolution: {integrity: sha512-4wBQFfjDuXYN/SVI8inBF3Aa+isq40rc6VMFbk5jcpolUBTe5cYnMsHZ51nFWsx3PVyyNN3vgoESki0Hmr/4BA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': - resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.13': + resolution: {integrity: sha512-JW/e4yPIXLms+jmnbwwy5LA/LxVwZUWLN8xug+V200wzaVi5TEGIWQlh8o91gWYFxW609euI98OCCemmWGuPrw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.13': + resolution: {integrity: sha512-ZfKWpXiUymDnavepCaM6KG/uGydJ4l2nBmMxg60Ci4CbeefpqjPWpfaZM7PThOhk2dssqBAcwLc6rAyr0uTdXg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': - resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.13': + resolution: {integrity: sha512-bmRg3O6Z0gq9yodKKWCIpnlH051sEfdVwt+6m5UDffAQMUUqU0xjnQqqAUm+Gu7ofAAly9DqiQDtKu2nPDEABA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.13': + resolution: {integrity: sha512-8Wtnbw4k7pMYN9B/mOEAsQ8HOiq7AZ31Ig4M9BKn2So4xRaFEhtCSa4ZJaOutOWq50zpgR4N5+L/opnlaCx8wQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.13': + resolution: {integrity: sha512-D/0Nlo8mQuxSMohNJUF2lDXWRsFDsHldfRRgD9bRgktj+EndGPj4DOV37LqDKPYS+osdyhZEH7fTakTAEcW7qg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.13': + resolution: {integrity: sha512-eRrPvat2YaVQcwwKi/JzOP6MKf1WRnOCr+VaI3cTWz3ZoLcP/654z90lVCJ4dAuMEpPdke0n+qyAqXDZdIC4rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': - resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.13': + resolution: {integrity: sha512-PsdONiFRp8hR8KgVjTWjZ9s7uA3uueWL0t74/cKHfM4dR5zXYv4AjB8BvA+QDToqxAFg4ZkcVEqeu5F7inoz5w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.13': + resolution: {integrity: sha512-hCNXgC5dI3TVOLrPT++PKFNZ+1EtS0mLQwfXXXSUD/+rGlB65gZDwN/IDuxLpQP4x8RYYHqGomlUXzpO8aVI2w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': - resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.13': + resolution: {integrity: sha512-viLS5C5et8NFtLWw9Sw3M/w4vvnVkbWkO7wSNh3C+7G1+uCkGpr6PcjNDSFcNtmXY/4trjPBqUfcOL+P3sWy/g==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': - resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.13': + resolution: {integrity: sha512-Fqa3Tlt1xL4wzmAYxGNFV36Hb+VfPc9PYU+E25DAnswXv3ODDu/yyWjQDbXMo5AGWkQVjLgQExuVu8I/UaZhPQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': - resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.13': + resolution: {integrity: sha512-/pLI5kPkGEi44TDlnbio3St/5gUFeN51YWNAk/Gnv6mEQBOahRBh52qVFVBpmrnU01n2yysvBML9Ynu7K4kGAQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-rc.12': - resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + '@rolldown/pluginutils@1.0.0-rc.13': + resolution: {integrity: sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==} '@rolldown/pluginutils@1.0.0-rc.7': resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} @@ -1086,8 +1121,8 @@ packages: peerDependencies: vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - '@tanstack/devtools@0.11.1': - resolution: {integrity: sha512-g3nHgVP76kT9190d6O32AjANoEnujLEB+51PDtBzlah8hvKeEygK53cunN+HXhjlfhM4PoOCi8/B96cdJVSnLg==} + '@tanstack/devtools@0.11.2': + resolution: {integrity: sha512-K8+tsBx+ptTLqqd4dOF10B6laj1g+XYImqYZL9n0jBINGaT+sOf17PKV9pbBt8kdbZeIGsHaJ5OZWCyZoHqN4A==} engines: {node: '>=18'} hasBin: true peerDependencies: @@ -1104,14 +1139,14 @@ packages: resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==} engines: {node: '>=18'} - '@tanstack/query-core@5.96.1': - resolution: {integrity: sha512-u1yBgtavSy+N8wgtW3PiER6UpxcplMje65yXnnVgiHTqiMwLlxiw4WvQDrXyn+UD6lnn8kHaxmerJUzQcV/MMg==} + '@tanstack/query-core@5.96.2': + resolution: {integrity: sha512-hzI6cTVh4KNRk8UtoIBS7Lv9g6BnJPXvBKsvYH1aGWvv0347jT3BnSvztOE+kD76XGvZnRC/t6qdW1CaIfwCeA==} - '@tanstack/query-devtools@5.96.1': - resolution: {integrity: sha512-A4+uQTWbiqZDgrLeyjpFYLfMaWaKWpkwTkR1cUfocVj6vPYgym7QTG2se9A01WSxceDdmgxOqvn1ivcTvgWD8w==} + '@tanstack/query-devtools@5.96.2': + resolution: {integrity: sha512-vBTB1Qhbm3nHSbEUtQwks/EdcAtFfEapr1WyBW4w2ExYKuXVi3jIxUIHf5MlSltiHuL7zNyUuanqT/7sI2sb6g==} - '@tanstack/react-devtools@0.10.1': - resolution: {integrity: sha512-cvcd0EqN7Q2LYatQXxFhOkEa9RUQXZlhXnM1mwuibxmyRX+CMyohUZcgjodtIfgh+RT0Pmvt49liTdZby5ovZw==} + '@tanstack/react-devtools@0.10.2': + resolution: {integrity: sha512-1BmZyxOrI5SqmRJ5MgkYZNNdnlLsJxQRI2YgorrAvcF2MxK6x5RcuStvD8+YlXoMw3JtNukPxoITirKAnKYDQA==} engines: {node: '>=18'} peerDependencies: '@types/react': '>=16.8' @@ -1128,14 +1163,14 @@ packages: '@tanstack/react-start': optional: true - '@tanstack/react-query-devtools@5.96.1': - resolution: {integrity: sha512-3ZZ58fupIXtJFM0evj8YvWrauaZPUrQEqRYaq9e4ER/WPqTKeWEucqWCXn+KJLgWlcot5JIIUtQNynbovGjTTA==} + '@tanstack/react-query-devtools@5.96.2': + resolution: {integrity: sha512-nTFKLGuTOFvmFRvcyZ3ArWC/DnMNPoBh6h/2yD6rsf7TCTJCQt+oUWOp2uKPTIuEPtF/vN9Kw5tl5mD1Kbposw==} peerDependencies: - '@tanstack/react-query': ^5.96.1 + '@tanstack/react-query': ^5.96.2 react: ^18 || ^19 - '@tanstack/react-query@5.96.1': - resolution: {integrity: sha512-2X7KYK5KKWUKGeWCVcqxXAkYefJtrKB7tSKWgeG++b0H6BRHxQaLSSi8AxcgjmUnnosHuh9WsFZqvE16P1WCzA==} + '@tanstack/react-query@5.96.2': + resolution: {integrity: sha512-sYyzzJT4G0g02azzJ8o55VFFV31XvFpdUpG+unxS0vSaYsJnSPKGoI6WdPwUucJL1wpgGfwfmntNX/Ub1uOViA==} peerDependencies: react: ^18 || ^19 @@ -1303,8 +1338,8 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@25.5.0': - resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + '@types/node@25.5.2': + resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==} '@types/qs@6.15.0': resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} @@ -1349,11 +1384,11 @@ packages: babel-plugin-react-compiler: optional: true - '@vitest/expect@4.1.2': - resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} + '@vitest/expect@4.1.3': + resolution: {integrity: sha512-CW8Q9KMtXDGHj0vCsqui0M5KqRsu0zm0GNDW7Gd3U7nZ2RFpPKSCpeCXoT+/+5zr1TNlsoQRDEz+LzZUyq6gnQ==} - '@vitest/mocker@4.1.2': - resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} + '@vitest/mocker@4.1.3': + resolution: {integrity: sha512-XN3TrycitDQSzGRnec/YWgoofkYRhouyVQj4YNsJ5r/STCUFqMrP4+oxEv3e7ZbLi4og5kIHrZwekDJgw6hcjw==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1363,25 +1398,25 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.2': - resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} + '@vitest/pretty-format@4.1.3': + resolution: {integrity: sha512-hYqqwuMbpkkBodpRh4k4cQSOELxXky1NfMmQvOfKvV8zQHz8x8Dla+2wzElkMkBvSAJX5TRGHJAQvK0TcOafwg==} - '@vitest/runner@4.1.2': - resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} + '@vitest/runner@4.1.3': + resolution: {integrity: sha512-VwgOz5MmT0KhlUj40h02LWDpUBVpflZ/b7xZFA25F29AJzIrE+SMuwzFf0b7t4EXdwRNX61C3B6auIXQTR3ttA==} - '@vitest/snapshot@4.1.2': - resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} + '@vitest/snapshot@4.1.3': + resolution: {integrity: sha512-9l+k/J9KG5wPJDX9BcFFzhhwNjwkRb8RsnYhaT1vPY7OufxmQFc9sZzScRCPTiETzl37mrIWVY9zxzmdVeJwDQ==} - '@vitest/spy@4.1.2': - resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} + '@vitest/spy@4.1.3': + resolution: {integrity: sha512-ujj5Uwxagg4XUIfAUyRQxAg631BP6e9joRiN99mr48Bg9fRs+5mdUElhOoZ6rP5mBr8Bs3lmrREnkrQWkrsTCw==} - '@vitest/ui@4.1.2': - resolution: {integrity: sha512-/irhyeAcKS2u6Zokagf9tqZJ0t8S6kMZq4ZG9BHZv7I+fkRrYfQX4w7geYeC2r6obThz39PDxvXQzZX+qXqGeg==} + '@vitest/ui@4.1.3': + resolution: {integrity: sha512-xBPy+43o1fgMLUDlufUXh7tlT/Es8uS5eiyBY2PyPfFYSGpApZskLw65DROoDz+rgYkPuAmb20Mv9Z9g1WQE7w==} peerDependencies: - vitest: 4.1.2 + vitest: 4.1.3 - '@vitest/utils@4.1.2': - resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} + '@vitest/utils@4.1.3': + resolution: {integrity: sha512-Pc/Oexse/khOWsGB+w3q4yzA4te7W4gpZZAvk+fr8qXfTURZUMj5i7kuxsNK5mP/dEB6ao3jfr0rs17fHhbHdw==} acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} @@ -1452,8 +1487,8 @@ packages: bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - baseline-browser-mapping@2.10.13: - resolution: {integrity: sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==} + baseline-browser-mapping@2.10.16: + resolution: {integrity: sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==} engines: {node: '>=6.0.0'} hasBin: true @@ -1494,8 +1529,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001784: - resolution: {integrity: sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==} + caniuse-lite@1.0.30001787: + resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1688,8 +1723,8 @@ packages: easy-file-picker@1.2.0: resolution: {integrity: sha512-GJxOW5s+g/pBr8Ha86a768yx0UZ6fYw+iAOrxK5HOzQ8q9hZxEJF0C8ztdAsH0mcze58FSpzv/d9flRCAuUKHg==} - electron-to-chromium@1.5.331: - resolution: {integrity: sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==} + electron-to-chromium@1.5.334: + resolution: {integrity: sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1727,8 +1762,8 @@ packages: es-toolkit@1.45.1: resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} - esbuild@0.27.5: - resolution: {integrity: sha512-zdQoHBjuDqKsvV5OPaWansOwfSQ0Js+Uj9J85TBvj3bFW1JjWTSULMRwdQAc8qMeIScbClxeMK0jlrtB9linhA==} + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} engines: {node: '>=18'} hasBin: true @@ -2118,24 +2153,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -2390,8 +2429,8 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + postcss@8.5.9: + resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} engines: {node: ^10 || ^12 || >=14} prettier@3.8.1: @@ -2525,8 +2564,8 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rolldown@1.0.0-rc.12: - resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + rolldown@1.0.0-rc.13: + resolution: {integrity: sha512-bvVj8YJmf0rq4pSFmH7laLa6pYrhghv3PRzrCdRAr23g66zOKVJ4wkvFtgohtPLWmthgg8/rkaqRHrpUEh0Zbw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -2536,8 +2575,8 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - sass@1.98.0: - resolution: {integrity: sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==} + sass@1.99.0: + resolution: {integrity: sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==} engines: {node: '>=14.0.0'} hasBin: true @@ -2559,8 +2598,8 @@ packages: peerDependencies: seroval: ^1.0 - seroval-plugins@1.5.1: - resolution: {integrity: sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw==} + seroval-plugins@1.5.2: + resolution: {integrity: sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg==} engines: {node: '>=10'} peerDependencies: seroval: ^1.0 @@ -2569,8 +2608,8 @@ packages: resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} engines: {node: '>=10'} - seroval@1.5.1: - resolution: {integrity: sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA==} + seroval@1.5.2: + resolution: {integrity: sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==} engines: {node: '>=10'} sharp@0.34.5: @@ -2800,12 +2839,12 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.0.4: - resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} engines: {node: '>=18'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} tinyrainbow@3.1.0: @@ -2918,14 +2957,14 @@ packages: svgo: optional: true - vite@8.0.3: - resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} + vite@8.0.7: + resolution: {integrity: sha512-P1PbweD+2/udplnThz3btF4cf6AgPky7kk23RtHUkJIU5BIxwPprhRGmOAHs6FTI7UiGbTNrgNP6jSYD6JaRnw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 '@vitejs/devtools': ^0.1.0 - esbuild: ^0.27.0 + esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 sass: ^1.70.0 @@ -2961,18 +3000,20 @@ packages: yaml: optional: true - vitest@4.1.2: - resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} + vitest@4.1.3: + resolution: {integrity: sha512-DBc4Tx0MPNsqb9isoyOq00lHftVx/KIU44QOm2q59npZyLUkENn8TMFsuzuO+4U2FUa9rgbbPt3udrP25GcjXw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.2 - '@vitest/browser-preview': 4.1.2 - '@vitest/browser-webdriverio': 4.1.2 - '@vitest/ui': 4.1.2 + '@vitest/browser-playwright': 4.1.3 + '@vitest/browser-preview': 4.1.3 + '@vitest/browser-webdriverio': 4.1.3 + '@vitest/coverage-istanbul': 4.1.3 + '@vitest/coverage-v8': 4.1.3 + '@vitest/ui': 4.1.3 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -2989,6 +3030,10 @@ packages: optional: true '@vitest/browser-webdriverio': optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true '@vitest/ui': optional: true happy-dom: @@ -3262,87 +3307,92 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.2.0': dependencies: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.27.5': + '@esbuild/aix-ppc64@0.27.7': optional: true - '@esbuild/android-arm64@0.27.5': + '@esbuild/android-arm64@0.27.7': optional: true - '@esbuild/android-arm@0.27.5': + '@esbuild/android-arm@0.27.7': optional: true - '@esbuild/android-x64@0.27.5': + '@esbuild/android-x64@0.27.7': optional: true - '@esbuild/darwin-arm64@0.27.5': + '@esbuild/darwin-arm64@0.27.7': optional: true - '@esbuild/darwin-x64@0.27.5': + '@esbuild/darwin-x64@0.27.7': optional: true - '@esbuild/freebsd-arm64@0.27.5': + '@esbuild/freebsd-arm64@0.27.7': optional: true - '@esbuild/freebsd-x64@0.27.5': + '@esbuild/freebsd-x64@0.27.7': optional: true - '@esbuild/linux-arm64@0.27.5': + '@esbuild/linux-arm64@0.27.7': optional: true - '@esbuild/linux-arm@0.27.5': + '@esbuild/linux-arm@0.27.7': optional: true - '@esbuild/linux-ia32@0.27.5': + '@esbuild/linux-ia32@0.27.7': optional: true - '@esbuild/linux-loong64@0.27.5': + '@esbuild/linux-loong64@0.27.7': optional: true - '@esbuild/linux-mips64el@0.27.5': + '@esbuild/linux-mips64el@0.27.7': optional: true - '@esbuild/linux-ppc64@0.27.5': + '@esbuild/linux-ppc64@0.27.7': optional: true - '@esbuild/linux-riscv64@0.27.5': + '@esbuild/linux-riscv64@0.27.7': optional: true - '@esbuild/linux-s390x@0.27.5': + '@esbuild/linux-s390x@0.27.7': optional: true - '@esbuild/linux-x64@0.27.5': + '@esbuild/linux-x64@0.27.7': optional: true - '@esbuild/netbsd-arm64@0.27.5': + '@esbuild/netbsd-arm64@0.27.7': optional: true - '@esbuild/netbsd-x64@0.27.5': + '@esbuild/netbsd-x64@0.27.7': optional: true - '@esbuild/openbsd-arm64@0.27.5': + '@esbuild/openbsd-arm64@0.27.7': optional: true - '@esbuild/openbsd-x64@0.27.5': + '@esbuild/openbsd-x64@0.27.7': optional: true - '@esbuild/openharmony-arm64@0.27.5': + '@esbuild/openharmony-arm64@0.27.7': optional: true - '@esbuild/sunos-x64@0.27.5': + '@esbuild/sunos-x64@0.27.7': optional: true - '@esbuild/win32-arm64@0.27.5': + '@esbuild/win32-arm64@0.27.7': optional: true - '@esbuild/win32-ia32@0.27.5': + '@esbuild/win32-ia32@0.27.7': optional: true - '@esbuild/win32-x64@0.27.5': + '@esbuild/win32-x64@0.27.7': optional: true '@floating-ui/core@1.7.5': @@ -3454,7 +3504,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.9.1 + '@emnapi/runtime': 1.9.2 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -3466,7 +3516,7 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@inlang/paraglide-js@2.15.1': + '@inlang/paraglide-js@2.15.2': dependencies: '@inlang/recommend-sherlock': 0.2.1 '@inlang/sdk': 2.9.1 @@ -3533,7 +3583,7 @@ snapshots: '@lix-js/server-protocol-schema@0.1.1': {} - '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': dependencies: '@emnapi/core': 1.9.1 '@emnapi/runtime': 1.9.1 @@ -3552,7 +3602,7 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@oxc-project/types@0.122.0': {} + '@oxc-project/types@0.123.0': {} '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -3643,57 +3693,56 @@ snapshots: react: 19.2.4 react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1) - '@rolldown/binding-android-arm64@1.0.0-rc.12': + '@rolldown/binding-android-arm64@1.0.0-rc.13': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + '@rolldown/binding-darwin-arm64@1.0.0-rc.13': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.12': + '@rolldown/binding-darwin-x64@1.0.0-rc.13': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + '@rolldown/binding-freebsd-x64@1.0.0-rc.13': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.13': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.13': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.13': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.13': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.13': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.13': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.13': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.13': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.13': dependencies: - '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 + '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.13': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.13': optional: true - '@rolldown/pluginutils@1.0.0-rc.12': {} + '@rolldown/pluginutils@1.0.0-rc.13': {} '@rolldown/pluginutils@1.0.0-rc.7': {} @@ -3792,7 +3841,7 @@ snapshots: transitivePeerDependencies: - csstype - '@tanstack/devtools-vite@0.6.0(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0))': + '@tanstack/devtools-vite@0.6.0(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0))': dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 @@ -3804,13 +3853,13 @@ snapshots: chalk: 5.6.2 launch-editor: 2.13.2 picomatch: 4.0.4 - vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0) + vite: 8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - '@tanstack/devtools@0.11.1(csstype@3.2.3)(solid-js@1.9.9)': + '@tanstack/devtools@0.11.2(csstype@3.2.3)(solid-js@1.9.9)': dependencies: '@solid-primitives/event-listener': 2.4.5(solid-js@1.9.9) '@solid-primitives/keyboard': 1.3.5(solid-js@1.9.9) @@ -3836,13 +3885,13 @@ snapshots: '@tanstack/pacer-lite@0.1.1': {} - '@tanstack/query-core@5.96.1': {} + '@tanstack/query-core@5.96.2': {} - '@tanstack/query-devtools@5.96.1': {} + '@tanstack/query-devtools@5.96.2': {} - '@tanstack/react-devtools@0.10.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.9)': + '@tanstack/react-devtools@0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.9)': dependencies: - '@tanstack/devtools': 0.11.1(csstype@3.2.3)(solid-js@1.9.9) + '@tanstack/devtools': 0.11.2(csstype@3.2.3)(solid-js@1.9.9) '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) react: 19.2.4 @@ -3861,15 +3910,15 @@ snapshots: transitivePeerDependencies: - react-dom - '@tanstack/react-query-devtools@5.96.1(@tanstack/react-query@5.96.1(react@19.2.4))(react@19.2.4)': + '@tanstack/react-query-devtools@5.96.2(@tanstack/react-query@5.96.2(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/query-devtools': 5.96.1 - '@tanstack/react-query': 5.96.1(react@19.2.4) + '@tanstack/query-devtools': 5.96.2 + '@tanstack/react-query': 5.96.2(react@19.2.4) react: 19.2.4 - '@tanstack/react-query@5.96.1(react@19.2.4)': + '@tanstack/react-query@5.96.2(react@19.2.4)': dependencies: - '@tanstack/query-core': 5.96.1 + '@tanstack/query-core': 5.96.2 react: 19.2.4 '@tanstack/react-router-devtools@1.166.11(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.168.9)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': @@ -3915,8 +3964,8 @@ snapshots: dependencies: '@tanstack/history': 1.161.6 cookie-es: 2.0.1 - seroval: 1.5.1 - seroval-plugins: 1.5.1(seroval@1.5.1) + seroval: 1.5.2 + seroval-plugins: 1.5.2(seroval@1.5.2) '@tanstack/router-devtools-core@1.167.1(@tanstack/router-core@1.168.9)(csstype@3.2.3)': dependencies: @@ -3939,7 +3988,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.167.12(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0))': + '@tanstack/router-plugin@1.167.12(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) @@ -3956,7 +4005,7 @@ snapshots: zod: 3.25.76 optionalDependencies: '@tanstack/react-router': 1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0) + vite: 8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) transitivePeerDependencies: - supports-color @@ -3970,7 +4019,7 @@ snapshots: babel-dead-code-elimination: 1.0.12 diff: 8.0.4 pathe: 2.0.3 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 transitivePeerDependencies: - supports-color @@ -4048,7 +4097,7 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@25.5.0': + '@types/node@25.5.2': dependencies: undici-types: 7.18.2 @@ -4075,60 +4124,60 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0))': + '@vitejs/plugin-react@6.0.1(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0) + vite: 8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) - '@vitest/expect@4.1.2': + '@vitest/expect@4.1.3': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/spy': 4.1.3 + '@vitest/utils': 4.1.3 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0))': + '@vitest/mocker@4.1.3(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0))': dependencies: - '@vitest/spy': 4.1.2 + '@vitest/spy': 4.1.3 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0) + vite: 8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) - '@vitest/pretty-format@4.1.2': + '@vitest/pretty-format@4.1.3': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.2': + '@vitest/runner@4.1.3': dependencies: - '@vitest/utils': 4.1.2 + '@vitest/utils': 4.1.3 pathe: 2.0.3 - '@vitest/snapshot@4.1.2': + '@vitest/snapshot@4.1.3': dependencies: - '@vitest/pretty-format': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/pretty-format': 4.1.3 + '@vitest/utils': 4.1.3 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.2': {} + '@vitest/spy@4.1.3': {} - '@vitest/ui@4.1.2(vitest@4.1.2)': + '@vitest/ui@4.1.3(vitest@4.1.3)': dependencies: - '@vitest/utils': 4.1.2 + '@vitest/utils': 4.1.3 fflate: 0.8.2 flatted: 3.4.2 pathe: 2.0.3 sirv: 3.0.2 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vitest: 4.1.2(@types/node@25.5.0)(@vitest/ui@4.1.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0)) + vitest: 4.1.3(@types/node@25.5.2)(@vitest/ui@4.1.3)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) - '@vitest/utils@4.1.2': + '@vitest/utils@4.1.3': dependencies: - '@vitest/pretty-format': 4.1.2 + '@vitest/pretty-format': 4.1.3 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -4172,13 +4221,13 @@ snapshots: asynckit@0.4.0: {} - autoprefixer@10.4.27(postcss@8.5.8): + autoprefixer@10.4.27(postcss@8.5.9): dependencies: browserslist: 4.28.2 - caniuse-lite: 1.0.30001784 + caniuse-lite: 1.0.30001787 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.8 + postcss: 8.5.9 postcss-value-parser: 4.2.0 axios@1.14.0: @@ -4200,7 +4249,7 @@ snapshots: bail@2.0.2: {} - baseline-browser-mapping@2.10.13: {} + baseline-browser-mapping@2.10.16: {} binary-extensions@2.3.0: {} @@ -4210,9 +4259,9 @@ snapshots: browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.13 - caniuse-lite: 1.0.30001784 - electron-to-chromium: 1.5.331 + baseline-browser-mapping: 2.10.16 + caniuse-lite: 1.0.30001787 + electron-to-chromium: 1.5.334 node-releases: 2.0.37 update-browserslist-db: 1.2.3(browserslist@4.28.2) @@ -4238,7 +4287,7 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001784: {} + caniuse-lite@1.0.30001787: {} ccount@2.0.1: {} @@ -4391,7 +4440,7 @@ snapshots: easy-file-picker@1.2.0: {} - electron-to-chromium@1.5.331: {} + electron-to-chromium@1.5.334: {} emoji-regex@8.0.0: {} @@ -4422,34 +4471,34 @@ snapshots: es-toolkit@1.45.1: {} - esbuild@0.27.5: + esbuild@0.27.7: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.5 - '@esbuild/android-arm': 0.27.5 - '@esbuild/android-arm64': 0.27.5 - '@esbuild/android-x64': 0.27.5 - '@esbuild/darwin-arm64': 0.27.5 - '@esbuild/darwin-x64': 0.27.5 - '@esbuild/freebsd-arm64': 0.27.5 - '@esbuild/freebsd-x64': 0.27.5 - '@esbuild/linux-arm': 0.27.5 - '@esbuild/linux-arm64': 0.27.5 - '@esbuild/linux-ia32': 0.27.5 - '@esbuild/linux-loong64': 0.27.5 - '@esbuild/linux-mips64el': 0.27.5 - '@esbuild/linux-ppc64': 0.27.5 - '@esbuild/linux-riscv64': 0.27.5 - '@esbuild/linux-s390x': 0.27.5 - '@esbuild/linux-x64': 0.27.5 - '@esbuild/netbsd-arm64': 0.27.5 - '@esbuild/netbsd-x64': 0.27.5 - '@esbuild/openbsd-arm64': 0.27.5 - '@esbuild/openbsd-x64': 0.27.5 - '@esbuild/openharmony-arm64': 0.27.5 - '@esbuild/sunos-x64': 0.27.5 - '@esbuild/win32-arm64': 0.27.5 - '@esbuild/win32-ia32': 0.27.5 - '@esbuild/win32-x64': 0.27.5 + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 escalade@3.2.0: {} @@ -5162,13 +5211,13 @@ snapshots: postcss-resolve-nested-selector@0.1.6: {} - postcss-safe-parser@7.0.1(postcss@8.5.8): + postcss-safe-parser@7.0.1(postcss@8.5.9): dependencies: - postcss: 8.5.8 + postcss: 8.5.9 - postcss-scss@4.0.9(postcss@8.5.8): + postcss-scss@4.0.9(postcss@8.5.9): dependencies: - postcss: 8.5.8 + postcss: 8.5.9 postcss-selector-parser@7.1.1: dependencies: @@ -5177,7 +5226,7 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.5.8: + postcss@8.5.9: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -5324,29 +5373,26 @@ snapshots: reusify@1.1.0: {} - rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + rolldown@1.0.0-rc.13: dependencies: - '@oxc-project/types': 0.122.0 - '@rolldown/pluginutils': 1.0.0-rc.12 + '@oxc-project/types': 0.123.0 + '@rolldown/pluginutils': 1.0.0-rc.13 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.12 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 - '@rolldown/binding-darwin-x64': 1.0.0-rc.12 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' + '@rolldown/binding-android-arm64': 1.0.0-rc.13 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.13 + '@rolldown/binding-darwin-x64': 1.0.0-rc.13 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.13 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.13 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.13 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.13 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.13 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.13 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.13 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.13 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.13 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.13 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.13 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.13 run-parallel@1.2.0: dependencies: @@ -5356,7 +5402,7 @@ snapshots: dependencies: tslib: 2.8.1 - sass@1.98.0: + sass@1.99.0: dependencies: chokidar: 4.0.3 immutable: 5.1.5 @@ -5374,13 +5420,13 @@ snapshots: dependencies: seroval: 1.3.2 - seroval-plugins@1.5.1(seroval@1.5.1): + seroval-plugins@1.5.2(seroval@1.5.2): dependencies: - seroval: 1.5.1 + seroval: 1.5.2 seroval@1.3.2: {} - seroval@1.5.1: {} + seroval@1.5.2: {} sharp@0.34.5: dependencies: @@ -5516,26 +5562,26 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - stylelint-config-recommended-scss@17.0.0(postcss@8.5.8)(stylelint@17.6.0(typescript@5.9.3)): + stylelint-config-recommended-scss@17.0.0(postcss@8.5.9)(stylelint@17.6.0(typescript@5.9.3)): dependencies: - postcss-scss: 4.0.9(postcss@8.5.8) + postcss-scss: 4.0.9(postcss@8.5.9) stylelint: 17.6.0(typescript@5.9.3) stylelint-config-recommended: 18.0.0(stylelint@17.6.0(typescript@5.9.3)) stylelint-scss: 7.0.0(stylelint@17.6.0(typescript@5.9.3)) optionalDependencies: - postcss: 8.5.8 + postcss: 8.5.9 stylelint-config-recommended@18.0.0(stylelint@17.6.0(typescript@5.9.3)): dependencies: stylelint: 17.6.0(typescript@5.9.3) - stylelint-config-standard-scss@17.0.0(postcss@8.5.8)(stylelint@17.6.0(typescript@5.9.3)): + stylelint-config-standard-scss@17.0.0(postcss@8.5.9)(stylelint@17.6.0(typescript@5.9.3)): dependencies: stylelint: 17.6.0(typescript@5.9.3) - stylelint-config-recommended-scss: 17.0.0(postcss@8.5.8)(stylelint@17.6.0(typescript@5.9.3)) + stylelint-config-recommended-scss: 17.0.0(postcss@8.5.9)(stylelint@17.6.0(typescript@5.9.3)) stylelint-config-standard: 40.0.0(stylelint@17.6.0(typescript@5.9.3)) optionalDependencies: - postcss: 8.5.8 + postcss: 8.5.9 stylelint-config-standard@40.0.0(stylelint@17.6.0(typescript@5.9.3)): dependencies: @@ -5583,8 +5629,8 @@ snapshots: micromatch: 4.0.8 normalize-path: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.8 - postcss-safe-parser: 7.0.1(postcss@8.5.8) + postcss: 8.5.9 + postcss-safe-parser: 7.0.1(postcss@8.5.9) postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 string-width: 8.2.0 @@ -5712,9 +5758,9 @@ snapshots: tinybench@2.9.0: {} - tinyexec@1.0.4: {} + tinyexec@1.1.1: {} - tinyglobby@0.2.15: + tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 @@ -5735,7 +5781,7 @@ snapshots: tsx@4.21.0: dependencies: - esbuild: 0.27.5 + esbuild: 0.27.7 get-tsconfig: 4.13.7 optionalDependencies: fsevents: 2.3.3 @@ -5836,40 +5882,37 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-plugin-image-optimizer@2.0.3(sharp@0.34.5)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0)): + vite-plugin-image-optimizer@2.0.3(sharp@0.34.5)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)): dependencies: ansi-colors: 4.1.3 pathe: 2.0.3 - vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0) + vite: 8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) optionalDependencies: sharp: 0.34.5 - vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0): + vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.8 - rolldown: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) - tinyglobby: 0.2.15 + postcss: 8.5.9 + rolldown: 1.0.0-rc.13 + tinyglobby: 0.2.16 optionalDependencies: - '@types/node': 25.5.0 - esbuild: 0.27.5 + '@types/node': 25.5.2 + esbuild: 0.27.7 fsevents: 2.3.3 - sass: 1.98.0 + sass: 1.99.0 tsx: 4.21.0 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - - vitest@4.1.2(@types/node@25.5.0)(@vitest/ui@4.1.2)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0)): - dependencies: - '@vitest/expect': 4.1.2 - '@vitest/mocker': 4.1.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0)) - '@vitest/pretty-format': 4.1.2 - '@vitest/runner': 4.1.2 - '@vitest/snapshot': 4.1.2 - '@vitest/spy': 4.1.2 - '@vitest/utils': 4.1.2 + + vitest@4.1.3(@types/node@25.5.2)(@vitest/ui@4.1.3)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)): + dependencies: + '@vitest/expect': 4.1.3 + '@vitest/mocker': 4.1.3(vite@8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) + '@vitest/pretty-format': 4.1.3 + '@vitest/runner': 4.1.3 + '@vitest/snapshot': 4.1.3 + '@vitest/spy': 4.1.3 + '@vitest/utils': 4.1.3 es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -5878,14 +5921,14 @@ snapshots: picomatch: 4.0.4 std-env: 4.0.0 tinybench: 2.9.0 - tinyexec: 1.0.4 - tinyglobby: 0.2.15 + tinyexec: 1.1.1 + tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.5)(sass@1.98.0)(tsx@4.21.0) + vite: 8.0.7(@types/node@25.5.2)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.5.0 - '@vitest/ui': 4.1.2(vitest@4.1.2) + '@types/node': 25.5.2 + '@vitest/ui': 4.1.3(vitest@4.1.3) transitivePeerDependencies: - msw diff --git a/web/src/pages/EditLocationPage/EditLocationPage.tsx b/web/src/pages/EditLocationPage/EditLocationPage.tsx index ad8bce61b0..a881223dfe 100644 --- a/web/src/pages/EditLocationPage/EditLocationPage.tsx +++ b/web/src/pages/EditLocationPage/EditLocationPage.tsx @@ -465,7 +465,7 @@ const EditLocationForm = ({ location }: { location: NetworkLocation }) => { }), actionPromise: () => submitLocationChanges(value), submitProps: { - text: m.controls_save_changes(), + text: m.controls_save_changes_anyway(), variant: 'critical', }, }); diff --git a/web/src/pages/settings/SettingsLdapPage/SettingsLdapPage.tsx b/web/src/pages/settings/SettingsLdapPage/SettingsLdapPage.tsx index 322a9b2343..ec98340fe1 100644 --- a/web/src/pages/settings/SettingsLdapPage/SettingsLdapPage.tsx +++ b/web/src/pages/settings/SettingsLdapPage/SettingsLdapPage.tsx @@ -5,6 +5,7 @@ import { SettingsCard } from '../../../shared/components/SettingsCard/SettingsCa import { SettingsHeader } from '../../../shared/components/SettingsHeader/SettingsHeader'; import { SettingsLayout } from '../../../shared/components/SettingsLayout/SettingsLayout'; import './style.scss'; +import { useStore } from '@tanstack/react-form'; import { useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query'; import { Suspense, useMemo } from 'react'; import Skeleton from 'react-loading-skeleton'; @@ -85,9 +86,9 @@ export const SettingsLdapPage = () => { }; const formSchema = z.object({ - ldap_bind_password: z.string().trim().nullable(), - ldap_bind_username: z.string().trim().nullable(), - ldap_url: z.string().trim(), + ldap_bind_password: z.string().trim().min(1, m.form_error_required()), + ldap_bind_username: z.string().trim().min(1, m.form_error_required()), + ldap_url: z.url(m.form_error_invalid()).min(1, m.form_error_required()), ldap_group_member_attr: z.string().trim().min(1, m.form_error_required()), ldap_group_obj_class: z.string().trim().min(1, m.form_error_required()), ldap_group_search_base: z.string().trim().min(1, m.form_error_required()), @@ -111,12 +112,15 @@ const formSchema = z.object({ ldap_uses_ad: z.boolean(), ldap_user_rdn_attr: z.string().trim().nullable(), ldap_sync_groups: z.string().trim().nullable(), + ldap_remote_enrollment_enabled: z.boolean(), + ldap_remote_enrollment_send_invite: z.boolean(), }); type FormFields = z.infer; const PageForm = () => { const isAppLdapEnabled = useApp((s) => s.appInfo.ldap_info.enabled); + const smtpEnabled = useApp((s) => s.appInfo.smtp_enabled); const { data: licenseInfo } = useSuspenseQuery(getLicenseInfoQueryOptions); const { data: settings } = useSuspenseQuery(getSettingsQueryOptions); @@ -149,6 +153,9 @@ const PageForm = () => { ldap_uses_ad: settings?.ldap_uses_ad ?? false, ldap_user_rdn_attr: settings?.ldap_user_rdn_attr ?? '', ldap_sync_groups: settings?.ldap_sync_groups.join(', ') || null, + ldap_remote_enrollment_enabled: settings?.ldap_remote_enrollment_enabled ?? false, + ldap_remote_enrollment_send_invite: + settings?.ldap_remote_enrollment_send_invite ?? false, }; }, [settings]); @@ -209,6 +216,36 @@ const PageForm = () => { }, }); + const requiredFieldsFilled = useStore(form.store, (s) => { + const v = s.values; + return ( + URL.canParse(v.ldap_url.trim()) && + v.ldap_bind_username !== null && + v.ldap_bind_username.trim().length > 0 && + v.ldap_bind_password !== null && + v.ldap_bind_password.trim().length > 0 && + v.ldap_username_attr.trim().length > 0 && + v.ldap_user_search_base.trim().length > 0 && + v.ldap_user_obj_class.trim().length > 0 && + v.ldap_member_attr.trim().length > 0 && + v.ldap_groupname_attr.trim().length > 0 && + v.ldap_group_obj_class.trim().length > 0 && + v.ldap_group_member_attr.trim().length > 0 && + v.ldap_group_search_base.trim().length > 0 + ); + }); + + const remoteEnrollmentSectionDisabled = !requiredFieldsFilled || !smtpEnabled; + const remoteEnrollmentWarning = useMemo( + () => + !requiredFieldsFilled + ? m.settings_ldap_remote_enrollment_warning_no_sync_no_smtp() + : !smtpEnabled + ? m.settings_ldap_remote_enrollment_warning_no_smtp() + : undefined, + [requiredFieldsFilled, smtpEnabled], + ); + return (
{ @@ -418,12 +455,16 @@ const PageForm = () => { {(field) => ( @@ -435,6 +476,7 @@ const PageForm = () => { @@ -487,10 +529,50 @@ const PageForm = () => { )} + + + + + {(field) => ( + + )} + + s.values.ldap_remote_enrollment_enabled}> + {(remoteEnrollmentEnabled) => ( + + + + {(field) => ( + + )} + + + )} + + {(field) => ( - + )}
diff --git a/web/src/pages/user-profile/UserProfilePage/tabs/ProfileDetailsTab/modals/EmailMfaSetupModal/EmailMfaSetupModal.tsx b/web/src/pages/user-profile/UserProfilePage/tabs/ProfileDetailsTab/modals/EmailMfaSetupModal/EmailMfaSetupModal.tsx index 2559ecc68a..e726b450a4 100644 --- a/web/src/pages/user-profile/UserProfilePage/tabs/ProfileDetailsTab/modals/EmailMfaSetupModal/EmailMfaSetupModal.tsx +++ b/web/src/pages/user-profile/UserProfilePage/tabs/ProfileDetailsTab/modals/EmailMfaSetupModal/EmailMfaSetupModal.tsx @@ -19,6 +19,7 @@ import api from '../../../../../../../shared/api/api'; import type { ApiError } from '../../../../../../../shared/api/types'; import { Button } from '../../../../../../../shared/defguard-ui/components/Button/Button'; import { SizedBox } from '../../../../../../../shared/defguard-ui/components/SizedBox/SizedBox'; +import { useEffectOnce } from '../../../../../../../shared/defguard-ui/hooks/useEffectOnce'; import { ThemeSpacing } from '../../../../../../../shared/defguard-ui/types'; import { isPresent } from '../../../../../../../shared/defguard-ui/utils/isPresent'; import { formChangeLogic } from '../../../../../../../shared/formLogic'; @@ -118,9 +119,9 @@ const ModalContent = () => { const isSubmitting = useStore(form.store, (s) => s.isSubmitting); - useEffect(() => { + useEffectOnce(() => { void api.auth.mfa.email.init(); - }, []); + }); return ( <> diff --git a/web/src/shared/api/types.ts b/web/src/shared/api/types.ts index 7c07f6f2eb..3a6270d9fa 100644 --- a/web/src/shared/api/types.ts +++ b/web/src/shared/api/types.ts @@ -991,6 +991,8 @@ export interface SettingsLDAP { ldap_uses_ad: boolean; ldap_user_rdn_attr: string | null; ldap_sync_groups: string[]; + ldap_remote_enrollment_enabled: boolean; + ldap_remote_enrollment_send_invite: boolean; } export interface SettingsOpenID { diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index 2e18ae13bd..78679467f1 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit 2e18ae13bd9e96a5574042eb9323171ca02d0943 +Subproject commit 78679467f1b20c4562c1f5b49cd9f9b3a72439e8