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 =
90 crate::auth::verification_token::format_token_for_display(&code);
91
92 let hostname =
93 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
94 if let Err(e) = crate::comms::enqueue_email_update_token(
95 &state.db,
96 user.id,
97 &formatted_code,
98 &hostname,
99 )
100 .await
101 {
102 warn!("Failed to enqueue email update notification: {:?}", e);
103 }
104 }
105
106 info!("Email update requested for user {}", user.id);
107 (StatusCode::OK, Json(json!({ "tokenRequired": token_required }))).into_response()
108}
109
110#[derive(Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct ConfirmEmailInput {
113 pub email: String,
114 pub token: String,
115}
116
117pub async fn confirm_email(
118 State(state): State<AppState>,
119 headers: axum::http::HeaderMap,
120 auth: BearerAuth,
121 Json(input): Json<ConfirmEmailInput>,
122) -> Response {
123 let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
124 if !state
125 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip)
126 .await
127 {
128 warn!(ip = %client_ip, "Confirm email rate limit exceeded");
129 return (
130 StatusCode::TOO_MANY_REQUESTS,
131 Json(json!({
132 "error": "RateLimitExceeded",
133 "message": "Too many requests. Please try again later."
134 })),
135 )
136 .into_response();
137 }
138
139 if let Err(e) = crate::auth::scope_check::check_account_scope(
140 auth.0.is_oauth,
141 auth.0.scope.as_deref(),
142 crate::oauth::scopes::AccountAttr::Email,
143 crate::oauth::scopes::AccountAction::Manage,
144 ) {
145 return e;
146 }
147
148 let did = auth.0.did;
149 let user = match sqlx::query!(
150 "SELECT id, email, email_verified FROM users WHERE did = $1",
151 did
152 )
153 .fetch_optional(&state.db)
154 .await
155 {
156 Ok(Some(row)) => row,
157 Ok(None) => {
158 return (
159 StatusCode::BAD_REQUEST,
160 Json(json!({"error": "AccountNotFound", "message": "user not found"})),
161 )
162 .into_response();
163 }
164 Err(e) => {
165 error!("DB error: {:?}", e);
166 return (
167 StatusCode::INTERNAL_SERVER_ERROR,
168 Json(json!({"error": "InternalError"})),
169 )
170 .into_response();
171 }
172 };
173
174 let current_email = match &user.email {
175 Some(e) => e.to_lowercase(),
176 None => {
177 return (
178 StatusCode::BAD_REQUEST,
179 Json(json!({"error": "InvalidEmail", "message": "account does not have an email address"})),
180 )
181 .into_response();
182 }
183 };
184
185 let provided_email = input.email.trim().to_lowercase();
186 if provided_email != current_email {
187 return (
188 StatusCode::BAD_REQUEST,
189 Json(json!({"error": "InvalidEmail", "message": "invalid email"})),
190 )
191 .into_response();
192 }
193
194 if user.email_verified {
195 return (StatusCode::OK, Json(json!({}))).into_response();
196 }
197
198 let confirmation_code =
199 crate::auth::verification_token::normalize_token_input(input.token.trim());
200
201 let verified = crate::auth::verification_token::verify_signup_token(
202 &confirmation_code,
203 "email",
204 &provided_email,
205 );
206
207 match verified {
208 Ok(token_data) => {
209 if token_data.did != did {
210 return (
211 StatusCode::BAD_REQUEST,
212 Json(
213 json!({"error": "InvalidToken", "message": "Token does not match account"}),
214 ),
215 )
216 .into_response();
217 }
218 }
219 Err(crate::auth::verification_token::VerifyError::Expired) => {
220 return (
221 StatusCode::BAD_REQUEST,
222 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
223 )
224 .into_response();
225 }
226 Err(_) => {
227 return (
228 StatusCode::BAD_REQUEST,
229 Json(json!({"error": "InvalidToken", "message": "Invalid token"})),
230 )
231 .into_response();
232 }
233 }
234
235 let update = sqlx::query!(
236 "UPDATE users SET email_verified = TRUE, updated_at = NOW() WHERE id = $1",
237 user.id
238 )
239 .execute(&state.db)
240 .await;
241
242 if let Err(e) = update {
243 error!("DB error confirming email: {:?}", e);
244 return (
245 StatusCode::INTERNAL_SERVER_ERROR,
246 Json(json!({"error": "InternalError"})),
247 )
248 .into_response();
249 }
250
251 info!("Email confirmed for user {}", user.id);
252 (StatusCode::OK, Json(json!({}))).into_response()
253}
254
255#[derive(Deserialize)]
256#[serde(rename_all = "camelCase")]
257pub struct UpdateEmailInput {
258 pub email: String,
259 #[serde(default)]
260 pub email_auth_factor: Option<bool>,
261 pub token: Option<String>,
262}
263
264pub async fn update_email(
265 State(state): State<AppState>,
266 headers: axum::http::HeaderMap,
267 Json(input): Json<UpdateEmailInput>,
268) -> Response {
269 let bearer_token = match crate::auth::extract_bearer_token_from_header(
270 headers.get("Authorization").and_then(|h| h.to_str().ok()),
271 ) {
272 Some(t) => t,
273 None => {
274 return (
275 StatusCode::UNAUTHORIZED,
276 Json(json!({"error": "AuthenticationRequired"})),
277 )
278 .into_response();
279 }
280 };
281
282 let auth_result = crate::auth::validate_bearer_token(&state.db, &bearer_token).await;
283 let auth_user = match auth_result {
284 Ok(user) => user,
285 Err(e) => return ApiError::from(e).into_response(),
286 };
287
288 if let Err(e) = crate::auth::scope_check::check_account_scope(
289 auth_user.is_oauth,
290 auth_user.scope.as_deref(),
291 crate::oauth::scopes::AccountAttr::Email,
292 crate::oauth::scopes::AccountAction::Manage,
293 ) {
294 return e;
295 }
296
297 let did = auth_user.did;
298 let user = match sqlx::query!(
299 "SELECT id, email, email_verified FROM users WHERE did = $1",
300 did
301 )
302 .fetch_optional(&state.db)
303 .await
304 {
305 Ok(Some(row)) => row,
306 Ok(None) => {
307 return (
308 StatusCode::BAD_REQUEST,
309 Json(json!({"error": "InvalidRequest", "message": "account not found"})),
310 )
311 .into_response();
312 }
313 Err(e) => {
314 error!("DB error: {:?}", e);
315 return (
316 StatusCode::INTERNAL_SERVER_ERROR,
317 Json(json!({"error": "InternalError"})),
318 )
319 .into_response();
320 }
321 };
322
323 let user_id = user.id;
324 let current_email = user.email.clone();
325 let email_verified = user.email_verified;
326 let new_email = input.email.trim().to_lowercase();
327
328 if !crate::api::validation::is_valid_email(&new_email) {
329 return (
330 StatusCode::BAD_REQUEST,
331 Json(json!({
332 "error": "InvalidRequest",
333 "message": "This email address is not supported, please use a different email."
334 })),
335 )
336 .into_response();
337 }
338
339 if let Some(ref current) = current_email
340 && new_email == current.to_lowercase()
341 {
342 return (StatusCode::OK, Json(json!({}))).into_response();
343 }
344
345 if email_verified {
346 let confirmation_token = match &input.token {
347 Some(t) => crate::auth::verification_token::normalize_token_input(t.trim()),
348 None => {
349 return (
350 StatusCode::BAD_REQUEST,
351 Json(json!({
352 "error": "TokenRequired",
353 "message": "confirmation token required"
354 })),
355 )
356 .into_response();
357 }
358 };
359
360 let current_email_lower = current_email
361 .as_ref()
362 .map(|e| e.to_lowercase())
363 .unwrap_or_default();
364
365 let verified = crate::auth::verification_token::verify_channel_update_token(
366 &confirmation_token,
367 "email_update",
368 ¤t_email_lower,
369 );
370
371 match verified {
372 Ok(token_data) => {
373 if token_data.did != did {
374 return (
375 StatusCode::BAD_REQUEST,
376 Json(
377 json!({"error": "InvalidToken", "message": "Token does not match account"}),
378 ),
379 )
380 .into_response();
381 }
382 }
383 Err(crate::auth::verification_token::VerifyError::Expired) => {
384 return (
385 StatusCode::BAD_REQUEST,
386 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
387 )
388 .into_response();
389 }
390 Err(_) => {
391 return (
392 StatusCode::BAD_REQUEST,
393 Json(json!({"error": "InvalidToken", "message": "Invalid token"})),
394 )
395 .into_response();
396 }
397 }
398 }
399
400 let exists = sqlx::query!(
401 "SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2",
402 new_email,
403 user_id
404 )
405 .fetch_optional(&state.db)
406 .await;
407
408 if let Ok(Some(_)) = exists {
409 return (
410 StatusCode::BAD_REQUEST,
411 Json(json!({
412 "error": "InvalidRequest",
413 "message": "This email address is already in use, please use a different email."
414 })),
415 )
416 .into_response();
417 }
418
419 let update: Result<sqlx::postgres::PgQueryResult, sqlx::Error> = sqlx::query!(
420 "UPDATE users SET email = $1, email_verified = FALSE, updated_at = NOW() WHERE id = $2",
421 new_email,
422 user_id
423 )
424 .execute(&state.db)
425 .await;
426
427 if let Err(e) = update {
428 error!("DB error updating email: {:?}", e);
429 if e.as_database_error()
430 .map(|db_err: &dyn sqlx::error::DatabaseError| db_err.is_unique_violation())
431 .unwrap_or(false)
432 {
433 return (
434 StatusCode::BAD_REQUEST,
435 Json(json!({
436 "error": "InvalidRequest",
437 "message": "This email address is already in use, please use a different email."
438 })),
439 )
440 .into_response();
441 }
442 return (
443 StatusCode::INTERNAL_SERVER_ERROR,
444 Json(json!({"error": "InternalError"})),
445 )
446 .into_response();
447 }
448
449 let verification_token =
450 crate::auth::verification_token::generate_signup_token(&did, "email", &new_email);
451 let formatted_token =
452 crate::auth::verification_token::format_token_for_display(&verification_token);
453 if let Err(e) = crate::comms::enqueue_signup_verification(
454 &state.db,
455 user_id,
456 "email",
457 &new_email,
458 &formatted_token,
459 None,
460 )
461 .await
462 {
463 warn!("Failed to send verification email to new address: {:?}", e);
464 }
465
466 match sqlx::query!(
467 "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",
468 user_id,
469 json!(input.email_auth_factor.unwrap_or(false))
470 )
471 .execute(&state.db)
472 .await
473 {
474 Ok(_) => {}
475 Err(e) => warn!("Failed to update email_auth_factor preference: {}", e),
476 }
477
478 info!("Email updated for user {}", user_id);
479 (StatusCode::OK, Json(json!({}))).into_response()
480}