1
use std::sync::Arc;
2

            
3
use axum::{
4
    Router, middleware,
5
    routing::{get, post},
6
};
7

            
8
use crate::pages::script;
9
use crate::pages::template;
10
use 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.
34
2
pub fn create_admin_pages_router(app_state: Arc<AppState>) -> Router<Arc<AppState>> {
35
2
    Router::new()
36
2
        .route("/register", get(crate::pages::register))
37
2
        .route_layer(middleware::from_fn_with_state(app_state.clone(), admin))
38
2
        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
39
2
        .route_layer(middleware::from_fn(redirect_on_auth_error))
40
2
        .with_state(app_state.clone())
41
2
}
42

            
43
1
pub fn create_pages_router(app_state: Arc<AppState>) -> Router<Arc<AppState>> {
44
1
    Router::new()
45
1
        .route(
46
1
            "/commodity/create",
47
1
            get(crate::pages::commodity::create::submit::commodity_create_page),
48
        )
49
1
        .route(
50
1
            "/commodity/convert",
51
1
            get(crate::pages::commodity::convert::submit::commodity_convert_page),
52
        )
53
1
        .route(
54
1
            "/commodity/list",
55
1
            get(crate::pages::commodity::list::commodity_list_page),
56
        )
57
1
        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
58
1
        .route_layer(middleware::from_fn(redirect_on_auth_error))
59
1
        .with_state(app_state.clone())
60
1
}
61

            
62
1
pub fn create_accounts_router(app_state: Arc<AppState>) -> Router<Arc<AppState>> {
63
1
    Router::new()
64
1
        .route(
65
1
            "/account/create",
66
1
            get(crate::pages::account::create::submit::account_create_page),
67
        )
68
1
        .route(
69
1
            "/account/list",
70
1
            get(crate::pages::account::list::account_list_page),
71
        )
72
1
        .route(
73
1
            "/account/manage",
74
1
            get(crate::pages::account::manage::account_manage_page),
75
        )
76
1
        .route(
77
1
            "/account/ssh-key",
78
1
            get(crate::pages::account::ssh_key::ssh_key_page),
79
        )
80
1
        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
81
1
        .route_layer(middleware::from_fn(redirect_on_auth_error))
82
1
        .with_state(app_state.clone())
83
1
}
84

            
85
1
pub fn create_transactions_router(app_state: Arc<AppState>) -> Router<Arc<AppState>> {
86
1
    Router::new()
87
1
        .route(
88
1
            "/transaction/create",
89
1
            get(crate::pages::transaction::create::submit::transaction_create_page),
90
        )
91
1
        .route(
92
1
            "/transaction/list",
93
1
            get(crate::pages::transaction::list::transaction_list_page),
94
        )
95
1
        .route(
96
1
            "/transaction/edit/{id}",
97
1
            get(crate::pages::transaction::edit::submit::transaction_edit_page),
98
        )
99
1
        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
100
1
        .route_layer(middleware::from_fn(redirect_on_auth_error))
101
1
        .with_state(app_state.clone())
102
1
}
103

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

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

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

            
155
4
pub fn create_api_router(
156
4
    app_state: Arc<AppState>,
157
4
    limiter: Arc<AuthLimiter>,
158
4
) -> 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
4
    let login_router = Router::new()
164
4
        .route("/auth/login", post(login_form_handler))
165
4
        .route("/auth/login/json", post(login_user_handler))
166
4
        .route_layer(middleware::from_fn_with_state(
167
4
            limiter.clone(),
168
            throttle_auth,
169
        ));
170

            
171
4
    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
4
    let password_router = Router::new()
176
4
        .route("/auth/password", post(change_password_handler))
177
4
        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
178
4
        .route_layer(middleware::from_fn_with_state(limiter, throttle_auth))
179
4
        .with_state(app_state.clone());
180

            
181
4
    let auth_router = Router::new()
182
4
        .route("/auth/refresh", get(refresh_access_token_handler))
183
4
        .route("/files/list", get(file_table))
184
4
        .route("/files/download/{file_name}", get(download_file))
185
4
        .route("/auth/logout", get(logout_handler))
186
4
        .route("/users/me", get(get_me_handler))
187
4
        .route("/htmx/logoutlink", get(get_logout_link))
188
4
        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
189
4
        .with_state(app_state.clone());
190

            
191
4
    let account_router = Router::new()
192
4
        .route("/account/list", get(account::list::account_table))
193
4
        .route("/account/info", get(account::list::account_info))
194
4
        .route("/account/tree", get(account::manage::account_tree))
195
4
        .route(
196
4
            "/account/manage/view",
197
4
            get(account::manage::account_manage_view),
198
        )
199
4
        .route(
200
4
            "/account/manage/details",
201
4
            get(account::manage::account_manage_details),
202
        )
