Skip to content

Commit 71f09cd

Browse files
authored
Email Notifications Register API (#618)
1 parent 5ca9827 commit 71f09cd

File tree

23 files changed

+519
-23
lines changed

23 files changed

+519
-23
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# 0.4.5
22

33
* Add handling for `RemoveSignatory` from company, which flags the company as not active
4-
* Add email notifications API
4+
* Email Notifications
5+
* Add email notifications API
6+
* Add email notifications registration API
57
* Added `app_url` property to config - defaults to `https://bitcredit-dev.minibill.tech` (config break)
68

79
# 0.4.4

crates/bcr-ebill-api/src/external/email.rs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,16 @@ use crate::{external::file_storage::to_url, get_config};
4343
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
4444
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
4545
pub trait EmailClientApi: ServiceTraitBounds {
46-
/// Start register flow, receiving a challenge string
47-
async fn start(&self, relay_url: &str, npub: &str) -> Result<StartEmailRegisterResponse>;
48-
/// Register for email notifications
46+
/// Start register flow, returning a challenge string
47+
async fn start(&self, relay_url: &str, node_id: &NodeId) -> Result<String>;
48+
/// Register for email notifications, returning an email preferences link
4949
async fn register(
5050
&self,
5151
relay_url: &str,
5252
email: &str,
5353
private_key: &nostr::SecretKey,
5454
challenge: &str,
55-
) -> Result<RegisterEmailNotificationResponse>;
55+
) -> Result<url::Url>;
5656
/// Send a bill notification email
5757
async fn send_bill_notification(
5858
&self,
@@ -69,6 +69,14 @@ pub struct EmailClient {
6969
cl: reqwest::Client,
7070
}
7171

72+
impl EmailClient {
73+
pub fn new() -> Self {
74+
Self {
75+
cl: reqwest::Client::new(),
76+
}
77+
}
78+
}
79+
7280
impl ServiceTraitBounds for EmailClient {}
7381

7482
#[cfg(test)]
@@ -77,10 +85,9 @@ impl ServiceTraitBounds for MockEmailClientApi {}
7785
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
7886
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
7987
impl EmailClientApi for EmailClient {
80-
async fn start(&self, relay_url: &str, npub: &str) -> Result<StartEmailRegisterResponse> {
81-
let req = StartEmailRegisterRequest {
82-
npub: npub.to_owned(),
83-
};
88+
async fn start(&self, relay_url: &str, node_id: &NodeId) -> Result<String> {
89+
let npub = node_id.npub().to_bech32().map_err(|_| Error::NostrKey)?;
90+
let req = StartEmailRegisterRequest { npub };
8491

8592
let resp: StartEmailRegisterResponse = self
8693
.cl
@@ -91,7 +98,7 @@ impl EmailClientApi for EmailClient {
9198
.json()
9299
.await?;
93100

94-
Ok(resp)
101+
Ok(resp.challenge)
95102
}
96103

97104
async fn register(
@@ -100,7 +107,7 @@ impl EmailClientApi for EmailClient {
100107
email: &str,
101108
private_key: &nostr::SecretKey,
102109
challenge: &str,
103-
) -> Result<RegisterEmailNotificationResponse> {
110+
) -> Result<url::Url> {
104111
let key_pair = Keypair::from_secret_key(SECP256K1, private_key);
105112
let msg = Message::from_digest_slice(&hex::decode(challenge).map_err(Error::Hex)?)
106113
.map_err(Error::Signature)?;
@@ -127,7 +134,10 @@ impl EmailClientApi for EmailClient {
127134
.json()
128135
.await?;
129136

130-
Ok(resp)
137+
to_url(
138+
relay_url,
139+
&format!("notifications/preferences/{}", resp.preferences_token),
140+
)
131141
}
132142

133143
async fn send_bill_notification(

crates/bcr-ebill-api/src/persistence/mod.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ use bcr_ebill_persistence::{
1010
bill::{BillChainStoreApi, BillStoreApi},
1111
company::{CompanyChainStoreApi, CompanyStoreApi},
1212
db::{
13-
mint::SurrealMintStore, nostr_contact_store::SurrealNostrContactStore,
13+
email_notification::SurrealEmailNotificationStore, mint::SurrealMintStore,
14+
nostr_contact_store::SurrealNostrContactStore,
1415
nostr_send_queue::SurrealNostrEventQueueStore, surreal::SurrealWrapper,
1516
},
1617
file_upload::FileUploadStoreApi,
1718
identity::{IdentityChainStoreApi, IdentityStoreApi},
1819
mint::MintStoreApi,
1920
nostr::{NostrContactStoreApi, NostrQueuedMessageStoreApi},
21+
notification::EmailNotificationStoreApi,
2022
};
2123
use log::error;
2224
use std::sync::Arc;
@@ -45,6 +47,7 @@ pub struct DbContext {
4547
pub file_upload_store: Arc<dyn FileUploadStoreApi>,
4648
pub nostr_event_offset_store: Arc<dyn NostrEventOffsetStoreApi>,
4749
pub notification_store: Arc<dyn NotificationStoreApi>,
50+
pub email_notification_store: Arc<dyn EmailNotificationStoreApi>,
4851
pub backup_store: Arc<dyn BackupStoreApi>,
4952
pub queued_message_store: Arc<dyn NostrQueuedMessageStoreApi>,
5053
pub nostr_contact_store: Arc<dyn NostrContactStoreApi>,
@@ -101,6 +104,8 @@ pub async fn get_db_context(
101104
let nostr_event_offset_store =
102105
Arc::new(SurrealNostrEventOffsetStore::new(surreal_wrapper.clone()));
103106
let notification_store = Arc::new(SurrealNotificationStore::new(surreal_wrapper.clone()));
107+
let email_notification_store =
108+
Arc::new(SurrealEmailNotificationStore::new(surreal_wrapper.clone()));
104109

105110
#[cfg(target_arch = "wasm32")]
106111
let backup_store = Arc::new(SurrealBackupStore {});
@@ -124,6 +129,7 @@ pub async fn get_db_context(
124129
file_upload_store,
125130
nostr_event_offset_store,
126131
notification_store,
132+
email_notification_store,
127133
backup_store,
128134
queued_message_store,
129135
nostr_contact_store,

crates/bcr-ebill-api/src/service/notification_service/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::util;
1+
use crate::{external, util};
22
use bcr_ebill_core::util::crypto;
33

44
pub mod chain_keys;
@@ -49,6 +49,13 @@ pub enum Error {
4949
/// errors that stem from validation in core
5050
#[error("Validation Error: {0}")]
5151
Validation(#[from] bcr_ebill_core::ValidationError),
52+
53+
#[error("External API error: {0}")]
54+
ExternalApi(#[from] external::Error),
55+
56+
/// errors if something couldn't be found
57+
#[error("not found")]
58+
NotFound,
5259
}
5360

5461
impl From<serde_json::Error> for Error {

crates/bcr-ebill-api/src/service/notification_service/service.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,16 @@ pub trait NotificationServiceApi: ServiceTraitBounds {
184184

185185
/// Attempts to resolve the nostr contact for the given Node Id
186186
async fn resolve_contact(&self, node_id: &NodeId) -> Result<Option<NostrContactData>>;
187+
188+
/// Register email notifications for the currently selected identity
189+
async fn register_email_notifications(
190+
&self,
191+
relay_url: &str,
192+
email: &str,
193+
node_id: &NodeId,
194+
caller_keys: &BcrKeys,
195+
) -> Result<()>;
196+
197+
/// Fetch email notifications preferences link for the currently selected identity
198+
async fn get_email_notifications_preferences_link(&self, node_id: &NodeId) -> Result<url::Url>;
187199
}

crates/bcr-ebill-api/src/tests/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub mod tests {
2121
notification::{ActionType, Notification, NotificationType},
2222
util::crypto::BcrKeys,
2323
};
24+
use bcr_ebill_persistence::notification::EmailNotificationStoreApi;
2425
use bcr_ebill_persistence::{
2526
BackupStoreApi, ContactStoreApi, NostrEventOffset, NostrEventOffsetStoreApi,
2627
NotificationStoreApi, Result, SurrealDbConfig,
@@ -370,6 +371,22 @@ pub mod tests {
370371
}
371372
}
372373

374+
mockall::mock! {
375+
pub EmailNotificationStoreApiMock {}
376+
377+
impl ServiceTraitBounds for EmailNotificationStoreApiMock {}
378+
379+
#[async_trait]
380+
impl EmailNotificationStoreApi for EmailNotificationStoreApiMock {
381+
async fn add_email_preferences_link_for_node_id(
382+
&self,
383+
email_preferences_link: &url::Url,
384+
node_id: &NodeId,
385+
) -> Result<()>;
386+
async fn get_email_preferences_link_for_node_id(&self, node_id: &NodeId) -> Result<Option<url::Url>>;
387+
}
388+
}
389+
373390
mockall::mock! {
374391
pub FileUploadStoreApiMock {}
375392

@@ -402,6 +419,7 @@ pub mod tests {
402419
file_upload_store: Arc::new(MockFileUploadStoreApiMock::new()),
403420
nostr_event_offset_store: Arc::new(MockNostrEventOffsetStoreApiMock::new()),
404421
notification_store: Arc::new(MockNotificationStoreApiMock::new()),
422+
email_notification_store: Arc::new(MockEmailNotificationStoreApiMock::new()),
405423
backup_store: Arc::new(MockBackupStoreApiMock::new()),
406424
queued_message_store: Arc::new(MockNostrQueuedMessageStore::new()),
407425
nostr_contact_store: Arc::new(nostr_contact_store.unwrap_or_default()),

crates/bcr-ebill-api/src/util/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub mod file;
22
pub mod numbers_to_words;
33

4+
pub use bcr_ebill_core::Field;
45
pub use bcr_ebill_core::ValidationError;
56
pub use bcr_ebill_core::constants::VALID_CURRENCIES;
67
pub use bcr_ebill_core::util::crypto;

crates/bcr-ebill-core/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,10 @@ pub enum ValidationError {
621621
/// errors that stem from interacting with a blockchain
622622
#[error("Blockchain error: {0}")]
623623
Blockchain(String),
624+
625+
/// error returned if the relay url was invalid
626+
#[error("invalid relay url")]
627+
InvalidRelayUrl,
624628
}
625629

626630
impl From<crate::blockchain::Error> for ValidationError {

crates/bcr-ebill-persistence/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ bcr-ebill-core.workspace = true
2222
tokio.workspace = true
2323
tokio_with_wasm.workspace = true
2424
bitcoin.workspace = true
25+
url.workspace = true
2526
arc-swap = "1.7"
2627

2728
# Enable "kv-indxdb" only for WebAssembly (wasm32)
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use super::super::Result;
2+
use async_trait::async_trait;
3+
use bcr_ebill_core::{NodeId, ServiceTraitBounds};
4+
use serde::{Deserialize, Serialize};
5+
6+
use crate::{db::surreal::SurrealWrapper, notification::EmailNotificationStoreApi};
7+
8+
#[derive(Clone)]
9+
pub struct SurrealEmailNotificationStore {
10+
db: SurrealWrapper,
11+
}
12+
13+
impl SurrealEmailNotificationStore {
14+
const TABLE: &'static str = "email_notifications";
15+
16+
pub fn new(db: SurrealWrapper) -> Self {
17+
Self { db }
18+
}
19+
}
20+
21+
impl ServiceTraitBounds for SurrealEmailNotificationStore {}
22+
23+
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
24+
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
25+
impl EmailNotificationStoreApi for SurrealEmailNotificationStore {
26+
async fn add_email_preferences_link_for_node_id(
27+
&self,
28+
email_preferences_link: &url::Url,
29+
node_id: &NodeId,
30+
) -> Result<()> {
31+
let db = EmailNotificationPreferencesDb {
32+
node_id: node_id.to_owned(),
33+
email_preferences_link: email_preferences_link.to_string(),
34+
};
35+
let _: Option<EmailNotificationPreferencesDb> = self
36+
.db
37+
.create(Self::TABLE, Some(node_id.to_string()), db)
38+
.await?;
39+
Ok(())
40+
}
41+
42+
async fn get_email_preferences_link_for_node_id(
43+
&self,
44+
node_id: &NodeId,
45+
) -> Result<Option<url::Url>> {
46+
let result: Option<EmailNotificationPreferencesDb> =
47+
self.db.select_one(Self::TABLE, node_id.to_string()).await?;
48+
49+
Ok(match result {
50+
Some(r) => url::Url::parse(&r.email_preferences_link).ok(),
51+
None => None,
52+
})
53+
}
54+
}
55+
56+
#[derive(Debug, Clone, Serialize, Deserialize)]
57+
struct EmailNotificationPreferencesDb {
58+
pub node_id: NodeId,
59+
pub email_preferences_link: String,
60+
}
61+
62+
#[cfg(test)]
63+
mod tests {
64+
use crate::{db::get_memory_db, tests::tests::node_id_test};
65+
66+
use super::*;
67+
68+
async fn get_store() -> SurrealEmailNotificationStore {
69+
let db = get_memory_db("test", "email_notification")
70+
.await
71+
.expect("could not create memory db");
72+
SurrealEmailNotificationStore::new(SurrealWrapper { db, files: false })
73+
}
74+
75+
#[tokio::test]
76+
async fn test_email_preferences_link_for_node_id() {
77+
let store = get_store().await;
78+
let link_before = store
79+
.get_email_preferences_link_for_node_id(&node_id_test())
80+
.await
81+
.expect("can fetch empty link if it's not set");
82+
assert!(link_before.is_none());
83+
84+
store
85+
.add_email_preferences_link_for_node_id(
86+
&url::Url::parse("https://www.bit.cr/").unwrap(),
87+
&node_id_test(),
88+
)
89+
.await
90+
.unwrap();
91+
92+
let link = store
93+
.get_email_preferences_link_for_node_id(&node_id_test())
94+
.await
95+
.expect("can fetch link after it was set");
96+
assert_eq!(link, Some(url::Url::parse("https://www.bit.cr/").unwrap()));
97+
}
98+
}

0 commit comments

Comments
 (0)