Skip to main content

web/pages/template/
edit.rs

1use askama::Template;
2use axum::{
3    Extension, Json,
4    extract::{Path, State},
5    http::StatusCode,
6    response::IntoResponse,
7};
8use serde::Deserialize;
9use std::sync::Arc;
10use uuid::Uuid;
11
12use crate::{AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate};
13
14fn fail(message: &str, err: impl std::fmt::Debug) -> (StatusCode, Json<serde_json::Value>) {
15    log::error!("{message}: {err:?}");
16    (
17        StatusCode::INTERNAL_SERVER_ERROR,
18        Json(serde_json::json!({ "status": "fail", "message": message })),
19    )
20}
21
22/// Rejects a template whose source does not compile against the restricted
23/// render surface — so a template naming a mutating/secret native (or any
24/// syntax error) is caught at save time, never at render time.
25fn validate_source(source: &str) -> Result<(), (StatusCode, Json<serde_json::Value>)> {
26    rpc::compile_template(source).map(|_| ()).map_err(|e| {
27        (
28            StatusCode::BAD_REQUEST,
29            Json(serde_json::json!({
30                "status": "fail",
31                "message": format!("Template does not compile: {e}"),
32            })),
33        )
34    })
35}
36
37#[derive(Template)]
38#[template(path = "pages/template/create.html")]
39struct TemplateCreatePage;
40
41pub async fn template_create_page() -> impl IntoResponse {
42    HtmlTemplate(TemplateCreatePage {})
43}
44
45#[derive(Deserialize)]
46pub struct TemplateCreateForm {
47    name: Option<String>,
48    source: String,
49}
50
51pub async fn create_template(
52    State(_data): State<Arc<AppState>>,
53    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
54    Json(form): Json<TemplateCreateForm>,
55) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
56    validate_source(&form.source)?;
57
58    let server_user = server::user::User {
59        id: jwt_auth.user.id,
60    };
61    let name = form.name.filter(|n| !n.trim().is_empty());
62    let id = server_user
63        .create_template(form.source, name)
64        .await
65        .map_err(|e| fail("Failed to create template", e))?;
66
67    Ok(format!("{}: {}", t!("Template created"), id))
68}
69
70#[derive(Deserialize)]
71pub struct TemplateTestForm {
72    source: String,
73}
74
75/// Dry-runs a template against the caller's own data: compiles it against the
76/// render surface, renders it (DB-backed), and reports whether the resulting
77/// draft can pre-fill the create form. Lets an author see "does this work?"
78/// before saving — surfacing the same compile / render / not-representable
79/// failures the create page would otherwise hit silently. Mutates nothing.
80pub async fn test_template(
81    State(_data): State<Arc<AppState>>,
82    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
83    Json(form): Json<TemplateTestForm>,
84) -> impl IntoResponse {
85    if let Err(e) = rpc::compile_template(&form.source) {
86        return Json(serde_json::json!({
87            "ok": false,
88            "stage": "compile",
89            "message": format!("{e}"),
90        }));
91    }
92
93    let ctx = rpc::ScriptCtx::new(jwt_auth.user.id);
94    let draft = match rpc::render_template(&ctx, &form.source).await {
95        Ok(d) => d,
96        Err(e) => {
97            return Json(serde_json::json!({
98                "ok": false,
99                "stage": "render",
100                "message": format!("{e}"),
101            }));
102        }
103    };
104
105    let splits = draft.splits.len();
106    let tags = draft.tags.len();
107    let note = draft.note.clone();
108    let date = draft.date.clone();
109    let prefillable =
110        super::super::transaction::create::prefill::draft_to_prefilled(draft).is_some();
111
112    Json(serde_json::json!({
113        "ok": true,
114        "stage": "render",
115        "prefillable": prefillable,
116        "note": note,
117        "date": date,
118        "splits": splits,
119        "tags": tags,
120        "message": if prefillable {
121            format!("Renders OK: {splits} split(s), {tags} tag(s). Ready to pre-fill the create form.")
122        } else {
123            format!("Renders OK ({splits} split(s)), but the draft can't pre-fill the form: \
124                     splits must be consecutive from→to pairs with exact, balancing amounts. \
125                     It will still save; the create form just won't auto-fill.")
126        },
127    }))
128}
129
130#[derive(Template)]
131#[template(path = "pages/template/edit.html")]
132struct TemplateEditPage {
133    id: Uuid,
134    name: Option<String>,
135    source: String,
136}
137
138pub async fn template_edit_page(
139    State(_data): State<Arc<AppState>>,
140    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
141    Path(id): Path<Uuid>,
142) -> Result<impl IntoResponse, (StatusCode, String)> {
143    let server_user = server::user::User {
144        id: jwt_auth.user.id,
145    };
146    let template = server_user
147        .get_template(id)
148        .await
149        .map_err(|e| (StatusCode::NOT_FOUND, format!("Template not found: {e:?}")))?;
150
151    Ok(HtmlTemplate(TemplateEditPage {
152        id: template.id,
153        name: template.name,
154        source: template.source,
155    }))
156}
157
158#[derive(Deserialize)]
159pub struct TemplateEditForm {
160    id: Uuid,
161    name: Option<String>,
162    source: String,
163}
164
165pub async fn edit_template(
166    State(_data): State<Arc<AppState>>,
167    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
168    Json(form): Json<TemplateEditForm>,
169) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
170    validate_source(&form.source)?;
171
172    let server_user = server::user::User {
173        id: jwt_auth.user.id,
174    };
175    server_user
176        .update_template_source(form.id, form.source)
177        .await
178        .map_err(|e| fail("Failed to update template source", e))?;
179    server_user
180        .update_template_name(form.id, form.name.filter(|n| !n.trim().is_empty()))
181        .await
182        .map_err(|e| fail("Failed to update template name", e))?;
183
184    Ok(format!("{}: {}", t!("Template updated"), form.id))
185}
186
187pub async fn delete_template(
188    State(_data): State<Arc<AppState>>,
189    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
190    Path(id): Path<Uuid>,
191) -> Result<impl IntoResponse, (StatusCode, String)> {
192    let server_user = server::user::User {
193        id: jwt_auth.user.id,
194    };
195    server_user
196        .delete_template(id)
197        .await
198        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")))?;
199
200    Ok(format!("{}: {}", t!("Template deleted"), id))
201}