feat(api): config from env vars (HOST, PORT, CORS_ALLOWED_ORIGINS, DATABASE_URL)

This commit is contained in:
2026-04-09 01:23:10 +02:00
parent 808ce287a5
commit b1d778284c
5 changed files with 93 additions and 11 deletions

View File

@@ -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

View File

@@ -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

52
crates/api/src/config.rs Normal file
View File

@@ -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<String>),
}
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::<u16>().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)
}
}

View File

@@ -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<HeaderValue> = 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();
}

View File

@@ -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