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