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