diff --git a/.gitignore b/.gitignore index ea2b7bf..dc127c3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,12 @@ __pycache__/ # Environment variables .env +.env.prod .env.1 .env.2 # OS files -.DS_Store \ No newline at end of file +.DS_Store + +# build files +backend/app/static/ \ No newline at end of file diff --git a/backend/Dockerfile.prod b/backend/Dockerfile.prod new file mode 100644 index 0000000..8082e7e --- /dev/null +++ b/backend/Dockerfile.prod @@ -0,0 +1,17 @@ +# backend/Dockerfile.prod +# Build frontend +FROM node:18 AS frontend-builder +WORKDIR /frontend +COPY frontend/package*.json ./ +RUN npm install +COPY frontend/ ./ +RUN npm run build + +# Backend +FROM python:3.9 +WORKDIR /app +COPY backend/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY backend/ . +COPY --from=frontend-builder /frontend/dist ./app/static +CMD ["python", "-m", "app.main"] \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py index 5bab35f..c0d0c5e 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -23,9 +23,19 @@ def create_app(): logger = setup_logging() logger.info("Creating Flask application") - app = Flask(__name__) + app = Flask(__name__, static_folder='static') CORS(app, resources={r"/*": {"origins": "*"}}) + # Serve static files + @app.route('/', defaults={'path': ''}) + @app.route('/') + def serve_static(path): + if path.startswith('api/'): + return # Let API routes handle these + if path and os.path.exists(os.path.join(app.static_folder, path)): + return send_from_directory(app.static_folder, path) + return send_from_directory(app.static_folder, 'index.html') + # Initialize directories and database logger.info("Ensuring required directories exist") config.ensure_directories() @@ -58,7 +68,7 @@ def create_app(): app.register_blueprint(logs_bp, url_prefix='/api/logs') app.register_blueprint(git_bp, url_prefix='/api/git') app.register_blueprint(data_bp, url_prefix='/api/data') - app.register_blueprint(importarr_bp, url_prefix='/api/importarr') + app.register_blueprint(importarr_bp, url_prefix='/api/import') app.register_blueprint(arr_bp, url_prefix='/api/arr') app.register_blueprint(tasks_bp, url_prefix='/api/tasks') @@ -67,7 +77,7 @@ def create_app(): init_middleware(app) # Add settings route - @app.route('/settings', methods=['GET']) + @app.route('/api/settings', methods=['GET']) def handle_settings(): settings = get_settings() return jsonify(settings), 200 diff --git a/backend/app/middleware.py b/backend/app/middleware.py index bb4e8e3..0b0dd5f 100644 --- a/backend/app/middleware.py +++ b/backend/app/middleware.py @@ -1,7 +1,6 @@ # backend/app/middleware.py -from functools import wraps -from flask import request, session, jsonify, current_app +from flask import request, session, jsonify, send_from_directory from .db import get_db import logging @@ -13,41 +12,49 @@ def init_middleware(app): @app.before_request def authenticate_request(): - # Skip authentication for auth blueprint routes - if request.blueprint == 'auth': - return - # Skip authentication for OPTIONS requests (CORS preflight) if request.method == 'OPTIONS': return - # List of paths that don't require authentication - PUBLIC_PATHS = ['/auth/setup', '/auth/authenticate'] - - if request.path in PUBLIC_PATHS: + # Always allow auth endpoints + if request.path.startswith('/api/auth/'): return - # Check session authentication (for web users) - if session.get('authenticated'): - db = get_db() - user = db.execute('SELECT session_id FROM auth').fetchone() - if user and session.get('session_id') == user['session_id']: - return + # Allow static assets needed for auth pages + if request.path.startswith( + ('/assets/', + '/static/')) or request.path in ['/', '/regex.svg', '/clone.svg']: + return - # Check API key authentication (for API users) - api_key = request.headers.get('X-Api-Key') - if api_key: - db = get_db() - try: - user = db.execute('SELECT 1 FROM auth WHERE api_key = ?', - (api_key, )).fetchone() - if user: + # For API routes, require auth + if request.path.startswith('/api/'): + # Check session authentication (for web users) + if session.get('authenticated'): + db = get_db() + user = db.execute('SELECT session_id FROM auth').fetchone() + if user and session.get('session_id') == user['session_id']: return - logger.warning(f'Invalid API key attempt: {api_key[:10]}...') - except Exception as e: - logger.error(f'Database error during API key check: {str(e)}') - return jsonify({'error': 'Internal server error'}), 500 - # If no valid authentication is found, return 401 - logger.warning(f'Unauthorized access attempt to {request.path}') - return jsonify({'error': 'Unauthorized'}), 401 + # Check API key authentication (for API users) + api_key = request.headers.get('X-Api-Key') + if api_key: + db = get_db() + try: + user = db.execute('SELECT 1 FROM auth WHERE api_key = ?', + (api_key, )).fetchone() + if user: + return + logger.warning( + f'Invalid API key attempt: {api_key[:10]}...') + except Exception as e: + logger.error( + f'Database error during API key check: {str(e)}') + return jsonify({'error': 'Internal server error'}), 500 + + # If no valid authentication is found, return 401 + logger.warning(f'Unauthorized access attempt to {request.path}') + return jsonify({'error': 'Unauthorized'}), 401 + + # For all other routes (frontend routes), serve index.html + # This lets React handle auth and routing + return send_from_directory(app.static_folder, 'index.html') diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..1e5339f --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,21 @@ +# docker-compose.prod.yml +version: '3.8' +services: + backend: + build: + context: . + dockerfile: backend/Dockerfile.prod + ports: + - '5000:5000' + volumes: + - profilarr_data:/config + environment: + - FLASK_ENV=production + - TZ=Australia/Adelaide + env_file: + - .env.prod + restart: always + +volumes: + profilarr_data: + name: profilarr_data