Plume-org / Plume

@@ -16,10 +16,10 @@
Loading
16 16
17 17
use mail::{build_mail, Mailer};
18 18
use plume_models::{
19 +
    password_reset_requests::*,
19 20
    users::{User, AUTH_COOKIE},
20 21
    Error, PlumeRocket, CONFIG,
21 22
};
22 -
use routes::errors::ErrorPage;
23 23
use template_utils::{IntoContext, Ructe};
24 24
25 25
#[get("/login?<m>")]
@@ -164,29 +164,17 @@
Loading
164 164
pub fn password_reset_request(
165 165
    mail: State<Arc<Mutex<Mailer>>>,
166 166
    form: Form<ResetForm>,
167 -
    requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
168 167
    rockets: PlumeRocket,
169 168
) -> Ructe {
170 -
    let mut requests = requests.lock().unwrap();
171 -
    // Remove outdated requests (more than 1 day old) to avoid the list to grow too much
172 -
    requests.retain(|r| r.creation_date.elapsed().as_secs() < 24 * 60 * 60);
169 +
    if User::find_by_email(&*rockets.conn, &form.email).is_ok() {
170 +
        let token = PasswordResetRequest::insert(&*rockets.conn, &form.email)
171 +
            .expect("password_reset_request::insert: error");
173 172
174 -
    if User::find_by_email(&*rockets.conn, &form.email).is_ok()
175 -
        && !requests.iter().any(|x| x.mail == form.email.clone())
176 -
    {
177 -
        let id = plume_common::utils::random_hex();
178 -
179 -
        requests.push(ResetRequest {
180 -
            mail: form.email.clone(),
181 -
            id: id.clone(),
182 -
            creation_date: Instant::now(),
183 -
        });
184 -
185 -
        let link = format!("https://{}/password-reset/{}", CONFIG.base_url, id);
173 +
        let url = format!("https://{}/password-reset/{}", CONFIG.base_url, token);
186 174
        if let Some(message) = build_mail(
187 175
            form.email.clone(),
188 176
            i18n!(rockets.intl.catalog, "Password reset"),
189 -
            i18n!(rockets.intl.catalog, "Here is the link to reset your password: {0}"; link),
177 +
            i18n!(rockets.intl.catalog, "Here is the link to reset your password: {0}"; url),
190 178
        ) {
191 179
            if let Some(ref mut mail) = *mail.lock().unwrap() {
192 180
                mail.send(message.into())
@@ -199,17 +187,10 @@
Loading
199 187
}
200 188
201 189
#[get("/password-reset/<token>")]
202 -
pub fn password_reset_form(
203 -
    token: String,
204 -
    requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
205 -
    rockets: PlumeRocket,
206 -
) -> Result<Ructe, ErrorPage> {
207 -
    requests
208 -
        .lock()
209 -
        .unwrap()
210 -
        .iter()
211 -
        .find(|x| x.id == token.clone())
212 -
        .ok_or(Error::NotFound)?;
190 +
pub fn password_reset_form(token: String, rockets: PlumeRocket) -> Result<Ructe, Ructe> {
191 +
    PasswordResetRequest::find_by_token(&*rockets.conn, &token)
192 +
        .map_err(|err| password_reset_error_response(err, &rockets))?;
193 +
213 194
    Ok(render!(session::password_reset(
214 195
        &rockets.to_context(),
215 196
        &NewPasswordForm::default(),
@@ -239,56 +220,33 @@
Loading
239 220
#[post("/password-reset/<token>", data = "<form>")]
240 221
pub fn password_reset(
241 222
    token: String,
242 -
    requests: State<Arc<Mutex<Vec<ResetRequest>>>>,
243 223
    form: Form<NewPasswordForm>,
244 224
    rockets: PlumeRocket,
245 225
) -> Result<Flash<Redirect>, Ructe> {
246 226
    form.validate()
247 -
        .and_then(|_| {
248 -
            let mut requests = requests.lock().unwrap();
249 -
            let req = requests
250 -
                .iter()
251 -
                .find(|x| x.id == token.clone())
252 -
                .ok_or_else(|| to_validation(0))?
253 -
                .clone();
254 -
            if req.creation_date.elapsed().as_secs() < 60 * 60 * 2 {
255 -
                // Reset link is only valid for 2 hours
256 -
                requests.retain(|r| *r != req);
257 -
                let user = User::find_by_email(&*rockets.conn, &req.mail).map_err(to_validation)?;
258 -
                user.reset_password(&*rockets.conn, &form.password).ok();
259 -
                Ok(Flash::success(
260 -
                    Redirect::to(uri!(
261 -
                        new: m = _
262 -
                    )),
263 -
                    i18n!(
264 -
                        rockets.intl.catalog,
265 -
                        "Your password was successfully reset."
266 -
                    ),
267 -
                ))
268 -
            } else {
269 -
                Ok(Flash::error(
270 -
                    Redirect::to(uri!(
271 -
                        new: m = _
272 -
                    )),
273 -
                    i18n!(
274 -
                        rockets.intl.catalog,
275 -
                        "Sorry, but the link expired. Try again"
276 -
                    ),
277 -
                ))
278 -
            }
279 -
        })
280 -
        .map_err(|err| render!(session::password_reset(&rockets.to_context(), &form, err)))
227 +
        .map_err(|err| render!(session::password_reset(&rockets.to_context(), &form, err)))?;
228 +
229 +
    PasswordResetRequest::find_and_delete_by_token(&*rockets.conn, &token)
230 +
        .and_then(|request| User::find_by_email(&*rockets.conn, &request.email))
231 +
        .and_then(|user| user.reset_password(&*rockets.conn, &form.password))
232 +
        .map_err(|err| password_reset_error_response(err, &rockets))?;
233 +
234 +
    Ok(Flash::success(
235 +
        Redirect::to(uri!(
236 +
            new: m = _
237 +
        )),
238 +
        i18n!(
239 +
            rockets.intl.catalog,
240 +
            "Your password was successfully reset."
241 +
        ),
242 +
    ))
281 243
}
282 244
283 -
fn to_validation<T>(_: T) -> ValidationErrors {
284 -
    let mut errors = ValidationErrors::new();
285 -
    errors.add(
286 -
        "",
287 -
        ValidationError {
288 -
            code: Cow::from("server_error"),
289 -
            message: Some(Cow::from("An unknown error occured")),
290 -
            params: std::collections::HashMap::new(),
291 -
        },
292 -
    );
293 -
    errors
245 +
fn password_reset_error_response(err: Error, rockets: &PlumeRocket) -> Ructe {
246 +
    match err {
247 +
        Error::Expired => render!(session::password_reset_request_expired(
248 +
            &rockets.to_context()
249 +
        )),
250 +
        _ => render!(errors::not_found(&rockets.to_context())),
251 +
    }
294 252
}

@@ -0,0 +1,164 @@
Loading
1 +
use chrono::{offset::Utc, Duration, NaiveDateTime};
2 +
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
3 +
use schema::password_reset_requests;
4 +
use {Connection, Error, Result};
5 +
6 +
#[derive(Clone, Identifiable, Queryable)]
7 +
pub struct PasswordResetRequest {
8 +
    pub id: i32,
9 +
    pub email: String,
10 +
    pub token: String,
11 +
    pub expiration_date: NaiveDateTime,
12 +
}
13 +
14 +
#[derive(Insertable)]
15 +
#[table_name = "password_reset_requests"]
16 +
pub struct NewPasswordResetRequest {
17 +
    pub email: String,
18 +
    pub token: String,
19 +
    pub expiration_date: NaiveDateTime,
20 +
}
21 +
22 +
const TOKEN_VALIDITY_HOURS: i64 = 2;
23 +
24 +
impl PasswordResetRequest {
25 +
    pub fn insert(conn: &Connection, email: &str) -> Result<String> {
26 +
        // first, delete other password reset tokens associated with this email:
27 +
        let existing_requests =
28 +
            password_reset_requests::table.filter(password_reset_requests::email.eq(email));
29 +
        diesel::delete(existing_requests).execute(conn)?;
30 +
31 +
        // now, generate a random token, set the expiry date,
32 +
        // and insert it into the DB:
33 +
        let token = plume_common::utils::random_hex();
34 +
        let expiration_date = Utc::now()
35 +
            .naive_utc()
36 +
            .checked_add_signed(Duration::hours(TOKEN_VALIDITY_HOURS))
37 +
            .expect("could not calculate expiration date");
38 +
        let new_request = NewPasswordResetRequest {
39 +
            email: email.to_owned(),
40 +
            token: token.clone(),
41 +
            expiration_date,
42 +
        };
43 +
        diesel::insert_into(password_reset_requests::table)
44 +
            .values(new_request)
45 +
            .execute(conn)
46 +
            .map_err(Error::from)?;
47 +
48 +
        Ok(token)
49 +
    }
50 +
51 +
    pub fn find_by_token(conn: &Connection, token: &str) -> Result<Self> {
52 +
        let token = password_reset_requests::table
53 +
            .filter(password_reset_requests::token.eq(token))
54 +
            .first::<Self>(conn)
55 +
            .map_err(Error::from)?;
56 +
57 +
        if token.expiration_date < Utc::now().naive_utc() {
58 +
            return Err(Error::Expired);
59 +
        }
60 +
61 +
        Ok(token)
62 +
    }
63 +
64 +
    pub fn find_and_delete_by_token(conn: &Connection, token: &str) -> Result<Self> {
65 +
        let request = Self::find_by_token(&conn, &token)?;
66 +
67 +
        let filter =
68 +
            password_reset_requests::table.filter(password_reset_requests::id.eq(request.id));
69 +
        diesel::delete(filter).execute(conn)?;
70 +
71 +
        Ok(request)
72 +
    }
73 +
}
74 +
75 +
#[cfg(test)]
76 +
mod tests {
77 +
    use super::*;
78 +
    use diesel::Connection;
79 +
    use tests::db;
80 +
    use users::tests as user_tests;
81 +
82 +
    #[test]
83 +
    fn test_insert_and_find_password_reset_request() {
84 +
        let conn = db();
85 +
        conn.test_transaction::<_, (), _>(|| {
86 +
            user_tests::fill_database(&conn);
87 +
            let admin_email = "admin@example.com";
88 +
89 +
            let token = PasswordResetRequest::insert(&conn, admin_email)
90 +
                .expect("couldn't insert new request");
91 +
            let request = PasswordResetRequest::find_by_token(&conn, &token)
92 +
                .expect("couldn't retrieve request");
93 +
94 +
            assert!(&token.len() > &32);
95 +
            assert_eq!(&request.email, &admin_email);
96 +
97 +
            Ok(())
98 +
        });
99 +
    }
100 +
101 +
    #[test]
102 +
    fn test_insert_delete_previous_password_reset_request() {
103 +
        let conn = db();
104 +
        conn.test_transaction::<_, (), _>(|| {
105 +
            user_tests::fill_database(&conn);
106 +
            let admin_email = "admin@example.com";
107 +
108 +
            PasswordResetRequest::insert(&conn, &admin_email).expect("couldn't insert new request");
109 +
            PasswordResetRequest::insert(&conn, &admin_email)
110 +
                .expect("couldn't insert second request");
111 +
112 +
            let count = password_reset_requests::table.count().get_result(&*conn);
113 +
            assert_eq!(Ok(1), count);
114 +
115 +
            Ok(())
116 +
        });
117 +
    }
118 +
119 +
    #[test]
120 +
    fn test_find_password_reset_request_by_token_time() {
121 +
        let conn = db();
122 +
        conn.test_transaction::<_, (), _>(|| {
123 +
            user_tests::fill_database(&conn);
124 +
            let admin_email = "admin@example.com";
125 +
            let token = "abcdef";
126 +
            let now = Utc::now().naive_utc();
127 +
128 +
            diesel::insert_into(password_reset_requests::table)
129 +
                .values((
130 +
                    password_reset_requests::email.eq(&admin_email),
131 +
                    password_reset_requests::token.eq(&token),
132 +
                    password_reset_requests::expiration_date.eq(now),
133 +
                ))
134 +
                .execute(&*conn)
135 +
                .expect("could not insert request");
136 +
137 +
            match PasswordResetRequest::find_by_token(&conn, &token) {
138 +
                Err(Error::Expired) => (),
139 +
                _ => panic!("Received unexpected result finding expired token"),
140 +
            }
141 +
142 +
            Ok(())
143 +
        });
144 +
    }
145 +
146 +
    #[test]
147 +
    fn test_find_and_delete_password_reset_request() {
148 +
        let conn = db();
149 +
        conn.test_transaction::<_, (), _>(|| {
150 +
            user_tests::fill_database(&conn);
151 +
            let admin_email = "admin@example.com";
152 +
153 +
            let token = PasswordResetRequest::insert(&conn, &admin_email)
154 +
                .expect("couldn't insert new request");
155 +
            PasswordResetRequest::find_and_delete_by_token(&conn, &token)
156 +
                .expect("couldn't find and delete request");
157 +
158 +
            let count = password_reset_requests::table.count().get_result(&*conn);
159 +
            assert_eq!(Ok(0), count);
160 +
161 +
            Ok(())
162 +
        });
163 +
    }
164 +
}

@@ -65,6 +65,7 @@
Loading
65 65
    Unauthorized,
66 66
    Url,
67 67
    Webfinger,
68 +
    Expired,
68 69
}
69 70
70 71
impl From<bcrypt::BcryptError> for Error {
@@ -367,6 +368,7 @@
Loading
367 368
pub mod mentions;
368 369
pub mod migrations;
369 370
pub mod notifications;
371 +
pub mod password_reset_requests;
370 372
pub mod plume_rocket;
371 373
pub mod post_authors;
372 374
pub mod posts;
Files Coverage
plume-api/src 0.00%
plume-cli/src 52.63%
plume-common/src 45.99%
plume-models/src 49.35%
src 4.16%
plume-macro/src/lib.rs 87.50%
Project Totals (68 files) 35.31%
1
codecov:
2
  notify:
3
    require_ci_to_pass: yes
4

5
coverage:
6
  precision: 2
7
  round: down
8
  range: "70...100"
9

10
  status:
11
    project: no
12
    patch: no
13
    changes: no
14

15
parsers:
16
  gcov:
17
    branch_detection:
18
      conditional: yes
19
      loop: yes
20
      method: no
21
      macro: no
22

23
comment:
24
  layout: "header, diff"
25
  behavior: default
26
  require_changes: no
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading