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