//! K-Notes API Server //! //! A high-performance, self-hosted note-taking API following hexagonal architecture. use std::sync::Arc; use time::Duration; use axum::Router; use axum_login::AuthManagerLayerBuilder; use tower_http::cors::CorsLayer; use tower_http::trace::TraceLayer; use tower_sessions::{Expiry, SessionManagerLayer}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use notes_infra::{DatabaseConfig, run_migrations}; mod auth; mod config; mod dto; mod error; mod routes; mod state; use auth::AuthBackend; use config::Config; use state::AppState; #[tokio::main] async fn main() -> anyhow::Result<()> { // Initialize tracing tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "notes_api=debug,tower_http=debug,axum_login=debug".into()), ) .with(tracing_subscriber::fmt::layer()) .init(); // Load configuration let config = Config::from_env(); // Setup database tracing::info!("Connecting to database: {}", config.database_url); let db_config = DatabaseConfig::new(&config.database_url); #[cfg(feature = "smart-features")] use notes_infra::factory::build_link_repository; use notes_infra::factory::{ build_database_pool, build_note_repository, build_session_store, build_tag_repository, build_user_repository, }; let pool = build_database_pool(&db_config) .await .map_err(|e| anyhow::anyhow!(e))?; // Run migrations if let Err(e) = run_migrations(&pool).await { tracing::warn!( "Migration error (might be expected if not implemented for this DB): {}", e ); } // Create a default user for development create_dev_user(&pool).await.ok(); // Create repositories via factory let note_repo = build_note_repository(&pool) .await .map_err(|e| anyhow::anyhow!(e))?; let tag_repo = build_tag_repository(&pool) .await .map_err(|e| anyhow::anyhow!(e))?; let user_repo = build_user_repository(&pool) .await .map_err(|e| anyhow::anyhow!(e))?; #[cfg(feature = "smart-features")] let link_repo = build_link_repository(&pool) .await .map_err(|e| anyhow::anyhow!(e))?; // Create services use notes_domain::{NoteService, TagService, UserService}; let note_service = Arc::new(NoteService::new(note_repo.clone(), tag_repo.clone())); let tag_service = Arc::new(TagService::new(tag_repo.clone())); let user_service = Arc::new(UserService::new(user_repo.clone())); // Connect to NATS // Connect to NATS #[cfg(feature = "smart-features")] let nats_client = { tracing::info!("Connecting to NATS: {}", config.broker_url); async_nats::connect(&config.broker_url) .await .map_err(|e| anyhow::anyhow!("NATS connection failed: {}", e))? }; // Create application state let state = AppState::new( note_repo, tag_repo, user_repo.clone(), #[cfg(feature = "smart-features")] link_repo, note_service, tag_service, user_service, #[cfg(feature = "smart-features")] nats_client, config.clone(), ); // Auth backend let backend = AuthBackend::new(user_repo); // Session layer // Use the factory to build the session store, agnostic of the underlying DB let session_store = build_session_store(&pool) .await .map_err(|e| anyhow::anyhow!(e))?; session_store .migrate() .await .map_err(|e| anyhow::anyhow!(e))?; let session_layer = SessionManagerLayer::new(session_store) .with_secure(false) // Set to true in production with HTTPS .with_expiry(Expiry::OnInactivity(Duration::seconds(60 * 60 * 24 * 7))); // 7 days let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer).build(); let mut cors = CorsLayer::new() .allow_methods([ axum::http::Method::GET, axum::http::Method::POST, axum::http::Method::PATCH, axum::http::Method::DELETE, axum::http::Method::OPTIONS, ]) .allow_headers([ axum::http::header::AUTHORIZATION, axum::http::header::ACCEPT, axum::http::header::CONTENT_TYPE, ]) .allow_credentials(true); let mut allowed_origins = Vec::new(); for origin in &config.cors_allowed_origins { tracing::debug!("Allowing CORS origin: {}", origin); if let Ok(value) = origin.parse::() { allowed_origins.push(value); } else { tracing::warn!("Invalid CORS origin: {}", origin); } } if !allowed_origins.is_empty() { cors = cors.allow_origin(allowed_origins); } let app = Router::new() .nest("/api/v1", routes::api_v1_router()) .layer(auth_layer) .layer(cors) .layer(TraceLayer::new_for_http()) .with_state(state); let addr = format!("{}:{}", config.host, config.port); let listener = tokio::net::TcpListener::bind(&addr).await?; tracing::info!("🚀 K-Notes API server running at http://{}", addr); tracing::info!("🔒 Authentication enabled (axum-login)"); tracing::info!("📝 API endpoints available at /api/v1/..."); axum::serve(listener, app).await?; Ok(()) } async fn create_dev_user(pool: ¬es_infra::db::DatabasePool) -> anyhow::Result<()> { use notes_domain::User; use notes_infra::factory::build_user_repository; use password_auth::generate_hash; use uuid::Uuid; let user_repo = build_user_repository(pool) .await .map_err(|e| anyhow::anyhow!(e))?; // Check if dev user exists let dev_user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); if user_repo.find_by_id(dev_user_id).await?.is_none() { let hash = generate_hash("password"); let user = User::with_id( dev_user_id, "dev|local", "dev@localhost.com", Some(hash), chrono::Utc::now(), ); user_repo.save(&user).await?; tracing::info!("Created development user: dev@localhost.com / password"); } Ok(()) }