Skip to content

Commit c9f1c3d

Browse files
mario-ntda2ce7
authored andcommitted
wip
1 parent 8c32d7b commit c9f1c3d

12 files changed

Lines changed: 357 additions & 2 deletions

File tree

Lines changed: 7 additions & 0 deletions
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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,13 @@ pub async fn run(configuration: Configuration, api_version: &Version) -> Running
166166
authorization_service.clone(),
167167
));
168168

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

171178
let app_data = Arc::new(AppData::new(
@@ -201,6 +208,7 @@ pub async fn run(configuration: Configuration, api_version: &Version) -> Running
201208
ban_service,
202209
about_service,
203210
listing_service,
211+
password_reset_service,
204212
));
205213

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

src/common.rs

Lines changed: 1 addition & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 8 additions & 0 deletions
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

Lines changed: 8 additions & 0 deletions
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

Lines changed: 7 additions & 0 deletions
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 const fn http_status_code_for_service_error(error: &ServiceError) -> StatusC
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 const fn http_status_code_for_service_error(error: &ServiceError) -> StatusC
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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,29 @@ impl Service {
153153

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

158181
fn build_letter(verification_url: &str, username: &str, builder: MessageBuilder) -> Result<Message, ServiceError> {
@@ -197,6 +220,50 @@ fn build_content(verification_url: &str, username: &str) -> Result<(String, Stri
197220
Ok((plain_body, html_body))
198221
}
199222

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

202269
#[cfg(test)]

src/services/authorization.rs

Lines changed: 4 additions & 0 deletions
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 {
@@ -251,6 +252,7 @@ impl Default for CasbinConfiguration {
251252
admin, ChangePassword
252253
admin, BanUser
253254
admin, GenerateUserProfileSpecification
255+
admin, SendPasswordResetLink
254256
registered, GetAboutPage
255257
registered, GetLicensePage
256258
registered, GetCategories
@@ -264,6 +266,7 @@ impl Default for CasbinConfiguration {
264266
registered, GenerateTorrentInfoListing
265267
registered, GetCanonicalInfoHash
266268
registered, ChangePassword
269+
registered, SendPasswordResetLink
267270
guest, GetAboutPage
268271
guest, GetLicensePage
269272
guest, GetCategories
@@ -274,6 +277,7 @@ impl Default for CasbinConfiguration {
274277
guest, GetTorrentInfo
275278
guest, GenerateTorrentInfoListing
276279
guest, GetCanonicalInfoHash
280+
guest, SendPasswordResetLink
277281
",
278282
),
279283
}

0 commit comments

Comments
 (0)