from flask import Flask, render_template, send_from_directory, jsonify, request, send_file, session, redirect, url_for import os from io import BytesIO import base64 import json from werkzeug.security import generate_password_hash, check_password_hash from functools import wraps import secrets # Import image generator try: from image_generator import ProductImageGenerator, image_to_base64 IMAGE_GENERATOR_AVAILABLE = True print("✓ Image generation available") except ImportError as e: IMAGE_GENERATOR_AVAILABLE = False print(f"Warning: Could not import image generator: {e}") print("Image generation endpoints will be disabled.") except Exception as e: IMAGE_GENERATOR_AVAILABLE = False print(f"Error loading image generator: {e}") print("Image generation endpoints will be disabled.") # Get the directory where this script is located basedir = os.path.abspath(os.path.dirname(__file__)) # Initialize Flask with explicit paths app = Flask(__name__, template_folder=os.path.join(basedir, 'templates'), static_folder=os.path.join(basedir, 'static')) # Configure session - generate a random secret key app.secret_key = secrets.token_hex(32) app.config['SESSION_COOKIE_HTTPONLY'] = True app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # Configure static folders app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 # Disable caching during development # Support for subdirectory deployments (e.g., /product-finder/) # Auto-detect production environment based on Python path import sys if 'virtualenv/product-finder' in sys.executable or '/home/bmdwtjuw' in sys.executable: # Running on production server app.config['APPLICATION_ROOT'] = '/product-finder' print("[OK] Production environment detected - using /product-finder") else: # Local development app.config['APPLICATION_ROOT'] = os.environ.get('APPLICATION_ROOT', '/') print(f"[OK] Development environment - using {app.config['APPLICATION_ROOT']}") # ===== SUBDIRECTORY DEPLOYMENT MIDDLEWARE ===== class PrefixMiddleware: """ Middleware to handle subdirectory deployments. This ensures Flask knows about the /product-finder prefix when deployed. """ def __init__(self, app, prefix=''): self.app = app self.prefix = prefix.rstrip('/') # Remove trailing slash if present def __call__(self, environ, start_response): # Only apply prefix if not already in SCRIPT_NAME and prefix is set if self.prefix and self.prefix != '/': # Check if the URL path starts with our prefix path = environ.get('PATH_INFO', '') script_name = environ.get('SCRIPT_NAME', '') # If prefix not already in SCRIPT_NAME, add it if not script_name.startswith(self.prefix): environ['SCRIPT_NAME'] = self.prefix + script_name # Remove prefix from PATH_INFO if it's there if path.startswith(self.prefix): environ['PATH_INFO'] = path[len(self.prefix):] return self.app(environ, start_response) # Apply middleware if APPLICATION_ROOT is set to a subdirectory application_root = app.config.get('APPLICATION_ROOT', '/') if application_root and application_root != '/': app.wsgi_app = PrefixMiddleware(app.wsgi_app, prefix=application_root) print(f"✓ PrefixMiddleware applied for subdirectory: {application_root}") # Make base URL available to all templates @app.context_processor def inject_base_url(): """Inject base URL into all templates for relative paths""" return { 'base_url': request.script_root or '', 'url_for': url_for } # ===== AUTHENTICATION HELPERS ===== def login_required(f): """Decorator to require login for routes""" @wraps(f) def decorated_function(*args, **kwargs): if 'user_id' not in session: return redirect(url_for('login_page')) # Check if user is active users = load_users() if session['user_id'] >= len(users): session.clear() return redirect(url_for('login_page', message='User not found')) user = users[session['user_id']] if not user.get('active', True): session.clear() return redirect(url_for('login_page', message='Account is inactive')) return f(*args, **kwargs) return decorated_function def get_current_user(): """Get the currently logged in user""" if 'user_id' not in session: return None users = load_users() if session['user_id'] >= len(users): return None return users[session['user_id']] def can_user(permission, location=None): """ Check if current user has a specific permission. WordPress-style permission checking. Args: permission (str): Permission name (e.g., 'manage_users', 'create_quotes') location (str, optional): Check permission for specific location. If None, uses current session location. If 'global', checks global permissions only. Returns: bool: True if user has permission, False otherwise Examples: can_user('manage_users') # Check global permission can_user('create_quotes') # Check at current location can_user('view_reports', 'LINDS') # Check at specific location """ user = get_current_user() if not user: return False # Check if user is active if not user.get('active', True): return False # Check global permissions first global_permissions = user.get('permissions', {}) if permission in global_permissions: return global_permissions[permission] is True # If location is 'global', only check global permissions if location == 'global': return False # Determine which location to check check_location = location if location else session.get('currentLocation') if not check_location: return False # Check location-specific permissions location_settings = user.get('locationSettings', {}) if check_location in location_settings: loc_permissions = location_settings[check_location].get('permissions', {}) if permission in loc_permissions: return loc_permissions[permission] is True return False def user_has_any_permission(permissions, location=None): """ Check if user has ANY of the provided permissions. Args: permissions (list): List of permission names location (str, optional): Location to check Returns: bool: True if user has at least one permission """ return any(can_user(perm, location) for perm in permissions) def user_has_all_permissions(permissions, location=None): """ Check if user has ALL of the provided permissions. Args: permissions (list): List of permission names location (str, optional): Location to check Returns: bool: True if user has all permissions """ return all(can_user(perm, location) for perm in permissions) def permission_required(permission, location=None): """ Decorator to require specific permission for a route. Similar to @login_required but checks permissions. Args: permission (str): Required permission name location (str, optional): Location to check permission for Example: @app.route('/admin/users') @login_required @permission_required('manage_users') def manage_users_page(): return render_template('user_manager.html') """ def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): if not can_user(permission, location): return render_template('access_denied.html', required_permission=permission), 403 return f(*args, **kwargs) return decorated_function return decorator def get_user_permissions(location=None): """ Get all permissions for the current user. Args: location (str, optional): Get permissions for specific location. If None, returns global + current location. Returns: dict: Dictionary of permission_name: True/False """ user = get_current_user() if not user: return {} permissions = {} # Get global permissions global_perms = user.get('permissions', {}) permissions.update(global_perms) # Get location-specific permissions if location != 'global': check_location = location if location else session.get('currentLocation') if check_location: location_settings = user.get('locationSettings', {}) if check_location in location_settings: loc_perms = location_settings[check_location].get('permissions', {}) permissions.update(loc_perms) return permissions # ===== USER MANAGEMENT FUNCTIONS (moved here for use in auth) ===== USERS_FILE = os.path.join(basedir, 'data', 'users.json') def load_users(): """Load users from JSON file""" if os.path.exists(USERS_FILE): try: with open(USERS_FILE, 'r') as f: return json.load(f) except: return [] return [] def save_users(users): """Save users to JSON file""" with open(USERS_FILE, 'w') as f: json.dump(users, f, indent=2) # ===== AUTHENTICATION ROUTES ===== @app.route('/login') def login_page(): """Serve the login page""" # If already logged in, redirect to main app if 'user_id' in session: return redirect(url_for('index')) return render_template('login.html') @app.route('/select-location') def select_location_page(): """Serve the location selection page""" if 'user_id' not in session: return redirect(url_for('login_page')) return render_template('select_location.html') @app.route('/api/login', methods=['POST']) def login(): """Authenticate user and create session""" try: data = request.json username = data.get('username') password = data.get('password') if not username or not password: return jsonify({ 'status': 'error', 'message': 'Username and password are required' }), 400 users = load_users() # Find user by username user_index = None user = None for i, u in enumerate(users): if u['username'] == username: user_index = i user = u break if not user: return jsonify({ 'status': 'error', 'message': 'Invalid username or password' }), 401 # Check if user is active if not user.get('active', True): return jsonify({ 'status': 'error', 'message': 'Account is inactive. Please contact an administrator.' }), 403 # Verify password if not check_password_hash(user['password'], password): return jsonify({ 'status': 'error', 'message': 'Invalid username or password' }), 401 # Create session session['user_id'] = user_index session['username'] = user['username'] session['defaultLocation'] = user.get('defaultLocation') # Get accessible locations accessible_locations = [] if 'locationSettings' in user: for loc_code, settings in user['locationSettings'].items(): if settings.get('accessible', False): accessible_locations.append(loc_code) session['accessibleLocations'] = accessible_locations # Check if user needs to select a location requiresLocationSelection = len(accessible_locations) > 1 # If only one location or no accessible locations, auto-select default if not requiresLocationSelection: session['currentLocation'] = user.get('defaultLocation') return jsonify({ 'status': 'success', 'message': 'Login successful', 'requiresLocationSelection': requiresLocationSelection }) except Exception as e: return jsonify({ 'status': 'error', 'message': str(e) }), 500 @app.route('/api/logout', methods=['POST']) def logout(): """Log out user and clear session""" session.clear() return jsonify({ 'status': 'success', 'message': 'Logged out successfully' }) @app.route('/api/session', methods=['GET']) def get_session(): """Get current session information""" if 'user_id' not in session: return jsonify({ 'status': 'error', 'message': 'Not logged in' }), 401 return jsonify({ 'status': 'success', 'username': session.get('username'), 'defaultLocation': session.get('defaultLocation'), 'currentLocation': session.get('currentLocation'), 'accessibleLocations': session.get('accessibleLocations', []) }) @app.route('/api/select-location', methods=['POST']) def select_location(): """Select a location for the current session""" if 'user_id' not in session: return jsonify({ 'status': 'error', 'message': 'Not logged in' }), 401 try: data = request.json location = data.get('location') # Verify user has access to this location accessible = session.get('accessibleLocations', []) if location not in accessible: return jsonify({ 'status': 'error', 'message': 'You do not have access to this location' }), 403 session['currentLocation'] = location return jsonify({ 'status': 'success', 'message': 'Location selected', 'currentLocation': location }) except Exception as e: return jsonify({ 'status': 'error', 'message': str(e) }), 500 # ===== PERMISSION CHECK ENDPOINT ===== @app.route('/api/check-permission', methods=['POST']) @login_required def check_permission(): """Check if current user has specific permission(s)""" try: data = request.json permission = data.get('permission') location = data.get('location') # Optional, defaults to current location if not permission: return jsonify({ 'status': 'error', 'message': 'Permission name required' }), 400 has_permission = can_user(permission, location) return jsonify({ 'status': 'success', 'hasPermission': has_permission, 'permission': permission, 'location': location if location else session.get('currentLocation', 'global') }) except Exception as e: return jsonify({ 'status': 'error', 'message': str(e) }), 500 @app.route('/api/user-permissions', methods=['GET']) @login_required def get_user_permissions_endpoint(): """Get all permissions for current user""" try: location = request.args.get('location') # Optional query parameter permissions = get_user_permissions(location) return jsonify({ 'status': 'success', 'permissions': permissions, 'location': location if location else session.get('currentLocation', 'global') }) except Exception as e: return jsonify({ 'status': 'error', 'message': str(e) }), 500 # ===== MAIN APPLICATION ROUTES ===== @app.route('/') def index(): """Serve the quiz page as the main page or redirect to login""" # Simple test to see if app is running if 'username' not in session: return redirect(url_for('login_page')) # If user hasn't selected a location yet, redirect to selection if not session.get('currentLocation') and len(session.get('accessibleLocations', [])) > 1: return redirect(url_for('select_location_page')) return render_template('index2.html') @app.route('/test') def test_route(): """Simple test route to verify app is running - NO AUTH REQUIRED""" import sys info = { 'status': 'OK', 'message': 'Flask app is running!', 'python': sys.version, 'application_root': app.config.get('APPLICATION_ROOT'), 'middleware': type(app.wsgi_app).__name__, 'routes': len(list(app.url_map.iter_rules())) } return f"""