""" API routes for QR Code Manager """ import os import io import base64 import uuid from datetime import datetime from flask import Blueprint, request, jsonify, send_file, redirect, Response, current_app from app.utils.auth import login_required from app.utils.qr_generator import QRCodeGenerator from app.utils.link_manager import LinkPageManager from app.utils.data_manager import QRDataManager bp = Blueprint('api', __name__) # Initialize managers qr_generator = QRCodeGenerator() link_manager = LinkPageManager() data_manager = QRDataManager() # Configuration for file uploads - use paths relative to app root UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), '..', 'static', 'qr_codes') LOGOS_FOLDER = os.path.join(os.path.dirname(__file__), '..', 'static', 'logos') UPLOAD_FOLDER = os.path.abspath(UPLOAD_FOLDER) LOGOS_FOLDER = os.path.abspath(LOGOS_FOLDER) os.makedirs(UPLOAD_FOLDER, exist_ok=True) os.makedirs(LOGOS_FOLDER, exist_ok=True) @bp.route('/generate', methods=['POST']) @login_required def generate_qr(): """Generate QR code API endpoint""" try: data = request.json # Extract QR code content qr_type = data.get('type', 'text') content = data.get('content', '') # Process content based on type if qr_type == 'url': qr_content = content if content.startswith(('http://', 'https://')) else f'https://{content}' elif qr_type == 'wifi': wifi_data = data.get('wifi', {}) qr_content = f"WIFI:T:{wifi_data.get('security', 'WPA')};S:{wifi_data.get('ssid', '')};P:{wifi_data.get('password', '')};H:{wifi_data.get('hidden', 'false')};;" elif qr_type == 'email': email_data = data.get('email', {}) qr_content = f"mailto:{email_data.get('email', '')}?subject={email_data.get('subject', '')}&body={email_data.get('body', '')}" elif qr_type == 'phone': qr_content = f"tel:{content}" elif qr_type == 'sms': sms_data = data.get('sms', {}) qr_content = f"smsto:{sms_data.get('phone', '')}:{sms_data.get('message', '')}" elif qr_type == 'vcard': vcard_data = data.get('vcard', {}) qr_content = f"""BEGIN:VCARD VERSION:3.0 FN:{vcard_data.get('name', '')} ORG:{vcard_data.get('organization', '')} TEL:{vcard_data.get('phone', '')} EMAIL:{vcard_data.get('email', '')} URL:{vcard_data.get('website', '')} END:VCARD""" else: # text qr_content = content # Extract styling options settings = { 'size': data.get('size', 10), 'border': data.get('border', 4), 'foreground_color': data.get('foreground_color', '#000000'), 'background_color': data.get('background_color', '#FFFFFF'), 'style': data.get('style', 'square') } # Generate QR code qr_img = qr_generator.generate_qr_code(qr_content, settings) # Add logo if provided logo_path = data.get('logo_path') if logo_path and os.path.exists(logo_path): qr_img = qr_generator.add_logo(qr_img, logo_path) # Convert to base64 img_buffer = io.BytesIO() qr_img.save(img_buffer, format='PNG') img_buffer.seek(0) img_base64 = base64.b64encode(img_buffer.getvalue()).decode() # Save QR code record qr_id = data_manager.save_qr_record(qr_type, qr_content, settings, img_base64) # Save image file img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png') qr_img.save(img_path) return jsonify({ 'success': True, 'qr_id': qr_id, 'image_data': f'data:image/png;base64,{img_base64}', 'download_url': f'/api/download/{qr_id}' }) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @bp.route('/download/') @login_required def download_qr(qr_id): """Download QR code in PNG format""" try: img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png') if os.path.exists(img_path): return send_file(img_path, as_attachment=True, download_name=f'qr_code_{qr_id}.png') else: return jsonify({'error': 'QR code not found'}), 404 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/download//svg') @login_required def download_qr_svg(qr_id): """Download QR code in SVG format""" try: # Get QR code data from database qr_data = data_manager.get_qr_code(qr_id) if not qr_data: return jsonify({'error': 'QR code not found'}), 404 # Regenerate QR code as SVG settings = qr_data.get('settings', {}) content = qr_data.get('content', '') # Generate SVG QR code svg_string = qr_generator.generate_qr_code_svg_string(content, settings) # Create a response with SVG content response = Response(svg_string, mimetype='image/svg+xml') response.headers['Content-Disposition'] = f'attachment; filename=qr_code_{qr_id}.svg' return response except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/qr_codes') @login_required def list_qr_codes(): """List all generated QR codes""" return jsonify(data_manager.list_qr_codes()) @bp.route('/qr_codes/') @login_required def get_qr_code(qr_id): """Get specific QR code details""" qr_data = data_manager.get_qr_record(qr_id) if qr_data: return jsonify(qr_data) else: return jsonify({'error': 'QR code not found'}), 404 @bp.route('/qr_codes/', methods=['DELETE']) @login_required def delete_qr_code(qr_id): """Delete QR code""" try: if data_manager.qr_exists(qr_id): # Remove from database data_manager.delete_qr_record(qr_id) # Remove image file img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png') if os.path.exists(img_path): os.remove(img_path) return jsonify({'success': True}) else: return jsonify({'error': 'QR code not found'}), 404 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/upload_logo', methods=['POST']) @login_required def upload_logo(): """Upload logo for QR code""" try: if 'logo' not in request.files: return jsonify({'error': 'No logo file provided'}), 400 file = request.files['logo'] if file.filename == '': return jsonify({'error': 'No file selected'}), 400 # Save logo logo_id = str(uuid.uuid4()) logo_extension = file.filename.rsplit('.', 1)[1].lower() logo_filename = f'{logo_id}.{logo_extension}' logo_path = os.path.join(LOGOS_FOLDER, logo_filename) file.save(logo_path) return jsonify({ 'success': True, 'logo_path': logo_path, 'logo_id': logo_id }) except Exception as e: return jsonify({'error': str(e)}), 500 # Dynamic Link Pages API Routes @bp.route('/create_link_page', methods=['POST']) @login_required def create_link_page(): """Create a new dynamic link page and QR code""" try: print("DEBUG: Starting create_link_page") data = request.json title = data.get('title', 'My Links') description = data.get('description', 'Collection of useful links') print(f"DEBUG: Creating link page with title='{title}', description='{description}'") # Create the link page page_id = link_manager.create_link_page(title, description) print(f"DEBUG: Created link page with ID: {page_id}") # Create the original page URL original_page_url = f"{request.url_root}links/{page_id}" print(f"DEBUG: Original page URL: {original_page_url}") # Automatically create a short URL for the link page print(f"DEBUG: Creating short URL for: {original_page_url}") short_result = link_manager.create_standalone_short_url( original_page_url, title=f"Link Page: {title}", custom_code=None ) print(f"DEBUG: Short URL result: {short_result}") short_page_url = short_result['short_url'] # Store the short URL info with the page print(f"DEBUG: Setting page short URL: {short_page_url}, code: {short_result['short_code']}") link_manager.set_page_short_url(page_id, short_page_url, short_result['short_code']) print(f"DEBUG: Page short URL set successfully") settings = { 'size': data.get('size', 10), 'border': data.get('border', 4), 'foreground_color': data.get('foreground_color', '#000000'), 'background_color': data.get('background_color', '#FFFFFF'), 'style': data.get('style', 'square') } # Generate QR code pointing to the SHORT URL (not the original long URL) qr_img = qr_generator.generate_qr_code(short_page_url, settings) # Convert to base64 img_buffer = io.BytesIO() qr_img.save(img_buffer, format='PNG') img_buffer.seek(0) img_base64 = base64.b64encode(img_buffer.getvalue()).decode() # Save QR code record with the short URL qr_id = data_manager.save_qr_record('link_page', short_page_url, settings, img_base64, page_id) # Save image file img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png') qr_img.save(img_path) return jsonify({ 'success': True, 'qr_id': qr_id, 'page_id': page_id, 'page_url': short_page_url, # Return the short URL as the main page URL 'original_url': original_page_url, # Keep original for reference 'short_code': short_result['short_code'], 'edit_url': f"{request.url_root}edit/{page_id}", 'image_data': f'data:image/png;base64,{img_base64}', 'download_url': f'/api/download/{qr_id}' }) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @bp.route('/link_pages//links', methods=['POST']) @login_required def add_link_to_page(page_id): """Add a link to a page""" try: print(f"DEBUG: Adding link to page {page_id}") data = request.json print(f"DEBUG: Request data: {data}") title = data.get('title', '') url = data.get('url', '') description = data.get('description', '') enable_shortener = data.get('enable_shortener', False) custom_short_code = data.get('custom_short_code', None) if not title or not url: print("DEBUG: Missing title or URL") return jsonify({'error': 'Title and URL are required'}), 400 print(f"DEBUG: Calling link_manager.add_link with shortener={enable_shortener}") success = link_manager.add_link( page_id, title, url, description, enable_shortener=enable_shortener, custom_short_code=custom_short_code ) if success: print("DEBUG: Link added successfully") return jsonify({'success': True}) else: print(f"DEBUG: Failed to add link - page {page_id} not found") return jsonify({'error': 'Page not found'}), 404 except Exception as e: print(f"DEBUG: Exception in add_link_to_page: {e}") return jsonify({'error': str(e)}), 500 @bp.route('/link_pages//links/', methods=['PUT']) @login_required def update_link_in_page(page_id, link_id): """Update a link in a page""" try: data = request.json title = data.get('title') url = data.get('url') description = data.get('description') enable_shortener = data.get('enable_shortener') custom_short_code = data.get('custom_short_code') success = link_manager.update_link( page_id, link_id, title, url, description, enable_shortener=enable_shortener, custom_short_code=custom_short_code ) if success: return jsonify({'success': True}) else: return jsonify({'error': 'Page or link not found'}), 404 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/link_pages//links/', methods=['DELETE']) @login_required def delete_link_from_page(page_id, link_id): """Delete a link from a page""" try: success = link_manager.delete_link(page_id, link_id) if success: return jsonify({'success': True}) else: return jsonify({'error': 'Page or link not found'}), 404 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/link_pages/') @login_required def get_link_page(page_id): """Get link page data""" page_data = link_manager.get_page(page_id) if page_data: return jsonify(page_data) else: return jsonify({'error': 'Page not found'}), 404 # URL Shortener API Routes @bp.route('/shorten', methods=['POST']) @login_required def create_short_url(): """Create a shortened URL""" try: data = request.json url = data.get('url', '') title = data.get('title', '') custom_code = data.get('custom_code', None) if not url: return jsonify({'error': 'URL is required'}), 400 result = link_manager.create_standalone_short_url(url, title, custom_code) return jsonify({ 'success': True, 'short_url': result['short_url'], 'short_code': result['short_code'], 'original_url': result['original_url'] }) except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/short_urls') @login_required def list_short_urls(): """List all shortened URLs""" try: urls = link_manager.list_all_short_urls() return jsonify({'success': True, 'urls': urls}) except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/short_urls//stats') @login_required def get_short_url_stats(short_code): """Get statistics for a short URL""" try: stats = link_manager.get_short_url_stats(short_code) if stats: return jsonify({'success': True, 'stats': stats}) else: return jsonify({'error': 'Short URL not found'}), 404 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/generate_shortened_qr', methods=['POST']) @login_required def generate_shortened_qr(): """Generate QR code for a shortened URL""" try: data = request.json shortener_data = data.get('shortener', {}) url = shortener_data.get('url', '') title = shortener_data.get('title', '') custom_code = shortener_data.get('custom_code', '').strip() or None if not url: return jsonify({'error': 'URL is required'}), 400 # Create shortened URL result = link_manager.create_standalone_short_url(url, title, custom_code) short_url = result['short_url'] # Generate QR code for the short URL settings = { 'size': data.get('size', 10), 'border': data.get('border', 4), 'foreground_color': data.get('foreground_color', '#000000'), 'background_color': data.get('background_color', '#FFFFFF'), 'style': data.get('style', 'square') } qr_img = qr_generator.generate_qr_code(short_url, settings) # Convert to base64 img_buffer = io.BytesIO() qr_img.save(img_buffer, format='PNG') img_buffer.seek(0) img_base64 = base64.b64encode(img_buffer.getvalue()).decode() # Save QR code record qr_id = data_manager.save_qr_record('url_shortener', short_url, settings, img_base64) # Save image file img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png') qr_img.save(img_path) return jsonify({ 'success': True, 'qr_id': qr_id, 'short_url': short_url, 'short_code': result['short_code'], 'original_url': result['original_url'], 'image_data': f'data:image/png;base64,{img_base64}', 'download_url': f'/api/download/{qr_id}' }) except Exception as e: return jsonify({'error': str(e)}), 500 # Backup Management API Endpoints @bp.route('/backup/create', methods=['POST']) @login_required def create_backup(): """Create backup via API""" try: import subprocess import os from pathlib import Path data = request.json or {} backup_type = data.get('type', 'data') # data, config, full # Use absolute path for Docker container or relative path for development if os.path.exists('/app/backup.py'): # Running in Docker container app_root = '/app' backup_script = '/app/backup.py' else: # Running in development app_root = Path(__file__).parent.parent.parent backup_script = 'backup.py' if backup_type == 'data': # Create data backup using Python script result = subprocess.run( ['python3', backup_script, '--data-only'], cwd=app_root, capture_output=True, text=True ) elif backup_type == 'config': # Create config backup result = subprocess.run( ['python3', backup_script, '--config'], cwd=app_root, capture_output=True, text=True ) elif backup_type == 'full': # Create full backup result = subprocess.run( ['python3', backup_script, '--full'], cwd=app_root, capture_output=True, text=True ) else: return jsonify({'error': 'Invalid backup type'}), 400 if result.returncode == 0: return jsonify({ 'success': True, 'message': 'Backup created successfully', 'output': result.stdout }) else: return jsonify({ 'error': 'Backup creation failed', 'output': result.stderr }), 500 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/backup/list', methods=['GET']) @login_required def list_backups(): """List available backups""" try: import subprocess import os from pathlib import Path # Use absolute path for Docker container or relative path for development if os.path.exists('/app/backup.py'): # Running in Docker container app_root = '/app' backup_script = '/app/backup.py' else: # Running in development app_root = Path(__file__).parent.parent.parent backup_script = 'backup.py' result = subprocess.run( ['python3', backup_script, '--list'], cwd=app_root, capture_output=True, text=True ) if result.returncode == 0: # Parse backup list from output lines = result.stdout.strip().split('\n') backups = [] for line in lines: if '|' in line and not line.startswith('─') and 'Available backups' not in line: parts = [part.strip() for part in line.split('|')] if len(parts) >= 4: backups.append({ 'type': parts[0], 'filename': parts[1], 'size': parts[2], 'date': parts[3] }) return jsonify({ 'success': True, 'backups': backups }) else: return jsonify({ 'error': 'Failed to list backups', 'output': result.stderr }), 500 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/backup/download/', methods=['GET']) @login_required def download_backup(filename): """Download backup file""" try: import os from pathlib import Path # Use absolute path for Docker container or relative path for development if os.path.exists('/app/backup.py'): # Running in Docker container backup_dir = Path('/app/backups') else: # Running in development app_root = Path(__file__).parent.parent.parent backup_dir = app_root / 'backups' backup_file = backup_dir / filename if not backup_file.exists(): return jsonify({'error': 'Backup file not found'}), 404 return send_file( str(backup_file), as_attachment=True, download_name=filename, mimetype='application/gzip' ) except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/backup/status', methods=['GET']) @login_required def backup_status(): """Get backup system status""" try: import os from pathlib import Path # Use absolute path for Docker container or relative path for development if os.path.exists('/app/backup.py'): # Running in Docker container backup_dir = Path('/app/backups') data_dir = Path('/app/data') else: # Running in development app_root = Path(__file__).parent.parent.parent backup_dir = app_root / 'backups' data_dir = app_root / 'data' # Count backups backup_files = list(backup_dir.glob('*.tar.gz')) if backup_dir.exists() else [] # Get data directory size data_size = 0 if data_dir.exists(): for file_path in data_dir.rglob('*'): if file_path.is_file(): data_size += file_path.stat().st_size # Get backup directory size backup_size = 0 for backup_file in backup_files: backup_size += backup_file.stat().st_size # Check last backup time last_backup = None if backup_files: latest_backup = max(backup_files, key=lambda x: x.stat().st_mtime) last_backup = datetime.fromtimestamp(latest_backup.stat().st_mtime).isoformat() return jsonify({ 'success': True, 'status': { 'backup_count': len(backup_files), 'data_size': data_size, 'backup_size': backup_size, 'last_backup': last_backup, 'backup_directory': str(backup_dir), 'data_directory': str(data_dir) } }) except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/backup/delete/', methods=['DELETE']) @login_required def delete_backup(filename): """Delete a backup file""" try: import os from pathlib import Path # Use absolute path for Docker container or relative path for development if os.path.exists('/app/backup.py'): # Running in Docker container backup_dir = Path('/app/backups') else: # Running in development app_root = Path(__file__).parent.parent.parent backup_dir = app_root / 'backups' backup_file = backup_dir / filename # Security check: ensure filename is just a filename, not a path if '/' in filename or '\\' in filename or '..' in filename: return jsonify({'error': 'Invalid filename'}), 400 # Check if file exists if not backup_file.exists(): return jsonify({'error': 'Backup file not found'}), 404 # Delete the file backup_file.unlink() # Also delete associated metadata file if it exists metadata_file = backup_dir / f"{filename.replace('.tar.gz', '.json')}" if metadata_file.exists(): metadata_file.unlink() return jsonify({ 'success': True, 'message': f'Backup {filename} deleted successfully' }) except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/backup/cleanup', methods=['POST']) @login_required def cleanup_old_backups(): """Delete old backup files, keeping only the N most recent""" try: import os from pathlib import Path data = request.json or {} keep_count = data.get('keep', 5) # Default: keep 5 most recent # Use absolute path for Docker container or relative path for development if os.path.exists('/app/backup.py'): # Running in Docker container backup_dir = Path('/app/backups') else: # Running in development app_root = Path(__file__).parent.parent.parent backup_dir = app_root / 'backups' if not backup_dir.exists(): return jsonify({'error': 'Backup directory not found'}), 404 # Get all backup files sorted by modification time (newest first) backup_files = list(backup_dir.glob('*.tar.gz')) backup_files.sort(key=lambda x: x.stat().st_mtime, reverse=True) # Delete old backups deleted_files = [] files_to_delete = backup_files[keep_count:] for backup_file in files_to_delete: # Delete the backup file backup_file.unlink() deleted_files.append(backup_file.name) # Also delete associated metadata file if it exists metadata_file = backup_dir / f"{backup_file.stem}.json" if metadata_file.exists(): metadata_file.unlink() return jsonify({ 'success': True, 'message': f'Cleanup completed. Deleted {len(deleted_files)} old backups.', 'deleted_files': deleted_files, 'kept_count': min(len(backup_files), keep_count) }) except Exception as e: return jsonify({'error': str(e)}), 500