feat: add support for user registration toggle and Traefik integration in Docker setup

This commit is contained in:
2026-03-11 22:56:23 +01:00
parent dc29976c1f
commit 4e1de172f7
7 changed files with 85 additions and 3 deletions

View File

@@ -13,6 +13,9 @@ COOKIE_SECRET=change-me-must-be-at-least-64-characters-long-for-production!!
JWT_EXPIRY_HOURS=24 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 # Set to true when serving over HTTPS
SECURE_COOKIE=false SECURE_COOKIE=false
PRODUCTION=false PRODUCTION=false
@@ -42,3 +45,17 @@ DB_MIN_CONNECTIONS=1
# ── PostgreSQL (optional, uncomment db service in compose.yml first) ────────── # ── PostgreSQL (optional, uncomment db service in compose.yml first) ──────────
# POSTGRES_PASSWORD=change-me # 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

49
compose.traefik.yml Normal file
View File

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

View File

@@ -18,6 +18,7 @@ services:
- JWT_EXPIRY_HOURS=${JWT_EXPIRY_HOURS:-24} - JWT_EXPIRY_HOURS=${JWT_EXPIRY_HOURS:-24}
- SECURE_COOKIE=${SECURE_COOKIE:-false} - SECURE_COOKIE=${SECURE_COOKIE:-false}
- PRODUCTION=${PRODUCTION:-false} - PRODUCTION=${PRODUCTION:-false}
- ALLOW_REGISTRATION=${ALLOW_REGISTRATION:-true}
- DB_MAX_CONNECTIONS=${DB_MAX_CONNECTIONS:-5} - DB_MAX_CONNECTIONS=${DB_MAX_CONNECTIONS:-5}
- DB_MIN_CONNECTIONS=${DB_MIN_CONNECTIONS:-1} - DB_MIN_CONNECTIONS=${DB_MIN_CONNECTIONS:-1}
# Jellyfin — all three required for schedule generation # Jellyfin — all three required for schedule generation

View File

@@ -32,6 +32,9 @@ pub struct Config {
/// Whether the application is running in production mode /// Whether the application is running in production mode
pub is_production: bool, pub is_production: bool,
/// Whether new user registration is open. Set ALLOW_REGISTRATION=false to lock down.
pub allow_registration: bool,
// Jellyfin media provider // Jellyfin media provider
pub jellyfin_base_url: Option<String>, pub jellyfin_base_url: Option<String>,
pub jellyfin_api_key: Option<String>, pub jellyfin_api_key: Option<String>,
@@ -100,6 +103,10 @@ impl Config {
.map(|v| v.to_lowercase() == "production" || v == "1" || v == "true") .map(|v| v.to_lowercase() == "production" || v == "1" || v == "true")
.unwrap_or(false); .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_base_url = env::var("JELLYFIN_BASE_URL").ok();
let jellyfin_api_key = env::var("JELLYFIN_API_KEY").ok(); let jellyfin_api_key = env::var("JELLYFIN_API_KEY").ok();
let jellyfin_user_id = env::var("JELLYFIN_USER_ID").ok(); let jellyfin_user_id = env::var("JELLYFIN_USER_ID").ok();
@@ -123,6 +130,7 @@ impl Config {
jwt_audience, jwt_audience,
jwt_expiry_hours, jwt_expiry_hours,
is_production, is_production,
allow_registration,
jellyfin_base_url, jellyfin_base_url,
jellyfin_api_key, jellyfin_api_key,
jellyfin_user_id, jellyfin_user_id,

View File

@@ -73,6 +73,10 @@ async fn register(
State(state): State<AppState>, State(state): State<AppState>,
Json(payload): Json<RegisterRequest>, Json(payload): Json<RegisterRequest>,
) -> Result<impl IntoResponse, ApiError> { ) -> Result<impl IntoResponse, ApiError> {
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 password_hash = infra::auth::hash_password(payload.password.as_ref());
let user = state let user = state

View File

@@ -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::dto::ConfigResponse;
use crate::state::AppState; use crate::state::AppState;
@@ -6,8 +8,8 @@ pub fn router() -> Router<AppState> {
Router::new().route("/", get(get_config)) Router::new().route("/", get(get_config))
} }
async fn get_config() -> Json<ConfigResponse> { async fn get_config(State(config): State<Arc<Config>>) -> Json<ConfigResponse> {
Json(ConfigResponse { Json(ConfigResponse {
allow_registration: true, // Default to true for template allow_registration: config.allow_registration,
}) })
} }

View File

@@ -19,6 +19,7 @@ services:
- JWT_EXPIRY_HOURS=24 - JWT_EXPIRY_HOURS=24
- SECURE_COOKIE=false # set to true when serving over HTTPS - SECURE_COOKIE=false # set to true when serving over HTTPS
- PRODUCTION=false - PRODUCTION=false
- ALLOW_REGISTRATION=true # set to false to disable new user registration
# Database pool # Database pool
- DB_MAX_CONNECTIONS=5 - DB_MAX_CONNECTIONS=5
- DB_MIN_CONNECTIONS=1 - DB_MIN_CONNECTIONS=1