1
#[macro_use]
2
extern crate rust_i18n;
3

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
94
    response
95
}
96

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

            
108
    let conf = Config::init().await.unwrap();
109
    log::debug!("Config ready");
110

            
111
    let redis_client = Client::open(conf.redis_url.clone())
112
        .and_then(|client| {
113
            client.get_connection().map(|_| {
114
                log::debug!(
115
                    "Connection to the redis db {} successful!",
116
                    client.get_connection_info().redis_settings().db()
117
                );
118
                client
119
            })
120
        })
121
        .unwrap_or_else(|e| {
122
            log::error!("Error connecting to Redis: {e}");
123
            std::process::exit(1);
124
        });
125

            
126
    let cors = CorsLayer::new()
127
        .allow_origin(conf.site_url.parse::<HeaderValue>()?)
128
        .allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE])
129
        .allow_credentials(true)
130
        .allow_headers([AUTHORIZATION, ACCEPT, CONTENT_TYPE]);
131

            
132
    let state = Arc::new(AppState {
133
        conf,
134
        redis_client: redis_client.clone(),
135
        frac: 0,
136
    });
137

            
138
    let router = Router::new()
139
        .route("/", get(pages::index))
140
        .route("/register", get(pages::register))
141
        .route("/login", get(pages::login))
142
        .merge(create_pages_router(state.clone()))
143
        .merge(create_accounts_router(state.clone()))
144
        .merge(create_transactions_router(state.clone()))
145
        .merge(create_tags_router(state.clone()));
146

            
147
    #[cfg(feature = "scripting")]
148
    let router = router.merge(create_scripts_router(state.clone()));
149

            
150
    let router = router
151
        .nest("/api", create_api_router(state.clone()))
152
        .nest_service(
153
            "/static",
154
            ServeDir::new(std::env::var("STATIC_PATH").unwrap_or("web/static".to_string())),
155
        )
156
        .with_state(state)
157
        .layer(cors)
158
        .route_layer(middleware::from_fn(track_metrics));
159

            
160
    let app = metrics_app();
161

            
162
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
163
    let metrics_listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await?;
164
    let (_main_server, _metrics_server) = tokio::join!(
165
        run_server(metrics_listener, app),
166
        run_server(listener, router)
167
    );
168
    Ok(())
169
}
170

            
171
async fn run_server(listener: tokio::net::TcpListener, app: Router) {
172
    axum::serve(listener, app).await.unwrap();
173
}