From b1d778284c1d73933ed1346db69fb4e0c3241813 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 9 Apr 2026 01:23:10 +0200 Subject: [PATCH] feat(api): config from env vars (HOST, PORT, CORS_ALLOWED_ORIGINS, DATABASE_URL) --- .env.compose | 10 +++++++- Dockerfile | 2 +- crates/api/src/config.rs | 52 ++++++++++++++++++++++++++++++++++++++++ crates/api/src/main.rs | 35 ++++++++++++++++++++------- docker-compose.yml | 5 ++++ 5 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 crates/api/src/config.rs diff --git a/.env.compose b/.env.compose index 5846b95..6dd7bb3 100644 --- a/.env.compose +++ b/.env.compose @@ -4,11 +4,19 @@ # Then run: # docker compose --env-file .env.compose.local up -d --build +# ── Frontend ────────────────────────────────────────────────────────────────── # URL your browser (and the SSR server) uses to reach the API. +# Baked into the JS bundle at build time — rebuild the app image when changing. # LAN example: http://192.168.1.100:8000 # Reverse proxy: https://pocketchords.yourdomain.com/api VITE_API_URL=http://localhost:8000 -# Host ports (change if something else is already using them) +# ── Backend ─────────────────────────────────────────────────────────────────── +# Comma-separated allowed CORS origins, or * for any. +# Lock this down when exposing publicly: https://pocketchords.yourdomain.com +CORS_ALLOWED_ORIGINS=* + +# ── Ports (host-side) ───────────────────────────────────────────────────────── +# Change if something else is already using these ports on the host. API_PORT=8000 APP_PORT=3000 diff --git a/Dockerfile b/Dockerfile index 9ce60fc..54d2de0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ COPY --from=builder /app/target/release/api . # Create data directory for SQLite RUN mkdir -p /app/data -ENV DATABASE_URL=sqlite:///app/data/template.db +ENV DATABASE_URL=sqlite:///app/data/pocket-chords.db EXPOSE 8000 diff --git a/crates/api/src/config.rs b/crates/api/src/config.rs new file mode 100644 index 0000000..650dc9b --- /dev/null +++ b/crates/api/src/config.rs @@ -0,0 +1,52 @@ +use std::env; + +#[derive(Debug)] +pub struct Config { + pub database_url: String, + pub host: String, + pub port: u16, + /// Parsed CORS origin policy + pub cors_origins: CorsOrigins, +} + +#[derive(Debug)] +pub enum CorsOrigins { + /// Allow any origin (`CORS_ALLOWED_ORIGINS=*`) + Any, + /// Allow specific origins (`CORS_ALLOWED_ORIGINS=https://a.com,https://b.com`) + List(Vec), +} + +impl Config { + pub fn from_env() -> Self { + let database_url = env::var("DATABASE_URL") + .unwrap_or_else(|_| "sqlite://./pocket-chords.db".into()); + + let host = env::var("HOST").unwrap_or_else(|_| "0.0.0.0".into()); + + let port = env::var("PORT") + .ok() + .and_then(|v| v.parse::().ok()) + .unwrap_or(8000); + + let cors_origins = match env::var("CORS_ALLOWED_ORIGINS") + .unwrap_or_else(|_| "*".into()) + .trim() + .to_string() + { + s if s == "*" => CorsOrigins::Any, + s => CorsOrigins::List( + s.split(',') + .map(|o| o.trim().to_string()) + .filter(|o| !o.is_empty()) + .collect(), + ), + }; + + Self { database_url, host, port, cors_origins } + } + + pub fn bind_addr(&self) -> String { + format!("{}:{}", self.host, self.port) + } +} diff --git a/crates/api/src/main.rs b/crates/api/src/main.rs index 9bb6fc3..b0c8b9e 100644 --- a/crates/api/src/main.rs +++ b/crates/api/src/main.rs @@ -1,7 +1,9 @@ +mod config; mod routes; -use axum::{Router, routing::{get, post}}; +use axum::{Router, http::HeaderValue, routing::{get, post}}; use common::{SongSearchService, SongService}; +use config::{Config, CorsOrigins}; use persistence::SqliteRepositoryFactory; use routes::songs::{create_song, delete_song, get_song, list_songs, update_song}; use routes::tabs::{AppState, parse_tab}; @@ -13,9 +15,10 @@ use ug_parser::{UgHtmlParser, UgTabFetcher}; async fn main() { tracing_subscriber::fmt::init(); - let database_url = std::env::var("DATABASE_URL") - .unwrap_or_else(|_| "sqlite://./pocket-chords.db".into()); - let repo = SqliteRepositoryFactory::create(&database_url) + let config = Config::from_env(); + tracing::info!(?config, "starting with config"); + + let repo = SqliteRepositoryFactory::create(&config.database_url) .await .expect("failed to connect to database"); let songs = SongService::new(Box::new(repo.clone())); @@ -28,10 +31,22 @@ async fn main() { search, }); - let cors = CorsLayer::new() - .allow_origin(Any) - .allow_methods(Any) - .allow_headers(Any); + let cors = match config.cors_origins { + CorsOrigins::Any => CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(Any), + CorsOrigins::List(ref origins) => { + let parsed: Vec = origins + .iter() + .map(|o| o.parse().unwrap_or_else(|_| panic!("invalid CORS origin: {o}"))) + .collect(); + CorsLayer::new() + .allow_origin(parsed) + .allow_methods(Any) + .allow_headers(Any) + } + }; let app = Router::new() .route("/tabs/parse", post(parse_tab)) @@ -40,7 +55,9 @@ async fn main() { .layer(cors) .with_state(state); - let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap(); + let addr = config.bind_addr(); + let listener = tokio::net::TcpListener::bind(&addr).await + .unwrap_or_else(|e| panic!("failed to bind {addr}: {e}")); tracing::info!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); } diff --git a/docker-compose.yml b/docker-compose.yml index b4638dc..ce994d5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,11 @@ services: - "${API_PORT:-8000}:8000" environment: DATABASE_URL: sqlite:///app/data/pocket-chords.db + HOST: 0.0.0.0 + PORT: 8000 + # Comma-separated allowed origins, or * for any. + # Lock this down in production: https://pocketchords.yourdomain.com + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-*} volumes: - api-data:/app/data