runique 3.0.2

A Django-inspired web framework for Rust with ORM, templates, and comprehensive security middleware
Documentation
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};

/// Default lifetime of an admin-issued reset link (1 hour), matching the prior behavior.
const ADMIN_RESET_TTL: std::time::Duration = std::time::Duration::from_secs(3600);

/// The reset page the admin's links point to: the reset route on the site URL
/// (see [`public_link_base`](crate::auth::password::public_link_base)).
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('/')
    ))
}

/// Returns whether the email actually went out — callers must not show a
/// blanket success notice on top of the error notice this already pushes.
#[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 {
    // Resolve the just-created user to bind the token to their id (IDOR-safe).
    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
    );

    // The account is the only target: loaded by its id, and the link goes to
    // its own email — never to a field of the object shown, which on another
    // table would be anyone's address. Only the accounts table qualifies (the
    // built-in `users`, or a model extending it): elsewhere the id is no
    // account's id. The template shows the button on the same condition.
    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())
}