Files
k-notes/notes-api/src/main.rs

207 lines
6.3 KiB
Rust

//! 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::<axum::http::HeaderValue>() {
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: &notes_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(())
}