Skip to main content

web/
route.rs

1use std::sync::Arc;
2
3use axum::{
4    Router, middleware,
5    routing::{get, post},
6};
7
8use crate::pages::script;
9use crate::pages::template;
10use crate::{
11    AppState,
12    files::download_file,
13    handler::{
14        get_home_link, get_logout_link, get_me_handler, get_version, login_form_handler,
15        login_user_handler, logout_handler, refresh_access_token_handler, register_user_handler,
16    },
17    jwt_auth::{admin, auth},
18    pages::{account, commodity, file_table, report, tag},
19    redirect_middleware::redirect_on_auth_error,
20};
21
22/// Pages only an admin may see.
23///
24/// Registration is invite-only: `POST /api/auth/register` has always been
25/// admin-gated, but the page carrying its form was public, so an anonymous
26/// visitor was handed a signup form whose every submission is refused. The
27/// page now sits behind the same gate as the endpoint it posts to.
28///
29/// Layer order matters: `auth` must run before `admin`, which reads the
30/// extension `auth` inserts, and `redirect_on_auth_error` wraps both so an
31/// HTML visitor is redirected rather than shown a JSON error.
32pub fn create_admin_pages_router(app_state: Arc<AppState>) -> Router<Arc<AppState>> {
33    Router::new()
34        .route("/register", get(crate::pages::register))
35        .route_layer(middleware::from_fn_with_state(app_state.clone(), admin))
36        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
37        .route_layer(middleware::from_fn(redirect_on_auth_error))
38        .with_state(app_state.clone())
39}
40
41pub fn create_pages_router(app_state: Arc<AppState>) -> Router<Arc<AppState>> {
42    Router::new()
43        .route(
44            "/commodity/create",
45            get(crate::pages::commodity::create::submit::commodity_create_page),
46        )
47        .route(
48            "/commodity/convert",
49            get(crate::pages::commodity::convert::submit::commodity_convert_page),
50        )
51        .route(
52            "/commodity/list",
53            get(crate::pages::commodity::list::commodity_list_page),
54        )
55        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
56        .route_layer(middleware::from_fn(redirect_on_auth_error))
57        .with_state(app_state.clone())
58}
59
60pub fn create_accounts_router(app_state: Arc<AppState>) -> Router<Arc<AppState>> {
61    Router::new()
62        .route(
63            "/account/create",
64            get(crate::pages::account::create::submit::account_create_page),
65        )
66        .route(
67            "/account/list",
68            get(crate::pages::account::list::account_list_page),
69        )
70        .route(
71            "/account/manage",
72            get(crate::pages::account::manage::account_manage_page),
73        )
74        .route(
75            "/account/ssh-key",
76            get(crate::pages::account::ssh_key::ssh_key_page),
77        )
78        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
79        .route_layer(middleware::from_fn(redirect_on_auth_error))
80        .with_state(app_state.clone())
81}
82
83pub fn create_transactions_router(app_state: Arc<AppState>) -> Router<Arc<AppState>> {
84    Router::new()
85        .route(
86            "/transaction/create",
87            get(crate::pages::transaction::create::submit::transaction_create_page),
88        )
89        .route(
90            "/transaction/list",
91            get(crate::pages::transaction::list::transaction_list_page),
92        )
93        .route(
94            "/transaction/edit/{id}",
95            get(crate::pages::transaction::edit::submit::transaction_edit_page),
96        )
97        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
98        .route_layer(middleware::from_fn(redirect_on_auth_error))
99        .with_state(app_state.clone())
100}
101
102pub fn create_tags_router(app_state: Arc<AppState>) -> Router<Arc<AppState>> {
103    Router::new()
104        .route(
105            "/tag/create",
106            get(crate::pages::tag::create::tag_create_page),
107        )
108        .route("/tag/list", get(crate::pages::tag::list::tag_list_page))
109        .route(
110            "/tag/edit/{id}",
111            get(crate::pages::tag::edit::tag_edit_page),
112        )
113        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
114        .route_layer(middleware::from_fn(redirect_on_auth_error))
115        .with_state(app_state.clone())
116}
117
118pub fn create_scripts_router(app_state: Arc<AppState>) -> Router<Arc<AppState>> {
119    Router::new()
120        .route("/script/create", get(script::edit::script_create_page))
121        .route("/script/list", get(script::list::script_list_page))
122        .route("/script/edit/{id}", get(script::edit::script_edit_page))
123        .route(
124            "/template/create",
125            get(template::edit::template_create_page),
126        )
127        .route("/template/list", get(template::list::template_list_page))
128        .route(
129            "/template/edit/{id}",
130            get(template::edit::template_edit_page),
131        )
132        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
133        .route_layer(middleware::from_fn(redirect_on_auth_error))
134        .with_state(app_state.clone())
135}
136
137pub fn create_reports_router(app_state: Arc<AppState>) -> Router<Arc<AppState>> {
138    Router::new()
139        .route("/report/balance", get(report::balance::balance_report_page))
140        .route(
141            "/report/activity",
142            get(report::activity::activity_report_page),
143        )
144        .route(
145            "/report/category-breakdown",
146            get(report::category_breakdown::category_breakdown_report_page),
147        )
148        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
149        .route_layer(middleware::from_fn(redirect_on_auth_error))
150        .with_state(app_state.clone())
151}
152
153pub fn create_api_router(app_state: Arc<AppState>) -> Router<Arc<AppState>> {
154    let public_router = Router::new()
155        .route("/auth/login", post(login_form_handler))
156        .route("/auth/login/json", post(login_user_handler))
157        .route("/version", get(get_version));
158
159    let auth_router = Router::new()
160        .route("/auth/refresh", get(refresh_access_token_handler))
161        .route("/files/list", get(file_table))
162        .route("/files/download/{file_name}", get(download_file))
163        .route("/auth/logout", get(logout_handler))
164        .route("/users/me", get(get_me_handler))
165        .route("/htmx/logoutlink", get(get_logout_link))
166        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
167        .with_state(app_state.clone());
168
169    let account_router = Router::new()
170        .route("/account/list", get(account::list::account_table))
171        .route("/account/info", get(account::list::account_info))
172        .route("/account/tree", get(account::manage::account_tree))
173        .route(
174            "/account/manage/view",
175            get(account::manage::account_manage_view),
176        )
177        .route(
178            "/account/manage/details",
179            get(account::manage::account_manage_details),
180        )
181        .route(
182            "/account/manage/tag",
183            post(account::manage::account_manage_set_tag),
184        )
185        .route(
186            "/account/manage/tag/submit",
187            post(account::manage::account_manage_set_tag_submit),
188        )
189        .route(
190            "/account/manage/tag/delete/{tag_id}",
191            post(account::manage::account_manage_delete_tag_submit),
192        )
193        .route("/account/search", post(account::search::search_accounts))
194        .route(
195            "/account/create/submit",
196            post(account::create::submit::create_account),
197        )
198        .route(
199            "/account/create/validate/name",
200            post(account::create::validate::validate_name),
201        )
202        .route("/account/rename", post(account::edit::rename_account))
203        .route(
204            "/account/{id}/tags/submit",
205            post(account::edit::account_tags_submit),
206        )
207        .route("/account/ssh-key/add", post(account::ssh_key::ssh_key_add))
208        .route(
209            "/account/ssh-key/remove",
210            post(account::ssh_key::ssh_key_remove),
211        )
212        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
213        .with_state(app_state.clone());
214
215    let nav_router = Router::new()
216        .route("/nav/home", get(get_home_link))
217        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
218        .with_state(app_state.clone());
219
220    let commodity_router = Router::new()
221        .route("/commodity/list", get(commodity::list::commodity_table))
222        .route(
223            "/commodity/search",
224            post(commodity::search::search_commodities),
225        )
226        .route(
227            "/commodity/create/submit",
228            post(commodity::create::submit::commodity_submit),
229        )
230        .route(
231            "/commodity/create/validate/symbol",
232            post(commodity::create::validate::validate_symbol),
233        )
234        .route(
235            "/commodity/create/validate/name",
236            post(commodity::create::validate::validate_name),
237        )
238        .route(
239            "/commodity/convert/submit",
240            post(commodity::convert::submit::commodity_convert_submit),
241        )
242        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
243        .with_state(app_state.clone());
244
245    let transaction_router = Router::new()
246        .route(
247            "/transaction/list",
248            get(crate::pages::transaction::list::transaction_table),
249        )
250        .route(
251            "/transaction/split/create",
252            get(crate::pages::transaction::create::split::split_create_handler),
253        )
254        .route(
255            "/transaction/create/submit",
256            post(crate::pages::transaction::create::submit::transaction_submit),
257        )
258        .route(
259            "/transaction/create/validate/amount",
260            post(crate::pages::transaction::validate::validate_amount_html),
261        )
262        .route(
263            "/transaction/create/validate/from_account",
264            post(crate::pages::transaction::validate::validate_from_account_html),
265        )
266        .route(
267            "/transaction/create/validate/to_account",
268            post(crate::pages::transaction::validate::validate_to_account_html),
269        )
270        .route(
271            "/transaction/create/validate/note",
272            post(crate::pages::transaction::validate::validate_note_html),
273        )
274        .route(
275            "/transaction/edit/submit",
276            post(crate::pages::transaction::edit::submit::transaction_edit_submit),
277        )
278        .route(
279            "/transaction/edit/validate/amount",
280            post(crate::pages::transaction::validate::validate_amount_json),
281        )
282        .route(
283            "/transaction/edit/validate/from_account",
284            post(crate::pages::transaction::validate::validate_from_account_json),
285        )
286        .route(
287            "/transaction/edit/validate/to_account",
288            post(crate::pages::transaction::validate::validate_to_account_json),
289        )
290        .route(
291            "/transaction/edit/validate/note",
292            post(crate::pages::transaction::validate::validate_note_json),
293        )
294        .route(
295            "/transaction/delete/{id}",
296            post(crate::pages::transaction::delete::submit::transaction_delete_submit),
297        )
298        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
299        .with_state(app_state.clone());
300
301    let tag_router = Router::new()
302        .route("/tag/list", get(tag::list::tag_table))
303        .route("/tag/create/submit", post(tag::create::create_tag))
304        .route(
305            "/transaction/{id}/tag/create/submit",
306            post(tag::create::create_transaction_tag),
307        )
308        .route(
309            "/split/{id}/tag/create/submit",
310            post(tag::create::create_split_tag),
311        )
312        .route(
313            "/account/{id}/tag/create/submit",
314            post(tag::create::create_account_tag),
315        )
316        .route("/tag/edit/submit", post(tag::edit::edit_tag))
317        .route("/tag/delete/{id}", post(tag::edit::delete_tag))
318        .route(
319            "/tags/transaction/names",
320            get(tag::autocomplete::transaction_tag_names),
321        )
322        .route(
323            "/tags/transaction/values",
324            get(tag::autocomplete::transaction_tag_values),
325        )
326        .route("/tags/split/names", get(tag::autocomplete::split_tag_names))
327        .route(
328            "/tags/split/values",
329            get(tag::autocomplete::split_tag_values),
330        )
331        .route(
332            "/tags/account/names",
333            get(tag::autocomplete::account_tag_names),
334        )
335        .route(
336            "/tags/account/values",
337            get(tag::autocomplete::account_tag_values),
338        )
339        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
340        .with_state(app_state.clone());
341
342    let script_router = Router::new()
343        .route("/script/list", get(script::list::script_table))
344        .route("/script/create/submit", post(script::edit::create_script))
345        .route("/script/edit/submit", post(script::edit::edit_script))
346        .route(
347            "/script/bytecode/{id}",
348            post(script::edit::upload_script_bytecode),
349        )
350        .route("/script/delete/{id}", post(script::edit::delete_script))
351        .route(
352            "/account/{account_id}/script/run/{script_id}",
353            post(account::edit::run_account_script),
354        )
355        .route("/template/list", get(template::list::template_table))
356        .route(
357            "/template/create/submit",
358            post(template::edit::create_template),
359        )
360        .route("/template/test", post(template::edit::test_template))
361        .route("/template/edit/submit", post(template::edit::edit_template))
362        .route(
363            "/template/delete/{id}",
364            post(template::edit::delete_template),
365        )
366        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
367        .with_state(app_state.clone());
368
369    let report_router = Router::new()
370        .route(
371            "/report/balance",
372            get(report::balance::balance_report_table),
373        )
374        .route(
375            "/report/balance/chart.svg",
376            get(report::balance::balance_report_chart_svg),
377        )
378        .route(
379            "/report/balance/chart.json",
380            get(report::balance::balance_report_chart_json),
381        )
382        .route(
383            "/report/activity",
384            get(report::activity::activity_report_table),
385        )
386        .route(
387            "/report/activity/chart.svg",
388            get(report::activity::activity_report_chart_svg),
389        )
390        .route(
391            "/report/activity/chart.json",
392            get(report::activity::activity_report_chart_json),
393        )
394        .route(
395            "/report/category-breakdown",
396            get(report::category_breakdown::category_breakdown_report_table),
397        )
398        .route(
399            "/report/category-breakdown/chart.svg",
400            get(report::category_breakdown::category_breakdown_report_chart_svg),
401        )
402        .route(
403            "/report/category-breakdown/chart.json",
404            get(report::category_breakdown::category_breakdown_report_chart_json),
405        )
406        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
407        .with_state(app_state.clone());
408
409    let admin_router = Router::new()
410        .route("/auth/register", post(register_user_handler))
411        .route_layer(middleware::from_fn_with_state(app_state.clone(), admin))
412        .route_layer(middleware::from_fn_with_state(app_state, auth));
413
414    let router = Router::new()
415        .merge(public_router)
416        .merge(nav_router)
417        .merge(auth_router)
418        .merge(account_router)
419        .merge(commodity_router)
420        .merge(transaction_router)
421        .merge(tag_router)
422        .merge(report_router)
423        .merge(admin_router);
424
425    router.merge(script_router)
426}