Skip to content

Commit 43f06af

Browse files
committed
wip
1 parent 2a45dcc commit 43f06af

File tree

12 files changed

+357
-2
lines changed

12 files changed

+357
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- Add migration script here
2+
CREATE TABLE IF NOT EXISTS torrust_reset_passwords_tokens (
3+
user_id INTEGER NOT NULL PRIMARY KEY,
4+
token INTEGER NOT NULL,
5+
expiration_date DATE NOT NULL,
6+
FOREIGN KEY(user_id) REFERENCES torrust_users(user_id) ON DELETE CASCADE
7+
)

src/app.rs

+8
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,13 @@ pub async fn run(configuration: Configuration, api_version: &Version) -> Running
167167
))
168168
.clone();
169169

170+
let password_reset_service = Arc::new(user::PasswordResetService::new(
171+
configuration.clone(),
172+
user_profile_repository.clone(),
173+
authorization_service.clone(),
174+
))
175+
.clone();
176+
170177
// Build app container
171178

172179
let app_data = Arc::new(AppData::new(
@@ -202,6 +209,7 @@ pub async fn run(configuration: Configuration, api_version: &Version) -> Running
202209
ban_service,
203210
about_service,
204211
listing_service,
212+
password_reset_service,
205213
));
206214

207215
// Start cronjob to import tracker torrent data and updating

src/common.rs

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ pub struct AppData {
5353
pub ban_service: Arc<user::BanService>,
5454
pub about_service: Arc<about::Service>,
5555
pub listing_service: Arc<user::ListingService>,
56+
pub password_reset_service: Arc<user::PasswordResetService>,
5657
}
5758

