Skip to content

Commit c952207

Browse files
authored
Merge pull request #368 from BYTES-TECHES/guard
Fixed Issue #303
2 parents 9fdf8d1 + 21b5483 commit c952207

9 files changed

Lines changed: 382 additions & 89 deletions

File tree

indexer/src/database.rs

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,22 +1057,8 @@ impl Database {
10571057
) -> Result<crate::models::NotificationPreferences, AppError> {
10581058
// Fetch existing or use defaults, then apply partial update
10591059
let existing = self.get_notification_preferences(address).await?;
1060-
let base = existing.unwrap_or_else(|| crate::models::NotificationPreferences {
1061-
address: address.to_string(),
1062-
email_enabled: false,
1063-
email_address: None,
1064-
sms_enabled: false,
1065-
phone_number: None,
1066-
push_enabled: false,
1067-
push_token: None,
1068-
on_trade_created: true,
1069-
on_trade_funded: true,
1070-
on_trade_completed: true,
1071-
on_trade_confirmed: true,
1072-
on_dispute_raised: true,
1073-
on_dispute_resolved: true,
1074-
on_trade_cancelled: true,
1075-
updated_at: chrono::Utc::now(),
1060+
let base = existing.unwrap_or_else(|| {
1061+
crate::models::NotificationPreferences::default_for_address(address.to_string())
10761062
});
10771063

10781064
let row = sqlx::query_as::<_, crate::models::NotificationPreferences>(
@@ -1121,6 +1107,23 @@ impl Database {
11211107
Ok(row)
11221108
}
11231109

1110+
pub async fn unregister_push_token(&self, token: &str) -> Result<u64, AppError> {
1111+
let result = sqlx::query(
1112+
r#"
1113+
UPDATE notification_preferences
1114+
SET push_enabled = FALSE,
1115+
push_token = NULL,
1116+
updated_at = NOW()
1117+
WHERE push_token = $1
1118+
"#,
1119+
)
1120+
.bind(token)
1121+
.execute(&self.pool)
1122+
.await?;
1123+
1124+
Ok(result.rows_affected())
1125+
}
1126+
11241127
pub async fn log_notification(
11251128
&self,
11261129
address: &str,

indexer/src/error.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ pub enum AppError {
2323
#[error("Invalid event data: {0}")]
2424
InvalidEventData(String),
2525

26+
#[error("Bad request: {0}")]
27+
BadRequest(String),
28+
2629
#[error("Event not found")]
2730
EventNotFound,
2831

@@ -82,6 +85,11 @@ impl IntoResponse for AppError {
8285
"INVALID_EVENT_DATA",
8386
"Invalid event data",
8487
),
88+
AppError::BadRequest(_) => (
89+
StatusCode::BAD_REQUEST,
90+
"BAD_REQUEST",
91+
"Bad request",
92+
),
8593
AppError::EventNotFound => {
8694
(StatusCode::NOT_FOUND, "EVENT_NOT_FOUND", "Event not found")
8795
}

indexer/src/event_monitor.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,9 @@ impl EventMonitor {
277277
};
278278
self.ws_manager.broadcast(ws_message).await;
279279

280+
// Notifications are best-effort and should not block the rest of event processing.
281+
self.notification_service.process_event(event).await;
282+
280283
// Fraud detection for high-value trade events
281284
let report = match event.event_type.as_str() {
282285
"trade_created" => self.fraud_service.process_event(event).await,

indexer/src/handlers.rs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ pub async fn api_index() -> Json<serde_json::Value> {
6262
"notif_prefs_get": "GET /notifications/preferences/:address",
6363
"notif_prefs_put": "PUT /notifications/preferences/:address",
6464
"notif_log": "GET /notifications/log/:address",
65+
"push_register": "POST /push/register",
66+
"push_unregister": "DELETE /push/unregister/:device_token",
6567
"help": "GET /help"
6668
}
6769
}))
@@ -473,7 +475,7 @@ pub async fn get_notification_preferences(
473475
.database
474476
.get_notification_preferences(&address)
475477
.await?
476-
.ok_or_else(|| AppError::NotFound("preferences not found".into()))?;
478+
.unwrap_or_else(|| crate::models::NotificationPreferences::default_for_address(address));
477479
Ok(Json(prefs))
478480
}
479481

@@ -503,6 +505,61 @@ pub async fn get_notification_log(
503505
Ok(Json(entries))
504506
}
505507

508+
/// POST /push/register
509+
pub async fn register_push_token(
510+
State(state): State<AppState>,
511+
Json(body): Json<crate::models::PushRegistrationRequest>,
512+
) -> Result<Json<crate::models::NotificationPreferences>, AppError> {
513+
let token = body.device_token.trim();
514+
let address = body.address.trim();
515+
516+
if token.is_empty() || address.is_empty() {
517+
return Err(AppError::BadRequest(
518+
"address and device_token are required".to_string(),
519+
));
520+
}
521+
522+
let prefs = state
523+
.database
524+
.upsert_notification_preferences(
525+
address,
526+
&crate::models::UpdateNotificationPreferences {
527+
email_enabled: None,
528+
email_address: None,
529+
sms_enabled: None,
530+
phone_number: None,
531+
push_enabled: Some(true),
532+
push_token: Some(token.to_string()),
533+
on_trade_created: None,
534+
on_trade_funded: None,
535+
on_trade_completed: None,
536+
on_trade_confirmed: None,
537+
on_dispute_raised: None,
538+
on_dispute_resolved: None,
539+
on_trade_cancelled: None,
540+
},
541+
)
542+
.await?;
543+
544+
Ok(Json(prefs))
545+
}
546+
547+
/// DELETE /push/unregister/:device_token
548+
pub async fn unregister_push_token(
549+
Path(device_token): Path<String>,
550+
State(state): State<AppState>,
551+
) -> Result<Json<serde_json::Value>, AppError> {
552+
let removed = state
553+
.database
554+
.unregister_push_token(device_token.trim())
555+
.await?;
556+
557+
Ok(Json(json!({
558+
"status": "ok",
559+
"removed": removed,
560+
})))
561+
}
562+
506563
// =============================================================================
507564
// Gateway Handlers
508565
// =============================================================================

indexer/src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
289289
get(get_notification_preferences).put(upsert_notification_preferences),
290290
)
291291
.route("/notifications/log/:address", get(get_notification_log))
292+
.route("/push/register", post(register_push_token))
293+
.route("/push/unregister/:device_token", delete(unregister_push_token))
292294
// Performance monitoring
293295
.route("/performance/dashboard", get(get_performance_dashboard))
294296
.route("/performance/alerts", get(get_performance_alerts))

indexer/src/models.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,28 @@ pub struct NotificationPreferences {
517517
pub updated_at: DateTime<Utc>,
518518
}
519519

520+
impl NotificationPreferences {
521+
pub fn default_for_address(address: impl Into<String>) -> Self {
522+
Self {
523+
address: address.into(),
524+
email_enabled: false,
525+
email_address: None,
526+
sms_enabled: false,
527+
phone_number: None,
528+
push_enabled: false,
529+
push_token: None,
530+
on_trade_created: true,
531+
on_trade_funded: true,
532+
on_trade_completed: true,
533+
on_trade_confirmed: true,
534+
on_dispute_raised: true,
535+
on_dispute_resolved: true,
536+
on_trade_cancelled: true,
537+
updated_at: Utc::now(),
538+
}
539+
}
540+
}
541+
520542
/// Upsert payload — all fields optional so callers only send what they want to change.
521543
#[derive(Debug, Clone, Serialize, Deserialize)]
522544
pub struct UpdateNotificationPreferences {
@@ -547,3 +569,10 @@ pub struct NotificationLogEntry {
547569
pub error: Option<String>,
548570
pub created_at: DateTime<Utc>,
549571
}
572+
573+
#[derive(Debug, Clone, Serialize, Deserialize)]
574+
pub struct PushRegistrationRequest {
575+
pub device_token: String,
576+
pub platform: String,
577+
pub address: String,
578+
}

indexer/src/notification_service/channels.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,19 @@ pub async fn send_email(
1414
subject: &str,
1515
body: &str,
1616
) -> Result<(), String> {
17+
if cfg.email_api_url.trim().is_empty()
18+
|| cfg.email_api_key.trim().is_empty()
19+
|| cfg.email_from.trim().is_empty()
20+
{
21+
return Err("email provider is not configured".to_string());
22+
}
23+
if to.trim().is_empty() {
24+
return Err("email recipient is empty".to_string());
25+
}
26+
if subject.trim().is_empty() || body.trim().is_empty() {
27+
return Err("email content is empty".to_string());
28+
}
29+
1730
let client = Client::new();
1831
// SendGrid-compatible POST /v3/mail/send
1932
let payload = json!({
@@ -46,6 +59,20 @@ pub async fn send_email(
4659
}
4760

4861
pub async fn send_sms(cfg: &NotificationConfig, to: &str, body: &str) -> Result<(), String> {
62+
if cfg.sms_api_url.trim().is_empty()
63+
|| cfg.sms_account_sid.trim().is_empty()
64+
|| cfg.sms_auth_token.trim().is_empty()
65+
|| cfg.sms_from.trim().is_empty()
66+
{
67+
return Err("sms provider is not configured".to_string());
68+
}
69+
if to.trim().is_empty() {
70+
return Err("sms recipient is empty".to_string());
71+
}
72+
if body.trim().is_empty() {
73+
return Err("sms content is empty".to_string());
74+
}
75+
4976
let client = Client::new();
5077
// Twilio-compatible POST /2010-04-01/Accounts/{sid}/Messages.json
5178
let url = format!(
@@ -81,6 +108,19 @@ pub async fn send_push(
81108
title: &str,
82109
body: &str,
83110
) -> Result<(), String> {
111+
if cfg.push_api_url.trim().is_empty()
112+
|| cfg.push_project_id.trim().is_empty()
113+
|| cfg.push_server_key.trim().is_empty()
114+
{
115+
return Err("push provider is not configured".to_string());
116+
}
117+
if token.trim().is_empty() {
118+
return Err("push token is empty".to_string());
119+
}
120+
if title.trim().is_empty() || body.trim().is_empty() {
121+
return Err("push content is empty".to_string());
122+
}
123+
84124
let client = Client::new();
85125
// FCM v1 HTTP API
86126
let payload = json!({
@@ -102,7 +142,7 @@ pub async fn send_push(
102142
.map_err(|e| e.to_string())?;
103143

104144
if res.status().is_success() {
105-
info!("Push sent to token {}", &token[..8]);
145+
info!("Push sent to token {}", token_preview(token));
106146
Ok(())
107147
} else {
108148
let msg = format!(
@@ -114,3 +154,12 @@ pub async fn send_push(
114154
Err(msg)
115155
}
116156
}
157+
158+
fn token_preview(token: &str) -> &str {
159+
let end = token
160+
.char_indices()
161+
.nth(8)
162+
.map(|(idx, _)| idx)
163+
.unwrap_or(token.len());
164+
&token[..end]
165+
}

0 commit comments

Comments
 (0)