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