max / pom
7 files changed,
+56 insertions,
-10 deletions
| @@ -1665,7 +1665,7 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" | |||
| 1665 | 1665 | ||
| 1666 | 1666 | [[package]] | |
| 1667 | 1667 | name = "pom" | |
| 1668 | - | version = "0.3.0" | |
| 1668 | + | version = "0.3.1" | |
| 1669 | 1669 | dependencies = [ | |
| 1670 | 1670 | "axum", | |
| 1671 | 1671 | "chrono", |
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | [package] | |
| 2 | 2 | name = "pom" | |
| 3 | - | version = "0.3.0" | |
| 3 | + | version = "0.3.1" | |
| 4 | 4 | edition = "2024" | |
| 5 | 5 | license-file = "LICENSE" | |
| 6 | 6 |
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | [serve] | |
| 2 | 2 | interval_secs = 300 | |
| 3 | 3 | prune_days = 30 | |
| 4 | - | listen = "0.0.0.0:9100" | |
| 4 | + | listen = "100.106.221.39:9100" | |
| 5 | 5 | peer_heartbeat_secs = 60 | |
| 6 | 6 | route_check_interval_secs = 300 | |
| 7 | 7 | dashboard = true |
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | [serve] | |
| 2 | 2 | interval_secs = 300 | |
| 3 | 3 | prune_days = 30 | |
| 4 | - | listen = "0.0.0.0:9100" | |
| 4 | + | listen = "100.120.174.96:9100" | |
| 5 | 5 | peer_heartbeat_secs = 60 | |
| 6 | 6 | route_check_interval_secs = 300 | |
| 7 | 7 | dashboard = false |
| @@ -392,7 +392,7 @@ mod tests { | |||
| 392 | 392 | let toml = r#" | |
| 393 | 393 | [serve] | |
| 394 | 394 | interval_secs = 120 | |
| 395 | - | listen = "0.0.0.0:9100" | |
| 395 | + | listen = "127.0.0.1:9100" | |
| 396 | 396 | peer_heartbeat_secs = 30 | |
| 397 | 397 | ||
| 398 | 398 | [instance] | |
| @@ -415,7 +415,7 @@ grace_count = 5 | |||
| 415 | 415 | ||
| 416 | 416 | let config: Config = toml::from_str(toml).unwrap(); | |
| 417 | 417 | assert_eq!(config.serve.interval_secs, 120); | |
| 418 | - | assert_eq!(config.serve.listen, "0.0.0.0:9100"); | |
| 418 | + | assert_eq!(config.serve.listen, "127.0.0.1:9100"); | |
| 419 | 419 | assert_eq!(config.serve.peer_heartbeat_secs, 30); | |
| 420 | 420 | assert_eq!(config.instance.name.as_deref(), Some("hetzner")); | |
| 421 | 421 | assert_eq!(config.target_names(), vec!["mnw"]); |
| @@ -14,9 +14,21 @@ pub async fn dashboard_handler(AxumState(state): AxumState<ApiState>) -> impl In | |||
| 14 | 14 | Html(render_dashboard(&instance_name, version, api_token, has_mesh)) | |
| 15 | 15 | } | |
| 16 | 16 | ||
| 17 | - | /// Escape a string for safe embedding in a JS string literal. | |
| 17 | + | /// Escape a string for safe embedding in a JS string literal (double-quoted). | |
| 18 | 18 | pub(crate) fn escape_js(s: &str) -> String { | |
| 19 | - | s.replace('\\', "\\\\").replace('"', "\\\"") | |
| 19 | + | let mut out = String::with_capacity(s.len()); | |
| 20 | + | for ch in s.chars() { | |
| 21 | + | match ch { | |
| 22 | + | '\\' => out.push_str("\\\\"), | |
| 23 | + | '"' => out.push_str("\\\""), | |
| 24 | + | '\n' => out.push_str("\\n"), | |
| 25 | + | '\r' => out.push_str("\\r"), | |
| 26 | + | '<' => out.push_str("\\x3c"), | |
| 27 | + | '\0' => out.push_str("\\0"), | |
| 28 | + | _ => out.push(ch), | |
| 29 | + | } | |
| 30 | + | } | |
| 31 | + | out | |
| 20 | 32 | } | |
| 21 | 33 | ||
| 22 | 34 | fn render_dashboard(instance_name: &str, version: &str, api_token: &str, has_mesh: bool) -> String { | |
| @@ -436,4 +448,24 @@ mod tests { | |||
| 436 | 448 | fn escape_js_empty() { | |
| 437 | 449 | assert_eq!(escape_js(""), ""); | |
| 438 | 450 | } | |
| 451 | + | ||
| 452 | + | #[test] | |
| 453 | + | fn escape_js_newline() { | |
| 454 | + | assert_eq!(escape_js("a\nb"), "a\\nb"); | |
| 455 | + | } | |
| 456 | + | ||
| 457 | + | #[test] | |
| 458 | + | fn escape_js_carriage_return() { | |
| 459 | + | assert_eq!(escape_js("a\rb"), "a\\rb"); | |
| 460 | + | } | |
| 461 | + | ||
| 462 | + | #[test] | |
| 463 | + | fn escape_js_script_close_tag() { | |
| 464 | + | assert_eq!(escape_js("</script>"), "\\x3c/script>"); | |
| 465 | + | } | |
| 466 | + | ||
| 467 | + | #[test] | |
| 468 | + | fn escape_js_null() { | |
| 469 | + | assert_eq!(escape_js("a\0b"), "a\\0b"); | |
| 470 | + | } | |
| 439 | 471 | } |
| @@ -239,6 +239,7 @@ async fn has_existing_tables(pool: &SqlitePool) -> Result<bool> { | |||
| 239 | 239 | } | |
| 240 | 240 | ||
| 241 | 241 | /// Execute a single migration's SQL and record it in schema_version. | |
| 242 | + | /// Wrapped in an explicit transaction so partial failures roll back cleanly. | |
| 242 | 243 | async fn run_one_migration( | |
| 243 | 244 | pool: &SqlitePool, | |
| 244 | 245 | version: i64, | |
| @@ -247,15 +248,28 @@ async fn run_one_migration( | |||
| 247 | 248 | ) -> Result<()> { | |
| 248 | 249 | info!(version, description, "running migration"); | |
| 249 | 250 | ||
| 251 | + | let mut tx = pool.begin().await?; | |
| 252 | + | ||
| 250 | 253 | // Execute each statement in the migration SQL | |
| 251 | 254 | for statement in sql.split(';') { | |
| 252 | 255 | let trimmed = statement.trim(); | |
| 253 | 256 | if !trimmed.is_empty() { | |
| 254 | - | sqlx::query(trimmed).execute(pool).await?; | |
| 257 | + | sqlx::query(trimmed).execute(&mut *tx).await?; | |
| 255 | 258 | } | |
| 256 | 259 | } | |
| 257 | 260 | ||
| 258 | - | stamp_version(pool, version, description).await?; | |
| 261 | + | // Record the version inside the same transaction | |
| 262 | + | let now = chrono::Utc::now().to_rfc3339(); | |
| 263 | + | sqlx::query( | |
| 264 | + | "INSERT INTO schema_version (version, description, applied_at) VALUES (?, ?, ?)", | |
| 265 | + | ) | |
| 266 | + | .bind(version) | |
| 267 | + | .bind(description) | |
| 268 | + | .bind(&now) | |
| 269 | + | .execute(&mut *tx) | |
| 270 | + | .await?; | |
| 271 | + | ||
| 272 | + | tx.commit().await?; | |
| 259 | 273 | Ok(()) | |
| 260 | 274 | } | |
| 261 | 275 |