203
4
        .route(
204
4
            "/account/manage/tag",
205
4
            post(account::manage::account_manage_set_tag),
206
        )
207
4
        .route(
208
4
            "/account/manage/tag/submit",
209
4
            post(account::manage::account_manage_set_tag_submit),
210
        )
211
4
        .route(
212
4
            "/account/manage/tag/delete/{tag_id}",
213
4
            post(account::manage::account_manage_delete_tag_submit),
214
        )
215
4
        .route("/account/search", post(account::search::search_accounts))
216
4
        .route(
217
4
            "/account/create/submit",
218
4
            post(account::create::submit::create_account),
219
        )
220
4
        .route(
221
4
            "/account/create/validate/name",
222
4
            post(account::create::validate::validate_name),
223
        )
224
4
        .route("/account/rename", post(account::edit::rename_account))
225
4
        .route(
226
4
            "/account/{id}/tags/submit",
227
4
            post(account::edit::account_tags_submit),
228
        )
229
4
        .route("/account/ssh-key/add", post(account::ssh_key::ssh_key_add))
230
4
        .route(
231
4
            "/account/ssh-key/remove",
232
4
            post(account::ssh_key::ssh_key_remove),
233
        )
234
4
        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
235
4
        .with_state(app_state.clone());
236

            
237
4
    let nav_router = Router::new()
238
4
        .route("/nav/home", get(get_home_link))
239
4
        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
240
4
        .with_state(app_state.clone());
241

            
242
4
    let commodity_router = Router::new()
243
4
        .route("/commodity/list", get(commodity::list::commodity_table))
244
4
        .route(
245
4
            "/commodity/search",
246
4
            post(commodity::search::search_commodities),
247
        )
248
4
        .route(
249
4
            "/commodity/create/submit",
250
4
            post(commodity::create::submit::commodity_submit),
251
        )
252
4
        .route(
253
4
            "/commodity/create/validate/symbol",
254
4
            post(commodity::create::validate::validate_symbol),
255
        )
256
4
        .route(
257
4
            "/commodity/create/validate/name",
258
4
            post(commodity::create::validate::validate_name),
259
        )
260
4
        .route(
261
4
            "/commodity/convert/submit",
262
4
            post(commodity::convert::submit::commodity_convert_submit),
263
        )
264
4
        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
265
4
        .with_state(app_state.clone());
266

            
267
4
    let transaction_router = Router::new()
268
4
        .route(
269
4
            "/transaction/list",
270
4
            get(crate::pages::transaction::list::transaction_table),
271
        )
272
4
        .route(
273
4
            "/transaction/split/create",
274
4
            get(crate::pages::transaction::create::split::split_create_handler),
275
        )
276
4
        .route(
277
4
            "/transaction/create/submit",
278
4
            post(crate::pages::transaction::create::submit::transaction_submit),
279
        )
280
4
        .route(
281
4
            "/transaction/create/validate/amount",
282
4
            post(crate::pages::transaction::validate::validate_amount_html),
283
        )
284
4
        .route(
285
4
            "/transaction/create/validate/from_account",
286
4
            post(crate::pages::transaction::validate::validate_from_account_html),
287
        )
288
4
        .route(
289
4
            "/transaction/create/validate/to_account",
290
4
            post(crate::pages::transaction::validate::validate_to_account_html),
291
        )
292
4
        .route(
293
4
            "/transaction/create/validate/note",
294
4
            post(crate::pages::transaction::validate::validate_note_html),
295
        )
296
4
        .route(
297
4
            "/transaction/edit/submit",
298
4
            post(crate::pages::transaction::edit::submit::transaction_edit_submit),
299
        )
300
4
        .route(
301
4
            "/transaction/edit/validate/amount",
302
4
            post(crate::pages::transaction::validate::validate_amount_json),
303
        )
304
4
        .route(
305
4
            "/transaction/edit/validate/from_account",
306
4
            post(crate::pages::transaction::validate::validate_from_account_json),
307
        )
308
4
        .route(
309
4
            "/transaction/edit/validate/to_account",
310
4
            post(crate::pages::transaction::validate::validate_to_account_json),
311
        )
312
4
        .route(
313
4
            "/transaction/edit/validate/note",
314
4
            post(crate::pages::transaction::validate::validate_note_json),
315
        )
316
4
        .route(
317
4
            "/transaction/delete/{id}",
318
4
            post(crate::pages::transaction::delete::submit::transaction_delete_submit),
319
        )
320
4
        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
321
4
        .with_state(app_state.clone());
322

            
323
4
    let tag_router = Router::new()
324
4
        .route("/tag/list", get(tag::list::tag_table))
325
4
        .route("/tag/create/submit", post(tag::create::create_tag))
