feat: update dependencies and enhance configuration loading for CORS support
This commit is contained in:
21
Cargo.lock
generated
21
Cargo.lock
generated
@@ -1222,15 +1222,22 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "k-core"
|
name = "k-core"
|
||||||
version = "0.1.1"
|
version = "0.1.10"
|
||||||
source = "git+https://git.gabrielkaszewski.dev/GKaszewski/k-core#bda288362ade1cd3508bbc985a0aebd7714d9a18"
|
source = "git+https://git.gabrielkaszewski.dev/GKaszewski/k-core#7a72f5f54ad45ba82f451e90c44c0581d13194d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
|
"time",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
"tower-sessions",
|
"tower-sessions",
|
||||||
|
"tower-sessions-sqlx-store",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -3107,14 +3114,14 @@ version = "0.26.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"webpki-roots 1.0.4",
|
"webpki-roots 1.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-roots"
|
name = "webpki-roots"
|
||||||
version = "1.0.4"
|
version = "1.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e"
|
checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
@@ -3547,6 +3554,6 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.7"
|
version = "1.0.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de9211a9f64b825911bdf0240f58b7a8dac217fe260fc61f080a07f61372fbd5"
|
checksum = "317f17ff091ac4515f17cc7a190d2769a8c9a96d227de5d64b500b01cda8f2cd"
|
||||||
|
|||||||
@@ -13,14 +13,15 @@ sqlite = [
|
|||||||
postgres = [
|
postgres = [
|
||||||
"infra/postgres",
|
"infra/postgres",
|
||||||
"tower-sessions-sqlx-store/postgres",
|
"tower-sessions-sqlx-store/postgres",
|
||||||
"k-core/postgres",
|
|
||||||
]
|
]
|
||||||
broker-nats = ["infra/broker-nats"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
k-core = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-core", features = [
|
k-core = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-core", features = [
|
||||||
"logging",
|
"logging",
|
||||||
"db-sqlx",
|
"db-sqlx",
|
||||||
|
"sqlite",
|
||||||
|
"http",
|
||||||
|
"auth","sessions-db"
|
||||||
] }
|
] }
|
||||||
domain = { path = "../domain" }
|
domain = { path = "../domain" }
|
||||||
infra = { path = "../infra", default-features = false, features = [
|
infra = { path = "../infra", default-features = false, features = [
|
||||||
@@ -62,8 +63,6 @@ uuid = { version = "1.19.0", features = ["v4", "serde"] }
|
|||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||||
|
|
||||||
# Database
|
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
|
config = "0.15.19"
|
||||||
|
|
||||||
# Configuration
|
|
||||||
config = "0.15.9"
|
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
//!
|
//!
|
||||||
//! Loads configuration from environment variables.
|
//! Loads configuration from environment variables.
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub database_url: String,
|
pub database_url: String,
|
||||||
pub session_secret: String,
|
pub session_secret: String,
|
||||||
|
pub cors_allowed_origins: Vec<String>,
|
||||||
|
|
||||||
#[serde(default = "default_port")]
|
#[serde(default = "default_port")]
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
@@ -32,4 +35,39 @@ impl Config {
|
|||||||
.build()?
|
.build()?
|
||||||
.try_deserialize()
|
.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,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::time::Duration as StdDuration;
|
use std::time::Duration as StdDuration;
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
use domain::UserService;
|
use domain::UserService;
|
||||||
use infra::factory::build_session_store;
|
use infra::factory::build_session_store;
|
||||||
use infra::factory::build_user_repository;
|
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 k_core::logging;
|
||||||
|
use time::Duration;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tower_sessions::{Expiry, SessionManagerLayer};
|
use tower_sessions::{Expiry, SessionManagerLayer};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@@ -16,6 +21,7 @@ mod error;
|
|||||||
mod routes;
|
mod routes;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
|
use crate::auth::setup_auth_layer;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
@@ -23,38 +29,61 @@ use crate::state::AppState;
|
|||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
logging::init("api");
|
logging::init("api");
|
||||||
|
|
||||||
dotenvy::dotenv().ok();
|
let config = Config::from_env();
|
||||||
let config = Config::new().expect("Failed to load configuration");
|
|
||||||
|
|
||||||
info!("Starting server on {}:{}", config.host, config.port);
|
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 {
|
let db_config = k_core::db::DatabaseConfig {
|
||||||
url: config.database_url.clone(),
|
url: config.database_url.clone(),
|
||||||
max_connections: 5,
|
max_connections: 5,
|
||||||
|
min_connections: 1,
|
||||||
acquire_timeout: StdDuration::from_secs(30),
|
acquire_timeout: StdDuration::from_secs(30),
|
||||||
};
|
};
|
||||||
|
|
||||||
let db_pool = k_core::db::connect(&db_config).await?;
|
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_repo = build_user_repository(&db_pool).await?;
|
||||||
let user_service = UserService::new(user_repo.clone());
|
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 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 addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?;
|
||||||
let listener = TcpListener::bind(addr).await?;
|
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?;
|
axum::serve(listener, app).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -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> {
|
pub async fn run_migrations(pool: &DatabasePool) -> Result<(), sqlx::Error> {
|
||||||
match pool {
|
match pool {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ use crate::SqliteUserRepository;
|
|||||||
use crate::db::DatabasePool;
|
use crate::db::DatabasePool;
|
||||||
use domain::UserRepository;
|
use domain::UserRepository;
|
||||||
|
|
||||||
|
use k_core::session::store::InfraSessionStore;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum FactoryError {
|
pub enum FactoryError {
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
@@ -35,20 +37,14 @@ pub async fn build_user_repository(pool: &DatabasePool) -> FactoryResult<Arc<dyn
|
|||||||
pub async fn build_session_store(
|
pub async fn build_session_store(
|
||||||
pool: &DatabasePool,
|
pool: &DatabasePool,
|
||||||
) -> FactoryResult<crate::session_store::InfraSessionStore> {
|
) -> FactoryResult<crate::session_store::InfraSessionStore> {
|
||||||
match pool {
|
Ok(match pool {
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
DatabasePool::Sqlite(pool) => {
|
DatabasePool::Sqlite(p) => {
|
||||||
let store = tower_sessions_sqlx_store::SqliteStore::new(pool.clone());
|
InfraSessionStore::Sqlite(tower_sessions_sqlx_store::SqliteStore::new(p.clone()))
|
||||||
Ok(crate::session_store::InfraSessionStore::Sqlite(store))
|
|
||||||
}
|
}
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
DatabasePool::Postgres(pool) => {
|
DatabasePool::Postgres(p) => {
|
||||||
let store = tower_sessions_sqlx_store::PostgresStore::new(pool.clone());
|
InfraSessionStore::Postgres(tower_sessions_sqlx_store::PostgresStore::new(p.clone()))
|
||||||
Ok(crate::session_store::InfraSessionStore::Postgres(store))
|
|
||||||
}
|
|
||||||
#[allow(unreachable_patterns)]
|
|
||||||
_ => Err(FactoryError::NotImplemented(
|
|
||||||
"No database feature enabled".to_string(),
|
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,6 @@ pub mod session_store;
|
|||||||
mod user_repository;
|
mod user_repository;
|
||||||
|
|
||||||
// Re-export for convenience
|
// Re-export for convenience
|
||||||
pub use db::{DatabaseConfig, run_migrations};
|
pub use db::run_migrations;
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
pub use user_repository::SqliteUserRepository;
|
pub use user_repository::SqliteUserRepository;
|
||||||
|
|||||||
@@ -1,73 +1 @@
|
|||||||
use async_trait::async_trait;
|
pub use k_core::session::store::InfraSessionStore;
|
||||||
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<Option<Record>> {
|
|
||||||
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())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -141,18 +141,17 @@ impl UserRepository for SqliteUserRepository {
|
|||||||
#[cfg(all(test, feature = "sqlite"))]
|
#[cfg(all(test, feature = "sqlite"))]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::db::{DatabaseConfig, DatabasePool, run_migrations};
|
use crate::db::run_migrations;
|
||||||
use k_core::db::connect; // Import k_core::db::connect
|
use k_core::db::{DatabaseConfig, DatabasePool, connect};
|
||||||
|
|
||||||
async fn setup_test_db() -> SqlitePool {
|
async fn setup_test_db() -> SqlitePool {
|
||||||
let config = DatabaseConfig::default();
|
let config = DatabaseConfig::default();
|
||||||
// connect returns DatabasePool directly now
|
|
||||||
let db_pool = connect(&config).await.expect("Failed to create pool");
|
let db_pool = connect(&config).await.expect("Failed to create pool");
|
||||||
|
|
||||||
run_migrations(&db_pool).await.unwrap();
|
run_migrations(&db_pool).await.unwrap();
|
||||||
// Extract SqlitePool from DatabasePool for SqliteUserRepository
|
|
||||||
match db_pool {
|
match db_pool {
|
||||||
DatabasePool::Sqlite(pool) => pool,
|
DatabasePool::Sqlite(pool) => pool,
|
||||||
_ => panic!("Expected SqlitePool for testing"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user