runique 2.2.0

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::session::UserEntity;
use crate::auth::user::BuiltinUserEntity;
use crate::auth::user_trait::RuniqueUser;
use crate::context::template::{AppError, Request};
use crate::errors::error::ErrorContext;
use crate::utils::{aliases::AppResult, trad::t};
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);

/// 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 Ok(token) =
        crate::utils::reset_token::generate(&req.engine.db, user.user_id(), ADMIN_RESET_TTL).await
    else {
        return false;
    };
    let encrypted = crate::utils::reset_token::encrypt_email(&token, email);

    let reset_url = if let Some(base) = &state.config.reset_password_url {
        format!("{}/{}/{}", base.trim_end_matches('/'), token, encrypted)
    } else {
        let host = headers
            .get(axum::http::header::HOST)
            .and_then(|v| v.to_str().ok())
            .unwrap_or("localhost");
        format!("http://{}/reset-password/{}/{}", host, 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 object = match &entry.get_fn {
        Some(f) => f(req.engine.db.clone(), id.clone())
            .await
            .map_err(|e| Box::new(AppError::new(ErrorContext::database(e))))?,
        None => None,
    };

    let detail_url = format!(
        "{}/{}/{}/detail",
        state.config.prefix.trim_end_matches('/'),
        entry.meta.key,
        id
    );

    let fields = object.as_ref().and_then(|v| v.as_object());

    let email = fields
        .and_then(|m| m.get("email"))
        .and_then(|v| v.as_str())
        .map(|s| s.to_string());

    let Some(email) = email else {
        req.notices
            .error(t("admin.reset_password.error_no_email"))
            .await;
        return Ok(Redirect::to(&detail_url).into_response());
    };

    let username = fields
        .and_then(|m| m.get("username").or_else(|| m.get("name")))
        .and_then(|v| v.as_str())
        .unwrap_or(&email);

    // The admin object's pk is the authoritative target — bind the token to it.
    let Ok(user_id) = id.parse::<crate::utils::pk::Pk>() else {
        req.notices
            .error(t("admin.reset_password.error_no_email"))
            .await;
        return Ok(Redirect::to(&detail_url).into_response());
    };
    let Ok(token) =
        crate::utils::reset_token::generate(&req.engine.db, user_id, ADMIN_RESET_TTL).await
    else {
        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 reset_url = if let Some(base) = &state.config.reset_password_url {
        format!(
            "{}/{}/{}",
            base.trim_end_matches('/'),
            token,
            encrypted_email
        )
    } else {
        let host = headers
            .get(axum::http::header::HOST)
            .and_then(|v| v.to_str().ok())
            .unwrap_or("localhost");
        format!(
            "http://{}/reset-password/{}/{}",
            host, 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())
}