diff --git a/Cargo.lock b/Cargo.lock index 7ae963e..38c8841 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1047,6 +1047,7 @@ dependencies = [ "axum", "axum-login", "chrono", + "dotenvy", "notes-domain", "notes-infra", "password-auth", diff --git a/notes-api/.env.example b/notes-api/.env.example new file mode 100644 index 0000000..897f802 --- /dev/null +++ b/notes-api/.env.example @@ -0,0 +1,14 @@ +# Server Configuration +HOST=127.0.0.1 +PORT=3000 + +# Database +DATABASE_URL=sqlite:data.db?mode=rwc + +# Security +# Generate a secure random string for production (min 64 chars recommended) +SESSION_SECRET=k-notes-super-secret-key-must-be-at-least-64-bytes-long!!!! + +# CORS +# Comma-separated list of allowed origins +CORS_ALLOWED_ORIGINS=http://localhost:5173 diff --git a/notes-api/Cargo.toml b/notes-api/Cargo.toml index 1507cd1..939dfcb 100644 --- a/notes-api/Cargo.toml +++ b/notes-api/Cargo.toml @@ -44,3 +44,4 @@ tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } # Database sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio"] } +dotenvy = "0.15.7" diff --git a/notes-api/src/config.rs b/notes-api/src/config.rs new file mode 100644 index 0000000..0b3a7f3 --- /dev/null +++ b/notes-api/src/config.rs @@ -0,0 +1,61 @@ +use std::env; + +/// Server configuration +#[derive(Debug, Clone)] +pub struct Config { + pub host: String, + pub port: u16, + pub database_url: String, + pub session_secret: String, + pub cors_allowed_origins: Vec, +} + +impl Default for Config { + fn default() -> Self { + Self { + host: "127.0.0.1".to_string(), + port: 3000, + database_url: "sqlite:data.db?mode=rwc".to_string(), + session_secret: "k-notes-super-secret-key-must-be-at-least-64-bytes-long!!!!" + .to_string(), + cors_allowed_origins: vec!["http://localhost:5173".to_string()], + } + } +} + +impl Config { + 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/notes-api/src/main.rs b/notes-api/src/main.rs index 2934468..1f2d5b2 100644 --- a/notes-api/src/main.rs +++ b/notes-api/src/main.rs @@ -19,51 +19,16 @@ use notes_infra::{ }; mod auth; +mod config; mod dto; mod error; mod routes; mod state; use auth::AuthBackend; +use config::Config; use state::AppState; -/// Server configuration -pub struct ServerConfig { - pub host: String, - pub port: u16, - pub database_url: String, - pub session_secret: String, -} - -impl Default for ServerConfig { - fn default() -> Self { - Self { - host: "127.0.0.1".to_string(), - port: 3000, - database_url: "sqlite:data.db?mode=rwc".to_string(), - session_secret: "k-notes-super-secret-key-must-be-at-least-64-bytes-long!!!!" - .to_string(), - } - } -} - -impl ServerConfig { - pub fn from_env() -> Self { - Self { - host: std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string()), - port: std::env::var("PORT") - .ok() - .and_then(|p| p.parse().ok()) - .unwrap_or(3000), - database_url: std::env::var("DATABASE_URL") - .unwrap_or_else(|_| "sqlite:data.db?mode=rwc".to_string()), - session_secret: std::env::var("SESSION_SECRET").unwrap_or_else(|_| { - "k-notes-super-secret-key-must-be-at-least-64-bytes-long!!!!".to_string() - }), - } - } -} - #[tokio::main] async fn main() -> anyhow::Result<()> { // Initialize tracing @@ -76,7 +41,7 @@ async fn main() -> anyhow::Result<()> { .init(); // Load configuration - let config = ServerConfig::from_env(); + let config = Config::from_env(); // Setup database tracing::info!("Connecting to database: {}", config.database_url); @@ -112,30 +77,35 @@ async fn main() -> anyhow::Result<()> { // Auth layer let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer).build(); + // Parse CORS origins + 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); + + // Add allowed origins + for origin in &config.cors_allowed_origins { + if let Ok(value) = origin.parse::() { + cors = cors.allow_origin(value); + } else { + tracing::warn!("Invalid CORS origin: {}", origin); + } + } + // Build the application let app = Router::new() .nest("/api/v1", routes::api_v1_router()) - .layer( - CorsLayer::new() - .allow_origin( - "http://localhost:5173" - .parse::() - .unwrap(), - ) - .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), - ) + .layer(cors) .layer(auth_layer) .layer(TraceLayer::new_for_http()) .with_state(state);