brewlog/src/server/server.rs
copilot-swe-agent[bot] 6d0ee869f7
feat(api): add authentication middleware and token management routes
Co-authored-by: jnsgruk <668505+jnsgruk@users.noreply.github.com>
2025-11-25 16:28:10 +00:00

163 lines
4.6 KiB
Rust

use std::net::SocketAddr;
use std::sync::Arc;
use anyhow::Context;
use axum::Router;
use chrono::Utc;
use tokio::net::TcpListener;
use tokio::signal;
use tracing::info;
use crate::domain::ids::generate_id;
use crate::domain::repositories::{
RoastRepository, RoasterRepository, TimelineEventRepository, TokenRepository, UserRepository,
};
use crate::domain::users::User;
use crate::infrastructure::auth::hash_password;
use crate::infrastructure::database::Database;
use crate::infrastructure::repositories::roasters::SqlRoasterRepository;
use crate::infrastructure::repositories::roasts::SqlRoastRepository;
use crate::infrastructure::repositories::timeline_events::SqlTimelineEventRepository;
use crate::infrastructure::repositories::tokens::SqlTokenRepository;
use crate::infrastructure::repositories::users::SqlUserRepository;
use crate::server::routes::app_router;
pub struct ServerConfig {
pub bind_address: SocketAddr,
pub database_url: String,
pub admin_password: Option<String>,
}
#[derive(Clone)]
pub struct AppState {
pub roaster_repo: Arc<dyn RoasterRepository>,
pub roast_repo: Arc<dyn RoastRepository>,
pub timeline_repo: Arc<dyn TimelineEventRepository>,
pub user_repo: Arc<dyn UserRepository>,
pub token_repo: Arc<dyn TokenRepository>,
}
impl AppState {
pub fn new(
roaster_repo: Arc<dyn RoasterRepository>,
roast_repo: Arc<dyn RoastRepository>,
timeline_repo: Arc<dyn TimelineEventRepository>,
user_repo: Arc<dyn UserRepository>,
token_repo: Arc<dyn TokenRepository>,
) -> Self {
Self {
roaster_repo,
roast_repo,
timeline_repo,
user_repo,
token_repo,
}
}
}
pub async fn serve(config: ServerConfig) -> anyhow::Result<()> {
let database = Database::connect(&config.database_url)
.await
.context("failed to connect to database")?;
database.migrate().await?;
let roaster_repo = Arc::new(SqlRoasterRepository::new(database.clone_pool()));
let roast_repo = Arc::new(SqlRoastRepository::new(database.clone_pool()));
let timeline_repo = Arc::new(SqlTimelineEventRepository::new(database.clone_pool()));
let user_repo: Arc<dyn UserRepository> =
Arc::new(SqlUserRepository::new(database.clone_pool()));
let token_repo: Arc<dyn TokenRepository> =
Arc::new(SqlTokenRepository::new(database.clone_pool()));
// Bootstrap admin user if no users exist
bootstrap_admin_user(&user_repo, config.admin_password).await?;
let state = AppState::new(
roaster_repo,
roast_repo,
timeline_repo,
user_repo,
token_repo,
);
let listener = TcpListener::bind(config.bind_address)
.await
.with_context(|| format!("failed to bind to {}", config.bind_address))?;
let app: Router = app_router(state);
info!(address = %config.bind_address, "starting HTTP server");
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
.context("server terminated unexpectedly")?;
info!("server shutdown complete");
Ok(())
}
async fn bootstrap_admin_user(
user_repo: &Arc<dyn UserRepository>,
admin_password: Option<String>,
) -> anyhow::Result<()> {
// Check if any users exist
let users_exist = user_repo
.exists()
.await
.context("failed to check if users exist")?;
if users_exist {
// Users already exist, no need to bootstrap
return Ok(());
}
// No users exist - we need to create the admin user
let password = admin_password.ok_or_else(|| {
anyhow::anyhow!(
"No users exist in the database. Please provide BREWLOG_ADMIN_PASSWORD \
environment variable to create the admin user."
)
})?;
info!("No users found. Creating admin user...");
let password_hash = hash_password(&password).context("failed to hash admin password")?;
let admin_user = User::new(
generate_id(),
"admin".to_string(),
password_hash,
Utc::now(),
);
user_repo
.insert(admin_user)
.await
.context("failed to create admin user")?;
info!("Admin user created successfully");
Ok(())
}
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install SIGTERM handler")
.recv()
.await;
};
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
}