1
#[macro_use]
2
extern crate rust_i18n;
3

            
4
i18n!("locales", fallback = "en");
5

            
6
mod auth_keys;
7
mod config;
8
mod files;
9
mod handler;
10
mod jwt_auth;
11
mod model;
12
mod pages;
13
mod redirect_middleware;
14
mod response;
15
mod route;
16
mod token;
17

            
18
use axum::{
19
    Router,
20
    extract::{MatchedPath, Request},
21
    middleware::{self, Next},
22
    response::IntoResponse,
23
    routing::get,
24
};
25

            
26
use axum::http::{
27
    HeaderValue, Method,
28
    header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
29
};
30

            
31
use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle};
32
use redis::Client;
33
use std::sync::Arc;
34
use std::{future::ready, time::Instant};
35
use tower_http::{cors::CorsLayer, services::ServeDir};
36
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
37

            
38
use config::Config;
39
#[cfg(feature = "scripting")]
40
use route::create_scripts_router;
41
use route::{
42
    create_accounts_router, create_api_router, create_pages_router, create_reports_router,
43
    create_tags_router, create_transactions_router,
44
};
45

            
46
pub struct AppState {
47
    conf: Config,
48
    redis_client: Client,
49
    frac: i64,
50
}
51

            
52
fn metrics_app() -> Router {
53
    let recorder_handle = setup_metrics_recorder();
54
    Router::new().route("/metrics", get(move || ready(recorder_handle.render())))
55
}
56

            
57
fn setup_metrics_recorder() -> PrometheusHandle {
58
    const EXPONENTIAL_SECONDS: &[f64] = &[
59
        0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
60
    ];
61

            
62
    PrometheusBuilder::new()
63
        .set_buckets_for_metric(
64
            Matcher::Full("http_requests_duration_seconds".to_string()),
65
            EXPONENTIAL_SECONDS,
66
        )
67
        .unwrap()
68
        .install_recorder()
69
        .unwrap()
70
}
71

            
72
async fn track_metrics(req: Request, next: Next) -> impl IntoResponse {
73
    let start = Instant::now();
74
    let path = if let Some(matched_path) = req.extensions().get::<MatchedPath>() {
75
        matched_path.as_str().to_owned()
76
    } else {
77
        req.uri().path().to_owned()
78
    };
79
    let method = req.method().clone();
80

            
81
    let response = next.run(req).await;
82

            
83
    let latency = start.elapsed().as_secs_f64();
84
    let status = response.status().as_u16().to_string();
85

            
86
    let labels = [
87
        ("method", method.to_string()),
88
        ("path", path),
89
        ("status", status),
90
    ];
91

            
92
    metrics::counter!("http_requests_total", &labels).increment(1);
93
    metrics::histogram!("http_requests_duration_seconds", &labels).record(latency);
94

            
95
    response
96
}
97

            
98
#[tokio::main]
99
async fn main() -> anyhow::Result<()> {
100
    tracing_subscriber::registry()
101
        .with(
102
            tracing_subscriber::EnvFilter::try_from_default_env()
103
                .unwrap_or_else(|_| "with_axum_htmx_askama=debug".into()),
104
        )
105
        .with(tracing_subscriber::fmt::layer())
106
        .try_init()
107
        .expect("Failed to set tracing subscriber");
108

            
109
    // Migrate + seed the admin DB before reading config: seeding moved out of
110
    // the migration set into the `seed_complete`-guarded bootstrap, so a fresh
111
    // DB has no `site_url` until `boot` runs. Idempotent on an already-booted
112
    // DB. Without this, `Config::init` panics with NoConfig("site_url").
113
    server::boot()
114
        .await
115
        .map_err(|e| anyhow::anyhow!("server boot failed: {e:?}"))?;
116
    log::debug!("Server boot complete");
117

            
118
    let conf = Config::init()
119
        .await
120
        .map_err(|e| anyhow::anyhow!("config init failed: {e:?}"))?;
121
    log::debug!("Config ready");
122

            
123
    let redis_client = Client::open(conf.redis_url.clone())
124
        .and_then(|client| {
125
            client.get_connection().map(|_| {
126
                log::debug!(
127
                    "Connection to the redis db {} successful!",
128
                    client.get_connection_info().redis_settings().db()
129
                );
130
                client
131
            })
132
        })
133
        .unwrap_or_else(|e| {
134
            log::error!("Error connecting to Redis: {e}");
135
            std::process::exit(1);
136
        });
137

            
138
    let cors = CorsLayer::new()
139
        .allow_origin(conf.site_url.parse::<HeaderValue>()?)
140
        .allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE])
141
        .allow_credentials(true)
142
        .allow_headers([AUTHORIZATION, ACCEPT, CONTENT_TYPE]);
143

            
144
    let state = Arc::new(AppState {
145
        conf,
146
        redis_client: redis_client.clone(),
147
        frac: 0,
148
    });
149

            
150
    let router = Router::new()
151
        .route("/", get(pages::index))
152
        .route("/register", get(pages::register))
153
        .route("/login", get(pages::login))
154
        .merge(create_pages_router(state.clone()))
155
        .merge(create_accounts_router(state.clone()))
156
        .merge(create_transactions_router(state.clone()))
157
        .merge(create_tags_router(state.clone()))
158
        .merge(create_reports_router(state.clone()));
159

            
160
    #[cfg(feature = "scripting")]
161
    let router = router.merge(create_scripts_router(state.clone()));
162

            
163
    let router = router
164
        .nest("/api", create_api_router(state.clone()))
165
        .nest_service(
166
            "/static",
167
            ServeDir::new(std::env::var("STATIC_PATH").unwrap_or("web/static".to_string())),
168
        )
169
        .with_state(state)
170
        .layer(cors)
171
        .route_layer(middleware::from_fn(track_metrics));
172

            
173
    let app = metrics_app();
174

            
175
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
176
    let metrics_listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await?;
177
    let (_main_server, _metrics_server) = tokio::join!(
178
        run_server(metrics_listener, app),
179
        run_server(listener, router)
180
    );
181
    Ok(())
182
}
183

            
184
async fn run_server(listener: tokio::net::TcpListener, app: Router) {
185
    axum::serve(listener, app).await.unwrap();
186
}