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