this repo has no description
1use crate::state::{AppState, RateLimitKind};
2use axum::{
3 Json,
4 extract::State,
5 http::{HeaderMap, StatusCode},
6 response::{IntoResponse, Response},
7};
8use bcrypt::{hash, DEFAULT_COST};
9use chrono::{Duration, Utc};
10use serde::Deserialize;
11use serde_json::json;
12use tracing::{error, info, warn};
13
14fn generate_reset_code() -> String {
15 crate::util::generate_token_code()
16}
17
18fn extract_client_ip(headers: &HeaderMap) -> String {
19 if let Some(forwarded) = headers.get("x-forwarded-for") {
20 if let Ok(value) = forwarded.to_str() {
21 if let Some(first_ip) = value.split(',').next() {
22 return first_ip.trim().to_string();
23 }
24 }
25 }
26 if let Some(real_ip) = headers.get("x-real-ip") {
27 if let Ok(value) = real_ip.to_str() {
28 return value.trim().to_string();
29 }
30 }
31 "unknown".to_string()
32}
33
34#[derive(Deserialize)]
35pub struct RequestPasswordResetInput {
36 pub email: String,
37}
38
39pub async fn request_password_reset(
40 State(state): State<AppState>,
41 headers: HeaderMap,
42 Json(input): Json<RequestPasswordResetInput>,
43) -> Response {
44 let client_ip = extract_client_ip(&headers);
45 if !state.check_rate_limit(RateLimitKind::PasswordReset, &client_ip).await {
46 warn!(ip = %client_ip, "Password reset rate limit exceeded");
47 return (
48 StatusCode::TOO_MANY_REQUESTS,
49 Json(json!({
50 "error": "RateLimitExceeded",
51 "message": "Too many password reset requests. Please try again later."
52 })),
53 )
54 .into_response();
55 }
56
57 let email = input.email.trim().to_lowercase();
58 if email.is_empty() {
59 return (
60 StatusCode::BAD_REQUEST,
61 Json(json!({"error": "InvalidRequest", "message": "email is required"})),
62 )
63 .into_response();
64 }
65
66 let user = sqlx::query!("SELECT id FROM users WHERE LOWER(email) = $1", email)
67 .fetch_optional(&state.db)
68 .await;
69
70 let user_id = match user {
71 Ok(Some(row)) => row.id,
72 Ok(None) => {
73 info!("Password reset requested for unknown email");
74 return (StatusCode::OK, Json(json!({}))).into_response();
75 }
76 Err(e) => {
77 error!("DB error in request_password_reset: {:?}", e);
78 return (
79 StatusCode::INTERNAL_SERVER_ERROR,
80 Json(json!({"error": "InternalError"})),
81 )
82 .into_response();
83 }
84 };
85
86 let code = generate_reset_code();
87 let expires_at = Utc::now() + Duration::minutes(10);
88
89 let update = sqlx::query!(
90 "UPDATE users SET password_reset_code = $1, password_reset_code_expires_at = $2 WHERE id = $3",
91 code,
92 expires_at,
93 user_id
94 )
95 .execute(&state.db)
96 .await;
97
98 if let Err(e) = update {
99 error!("DB error setting reset code: {:?}", e);
100 return (
101 StatusCode::INTERNAL_SERVER_ERROR,
102 Json(json!({"error": "InternalError"})),
103 )
104 .into_response();
105 }
106
107 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
108 if let Err(e) =
109 crate::notifications::enqueue_password_reset(&state.db, user_id, &code, &hostname).await
110 {
111 warn!("Failed to enqueue password reset notification: {:?}", e);
112 }
113
114 info!("Password reset requested for user {}", user_id);
115
116 (StatusCode::OK, Json(json!({}))).into_response()
117}
118
119#[derive(Deserialize)]
120pub struct ResetPasswordInput {
121 pub token: String,
122 pub password: String,
123}
124
125pub async fn reset_password(
126 State(state): State<AppState>,
127 headers: HeaderMap,
128 Json(input): Json<ResetPasswordInput>,
129) -> Response {
130 let client_ip = extract_client_ip(&headers);
131 if !state.check_rate_limit(RateLimitKind::ResetPassword, &client_ip).await {
132 warn!(ip = %client_ip, "Reset password rate limit exceeded");
133 return (
134 StatusCode::TOO_MANY_REQUESTS,
135 Json(json!({
136 "error": "RateLimitExceeded",
137 "message": "Too many requests. Please try again later."
138 })),
139 ).into_response();
140 }
141
142 let token = input.token.trim();
143 let password = &input.password;
144
145 if token.is_empty() {
146 return (
147 StatusCode::BAD_REQUEST,
148 Json(json!({"error": "InvalidToken", "message": "token is required"})),
149 )
150 .into_response();
151 }
152
153 if password.is_empty() {
154 return (
155 StatusCode::BAD_REQUEST,
156 Json(json!({"error": "InvalidRequest", "message": "password is required"})),
157 )
158 .into_response();
159 }
160
161 let user = sqlx::query!(
162 "SELECT id, password_reset_code, password_reset_code_expires_at FROM users WHERE password_reset_code = $1",
163 token
164 )
165 .fetch_optional(&state.db)
166 .await;
167
168 let (user_id, expires_at) = match user {
169 Ok(Some(row)) => {
170 let expires = row.password_reset_code_expires_at;
171 (row.id, expires)
172 }
173 Ok(None) => {
174 return (
175 StatusCode::BAD_REQUEST,
176 Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})),
177 )
178 .into_response();
179 }
180 Err(e) => {
181 error!("DB error in reset_password: {:?}", e);
182 return (
183 StatusCode::INTERNAL_SERVER_ERROR,
184 Json(json!({"error": "InternalError"})),
185 )
186 .into_response();
187 }
188 };
189
190 if let Some(exp) = expires_at {
191 if Utc::now() > exp {
192 if let Err(e) = sqlx::query!(
193 "UPDATE users SET password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $1",
194 user_id
195 )
196 .execute(&state.db)
197 .await
198 {
199 error!("Failed to clear expired reset code: {:?}", e);
200 }
201
202 return (
203 StatusCode::BAD_REQUEST,
204 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
205 )
206 .into_response();
207 }
208 } else {
209 return (
210 StatusCode::BAD_REQUEST,
211 Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})),
212 )
213 .into_response();
214 }
215
216 let password_hash = match hash(password, DEFAULT_COST) {
217 Ok(h) => h,
218 Err(e) => {
219 error!("Failed to hash password: {:?}", e);
220 return (
221 StatusCode::INTERNAL_SERVER_ERROR,
222 Json(json!({"error": "InternalError"})),
223 )
224 .into_response();
225 }
226 };
227
228 let mut tx = match state.db.begin().await {
229 Ok(tx) => tx,
230 Err(e) => {
231 error!("Failed to begin transaction: {:?}", e);
232 return (
233 StatusCode::INTERNAL_SERVER_ERROR,
234 Json(json!({"error": "InternalError"})),
235 )
236 .into_response();
237 }
238 };
239
240 if let Err(e) = sqlx::query!(
241 "UPDATE users SET password_hash = $1, password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $2",
242 password_hash,
243 user_id
244 )
245 .execute(&mut *tx)
246 .await
247 {
248 error!("DB error updating password: {:?}", e);
249 return (
250 StatusCode::INTERNAL_SERVER_ERROR,
251 Json(json!({"error": "InternalError"})),
252 )
253 .into_response();
254 }
255
256 let user_did = match sqlx::query_scalar!(
257 "SELECT did FROM users WHERE id = $1",
258 user_id
259 )
260 .fetch_one(&mut *tx)
261 .await
262 {
263 Ok(did) => did,
264 Err(e) => {
265 error!("Failed to get DID for user {}: {:?}", user_id, e);
266 return (
267 StatusCode::INTERNAL_SERVER_ERROR,
268 Json(json!({"error": "InternalError"})),
269 )
270 .into_response();
271 }
272 };
273
274 let session_jtis: Vec<String> = match sqlx::query_scalar!(
275 "SELECT access_jti FROM session_tokens WHERE did = $1",
276 user_did
277 )
278 .fetch_all(&mut *tx)
279 .await
280 {
281 Ok(jtis) => jtis,
282 Err(e) => {
283 error!("Failed to fetch session JTIs: {:?}", e);
284 vec![]
285 }
286 };
287
288 if let Err(e) = sqlx::query!("DELETE FROM session_tokens WHERE did = $1", user_did)
289 .execute(&mut *tx)
290 .await
291 {
292 error!("Failed to invalidate sessions after password reset: {:?}", e);
293 return (
294 StatusCode::INTERNAL_SERVER_ERROR,
295 Json(json!({"error": "InternalError"})),
296 )
297 .into_response();
298 }
299
300 if let Err(e) = tx.commit().await {
301 error!("Failed to commit password reset transaction: {:?}", e);
302 return (
303 StatusCode::INTERNAL_SERVER_ERROR,
304 Json(json!({"error": "InternalError"})),
305 )
306 .into_response();
307 }
308
309 for jti in session_jtis {
310 let cache_key = format!("auth:session:{}:{}", user_did, jti);
311 if let Err(e) = state.cache.delete(&cache_key).await {
312 warn!("Failed to invalidate session cache for {}: {:?}", cache_key, e);
313 }
314 }
315
316 info!("Password reset completed for user {}", user_id);
317
318 (StatusCode::OK, Json(json!({}))).into_response()
319}