Skip to content

Commit ac04da1

Browse files
chore: remove dead code, dedupe settings ui, trim dependencies
Signed-off-by: Henry <mail@henrygressmann.de>
1 parent 928d606 commit ac04da1

57 files changed

Lines changed: 770 additions & 1996 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Cargo.lock‎

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

‎Cargo.toml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ opt-level = 1
2424
opt-level = 3
2525
debug = false
2626
strip = true
27-
lto = "thin"
27+
lto = "fat"
28+
codegen-units = 1
2829
panic = "abort"
2930

3031
[profile.profiling]

‎crates/api/Cargo.toml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ homepage.workspace = true
88
repository.workspace = true
99
license.workspace = true
1010

11+
[package.metadata.cargo-machete]
12+
ignored = ["ipnet"]
13+
1114
[[example]]
1215
name = "background_job"
1316
required-features = ["reqwest", "tokio"]
@@ -32,14 +35,12 @@ futures-timer = "3"
3235
http = "1"
3336
ipnet = "2"
3437
jiff = { version = "0.2", optional = true }
35-
rand = "0.10"
3638
reqwest = { version = "0.13", default-features = false, optional = true }
3739
serde = { version = "1", features = ["derive"] }
3840
serde_json = "1"
3941
thiserror = "2"
4042
tokio = { version = "1", features = ["rt"], optional = true }
4143
tower = { version = "0.5", default-features = false, optional = true }
42-
url = "2"
4344

4445
[dev-dependencies]
4546
axum = "0.8"

