Files
qr-code_manager/app/routes/api.py
ske087 53f5c513d4 🚀 Major improvements: Fix short URLs, separate public/admin views, and enhance UX
 Fixed Critical Issues:
- Fixed dynamic QR code short URL redirect functionality
- Resolved data consistency issues with multiple LinkPageManager instances
- Fixed worker concurrency problems in Gunicorn configuration

🎨 UI/UX Enhancements:
- Separated public page from admin statistics view
- Created clean public_page.html for QR code users (no admin info)
- Added comprehensive statistics_page.html for admin analytics
- Enhanced dashboard with separate 'Manage' and 'Stats' buttons
- Improved navigation flow throughout the application

🔧 Technical Improvements:
- Added URLShortener instance reloading for data consistency
- Reduced Gunicorn workers to 1 to prevent file conflicts
- Increased timeout to 60s for better performance
- Enhanced debug logging for troubleshooting
- Added proper error handling and 404 responses

📁 New Files:
- app/templates/public_page.html - Clean public interface
- app/templates/statistics_page.html - Admin analytics dashboard

�� Modified Files:
- app/routes/main.py - Added /stats route, improved short URL handling
- app/templates/edit_links.html - Added Statistics button
- app/templates/index.html - Added Stats button for QR codes
- app/utils/link_manager.py - Enhanced data reloading
- app/utils/url_shortener.py - Added debug logging
- gunicorn.conf.py - Optimized worker configuration

This update provides a professional separation between public content and admin functionality while ensuring reliable short URL operation.
2025-07-18 09:21:36 -04:00

479 lines
17 KiB
Python
Executable File

"""
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/<qr_id>')
@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/<qr_id>/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/<qr_id>')
@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/<qr_id>', 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/<page_id>/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/<page_id>/links/<link_id>', 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/<page_id>/links/<link_id>', 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/<page_id>')
@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/<short_code>/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