diff --git a/.env.example b/.env.example index 545e37d..9597175 100644 --- a/.env.example +++ b/.env.example @@ -13,6 +13,9 @@ COOKIE_SECRET=change-me-must-be-at-least-64-characters-long-for-production!! JWT_EXPIRY_HOURS=24 +# Set to false to disable new user registration (existing users can still log in) +ALLOW_REGISTRATION=true + # Set to true when serving over HTTPS SECURE_COOKIE=false PRODUCTION=false @@ -42,3 +45,17 @@ DB_MIN_CONNECTIONS=1 # ── PostgreSQL (optional, uncomment db service in compose.yml first) ────────── # POSTGRES_PASSWORD=change-me + +# ── Traefik (only needed with compose.traefik.yml) ──────────────────────────── +# External Docker network Traefik is attached to +TRAEFIK_NETWORK=traefik_proxy +# Traefik entrypoint (usually websecure for HTTPS, web for HTTP) +TRAEFIK_ENTRYPOINT=websecure +# Cert resolver defined in your Traefik static config +TRAEFIK_CERT_RESOLVER=letsencrypt +# Public hostnames routed by Traefik +FRONTEND_HOST=tv.example.com +BACKEND_HOST=tv-api.example.com +# When using Traefik, update these to the public URLs: +# NEXT_PUBLIC_API_URL=https://tv-api.example.com/api/v1 +# CORS_ALLOWED_ORIGINS=https://tv.example.com diff --git a/compose.traefik.yml b/compose.traefik.yml new file mode 100644 index 0000000..e6a372a --- /dev/null +++ b/compose.traefik.yml @@ -0,0 +1,49 @@ +# Traefik integration overlay. +# +# Usage: +# docker compose -f compose.yml -f compose.traefik.yml up -d --build +# +# Assumes Traefik is already running on your host with an external Docker +# network. Add these variables to your .env (see .env.example): +# +# TRAEFIK_NETWORK name of the external Traefik network (default: traefik_proxy) +# TRAEFIK_ENTRYPOINT Traefik entrypoint name (default: websecure) +# TRAEFIK_CERT_RESOLVER cert resolver name for TLS (default: letsencrypt) +# FRONTEND_HOST public hostname for the frontend e.g. tv.example.com +# BACKEND_HOST public hostname for the backend API e.g. tv-api.example.com +# +# Remember: NEXT_PUBLIC_API_URL in .env must be the *public* backend URL, +# e.g. https://tv-api.example.com/api/v1, and you must rebuild after changing it. + +services: + + backend: + ports: [] # Traefik handles ingress; no direct port exposure needed + networks: + - default + - traefik + labels: + - "traefik.enable=true" + - "traefik.docker.network=${TRAEFIK_NETWORK:-traefik_proxy}" + - "traefik.http.routers.ktv-backend.rule=Host(`${BACKEND_HOST}`)" + - "traefik.http.routers.ktv-backend.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" + - "traefik.http.routers.ktv-backend.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-letsencrypt}" + - "traefik.http.services.ktv-backend.loadbalancer.server.port=3000" + + frontend: + ports: [] + networks: + - default + - traefik + labels: + - "traefik.enable=true" + - "traefik.docker.network=${TRAEFIK_NETWORK:-traefik_proxy}" + - "traefik.http.routers.ktv-frontend.rule=Host(`${FRONTEND_HOST}`)" + - "traefik.http.routers.ktv-frontend.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" + - "traefik.http.routers.ktv-frontend.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-letsencrypt}" + - "traefik.http.services.ktv-frontend.loadbalancer.server.port=3001" + +networks: + traefik: + external: true + name: ${TRAEFIK_NETWORK:-traefik_proxy} diff --git a/compose.yml b/compose.yml index 348e85a..c7f4570 100644 --- a/compose.yml +++ b/compose.yml @@ -18,6 +18,7 @@ services: - JWT_EXPIRY_HOURS=${JWT_EXPIRY_HOURS:-24} - SECURE_COOKIE=${SECURE_COOKIE:-false} - PRODUCTION=${PRODUCTION:-false} + - ALLOW_REGISTRATION=${ALLOW_REGISTRATION:-true} - DB_MAX_CONNECTIONS=${DB_MAX_CONNECTIONS:-5} - DB_MIN_CONNECTIONS=${DB_MIN_CONNECTIONS:-1} # Jellyfin — all three required for schedule generation diff --git a/k-tv-backend/api/src/config.rs b/k-tv-backend/api/src/config.rs index 6fe7d92..ffb99a9 100644 --- a/k-tv-backend/api/src/config.rs +++ b/k-tv-backend/api/src/config.rs @@ -32,6 +32,9 @@ pub struct Config { /// Whether the application is running in production mode pub is_production: bool, + /// Whether new user registration is open. Set ALLOW_REGISTRATION=false to lock down. + pub allow_registration: bool, + // Jellyfin media provider pub jellyfin_base_url: Option, pub jellyfin_api_key: Option, @@ -100,6 +103,10 @@ impl Config { .map(|v| v.to_lowercase() == "production" || v == "1" || v == "true") .unwrap_or(false); + let allow_registration = env::var("ALLOW_REGISTRATION") + .map(|v| !(v == "false" || v == "0")) + .unwrap_or(true); + let jellyfin_base_url = env::var("JELLYFIN_BASE_URL").ok(); let jellyfin_api_key = env::var("JELLYFIN_API_KEY").ok(); let jellyfin_user_id = env::var("JELLYFIN_USER_ID").ok(); @@ -123,6 +130,7 @@ impl Config { jwt_audience, jwt_expiry_hours, is_production, + allow_registration, jellyfin_base_url, jellyfin_api_key, jellyfin_user_id, diff --git a/k-tv-backend/api/src/routes/auth.rs b/k-tv-backend/api/src/routes/auth.rs index 4bfbed5..4feb871 100644 --- a/k-tv-backend/api/src/routes/auth.rs +++ b/k-tv-backend/api/src/routes/auth.rs @@ -73,6 +73,10 @@ async fn register( State(state): State, Json(payload): Json, ) -> Result { + if !state.config.allow_registration { + return Err(ApiError::Forbidden("Registration is disabled".to_string())); + } + let password_hash = infra::auth::hash_password(payload.password.as_ref()); let user = state diff --git a/k-tv-backend/api/src/routes/config.rs b/k-tv-backend/api/src/routes/config.rs index 28278f8..ada278f 100644 --- a/k-tv-backend/api/src/routes/config.rs +++ b/k-tv-backend/api/src/routes/config.rs @@ -1,4 +1,6 @@ -use axum::{Json, Router, routing::get}; +use axum::{Json, Router, extract::State, routing::get}; +use std::sync::Arc; +use crate::config::Config; use crate::dto::ConfigResponse; use crate::state::AppState; @@ -6,8 +8,8 @@ pub fn router() -> Router { Router::new().route("/", get(get_config)) } -async fn get_config() -> Json { +async fn get_config(State(config): State>) -> Json { Json(ConfigResponse { - allow_registration: true, // Default to true for template + allow_registration: config.allow_registration, }) } diff --git a/k-tv-backend/compose.yml b/k-tv-backend/compose.yml index 616a61f..2dcc158 100644 --- a/k-tv-backend/compose.yml +++ b/k-tv-backend/compose.yml @@ -19,6 +19,7 @@ services: - JWT_EXPIRY_HOURS=24 - SECURE_COOKIE=false # set to true when serving over HTTPS - PRODUCTION=false + - ALLOW_REGISTRATION=true # set to false to disable new user registration # Database pool - DB_MAX_CONNECTIONS=5 - DB_MIN_CONNECTIONS=1