this repo has no description
1use crate::state::AppState;
2use axum::{
3 Json,
4 extract::State,
5 http::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
18#[derive(Deserialize)]
19pub struct RequestPasswordResetInput {
20 pub email: String,
21}
22
23pub async fn request_password_reset(
24 State(state): State<AppState>,
25 Json(input): Json<RequestPasswordResetInput>,
26) -> Response {
27 let email = input.email.trim().to_lowercase();
28 if email.is_empty() {
29 return (
30 StatusCode::BAD_REQUEST,
31 Json(json!({"error": "InvalidRequest", "message": "email is required"})),
32 )
33 .into_response();
34 }
35
36 let user = sqlx::query!("SELECT id FROM users WHERE LOWER(email) = $1", email)
37 .fetch_optional(&state.db)
38 .await;
39
40 let user_id = match user {
41 Ok(Some(row)) => row.id,
42 Ok(None) => {
43 info!("Password reset requested for unknown email");
44 return (StatusCode::OK, Json(json!({}))).into_response();
45 }
46 Err(e) => {
47 error!("DB error in request_password_reset: {:?}", e);
48 return (
49 StatusCode::INTERNAL_SERVER_ERROR,
50 Json(json!({"error": "InternalError"})),
51 )
52 .into_response();
53 }
54 };
55
56 let code = generate_reset_code();
57 let expires_at = Utc::now() + Duration::minutes(10);
58
59 let update = sqlx::query!(
60 "UPDATE users SET password_reset_code = $1, password_reset_code_expires_at = $2 WHERE id = $3",
61 code,
62 expires_at,
63 user_id
64 )
65 .execute(&state.db)
66 .await;
67
68 if let Err(e) = update {
69 error!("DB error setting reset code: {:?}", e);
70 return (
71 StatusCode::INTERNAL_SERVER_ERROR,
72 Json(json!({"error": "InternalError"})),
73 )
74 .into_response();
75 }
76
77 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
78 if let Err(e) =
79 crate::notifications::enqueue_password_reset(&state.db, user_id, &code, &hostname).await
80 {
81 warn!("Failed to enqueue password reset notification: {:?}", e);
82 }
83
84 info!("Password reset requested for user {}", user_id);
85
86 (StatusCode::OK, Json(json!({}))).into_response()
87}
88
89#[derive(Deserialize)]
90pub struct ResetPasswordInput {
91 pub token: String,
92 pub password: String,
93}
94
95pub async fn reset_password(
96 State(state): State<AppState>,
97 Json(input): Json<ResetPasswordInput>,
98) -> Response {
99 let token = input.token.trim();
100 let password = &input.password;
101
102 if token.is_empty() {
103 return (
104 StatusCode::BAD_REQUEST,
105 Json(json!({"error": "InvalidToken", "message": "token is required"})),
106 )
107 .into_response();
108 }
109
110 if password.is_empty() {
111 return (
112 StatusCode::BAD_REQUEST,
113 Json(json!({"error": "InvalidRequest", "message": "password is required"})),
114 )
115 .into_response();
116 }
117
118 let user = sqlx::query!(
119 "SELECT id, password_reset_code, password_reset_code_expires_at FROM users WHERE password_reset_code = $1",
120 token
121 )
122 .fetch_optional(&state.db)
123 .await;
124
125 let (user_id, expires_at) = match user {
126 Ok(Some(row)) => {
127 let expires = row.password_reset_code_expires_at;
128 (row.id, expires)
129 }
130 Ok(None) => {
131 return (
132 StatusCode::BAD_REQUEST,
133 Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})),
134 )
135 .into_response();
136 }
137 Err(e) => {
138 error!("DB error in reset_password: {:?}", e);
139 return (
140 StatusCode::INTERNAL_SERVER_ERROR,
141 Json(json!({"error": "InternalError"})),
142 )
143 .into_response();
144 }
145 };
146
147 if let Some(exp) = expires_at {
148 if Utc::now() > exp {
149 if let Err(e) = sqlx::query!(
150 "UPDATE users SET password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $1",
151 user_id
152 )
153 .execute(&state.db)
154 .await
155 {
156 error!("Failed to clear expired reset code: {:?}", e);
157 }
158
159 return (
160 StatusCode::BAD_REQUEST,
161 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
162 )
163 .into_response();
164 }
165 } else {
166 return (
167 StatusCode::BAD_REQUEST,
168 Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})),
169 )
170 .into_response();
171 }
172
173 let password_hash = match hash(password, DEFAULT_COST) {
174 Ok(h) => h,
175 Err(e) => {
176 error!("Failed to hash password: {:?}", e);
177 return (
178 StatusCode::INTERNAL_SERVER_ERROR,
179 Json(json!({"error": "InternalError"})),
180 )
181 .into_response();
182 }
183 };
184
185 let mut tx = match state.db.begin().await {
186 Ok(tx) => tx,
187 Err(e) => {
188 error!("Failed to begin transaction: {:?}", e);
189 return (
190 StatusCode::INTERNAL_SERVER_ERROR,
191 Json(json!({"error": "InternalError"})),
192 )
193 .into_response();
194 }
195 };
196
197 if let Err(e) = sqlx::query!(
198 "UPDATE users SET password_hash = $1, password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $2",
199 password_hash,
200 user_id
201 )
202 .execute(&mut *tx)
203 .await
204 {
205 error!("DB error updating password: {:?}", e);
206 return (
207 StatusCode::INTERNAL_SERVER_ERROR,
208 Json(json!({"error": "InternalError"})),
209 )
210 .into_response();
211 }
212
213 if let Err(e) = sqlx::query!("DELETE FROM session_tokens WHERE did = (SELECT did FROM users WHERE id = $1)", user_id)
214 .execute(&mut *tx)
215 .await
216 {
217 error!("Failed to invalidate sessions after password reset: {:?}", e);
218 return (
219 StatusCode::INTERNAL_SERVER_ERROR,
220 Json(json!({"error": "InternalError"})),
221 )
222 .into_response();
223 }
224
225 if let Err(e) = tx.commit().await {
226 error!("Failed to commit password reset transaction: {:?}", e);
227 return (
228 StatusCode::INTERNAL_SERVER_ERROR,
229 Json(json!({"error": "InternalError"})),
230 )
231 .into_response();
232 }
233
234 info!("Password reset completed for user {}", user_id);
235
236 (StatusCode::OK, Json(json!({}))).into_response()
237}