‎crates/api/src/client.rs‎

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,8 @@ use std::{
1111

1212
use async_channel::{Receiver, Sender};
1313
use futures_lite::future;
14-
use http::{HeaderValue, Request, StatusCode, header};
15-
use rand::RngExt;
14+
use http::{HeaderValue, Request, StatusCode, Uri, header};
1615
use serde::Serialize;
17-
use url::Url;
1816

1917
use crate::{Event, Transport};
2018

@@ -73,7 +71,7 @@ impl Client {
7371

7472
/// Configures a buffered client.
7573
pub struct Builder<T: Transport> {
76-
endpoint: Url,
74+
endpoint: Uri,
7775
api_key: String,
7876
transport: T,
7977
batch_size: usize,
@@ -85,13 +83,18 @@ pub struct Builder<T: Transport> {
8583

8684
impl<T: Transport> Builder<T> {
8785
fn new(base_url: impl AsRef<str>, api_key: impl Into<String>, transport: T) -> Result<Self, Error> {
88-
let mut endpoint =
89-
Url::parse(base_url.as_ref()).map_err(|_| Error::InvalidConfiguration("invalid base URL"))?;
90-
let path = endpoint.path().trim_end_matches('/');
86+
let base: Uri = base_url.as_ref().parse().map_err(|_| Error::InvalidConfiguration("invalid base URL"))?;
87+
let (Some(scheme), Some(authority)) = (base.scheme(), base.authority()) else {
88+
return Err(Error::InvalidConfiguration("invalid base URL"));
89+
};
90+
let path = base.path().trim_end_matches('/');
9191
let path = if path.ends_with("/api/v1/events") { path.to_owned() } else { format!("{path}/api/v1/events") };
92-
endpoint.set_path(&path);
93-
endpoint.set_query(None);
94-
endpoint.set_fragment(None);
92+
let endpoint = Uri::builder()
93+
.scheme(scheme.clone())
94+
.authority(authority.clone())
95+
.path_and_query(path)
96+
.build()
97+
.map_err(|_| Error::InvalidConfiguration("invalid base URL"))?;
9598
Ok(Self {
9699
endpoint,
97100
api_key: api_key.into(),
@@ -289,7 +292,7 @@ async fn send_batch<T: Transport>(config: &Builder<T>, entity_id: &str, events:
289292
let mut authorization = HeaderValue::from_str(&format!("Bearer {}", config.api_key))
290293
.map_err(|_| Error::InvalidConfiguration("invalid API key"))?;
291294
authorization.set_sensitive(true);
292-
let request = Request::post(config.endpoint.as_str())
295+
let request = Request::post(config.endpoint.clone())
293296
.header(header::AUTHORIZATION, authorization)
294297
.header(header::CONTENT_TYPE, "application/json")
295298
.body(body.clone())
@@ -325,9 +328,8 @@ async fn send_batch<T: Transport>(config: &Builder<T>, entity_id: &str, events:
325328
Attempt::Response(Err(_)) | Attempt::Timeout => None,
326329
};
327330
if attempt < config.max_retries {
328-
let base = 100_u64.saturating_mul(2_u64.saturating_pow(attempt));
329-
let jitter = rand::rng().random_range(0..=(base / 4));
330-
let delay = retry_after.unwrap_or_else(|| Duration::from_millis(base + jitter));
331+
let backoff = Duration::from_millis(100_u64.saturating_mul(2_u64.saturating_pow(attempt)));
332+
let delay = retry_after.unwrap_or(backoff);
331333
futures_timer::Delay::new(delay).await;
332334
}
333335
}

‎crates/liwan/Cargo.toml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ repository.workspace = true
77
license.workspace = true
88
publish = false
99

10+
[package.metadata.cargo-machete]
11+
ignored = ["md-5"]
12+
1013
[lib]
1114
path = "src/lib.rs"
1215

@@ -66,7 +69,6 @@ axum = { version = "0.8", default-features = false, features = [
6669
"tokio",
6770
] }
6871
axum-extra = { version = "0.12", default-features = false, features = ["cookie", "typed-header"] }
69-
base64 = "0.23"
7072
headers = "0.4"
7173
http = "1.5"
7274
ipnet = "2.12"

‎crates/liwan/src/app/core/api_keys.rs‎

Lines changed: 31 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
use std::collections::HashSet;
22

33
use anyhow::{Result, bail};
4-
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
54
use chrono::Utc;
6-
use rand::RngExt;
75
use rand::distr::{Alphanumeric, SampleString};
8-
use rusqlite::{OptionalExtension, Transaction};
6+
use rusqlite::{Connection, OptionalExtension, Transaction};
97

108
use crate::app::{
119
SqlitePool,
@@ -42,7 +40,7 @@ impl LiwanApiKeys {
4240
) -> Result<(ApiKey, String)> {
4341
let display_name = display_name.trim();
4442
validate_display_name(display_name)?;
45-
let secret = URL_SAFE_NO_PAD.encode(rand::rng().random::<[u8; 24]>());
43+
let secret = Alphanumeric.sample_string(&mut rand::rng(), 32);
4644
let plaintext = format!("{KEY_PREFIX}{secret}");
4745
let id = Alphanumeric.sample_string(&mut rand::rng(), 16);
4846
let secret_hash = blake3::hash(secret.as_bytes()).to_hex().to_string();
@@ -76,61 +74,7 @@ impl LiwanApiKeys {
7674
/// Lists all key metadata without exposing hashes.
7775
pub fn all(&self) -> Result<Vec<ApiKey>> {
7876
let conn = self.pool.get()?;
79-
let mut stmt = conn.prepare_cached(
80-
"select id, display_name, created_at, expires_at, last_used_at,
81-
(select json_group_array(entity_id) from api_key_entities where key_id = api_keys.id),
82-
permissions_json, all_entities,
83-
(select json_group_array(project_id) from api_key_projects where key_id = api_keys.id), all_projects
84-
from api_keys order by created_at desc",
85-
)?;
86-
let rows = stmt.query_map([], |row| {
87-
Ok((
88-
row.get::<_, String>(0)?,
89-
row.get::<_, String>(1)?,
90-
row.get(2)?,
91-
row.get(3)?,
92-
row.get(4)?,
93-
row.get::<_, String>(5)?,
94-
row.get::<_, String>(6)?,
95-
row.get::<_, bool>(7)?,
96-
row.get::<_, String>(8)?,
97-
row.get::<_, bool>(9)?,
98-
))
99-
})?;
100-
let mut keys = Vec::new();
101-
for row in rows {
102-
let (
103-
id,
104-
display_name,
105-
created_at,
106-
expires_at,
107-
last_used_at,
108-
entities,
109-
permissions,
110-
all_entities,
111-
projects,
112-
all_projects,
113-
) = row?;
114-
keys.push(ApiKey {
115-
id,
116-
display_name,
117-
entities: if all_entities {
118-
AccessScope::All
119-
} else {
120-
AccessScope::Selected(serde_json::from_str(&entities)?)
121-
},
122-
projects: if all_projects {
123-
AccessScope::All
124-
} else {
125-
AccessScope::Selected(serde_json::from_str(&projects)?)
126-
},
127-
permissions: serde_json::from_str(&permissions)?,
128-
created_at,
129-
last_used_at,
130-
expires_at,
131-
});
132-
}
133-
Ok(keys)
77+
query_keys(&conn, "", [])
13478
}
13579

13680
/// Updates a key's display name, access, and permissions.
@@ -168,54 +112,16 @@ impl LiwanApiKeys {
168112

169113
/// Replaces a key's secret and sets a new expiration, without changing its access.
170114
pub fn regenerate(&self, key_id: &str, expiration: ApiKeyExpiration) -> Result<Option<(ApiKey, String)>> {
171-
let secret = URL_SAFE_NO_PAD.encode(rand::rng().random::<[u8; 24]>());
115+
let secret = Alphanumeric.sample_string(&mut rand::rng(), 32);
172116
let hash = blake3::hash(secret.as_bytes()).to_hex().to_string();
173117
let expires_at = expiration.expires_at();
174118
let mut conn = self.pool.get()?;
175119
let tx = conn.transaction()?;
176-
let changed = tx.execute(
120+
tx.execute(
177121
"update api_keys set secret_hash = ?, expires_at = ?, last_used_at = null where id = ?",
178122
rusqlite::params![hash, expires_at, key_id],
179123
)?;
180-
if changed == 0 {
181-
return Ok(None);
182-
}
183-
let (display_name, created_at, entities, all_entities, projects, all_projects, permissions) = tx.query_row(
184-
"select display_name, created_at,
185-
(select json_group_array(entity_id) from api_key_entities where key_id = api_keys.id), all_entities,
186-
(select json_group_array(project_id) from api_key_projects where key_id = api_keys.id), all_projects,
187-
permissions_json from api_keys where id = ?",
188-
[key_id],
189-
|row| {
190-
Ok((
191-
row.get::<_, String>(0)?,
192-
row.get(1)?,
193-
row.get::<_, String>(2)?,
194-
row.get::<_, bool>(3)?,
195-
row.get::<_, String>(4)?,
196-
row.get::<_, bool>(5)?,
197-
row.get::<_, String>(6)?,
198-
))
199-
},
200-
)?;
201-
let key = ApiKey {
202-
id: key_id.to_string(),
203-
display_name,
204-
entities: if all_entities {
205-
AccessScope::All
206-
} else {
207-
AccessScope::Selected(serde_json::from_str(&entities)?)
208-
},
209-
projects: if all_projects {
210-
AccessScope::All
211-
} else {
212-
AccessScope::Selected(serde_json::from_str(&projects)?)
213-
},
214-
permissions: serde_json::from_str(&permissions)?,
215-
created_at,
216-
last_used_at: None,
217-
expires_at,
218-
};
124+
let Some(key) = query_keys(&tx, "where id = ?", [key_id])?.pop() else { return Ok(None) };
219125
tx.commit()?;
220126
Ok(Some((key, format!("{KEY_PREFIX}{secret}"))))
221127
}
@@ -288,17 +194,37 @@ impl LiwanApiKeys {
288194
} else {
289195
AccessScope::Selected(permitted_entities.into_iter().collect())
290196
},
291-
projects: if all_projects {
292-
AccessScope::All
293-
} else {
294-
AccessScope::Selected(serde_json::from_str(&projects)?)
295-
},
197+
projects: AccessScope::from_db(all_projects, &projects)?,
296198
permissions: serde_json::from_str(&permissions)?,
297199
},
298200
}))
299201
}
300202
}
301203

204+
fn query_keys(conn: &Connection, filter: &str, params: impl rusqlite::Params) -> Result<Vec<ApiKey>> {
205+
let mut stmt = conn.prepare_cached(&format!(
206+
"select id, display_name, created_at, expires_at, last_used_at, permissions_json, all_entities,
207+
(select json_group_array(entity_id) from api_key_entities where key_id = api_keys.id), all_projects,
208+
(select json_group_array(project_id) from api_key_projects where key_id = api_keys.id)
209+
from api_keys {filter} order by created_at desc"
210+
))?;
211+
let mut rows = stmt.query(params)?;
212+
let mut keys = Vec::new();
213+
while let Some(row) = rows.next()? {
214+
keys.push(ApiKey {
215+
id: row.get(0)?,
216+
display_name: row.get(1)?,
217+
created_at: row.get(2)?,
218+
expires_at: row.get(3)?,
219+
last_used_at: row.get(4)?,
220+
permissions: serde_json::from_str(&row.get::<_, String>(5)?)?,
221+
entities: AccessScope::from_db(row.get(6)?, &row.get::<_, String>(7)?)?,
222+
projects: AccessScope::from_db(row.get(8)?, &row.get::<_, String>(9)?)?,
223+
});
224+
}
225+
Ok(keys)
226+
}
227+
302228
fn validate_display_name(display_name: &str) -> Result<()> {
303229
if display_name.is_empty() || display_name.len() > 100 {
304230
bail!("API key name must be between 1 and 100 characters");

‎crates/liwan/src/app/core/reports/mod.rs‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,6 @@ impl DateRange {
7676
Ok(Self { start, end: self.start })
7777
}
7878

79-
/// Return whether the range ends after the current time
80-
pub fn ends_in_future(&self) -> bool {
81-
self.end > Utc::now()
82-
}
83-
8479
/// Return the range duration
8580
pub fn duration(&self) -> chrono::Duration {
8681
self.end - self.start

‎crates/liwan/src/app/core/teams.rs‎

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,8 @@ impl LiwanTeams {
4848
display_name,
4949
users: serde_json::from_str(&users)?,
5050
access: Access {
51-
entities: if all_entities {
52-
AccessScope::All
53-
} else {
54-
AccessScope::Selected(serde_json::from_str(&entities)?)
55-
},
56-
projects: if all_projects {
57-
AccessScope::All
58-
} else {
59-
AccessScope::Selected(serde_json::from_str(&projects)?)
60-
},
51+
entities: AccessScope::from_db(all_entities, &entities)?,
52+
projects: AccessScope::from_db(all_projects, &projects)?,
6153
permissions: serde_json::from_str::<HashSet<AccessPermission>>(&permissions)?,
6254
},
6355
});

‎crates/liwan/src/app/models.rs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use schemars::JsonSchema;
55
use serde::{Deserialize, Serialize};
66
use std::collections::BTreeMap;
77

8-
#[derive(Debug, Clone)]
8+
#[derive(Debug, Clone, Default)]
99
pub struct Event {
1010
pub entity_id: String,
1111
pub visitor_group_id: String,
@@ -163,6 +163,11 @@ impl Default for AccessScope {
163163
}
164164

165165
impl AccessScope {
166+
/// Build a scope from a stored "all" flag and a JSON array of selected IDs.
167+
pub fn from_db(all: bool, ids: &str) -> serde_json::Result<Self> {
168+
if all { Ok(Self::All) } else { serde_json::from_str(ids).map(Self::Selected) }
169+
}
170+
166171
/// Check whether an ID falls within this scope.
167172
pub fn contains(&self, id: &str) -> bool {
168173
match self {

0 commit comments

Comments
 (0)