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