diff --git a/Cargo.lock b/Cargo.lock index 77e0814..89857fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1222,15 +1222,22 @@ dependencies = [ [[package]] name = "k-core" -version = "0.1.1" -source = "git+https://git.gabrielkaszewski.dev/GKaszewski/k-core#bda288362ade1cd3508bbc985a0aebd7714d9a18" +version = "0.1.10" +source = "git+https://git.gabrielkaszewski.dev/GKaszewski/k-core#7a72f5f54ad45ba82f451e90c44c0581d13194d9" dependencies = [ "anyhow", + "async-trait", + "axum", "chrono", "serde", "sqlx", "thiserror 2.0.17", + "time", + "tokio", + "tower", + "tower-http", "tower-sessions", + "tower-sessions-sqlx-store", "tracing", "tracing-subscriber", "uuid", @@ -3107,14 +3114,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.4", + "webpki-roots 1.0.5", ] [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" dependencies = [ "rustls-pki-types", ] @@ -3547,6 +3554,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de9211a9f64b825911bdf0240f58b7a8dac217fe260fc61f080a07f61372fbd5" +checksum = "317f17ff091ac4515f17cc7a190d2769a8c9a96d227de5d64b500b01cda8f2cd" diff --git a/api/Cargo.toml b/api/Cargo.toml index a9efc71..521aee7 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -13,21 +13,22 @@ sqlite = [ postgres = [ "infra/postgres", "tower-sessions-sqlx-store/postgres", - "k-core/postgres", ] -broker-nats = ["infra/broker-nats"] [dependencies] k-core = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-core", features = [ "logging", "db-sqlx", + "sqlite", + "http", + "auth","sessions-db" ] } domain = { path = "../domain" } infra = { path = "../infra", default-features = false, features = [ "sqlite", ] } -# Web framework +#Web framework axum = { version = "0.8.8", features = ["macros"] } tower = "0.5.2" tower-http = { version = "0.6.2", features = ["cors", "trace"] } @@ -62,8 +63,6 @@ uuid = { version = "1.19.0", features = ["v4", "serde"] } tracing = "0.1" tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } -# Database dotenvy = "0.15.7" +config = "0.15.19" -# Configuration -config = "0.15.9" diff --git a/api/src/config.rs b/api/src/config.rs index ba33a59..aa06dbd 100644 --- a/api/src/config.rs +++ b/api/src/config.rs @@ -2,12 +2,15 @@ //! //! Loads configuration from environment variables. +use std::env; + use serde::Deserialize; #[derive(Debug, Clone, Deserialize)] pub struct Config { pub database_url: String, pub session_secret: String, + pub cors_allowed_origins: Vec, #[serde(default = "default_port")] pub port: u16, @@ -32,4 +35,39 @@ impl Config { .build()? .try_deserialize() } + + pub fn from_env() -> Self { + // Load .env file if it exists, ignore errors if it doesn't + let _ = dotenvy::dotenv(); + + let host = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); + let port = env::var("PORT") + .ok() + .and_then(|p| p.parse().ok()) + .unwrap_or(3000); + + let database_url = + env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite:data.db?mode=rwc".to_string()); + + let session_secret = env::var("SESSION_SECRET").unwrap_or_else(|_| { + "k-notes-super-secret-key-must-be-at-least-64-bytes-long!!!!".to_string() + }); + + let cors_origins_str = env::var("CORS_ALLOWED_ORIGINS") + .unwrap_or_else(|_| "http://localhost:5173".to_string()); + + let cors_allowed_origins = cors_origins_str + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + + Self { + host, + port, + database_url, + session_secret, + cors_allowed_origins, + } + } } diff --git a/api/src/main.rs b/api/src/main.rs index 0e5d11c..9eb06db 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -1,10 +1,15 @@ use std::net::SocketAddr; use std::time::Duration as StdDuration; +use axum::Router; use domain::UserService; use infra::factory::build_session_store; use infra::factory::build_user_repository; +use infra::run_migrations; +use k_core::http::server::ServerConfig; +use k_core::http::server::apply_standard_middleware; use k_core::logging; +use time::Duration; use tokio::net::TcpListener; use tower_sessions::{Expiry, SessionManagerLayer}; use tracing::info; @@ -16,6 +21,7 @@ mod error; mod routes; mod state; +use crate::auth::setup_auth_layer; use crate::config::Config; use crate::state::AppState; @@ -23,38 +29,61 @@ use crate::state::AppState; async fn main() -> anyhow::Result<()> { logging::init("api"); - dotenvy::dotenv().ok(); - let config = Config::new().expect("Failed to load configuration"); + let config = Config::from_env(); info!("Starting server on {}:{}", config.host, config.port); + // Setup database + tracing::info!("Connecting to database: {}", config.database_url); let db_config = k_core::db::DatabaseConfig { url: config.database_url.clone(), max_connections: 5, + min_connections: 1, acquire_timeout: StdDuration::from_secs(30), }; let db_pool = k_core::db::connect(&db_config).await?; - infra::db::run_migrations(&db_pool).await?; + run_migrations(&db_pool).await?; let user_repo = build_user_repository(&db_pool).await?; let user_service = UserService::new(user_repo.clone()); - let session_store = build_session_store(&db_pool).await?; - - let session_layer = SessionManagerLayer::new(session_store) - .with_secure(false) // Set to true in production with HTTPS - .with_expiry(Expiry::OnInactivity(time::Duration::hours(1))); - - let auth_layer = auth::setup_auth_layer(session_layer, user_repo.clone()).await?; - let state = AppState::new(user_service, config.clone()); - let app = routes::api_v1_router().layer(auth_layer).with_state(state); + let session_store = build_session_store(&db_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 prod + .with_expiry(Expiry::OnInactivity(Duration::days(7))); + + let auth_layer = setup_auth_layer(session_layer, user_repo).await?; + + let server_config = ServerConfig { + cors_origins: config.cors_allowed_origins.clone(), + session_secret: Some(config.session_secret.clone()), + }; + + let app = Router::new() + .nest("/api/v1", routes::api_v1_router()) + .layer(auth_layer) + .with_state(state); + + let app = apply_standard_middleware(app, &server_config); let addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?; let listener = TcpListener::bind(addr).await?; + + tracing::info!("🚀 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(()) diff --git a/infra/src/db.rs b/infra/src/db.rs index facfd9a..def2d5d 100644 --- a/infra/src/db.rs +++ b/infra/src/db.rs @@ -1,4 +1,4 @@ -pub use k_core::db::{DatabaseConfig, DatabasePool}; +pub use k_core::db::DatabasePool; pub async fn run_migrations(pool: &DatabasePool) -> Result<(), sqlx::Error> { match pool { diff --git a/infra/src/factory.rs b/infra/src/factory.rs index d60c94a..388e984 100644 --- a/infra/src/factory.rs +++ b/infra/src/factory.rs @@ -5,6 +5,8 @@ use crate::SqliteUserRepository; use crate::db::DatabasePool; use domain::UserRepository; +use k_core::session::store::InfraSessionStore; + #[derive(Debug, thiserror::Error)] pub enum FactoryError { #[error("Database error: {0}")] @@ -35,20 +37,14 @@ pub async fn build_user_repository(pool: &DatabasePool) -> FactoryResult FactoryResult { - match pool { + Ok(match pool { #[cfg(feature = "sqlite")] - DatabasePool::Sqlite(pool) => { - let store = tower_sessions_sqlx_store::SqliteStore::new(pool.clone()); - Ok(crate::session_store::InfraSessionStore::Sqlite(store)) + DatabasePool::Sqlite(p) => { + InfraSessionStore::Sqlite(tower_sessions_sqlx_store::SqliteStore::new(p.clone())) } #[cfg(feature = "postgres")] - DatabasePool::Postgres(pool) => { - let store = tower_sessions_sqlx_store::PostgresStore::new(pool.clone()); - Ok(crate::session_store::InfraSessionStore::Postgres(store)) + DatabasePool::Postgres(p) => { + InfraSessionStore::Postgres(tower_sessions_sqlx_store::PostgresStore::new(p.clone())) } - #[allow(unreachable_patterns)] - _ => Err(FactoryError::NotImplemented( - "No database feature enabled".to_string(), - )), - } + }) } diff --git a/infra/src/lib.rs b/infra/src/lib.rs index 2d71602..8d46f61 100644 --- a/infra/src/lib.rs +++ b/infra/src/lib.rs @@ -20,6 +20,6 @@ pub mod session_store; mod user_repository; // Re-export for convenience -pub use db::{DatabaseConfig, run_migrations}; +pub use db::run_migrations; #[cfg(feature = "sqlite")] pub use user_repository::SqliteUserRepository; diff --git a/infra/src/session_store.rs b/infra/src/session_store.rs index 462aa85..e9f5bee 100644 --- a/infra/src/session_store.rs +++ b/infra/src/session_store.rs @@ -1,73 +1 @@ -use async_trait::async_trait; -use sqlx; -use tower_sessions::{ - SessionStore, - session::{Id, Record}, -}; -#[cfg(feature = "postgres")] -use tower_sessions_sqlx_store::PostgresStore; -#[cfg(feature = "sqlite")] -use tower_sessions_sqlx_store::SqliteStore; - -#[derive(Clone, Debug)] -pub enum InfraSessionStore { - #[cfg(feature = "sqlite")] - Sqlite(SqliteStore), - #[cfg(feature = "postgres")] - Postgres(PostgresStore), -} - -#[async_trait] -impl SessionStore for InfraSessionStore { - async fn save(&self, session_record: &Record) -> tower_sessions::session_store::Result<()> { - match self { - #[cfg(feature = "sqlite")] - Self::Sqlite(store) => store.save(session_record).await, - #[cfg(feature = "postgres")] - Self::Postgres(store) => store.save(session_record).await, - #[allow(unreachable_patterns)] - _ => Err(tower_sessions::session_store::Error::Backend( - "No backend enabled".to_string(), - )), - } - } - - async fn load(&self, session_id: &Id) -> tower_sessions::session_store::Result> { - match self { - #[cfg(feature = "sqlite")] - Self::Sqlite(store) => store.load(session_id).await, - #[cfg(feature = "postgres")] - Self::Postgres(store) => store.load(session_id).await, - #[allow(unreachable_patterns)] - _ => Err(tower_sessions::session_store::Error::Backend( - "No backend enabled".to_string(), - )), - } - } - - async fn delete(&self, session_id: &Id) -> tower_sessions::session_store::Result<()> { - match self { - #[cfg(feature = "sqlite")] - Self::Sqlite(store) => store.delete(session_id).await, - #[cfg(feature = "postgres")] - Self::Postgres(store) => store.delete(session_id).await, - #[allow(unreachable_patterns)] - _ => Err(tower_sessions::session_store::Error::Backend( - "No backend enabled".to_string(), - )), - } - } -} - -impl InfraSessionStore { - pub async fn migrate(&self) -> Result<(), sqlx::Error> { - match self { - #[cfg(feature = "sqlite")] - Self::Sqlite(store) => store.migrate().await, - #[cfg(feature = "postgres")] - Self::Postgres(store) => store.migrate().await, - #[allow(unreachable_patterns)] - _ => Err(sqlx::Error::Configuration("No backend enabled".into())), - } - } -} +pub use k_core::session::store::InfraSessionStore; diff --git a/infra/src/user_repository.rs b/infra/src/user_repository.rs index 64919f5..a2cfec6 100644 --- a/infra/src/user_repository.rs +++ b/infra/src/user_repository.rs @@ -141,18 +141,17 @@ impl UserRepository for SqliteUserRepository { #[cfg(all(test, feature = "sqlite"))] mod tests { use super::*; - use crate::db::{DatabaseConfig, DatabasePool, run_migrations}; - use k_core::db::connect; // Import k_core::db::connect + use crate::db::run_migrations; + use k_core::db::{DatabaseConfig, DatabasePool, connect}; async fn setup_test_db() -> SqlitePool { let config = DatabaseConfig::default(); - // connect returns DatabasePool directly now let db_pool = connect(&config).await.expect("Failed to create pool"); + run_migrations(&db_pool).await.unwrap(); - // Extract SqlitePool from DatabasePool for SqliteUserRepository + match db_pool { DatabasePool::Sqlite(pool) => pool, - _ => panic!("Expected SqlitePool for testing"), } }