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