Move CI auth middleware to composition root
web::router() returns bare routes. main.rs decides whether to layer auth on top (production) or skip it (seed/dev). server::run() accepts the pre-composed CI router and is agnostic about auth.
diff --git a/src/bin/quire/commands/dev.rs b/src/bin/quire/commands/dev.rs
index 5453eba..430138d 100644
--- a/src/bin/quire/commands/dev.rs
+++ b/src/bin/quire/commands/dev.rs
@@ -8,12 +8,23 @@ use quire::Quire;
/// Base timestamp for seed data: 2026-05-06T12:00:00Z.
const BASE_MS: i64 = 1746532800000;
-/// Seed the quire database with realistic CI run data.
+/// Seed a tempdir with realistic CI run data and return a `Quire` pointing at it.
///
-/// Wipes any existing data and inserts a fixed corpus of runs covering
-/// every interesting state (complete, failed, active, pending, superseded)
-/// with matching on-disk log artifacts. Idempotent — same input, same output.
-pub fn seed(quire: &Quire) -> Result<()> {
+/// Creates a fresh tempdir under `std::env::temp_dir()`, inserts a fixed corpus
+/// of runs covering every interesting state (complete, failed, active, pending,
+/// superseded) with matching on-disk log artifacts. Idempotent — same input,
+/// same output.
+pub fn seed() -> Result<Quire> {
+ let dir = tempfile::tempdir()
+ .into_diagnostic()
+ .context("failed to create tempdir")?;
+
+ // Leak the TempDir so it outlives the function. The server will
+ // clean up on shutdown, or the OS will when the process exits.
+ let base_dir = dir.keep();
+ tracing::info!(path = %base_dir.display(), "seeded tempdir");
+
+ let quire = Quire::new(base_dir);
let db_path = quire.db_path();
// Open and migrate.
@@ -24,22 +35,17 @@ pub fn seed(quire: &Quire) -> Result<()> {
.into_diagnostic()
.context("failed to run migrations")?;
- // Wipe existing seed data (if any).
- db.execute_batch("DELETE FROM sh_events; DELETE FROM jobs; DELETE FROM runs;")
- .into_diagnostic()
- .context("failed to wipe existing data")?;
-
insert_runs(&db)?;
insert_jobs(&db)?;
insert_sh_events(&db)?;
- write_log_artifacts(quire)?;
+ write_log_artifacts(&quire)?;
let run_count: i64 = db
.query_row("SELECT count(*) FROM runs", [], |row| row.get(0))
.into_diagnostic()?;
tracing::info!(%run_count, "seeded database");
- Ok(())
+ Ok(quire)
}
fn insert_runs(db: &rusqlite::Connection) -> Result<()> {
diff --git a/src/bin/quire/commands/serve.rs b/src/bin/quire/commands/serve.rs
index eab82eb..a187fa3 100644
--- a/src/bin/quire/commands/serve.rs
+++ b/src/bin/quire/commands/serve.rs
@@ -2,6 +2,6 @@ use miette::Result;
use quire::Quire;
-pub async fn run(quire: &Quire) -> Result<()> {
- crate::server::run(quire).await
+pub async fn run(quire: &Quire, ci_routes: axum::Router) -> Result<()> {
+ crate::server::run(quire, ci_routes).await
}
diff --git a/src/bin/quire/main.rs b/src/bin/quire/main.rs
index e120d35..3aae139 100644
--- a/src/bin/quire/main.rs
+++ b/src/bin/quire/main.rs
@@ -208,10 +208,16 @@ async fn main() -> Result<()> {
match command {
Commands::Serve { seed } => {
- if seed {
- commands::dev::seed(&quire)?;
- }
- commands::serve::run(&quire).await?
+ let quire = if seed { commands::dev::seed()? } else { quire };
+ let ci_routes = quire::quire::web::router(quire.clone());
+ let ci_routes = if seed {
+ ci_routes
+ } else {
+ ci_routes.layer(axum::middleware::from_fn(
+ quire::quire::web::auth::require_auth,
+ ))
+ };
+ commands::serve::run(&quire, ci_routes).await?
}
Commands::Exec { command } => commands::exec::run(&quire, command).await?,
Commands::Hook { hook_name } => commands::hook::run(&quire, hook_name).await?,
diff --git a/src/bin/quire/server.rs b/src/bin/quire/server.rs
index b86ec15..ba18bf9 100644
--- a/src/bin/quire/server.rs
+++ b/src/bin/quire/server.rs
@@ -17,7 +17,7 @@ async fn index() -> String {
format!("quire {}\n", crate::VERSION)
}
-pub async fn run(quire: &Quire) -> Result<()> {
+pub async fn run(quire: &Quire, ci_routes: axum::Router) -> Result<()> {
let addr: SocketAddr = ([0, 0, 0, 0], 3000).into();
// Set up event socket.
@@ -55,7 +55,7 @@ pub async fn run(quire: &Quire) -> Result<()> {
let app = Router::new()
.route("/health", get(health))
.route("/", get(index))
- .merge(quire::quire::web::router(quire.clone()));
+ .merge(ci_routes);
tracing::info!(%addr, "starting HTTP server");
diff --git a/src/quire/web/mod.rs b/src/quire/web/mod.rs
index 4bf7838..4c142a2 100644
--- a/src/quire/web/mod.rs
+++ b/src/quire/web/mod.rs
@@ -12,18 +12,18 @@ pub mod format;
pub mod handlers;
pub mod templates;
-use axum::middleware;
-
use crate::Quire;
+/// Bare CI routes without any auth middleware.
+///
+/// The caller decides whether to layer auth on top (e.g.
+/// `.layer(middleware::from_fn(auth::require_auth))`).
pub fn router(quire: Quire) -> axum::Router {
- let ci_routes = axum::Router::new()
+ axum::Router::new()
.route("/{repo}/ci", axum::routing::get(handlers::run_list))
.route(
"/{repo}/ci/{run_id}",
axum::routing::get(handlers::run_detail),
)
- .layer(middleware::from_fn(auth::require_auth));
-
- ci_routes.with_state(quire)
+ .with_state(quire)
}