Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 30 additions & 30 deletions lnvps_api/src/api/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,46 +66,46 @@ pub struct VMPatchRequest {

#[derive(Serialize, Deserialize)]
pub struct AccountPatchRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "lnvps_api_common::deserialize_nullable_option")]
pub email: Option<Option<String>>,
pub contact_nip17: bool,
pub contact_email: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub country_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub address_1: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub address_2: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub city: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub postcode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tax_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "lnvps_api_common::deserialize_nullable_option")]
pub country_code: Option<Option<String>>,
#[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "lnvps_api_common::deserialize_nullable_option")]
pub name: Option<Option<String>>,
#[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "lnvps_api_common::deserialize_nullable_option")]
pub address_1: Option<Option<String>>,
#[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "lnvps_api_common::deserialize_nullable_option")]
pub address_2: Option<Option<String>>,
#[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "lnvps_api_common::deserialize_nullable_option")]
pub state: Option<Option<String>>,
#[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "lnvps_api_common::deserialize_nullable_option")]
pub city: Option<Option<String>>,
#[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "lnvps_api_common::deserialize_nullable_option")]
pub postcode: Option<Option<String>>,
#[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "lnvps_api_common::deserialize_nullable_option")]
pub tax_id: Option<Option<String>>,
/// Nostr Wallet Connect connection string for automatic VM renewals
#[serde(skip_serializing_if = "Option::is_none")]
pub nwc_connection_string: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "lnvps_api_common::deserialize_nullable_option")]
pub nwc_connection_string: Option<Option<String>>,
}

impl From<lnvps_db::User> for AccountPatchRequest {
fn from(user: lnvps_db::User) -> Self {
AccountPatchRequest {
email: user.email.map(|e| e.into()),
email: Some(user.email.map(|e| e.into())),
contact_nip17: user.contact_nip17,
contact_email: user.contact_email,
country_code: user.country_code,
name: user.billing_name,
address_1: user.billing_address_1,
address_2: user.billing_address_2,
state: user.billing_state,
city: user.billing_city,
postcode: user.billing_postcode,
tax_id: user.billing_tax_id,
nwc_connection_string: user.nwc_connection_string.map(|nwc| nwc.into()),
country_code: Some(user.country_code),
name: Some(user.billing_name),
address_1: Some(user.billing_address_1),
address_2: Some(user.billing_address_2),
state: Some(user.billing_state),
city: Some(user.billing_city),
postcode: Some(user.billing_postcode),
tax_id: Some(user.billing_tax_id),
nwc_connection_string: Some(user.nwc_connection_string.map(|nwc| nwc.into())),
}
}
}
Expand Down
50 changes: 35 additions & 15 deletions lnvps_api/src/api/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ async fn v1_patch_account(

// validate nwc string
#[cfg(feature = "nostr-nwc")]
if let Some(nwc) = &req.nwc_connection_string {
if let Some(Some(nwc)) = &req.nwc_connection_string {
match nwc::prelude::NostrWalletConnectURI::parse(nwc) {
Ok(s) => {
// test connection
Expand All @@ -115,22 +115,42 @@ async fn v1_patch_account(
}
}

user.email = req.email.clone().map(|s| s.into());
// Update fields only if they are present in the request
if let Some(email) = &req.email {
user.email = email.clone().map(|s| s.into());
}
user.contact_nip17 = req.contact_nip17;
user.contact_email = req.contact_email;
user.country_code = req
.country_code
.as_ref()
.and_then(|c| CountryCode::for_alpha3(c).ok())
.map(|c| c.alpha3().to_string());
user.billing_name = req.name.clone();
user.billing_address_1 = req.address_1.clone();
user.billing_address_2 = req.address_2.clone();
user.billing_city = req.city.clone();
user.billing_state = req.state.clone();
user.billing_postcode = req.postcode.clone();
user.billing_tax_id = req.tax_id.clone();
user.nwc_connection_string = req.nwc_connection_string.clone().map(|s| s.into());
if let Some(country_code) = &req.country_code {
user.country_code = country_code
.as_ref()
.and_then(|c| CountryCode::for_alpha3(c).ok())
.map(|c| c.alpha3().to_string());
}
if let Some(name) = &req.name {
user.billing_name = name.clone();
}
if let Some(address_1) = &req.address_1 {
user.billing_address_1 = address_1.clone();
}
if let Some(address_2) = &req.address_2 {
user.billing_address_2 = address_2.clone();
}
if let Some(city) = &req.city {
user.billing_city = city.clone();
}
if let Some(state) = &req.state {
user.billing_state = state.clone();
}
if let Some(postcode) = &req.postcode {
user.billing_postcode = postcode.clone();
}
if let Some(tax_id) = &req.tax_id {
user.billing_tax_id = tax_id.clone();
}
if let Some(nwc_connection_string) = &req.nwc_connection_string {
user.nwc_connection_string = nwc_connection_string.clone().map(|s| s.into());
}

this.db.update_user(&user).await?;
ApiData::ok(())
Expand Down
15 changes: 15 additions & 0 deletions lnvps_api_common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,21 @@ where
}
}

/// Custom deserializer that distinguishes between missing field and explicit null
/// Used for PATCH endpoints to allow clearing optional fields
///
/// Returns:
/// - `None` when field is not present in JSON (due to `#[serde(default)]`)
/// - `Some(None)` when field is present with `null` value
/// - `Some(Some(value))` when field is present with a value
pub fn deserialize_nullable_option<'de, D, T>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
where
D: serde::Deserializer<'de>,
T: serde::Deserialize<'de>,
{
Ok(Some(Option::deserialize(deserializer)?))
}


// Custom deserializer to handle Proxmox's integer-to-boolean conversion for KVM field
pub fn deserialize_int_to_bool<'de, D>(deserializer: D) -> anyhow::Result<Option<bool>, D::Error>
Expand Down