Skip to content

Commit

Permalink
update done
Browse files Browse the repository at this point in the history
  • Loading branch information
darioAnongba committed Oct 7, 2024
1 parent 3f45dbe commit 44407fc
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 28 deletions.
18 changes: 18 additions & 0 deletions src/application/dtos/ln_address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,21 @@ pub struct RegisterLnAddressRequest {
#[schema(example = "npub1m8pwckdf3...")]
pub nostr_pubkey: Option<PublicKey>,
}

#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateLnAddressRequest {
/// Username such as `username@domain`
pub username: Option<String>,

/// Active status
#[serde(default)]
pub active: Option<bool>,

/// Nostr enabled
#[serde(default)]
pub allows_nostr: Option<bool>,

/// Nostr public key
#[schema(example = "npub1m8pwckdf3...")]
pub nostr_pubkey: Option<PublicKey>,
}
38 changes: 35 additions & 3 deletions src/domains/ln_address/ln_address_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::sync::Arc;

use axum::{
extract::{Path, State},
routing::{delete, get, post},
routing::{delete, get, post, put},
Json, Router,
};
use axum_extra::extract::Query;
Expand All @@ -15,7 +15,7 @@ use crate::{
BAD_REQUEST_EXAMPLE, FORBIDDEN_EXAMPLE, INTERNAL_EXAMPLE, NOT_FOUND_EXAMPLE,
UNAUTHORIZED_EXAMPLE, UNPROCESSABLE_EXAMPLE,
},
dtos::RegisterLnAddressRequest,
dtos::{RegisterLnAddressRequest, UpdateLnAddressRequest},
errors::ApplicationError,
},
domains::user::{Permission, User},
Expand All @@ -26,7 +26,7 @@ use super::{LnAddress, LnAddressFilter};

#[derive(OpenApi)]
#[openapi(
paths(register_address, get_address, list_addresses, delete_address, delete_addresses),
paths(register_address, get_address, list_addresses, update_address, delete_address, delete_addresses),
components(schemas(LnAddress, RegisterLnAddressRequest)),
tags(
(name = "Lightning Addresses", description = "LN Address management endpoints as defined in the [protocol specification](https://lightningaddress.com/). Require `read:ln_address` or `write:ln_address` permissions.")
Expand All @@ -40,6 +40,7 @@ pub fn router() -> Router<Arc<AppState>> {
.route("/", get(list_addresses))
.route("/", post(register_address))
.route("/:id", get(get_address))
.route("/:id", put(update_address))
.route("/:id", delete(delete_address))
.route("/", delete(delete_addresses))
}
Expand Down Expand Up @@ -141,6 +142,37 @@ async fn list_addresses(
Ok(response.into())
}

/// Update a LN Address
///
/// Updates an address. Returns the address details.
#[utoipa::path(
put,
path = "/{id}",
tag = "Lightning Addresses",
context_path = CONTEXT_PATH,
request_body = UpdateLnAddressRequest,
responses(
(status = 200, description = "LN Address Updated", body = LnAddress),
(status = 400, description = "Bad Request", body = ErrorResponse, example = json!(BAD_REQUEST_EXAMPLE)),
(status = 401, description = "Unauthorized", body = ErrorResponse, example = json!(UNAUTHORIZED_EXAMPLE)),
(status = 403, description = "Forbidden", body = ErrorResponse, example = json!(FORBIDDEN_EXAMPLE)),
(status = 404, description = "Not Found", body = ErrorResponse, example = json!(NOT_FOUND_EXAMPLE)),
(status = 422, description = "Unprocessable Entity", body = ErrorResponse, example = json!(UNPROCESSABLE_EXAMPLE)),
(status = 500, description = "Internal Server Error", body = ErrorResponse, example = json!(INTERNAL_EXAMPLE))
)
)]
async fn update_address(
State(app_state): State<Arc<AppState>>,
user: User,
Path(id): Path<Uuid>,
Json(payload): Json<UpdateLnAddressRequest>,
) -> Result<Json<LnAddress>, ApplicationError> {
user.check_permission(Permission::WriteLnAddress)?;

let ln_address = app_state.services.ln_address.update(id, payload).await?;
Ok(ln_address.into())
}

/// Delete a LN Address
///
/// Deletes an address by ID. Returns an empty body
Expand Down
1 change: 1 addition & 0 deletions src/domains/ln_address/ln_address_repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ pub trait LnAddressRepository: Send + Sync {
allows_nostr: bool,
nostr_pubkey: Option<PublicKey>,
) -> Result<LnAddress, DatabaseError>;
async fn update(&self, ln_address: LnAddress) -> Result<LnAddress, DatabaseError>;
async fn delete_many(&self, filter: LnAddressFilter) -> Result<u64, DatabaseError>;
}
84 changes: 73 additions & 11 deletions src/domains/ln_address/ln_address_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use uuid::Uuid;

use crate::{
application::{
dtos::UpdateLnAddressRequest,
entities::AppStore,
errors::{ApplicationError, DataError},
},
Expand Down Expand Up @@ -39,17 +40,7 @@ impl LnAddressUseCases for LnAddressService {
debug!(%wallet_id, username, "Registering lightning address");

username = username.to_lowercase();

if username.len() < MIN_USERNAME_LENGTH || username.len() > MAX_USERNAME_LENGTH {
return Err(DataError::Validation("Invalid username length.".to_string()).into());
}

// Regex validation for allowed characters in username
let email_username_re =
Regex::new(r"^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+$").expect("should not fail as a constant");
if !email_username_re.is_match(&username) {
return Err(DataError::Validation("Invalid username format.".to_string()).into());
}
validate_username(username.as_str())?;

if self
.store
Expand Down Expand Up @@ -109,6 +100,58 @@ impl LnAddressUseCases for LnAddressService {
Ok(ln_addresses)
}

async fn update(
&self,
id: Uuid,
request: UpdateLnAddressRequest,
) -> Result<LnAddress, ApplicationError> {
debug!(%id, ?request, "Updating lightning address");

let mut ln_address = self
.store
.ln_address
.find(id)
.await?
.ok_or_else(|| DataError::NotFound("Lightning address not found.".to_string()))?;

if let Some(mut username) = request.username {
username = username.to_lowercase();

if username != ln_address.username {
validate_username(username.as_str())?;

if self
.store
.ln_address
.find_by_username(&username)
.await?
.is_some()
{
return Err(DataError::Conflict("Duplicate username.".to_string()).into());
}

ln_address.username = username;
}
}

if let Some(active) = request.active {
ln_address.active = active;
}

if let Some(allows_nostr) = request.allows_nostr {
ln_address.allows_nostr = allows_nostr;
}

if let Some(nostr_pubkey) = request.nostr_pubkey {
ln_address.nostr_pubkey = Some(nostr_pubkey);
}

let ln_address = self.store.ln_address.update(ln_address).await?;

info!(%id, "Lightning address updated successfully");
Ok(ln_address)
}

async fn delete(&self, id: Uuid) -> Result<(), ApplicationError> {
debug!(%id, "Deleting lightning address");

Expand Down Expand Up @@ -141,3 +184,22 @@ impl LnAddressUseCases for LnAddressService {
Ok(n_deleted)
}
}

fn validate_username(username: &str) -> Result<(), DataError> {
if username.len() < MIN_USERNAME_LENGTH || username.len() > MAX_USERNAME_LENGTH {
return Err(DataError::Validation(
"Invalid username length.".to_string(),
));
}

// Regex validation for allowed characters in username
let email_username_re =
Regex::new(r"^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+$").expect("should not fail as a constant");
if !email_username_re.is_match(username) {
return Err(DataError::Validation(
"Invalid username format.".to_string(),
));
}

Ok(())
}
7 changes: 6 additions & 1 deletion src/domains/ln_address/ln_address_use_cases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use async_trait::async_trait;
use nostr_sdk::PublicKey;
use uuid::Uuid;

use crate::application::errors::ApplicationError;
use crate::application::{dtos::UpdateLnAddressRequest, errors::ApplicationError};

use super::{LnAddress, LnAddressFilter};

Expand All @@ -18,6 +18,11 @@ pub trait LnAddressUseCases: Send + Sync {
) -> Result<LnAddress, ApplicationError>;
async fn get(&self, id: Uuid) -> Result<LnAddress, ApplicationError>;
async fn list(&self, filter: LnAddressFilter) -> Result<Vec<LnAddress>, ApplicationError>;
async fn update(
&self,
id: Uuid,
request: UpdateLnAddressRequest,
) -> Result<LnAddress, ApplicationError>;
async fn delete(&self, id: Uuid) -> Result<(), ApplicationError>;
async fn delete_many(&self, filter: LnAddressFilter) -> Result<u64, ApplicationError>;
}
2 changes: 1 addition & 1 deletion src/domains/user/auth_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ impl AuthUseCases for AuthService {
let wallet_opt = self.store.wallet.find_by_user_id(&claims.sub).await?;

let wallet = match wallet_opt {
Some(user) => user,
Some(wallet) => wallet,
None => {
let wallet = self.store.wallet.insert(&claims.sub).await?;

Expand Down
90 changes: 87 additions & 3 deletions src/domains/wallet/user_wallet_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::sync::Arc;

use axum::{
extract::{Path, Query, State},
routing::{delete, get, post},
routing::{delete, get, post, put},
Json, Router,
};
use utoipa::OpenApi;
Expand All @@ -16,7 +16,7 @@ use crate::{
},
dtos::{
InvoiceResponse, NewInvoiceRequest, PaymentResponse, RegisterLnAddressRequest,
SendPaymentRequest, WalletResponse,
SendPaymentRequest, UpdateLnAddressRequest, WalletResponse,
},
errors::{ApplicationError, DataError},
},
Expand All @@ -33,7 +33,7 @@ use super::{Balance, Contact};

#[derive(OpenApi)]
#[openapi(
paths(get_user_wallet, get_wallet_balance, get_wallet_address, register_wallet_address, wallet_pay, list_wallet_payments,
paths(get_user_wallet, get_wallet_balance, get_wallet_address, register_wallet_address, update_wallet_address, delete_wallet_address, wallet_pay, list_wallet_payments,
get_wallet_payment, delete_failed_payments, list_wallet_invoices, get_wallet_invoice, new_wallet_invoice, delete_expired_invoices,
list_contacts
),
Expand All @@ -51,6 +51,8 @@ pub fn user_router() -> Router<Arc<AppState>> {
.route("/balance", get(get_wallet_balance))
.route("/lightning-address", get(get_wallet_address))
.route("/lightning-address", post(register_wallet_address))
.route("/lightning-address", put(update_wallet_address))
.route("/lightning-address", delete(delete_wallet_address))
.route("/payments", post(wallet_pay))
.route("/payments", get(list_wallet_payments))
.route("/payments/:id", get(get_wallet_payment))
Expand Down Expand Up @@ -254,6 +256,88 @@ async fn register_wallet_address(
Ok(ln_address.into())
}

/// Updates the user's LN Address
///
/// Updates the address. Returns the address details.
#[utoipa::path(
put,
path = "/lightning-address",
tag = "User Wallet",
context_path = CONTEXT_PATH,
request_body = UpdateLnAddressRequest,
responses(
(status = 200, description = "LN Address Updated", body = LnAddress),
(status = 400, description = "Bad Request", body = ErrorResponse, example = json!(BAD_REQUEST_EXAMPLE)),
(status = 401, description = "Unauthorized", body = ErrorResponse, example = json!(UNAUTHORIZED_EXAMPLE)),
(status = 422, description = "Unprocessable Entity", body = ErrorResponse, example = json!(UNPROCESSABLE_EXAMPLE)),
(status = 404, description = "Not Found", body = ErrorResponse, example = json!(NOT_FOUND_EXAMPLE)),
(status = 500, description = "Internal Server Error", body = ErrorResponse, example = json!(INTERNAL_EXAMPLE))
)
)]
async fn update_wallet_address(
State(app_state): State<Arc<AppState>>,
user: User,
Json(payload): Json<UpdateLnAddressRequest>,
) -> Result<Json<LnAddress>, ApplicationError> {
let ln_addresses = app_state
.services
.ln_address
.list(LnAddressFilter {
wallet_id: Some(user.wallet_id),
..Default::default()
})
.await?;

let ln_address = ln_addresses
.first()
.cloned()
.ok_or_else(|| DataError::NotFound("LN Address not found.".to_string()))?;

let ln_address = app_state
.services
.ln_address
.update(ln_address.id, payload)
.await?;

Ok(ln_address.into())
}

/// Deletes the user's LN Address
///
/// Deletes an address. Returns an empty body. Once the address is deleted, it will no longer be able to receive funds and its username can be claimed by another user.
#[utoipa::path(
delete,
path = "/lightning-address",
tag = "User Wallet",
context_path = CONTEXT_PATH,
responses(
(status = 200, description = "Deleted"),
(status = 400, description = "Bad Request", body = ErrorResponse, example = json!(BAD_REQUEST_EXAMPLE)),
(status = 401, description = "Unauthorized", body = ErrorResponse, example = json!(UNAUTHORIZED_EXAMPLE)),
(status = 404, description = "Not Found", body = ErrorResponse, example = json!(NOT_FOUND_EXAMPLE)),
(status = 500, description = "Internal Server Error", body = ErrorResponse, example = json!(INTERNAL_EXAMPLE))
)
)]
async fn delete_wallet_address(
State(app_state): State<Arc<AppState>>,
user: User,
) -> Result<(), ApplicationError> {
let n_deleted = app_state
.services
.ln_address
.delete_many(LnAddressFilter {
wallet_id: Some(user.wallet_id),
..Default::default()
})
.await?;

if n_deleted == 0 {
return Err(DataError::NotFound("Lightning address not found.".to_string()).into());
}

Ok(())
}

/// List payments
///
/// Returns all the payments given a filter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ use crate::{
};
use async_trait::async_trait;
use sea_orm::{
sea_query::Expr, ActiveModelTrait, ActiveValue::Set, ColumnTrait, Condition,
DatabaseConnection, DatabaseTransaction, EntityTrait, QueryFilter, QueryOrder, QuerySelect,
QueryTrait,
sea_query::Expr, ActiveModelTrait, ColumnTrait, Condition, DatabaseConnection,
DatabaseTransaction, EntityTrait, QueryFilter, QueryOrder, QuerySelect, QueryTrait, Set,
Unchanged,
};
use uuid::Uuid;

Expand Down Expand Up @@ -134,7 +134,7 @@ impl InvoiceRepository for SeaOrmInvoiceRepository {
invoice: Invoice,
) -> Result<Invoice, DatabaseError> {
let model = ActiveModel {
id: Set(invoice.id),
id: Unchanged(invoice.id),
fee_msat: Set(invoice.fee_msat.map(|v| v as i64)),
payment_time: Set(invoice.payment_time),
description: Set(invoice.description),
Expand Down
Loading

0 comments on commit 44407fc

Please sign in to comment.