207 lines
6.3 KiB
Rust
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: ¬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(())
|
|
}
|