diff --git a/.env.example b/.env.example index 210be31..65625fe 100644 --- a/.env.example +++ b/.env.example @@ -17,6 +17,11 @@ GRAFANA_PASSWORD=change-this-password-in-production CELERY_BROKER_URL=redis://redis:6379/0 CELERY_RESULT_BACKEND=redis://redis:6379/0 +# Registration +# Set to "false" to disable user registration. For prod, build the image with: +# docker build --build-arg REGISTRATION_ENABLED=false -t your-image . +REGISTRATION_ENABLED=true + # MinIO Configuration (Object Storage) # MinIO server stays hidden - Flask proxies all requests # MINIO_ENDPOINT: Internal Docker network address (for server-to-server communication) diff --git a/backend/app/config.py b/backend/app/config.py index 5816b54..a318783 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -16,6 +16,7 @@ class Config: MAX_FAILED_LOGIN_ATTEMPTS = int(os.environ.get("MAX_FAILED_LOGIN_ATTEMPTS", "5")) ACCOUNT_LOCK_DURATION_MINUTES = int(os.environ.get("ACCOUNT_LOCK_DURATION_MINUTES", "30")) LOCKOUT_STRATEGY = os.environ.get("LOCKOUT_STRATEGY", "timeout") # "timeout" | "permanent" + REGISTRATION_ENABLED = os.environ.get("REGISTRATION_ENABLED", "true").lower() == "true" # Celery Configuration CELERY = { diff --git a/backend/app/routes/api.py b/backend/app/routes/api.py index 1eb3904..fa8adb7 100644 --- a/backend/app/routes/api.py +++ b/backend/app/routes/api.py @@ -1,6 +1,6 @@ """Auth and user management API routes""" -from flask import Blueprint, jsonify, request +from flask import Blueprint, current_app, jsonify, request from flask_jwt_extended import ( create_access_token, create_refresh_token, @@ -24,6 +24,9 @@ api_bp = Blueprint("api", __name__) @api_bp.route("/auth/register", methods=["POST"]) def register(): """Register a new user""" + if not current_app.config.get("REGISTRATION_ENABLED", True): + return jsonify({"error": "Not found"}), 404 + try: data = request.get_json() diff --git a/backend/tests/test_routes.py b/backend/tests/test_routes.py index b8e0ab4..de1e6b7 100644 --- a/backend/tests/test_routes.py +++ b/backend/tests/test_routes.py @@ -82,6 +82,23 @@ class TestAuthRoutes: data = response.get_json() assert "already exists" in data["error"].lower() + @pytest.mark.auth + def test_register_disabled(self, app, client, monkeypatch): + """Test registration returns 404 when disabled via config""" + monkeypatch.setitem(app.config, "REGISTRATION_ENABLED", False) + response = client.post( + "/api/auth/register", + json={ + "email": "disabled@example.com", + "password": "password123", + "confirm_password": "password123", + }, + ) + + assert response.status_code == 404 + data = response.get_json() + assert data["error"] == "Not found" + @pytest.mark.auth def test_login_success(self, client, regular_user): """Test successful login""" diff --git a/backend/wsgi.py b/backend/wsgi.py index 489fcd0..99b105a 100644 --- a/backend/wsgi.py +++ b/backend/wsgi.py @@ -1,8 +1,11 @@ import os from app import create_app +from werkzeug.middleware.proxy_fix import ProxyFix env = os.environ.get('FLASK_ENV', 'dev') app = create_app(env) +app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1) + if __name__ == '__main__': app.run(host='0.0.0.0', port=8000) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 3c19411..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,191 +0,0 @@ -version: '3.8' - -services: - backend: - build: - context: ./backend - dockerfile: Dockerfile - container_name: crafting-shop-backend - ports: - - "5000:5000" - environment: - - FLASK_ENV=${FLASK_ENV:-prod} - - SECRET_KEY=${SECRET_KEY} - - JWT_SECRET_KEY=${JWT_SECRET_KEY} - - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} - depends_on: - - postgres - - redis - networks: - - crafting-shop-network - volumes: - - backend-data:/app/instance - restart: unless-stopped - - frontend: - build: - context: ./frontend - dockerfile: Dockerfile - container_name: crafting-shop-frontend - ports: - - "80:80" - depends_on: - - backend - networks: - - crafting-shop-network - restart: unless-stopped - - postgres: - image: postgres:15-alpine - container_name: crafting-shop-postgres - environment: - - POSTGRES_USER=${POSTGRES_USER:-crafting} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - - POSTGRES_DB=${POSTGRES_DB:-crafting_shop} - volumes: - - postgres-data:/var/lib/postgresql/data - networks: - - crafting-shop-network - restart: unless-stopped - - redis: - image: redis:7-alpine - container_name: crafting-shop-redis - networks: - - crafting-shop-network - restart: unless-stopped - - minio: - image: minio/minio:latest - container_name: crafting-shop-minio - command: server /data --console-address ":9001" - ports: - - "9000:9000" - - "9001:9001" - environment: - - MINIO_ROOT_USER=${MINIO_ROOT_USER:-minioadmin} - - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-minioadmin} - volumes: - - minio-data:/data - networks: - - crafting-shop-network - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - interval: 30s - timeout: 20s - retries: 3 - - celery_worker: - build: - context: ./backend - dockerfile: Dockerfile - container_name: crafting-shop-celery-worker - command: celery -A celery_worker worker --loglevel=info --concurrency=4 - environment: - - FLASK_ENV=${FLASK_ENV:-prod} - - SECRET_KEY=${SECRET_KEY} - - JWT_SECRET_KEY=${JWT_SECRET_KEY} - - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} - - CELERY_BROKER_URL=redis://redis:6379/0 - - CELERY_RESULT_BACKEND=redis://redis:6379/0 - depends_on: - - redis - - postgres - - backend - networks: - - crafting-shop-network - restart: unless-stopped - healthcheck: - test: ["CMD", "celery", "-A", "celery_worker", "inspect", "ping", "-d", "celery@$$HOSTNAME"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - celery_beat: - build: - context: ./backend - dockerfile: Dockerfile - container_name: crafting-shop-celery-beat - command: celery -A celery_worker beat --loglevel=info - environment: - - FLASK_ENV=${FLASK_ENV:-prod} - - SECRET_KEY=${SECRET_KEY} - - JWT_SECRET_KEY=${JWT_SECRET_KEY} - - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} - - CELERY_BROKER_URL=redis://redis:6379/0 - - CELERY_RESULT_BACKEND=redis://redis:6379/0 - depends_on: - - redis - - postgres - - backend - networks: - - crafting-shop-network - restart: unless-stopped - volumes: - - celery-beat-data:/app/celerybeat - - flower: - build: - context: ./backend - dockerfile: Dockerfile - container_name: crafting-shop-flower - command: celery -A celery_worker flower --port=5555 - ports: - - "5555:5555" - environment: - - FLASK_ENV=${FLASK_ENV:-prod} - - SECRET_KEY=${SECRET_KEY} - - JWT_SECRET_KEY=${JWT_SECRET_KEY} - - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} - - CELERY_BROKER_URL=redis://redis:6379/0 - - CELERY_RESULT_BACKEND=redis://redis:6379/0 - depends_on: - - redis - - celery_worker - networks: - - crafting-shop-network - restart: unless-stopped - - prometheus: - image: prom/prometheus:latest - container_name: crafting-shop-prometheus - ports: - - "9090:9090" - volumes: - - ./infrastructure/monitoring/prometheus.yml:/etc/prometheus/prometheus.yml - - prometheus-data:/prometheus - command: - - '--config.file=/etc/prometheus/prometheus.yml' - - '--storage.tsdb.path=/prometheus' - networks: - - crafting-shop-network - restart: unless-stopped - - grafana: - image: grafana/grafana:latest - container_name: crafting-shop-grafana - ports: - - "3001:3000" - environment: - - GF_SECURITY_ADMIN_USER=${GRAFANA_USER:-admin} - - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD} - volumes: - - grafana-data:/var/lib/grafana - networks: - - crafting-shop-network - restart: unless-stopped - -volumes: - postgres-data: - redis-data: - prometheus-data: - grafana-data: - backend-data: - celery-beat-data: - minio-data: - -networks: - crafting-shop-network: - driver: bridge \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index fa2d5fb..df21bfb 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -3,6 +3,10 @@ # --------------------------------------------------------- FROM node:18-alpine AS frontend-build +# Build-time configurable: pass --build-arg REGISTRATION_ENABLED=false to disable registration +ARG REGISTRATION_ENABLED=true +ENV VITE_REGISTRATION_ENABLED=$REGISTRATION_ENABLED + WORKDIR /app/frontend # Copy package files first for better caching @@ -20,6 +24,10 @@ FROM python:3.11-slim WORKDIR /app +# Build-time configurable for the backend: pass --build-arg REGISTRATION_ENABLED=false to disable registration +ARG REGISTRATION_ENABLED=true +ENV REGISTRATION_ENABLED=$REGISTRATION_ENABLED + # Install system dependencies (if needed for python packages) # RUN apt-get update && apt-get install -y ... && rm -rf /var/lib/apt/lists/* @@ -47,4 +55,4 @@ EXPOSE 8000 # --error-logfile - sends error logs to stderr # --capture-output ensures all output is captured # --log-level info sets the logging level -CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "--access-logfile", "-", "--error-logfile", "-", "--capture-output", "--log-level", "info", "wsgi:app"] +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--forwarded-allow-ips", "*", "--workers", "2", "--access-logfile", "-", "--access-logformat", "%({x-forwarded-for}i)s - - [%(t)s] \"%(r)s\" %(s)s %(b)s \"%(f)s\" \"%(a)s\"", "--error-logfile", "-", "--capture-output", "--log-level", "info", "wsgi:app"] \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 097ad2f..3b740d8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,6 +11,7 @@ import { Navbar } from './components/Navbar'; import { Home } from './pages/Home'; import Login from './pages/Login'; import { Register } from './pages/Register'; +import { REGISTRATION_ENABLED } from './config/registration'; import Profile from './pages/Profile'; import { ProtectedRoute } from './components/ProtectedRoute'; import { Boards } from './pages/Boards'; @@ -59,7 +60,10 @@ const App = () => { /> } /> } /> - } /> + : } + /> Login - - Register - + {REGISTRATION_ENABLED && ( + + Register + + )} )} @@ -136,13 +139,15 @@ export function Navbar() { > Login - setMobileMenuOpen(false)} - > - Register - + {REGISTRATION_ENABLED && ( + setMobileMenuOpen(false)} + > + Register + + )} )} diff --git a/frontend/src/config/registration.ts b/frontend/src/config/registration.ts new file mode 100644 index 0000000..96a8c0c --- /dev/null +++ b/frontend/src/config/registration.ts @@ -0,0 +1,5 @@ +// Inlined at build time by Vite via import.meta.env. +// Unset (local dev / no build arg) → registration is enabled (backward compatible). +// Set to "false" via docker build --build-arg REGISTRATION_ENABLED=false → registration disabled. +export const REGISTRATION_ENABLED = + import.meta.env.VITE_REGISTRATION_ENABLED !== 'false'; \ No newline at end of file diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index d87dc44..347ca48 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -3,6 +3,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Link, useSearchParams } from 'react-router-dom'; import { useAuth } from '../hooks/useAuth'; +import { REGISTRATION_ENABLED } from '../config/registration'; const loginSchema = z.object({ email: z.string().min(1, 'Email is required').email('Invalid email address'), @@ -81,12 +82,14 @@ export default function Login() { -

- Don‘t have an account? - - Register - -

+ {REGISTRATION_ENABLED && ( +

+ Don‘t have an account? + + Register + +

+ )} ); } diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..151aa68 --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file