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