diff --git a/crates/defguard_core/src/cert_settings.rs b/crates/defguard_core/src/cert_settings.rs index 60135d99d4..51567dd394 100644 --- a/crates/defguard_core/src/cert_settings.rs +++ b/crates/defguard_core/src/cert_settings.rs @@ -16,8 +16,18 @@ use utoipa::ToSchema; /// Errors arising from certificate settings operations. #[derive(Debug, Error)] pub enum CertSettingsError { - #[error("Invalid certificate: {0}")] - InvalidCert(String), + #[error("cert_pem is required for own_cert")] + MissingCertPem, + #[error("key_pem is required for own_cert")] + MissingKeyPem, + #[error("Invalid certificate or private key PEM")] + InvalidCertOrKey, + #[error("Certificate validity period is invalid")] + InvalidValidityPeriod, + #[error("Certificate has expired")] + CertExpired, + #[error("Certificate is not valid yet")] + CertNotYetValid, #[error("Certificate error: {0}")] Cert(#[from] CertificateError), #[error("URL parse error: {0}")] @@ -36,9 +46,7 @@ async fn parse_cert(cert_pem: &str, key_pem: &str) -> Result Result now { - return Err(CertSettingsError::InvalidCert( - "Certificate is not valid yet".to_owned(), - )); + return Err(CertSettingsError::CertNotYetValid); } Ok(info) @@ -206,12 +208,8 @@ pub async fn apply_internal_url_settings( }) } InternalSslType::OwnCert => { - let cert_pem_str = config.cert_pem.ok_or_else(|| { - CertSettingsError::InvalidCert("cert_pem is required for own_cert".to_owned()) - })?; - let key_pem_str = config.key_pem.ok_or_else(|| { - CertSettingsError::InvalidCert("key_pem is required for own_cert".to_owned()) - })?; + let cert_pem_str = config.cert_pem.ok_or(CertSettingsError::MissingCertPem)?; + let key_pem_str = config.key_pem.ok_or(CertSettingsError::MissingKeyPem)?; let info = parse_cert(&cert_pem_str, &key_pem_str).await?; let valid_for_days = (info.not_after.and_utc() - chrono::Utc::now()).num_days(); @@ -332,12 +330,8 @@ pub async fn apply_external_url_settings( }) } ExternalSslType::OwnCert => { - let cert_pem_str = config.cert_pem.ok_or_else(|| { - CertSettingsError::InvalidCert("cert_pem is required for own_cert".to_owned()) - })?; - let key_pem_str = config.key_pem.ok_or_else(|| { - CertSettingsError::InvalidCert("key_pem is required for own_cert".to_owned()) - })?; + let cert_pem_str = config.cert_pem.ok_or(CertSettingsError::MissingCertPem)?; + let key_pem_str = config.key_pem.ok_or(CertSettingsError::MissingKeyPem)?; let info = parse_cert(&cert_pem_str, &key_pem_str).await?; let valid_for_days = (info.not_after.and_utc() - chrono::Utc::now()).num_days(); @@ -438,7 +432,10 @@ pub(crate) async fn refresh_proxy_self_signed_cert( #[cfg(test)] mod tests { use chrono::Utc; - use defguard_certs::CertificateAuthority; + use defguard_certs::{ + CertificateAuthority, Csr, DnType, ExtendedKeyUsagePurpose, PemLabel, der_to_pem, + generate_key_pair, + }; use defguard_common::db::{ models::{ Certificates, CoreCertSource, ProxyCertSource, Settings, @@ -449,7 +446,7 @@ mod tests { use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::{ - CertSettingsError, extract_hostname, refresh_core_self_signed_cert, + CertSettingsError, extract_hostname, parse_cert, refresh_core_self_signed_cert, refresh_proxy_self_signed_cert, }; @@ -525,6 +522,68 @@ mod tests { ); } + #[tokio::test] + async fn parse_cert_invalid_pem() { + let err = parse_cert("not a certificate", "not a key") + .await + .err() + .expect("expected an error"); + assert!(matches!(err, CertSettingsError::InvalidCertOrKey)); + } + + #[tokio::test] + async fn parse_cert_key_mismatch() { + let ca = make_ca(); + + let key_pair = generate_key_pair().expect("failed to generate key pair"); + let san = vec!["example.com".to_owned()]; + let dn = vec![(DnType::CommonName, "example.com")]; + let csr = Csr::new(&key_pair, &san, dn).expect("failed to build CSR"); + let cert = ca + .sign_web_server_cert(&csr) + .expect("failed to sign certificate"); + let cert_pem = + der_to_pem(cert.der(), PemLabel::Certificate).expect("failed to encode cert"); + + let other_key_pair = generate_key_pair().expect("failed to generate key pair"); + let mismatched_key_pem = der_to_pem( + other_key_pair.serialize_der().as_slice(), + PemLabel::PrivateKey, + ) + .expect("failed to encode key"); + + let err = parse_cert(&cert_pem, &mismatched_key_pem) + .await + .err() + .expect("expected an error"); + assert!(matches!(err, CertSettingsError::InvalidCertOrKey)); + } + + #[tokio::test] + async fn parse_cert_expired() { + let ca = make_ca(); + + let key_pair = generate_key_pair().expect("failed to generate key pair"); + let san = vec!["example.com".to_owned()]; + let dn = vec![(DnType::CommonName, "example.com")]; + let csr = Csr::new(&key_pair, &san, dn).expect("failed to build CSR"); + let cert = ca + .sign_csr_with_validity(&csr, 0, &[ExtendedKeyUsagePurpose::ServerAuth]) + .expect("failed to sign certificate"); + let cert_pem = + der_to_pem(cert.der(), PemLabel::Certificate).expect("failed to encode cert"); + let key_pem = der_to_pem(key_pair.serialize_der().as_slice(), PemLabel::PrivateKey) + .expect("failed to encode key"); + + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + + let err = parse_cert(&cert_pem, &key_pem) + .await + .err() + .expect("expected an error"); + assert!(matches!(err, CertSettingsError::CertExpired)); + } + #[sqlx::test] async fn refresh_core_self_signed_cert_generates_new_cert( _: PgPoolOptions, diff --git a/crates/defguard_core/src/enterprise/directory_sync/tests.rs b/crates/defguard_core/src/enterprise/directory_sync/tests.rs index 2980397f44..aeee868ee7 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/tests.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/tests.rs @@ -897,7 +897,7 @@ mod test { true, ) .await; - provider.directory_sync_user_groups = Some(vec!["group1".to_string()]); + provider.directory_sync_user_groups = Some(vec!["group1".to_owned()]); provider.save(&pool).await.unwrap(); // no users in Defguard before sync @@ -938,7 +938,7 @@ mod test { true, ) .await; - provider.directory_sync_user_groups = Some(vec!["nonexistent-group".to_string()]); + provider.directory_sync_user_groups = Some(vec!["nonexistent-group".to_owned()]); provider.save(&pool).await.unwrap(); let (ldap_tx, _ldap_rx) = ldap_test_channel(); @@ -979,7 +979,7 @@ mod test { assert!(defguard_users.is_empty()); // only allow one of the directory users to be imported - let allowed_emails = HashSet::from(["testuser@email.com".to_string()]); + let allowed_emails = HashSet::from(["testuser@email.com".to_owned()]); let all_users = client.get_all_users().await.unwrap(); let (ldap_tx, _ldap_rx) = ldap_test_channel(); sync_all_users_state( @@ -1018,12 +1018,12 @@ mod test { // the test provider returns group1 as the only group of any user assert!( - user_in_directory_groups(&pool, "testuser@email.com", &["group1".to_string()]) + user_in_directory_groups(&pool, "testuser@email.com", &["group1".to_owned()]) .await .unwrap() ); assert!( - !user_in_directory_groups(&pool, "testuser@email.com", &["group2".to_string()]) + !user_in_directory_groups(&pool, "testuser@email.com", &["group2".to_owned()]) .await .unwrap() ); diff --git a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs index c9fffb6aca..c7ab88e131 100644 --- a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs +++ b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs @@ -165,7 +165,7 @@ pub(crate) async fn add_openid_provider( Some( user_groups .split(',') - .map(|s| s.trim().to_string()) + .map(|s| s.trim().to_owned()) .collect(), ) } @@ -410,7 +410,7 @@ pub(crate) async fn modify_openid_provider( Some( user_groups .split(',') - .map(|s| s.trim().to_string()) + .map(|s| s.trim().to_owned()) .collect(), ) } diff --git a/crates/defguard_core/src/enterprise/oauth2/mod.rs b/crates/defguard_core/src/enterprise/oauth2/mod.rs index 590d854798..6833439d30 100644 --- a/crates/defguard_core/src/enterprise/oauth2/mod.rs +++ b/crates/defguard_core/src/enterprise/oauth2/mod.rs @@ -95,7 +95,7 @@ async fn microsoft_access_token(smtp_settings: &mut SmtpSettings) -> Result for WebError { @@ -259,8 +273,13 @@ impl From for WebError { fn from(err: CertSettingsError) -> Self { error!("{err}"); match err { - CertSettingsError::InvalidCert(msg) => Self::BadRequest(msg), - CertSettingsError::Cert(e) => Self::CertificateError(e), + CertSettingsError::MissingCertPem => Self::CertMissingCertPem, + CertSettingsError::MissingKeyPem => Self::CertMissingKeyPem, + CertSettingsError::InvalidCertOrKey => Self::CertInvalidCertOrKey, + CertSettingsError::InvalidValidityPeriod => Self::CertInvalidValidityPeriod, + CertSettingsError::CertExpired => Self::CertExpired, + CertSettingsError::CertNotYetValid => Self::CertNotYetValid, + CertSettingsError::Cert(e) => Self::CertParseError(e.to_string()), CertSettingsError::Url(e) => Self::BadRequest(e), CertSettingsError::Settings(e) => Self::BadRequest(e.to_string()), CertSettingsError::Db(e) => Self::DbError(e.to_string()), diff --git a/crates/defguard_core/src/handlers/mod.rs b/crates/defguard_core/src/handlers/mod.rs index bf4cbe1633..bef91a7414 100644 --- a/crates/defguard_core/src/handlers/mod.rs +++ b/crates/defguard_core/src/handlers/mod.rs @@ -67,6 +67,13 @@ pub(crate) mod yubikey; pub enum WebErrorCode { NetworkFull, UserGroupsNotSynced, + CertMissingCertPem, + CertMissingKeyPem, + CertInvalidCertOrKey, + CertInvalidValidityPeriod, + CertExpired, + CertNotYetValid, + CertParseError, } pub static SESSION_COOKIE_NAME: &str = "defguard_session"; @@ -296,6 +303,37 @@ impl From for ApiResponse { StatusCode::UNAUTHORIZED, ) } + WebError::CertMissingCertPem => Self::new( + json!({"msg": web_error.to_string(), "code": WebErrorCode::CertMissingCertPem}), + StatusCode::BAD_REQUEST, + ), + WebError::CertMissingKeyPem => Self::new( + json!({"msg": web_error.to_string(), "code": WebErrorCode::CertMissingKeyPem}), + StatusCode::BAD_REQUEST, + ), + WebError::CertInvalidCertOrKey => Self::new( + json!({"msg": web_error.to_string(), "code": WebErrorCode::CertInvalidCertOrKey}), + StatusCode::BAD_REQUEST, + ), + WebError::CertInvalidValidityPeriod => Self::new( + json!({"msg": web_error.to_string(), "code": WebErrorCode::CertInvalidValidityPeriod}), + StatusCode::BAD_REQUEST, + ), + WebError::CertExpired => Self::new( + json!({"msg": web_error.to_string(), "code": WebErrorCode::CertExpired}), + StatusCode::BAD_REQUEST, + ), + WebError::CertNotYetValid => Self::new( + json!({"msg": web_error.to_string(), "code": WebErrorCode::CertNotYetValid}), + StatusCode::BAD_REQUEST, + ), + WebError::CertParseError(msg) => { + warn!(msg); + Self::new( + json!({"msg": msg, "code": WebErrorCode::CertParseError}), + StatusCode::BAD_REQUEST, + ) + } WebError::TemplateError(err) => { error!("Template error: {err}"); Self::new( diff --git a/crates/defguard_core/src/mail/mod.rs b/crates/defguard_core/src/mail/mod.rs index fa40427acb..dddebaabc5 100644 --- a/crates/defguard_core/src/mail/mod.rs +++ b/crates/defguard_core/src/mail/mod.rs @@ -114,7 +114,7 @@ pub struct Mail { impl Mail { /// Create new [`Mail`]. #[must_use] - pub fn new(to: T, subject: String, html: String, text: String) -> Mail + pub fn new(to: T, subject: String, html: String, text: String) -> Self where T: Into, { @@ -355,31 +355,31 @@ impl MailMessage { } } match self { - Self::Test => "Defguard: Test message".to_string(), - Self::Welcome => WELCOME_EMAIL_SUBJECT.to_string(), - Self::SupportData => "Defguard: Support data".to_string(), - Self::DesktopStart => "Defguard: Desktop client configuration".to_string(), - Self::NewAccount => "Defguard: User enrollment".to_string(), - Self::NewDevice => "Defguard: new device added to your account".to_string(), - Self::NewDeviceLogin => "Defguard: New device logged in to your account".to_string(), - Self::NewDeviceOIDCLogin => "New login to OIDC application".to_string(), - Self::GatewayDisconnect => "Defguard: Gateway disconnected".to_string(), - Self::GatewayReconnect => "Defguard: Gateway reconnected".to_string(), - Self::MFAActivation => "Multi-Factor Authentication activation".to_string(), + Self::Test => "Defguard: Test message".to_owned(), + Self::Welcome => WELCOME_EMAIL_SUBJECT.to_owned(), + Self::SupportData => "Defguard: Support data".to_owned(), + Self::DesktopStart => "Defguard: Desktop client configuration".to_owned(), + Self::NewAccount => "Defguard: User enrollment".to_owned(), + Self::NewDevice => "Defguard: new device added to your account".to_owned(), + Self::NewDeviceLogin => "Defguard: New device logged in to your account".to_owned(), + Self::NewDeviceOIDCLogin => "New login to OIDC application".to_owned(), + Self::GatewayDisconnect => "Defguard: Gateway disconnected".to_owned(), + Self::GatewayReconnect => "Defguard: Gateway reconnected".to_owned(), + Self::MFAActivation => "Multi-Factor Authentication activation".to_owned(), Self::MFAConfigured { method } => { format!("Multi-Factor Authentication {method} has been activated") } - Self::MFACode => "Defguard: Multi-Factor Authentication code for login".to_string(), - Self::PasswordReset => "Defguard: Password reset".to_string(), - Self::PasswordResetDone => "Defguard: Password reset success".to_string(), - Self::PasswordResetDisabled => "Defguard: Password reset disabled".to_string(), - Self::UserImportBlocked => "User import blocked".to_string(), - Self::EnrollmentNotification => "Defguard: User enrollment completed".to_string(), + Self::MFACode => "Defguard: Multi-Factor Authentication code for login".to_owned(), + Self::PasswordReset => "Defguard: Password reset".to_owned(), + Self::PasswordResetDone => "Defguard: Password reset success".to_owned(), + Self::PasswordResetDisabled => "Defguard: Password reset disabled".to_owned(), + Self::UserImportBlocked => "User import blocked".to_owned(), + Self::EnrollmentNotification => "Defguard: User enrollment completed".to_owned(), Self::LetsencryptCertRefreshFailed => { - "Defguard: automatic Let's Encrypt certificate refresh failed".to_string() + "Defguard: automatic Let's Encrypt certificate refresh failed".to_owned() } - Self::CertificateExpiration => "Defguard: Certificate expiration".to_string(), - Self::CertificateExpired => "Defguard: Certificate has expired".to_string(), + Self::CertificateExpiration => "Defguard: Certificate expiration".to_owned(), + Self::CertificateExpired => "Defguard: Certificate has expired".to_owned(), } } diff --git a/web/messages/en/api-error.json b/web/messages/en/api-error.json index af273eaf4a..b9f6ca9fb1 100644 --- a/web/messages/en/api-error.json +++ b/web/messages/en/api-error.json @@ -1,5 +1,12 @@ { "$schema": "https://inlang.com/schema/inlang-message-format", "api_error_network_full": "There are no free IP adresses in the network - please change the network settings", - "api_error_user_groups_not_synced": "You could not be logged in because your group is not synchronized, please contact your administrator" + "api_error_user_groups_not_synced": "You could not be logged in because your group is not synchronized, please contact your administrator", + "api_error_cert_missing_cert_pem": "Please provide a certificate file", + "api_error_cert_missing_key_pem": "Please provide a private key file", + "api_error_cert_invalid_cert_or_key": "The certificate or private key is invalid, or they don't match", + "api_error_cert_invalid_validity_period": "The certificate's validity period is invalid", + "api_error_cert_expired": "The uploaded certificate has expired", + "api_error_cert_not_yet_valid": "The uploaded certificate is not valid yet", + "api_error_cert_parse_error": "The certificate could not be parsed" } diff --git a/web/src/pages/MigrationWizardPage/steps/MigrationWizardExternalUrlSettingsStep.tsx b/web/src/pages/MigrationWizardPage/steps/MigrationWizardExternalUrlSettingsStep.tsx index 8a6a83d082..b12670eea8 100644 --- a/web/src/pages/MigrationWizardPage/steps/MigrationWizardExternalUrlSettingsStep.tsx +++ b/web/src/pages/MigrationWizardPage/steps/MigrationWizardExternalUrlSettingsStep.tsx @@ -1,7 +1,10 @@ import { useMutation } from '@tanstack/react-query'; +import type { AxiosError } from 'axios'; import z from 'zod'; import { m } from '../../../paraglide/messages'; import api from '../../../shared/api/api'; +import { getApiErrorMessage } from '../../../shared/api/apiErrorMessages'; +import type { ApiError } from '../../../shared/api/types'; import { Controls } from '../../../shared/components/Controls/Controls'; import { WizardCard } from '../../../shared/components/wizard/WizardCard/WizardCard'; import { Button } from '../../../shared/defguard-ui/components/Button/Button'; @@ -45,8 +48,15 @@ export const MigrationWizardExternalUrlSettingsStep = () => { }); useMigrationWizardStore.getState().next(); }, - onError: (error) => { - Snackbar.error(m.initial_setup_general_config_error_save_failed()); + onError: (error: AxiosError) => { + const code = error.response?.data?.code; + const fallback = + error.response?.data?.msg ?? m.initial_setup_general_config_error_save_failed(); + if (code) { + Snackbar.error(getApiErrorMessage(code, fallback)); + } else { + Snackbar.error(fallback); + } console.error('Failed to save external URL settings:', error); }, }); diff --git a/web/src/pages/MigrationWizardPage/steps/MigrationWizardInternalUrlSettingsStep.tsx b/web/src/pages/MigrationWizardPage/steps/MigrationWizardInternalUrlSettingsStep.tsx index f3975a31e6..f3f7e0cc0f 100644 --- a/web/src/pages/MigrationWizardPage/steps/MigrationWizardInternalUrlSettingsStep.tsx +++ b/web/src/pages/MigrationWizardPage/steps/MigrationWizardInternalUrlSettingsStep.tsx @@ -1,7 +1,10 @@ import { useMutation } from '@tanstack/react-query'; +import type { AxiosError } from 'axios'; import z from 'zod'; import { m } from '../../../paraglide/messages'; import api from '../../../shared/api/api'; +import { getApiErrorMessage } from '../../../shared/api/apiErrorMessages'; +import type { ApiError } from '../../../shared/api/types'; import { Controls } from '../../../shared/components/Controls/Controls'; import { WizardCard } from '../../../shared/components/wizard/WizardCard/WizardCard'; import { Button } from '../../../shared/defguard-ui/components/Button/Button'; @@ -53,8 +56,15 @@ export const MigrationWizardInternalUrlSettingsStep = () => { }); useMigrationWizardStore.getState().next(); }, - onError: (error) => { - Snackbar.error(m.initial_setup_general_config_error_save_failed()); + onError: (error: AxiosError) => { + const code = error.response?.data?.code; + const fallback = + error.response?.data?.msg ?? m.initial_setup_general_config_error_save_failed(); + if (code) { + Snackbar.error(getApiErrorMessage(code, fallback)); + } else { + Snackbar.error(fallback); + } console.error('Failed to save internal URL settings:', error); }, }); diff --git a/web/src/pages/SetupPage/autoAdoption/steps/AutoAdoptionExternalUrlSettingsStep.tsx b/web/src/pages/SetupPage/autoAdoption/steps/AutoAdoptionExternalUrlSettingsStep.tsx index 1582c3d050..d227c1f7e4 100644 --- a/web/src/pages/SetupPage/autoAdoption/steps/AutoAdoptionExternalUrlSettingsStep.tsx +++ b/web/src/pages/SetupPage/autoAdoption/steps/AutoAdoptionExternalUrlSettingsStep.tsx @@ -1,7 +1,10 @@ import { useMutation } from '@tanstack/react-query'; +import type { AxiosError } from 'axios'; import z from 'zod'; import { m } from '../../../../paraglide/messages'; import api from '../../../../shared/api/api'; +import { getApiErrorMessage } from '../../../../shared/api/apiErrorMessages'; +import type { ApiError } from '../../../../shared/api/types'; import { Controls } from '../../../../shared/components/Controls/Controls'; import { WizardCard } from '../../../../shared/components/wizard/WizardCard/WizardCard'; import { Button } from '../../../../shared/defguard-ui/components/Button/Button'; @@ -49,8 +52,15 @@ export const AutoAdoptionExternalUrlSettingsStep = () => { }); setActiveStep(AutoAdoptionSetupStep.ExternalUrlSslConfig); }, - onError: (error) => { - Snackbar.error(m.initial_setup_general_config_error_save_failed()); + onError: (error: AxiosError) => { + const code = error.response?.data?.code; + const fallback = + error.response?.data?.msg ?? m.initial_setup_general_config_error_save_failed(); + if (code) { + Snackbar.error(getApiErrorMessage(code, fallback)); + } else { + Snackbar.error(fallback); + } console.error(error); }, }); diff --git a/web/src/pages/SetupPage/autoAdoption/steps/AutoAdoptionInternalUrlSettingsStep.tsx b/web/src/pages/SetupPage/autoAdoption/steps/AutoAdoptionInternalUrlSettingsStep.tsx index b64c597062..6c89946c10 100644 --- a/web/src/pages/SetupPage/autoAdoption/steps/AutoAdoptionInternalUrlSettingsStep.tsx +++ b/web/src/pages/SetupPage/autoAdoption/steps/AutoAdoptionInternalUrlSettingsStep.tsx @@ -1,7 +1,10 @@ import { useMutation } from '@tanstack/react-query'; +import type { AxiosError } from 'axios'; import z from 'zod'; import { m } from '../../../../paraglide/messages'; import api from '../../../../shared/api/api'; +import { getApiErrorMessage } from '../../../../shared/api/apiErrorMessages'; +import type { ApiError } from '../../../../shared/api/types'; import { Controls } from '../../../../shared/components/Controls/Controls'; import { WizardCard } from '../../../../shared/components/wizard/WizardCard/WizardCard'; import { Button } from '../../../../shared/defguard-ui/components/Button/Button'; @@ -54,8 +57,15 @@ export const AutoAdoptionInternalUrlSettingsStep = () => { }); setActiveStep(AutoAdoptionSetupStep.InternalUrlSslConfig); }, - onError: (error) => { - Snackbar.error(m.initial_setup_general_config_error_save_failed()); + onError: (error: AxiosError) => { + const code = error.response?.data?.code; + const fallback = + error.response?.data?.msg ?? m.initial_setup_general_config_error_save_failed(); + if (code) { + Snackbar.error(getApiErrorMessage(code, fallback)); + } else { + Snackbar.error(fallback); + } console.error(error); }, }); diff --git a/web/src/pages/SetupPage/initial/steps/SetupExternalUrlSettingsStep.tsx b/web/src/pages/SetupPage/initial/steps/SetupExternalUrlSettingsStep.tsx index fe1f8a1d10..b4f43ed520 100644 --- a/web/src/pages/SetupPage/initial/steps/SetupExternalUrlSettingsStep.tsx +++ b/web/src/pages/SetupPage/initial/steps/SetupExternalUrlSettingsStep.tsx @@ -1,7 +1,10 @@ import { useMutation } from '@tanstack/react-query'; +import type { AxiosError } from 'axios'; import z from 'zod'; import { m } from '../../../../paraglide/messages'; import api from '../../../../shared/api/api'; +import { getApiErrorMessage } from '../../../../shared/api/apiErrorMessages'; +import type { ApiError } from '../../../../shared/api/types'; import { Controls } from '../../../../shared/components/Controls/Controls'; import { WizardCard } from '../../../../shared/components/wizard/WizardCard/WizardCard'; import { Button } from '../../../../shared/defguard-ui/components/Button/Button'; @@ -50,8 +53,15 @@ export const SetupExternalUrlSettingsStep = () => { }); setActiveStep(SetupPageStep.ExternalUrlSslConfig); }, - onError: (error) => { - Snackbar.error(m.initial_setup_general_config_error_save_failed()); + onError: (error: AxiosError) => { + const code = error.response?.data?.code; + const fallback = + error.response?.data?.msg ?? m.initial_setup_general_config_error_save_failed(); + if (code) { + Snackbar.error(getApiErrorMessage(code, fallback)); + } else { + Snackbar.error(fallback); + } console.error('Failed to save external URL settings:', error); }, }); diff --git a/web/src/pages/SetupPage/initial/steps/SetupInternalUrlSettingsStep.tsx b/web/src/pages/SetupPage/initial/steps/SetupInternalUrlSettingsStep.tsx index 1cad73f16f..30cce520e1 100644 --- a/web/src/pages/SetupPage/initial/steps/SetupInternalUrlSettingsStep.tsx +++ b/web/src/pages/SetupPage/initial/steps/SetupInternalUrlSettingsStep.tsx @@ -1,7 +1,10 @@ import { useMutation } from '@tanstack/react-query'; +import type { AxiosError } from 'axios'; import z from 'zod'; import { m } from '../../../../paraglide/messages'; import api from '../../../../shared/api/api'; +import { getApiErrorMessage } from '../../../../shared/api/apiErrorMessages'; +import type { ApiError } from '../../../../shared/api/types'; import { Controls } from '../../../../shared/components/Controls/Controls'; import { WizardCard } from '../../../../shared/components/wizard/WizardCard/WizardCard'; import { Button } from '../../../../shared/defguard-ui/components/Button/Button'; @@ -55,8 +58,15 @@ export const SetupInternalUrlSettingsStep = () => { }); setActiveStep(SetupPageStep.InternalUrlSslConfig); }, - onError: (error) => { - Snackbar.error(m.initial_setup_general_config_error_save_failed()); + onError: (error: AxiosError) => { + const code = error.response?.data?.code; + const fallback = + error.response?.data?.msg ?? m.initial_setup_general_config_error_save_failed(); + if (code) { + Snackbar.error(getApiErrorMessage(code, fallback)); + } else { + Snackbar.error(fallback); + } console.error('Failed to save internal URL settings:', error); }, }); diff --git a/web/src/pages/settings/SettingsCertificatesPage/SettingsCoreCertificateWizardPage/steps/SettingsCoreCertificateWizardInternalUrlSettingsStep.tsx b/web/src/pages/settings/SettingsCertificatesPage/SettingsCoreCertificateWizardPage/steps/SettingsCoreCertificateWizardInternalUrlSettingsStep.tsx index 7d94ff3c99..d459a4cdb8 100644 --- a/web/src/pages/settings/SettingsCertificatesPage/SettingsCoreCertificateWizardPage/steps/SettingsCoreCertificateWizardInternalUrlSettingsStep.tsx +++ b/web/src/pages/settings/SettingsCertificatesPage/SettingsCoreCertificateWizardPage/steps/SettingsCoreCertificateWizardInternalUrlSettingsStep.tsx @@ -1,8 +1,10 @@ import { useMutation } from '@tanstack/react-query'; +import type { AxiosError } from 'axios'; import z from 'zod'; import { m } from '../../../../../paraglide/messages'; import api from '../../../../../shared/api/api'; -import type { InternalSslType } from '../../../../../shared/api/types'; +import { getApiErrorMessage } from '../../../../../shared/api/apiErrorMessages'; +import type { ApiError, InternalSslType } from '../../../../../shared/api/types'; import { Controls } from '../../../../../shared/components/Controls/Controls'; import { WizardCard } from '../../../../../shared/components/wizard/WizardCard/WizardCard'; import { Button } from '../../../../../shared/defguard-ui/components/Button/Button'; @@ -15,7 +17,6 @@ import { ThemeSpacing } from '../../../../../shared/defguard-ui/types'; import { useAppForm } from '../../../../../shared/form'; import { formChangeLogic } from '../../../../../shared/formLogic'; import '../../../../SetupPage/autoAdoption/steps/style.scss'; -import { getApiErrorMessage } from '../../utils'; import { SettingsCoreCertificateWizardStep } from '../types'; import { useSettingsCoreCertificateWizardStore } from '../useSettingsCoreCertificateWizardStore'; @@ -43,8 +44,14 @@ export const SettingsCoreCertificateWizardInternalUrlSettingsStep = () => { activeStep: SettingsCoreCertificateWizardStep.InternalUrlSslConfig, }); }, - onError: (error) => { - Snackbar.error(getApiErrorMessage(error) ?? m.settings_msg_save_failed()); + onError: (error: AxiosError) => { + const code = error.response?.data?.code; + const fallback = error.response?.data?.msg ?? m.settings_msg_save_failed(); + if (code) { + Snackbar.error(getApiErrorMessage(code, fallback)); + } else { + Snackbar.error(fallback); + } console.error('Failed to save core internal URL settings:', error); }, }); diff --git a/web/src/pages/settings/SettingsCertificatesPage/SettingsEdgeCertificateWizardPage/steps/SettingsEdgeCertificateWizardExternalUrlSettingsStep.tsx b/web/src/pages/settings/SettingsCertificatesPage/SettingsEdgeCertificateWizardPage/steps/SettingsEdgeCertificateWizardExternalUrlSettingsStep.tsx index 9ae62d3b21..05186fdb47 100644 --- a/web/src/pages/settings/SettingsCertificatesPage/SettingsEdgeCertificateWizardPage/steps/SettingsEdgeCertificateWizardExternalUrlSettingsStep.tsx +++ b/web/src/pages/settings/SettingsCertificatesPage/SettingsEdgeCertificateWizardPage/steps/SettingsEdgeCertificateWizardExternalUrlSettingsStep.tsx @@ -1,8 +1,10 @@ import { useMutation } from '@tanstack/react-query'; +import type { AxiosError } from 'axios'; import z from 'zod'; import { m } from '../../../../../paraglide/messages'; import api from '../../../../../shared/api/api'; -import type { ExternalSslType } from '../../../../../shared/api/types'; +import { getApiErrorMessage } from '../../../../../shared/api/apiErrorMessages'; +import type { ApiError, ExternalSslType } from '../../../../../shared/api/types'; import { Controls } from '../../../../../shared/components/Controls/Controls'; import { WizardCard } from '../../../../../shared/components/wizard/WizardCard/WizardCard'; import { Button } from '../../../../../shared/defguard-ui/components/Button/Button'; @@ -15,7 +17,6 @@ import { ThemeSpacing } from '../../../../../shared/defguard-ui/types'; import { useAppForm } from '../../../../../shared/form'; import { formChangeLogic } from '../../../../../shared/formLogic'; import '../../../../SetupPage/autoAdoption/steps/style.scss'; -import { getApiErrorMessage } from '../../utils'; import { SettingsEdgeCertificateWizardStep } from '../types'; import { useSettingsEdgeCertificateWizardStore } from '../useSettingsEdgeCertificateWizardStore'; @@ -43,8 +44,14 @@ export const SettingsEdgeCertificateWizardExternalUrlSettingsStep = () => { activeStep: SettingsEdgeCertificateWizardStep.ExternalUrlSslConfig, }); }, - onError: (error) => { - Snackbar.error(getApiErrorMessage(error) ?? m.settings_msg_save_failed()); + onError: (error: AxiosError) => { + const code = error.response?.data?.code; + const fallback = error.response?.data?.msg ?? m.settings_msg_save_failed(); + if (code) { + Snackbar.error(getApiErrorMessage(code, fallback)); + } else { + Snackbar.error(fallback); + } console.error('Failed to save edge external URL settings:', error); }, }); diff --git a/web/src/pages/settings/SettingsCertificatesPage/utils.ts b/web/src/pages/settings/SettingsCertificatesPage/utils.ts deleted file mode 100644 index 40b11b6b7f..0000000000 --- a/web/src/pages/settings/SettingsCertificatesPage/utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -import axios from 'axios'; - -export const getApiErrorMessage = (error: unknown): string | null => { - if (axios.isAxiosError(error)) { - const message = error.response?.data?.msg; - return typeof message === 'string' ? message : null; - } - - return null; -}; diff --git a/web/src/shared/api/apiErrorMessages.ts b/web/src/shared/api/apiErrorMessages.ts index 0a4a2dde68..a941d3729c 100644 --- a/web/src/shared/api/apiErrorMessages.ts +++ b/web/src/shared/api/apiErrorMessages.ts @@ -1,7 +1,14 @@ import { m } from '../../paraglide/messages'; import type { ApiErrorMessageKey, WebErrorCode } from './types'; -export function getApiErrorMessage(code: WebErrorCode): string { +export function getApiErrorMessage(code: WebErrorCode, defaultMessage?: string): string { const key: ApiErrorMessageKey = `api_error_${code}`; - return (m as Record string>)[key](); + const messageFn = (m as Partial string>>)[key]; + if (messageFn) { + return messageFn(); + } + if (defaultMessage) { + return defaultMessage; + } + return m.error_unknown(); } diff --git a/web/src/shared/api/types.ts b/web/src/shared/api/types.ts index 70c6242811..6a995feaa1 100644 --- a/web/src/shared/api/types.ts +++ b/web/src/shared/api/types.ts @@ -403,6 +403,13 @@ export interface MfaFinishResponse { export const WebErrorCode = { NetworkFull: 'network_full', UserGroupsNotSynced: 'user_groups_not_synced', + CertMissingCertPem: 'cert_missing_cert_pem', + CertMissingKeyPem: 'cert_missing_key_pem', + CertInvalidCertOrKey: 'cert_invalid_cert_or_key', + CertInvalidValidityPeriod: 'cert_invalid_validity_period', + CertExpired: 'cert_expired', + CertNotYetValid: 'cert_not_yet_valid', + CertParseError: 'cert_parse_error', } as const; export type WebErrorCode = (typeof WebErrorCode)[keyof typeof WebErrorCode];