feat: centralize application configuration using environment variables and add dynamic CORS support
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1047,6 +1047,7 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"axum-login",
|
"axum-login",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"dotenvy",
|
||||||
"notes-domain",
|
"notes-domain",
|
||||||
"notes-infra",
|
"notes-infra",
|
||||||
"password-auth",
|
"password-auth",
|
||||||
|
|||||||
14
notes-api/.env.example
Normal file
14
notes-api/.env.example
Normal file
@@ -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
|
||||||
@@ -44,3 +44,4 @@ tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
|||||||
|
|
||||||
# Database
|
# Database
|
||||||
sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio"] }
|
sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio"] }
|
||||||
|
dotenvy = "0.15.7"
|
||||||
|
|||||||
61
notes-api/src/config.rs
Normal file
61
notes-api/src/config.rs
Normal file
@@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,51 +19,16 @@ use notes_infra::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
mod auth;
|
mod auth;
|
||||||
|
mod config;
|
||||||
mod dto;
|
mod dto;
|
||||||
mod error;
|
mod error;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
use auth::AuthBackend;
|
use auth::AuthBackend;
|
||||||
|
use config::Config;
|
||||||
use state::AppState;
|
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]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
// Initialize tracing
|
// Initialize tracing
|
||||||
@@ -76,7 +41,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.init();
|
.init();
|
||||||
|
|
||||||
// Load configuration
|
// Load configuration
|
||||||
let config = ServerConfig::from_env();
|
let config = Config::from_env();
|
||||||
|
|
||||||
// Setup database
|
// Setup database
|
||||||
tracing::info!("Connecting to database: {}", config.database_url);
|
tracing::info!("Connecting to database: {}", config.database_url);
|
||||||
@@ -112,16 +77,8 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
// Auth layer
|
// Auth layer
|
||||||
let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer).build();
|
let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer).build();
|
||||||
|
|
||||||
// Build the application
|
// Parse CORS origins
|
||||||
let app = Router::new()
|
let mut cors = CorsLayer::new()
|
||||||
.nest("/api/v1", routes::api_v1_router())
|
|
||||||
.layer(
|
|
||||||
CorsLayer::new()
|
|
||||||
.allow_origin(
|
|
||||||
"http://localhost:5173"
|
|
||||||
.parse::<axum::http::HeaderValue>()
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.allow_methods([
|
.allow_methods([
|
||||||
axum::http::Method::GET,
|
axum::http::Method::GET,
|
||||||
axum::http::Method::POST,
|
axum::http::Method::POST,
|
||||||
@@ -134,8 +91,21 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
axum::http::header::ACCEPT,
|
axum::http::header::ACCEPT,
|
||||||
axum::http::header::CONTENT_TYPE,
|
axum::http::header::CONTENT_TYPE,
|
||||||
])
|
])
|
||||||
.allow_credentials(true),
|
.allow_credentials(true);
|
||||||
)
|
|
||||||
|
// Add allowed origins
|
||||||
|
for origin in &config.cors_allowed_origins {
|
||||||
|
if let Ok(value) = origin.parse::<axum::http::HeaderValue>() {
|
||||||
|
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(cors)
|
||||||
.layer(auth_layer)
|
.layer(auth_layer)
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|||||||
Reference in New Issue
Block a user