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