this repo has no description
1use crate::api::ApiError;
2use crate::auth::BearerAuth;
3use crate::state::{AppState, RateLimitKind};
4use axum::{
5 Json,
6 extract::State,
7 http::StatusCode,
8 response::{IntoResponse, Response},
9};
10use serde::Deserialize;
11use serde_json::json;
12use tracing::{error, info, warn};
13
14pub async fn request_email_update(
15 State(state): State<AppState>,
16 headers: axum::http::HeaderMap,
17 auth: BearerAuth,
18) -> Response {
19 let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
20 if !state
21 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip)
22 .await
23 {
24 warn!(ip = %client_ip, "Email update rate limit exceeded");
25 return (
26 StatusCode::TOO_MANY_REQUESTS,
27 Json(json!({
28 "error": "RateLimitExceeded",
29 "message": "Too many requests. Please try again later."
30 })),
31 )
32 .into_response();
33 }
34
35 if let Err(e) = crate::auth::scope_check::check_account_scope(
36 auth.0.is_oauth,
37 auth.0.scope.as_deref(),
38 crate::oauth::scopes::AccountAttr::Email,
39 crate::oauth::scopes::AccountAction::Manage,
40 ) {
41 return e;
42 }
43
44 let did = auth.0.did.clone();
45 let user = match sqlx::query!(
46 "SELECT id, handle, email, email_verified FROM users WHERE did = $1",
47 did
48 )
49 .fetch_optional(&state.db)
50 .await
51 {
52 Ok(Some(row)) => row,
53 Ok(None) => {
54 return (
55 StatusCode::BAD_REQUEST,
56 Json(json!({"error": "InvalidRequest", "message": "account not found"})),
57 )
58 .into_response();
59 }
60 Err(e) => {
61 error!("DB error: {:?}", e);
62 return (
63 StatusCode::INTERNAL_SERVER_ERROR,
64 Json(json!({"error": "InternalError"})),
65 )
66 .into_response();
67 }
68 };
69
70 let current_email: String = match user.email {
71 Some(e) => e,
72 None => {
73 return (
74 StatusCode::BAD_REQUEST,
75 Json(json!({"error": "InvalidRequest", "message": "account does not have an email address"})),
76 )
77 .into_response();
78 }
79 };
80
81 let token_required = user.email_verified;
82
83 if token_required {
84 let code = crate::auth::verification_token::generate_channel_update_token(
85 &did,
86 "email_update",
87 ¤t_email.to_lowercase(),
88 );
89 let formatted_code = crate::auth::verification_token::format_token_for_display(&code);
90
91 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
92 if let Err(e) =
93 crate::comms::enqueue_email_update_token(&state.db, user.id, &formatted_code, &hostname)
94 .await
95 {
96 warn!("Failed to enqueue email update notification: {:?}", e);
97 }
98 }
99
100 info!("Email update requested for user {}", user.id);
101 (
102 StatusCode::OK,
103 Json(json!({ "tokenRequired": token_required })),
104 )
105 .into_response()
106}
107
108#[derive(Deserialize)]
109#[serde(rename_all = "camelCase")]
110pub struct ConfirmEmailInput {
111 pub email: String,
112 pub token: String,
113}
114
115pub async fn confirm_email(
116 State(state): State<AppState>,
117 headers: axum::http::HeaderMap,
118 auth: BearerAuth,
119 Json(input): Json<ConfirmEmailInput>,
120) -> Response {
121 let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
122 if !state
123 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip)
124 .await
125 {
126 warn!(ip = %client_ip, "Confirm email rate limit exceeded");
127 return (
128 StatusCode::TOO_MANY_REQUESTS,
129 Json(json!({
130 "error": "RateLimitExceeded",
131 "message": "Too many requests. Please try again later."
132 })),
133 )
134 .into_response();
135 }
136
137 if let Err(e) = crate::auth::scope_check::check_account_scope(
138 auth.0.is_oauth,
139 auth.0.scope.as_deref(),
140 crate::oauth::scopes::AccountAttr::Email,
141 crate::oauth::scopes::AccountAction::Manage,
142 ) {
143 return e;
144 }
145
146 let did = auth.0.did;
147 let user = match sqlx::query!(
148 "SELECT id, email, email_verified FROM users WHERE did = $1",
149 did
150 )
151 .fetch_optional(&state.db)
152 .await
153 {
154 Ok(Some(row)) => row,
155 Ok(None) => {
156 return (
157 StatusCode::BAD_REQUEST,
158 Json(json!({"error": "AccountNotFound", "message": "user not found"})),
159 )
160 .into_response();
161 }
162 Err(e) => {
163 error!("DB error: {:?}", e);
164 return (
165 StatusCode::INTERNAL_SERVER_ERROR,
166 Json(json!({"error": "InternalError"})),
167 )
168 .into_response();
169 }
170 };
171
172 let current_email = match &user.email {
173 Some(e) => e.to_lowercase(),
174 None => {
175 return (
176 StatusCode::BAD_REQUEST,
177 Json(json!({"error": "InvalidEmail", "message": "account does not have an email address"})),
178 )
179 .into_response();
180 }
181 };
182
183 let provided_email = input.email.trim().to_lowercase();
184 if provided_email != current_email {
185 return (
186 StatusCode::BAD_REQUEST,
187 Json(json!({"error": "InvalidEmail", "message": "invalid email"})),
188 )
189 .into_response();
190 }
191
192 if user.email_verified {
193 return (StatusCode::OK, Json(json!({}))).into_response();
194 }
195
196 let confirmation_code =
197 crate::auth::verification_token::normalize_token_input(input.token.trim());
198
199 let verified = crate::auth::verification_token::verify_signup_token(
200 &confirmation_code,
201 "email",
202 &provided_email,
203 );
204
205 match verified {
206 Ok(token_data) => {
207 if token_data.did != did {
208 return (
209 StatusCode::BAD_REQUEST,
210 Json(
211 json!({"error": "InvalidToken", "message": "Token does not match account"}),
212 ),
213 )
214 .into_response();
215 }
216 }
217 Err(crate::auth::verification_token::VerifyError::Expired) => {
218 return (
219 StatusCode::BAD_REQUEST,
220 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
221 )
222 .into_response();
223 }
224 Err(_) => {
225 return (
226 StatusCode::BAD_REQUEST,
227 Json(json!({"error": "InvalidToken", "message": "Invalid token"})),
228 )
229 .into_response();
230 }
231 }
232
233 let update = sqlx::query!(
234 "UPDATE users SET email_verified = TRUE, updated_at = NOW() WHERE id = $1",
235 user.id
236 )
237 .execute(&state.db)
238 .await;
239
240 if let Err(e) = update {
241 error!("DB error confirming email: {:?}", e);
242 return (
243 StatusCode::INTERNAL_SERVER_ERROR,
244 Json(json!({"error": "InternalError"})),
245 )
246 .into_response();
247 }
248
249 info!("Email confirmed for user {}", user.id);
250 (StatusCode::OK, Json(json!({}))).into_response()
251}
252
253#[derive(Deserialize)]
254#[serde(rename_all = "camelCase")]
255pub struct UpdateEmailInput {
256 pub email: String,
257 #[serde(default)]
258 pub email_auth_factor: Option<bool>,
259 pub token: Option<String>,
260}
261
262pub async fn update_email(
263 State(state): State<AppState>,
264 headers: axum::http::HeaderMap,
265 Json(input): Json<UpdateEmailInput>,
266) -> Response {
267 let bearer_token = match crate::auth::extract_bearer_token_from_header(
268 headers.get("Authorization").and_then(|h| h.to_str().ok()),
269 ) {
270 Some(t) => t,
271 None => {
272 return (
273 StatusCode::UNAUTHORIZED,
274 Json(json!({"error": "AuthenticationRequired"})),
275 )
276 .into_response();
277 }
278 };
279
280 let auth_result = crate::auth::validate_bearer_token(&state.db, &bearer_token).await;
281 let auth_user = match auth_result {
282 Ok(user) => user,
283 Err(e) => return ApiError::from(e).into_response(),
284 };
285
286 if let Err(e) = crate::auth::scope_check::check_account_scope(
287 auth_user.is_oauth,
288 auth_user.scope.as_deref(),
289 crate::oauth::scopes::AccountAttr::Email,
290 crate::oauth::scopes::AccountAction::Manage,
291 ) {
292 return e;
293 }
294
295 let did = auth_user.did;
296 let user = match sqlx::query!(
297 "SELECT id, email, email_verified FROM users WHERE did = $1",
298 did
299 )
300 .fetch_optional(&state.db)
301 .await
302 {
303 Ok(Some(row)) => row,
304 Ok(None) => {
305 return (
306 StatusCode::BAD_REQUEST,
307 Json(json!({"error": "InvalidRequest", "message": "account not found"})),
308 )
309 .into_response();
310 }
311 Err(e) => {
312 error!("DB error: {:?}", e);
313 return (
314 StatusCode::INTERNAL_SERVER_ERROR,
315 Json(json!({"error": "InternalError"})),
316 )
317 .into_response();
318 }
319 };
320
321 let user_id = user.id;
322 let current_email = user.email.clone();
323 let email_verified = user.email_verified;
324 let new_email = input.email.trim().to_lowercase();
325
326 if !crate::api::validation::is_valid_email(&new_email) {
327 return (
328 StatusCode::BAD_REQUEST,
329 Json(json!({
330 "error": "InvalidRequest",
331 "message": "This email address is not supported, please use a different email."
332 })),
333 )
334 .into_response();
335 }
336
337 if let Some(ref current) = current_email
338 && new_email == current.to_lowercase()
339 {
340 return (StatusCode::OK, Json(json!({}))).into_response();
341 }
342
343 if email_verified {
344 let confirmation_token = match &input.token {
345 Some(t) => crate::auth::verification_token::normalize_token_input(t.trim()),
346 None => {
347 return (
348 StatusCode::BAD_REQUEST,
349 Json(json!({
350 "error": "TokenRequired",
351 "message": "confirmation token required"
352 })),
353 )
354 .into_response();
355 }
356 };
357
358 let current_email_lower = current_email
359 .as_ref()
360 .map(|e| e.to_lowercase())
361 .unwrap_or_default();
362
363 let verified = crate::auth::verification_token::verify_channel_update_token(
364 &confirmation_token,
365 "email_update",
366 ¤t_email_lower,
367 );
368
369 match verified {
370 Ok(token_data) => {
371 if token_data.did != did {
372 return (
373 StatusCode::BAD_REQUEST,
374 Json(
375 json!({"error": "InvalidToken", "message": "Token does not match account"}),
376 ),
377 )
378 .into_response();
379 }
380 }
381 Err(crate::auth::verification_token::VerifyError::Expired) => {
382 return (
383 StatusCode::BAD_REQUEST,
384 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
385 )
386 .into_response();
387 }
388 Err(_) => {
389 return (
390 StatusCode::BAD_REQUEST,
391 Json(json!({"error": "InvalidToken", "message": "Invalid token"})),
392 )
393 .into_response();
394 }
395 }
396 }
397
398 let exists = sqlx::query!(
399 "SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2",
400 new_email,
401 user_id
402 )
403 .fetch_optional(&state.db)
404 .await;
405
406 if let Ok(Some(_)) = exists {
407 return (
408 StatusCode::BAD_REQUEST,
409 Json(json!({
410 "error": "InvalidRequest",
411 "message": "This email address is already in use, please use a different email."
412 })),
413 )
414 .into_response();
415 }
416
417 let update: Result<sqlx::postgres::PgQueryResult, sqlx::Error> = sqlx::query!(
418 "UPDATE users SET email = $1, email_verified = FALSE, updated_at = NOW() WHERE id = $2",
419 new_email,
420 user_id
421 )
422 .execute(&state.db)
423 .await;
424
425 if let Err(e) = update {
426 error!("DB error updating email: {:?}", e);
427 if e.as_database_error()
428 .map(|db_err: &dyn sqlx::error::DatabaseError| db_err.is_unique_violation())
429 .unwrap_or(false)
430 {
431 return (
432 StatusCode::BAD_REQUEST,
433 Json(json!({
434 "error": "InvalidRequest",
435 "message": "This email address is already in use, please use a different email."
436 })),
437 )
438 .into_response();
439 }
440 return (
441 StatusCode::INTERNAL_SERVER_ERROR,
442 Json(json!({"error": "InternalError"})),
443 )
444 .into_response();
445 }
446
447 let verification_token =
448 crate::auth::verification_token::generate_signup_token(&did, "email", &new_email);
449 let formatted_token =
450 crate::auth::verification_token::format_token_for_display(&verification_token);
451 if let Err(e) = crate::comms::enqueue_signup_verification(
452 &state.db,
453 user_id,
454 "email",
455 &new_email,
456 &formatted_token,
457 None,
458 )
459 .await
460 {
461 warn!("Failed to send verification email to new address: {:?}", e);
462 }
463
464 match sqlx::query!(
465 "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, 'email_auth_factor', $2) ON CONFLICT (user_id, name) DO UPDATE SET value_json = $2",
466 user_id,
467 json!(input.email_auth_factor.unwrap_or(false))
468 )
469 .execute(&state.db)
470 .await
471 {
472 Ok(_) => {}
473 Err(e) => warn!("Failed to update email_auth_factor preference: {}", e),
474 }
475
476 info!("Email updated for user {}", user_id);
477 (StatusCode::OK, Json(json!({}))).into_response()
478}
479
480#[derive(Deserialize)]
481pub struct CheckEmailVerifiedInput {
482 pub identifier: String,
483}
484
485pub async fn check_email_verified(
486 State(state): State<AppState>,
487 headers: axum::http::HeaderMap,
488 Json(input): Json<CheckEmailVerifiedInput>,
489) -> Response {
490 let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
491 if !state
492 .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip)
493 .await
494 {
495 return (
496 StatusCode::TOO_MANY_REQUESTS,
497 Json(json!({
498 "error": "RateLimitExceeded",
499 "message": "Too many requests. Please try again later."
500 })),
501 )
502 .into_response();
503 }
504
505 let user = sqlx::query!(
506 "SELECT email_verified FROM users WHERE email = $1 OR handle = $1",
507 input.identifier
508 )
509 .fetch_optional(&state.db)
510 .await;
511
512 match user {
513 Ok(Some(row)) => (
514 StatusCode::OK,
515 Json(json!({ "verified": row.email_verified })),
516 )
517 .into_response(),
518 Ok(None) => (
519 StatusCode::NOT_FOUND,
520 Json(json!({ "error": "AccountNotFound", "message": "Account not found" })),
521 )
522 .into_response(),
523 Err(e) => {
524 error!("DB error checking email verified: {:?}", e);
525 (
526 StatusCode::INTERNAL_SERVER_ERROR,
527 Json(json!({ "error": "InternalError" })),
528 )
529 .into_response()
530 }
531 }
532}