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