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 let Some(ref current) = current_email {
347 if new_email == current.to_lowercase() {
348 return (StatusCode::OK, Json(json!({}))).into_response();
349 }
350 }
351
352 let email_confirmed = stored_code.is_some() && email_pending_verification.is_some();
353
354 if email_confirmed {
355 let confirmation_token = match &input.token {
356 Some(t) => t.trim(),
357 None => {
358 return (
359 StatusCode::BAD_REQUEST,
360 Json(json!({"error": "TokenRequired", "message": "Token required for confirmed accounts. Call requestEmailUpdate first."})),
361 )
362 .into_response();
363 }
364 };
365
366 let pending_email = match email_pending_verification {
367 Some(p) => p,
368 None => {
369 return (
370 StatusCode::BAD_REQUEST,
371 Json(json!({"error": "InvalidRequest", "message": "No pending email update found"})),
372 )
373 .into_response();
374 }
375 };
376
377 if pending_email.to_lowercase() != new_email {
378 return (
379 StatusCode::BAD_REQUEST,
380 Json(json!({"error": "InvalidRequest", "message": "Email does not match pending update"})),
381 )
382 .into_response();
383 }
384
385 let saved_code = match stored_code {
386 Some(c) => c,
387 None => {
388 return (
389 StatusCode::BAD_REQUEST,
390 Json(json!({"error": "InvalidRequest", "message": "No pending email update found"})),
391 )
392 .into_response();
393 }
394 };
395
396 if saved_code != confirmation_token {
397 return (
398 StatusCode::BAD_REQUEST,
399 Json(json!({"error": "InvalidToken", "message": "Invalid token"})),
400 )
401 .into_response();
402 }
403
404 if let Some(exp) = expires_at {
405 if Utc::now() > exp {
406 return (
407 StatusCode::BAD_REQUEST,
408 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
409 )
410 .into_response();
411 }
412 }
413 }
414
415 let exists = sqlx::query!(
416 "SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2",
417 new_email,
418 user_id
419 )
420 .fetch_optional(&state.db)
421 .await;
422
423 if let Ok(Some(_)) = exists {
424 return (
425 StatusCode::BAD_REQUEST,
426 Json(json!({"error": "InvalidRequest", "message": "Email already in use"})),
427 )
428 .into_response();
429 }
430
431 let update = sqlx::query!(
432 r#"
433 UPDATE users
434 SET email = $1,
435 email_pending_verification = NULL,
436 email_confirmation_code = NULL,
437 email_confirmation_code_expires_at = NULL,
438 updated_at = NOW()
439 WHERE id = $2
440 "#,
441 new_email,
442 user_id
443 )
444 .execute(&state.db)
445 .await;
446
447 match update {
448 Ok(_) => {
449 info!("Email updated for user {}", user_id);
450 (StatusCode::OK, Json(json!({}))).into_response()
451 }
452 Err(e) => {
453 error!("DB error finalizing email update: {:?}", e);
454 if e.as_database_error()
455 .map(|db_err| db_err.is_unique_violation())
456 .unwrap_or(false)
457 {
458 return (
459 StatusCode::BAD_REQUEST,
460 Json(json!({"error": "InvalidRequest", "message": "Email already in use"})),
461 )
462 .into_response();
463 }
464
465 (
466 StatusCode::INTERNAL_SERVER_ERROR,
467 Json(json!({"error": "InternalError"})),
468 )
469 .into_response()
470 }
471 }
472}