Single-binary self-hosted website analytics on Rust axum: collector API, dashboards, world map, and PDF reports.
analyticsaxumdockerrustself-hostedsqliteviteweb-analytics
1use axum::{
2 extract::State,
3 response::{IntoResponse, Redirect, Response},
4 routing::get,
5 Router,
6};
7use tower_cookies::Cookies;
8
9use crate::render::render;
10use crate::routes::auth::is_authenticated;
11use crate::AppState;
12
13pub fn router() -> Router<AppState> {
14 Router::new()
15 .route("/", get(home))
16 .route("/changelog", get(changelog))
17 .route("/documentation", get(documentation))
18}
19
20fn render_page(
21 state: &AppState,
22 template: &str,
23 title: &str,
24 description: &str,
25 authed: bool,
26 path: &str,
27 extra: minijinja::Value,
28) -> Response {
29 render(
30 state,
31 template,
32 path,
33 authed,
34 minijinja::context! {
35 page => minijinja::context! { title => title, description => description },
36 ..extra
37 },
38 )
39}
40
41pub async fn home(State(state): State<AppState>, cookies: Cookies) -> Response {
42 let authed = is_authenticated(&cookies, &state);
43 if authed {
44 return Redirect::to("/properties").into_response();
45 }
46 let totals: (i64, i64, Option<i64>) = sqlx::query_as(
47 "SELECT \
48 (SELECT COUNT(*) FROM properties), \
49 (SELECT COUNT(*) FROM events), \
50 (SELECT MIN(created_at) FROM events)",
51 )
52 .fetch_one(&state.pool)
53 .await
54 .unwrap_or((0, 0, None));
55
56 let first = totals.2.and_then(|ms| {
57 chrono::DateTime::<chrono::Utc>::from_timestamp_millis(ms)
58 .map(|d| d.format("%b %-d, %Y").to_string())
59 });
60
61 render_page(
62 &state,
63 "pages/home.html",
64 "Self-hosted analytics",
65 "Self-hosted website analytics. Page views, clicks, scrolls, sessions, and custom events.",
66 authed,
67 "/",
68 minijinja::context! {
69 total_properties => totals.0,
70 total_events => totals.1,
71 total_users => 1,
72 first_event_created_at => first,
73 },
74 )
75}
76
77pub async fn changelog(State(state): State<AppState>, cookies: Cookies) -> Response {
78 let authed = is_authenticated(&cookies, &state);
79 render_page(
80 &state,
81 "pages/changelog.html",
82 "Changelog",
83 "What's new in Analytics.",
84 authed,
85 "/changelog",
86 minijinja::context! {},
87 )
88}
89
90pub async fn documentation(State(state): State<AppState>, cookies: Cookies) -> Response {
91 let authed = is_authenticated(&cookies, &state);
92 render_page(
93 &state,
94 "pages/documentation.html",
95 "Documentation",
96 "How to embed and operate Analytics.",
97 authed,
98 "/documentation",
99 minijinja::context! {},
100 )
101}