Skip to content

Commit 4efaefc

Browse files
chore: more progress
Signed-off-by: Henry Gressmann <mail@henrygressmann.de>
1 parent 861539e commit 4efaefc

18 files changed

Lines changed: 460 additions & 146 deletions

File tree

Cargo.lock

Lines changed: 296 additions & 36 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,31 +32,44 @@ async-compression={version="0.4", default-features=false, features=["gzip", "tok
3232
tokio-tar={package="astral-tokio-tar", version="0.5"}
3333
sha3={version="0.10"}
3434
argon2={version="0.5", features=["rand"]}
35+
password-hash={version="0.5", features=["rand_core", "getrandom"]}
3536
zstd={version="0.13", default-features=false}
3637
uuid={version="1.19", features=["v4"]}
3738

3839
# general
3940
argh={version="0.1", default-features=false, features=["help"]}
4041
anyhow={version="1.0"}
4142
rand={version="0.9", default-features=false, features=["std", "thread_rng"]}
42-
chrono={version="0.4", default-features=false, features=["std", "now"]}
43+
chrono={version="0.4", default-features=false, features=["std", "now", "serde"]}
4344
figment={version="0.10", features=["toml", "env"]}
4445
tracing={version="0.1", default-features=false, features=["std"]}
4546
tracing-subscriber={version="0.3", features=["env-filter"]}
4647
ahash="0.8"
4748

4849
# web
4950
axum="0.8"
50-
axum-extra={version="0.12", default-features=false, features=["cookie"]}
51+
axum-extra={version="0.12", default-features=false, features=["cookie", "typed-header"]}
5152
http="1.4"
52-
tower={version="0.5", default-features=false, features=["limit"]}
53+
headers="0.4"
54+
tower={version="0.5", default-features=false}
5355
tower-http={version="0.6", default-features=false, features=[
5456
"cors",
5557
"compression-zstd",
5658
"set-header",
5759
]}
58-
aide={version="0.15", default-features=false, features=["axum"]}
59-
schemars={version="1.2", features=["derive"]}
60+
tower_governor={version="0.8", default-features=false, features=["axum"]}
61+
aide={version="0.16.0-alpha.1", default-features=false, features=[
62+
"axum",
63+
"axum-json",
64+
"axum-query",
65+
"axum-matched-path",
66+
"axum-tokio",
67+
"axum-extra",
68+
"axum-extra-cookie",
69+
"axum-extra-headers",
70+
"macros",
71+
]}
72+
schemars={version="1.2", features=["derive", "chrono04"]}
6073

6174
ua-parser="0.2"
6275
rust-embed={version="8.9", features=["mime-guess"]}

src/app/core/geoip.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ fn get_file_meta(path: &PathBuf) -> Option<(u64, u64, u64, i64)> {
195195
#[cfg(unix)]
196196
{
197197
use std::os::unix::fs::MetadataExt;
198-
return Some((md.dev(), md.ino(), md.size(), md.mtime()));
198+
Some((md.dev(), md.ino(), md.size(), md.mtime()))
199199
}
200200

201201
#[cfg(windows)]

src/app/core/reports.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ use crate::utils::duckdb::{ParamVec, repeat_vars};
66
use anyhow::{Result, bail};
77
use chrono::{DateTime, Utc};
88
use duckdb::params_from_iter;
9-
use poem_openapi::{Enum, Object};
9+
use schemars::JsonSchema;
10+
use serde::{Deserialize, Serialize};
1011

1112
pub use super::reports_cached::*;
1213

13-
#[derive(Object, Debug, Clone, Hash, PartialEq, Eq)]
14+
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Hash, PartialEq, Eq)]
1415
pub struct DateRange {
1516
pub start: DateTime<Utc>,
1617
pub end: DateTime<Utc>,
@@ -37,7 +38,7 @@ impl Display for DateRange {
3738
}
3839
}
3940

40-
#[derive(Debug, Enum, Clone, Copy, PartialEq, Eq, Hash)]
41+
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, Copy, PartialEq, Eq, Hash)]
4142
#[serde(rename_all = "snake_case")]
4243
pub enum Metric {
4344
Views,
@@ -46,7 +47,7 @@ pub enum Metric {
4647
AvgTimeOnSite,
4748
}
4849

49-
#[derive(Debug, Enum, Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord)]
50+
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord)]
5051
#[serde(rename_all = "snake_case")]
5152
pub enum Dimension {
5253
Url,
@@ -65,7 +66,7 @@ pub enum Dimension {
6566
UtmTerm,
6667
}
6768

68-
#[derive(Enum, Debug, Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord)]
69+
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord)]
6970
#[serde(rename_all = "snake_case")]
7071
pub enum FilterType {
7172
// Generic filters
@@ -85,7 +86,7 @@ pub enum FilterType {
8586
pub type ReportGraph = Vec<f64>;
8687
pub type ReportTable = BTreeMap<String, f64>;
8788

88-
#[derive(Object, Clone, Debug, Default)]
89+
#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, Default)]
8990
#[serde(rename_all = "camelCase")]
9091
pub struct ReportStats {
9192
pub total_views: u64,
@@ -94,7 +95,7 @@ pub struct ReportStats {
9495
pub avg_time_on_site: f64,
9596
}
9697

97-
#[derive(Object, Debug, Clone, Hash, Eq, PartialEq, PartialOrd, Ord)]
98+
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Hash, Eq, PartialEq, PartialOrd, Ord)]
9899
#[serde(rename_all = "camelCase")]
99100
pub struct DimensionFilter {
100101
/// The dimension to filter by

src/utils/hash.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@ use argon2::password_hash::{PasswordHasher, SaltString, rand_core};
44

55
use anyhow::Result;
66
use rand::RngCore;
7-
use rand::rngs::OsRng;
87
use sha3::Digest;
98
use std::net::IpAddr;
109

1110
pub fn hash_password(password: &str) -> Result<String> {
12-
let salt = SaltString::generate(&mut OsRng);
11+
let salt = SaltString::generate(&mut rand_core::OsRng);
1312
let hash = Argon2::default()
1413
.hash_password(password.as_bytes(), &salt)
1514
.map_err(|_| anyhow::anyhow!("Failed to hash password"))?;
@@ -23,7 +22,7 @@ pub fn verify_password(password: &str, hash: &str) -> Result<()> {
2322
}
2423

2524
pub fn generate_salt() -> String {
26-
SaltString::generate(&mut OsRng).to_string()
25+
SaltString::generate(&mut rand_core::OsRng).to_string()
2726
}
2827

2928
pub fn hash_ip(ip: &IpAddr, user_agent: &str, daily_salt: &str, entity_id: &str) -> String {

src/utils/r2d2_sqlite.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@ impl SqliteConnectionManager {
1313
}
1414

1515
pub fn memory() -> Self {
16-
Self {
17-
source: format!("file:{}?mode=memory&cache=shared", Uuid::new_v4().to_string()).into(),
18-
flags: OpenFlags::default(),
19-
}
16+
Self { source: format!("file:{}?mode=memory&cache=shared", Uuid::new_v4()).into(), flags: OpenFlags::default() }
2017
}
2118

2219
pub fn with_flags(self, flags: OpenFlags) -> Self {

src/web/mod.rs

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ use std::ops::Deref;
77
use std::sync::{Arc, mpsc::Sender};
88

99
use anyhow::{Context, Result};
10+
use axum::handler::{Handler, HandlerWithoutStateExt};
1011
use rust_embed::RustEmbed;
1112

1213
use aide::{axum::ApiRouter, openapi};
13-
use http::{Method, header};
14+
use http::{HeaderValue, Method, header};
1415
use tower_http::{
1516
compression::CompressionLayer,
1617
cors::{Any, CorsLayer},
@@ -19,12 +20,12 @@ use tower_http::{
1920

2021
use crate::app::{Liwan, models::Event};
2122

22-
pub use session::SessionUser;
23+
pub use session::{MaybeExtract, SessionId, SessionUser};
2324
use webext::StaticFile;
2425

2526
#[derive(RustEmbed, Clone)]
2627
#[folder = "./web/dist"]
27-
struct Files;
28+
struct _Files;
2829

2930
#[derive(RustEmbed, Clone)]
3031
#[folder = "./tracker"]
@@ -45,18 +46,32 @@ impl Deref for RouterState {
4546
}
4647

4748
pub async fn start_webserver(app: Arc<Liwan>, events: Sender<Event>) -> Result<()> {
49+
let mut api = openapi::OpenApi {
50+
info: openapi::Info { title: "Liwan API".to_string(), ..Default::default() },
51+
..openapi::OpenApi::default()
52+
};
53+
4854
let event_cors = CorsLayer::new().allow_methods([Method::POST]).allow_origin(Any).allow_credentials(false);
4955
let script_cors = CorsLayer::new().allow_methods([Method::GET]).allow_origin(Any).allow_credentials(false);
5056

5157
let set_headers = tower::ServiceBuilder::new()
52-
.layer(SetResponseHeaderLayer::if_not_present(header::X_FRAME_OPTIONS, "DENY"))
53-
.layer(SetResponseHeaderLayer::if_not_present(header::X_CONTENT_TYPE_OPTIONS, "nosniff"))
54-
.layer(SetResponseHeaderLayer::if_not_present(header::X_XSS_PROTECTION, "1; mode=block"))
58+
.layer(SetResponseHeaderLayer::if_not_present(header::X_FRAME_OPTIONS, HeaderValue::from_static("DENY")))
59+
.layer(SetResponseHeaderLayer::if_not_present(
60+
header::X_CONTENT_TYPE_OPTIONS,
61+
HeaderValue::from_static("nosniff"),
62+
))
63+
.layer(SetResponseHeaderLayer::if_not_present(
64+
header::X_XSS_PROTECTION,
65+
HeaderValue::from_static("1; mode=block"),
66+
))
5567
.layer(SetResponseHeaderLayer::if_not_present(
5668
header::CONTENT_SECURITY_POLICY,
57-
"default-src 'self' data: 'unsafe-inline'; img-src 'self' data: https://*",
69+
HeaderValue::from_static("default-src 'self' data: 'unsafe-inline'; img-src 'self' data: https://*"),
5870
))
59-
.layer(SetResponseHeaderLayer::if_not_present(header::REFERRER_POLICY, "same-origin"));
71+
.layer(SetResponseHeaderLayer::if_not_present(
72+
header::REFERRER_POLICY,
73+
HeaderValue::from_static("same-origin"),
74+
));
6075

6176
let dashboard = ApiRouter::new()
6277
.merge(routes::admin::router())
@@ -66,10 +81,13 @@ pub async fn start_webserver(app: Arc<Liwan>, events: Sender<Event>) -> Result<(
6681
let router = ApiRouter::new()
6782
.nest("/api", routes::event::router().layer(event_cors))
6883
.nest("/api/dashboard", dashboard)
69-
.route_service("/script.js", StaticFile::<Script>::new("script.min.js").layer(script_cors))
84+
.route_service("/script.js", StaticFile::<Script>::new("script.min.js").layer(script_cors).into_service())
7085
.layer(CompressionLayer::new())
7186
.layer(set_headers)
72-
.with_state(RouterState { app: app.clone(), events });
87+
.with_state(RouterState { app: app.clone(), events })
88+
.finish_api(&mut api);
89+
90+
// todo: serve files with webext::call
7391

7492
match app.onboarding.token()? {
7593
Some(onboarding) => {
@@ -83,10 +101,7 @@ pub async fn start_webserver(app: Arc<Liwan>, events: Sender<Event>) -> Result<(
83101
}
84102
}
85103

86-
let mut api = openapi::OpenApi::default();
87-
api.info = openapi::Info { description: Some("an example API".to_string()), ..openapi::Info::default() };
88-
89104
let listener = tokio::net::TcpListener::bind(("0.0.0.0", app.config.port)).await.unwrap();
90-
let server = router.finish_api(&mut api).into_make_service_with_connect_info::<SocketAddr>();
91-
axum::serve(listener, server).await.context("server exited unexpectedly")
105+
let service = router.into_make_service_with_connect_info::<SocketAddr>();
106+
axum::serve(listener, service).await.context("server exited unexpectedly")
92107
}

src/web/routes/admin.rs

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::{
1111
app::models::{Entity, Project, UserRole},
1212
utils::validate::can_access_project,
1313
web::{
14-
RouterState, SessionUser,
14+
MaybeExtract, RouterState, SessionUser,
1515
webext::{ApiResult, AxumErrExt, empty_response, http_bail},
1616
},
1717
};
@@ -164,10 +164,10 @@ async fn get_users(app: State<RouterState>, SessionUser(user): SessionUser) -> A
164164
}
165165

166166
async fn update_user(
167-
Path(username): Path<String>,
168-
user: Json<UpdateUserRequest>,
169167
app: State<RouterState>,
168+
Path(username): Path<String>,
170169
SessionUser(session_user): SessionUser,
170+
user: Json<UpdateUserRequest>,
171171
) -> ApiResult<impl IntoApiResponse> {
172172
if session_user.role != UserRole::Admin {
173173
http_bail!(StatusCode::FORBIDDEN, "Forbidden")
@@ -185,10 +185,10 @@ async fn update_user(
185185
}
186186

187187
async fn update_user_password(
188-
Path(username): Path<String>,
189-
password: Json<UpdatePasswordRequest>,
190188
app: State<RouterState>,
189+
Path(username): Path<String>,
191190
SessionUser(session_user): SessionUser,
191+
password: Json<UpdatePasswordRequest>,
192192
) -> ApiResult<impl IntoApiResponse> {
193193
if session_user.role != UserRole::Admin || username != session_user.username {
194194
http_bail!(StatusCode::FORBIDDEN, "Forbidden")
@@ -202,8 +202,8 @@ async fn update_user_password(
202202
}
203203

204204
async fn remove_user(
205-
Path(username): Path<String>,
206205
app: State<RouterState>,
206+
Path(username): Path<String>,
207207
SessionUser(session_user): SessionUser,
208208
) -> ApiResult<impl IntoApiResponse> {
209209
if session_user.role != UserRole::Admin {
@@ -220,9 +220,9 @@ async fn remove_user(
220220
}
221221

222222
async fn create_user(
223-
user: Json<CreateUserRequest>,
224223
app: State<RouterState>,
225224
SessionUser(session_user): SessionUser,
225+
user: Json<CreateUserRequest>,
226226
) -> ApiResult<impl IntoApiResponse> {
227227
if session_user.role != UserRole::Admin {
228228
http_bail!(StatusCode::FORBIDDEN, "Forbidden")
@@ -239,9 +239,9 @@ async fn create_user(
239239

240240
async fn project_create_handler(
241241
app: State<RouterState>,
242-
Json(project): Json<CreateProjectRequest>,
243242
Path(project_id): Path<String>,
244243
SessionUser(user): SessionUser,
244+
Json(project): Json<CreateProjectRequest>,
245245
) -> ApiResult<impl IntoApiResponse> {
246246
if user.role != UserRole::Admin {
247247
http_bail!(StatusCode::FORBIDDEN, "Forbidden")
@@ -264,9 +264,9 @@ async fn project_create_handler(
264264

265265
async fn project_update_handler(
266266
app: State<RouterState>,
267-
Json(req): Json<UpdateProjectRequest>,
268267
Path(project_id): Path<String>,
269268
SessionUser(user): SessionUser,
269+
Json(req): Json<UpdateProjectRequest>,
270270
) -> ApiResult<impl IntoApiResponse> {
271271
if user.role != UserRole::Admin {
272272
http_bail!(StatusCode::FORBIDDEN, "Forbidden")
@@ -292,7 +292,10 @@ async fn project_update_handler(
292292
Ok(empty_response())
293293
}
294294

295-
async fn projects_handler(app: State<RouterState>, user: Option<SessionUser>) -> ApiResult<impl IntoApiResponse> {
295+
async fn projects_handler(
296+
app: State<RouterState>,
297+
MaybeExtract(user): MaybeExtract<SessionUser>,
298+
) -> ApiResult<impl IntoApiResponse> {
296299
let projects = app.projects.all().http_err("Failed to get projects", StatusCode::INTERNAL_SERVER_ERROR)?;
297300
let projects: Vec<Project> = projects.into_iter().filter(|p| can_access_project(p, user.as_ref())).collect();
298301

@@ -316,9 +319,9 @@ async fn projects_handler(app: State<RouterState>, user: Option<SessionUser>) ->
316319
}
317320

318321
async fn project_handler(
319-
Path(project_id): Path<String>,
320322
app: State<RouterState>,
321-
user: Option<SessionUser>,
323+
MaybeExtract(user): MaybeExtract<SessionUser>,
324+
Path(project_id): Path<String>,
322325
) -> ApiResult<impl IntoApiResponse> {
323326
let project = app.projects.get(&project_id).http_status(StatusCode::NOT_FOUND)?;
324327
if !can_access_project(&project, user.as_ref()) {
@@ -342,8 +345,8 @@ async fn project_handler(
342345
}
343346

344347
async fn project_delete_handler(
345-
Path(project_id): Path<String>,
346348
app: State<RouterState>,
349+
Path(project_id): Path<String>,
347350
SessionUser(user): SessionUser,
348351
) -> ApiResult<impl IntoApiResponse> {
349352
let project = app.projects.get(&project_id).http_status(StatusCode::NOT_FOUND)?;
@@ -386,8 +389,8 @@ async fn entities_handler(app: State<RouterState>, SessionUser(user): SessionUse
386389

387390
async fn entity_create_handler(
388391
app: State<RouterState>,
389-
Json(entity): Json<CreateEntityRequest>,
390392
SessionUser(user): SessionUser,
393+
Json(entity): Json<CreateEntityRequest>,
391394
) -> ApiResult<Json<EntityResponse>> {
392395
if user.role != UserRole::Admin {
393396
http_bail!(StatusCode::FORBIDDEN, "Forbidden")
@@ -405,9 +408,9 @@ async fn entity_create_handler(
405408

406409
async fn entity_update_handler(
407410
app: State<RouterState>,
408-
Json(entity): Json<UpdateEntityRequest>,
409411
Path(entity_id): Path<String>,
410412
SessionUser(user): SessionUser,
413+
Json(entity): Json<UpdateEntityRequest>,
411414
) -> ApiResult<impl IntoApiResponse> {
412415
if user.role != UserRole::Admin {
413416
http_bail!(StatusCode::FORBIDDEN, "Forbidden")
@@ -429,8 +432,8 @@ async fn entity_update_handler(
429432
}
430433

431434
async fn entity_delete_handler(
432-
Path(entity_id): Path<String>,
433435
app: State<RouterState>,
436+
Path(entity_id): Path<String>,
434437
SessionUser(user): SessionUser,
435438
) -> ApiResult<impl IntoApiResponse> {
436439
if user.role != UserRole::Admin {

0 commit comments

Comments
 (0)