use super::*;
use crate::{email::Resend, resend_key::ResendKey};
const OWNER: &str = "Email password setup is off. Ask the machine's owner to run thirdshift console password --email <address>.";
const SENT: &str = "If this address is allowed, a code has been sent. It lasts 15 minutes. <a href=\"/set-password\">Enter your code and new password</a>.";
#[derive(Template)]
#[template(path = "console/set_password.html")]
struct PasswordPage {
csrf: String,
theme: String,
email_enabled: bool,
}
pub(in crate::console) async fn set_password_page(
Extension(access): Extension<Access>,
headers: HeaderMap,
) -> Response {
anonymous_page(
access,
headers,
|AnonymousForm {
csrf,
theme,
email_enabled,
}| {
Ok(PasswordPage {
csrf,
theme,
email_enabled,
}
.render()?)
},
)
.await
}
#[derive(Deserialize)]
pub(in crate::console) struct PasswordForm {
email: String,
#[serde(default)]
code: String,
#[serde(default)]
password: String,
#[serde(default)]
csrf: String,
}
fn authorized(access: &Access, headers: &HeaderMap, form: &PasswordForm) -> bool {
!access.no_sign_in
&& cookie(headers, SIGN_IN).is_some_and(|token| valid_csrf(&form.csrf, &csrf(&token)))
}
pub(in crate::console) async fn code(
Extension(access): Extension<Access>,
headers: HeaderMap,
Form(form): Form<PasswordForm>,
) -> Response {
if access.no_sign_in {
return StatusCode::NOT_FOUND.into_response();
}
if !authorized(&access, &headers, &form) {
return StatusCode::FORBIDDEN.into_response();
}
match tokio::task::spawn_blocking(|| ResendKey::find().map(|key| key.is_some())).await {
Ok(Ok(true)) => {
tokio::task::spawn_blocking(move || {
let result = (|| -> Result<()> {
let config = UserConfig::load()?;
Accounts::load()?.send_code(&form.email, |code| {
Resend::new(Some(form.email.clone()), &config.email)?.send(
"Set your thirdshift Console password",
&format!("Your thirdshift Console code is {code}\n\nIt lasts 15 minutes and allows 5 tries. If you did not request it, ignore this email."),
)
})
})();
if let Err(error) = result {
eprintln!("thirdshift Console: can't send password code: {error:#}");
}
});
axum::response::Html(SENT).into_response()
}
Ok(Ok(false)) => (StatusCode::SERVICE_UNAVAILABLE, OWNER).into_response(),
error => failed(error),
}
}
pub(in crate::console) async fn set_password(
Extension(access): Extension<Access>,
headers: HeaderMap,
Form(form): Form<PasswordForm>,
) -> Response {
if access.no_sign_in {
return StatusCode::NOT_FOUND.into_response();
}
if !authorized(&access, &headers, &form) {
return StatusCode::FORBIDDEN.into_response();
}
let result = tokio::task::spawn_blocking(move || -> Result<Option<SignIn>> {
if ResendKey::find()?.is_none() {
return Ok(None);
}
Ok(Some(Accounts::load()?.set_password_by_code(
&form.email,
&form.code,
&form.password,
)?))
})
.await;
match result {
Ok(Ok(Some(SignIn::SignedIn(token)))) => {
let mut response = Redirect::to("/").into_response();
set_cookie(&mut response, SESSION, &token, accounts::LIFETIME);
response
},
Ok(Ok(Some(_))) => (StatusCode::UNAUTHORIZED, axum::response::Html("Password not set. Check your code or <a href=\"/set-password\">request another</a>.")).into_response(),
Ok(Ok(None)) => (StatusCode::SERVICE_UNAVAILABLE, OWNER).into_response(),
Ok(Err(error)) => failed(error),
Err(error) => failed(error),
}
}