1
use askama::Template;
2
use axum::{
3
    Extension, Json,
4
    extract::{Path, State},
5
    http::StatusCode,
6
    response::IntoResponse,
7
};
8
use serde::Deserialize;
9
use std::sync::Arc;
10
use uuid::Uuid;
11

            
12
use crate::{AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate};
13

            
14
fn 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.
25
fn 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")]
39
struct TemplateCreatePage;
40

            
41
pub async fn template_create_page() -> impl IntoResponse {
42
    HtmlTemplate(TemplateCreatePage {})
43
}
44

            
45
#[derive(Deserialize)]
46
pub struct TemplateCreateForm {
47
    name: Option<String>,
48
    source: String,
49
}
50

            
51
pub 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)]
71
pub 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.
80
pub 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")]
132
struct TemplateEditPage {
133
    id: Uuid,
134
    name: Option<String>,
135
    source: String,
136
}
137

            
138
pub 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)]
159
pub struct TemplateEditForm {
160
    id: Uuid,
161
    name: Option<String>,
162
    source: String,
163
}
164

            
165
pub 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

            
187
pub 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
}