Lines
0 %
Functions
Branches
100 %
use askama::Template;
use axum::{
Extension, Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use serde::Deserialize;
use std::sync::Arc;
use uuid::Uuid;
use crate::{AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate};
fn fail(message: &str, err: impl std::fmt::Debug) -> (StatusCode, Json<serde_json::Value>) {
log::error!("{message}: {err:?}");
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "status": "fail", "message": message })),
)
}
/// Rejects a template whose source does not compile against the restricted
/// render surface — so a template naming a mutating/secret native (or any
/// syntax error) is caught at save time, never at render time.
fn validate_source(source: &str) -> Result<(), (StatusCode, Json<serde_json::Value>)> {
rpc::compile_template(source).map(|_| ()).map_err(|e| {
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"status": "fail",
"message": format!("Template does not compile: {e}"),
})),
})
#[derive(Template)]
#[template(path = "pages/template/create.html")]
struct TemplateCreatePage;
pub async fn template_create_page() -> impl IntoResponse {
HtmlTemplate(TemplateCreatePage {})
#[derive(Deserialize)]
pub struct TemplateCreateForm {
name: Option<String>,
source: String,
pub async fn create_template(
State(_data): State<Arc<AppState>>,
Extension(jwt_auth): Extension<JWTAuthMiddleware>,
Json(form): Json<TemplateCreateForm>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
validate_source(&form.source)?;
let server_user = server::user::User {
id: jwt_auth.user.id,
let name = form.name.filter(|n| !n.trim().is_empty());
let id = server_user
.create_template(form.source, name)
.await
.map_err(|e| fail("Failed to create template", e))?;
Ok(format!("{}: {}", t!("Template created"), id))
pub struct TemplateTestForm {
/// Dry-runs a template against the caller's own data: compiles it against the
/// render surface, renders it (DB-backed), and reports whether the resulting
/// draft can pre-fill the create form. Lets an author see "does this work?"
/// before saving — surfacing the same compile / render / not-representable
/// failures the create page would otherwise hit silently. Mutates nothing.
pub async fn test_template(
Json(form): Json<TemplateTestForm>,
) -> impl IntoResponse {
if let Err(e) = rpc::compile_template(&form.source) {
return Json(serde_json::json!({
"ok": false,
"stage": "compile",
"message": format!("{e}"),
}));
let ctx = rpc::ScriptCtx::new(jwt_auth.user.id);
let draft = match rpc::render_template(&ctx, &form.source).await {
Ok(d) => d,
Err(e) => {
"stage": "render",
let splits = draft.splits.len();
let tags = draft.tags.len();
let note = draft.note.clone();
let date = draft.date.clone();
let prefillable =
super::super::transaction::create::prefill::draft_to_prefilled(draft).is_some();
"ok": true,
"prefillable": prefillable,
"note": note,
"date": date,
"splits": splits,
"tags": tags,
"message": if prefillable {
format!("Renders OK: {splits} split(s), {tags} tag(s). Ready to pre-fill the create form.")
} else {
format!("Renders OK ({splits} split(s)), but the draft can't pre-fill the form: \
splits must be consecutive from→to pairs with exact, balancing amounts. \
It will still save; the create form just won't auto-fill.")
},
}))
#[template(path = "pages/template/edit.html")]
struct TemplateEditPage {
id: Uuid,
pub async fn template_edit_page(
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
let template = server_user
.get_template(id)
.map_err(|e| (StatusCode::NOT_FOUND, format!("Template not found: {e:?}")))?;
Ok(HtmlTemplate(TemplateEditPage {
id: template.id,
name: template.name,
source: template.source,
pub struct TemplateEditForm {
pub async fn edit_template(
Json(form): Json<TemplateEditForm>,
server_user
.update_template_source(form.id, form.source)
.map_err(|e| fail("Failed to update template source", e))?;
.update_template_name(form.id, form.name.filter(|n| !n.trim().is_empty()))
.map_err(|e| fail("Failed to update template name", e))?;
Ok(format!("{}: {}", t!("Template updated"), form.id))
pub async fn delete_template(
.delete_template(id)
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")))?;
Ok(format!("{}: {}", t!("Template deleted"), id))