use tayvo_authifier::util::hash_password;
use tayvo_authifier::{Authifier, Result};
use rocket::serde::json::Json;
use rocket::State;
use tayvo_rocket_empty::EmptyResponse;
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct DataPasswordReset {
pub token: String,
pub password: String,
#[serde(default)]
pub remove_sessions: bool,
}
#[openapi(tag = "Account")]
#[patch("/reset_password", data = "<data>")]
pub async fn password_reset(
authifier: &State<Authifier>,
data: Json<DataPasswordReset>,
) -> Result<EmptyResponse> {
let data = data.into_inner();
let mut account = authifier
.database
.find_account_with_password_reset(&data.token)
.await?;
authifier
.config
.password_scanning
.assert_safe(&data.password)
.await?;
account.password = hash_password(data.password)?;
account.password_reset = None;
account.lockout = None;
account.save(authifier).await?;
if data.remove_sessions {
account.delete_all_sessions(authifier, None).await?;
}
Ok(EmptyResponse)
}
#[cfg(test)]
#[cfg(feature = "test")]
mod tests {
use chrono::Duration;
use iso8601_timestamp::Timestamp;
use crate::test::*;
#[async_std::test]
async fn success() {
let (authifier, session, mut account, _) =
for_test_authenticated("password_reset::success").await;
account.password_reset = Some(PasswordReset {
token: "token".into(),
expiry: Timestamp::from_unix_timestamp_ms(
chrono::Utc::now()
.checked_add_signed(Duration::seconds(100))
.expect("failed to checked_add_signed")
.timestamp_millis(),
),
});
account.save(&authifier).await.unwrap();
let client = bootstrap_rocket_with_auth(
authifier.clone(),
routes![
crate::routes::account::password_reset::password_reset,
crate::routes::session::login::login
],
)
.await;
let res = client
.patch("/reset_password")
.header(ContentType::JSON)
.body(
json!({
"token": "token",
"password": "valid password",
"remove_sessions": true
})
.to_string(),
)
.dispatch()
.await;
assert_eq!(res.status(), Status::NoContent);
assert!(authifier
.database
.find_account_with_password_reset("token")
.await
.is_err());
let res = client
.post("/login")
.header(ContentType::JSON)
.body(
json!({
"email": "email@tayvo.chat",
"password": "valid password"
})
.to_string(),
)
.dispatch()
.await;
assert_eq!(res.status(), Status::Ok);
assert!(serde_json::from_str::<Session>(&res.into_string().await.unwrap()).is_ok());
assert_eq!(
authifier
.database
.find_session(&session.id)
.await
.unwrap_err(),
Error::UnknownUser
);
}
#[async_std::test]
async fn fail_invalid_token() {
let (authifier, _) = for_test("password_reset::fail_invalid_token").await;
let client = bootstrap_rocket_with_auth(
authifier,
routes![crate::routes::account::password_reset::password_reset],
)
.await;
let res = client
.patch("/reset_password")
.header(ContentType::JSON)
.body(
json!({
"token": "invalid",
"password": "valid password"
})
.to_string(),
)
.dispatch()
.await;
assert_eq!(res.status(), Status::Unauthorized);
assert_eq!(
res.into_string().await,
Some("{\"type\":\"InvalidToken\"}".into())
);
}
}