Skip to content
Permalink
Browse files

feat: HTTP endpoint for account deletion (#205)

  • Loading branch information...
bstrie committed Aug 13, 2019
1 parent de6c5f1 commit 6c732fd16ee90f1d29c3f56ee06084398d1674fe
@@ -28,6 +28,11 @@ pub trait NodeStore: Clone + Send + Sync + 'static {
account: AccountDetails,
) -> Box<dyn Future<Item = Self::Account, Error = ()> + Send>;

fn remove_account(
&self,
id: <Self::Account as AccountTrait>::AccountId,
) -> Box<dyn Future<Item = Self::Account, Error = ()> + Send>;

// TODO limit the number of results and page through them
fn get_all_accounts(&self) -> Box<dyn Future<Item = Vec<Self::Account>, Error = ()> + Send>;

@@ -39,6 +39,17 @@ pub struct AccountsApi<T> {

const MAX_RETRIES: usize = 10;

// Convenience function to clean up error handling and reduce unwrap quantity
trait ErrorStatus {
fn error(code: u16) -> Self;
}

impl ErrorStatus for Response<()> {
fn error(code: u16) -> Self {
Response::builder().status(code).body(()).unwrap()
}
}

impl_web! {
impl<T, A> AccountsApi<T>
where T: NodeStore<Account = A> + HttpStore<Account = A> + BalanceStore<Account = A>,
@@ -61,7 +72,7 @@ impl_web! {
ok(self.store.clone())
} else {
error!("Admin API endpoint called with non-admin API key");
err(Response::builder().status(401).body(()).unwrap())
err(Response::error(401))
}
}

@@ -73,14 +84,14 @@ impl_web! {
let se_url = body.settlement_engine_url.clone();
self.validate_admin(authorization)
.and_then(move |store| store.insert_account(body)
.map_err(|_| Response::builder().status(500).body(()).unwrap())
.map_err(|_| Response::error(500))
.and_then(|account| {
// if the account had a SE associated with it, then register
// the account in the SE.
if let Some(se_url) = se_url {
let id = account.id();
Either::A(result(Url::parse(&se_url))
.map_err(|_| Response::builder().status(500).body(()).unwrap())
.map_err(|_| Response::error(500))
.and_then(move |mut se_url| {
se_url
.path_segments_mut()
@@ -109,7 +120,7 @@ impl_web! {
})
};
Retry::spawn(FixedInterval::from_millis(2000).take(MAX_RETRIES), action)
.map_err(|_| Response::builder().status(500).body(()).unwrap())
.map_err(|_| Response::error(500))
.and_then(move |_| {
Ok(json!(account))
})
@@ -126,12 +137,12 @@ impl_web! {
let store = self.store.clone();
if self.is_admin(&authorization) {
Either::A(store.get_all_accounts()
.map_err(|_| Response::builder().status(500).body(()).unwrap())
.map_err(|_| Response::error(500))
.and_then(|accounts| Ok(json!(accounts))))
} else {
// Only allow the user to see their own account
Either::B(store.get_account_from_http_token(authorization.as_str())
.map_err(|_| Response::builder().status(404).body(()).unwrap())
.map_err(|_| Response::error(404))
.and_then(|account| Ok(json!(vec![account]))))
}
}
@@ -143,32 +154,51 @@ impl_web! {
let is_admin = self.is_admin(&authorization);
let parsed_id: Result<A::AccountId, ()> = A::AccountId::from_str(&id).map_err(|_| error!("Invalid id"));
result(parsed_id)
.map_err(|_| Response::builder().status(400).body(()).unwrap())
.map_err(|_| Response::error(400))
.and_then(move |id| {
if is_admin {
Either::A(store.get_accounts(vec![id])
.map_err(move |_| {
debug!("Account not found: {}", id);
Response::builder().status(404).body(()).unwrap()
Response::error(404)
})
.and_then(|mut accounts| Ok(json!(accounts.pop().unwrap()))))
} else {
Either::B(store.get_account_from_http_token(&authorization[BEARER_TOKEN_START..])
.map_err(move |_| {
debug!("No account found with auth: {}", authorization);
Response::builder().status(401).body(()).unwrap()
Response::error(401)
})
.and_then(move |account| {
if account.id() == id {
Ok(json!(account))
} else {
Err(Response::builder().status(401).body(()).unwrap())
Err(Response::error(401))
}
}))
}
})
}

#[delete("/accounts/:id")]
#[content_type("application/json")]
fn delete_account(&self, id: String, authorization: String) -> impl Future<Item = Value, Error = Response<()>> {
let parsed_id: Result<A::AccountId, ()> = A::AccountId::from_str(&id).map_err(|_| error!("Invalid id"));
self.validate_admin(authorization)
.and_then(move |store| match parsed_id {
Ok(id) => Ok((store, id)),
Err(_) => Err(Response::error(400)),
})
.and_then(move |(store, id)|
store.remove_account(id)
.map_err(|_| Response::error(500))
.and_then(move |account| {
// TODO: deregister from SE if url is present
Ok(json!(account))
})
)
}

// TODO should this be combined into the account record?
#[get("/accounts/:id/balance")]
#[content_type("application/json")]
@@ -178,32 +208,32 @@ impl_web! {
let is_admin = self.is_admin(&authorization);
let parsed_id: Result<A::AccountId, ()> = A::AccountId::from_str(&id).map_err(|_| error!("Invalid id"));
result(parsed_id)
.map_err(|_| Response::builder().status(400).body(()).unwrap())
.map_err(|_| Response::error(400))
.and_then(move |id| {
if is_admin {
Either::A(store.get_accounts(vec![id])
.map_err(move |_| {
debug!("Account not found: {}", id);
Response::builder().status(404).body(()).unwrap()
Response::error(404)
})
.and_then(|mut accounts| Ok(accounts.pop().unwrap())))
} else {
Either::B(store.get_account_from_http_token(&authorization[BEARER_TOKEN_START..])
.map_err(move |_| {
debug!("No account found with auth: {}", authorization);
Response::builder().status(401).body(()).unwrap()
Response::error(401)
})
.and_then(move |account| {
if account.id() == id {
Ok(account)
} else {
Err(Response::builder().status(401).body(()).unwrap())
Err(Response::error(401))
}
}))
}
})
.and_then(move |account| store_clone.get_balance(account)
.map_err(|_| Response::builder().status(500).body(()).unwrap()))
.map_err(|_| Response::error(500)))
.and_then(|balance| Ok(BalanceResponse {
balance: balance.to_string(),
}))
@@ -1,3 +1,22 @@
// The informal schema of our data in redis:
// send_routes_to set used for CCP routing
// receive_routes_from set used for CCP routing
// next_account_id string unique ID for each new account
// rates:current hash exchange rates
// routes:current hash dynamic routing table
// routes:static hash static routing table
// accounts:<id> hash details for each account
// http_auth hash maps hmac of cryptographic credentials to an account
// btp_auth hash maps hmac of cryptographic credentials to an account
// btp_outgoing
// For interactive exploration of the store,
// use the redis-cli tool included with your redis install.
// Within redis-cli:
// keys * list all keys of any type in the store
// smembers <key> list the members of a set
// get <key> get the value of a key
// hgetall <key> the flattened list of every key/value entry within a hash

use super::account::*;
use super::crypto::generate_keys;
use bytes::Bytes;
@@ -376,15 +395,52 @@ impl RedisStore {
}),
)
}
}

impl AccountStore for RedisStore {
type Account = Account;
fn delete_account(&self, id: u64) -> Box<dyn Future<Item = Account, Error = ()> + Send> {
let connection = self.connection.as_ref().clone();
let routing_table = self.routes.clone();

// TODO cache results to avoid hitting Redis for each packet
fn get_accounts(
Box::new(
// TODO: a retrieve_account API to avoid making Vecs which we only need one element of
self.retrieve_accounts(vec![id])
.and_then(|accounts| accounts.get(0).cloned().ok_or(()))
.and_then(|account| {
let mut pipe = redis::pipe();
pipe.atomic();

pipe.del(account_details_key(account.id));

if account.send_routes {
pipe.srem("send_routes_to", account.id).ignore();
}

if account.receive_routes {
pipe.srem("receive_routes_from", account.id).ignore();
}

if account.btp_uri.is_some() {
pipe.srem("btp_outgoing", account.id).ignore();
}

pipe.hdel(ROUTES_KEY, account.ilp_address.to_bytes().to_vec())
.ignore();

pipe.query_async(connection)
.map_err(|err| error!("Error deleting account from DB: {:?}", err))
.and_then(move |(connection, _ret): (SharedConnection, Value)| {
update_routes(connection, routing_table)
})
.and_then(move |_| {
debug!("Deleted account {}", account.id);
Ok(account)
})
}),
)
}

fn retrieve_accounts(
&self,
account_ids: Vec<<Self::Account as AccountTrait>::AccountId>,
account_ids: Vec<u64>,
) -> Box<dyn Future<Item = Vec<Account>, Error = ()> + Send> {
let decryption_key = self.decryption_key.clone();
let num_accounts = account_ids.len();
@@ -417,6 +473,18 @@ impl AccountStore for RedisStore {
}
}

impl AccountStore for RedisStore {
type Account = Account;

// TODO cache results to avoid hitting Redis for each packet
fn get_accounts(
&self,
account_ids: Vec<<Self::Account as AccountTrait>::AccountId>,
) -> Box<dyn Future<Item = Vec<Account>, Error = ()> + Send> {
self.retrieve_accounts(account_ids)
}
}

impl BalanceStore for RedisStore {
/// Returns the balance **from the account holder's perspective**, meaning the sum of
/// the Payable Balance and Pending Outgoing minus the Receivable Balance and the Pending Incoming.
@@ -691,6 +759,10 @@ impl NodeStore for RedisStore {
self.create_new_account(account)
}

fn remove_account(&self, id: u64) -> Box<dyn Future<Item = Account, Error = ()> + Send> {
self.delete_account(id)
}

// TODO limit the number of results and page through them
fn get_all_accounts(&self) -> Box<dyn Future<Item = Vec<Self::Account>, Error = ()> + Send> {
let decryption_key = self.decryption_key.clone();
@@ -26,6 +26,25 @@ fn insert_accounts() {
.unwrap();
}

#[test]
fn delete_accounts() {
block_on(test_store().and_then(|(store, context)| {
store.get_all_accounts().and_then(move |accounts| {
let id = accounts[0].id();
store.remove_account(id).and_then(move |_| {
store.get_all_accounts().and_then(move |accounts| {
for a in accounts {
assert_ne!(id, a.id());
}
let _ = context;
Ok(())
})
})
})
}))
.unwrap();
}

#[test]
fn starts_with_zero_balance() {
block_on(test_store().and_then(|(store, context)| {

0 comments on commit 6c732fd

Please sign in to comment.
You can’t perform that action at this time.