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}