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