use crate::admin::helper::resource_entry::ResourceEntry;
use crate::auth::user::BuiltinUserEntity;
use crate::auth::user_trait::RuniqueUser;
use crate::context::template::Request;
use crate::utils::{
aliases::AppResult,
trad::{t, tf},
};
use axum::response::{IntoResponse, Redirect, Response};
const ADMIN_RESET_TTL: std::time::Duration = std::time::Duration::from_secs(3600);
fn admin_reset_base(
req: &Request,
state: &super::PrototypeAdminState,
headers: &axum::http::HeaderMap,
) -> Option<String> {
let config = &req.engine.config;
let origin = crate::auth::password::public_link_base(
config.server.public_url.as_deref(),
headers,
config.debug,
)?;
Some(format!(
"{origin}/{}",
state.config.reset_route.trim_matches('/')
))
}
#[must_use]
pub(super) async fn send_user_created_email(
req: &mut Request,
_entry: &ResourceEntry,
email: &str,
username: Option<&str>,
email_template: Option<&str>,
headers: &axum::http::HeaderMap,
state: &super::PrototypeAdminState,
) -> bool {
let Some(user) = BuiltinUserEntity::find_by_email(&req.engine.db, email).await else {
return false;
};
let token = match crate::utils::reset_token::generate(
&req.engine.db,
user.user_id(),
ADMIN_RESET_TTL,
)
.await
{
Ok(token) => token,
Err(e) => {
tracing::error!(user_id = %user.user_id(), error = %e, "reset token generation failed");
return false;
}
};
let encrypted = crate::utils::reset_token::encrypt_email(&token, email);
let Some(reset_base) = admin_reset_base(req, state, headers) else {
return false;
};
let reset_url = format!("{reset_base}/{token}/{encrypted}");
let template_name = email_template.unwrap_or("admin/user_created_email.html");
let username_str = username.unwrap_or(email);
let mut ctx = tera::Context::new();
ctx.insert("username", username_str);
ctx.insert("email", email);
ctx.insert("reset_url", &reset_url);
ctx.insert(
"t_greeting",
t("admin.user_created.email_greeting").as_ref(),
);
ctx.insert("t_body", t("admin.user_created.email_body").as_ref());
ctx.insert("t_btn", t("admin.user_created.email_btn").as_ref());
ctx.insert(
"t_validity",
t("admin.user_created.email_validity").as_ref(),
);
let body_html = match req.engine.tera.render(template_name, &ctx) {
Ok(rendered) => rendered,
Err(_) => format!(
"<p>Hello {username_str},</p><p>Click on the link to set your password:</p><p><a href=\"{reset_url}\">{reset_url}</a></p>"
),
};
if crate::utils::mailer_configured() {
match crate::utils::Email::new()
.to(email)
.subject(t("admin.user_created.email_subject"))
.html(body_html)
.send()
.await
{
Ok(_) => {
req.notices
.success(crate::utils::trad::tf(
"admin.user_created.email_sent",
&[email],
))
.await;
true
}
Err(e) => {
req.notices
.error(crate::utils::trad::tf(
"admin.reset_password.error_send",
&[&e],
))
.await;
false
}
}
} else {
req.notices
.success(crate::utils::trad::tf(
"admin.reset_password.success_link",
&[&reset_url],
))
.await;
true
}
}
pub(super) async fn handle_reset_password(
req: &mut Request,
entry: &ResourceEntry,
id: String,
headers: &axum::http::HeaderMap,
state: &super::PrototypeAdminState,
) -> AppResult<Response> {
let detail_url = format!(
"{}/{}/{}/detail",
state.config.prefix.trim_end_matches('/'),
entry.meta.key,
id
);
let account = match id.parse::<crate::utils::pk::Pk>() {
Ok(user_id) if entry.is_account_table() => {
BuiltinUserEntity::find_by_id(&req.engine.db, user_id).await
}
_ => None,
};
let Some(account) = account.filter(|account| !account.email.trim().is_empty()) else {
req.notices
.error(t("admin.reset_password.error_unavailable"))
.await;
return Ok(Redirect::to(&detail_url).into_response());
};
let (user_id, email, username) = (account.id, account.email.clone(), account.username.clone());
let username = username.as_str();
let token =
match crate::utils::reset_token::generate(&req.engine.db, user_id, ADMIN_RESET_TTL).await {
Ok(token) => token,
Err(e) => {
tracing::error!(user_id = %user_id, error = %e, "reset token generation failed");
req.notices
.error(t("admin.reset_password.error_no_email"))
.await;
return Ok(Redirect::to(&detail_url).into_response());
}
};
let encrypted_email = crate::utils::reset_token::encrypt_email(&token, &email);
let Some(reset_base) = admin_reset_base(req, state, headers) else {
req.notices
.error(tf("admin.reset_password.error_send", &["with_public_url"]))
.await;
return Ok(Redirect::to(&detail_url).into_response());
};
let reset_url = format!("{reset_base}/{token}/{encrypted_email}");
if crate::utils::mailer_configured() {
let template_name = state
.config
.reset_password_email_template
.as_deref()
.unwrap_or("admin/reset_password_email.html");
let mut ctx = tera::Context::new();
ctx.insert("username", username);
ctx.insert("email", &email);
ctx.insert("reset_url", &reset_url);
ctx.insert("t_title", t("admin.reset_password.email_title").as_ref());
ctx.insert(
"t_greeting",
t("admin.reset_password.email_greeting").as_ref(),
);
ctx.insert("t_body", t("admin.reset_password.email_body").as_ref());
ctx.insert("t_btn", t("admin.reset_password.btn").as_ref());
ctx.insert("t_ignore", t("admin.reset_password.email_ignore").as_ref());
let body = match req.engine.tera.render(template_name, &ctx) {
Ok(rendered) => rendered,
Err(_) => format!(
"<p>Hello {username},</p><p>Click on the following link to reset your password (valid for 1 hour):</p><p><a href=\"{reset_url}\">{reset_url}</a></p>"
),
};
match crate::utils::Email::new()
.to(email.clone())
.subject(t("admin.reset_password.email_subject"))
.html(body)
.send()
.await
{
Ok(_) => {
req.notices
.success(crate::utils::trad::tf(
"admin.reset_password.success_email",
&[&email],
))
.await;
}
Err(e) => {
req.notices
.error(crate::utils::trad::tf(
"admin.reset_password.error_send",
&[&e],
))
.await;
}
}
} else {
req.notices
.success(crate::utils::trad::tf(
"admin.reset_password.success_link",
&[&reset_url],
))
.await;
}
Ok(Redirect::to(&detail_url).into_response())
}