5859
impl AppData {

src/databases/database.rs

+3
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,9 @@ pub trait Database: Sync + Send {
214214
/// Get `UserProfile` from `username`.
215215
async fn get_user_profile_from_username(&self, username: &str) -> Result<UserProfile, Error>;
216216

217+
/// Get `UserProfile` from `email`.
218+
async fn get_user_profile_from_email(&self, email: &str) -> Result<UserProfile, Error>;
219+
217220
/// Get all user profiles in a paginated and sorted form as `UserProfilesResponse` from `search`, `filters`, `sort`, `offset` and `page_size`.
218221
async fn get_user_profiles_search_paginated(
219222
&self,

src/databases/mysql.rs

+8
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,14 @@ impl Database for Mysql {
155155
.map_err(|_| database::Error::UserNotFound)
156156
}
157157

158+
async fn get_user_profile_from_email(&self, email: &str) -> Result<UserProfile, Error> {
159+
query_as::<_, UserProfile>("SELECT * FROM torrust_user_profiles WHERE email = ?")
160+
.bind(email)
161+
.fetch_one(&self.pool)
162+
.await
163+
.map_err(|_| database::Error::UserNotFound)
164+
}
165+
158166
async fn get_user_profiles_search_paginated(
159167
&self,
160168
search: &Option<String>,

src/databases/sqlite.rs

+8
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,14 @@ impl Database for Sqlite {
156156
.map_err(|_| database::Error::UserNotFound)
157157
}
158158

159+
async fn get_user_profile_from_email(&self, email: &str) -> Result<UserProfile, Error> {
160+
query_as::<_, UserProfile>("SELECT * FROM torrust_user_profiles WHERE email = ?")
161+
.bind(email)
162+
.fetch_one(&self.pool)
163+
.await
164+
.map_err(|_| database::Error::UserNotFound)
165+
}
166+
159167
async fn get_user_profiles_search_paginated(
160168
&self,
161169
search: &Option<String>,

src/errors.rs

+7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ pub enum ServiceError {
2222

2323
#[display("Email is required")] //405j
2424
EmailMissing,
25+
#[display("A verified email is required")]
26+
VerifiedEmailMissing,
2527
#[display("Please enter a valid email address")] //405j
2628
EmailInvalid,
2729

@@ -60,6 +62,9 @@ pub enum ServiceError {
6062
#[display("Passwords don't match")]
6163
PasswordsDontMatch,
6264

65+
#[display("Couldn't send new password to the user")]
66+
FailedToSendResetPassword,
67+
6368
/// when the a username is already taken
6469
#[display("Username not available")]
6570
UsernameTaken,
@@ -290,6 +295,7 @@ pub fn http_status_code_for_service_error(error: &ServiceError) -> StatusCode {
290295
ServiceError::PasswordTooShort => StatusCode::BAD_REQUEST,
291296
ServiceError::PasswordTooLong => StatusCode::BAD_REQUEST,
292297
ServiceError::PasswordsDontMatch => StatusCode::BAD_REQUEST,
298+
ServiceError::FailedToSendResetPassword => StatusCode::INTERNAL_SERVER_ERROR,
293299
ServiceError::UsernameTaken => StatusCode::BAD_REQUEST,
294300
ServiceError::UsernameInvalid => StatusCode::BAD_REQUEST,
295301
ServiceError::EmailTaken => StatusCode::BAD_REQUEST,
@@ -318,6 +324,7 @@ pub fn http_status_code_for_service_error(error: &ServiceError) -> StatusCode {
318324
ServiceError::TagAlreadyExists => StatusCode::BAD_REQUEST,
319325
ServiceError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
320326
ServiceError::EmailMissing => StatusCode::NOT_FOUND,
327+
ServiceError::VerifiedEmailMissing => StatusCode::NOT_FOUND,
321328
ServiceError::FailedToSendVerificationEmail => StatusCode::INTERNAL_SERVER_ERROR,
322329
ServiceError::WhitelistingError => StatusCode::INTERNAL_SERVER_ERROR,
323330
ServiceError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR,

src/mailer.rs

+67
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,29 @@ impl Service {
151151

152152
format!("{base_url}/{API_VERSION_URL_PREFIX}/user/email/verify/{token}")
153153
}
154+
155+
/// Send reset password email.
156+
///
157+
/// # Errors
158+
///
159+
/// This function will return an error if unable to send an email.
160+
///
161+
/// # Panics
162+
///
163+
/// This function will panic if the multipart builder had an error.
164+
pub async fn send_reset_password_mail(&self, to: &str, username: &str, password: &str) -> Result<(), ServiceError> {
165+
let builder = self.get_builder(to).await;
166+
167+
let mail = build_letter(&password, username, builder)?;
168+
169+
match self.mailer.send(mail).await {
170+
Ok(_res) => Ok(()),
171+
Err(e) => {
172+
eprintln!("Failed to send email: {e}");
173+
Err(ServiceError::FailedToSendVerificationEmail)
174+
}
175+
}
176+
}
154177
}
155178

156179
fn build_letter(verification_url: &str, username: &str, builder: MessageBuilder) -> Result<Message, ServiceError> {
@@ -195,6 +218,50 @@ fn build_content(verification_url: &str, username: &str) -> Result<(String, Stri
195218
Ok((plain_body, html_body))
196219
}
197220

221+
fn build_reset_password_letter(password: &str, username: &str, builder: MessageBuilder) -> Result<Message, ServiceError> {
222+
let (plain_body, html_body) = build_reset_password_content(password, username).map_err(|e| {
223+
tracing::error!("{e}");
224+
ServiceError::InternalServerError
225+
})?;
226+
227+
Ok(builder
228+
.subject("Torrust - Password reset")
229+
.multipart(
230+
MultiPart::alternative()
231+
.singlepart(
232+
SinglePart::builder()
233+
.header(lettre::message::header::ContentType::TEXT_PLAIN)
234+
.body(plain_body),
235+
)
236+
.singlepart(
237+
SinglePart::builder()
238+
.header(lettre::message::header::ContentType::TEXT_HTML)
239+
.body(html_body),
240+
),
241+
)
242+
.expect("the `multipart` builder had an error"))
243+
}
244+
245+
fn build_reset_password_content(password: &str, username: &str) -> Result<(String, String), tera::Error> {
246+
let plain_body = format!(
247+
"
248+
Hello, {username}!
249+
250+
Your password has been reset.
251+
252+
Find below your new password:
253+
{password}
254+
255+
We recommend replacing it as soon as possible with a new and strong password of your own.
256+
"
257+
);
258+
let mut context = Context::new();
259+
context.insert("password", &password);
260+
context.insert("username", &username);
261+
let html_body = TEMPLATES.render("html_reset_password", &context)?;
262+
Ok((plain_body, html_body))
263+
}
264+
198265
pub type Mailer = AsyncSmtpTransport<Tokio1Executor>;
199266

200267
#[cfg(test)]

src/services/authorization.rs

+4
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ pub enum ACTION {
5353
ChangePassword,
5454
BanUser,
5555
GenerateUserProfileSpecification,
56+
SendPasswordResetLink,
5657
}
5758

5859
pub struct Service {
@@ -250,6 +251,7 @@ impl Default for CasbinConfiguration {
250251
admin, ChangePassword
251252
admin, BanUser
252253
admin, GenerateUserProfileSpecification
254+
admin, SendPasswordResetLink
253255
registered, GetAboutPage
254256
registered, GetLicensePage
255257
registered, GetCategories
@@ -263,6 +265,7 @@ impl Default for CasbinConfiguration {
263265
registered, GenerateTorrentInfoListing
264266
registered, GetCanonicalInfoHash
265267
registered, ChangePassword
268+
registered, SendPasswordResetLink
266269
guest, GetAboutPage
267270
guest, GetLicensePage
268271
guest, GetCategories
@@ -273,6 +276,7 @@ impl Default for CasbinConfiguration {
273276
guest, GetTorrentInfo
274277
guest, GenerateTorrentInfoListing
275278
guest, GetCanonicalInfoHash
279+
guest, SendPasswordResetLink
276280
",
277281
),
278282
}

0 commit comments

Comments
 (0)