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