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}