feat(api): config from env vars (HOST, PORT, CORS_ALLOWED_ORIGINS, DATABASE_URL)
This commit is contained in:
10
.env.compose
10
.env.compose
@@ -4,11 +4,19 @@
|
|||||||
# Then run:
|
# Then run:
|
||||||
# docker compose --env-file .env.compose.local up -d --build
|
# docker compose --env-file .env.compose.local up -d --build
|
||||||
|
|
||||||
|
# ── Frontend ──────────────────────────────────────────────────────────────────
|
||||||
# URL your browser (and the SSR server) uses to reach the API.
|
# 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
|
# LAN example: http://192.168.1.100:8000
|
||||||
# Reverse proxy: https://pocketchords.yourdomain.com/api
|
# Reverse proxy: https://pocketchords.yourdomain.com/api
|
||||||
VITE_API_URL=http://localhost:8000
|
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
|
API_PORT=8000
|
||||||
APP_PORT=3000
|
APP_PORT=3000
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ COPY --from=builder /app/target/release/api .
|
|||||||
# Create data directory for SQLite
|
# Create data directory for SQLite
|
||||||
RUN mkdir -p /app/data
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
ENV DATABASE_URL=sqlite:///app/data/template.db
|
ENV DATABASE_URL=sqlite:///app/data/pocket-chords.db
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
|
|||||||
52
crates/api/src/config.rs
Normal file
52
crates/api/src/config.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
mod config;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
|
||||||
use axum::{Router, routing::{get, post}};
|
use axum::{Router, http::HeaderValue, routing::{get, post}};
|
||||||
use common::{SongSearchService, SongService};
|
use common::{SongSearchService, SongService};
|
||||||
|
use config::{Config, CorsOrigins};
|
||||||
use persistence::SqliteRepositoryFactory;
|
use persistence::SqliteRepositoryFactory;
|
||||||
use routes::songs::{create_song, delete_song, get_song, list_songs, update_song};
|
use routes::songs::{create_song, delete_song, get_song, list_songs, update_song};
|
||||||
use routes::tabs::{AppState, parse_tab};
|
use routes::tabs::{AppState, parse_tab};
|
||||||
@@ -13,9 +15,10 @@ use ug_parser::{UgHtmlParser, UgTabFetcher};
|
|||||||
async fn main() {
|
async fn main() {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let database_url = std::env::var("DATABASE_URL")
|
let config = Config::from_env();
|
||||||
.unwrap_or_else(|_| "sqlite://./pocket-chords.db".into());
|
tracing::info!(?config, "starting with config");
|
||||||
let repo = SqliteRepositoryFactory::create(&database_url)
|
|
||||||
|
let repo = SqliteRepositoryFactory::create(&config.database_url)
|
||||||
.await
|
.await
|
||||||
.expect("failed to connect to database");
|
.expect("failed to connect to database");
|
||||||
let songs = SongService::new(Box::new(repo.clone()));
|
let songs = SongService::new(Box::new(repo.clone()));
|
||||||
@@ -28,10 +31,22 @@ async fn main() {
|
|||||||
search,
|
search,
|
||||||
});
|
});
|
||||||
|
|
||||||
let cors = CorsLayer::new()
|
let cors = match config.cors_origins {
|
||||||
|
CorsOrigins::Any => CorsLayer::new()
|
||||||
.allow_origin(Any)
|
.allow_origin(Any)
|
||||||
.allow_methods(Any)
|
.allow_methods(Any)
|
||||||
.allow_headers(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()
|
let app = Router::new()
|
||||||
.route("/tabs/parse", post(parse_tab))
|
.route("/tabs/parse", post(parse_tab))
|
||||||
@@ -40,7 +55,9 @@ async fn main() {
|
|||||||
.layer(cors)
|
.layer(cors)
|
||||||
.with_state(state);
|
.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());
|
tracing::info!("listening on {}", listener.local_addr().unwrap());
|
||||||
axum::serve(listener, app).await.unwrap();
|
axum::serve(listener, app).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ services:
|
|||||||
- "${API_PORT:-8000}:8000"
|
- "${API_PORT:-8000}:8000"
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: sqlite:///app/data/pocket-chords.db
|
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:
|
volumes:
|
||||||
- api-data:/app/data
|
- api-data:/app/data
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user