this repo has no description
1use crate::auth::validate_bearer_token; 2use crate::state::AppState; 3use axum::{ 4 Json, 5 extract::State, 6 http::{HeaderMap, StatusCode}, 7 response::{IntoResponse, Response}, 8}; 9use serde::{Deserialize, Serialize}; 10use serde_json::json; 11use sqlx::Row; 12use tracing::info; 13 14#[derive(Serialize)] 15#[serde(rename_all = "camelCase")] 16pub struct NotificationPrefsResponse { 17 pub preferred_channel: String, 18 pub email: String, 19 pub discord_id: Option<String>, 20 pub discord_verified: bool, 21 pub telegram_username: Option<String>, 22 pub telegram_verified: bool, 23 pub signal_number: Option<String>, 24 pub signal_verified: bool, 25} 26 27pub async fn get_notification_prefs(State(state): State<AppState>, headers: HeaderMap) -> Response { 28 let token = match crate::auth::extract_bearer_token_from_header( 29 headers.get("Authorization").and_then(|h| h.to_str().ok()), 30 ) { 31 Some(t) => t, 32 None => return ( 33 StatusCode::UNAUTHORIZED, 34 Json(json!({"error": "AuthenticationRequired", "message": "Authentication required"})), 35 ) 36 .into_response(), 37 }; 38 let user = match validate_bearer_token(&state.db, &token).await { 39 Ok(u) => u, 40 Err(_) => { 41 return ( 42 StatusCode::UNAUTHORIZED, 43 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token"})), 44 ) 45 .into_response(); 46 } 47 }; 48 let row = 49 match sqlx::query( 50 r#" 51 SELECT 52 email, 53 preferred_notification_channel::text as channel, 54 discord_id, 55 discord_verified, 56 telegram_username, 57 telegram_verified, 58 signal_number, 59 signal_verified 60 FROM users 61 WHERE did = $1 62 "#, 63 ) 64 .bind(&user.did) 65 .fetch_one(&state.db) 66 .await 67 { 68 Ok(r) => r, 69 Err(e) => return ( 70 StatusCode::INTERNAL_SERVER_ERROR, 71 Json( 72 json!({"error": "InternalError", "message": format!("Database error: {}", e)}), 73 ), 74 ) 75 .into_response(), 76 }; 77 let email: String = row.get("email"); 78 let channel: String = row.get("channel"); 79 let discord_id: Option<String> = row.get("discord_id"); 80 let discord_verified: bool = row.get("discord_verified"); 81 let telegram_username: Option<String> = row.get("telegram_username"); 82 let telegram_verified: bool = row.get("telegram_verified"); 83 let signal_number: Option<String> = row.get("signal_number"); 84 let signal_verified: bool = row.get("signal_verified"); 85 Json(NotificationPrefsResponse { 86 preferred_channel: channel, 87 email, 88 discord_id, 89 discord_verified, 90 telegram_username, 91 telegram_verified, 92 signal_number, 93 signal_verified, 94 }) 95 .into_response() 96} 97 98#[derive(Deserialize)] 99#[serde(rename_all = "camelCase")] 100pub struct UpdateNotificationPrefsInput { 101 pub preferred_channel: Option<String>, 102 pub discord_id: Option<String>, 103 pub telegram_username: Option<String>, 104 pub signal_number: Option<String>, 105} 106 107pub async fn update_notification_prefs( 108 State(state): State<AppState>, 109 headers: HeaderMap, 110 Json(input): Json<UpdateNotificationPrefsInput>, 111) -> Response { 112 let token = match crate::auth::extract_bearer_token_from_header( 113 headers.get("Authorization").and_then(|h| h.to_str().ok()), 114 ) { 115 Some(t) => t, 116 None => return ( 117 StatusCode::UNAUTHORIZED, 118 Json(json!({"error": "AuthenticationRequired", "message": "Authentication required"})), 119 ) 120 .into_response(), 121 }; 122 let user = match validate_bearer_token(&state.db, &token).await { 123 Ok(u) => u, 124 Err(_) => { 125 return ( 126 StatusCode::UNAUTHORIZED, 127 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token"})), 128 ) 129 .into_response(); 130 } 131 }; 132 if let Some(ref channel) = input.preferred_channel { 133 let valid_channels = ["email", "discord", "telegram", "signal"]; 134 if !valid_channels.contains(&channel.as_str()) { 135 return ( 136 StatusCode::BAD_REQUEST, 137 Json(json!({ 138 "error": "InvalidRequest", 139 "message": "Invalid channel. Must be one of: email, discord, telegram, signal" 140 })), 141 ) 142 .into_response(); 143 } 144 if let Err(e) = sqlx::query( 145 r#"UPDATE users SET preferred_notification_channel = $1::notification_channel, updated_at = NOW() WHERE did = $2"# 146 ) 147 .bind(channel) 148 .bind(&user.did) 149 .execute(&state.db) 150 .await 151 { 152 return ( 153 StatusCode::INTERNAL_SERVER_ERROR, 154 Json(json!({"error": "InternalError", "message": format!("Database error: {}", e)})), 155 ) 156 .into_response(); 157 } 158 info!(did = %user.did, channel = %channel, "Updated preferred notification channel"); 159 } 160 if let Some(ref discord_id) = input.discord_id { 161 let discord_id_clean: Option<&str> = if discord_id.is_empty() { 162 None 163 } else { 164 Some(discord_id.as_str()) 165 }; 166 if let Err(e) = sqlx::query( 167 r#"UPDATE users SET discord_id = $1, discord_verified = FALSE, updated_at = NOW() WHERE did = $2"# 168 ) 169 .bind(discord_id_clean) 170 .bind(&user.did) 171 .execute(&state.db) 172 .await 173 { 174 return ( 175 StatusCode::INTERNAL_SERVER_ERROR, 176 Json(json!({"error": "InternalError", "message": format!("Database error: {}", e)})), 177 ) 178 .into_response(); 179 } 180 info!(did = %user.did, "Updated Discord ID"); 181 } 182 if let Some(ref telegram) = input.telegram_username { 183 let telegram_clean: Option<&str> = if telegram.is_empty() { 184 None 185 } else { 186 Some(telegram.trim_start_matches('@')) 187 }; 188 if let Err(e) = sqlx::query( 189 r#"UPDATE users SET telegram_username = $1, telegram_verified = FALSE, updated_at = NOW() WHERE did = $2"# 190 ) 191 .bind(telegram_clean) 192 .bind(&user.did) 193 .execute(&state.db) 194 .await 195 { 196 return ( 197 StatusCode::INTERNAL_SERVER_ERROR, 198 Json(json!({"error": "InternalError", "message": format!("Database error: {}", e)})), 199 ) 200 .into_response(); 201 } 202 info!(did = %user.did, "Updated Telegram username"); 203 } 204 if let Some(ref signal) = input.signal_number { 205 let signal_clean: Option<&str> = if signal.is_empty() { 206 None 207 } else { 208 Some(signal.as_str()) 209 }; 210 if let Err(e) = sqlx::query( 211 r#"UPDATE users SET signal_number = $1, signal_verified = FALSE, updated_at = NOW() WHERE did = $2"# 212 ) 213 .bind(signal_clean) 214 .bind(&user.did) 215 .execute(&state.db) 216 .await 217 { 218 return ( 219 StatusCode::INTERNAL_SERVER_ERROR, 220 Json(json!({"error": "InternalError", "message": format!("Database error: {}", e)})), 221 ) 222 .into_response(); 223 } 224 info!(did = %user.did, "Updated Signal number"); 225 } 226 Json(json!({"success": true})).into_response() 227}