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 serde::Deserialize;
10use serde_json::json;
11use tracing::{error, info, warn};
12
13#[derive(Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct RequestEmailUpdateInput {
16 pub email: String,
17}
18
19pub async fn request_email_update(
20 State(state): State<AppState>,
21 headers: axum::http::HeaderMap,
22 Json(input): Json<RequestEmailUpdateInput>,
23) -> Response {
24 let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
25 if !state
26 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip)
27 .await
28 {
29 warn!(ip = %client_ip, "Email update rate limit exceeded");
30 return (
31 StatusCode::TOO_MANY_REQUESTS,
32 Json(json!({
33 "error": "RateLimitExceeded",
34 "message": "Too many requests. Please try again later."
35 })),
36 )
37 .into_response();
38 }
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
53 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
54 let auth_user = match auth_result {
55 Ok(user) => user,
56 Err(e) => return ApiError::from(e).into_response(),
57 };
58
59 if let Err(e) = crate::auth::scope_check::check_account_scope(
60 auth_user.is_oauth,
61 auth_user.scope.as_deref(),
62 crate::oauth::scopes::AccountAttr::Email,
63 crate::oauth::scopes::AccountAction::Manage,
64 ) {
65 return e;
66 }
67
68 let did = auth_user.did.clone();
69 let user = match sqlx::query!("SELECT id, handle, email FROM users WHERE did = $1", did)
70 .fetch_optional(&state.db)
71 .await
72 {
73 Ok(Some(row)) => row,
74 _ => {
75 return (
76 StatusCode::INTERNAL_SERVER_ERROR,
77 Json(json!({"error": "InternalError"})),
78 )
79 .into_response();
80 }
81 };
82
83 let user_id = user.id;
84 let handle = user.handle;
85 let current_email = user.email;
86 let email = input.email.trim().to_lowercase();
87
88 if !crate::api::validation::is_valid_email(&email) {
89 return (
90 StatusCode::BAD_REQUEST,
91 Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})),
92 )
93 .into_response();
94 }
95
96 if current_email.as_ref().map(|e| e.to_lowercase()) == Some(email.clone()) {
97 return (StatusCode::OK, Json(json!({ "tokenRequired": false }))).into_response();
98 }
99
100 let exists = sqlx::query!(
101 "SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2",
102 email,
103 user_id
104 )
105 .fetch_optional(&state.db)
106 .await;
107
108 if let Ok(Some(_)) = exists {
109 return (
110 StatusCode::BAD_REQUEST,
111 Json(json!({"error": "EmailTaken", "message": "Email already taken"})),
112 )
113 .into_response();
114 }
115
116 if let Err(e) = crate::api::notification_prefs::request_channel_verification(
117 &state.db,
118 user_id,
119 &did,
120 "email",
121 &email,
122 Some(&handle),
123 )
124 .await
125 {
126 error!("Failed to request email verification: {}", e);
127 return (
128 StatusCode::INTERNAL_SERVER_ERROR,
129 Json(json!({"error": "InternalError"})),
130 )
131 .into_response();
132 }
133
134 info!("Email update requested for user {}", user_id);
135 (StatusCode::OK, Json(json!({ "tokenRequired": true }))).into_response()
136}
137
138#[derive(Deserialize)]
139#[serde(rename_all = "camelCase")]
140pub struct ConfirmEmailInput {
141 pub email: String,
142 pub token: String,
143}
144
145pub async fn confirm_email(
146 State(state): State<AppState>,
147 headers: axum::http::HeaderMap,
148 Json(input): Json<ConfirmEmailInput>,
149) -> Response {
150 let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
151 if !state
152 .check_rate_limit(RateLimitKind::AppPassword, &client_ip)
153 .await
154 {
155 warn!(ip = %client_ip, "Confirm email rate limit exceeded");
156 return (
157 StatusCode::TOO_MANY_REQUESTS,
158 Json(json!({
159 "error": "RateLimitExceeded",
160 "message": "Too many requests. Please try again later."
161 })),
162 )
163 .into_response();
164 }
165
166 let token = match crate::auth::extract_bearer_token_from_header(
167 headers.get("Authorization").and_then(|h| h.to_str().ok()),
168 ) {
169 Some(t) => t,
170 None => {
171 return (
172 StatusCode::UNAUTHORIZED,
173 Json(json!({"error": "AuthenticationRequired"})),
174 )
175 .into_response();
176 }
177 };
178
179 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
180 let auth_user = match auth_result {
181 Ok(user) => user,
182 Err(e) => return ApiError::from(e).into_response(),
183 };
184
185 if let Err(e) = crate::auth::scope_check::check_account_scope(
186 auth_user.is_oauth,
187 auth_user.scope.as_deref(),
188 crate::oauth::scopes::AccountAttr::Email,
189 crate::oauth::scopes::AccountAction::Manage,
190 ) {
191 return e;
192 }
193
194 let did = auth_user.did;
195 let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
196 .fetch_one(&state.db)
197 .await
198 {
199 Ok(id) => id,
200 Err(_) => {
201 return (
202 StatusCode::INTERNAL_SERVER_ERROR,
203 Json(json!({"error": "InternalError"})),
204 )
205 .into_response();
206 }
207 };
208
209 let email = input.email.trim().to_lowercase();
210 let confirmation_code =
211 crate::auth::verification_token::normalize_token_input(input.token.trim());
212
213 let verified = crate::auth::verification_token::verify_channel_update_token(
214 &confirmation_code,
215 "email",
216 &email,
217 );
218
219 match verified {
220 Ok(token_data) => {
221 if token_data.did != did {
222 return (
223 StatusCode::BAD_REQUEST,
224 Json(
225 json!({"error": "InvalidToken", "message": "Token does not match account"}),
226 ),
227 )
228 .into_response();
229 }
230 }
231 Err(crate::auth::verification_token::VerifyError::Expired) => {
232 return (
233 StatusCode::BAD_REQUEST,
234 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
235 )
236 .into_response();
237 }
238 Err(_) => {
239 return (
240 StatusCode::BAD_REQUEST,
241 Json(json!({"error": "InvalidToken", "message": "Invalid token"})),
242 )
243 .into_response();
244 }
245 }
246
247 let update = sqlx::query!(
248 "UPDATE users SET email = $1, email_verified = TRUE, updated_at = NOW() WHERE id = $2",
249 email,
250 user_id
251 )
252 .execute(&state.db)
253 .await;
254
255 if let Err(e) = update {
256 error!("DB error finalizing email update: {:?}", e);
257 if e.as_database_error()
258 .map(|db_err| db_err.is_unique_violation())
259 .unwrap_or(false)
260 {
261 return (
262 StatusCode::BAD_REQUEST,
263 Json(json!({"error": "EmailTaken", "message": "Email already taken"})),
264 )
265 .into_response();
266 }
267 return (
268 StatusCode::INTERNAL_SERVER_ERROR,
269 Json(json!({"error": "InternalError"})),
270 )
271 .into_response();
272 }
273
274 info!("Email updated for user {}", user_id);
275 (StatusCode::OK, Json(json!({}))).into_response()
276}
277
278#[derive(Deserialize)]
279#[serde(rename_all = "camelCase")]
280pub struct UpdateEmailInput {
281 pub email: String,
282 #[serde(default)]
283 pub email_auth_factor: Option<bool>,
284 pub token: Option<String>,
285}
286
287pub async fn update_email(
288 State(state): State<AppState>,
289 headers: axum::http::HeaderMap,
290 Json(input): Json<UpdateEmailInput>,
291) -> Response {
292 let token = match crate::auth::extract_bearer_token_from_header(
293 headers.get("Authorization").and_then(|h| h.to_str().ok()),
294 ) {
295 Some(t) => t,
296 None => {
297 return (
298 StatusCode::UNAUTHORIZED,
299 Json(json!({"error": "AuthenticationRequired"})),
300 )
301 .into_response();
302 }
303 };
304
305 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
306 let auth_user = match auth_result {
307 Ok(user) => user,
308 Err(e) => return ApiError::from(e).into_response(),
309 };
310
311 if let Err(e) = crate::auth::scope_check::check_account_scope(
312 auth_user.is_oauth,
313 auth_user.scope.as_deref(),
314 crate::oauth::scopes::AccountAttr::Email,
315 crate::oauth::scopes::AccountAction::Manage,
316 ) {
317 return e;
318 }
319
320 let did = auth_user.did;
321 let user = match sqlx::query!("SELECT id, email FROM users WHERE did = $1", did)
322 .fetch_optional(&state.db)
323 .await
324 {
325 Ok(Some(row)) => row,
326 _ => {
327 return (
328 StatusCode::INTERNAL_SERVER_ERROR,
329 Json(json!({"error": "InternalError"})),
330 )
331 .into_response();
332 }
333 };
334
335 let user_id = user.id;
336 let current_email = user.email;
337 let new_email = input.email.trim().to_lowercase();
338
339 if !crate::api::validation::is_valid_email(&new_email) {
340 return (
341 StatusCode::BAD_REQUEST,
342 Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})),
343 )
344 .into_response();
345 }
346
347 if let Some(ref current) = current_email
348 && new_email == current.to_lowercase()
349 {
350 return (StatusCode::OK, Json(json!({}))).into_response();
351 }
352
353 let confirmation_token = match &input.token {
354 Some(t) => crate::auth::verification_token::normalize_token_input(t.trim()),
355 None => {
356 return (
357 StatusCode::BAD_REQUEST,
358 Json(json!({"error": "TokenRequired", "message": "Token required. Call requestEmailUpdate first."})),
359 )
360 .into_response();
361 }
362 };
363
364 let verified = crate::auth::verification_token::verify_channel_update_token(
365 &confirmation_token,
366 "email",
367 &new_email,
368 );
369
370 match verified {
371 Ok(token_data) => {
372 if token_data.did != did {
373 return (
374 StatusCode::BAD_REQUEST,
375 Json(
376 json!({"error": "InvalidToken", "message": "Token does not match account"}),
377 ),
378 )
379 .into_response();
380 }
381 }
382 Err(crate::auth::verification_token::VerifyError::Expired) => {
383 return (
384 StatusCode::BAD_REQUEST,
385 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
386 )
387 .into_response();
388 }
389 Err(_) => {
390 return (
391 StatusCode::BAD_REQUEST,
392 Json(json!({"error": "InvalidToken", "message": "Invalid token"})),
393 )
394 .into_response();
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!({"error": "InvalidRequest", "message": "Email already in use"})),
410 )
411 .into_response();
412 }
413
414 let update = sqlx::query!(
415 "UPDATE users SET email = $1, email_verified = TRUE, updated_at = NOW() WHERE id = $2",
416 new_email,
417 user_id
418 )
419 .execute(&state.db)
420 .await;
421
422 if let Err(e) = update {
423 error!("DB error finalizing email update: {:?}", e);
424 if e.as_database_error()
425 .map(|db_err| db_err.is_unique_violation())
426 .unwrap_or(false)
427 {
428 return (
429 StatusCode::BAD_REQUEST,
430 Json(json!({"error": "InvalidRequest", "message": "Email already in use"})),
431 )
432 .into_response();
433 }
434 return (
435 StatusCode::INTERNAL_SERVER_ERROR,
436 Json(json!({"error": "InternalError"})),
437 )
438 .into_response();
439 }
440
441 match sqlx::query!(
442 "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",
443 user_id,
444 json!(input.email_auth_factor.unwrap_or(false))
445 )
446 .execute(&state.db)
447 .await
448 {
449 Ok(_) => {}
450 Err(e) => warn!("Failed to update email_auth_factor preference: {}", e),
451 }
452
453 info!("Email updated for user {}", user_id);
454 (StatusCode::OK, Json(json!({}))).into_response()
455}