326
4
        .route(
327
4
            "/transaction/{id}/tag/create/submit",
328
4
            post(tag::create::create_transaction_tag),
329
        )
330
4
        .route(
331
4
            "/split/{id}/tag/create/submit",
332
4
            post(tag::create::create_split_tag),
333
        )
334
4
        .route(
335
4
            "/account/{id}/tag/create/submit",
336
4
            post(tag::create::create_account_tag),
337
        )
338
4
        .route("/tag/edit/submit", post(tag::edit::edit_tag))
339
4
        .route("/tag/delete/{id}", post(tag::edit::delete_tag))
340
4
        .route(
341
4
            "/tags/transaction/names",
342
4
            get(tag::autocomplete::transaction_tag_names),
343
        )
344
4
        .route(
345
4
            "/tags/transaction/values",
346
4
            get(tag::autocomplete::transaction_tag_values),
347
        )
348
4
        .route("/tags/split/names", get(tag::autocomplete::split_tag_names))
349
4
        .route(
350
4
            "/tags/split/values",
351
4
            get(tag::autocomplete::split_tag_values),
352
        )
353
4
        .route(
354
4
            "/tags/account/names",
355
4
            get(tag::autocomplete::account_tag_names),
356
        )
357
4
        .route(
358
4
            "/tags/account/values",
359
4
            get(tag::autocomplete::account_tag_values),
360
        )
361
4
        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
362
4
        .with_state(app_state.clone());
363

            
364
4
    let script_router = Router::new()
365
4
        .route("/script/list", get(script::list::script_table))
366
4
        .route("/script/create/submit", post(script::edit::create_script))
367
4
        .route("/script/edit/submit", post(script::edit::edit_script))
368
4
        .route(
369
4
            "/script/bytecode/{id}",
370
4
            post(script::edit::upload_script_bytecode),
371
        )
372
4
        .route("/script/delete/{id}", post(script::edit::delete_script))
373
4
        .route(
374
4
            "/account/{account_id}/script/run/{script_id}",
375
4
            post(account::edit::run_account_script),
376
        )
377
4
        .route("/template/list", get(template::list::template_table))
378
4
        .route(
379
4
            "/template/create/submit",
380
4
            post(template::edit::create_template),
381
        )
382
4
        .route("/template/test", post(template::edit::test_template))
383
4
        .route("/template/edit/submit", post(template::edit::edit_template))
384
4
        .route(
385
4
            "/template/delete/{id}",
386
4
            post(template::edit::delete_template),
387
        )
388
4
        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
389
4
        .with_state(app_state.clone());
390

            
391
4
    let report_router = Router::new()
392
4
        .route(
393
4
            "/report/balance",
394
4
            get(report::balance::balance_report_table),
395
        )
396
4
        .route(
397
4
            "/report/balance/chart.svg",
398
4
            get(report::balance::balance_report_chart_svg),
399
        )
400
4
        .route(
401
4
            "/report/balance/chart.json",
402
4
            get(report::balance::balance_report_chart_json),
403
        )
404
4
        .route(
405
4
            "/report/activity",
406
4
            get(report::activity::activity_report_table),
407
        )
408
4
        .route(
409
4
            "/report/activity/chart.svg",
410
4
            get(report::activity::activity_report_chart_svg),
411
        )
412
4
        .route(
413
4
            "/report/activity/chart.json",
414
4
            get(report::activity::activity_report_chart_json),
415
        )
416
4
        .route(
417
4
            "/report/category-breakdown",
418
4
            get(report::category_breakdown::category_breakdown_report_table),
419
        )
420
4
        .route(
421
4
            "/report/category-breakdown/chart.svg",
422
4
            get(report::category_breakdown::category_breakdown_report_chart_svg),
423
        )
424
4
        .route(
425
4
            "/report/category-breakdown/chart.json",
426
4
            get(report::category_breakdown::category_breakdown_report_chart_json),
427
        )
428
4
        .route_layer(middleware::from_fn_with_state(app_state.clone(), auth))
429
4
        .with_state(app_state.clone());
430

            
431
4
    let admin_router = Router::new()
432
4
        .route("/auth/register", post(register_user_handler))
433
4
        .route_layer(middleware::from_fn_with_state(app_state.clone(), admin))
434
4
        .route_layer(middleware::from_fn_with_state(app_state, auth));
435

            
436
4
    let router = Router::new()
437
4
        .merge(public_router)
438
4
        .merge(password_router)
439
4
        .merge(nav_router)
440
4
        .merge(auth_router)
441
4
        .merge(account_router)
442
4
        .merge(commodity_router)
443
4
        .merge(transaction_router)
444
4
        .merge(tag_router)
445
4
        .merge(report_router)
446
4
        .merge(admin_router);
447

            
448
4
    router.merge(script_router)
449
4
}