this repo has no description
1use crate::api::ApiError;
2use crate::state::{AppState, RateLimitKind};
3use axum::{
4 Json,
5 extract::State,
6 http::StatusCode,
7 response::{IntoResponse, Response},
8};
9use chrono::Utc;
10use serde::Deserialize;
11use serde_json::json;
12use tracing::{error, info, warn};
13
14#[derive(Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct RequestEmailUpdateInput {
17 pub email: String,
18}
19
20pub async fn request_email_update(
21 State(state): State<AppState>,
22 headers: axum::http::HeaderMap,
23 Json(input): Json<RequestEmailUpdateInput>,
24) -> Response {
25 let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
26 if !state
27 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip)
28 .await
29 {
30 warn!(ip = %client_ip, "Email update rate limit exceeded");
31 return (
32 StatusCode::TOO_MANY_REQUESTS,
33 Json(json!({
34 "error": "RateLimitExceeded",
35 "message": "Too many requests. Please try again later."
36 })),
37 )
38 .into_response();
39 }
40
41 let token = match crate::auth::extract_bearer_token_from_header(
42 headers.get("Authorization").and_then(|h| h.to_str().ok()),
43 ) {
44 Some(t) => t,
45 None => {
46 return (
47 StatusCode::UNAUTHORIZED,
48 Json(json!({"error": "AuthenticationRequired"})),
49 )
50 .into_response();
51 }
52 };
53
54 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
55 let did = match auth_result {
56 Ok(user) => user.did,
57 Err(e) => return ApiError::from(e).into_response(),
58 };
59
60 let user = match sqlx::query!("SELECT id, handle, email FROM users WHERE did = $1", did)
61 .fetch_optional(&state.db)
62 .await
63 {
64 Ok(Some(row)) => row,
65 _ => {
66 return (
67 StatusCode::INTERNAL_SERVER_ERROR,
68 Json(json!({"error": "InternalError"})),
69 )
70 .into_response();
71 }
72 };
73
74 let user_id = user.id;
75 let handle = user.handle;
76 let current_email = user.email;
77 let email = input.email.trim().to_lowercase();
78
79 if !crate::api::validation::is_valid_email(&email) {
80 return (
81 StatusCode::BAD_REQUEST,
82 Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})),
83 )
84 .into_response();
85 }
86
87 if current_email.as_ref().map(|e| e.to_lowercase()) == Some(email.clone()) {
88 return (StatusCode::OK, Json(json!({ "tokenRequired": false }))).into_response();
89 }
90
91 let exists = sqlx::query!(
92 "SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2",
93 email,
94 user_id
95 )
96 .fetch_optional(&state.db)
97 .await;
98
99 if let Ok(Some(_)) = exists {
100 return (
101 StatusCode::BAD_REQUEST,
102 Json(json!({"error": "EmailTaken", "message": "Email already taken"})),
103 )
104 .into_response();
105 }
106
107 if let Err(e) = crate::api::notification_prefs::request_channel_verification(
108 &state.db,
109 user_id,
110 "email",
111 &email,
112 Some(&handle),
113 )
114 .await
115 {
116 error!("Failed to request email verification: {}", e);
117 return (
118 StatusCode::INTERNAL_SERVER_ERROR,
119 Json(json!({"error": "InternalError"})),
120 )
121 .into_response();
122 }
123
124 info!("Email update requested for user {}", user_id);
125 (StatusCode::OK, Json(json!({ "tokenRequired": true }))).into_response()
126}
127
128#[derive(Deserialize)]
129#[serde(rename_all = "camelCase")]
130pub struct ConfirmEmailInput {
131 pub email: String,
132 pub token: String,
133}
134
135pub async fn confirm_email(
136 State(state): State<AppState>,
137 headers: axum::http::HeaderMap,
138 Json(input): Json<ConfirmEmailInput>,
139) -> Response {
140 let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
141 if !state
142 .check_rate_limit(RateLimitKind::AppPassword, &client_ip)
143 .await
144 {
145 warn!(ip = %client_ip, "Confirm email rate limit exceeded");
146 return (
147 StatusCode::TOO_MANY_REQUESTS,
148 Json(json!({
149 "error": "RateLimitExceeded",
150 "message": "Too many requests. Please try again later."
151 })),
152 )
153 .into_response();
154 }
155
156 let token = match crate::auth::extract_bearer_token_from_header(
157 headers.get("Authorization").and_then(|h| h.to_str().ok()),
158 ) {
159 Some(t) => t,
160 None => {
161 return (
162 StatusCode::UNAUTHORIZED,
163 Json(json!({"error": "AuthenticationRequired"})),
164 )
165 .into_response();
166 }
167 };
168
169 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
170 let did = match auth_result {
171 Ok(user) => user.did,
172 Err(e) => return ApiError::from(e).into_response(),
173 };
174
175 let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
176 .fetch_one(&state.db)
177 .await
178 {
179 Ok(id) => id,
180 Err(_) => {
181 return (
182 StatusCode::INTERNAL_SERVER_ERROR,
183 Json(json!({"error": "InternalError"})),
184 )
185 .into_response();
186 }
187 };
188
189 let verification = match sqlx::query!(
190 "SELECT code, pending_identifier, expires_at FROM channel_verifications WHERE user_id = $1 AND channel = 'email'",
191 user_id
192 )
193 .fetch_optional(&state.db)
194 .await
195 {
196 Ok(Some(row)) => row,
197 _ => {
198 return (
199 StatusCode::BAD_REQUEST,
200 Json(json!({"error": "InvalidRequest", "message": "No pending email update found"})),
201 )
202 .into_response();
203 }
204 };
205
206 let pending_email = verification.pending_identifier.unwrap_or_default();
207 let email = input.email.trim().to_lowercase();
208 let confirmation_code = input.token.trim();
209
210 if pending_email != email {
211 return (
212 StatusCode::BAD_REQUEST,
213 Json(json!({"error": "InvalidRequest", "message": "Email does not match pending update"})),
214 )
215 .into_response();
216 }
217
218 if verification.code != confirmation_code {
219 return (
220 StatusCode::BAD_REQUEST,
221 Json(json!({"error": "InvalidToken", "message": "Invalid token"})),
222 )
223 .into_response();
224 }
225
226 if Utc::now() > verification.expires_at {
227 return (
228 StatusCode::BAD_REQUEST,
229 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
230 )
231 .into_response();
232 }
233
234 let mut tx = match state.db.begin().await {
235 Ok(tx) => tx,
236 Err(_) => return ApiError::InternalError.into_response(),
237 };
238
239 let update = sqlx::query!(
240 "UPDATE users SET email = $1, updated_at = NOW() WHERE id = $2",
241 pending_email,
242 user_id
243 )
244 .execute(&mut *tx)
245 .await;
246
247 if let Err(e) = update {
248 error!("DB error finalizing email update: {:?}", e);
249 if e.as_database_error()
250 .map(|db_err| db_err.is_unique_violation())
251 .unwrap_or(false)
252 {
253 return (
254 StatusCode::BAD_REQUEST,
255 Json(json!({"error": "EmailTaken", "message": "Email already taken"})),
256 )
257 .into_response();
258 }
259 return (
260 StatusCode::INTERNAL_SERVER_ERROR,
261 Json(json!({"error": "InternalError"})),
262 )
263 .into_response();
264 }
265
266 if let Err(e) = sqlx::query!(
267 "DELETE FROM channel_verifications WHERE user_id = $1 AND channel = 'email'",
268 user_id
269 )
270 .execute(&mut *tx)
271 .await
272 {
273 error!("Failed to delete verification record: {:?}", e);
274 return ApiError::InternalError.into_response();
275 }
276
277 if let Err(_) = tx.commit().await {
278 return ApiError::InternalError.into_response();
279 }
280
281 info!("Email updated for user {}", user_id);
282 (StatusCode::OK, Json(json!({}))).into_response()
283}
284
285#[derive(Deserialize)]
286#[serde(rename_all = "camelCase")]
287pub struct UpdateEmailInput {
288 pub email: String,
289 #[serde(default)]
290 pub email_auth_factor: Option<bool>,
291 pub token: Option<String>,
292}
293
294pub async fn update_email(
295 State(state): State<AppState>,
296 headers: axum::http::HeaderMap,
297 Json(input): Json<UpdateEmailInput>,
298) -> Response {
299 let token = match crate::auth::extract_bearer_token_from_header(
300 headers.get("Authorization").and_then(|h| h.to_str().ok()),
301 ) {
302 Some(t) => t,
303 None => {
304 return (
305 StatusCode::UNAUTHORIZED,
306 Json(json!({"error": "AuthenticationRequired"})),
307 )
308 .into_response();
309 }
310 };
311
312 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
313 let did = match auth_result {
314 Ok(user) => user.did,
315 Err(e) => return ApiError::from(e).into_response(),
316 };
317
318 let user = match sqlx::query!(
319 "SELECT id, email FROM users WHERE did = $1",
320 did
321 )
322 .fetch_optional(&state.db)
323 .await
324 {
325 Ok(Some(row)) => row,
326 _ => {
327 return (
328 StatusCode::INTERNAL_SERVER_ERROR,
329 Json(json!({"error": "InternalError"})),
330 )
331 .into_response();
332 }
333 };
334
335 let user_id = user.id;
336 let current_email = user.email;
337 let new_email = input.email.trim().to_lowercase();
338
339 if !crate::api::validation::is_valid_email(&new_email) {
340 return (
341 StatusCode::BAD_REQUEST,
342 Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})),
343 )
344 .into_response();
345 }
346
347 if let Some(ref current) = current_email
348 && new_email == current.to_lowercase()
349 {
350 return (StatusCode::OK, Json(json!({}))).into_response();
351 }
352
353 let verification = sqlx::query!(
354 "SELECT code, pending_identifier, expires_at FROM channel_verifications WHERE user_id = $1 AND channel = 'email'",
355 user_id
356 )
357 .fetch_optional(&state.db)
358 .await
359 .unwrap_or(None);
360
361 if let Some(ver) = verification {
362 let confirmation_token = match &input.token {
363 Some(t) => t.trim(),
364 None => {
365 return (
366 StatusCode::BAD_REQUEST,
367 Json(json!({"error": "TokenRequired", "message": "Token required. Call requestEmailUpdate first."})),
368 )
369 .into_response();
370 }
371 };
372
373 let pending_email = ver.pending_identifier.unwrap_or_default();
374 if pending_email.to_lowercase() != new_email {
375 return (
376 StatusCode::BAD_REQUEST,
377 Json(json!({"error": "InvalidRequest", "message": "Email does not match pending update"})),
378 )
379 .into_response();
380 }
381
382 if ver.code != confirmation_token {
383 return (
384 StatusCode::BAD_REQUEST,
385 Json(json!({"error": "InvalidToken", "message": "Invalid token"})),
386 )
387 .into_response();
388 }
389
390 if Utc::now() > ver.expires_at {
391 return (
392 StatusCode::BAD_REQUEST,
393 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
394 )
395 .into_response();
396 }
397 }
398
399 let exists = sqlx::query!(
400 "SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2",
401 new_email,
402 user_id
403 )
404 .fetch_optional(&state.db)
405 .await;
406
407 if let Ok(Some(_)) = exists {
408 return (
409 StatusCode::BAD_REQUEST,
410 Json(json!({"error": "InvalidRequest", "message": "Email already in use"})),
411 )
412 .into_response();
413 }
414
415 let mut tx = match state.db.begin().await {
416 Ok(tx) => tx,
417 Err(_) => return ApiError::InternalError.into_response(),
418 };
419
420 let update = sqlx::query!(
421 "UPDATE users SET email = $1, updated_at = NOW() WHERE id = $2",
422 new_email,
423 user_id
424 )
425 .execute(&mut *tx)
426 .await;
427
428 if let Err(e) = update {
429 error!("DB error finalizing email update: {:?}", e);
430 if e.as_database_error()
431 .map(|db_err| db_err.is_unique_violation())
432 .unwrap_or(false)
433 {
434 return (
435 StatusCode::BAD_REQUEST,
436 Json(json!({"error": "InvalidRequest", "message": "Email already in use"})),
437 )
438 .into_response();
439 }
440 return (
441 StatusCode::INTERNAL_SERVER_ERROR,
442 Json(json!({"error": "InternalError"})),
443 )
444 .into_response();
445 }
446
447 let _ = sqlx::query!(
448 "DELETE FROM channel_verifications WHERE user_id = $1 AND channel = 'email'",
449 user_id
450 )
451 .execute(&mut *tx)
452 .await;
453
454 if let Err(_) = tx.commit().await {
455 return ApiError::InternalError.into_response();
456 }
457
458 match sqlx::query!(
459 "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, 'email_auth_factor', $2) ON CONFLICT (user_id, name) DO UPDATE SET value_json = $2",
460 user_id,
461 json!(input.email_auth_factor.unwrap_or(false))
462 )
463 .execute(&state.db)
464 .await
465 {
466 Ok(_) => {}
467 Err(e) => warn!("Failed to update email_auth_factor preference: {}", e),
468 }
469
470 info!("Email updated for user {}", user_id);
471 (StatusCode::OK, Json(json!({}))).into_response()
472}