this repo has no description
1use crate::api::error::ApiError;
2use crate::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse};
3use crate::auth::BearerAuth;
4use crate::state::{AppState, RateLimitKind};
5use axum::{
6 Json,
7 extract::State,
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 ApiError::RateLimitExceeded(None).into_response();
26 }
27
28 if let Err(e) = crate::auth::scope_check::check_account_scope(
29 auth.0.is_oauth,
30 auth.0.scope.as_deref(),
31 crate::oauth::scopes::AccountAttr::Email,
32 crate::oauth::scopes::AccountAction::Manage,
33 ) {
34 return e;
35 }
36
37 let did = auth.0.did.to_string();
38 let user = match sqlx::query!(
39 "SELECT id, handle, email, email_verified FROM users WHERE did = $1",
40 did
41 )
42 .fetch_optional(&state.db)
43 .await
44 {
45 Ok(Some(row)) => row,
46 Ok(None) => {
47 return ApiError::AccountNotFound.into_response();
48 }
49 Err(e) => {
50 error!("DB error: {:?}", e);
51 return ApiError::InternalError(None).into_response();
52 }
53 };
54
55 let Some(current_email) = user.email else {
56 return ApiError::InvalidRequest("account does not have an email address".into())
57 .into_response();
58 };
59
60 let token_required = user.email_verified;
61
62 if token_required {
63 let code = crate::auth::verification_token::generate_channel_update_token(
64 &did,
65 "email_update",
66 ¤t_email.to_lowercase(),
67 );
68 let formatted_code = crate::auth::verification_token::format_token_for_display(&code);
69
70 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
71 if let Err(e) =
72 crate::comms::enqueue_email_update_token(&state.db, user.id, &formatted_code, &hostname)
73 .await
74 {
75 warn!("Failed to enqueue email update notification: {:?}", e);
76 }
77 }
78
79 info!("Email update requested for user {}", user.id);
80 TokenRequiredResponse::response(token_required).into_response()
81}
82
83#[derive(Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct ConfirmEmailInput {
86 pub email: String,
87 pub token: String,
88}
89
90pub async fn confirm_email(
91 State(state): State<AppState>,
92 headers: axum::http::HeaderMap,
93 auth: BearerAuth,
94 Json(input): Json<ConfirmEmailInput>,
95) -> Response {
96 let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
97 if !state
98 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip)
99 .await
100 {
101 warn!(ip = %client_ip, "Confirm email rate limit exceeded");
102 return ApiError::RateLimitExceeded(None).into_response();
103 }
104
105 if let Err(e) = crate::auth::scope_check::check_account_scope(
106 auth.0.is_oauth,
107 auth.0.scope.as_deref(),
108 crate::oauth::scopes::AccountAttr::Email,
109 crate::oauth::scopes::AccountAction::Manage,
110 ) {
111 return e;
112 }
113
114 let did = auth.0.did.to_string();
115 let user = match sqlx::query!(
116 "SELECT id, email, email_verified FROM users WHERE did = $1",
117 did
118 )
119 .fetch_optional(&state.db)
120 .await
121 {
122 Ok(Some(row)) => row,
123 Ok(None) => {
124 return ApiError::AccountNotFound.into_response();
125 }
126 Err(e) => {
127 error!("DB error: {:?}", e);
128 return ApiError::InternalError(None).into_response();
129 }
130 };
131
132 let Some(ref email) = user.email else {
133 return ApiError::InvalidEmail.into_response();
134 };
135 let current_email = email.to_lowercase();
136
137 let provided_email = input.email.trim().to_lowercase();
138 if provided_email != current_email {
139 return ApiError::InvalidEmail.into_response();
140 }
141
142 if user.email_verified {
143 return EmptyResponse::ok().into_response();
144 }
145
146 let confirmation_code =
147 crate::auth::verification_token::normalize_token_input(input.token.trim());
148
149 let verified = crate::auth::verification_token::verify_signup_token(
150 &confirmation_code,
151 "email",
152 &provided_email,
153 );
154
155 match verified {
156 Ok(token_data) => {
157 if token_data.did != did {
158 return ApiError::InvalidToken(None).into_response();
159 }
160 }
161 Err(crate::auth::verification_token::VerifyError::Expired) => {
162 return ApiError::ExpiredToken(None).into_response();
163 }
164 Err(_) => {
165 return ApiError::InvalidToken(None).into_response();
166 }
167 }
168
169 let update = sqlx::query!(
170 "UPDATE users SET email_verified = TRUE, updated_at = NOW() WHERE id = $1",
171 user.id
172 )
173 .execute(&state.db)
174 .await;
175
176 if let Err(e) = update {
177 error!("DB error confirming email: {:?}", e);
178 return ApiError::InternalError(None).into_response();
179 }
180
181 info!("Email confirmed for user {}", user.id);
182 EmptyResponse::ok().into_response()
183}
184
185#[derive(Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct UpdateEmailInput {
188 pub email: String,
189 #[serde(default)]
190 pub email_auth_factor: Option<bool>,
191 pub token: Option<String>,
192}
193
194pub async fn update_email(
195 State(state): State<AppState>,
196 headers: axum::http::HeaderMap,
197 Json(input): Json<UpdateEmailInput>,
198) -> Response {
199 let Some(bearer_token) = crate::auth::extract_bearer_token_from_header(
200 headers.get("Authorization").and_then(|h| h.to_str().ok()),
201 ) else {
202 return ApiError::AuthenticationRequired.into_response();
203 };
204
205 let auth_result = crate::auth::validate_bearer_token(&state.db, &bearer_token).await;
206 let auth_user = match auth_result {
207 Ok(user) => user,
208 Err(e) => return ApiError::from(e).into_response(),
209 };
210
211 if let Err(e) = crate::auth::scope_check::check_account_scope(
212 auth_user.is_oauth,
213 auth_user.scope.as_deref(),
214 crate::oauth::scopes::AccountAttr::Email,
215 crate::oauth::scopes::AccountAction::Manage,
216 ) {
217 return e;
218 }
219
220 let did = auth_user.did.to_string();
221 let user = match sqlx::query!(
222 "SELECT id, email, email_verified FROM users WHERE did = $1",
223 did
224 )
225 .fetch_optional(&state.db)
226 .await
227 {
228 Ok(Some(row)) => row,
229 Ok(None) => {
230 return ApiError::AccountNotFound.into_response();
231 }
232 Err(e) => {
233 error!("DB error: {:?}", e);
234 return ApiError::InternalError(None).into_response();
235 }
236 };
237
238 let user_id = user.id;
239 let current_email = user.email.clone();
240 let email_verified = user.email_verified;
241 let new_email = input.email.trim().to_lowercase();
242
243 if !crate::api::validation::is_valid_email(&new_email) {
244 return ApiError::InvalidRequest(
245 "This email address is not supported, please use a different email.".into(),
246 )
247 .into_response();
248 }
249
250 if let Some(ref current) = current_email
251 && new_email == current.to_lowercase()
252 {
253 return EmptyResponse::ok().into_response();
254 }
255
256 if email_verified {
257 let Some(ref t) = input.token else {
258 return ApiError::TokenRequired.into_response();
259 };
260 let confirmation_token = crate::auth::verification_token::normalize_token_input(t.trim());
261
262 let current_email_lower = current_email
263 .as_ref()
264 .map(|e| e.to_lowercase())
265 .unwrap_or_default();
266
267 let verified = crate::auth::verification_token::verify_channel_update_token(
268 &confirmation_token,
269 "email_update",
270 ¤t_email_lower,
271 );
272
273 match verified {
274 Ok(token_data) => {
275 if token_data.did != did {
276 return ApiError::InvalidToken(None).into_response();
277 }
278 }
279 Err(crate::auth::verification_token::VerifyError::Expired) => {
280 return ApiError::ExpiredToken(None).into_response();
281 }
282 Err(_) => {
283 return ApiError::InvalidToken(None).into_response();
284 }
285 }
286 }
287
288 let exists = sqlx::query!(
289 "SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2",
290 new_email,
291 user_id
292 )
293 .fetch_optional(&state.db)
294 .await;
295
296 if let Ok(Some(_)) = exists {
297 return ApiError::InvalidRequest("Email is already in use".into()).into_response();
298 }
299
300 let update: Result<sqlx::postgres::PgQueryResult, sqlx::Error> = sqlx::query!(
301 "UPDATE users SET email = $1, email_verified = FALSE, updated_at = NOW() WHERE id = $2",
302 new_email,
303 user_id
304 )
305 .execute(&state.db)
306 .await;
307
308 if let Err(e) = update {
309 error!("DB error updating email: {:?}", e);
310 if e.as_database_error()
311 .map(|db_err: &dyn sqlx::error::DatabaseError| db_err.is_unique_violation())
312 .unwrap_or(false)
313 {
314 return ApiError::EmailTaken.into_response();
315 }
316 return ApiError::InternalError(None).into_response();
317 }
318
319 let verification_token =
320 crate::auth::verification_token::generate_signup_token(&did, "email", &new_email);
321 let formatted_token =
322 crate::auth::verification_token::format_token_for_display(&verification_token);
323 if let Err(e) = crate::comms::enqueue_signup_verification(
324 &state.db,
325 user_id,
326 "email",
327 &new_email,
328 &formatted_token,
329 None,
330 )
331 .await
332 {
333 warn!("Failed to send verification email to new address: {:?}", e);
334 }
335
336 match sqlx::query!(
337 "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",
338 user_id,
339 json!(input.email_auth_factor.unwrap_or(false))
340 )
341 .execute(&state.db)
342 .await
343 {
344 Ok(_) => {}
345 Err(e) => warn!("Failed to update email_auth_factor preference: {}", e),
346 }
347
348 info!("Email updated for user {}", user_id);
349 EmptyResponse::ok().into_response()
350}
351
352#[derive(Deserialize)]
353pub struct CheckEmailVerifiedInput {
354 pub identifier: String,
355}
356
357pub async fn check_email_verified(
358 State(state): State<AppState>,
359 headers: axum::http::HeaderMap,
360 Json(input): Json<CheckEmailVerifiedInput>,
361) -> Response {
362 let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
363 if !state
364 .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip)
365 .await
366 {
367 return ApiError::RateLimitExceeded(None).into_response();
368 }
369
370 let user = sqlx::query!(
371 "SELECT email_verified FROM users WHERE email = $1 OR handle = $1",
372 input.identifier
373 )
374 .fetch_optional(&state.db)
375 .await;
376
377 match user {
378 Ok(Some(row)) => VerifiedResponse::response(row.email_verified).into_response(),
379 Ok(None) => ApiError::AccountNotFound.into_response(),
380 Err(e) => {
381 error!("DB error checking email verified: {:?}", e);
382 ApiError::InternalError(None).into_response()
383 }
384 }
385}