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}