feat(server): create-user CLI + session-store migration on startup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 15:07:58 +02:00
parent 642f709bbe
commit dbff95c2a9
6 changed files with 159 additions and 8 deletions
+53 -3
View File
@@ -5,8 +5,9 @@ mod config;
pub use config::Config;
use anyhow::Context;
use api::{AppState, build_app};
use api::{AppState, build_app, migrate_sessions};
use db::Db;
use domain::{AuditActor, Email, NewUser, Role};
use tokio::net::TcpListener;
/// Connect dependencies from `config` and serve until shutdown.
@@ -17,16 +18,20 @@ pub async fn run(config: Config) -> anyhow::Result<()> {
db.migrate().await.context("running database migrations")?;
migrate_sessions(&db)
.await
.context("creating the session store")?;
let state = AppState {
db,
app_name: config.app_name.clone(),
// Wired to config in the auth CLI task; Secure by default.
cookie_secure: true,
cookie_secure: config.cookie_secure,
};
let listener = TcpListener::bind(&config.bind_addr)
.await
.with_context(|| format!("binding to {}", config.bind_addr))?;
tracing::info!(addr = %config.bind_addr, "server listening");
serve(listener, state).await
@@ -35,8 +40,53 @@ pub async fn run(config: Config) -> anyhow::Result<()> {
/// Serve the API on an already-bound listener (used by `run` and tests).
pub async fn serve(listener: TcpListener, state: AppState) -> anyhow::Result<()> {
let app = build_app(state);
axum::serve(listener, app)
.await
.context("running the HTTP server")?;
Ok(())
}
/// Create a user from the CLI (admin bootstrap). Reads the password from the
/// `BOOTSTRAP_PASSWORD` env var if set, otherwise prompts (hidden input).
pub async fn create_user(database_url: &str, email: &str, role: Role) -> anyhow::Result<()> {
let email = Email::parse(email).map_err(|err| anyhow::anyhow!("{err}"))?;
let password = match std::env::var("BOOTSTRAP_PASSWORD") {
Ok(p) => p,
Err(_) => rpassword::prompt_password("Password: ").context("reading password")?,
};
anyhow::ensure!(
password.len() >= 8,
"password must be at least 8 characters"
);
let password_hash =
auth::hash_password(&password).map_err(|err| anyhow::anyhow!("hashing password: {err}"))?;
let db = Db::connect(database_url)
.await
.context("connecting to the database")?;
let mut tx = db.pool().begin().await?;
let id = db::users::create_user(
&mut tx,
AuditActor::System,
&NewUser {
email,
password_hash,
role,
},
)
.await
.context("creating the user (is the email already taken?)")?;
tx.commit().await?;
println!("created user {id} ({role:?})");
Ok(())
}