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 auth: BearerAuth,
197 Json(input): Json<UpdateEmailInput>,
198) -> Response {
199 let auth_user = auth.0;
200
201 if let Err(e) = crate::auth::scope_check::check_account_scope(
202 auth_user.is_oauth,
203 auth_user.scope.as_deref(),
204 crate::oauth::scopes::AccountAttr::Email,
205 crate::oauth::scopes::AccountAction::Manage,
206 ) {
207 return e;
208 }
209
210 let did = auth_user.did.to_string();
211 let user = match sqlx::query!(
212 "SELECT id, email, email_verified FROM users WHERE did = $1",
213 did
214 )
215 .fetch_optional(&state.db)
216 .await
217 {
218 Ok(Some(row)) => row,
219 Ok(None) => {
220 return ApiError::AccountNotFound.into_response();
221 }
222 Err(e) => {
223 error!("DB error: {:?}", e);
224 return ApiError::InternalError(None).into_response();
225 }
226 };
227
228 let user_id = user.id;
229 let current_email = user.email.clone();
230 let email_verified = user.email_verified;
231 let new_email = input.email.trim().to_lowercase();
232
233 if !crate::api::validation::is_valid_email(&new_email) {
234 return ApiError::InvalidRequest(
235 "This email address is not supported, please use a different email.".into(),
236 )
237 .into_response();
238 }
239
240 if let Some(ref current) = current_email
241 && new_email == current.to_lowercase()
242 {
243 return EmptyResponse::ok().into_response();
244 }
245
246 if email_verified {
247 let Some(ref t) = input.token else {
248 return ApiError::TokenRequired.into_response();
249 };
250 let confirmation_token = crate::auth::verification_token::normalize_token_input(t.trim());
251
252 let current_email_lower = current_email
253 .as_ref()
254 .map(|e| e.to_lowercase())
255 .unwrap_or_default();
256
257 let verified = crate::auth::verification_token::verify_channel_update_token(
258 &confirmation_token,
259 "email_update",
260 ¤t_email_lower,
261 );
262
263 match verified {
264 Ok(token_data) => {
265 if token_data.did != did {
266 return ApiError::InvalidToken(None).into_response();
267 }
268 }
269 Err(crate::auth::verification_token::VerifyError::Expired) => {
270 return ApiError::ExpiredToken(None).into_response();
271 }
272 Err(_) => {
273 return ApiError::InvalidToken(None).into_response();
274 }
275 }
276 }
277
278 let exists = sqlx::query!(
279 "SELECT 1 as one FROM users WHERE LOWER(email) = $1 AND id != $2",
280 new_email,
281 user_id
282 )
283 .fetch_optional(&state.db)
284 .await;
285
286 if let Ok(Some(_)) = exists {
287 return ApiError::InvalidRequest("Email is already in use".into()).into_response();
288 }
289
290 let update: Result<sqlx::postgres::PgQueryResult, sqlx::Error> = sqlx::query!(
291 "UPDATE users SET email = $1, email_verified = FALSE, updated_at = NOW() WHERE id = $2",
292 new_email,
293 user_id
294 )
295 .execute(&state.db)
296 .await;
297
298 if let Err(e) = update {
299 error!("DB error updating email: {:?}", e);
300 if e.as_database_error()
301 .map(|db_err: &dyn sqlx::error::DatabaseError| db_err.is_unique_violation())
302 .unwrap_or(false)
303 {
304 return ApiError::EmailTaken.into_response();
305 }
306 return ApiError::InternalError(None).into_response();
307 }
308
309 let verification_token =
310 crate::auth::verification_token::generate_signup_token(&did, "email", &new_email);
311 let formatted_token =
312 crate::auth::verification_token::format_token_for_display(&verification_token);
313 if let Err(e) = crate::comms::enqueue_signup_verification(
314 &state.db,
315 user_id,
316 "email",
317 &new_email,
318 &formatted_token,
319 None,
320 )
321 .await
322 {
323 warn!("Failed to send verification email to new address: {:?}", e);
324 }
325
326 match sqlx::query!(
327 "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",
328 user_id,
329 json!(input.email_auth_factor.unwrap_or(false))
330 )
331 .execute(&state.db)
332 .await
333 {
334 Ok(_) => {}
335 Err(e) => warn!("Failed to update email_auth_factor preference: {}", e),
336 }
337
338 info!("Email updated for user {}", user_id);
339 EmptyResponse::ok().into_response()
340}
341
342#[derive(Deserialize)]
343pub struct CheckEmailVerifiedInput {
344 pub identifier: String,
345}
346
347pub async fn check_email_verified(
348 State(state): State<AppState>,
349 headers: axum::http::HeaderMap,
350 Json(input): Json<CheckEmailVerifiedInput>,
351) -> Response {
352 let client_ip = crate::rate_limit::extract_client_ip(&headers, None);
353 if !state
354 .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip)
355 .await
356 {
357 return ApiError::RateLimitExceeded(None).into_response();
358 }
359
360 let user = sqlx::query!(
361 "SELECT email_verified FROM users WHERE email = $1 OR handle = $1",
362 input.identifier
363 )
364 .fetch_optional(&state.db)
365 .await;
366
367 match user {
368 Ok(Some(row)) => VerifiedResponse::response(row.email_verified).into_response(),
369 Ok(None) => ApiError::AccountNotFound.into_response(),
370 Err(e) => {
371 error!("DB error checking email verified: {:?}", e);
372 ApiError::InternalError(None).into_response()
373 }
374 }
375}