final app for testing and deployment

This commit is contained in:
2025-07-16 20:45:12 +03:00
parent 729f64f411
commit e9a8f5e622
26 changed files with 1561 additions and 168 deletions

41
.env.production Normal file
View File

@@ -0,0 +1,41 @@
# QR Code Manager - Production Environment Configuration
# Copy this file to .env and customize for your deployment
# Flask Configuration
FLASK_ENV=production
SECRET_KEY=your-super-secret-key-change-this-in-production-please
# Admin Credentials (CHANGE THESE BEFORE DEPLOYMENT!)
ADMIN_USERNAME=admin # set your admin username
ADMIN_PASSWORD=admin-password-here # Set a strong password
# Application Domain (for URL shortener)
APP_DOMAIN=localhost:5000 # Change to your production domain, e.g., qr.[domain].com
# Database (Future use)
# DATABASE_URL=sqlite:///qr_manager.db
# SSL/TLS Configuration (Uncomment for HTTPS)
# SSL_KEYFILE=/path/to/your/private.key
# SSL_CERTFILE=/path/to/your/certificate.crt
# Logging Configuration
LOG_LEVEL=INFO
LOG_FILE=/app/logs/qr_manager.log
# Security Settings
SESSION_COOKIE_SECURE=false
SESSION_COOKIE_HTTPONLY=true
SESSION_COOKIE_SAMESITE=Lax
# Performance Settings
UPLOAD_MAX_SIZE=10485760 # 10MB in bytes
CACHE_TIMEOUT=3600 # 1 hour in seconds
# URL Shortener Settings
SHORT_URL_LENGTH=6
CUSTOM_DOMAIN_ENABLED=true # Enable custom domain for URL shortener and set APP_DOMAIN
# Health Check Settings
HEALTH_CHECK_ENABLED=true
HEALTH_CHECK_INTERVAL=30

19
.env.sample Normal file
View File

@@ -0,0 +1,19 @@
# QR Code Manager Environment Configuration
# Application Domain - Used for URL shortener functionality
# Examples:
# For development: APP_DOMAIN=localhost:5000
# For production: APP_DOMAIN=qr.moto-adv.com
# For production with HTTPS: APP_DOMAIN=https://qr.moto-adv.com
APP_DOMAIN=localhost:5000
# Flask Configuration
FLASK_ENV=development
SECRET_KEY=your-secret-key-change-in-production
# Database Configuration (if using a database in the future)
# DATABASE_URL=sqlite:///qr_manager.db
# Admin Credentials (optional override)
# ADMIN_USERNAME=admin
# ADMIN_PASSWORD_HASH=$2b$12$... # bcrypt hash of password

View File

@@ -23,10 +23,11 @@ RUN pip install --no-cache-dir -r requirements.txt
# Copy application code # Copy application code
COPY main.py . COPY main.py .
COPY gunicorn.conf.py .
COPY app/ ./app/ COPY app/ ./app/
# Create necessary directories # Create necessary directories
RUN mkdir -p app/static/qr_codes app/static/logos flask_session RUN mkdir -p app/static/qr_codes app/static/logos flask_session data
# Set environment variables # Set environment variables
ENV FLASK_APP=main.py ENV FLASK_APP=main.py
@@ -45,5 +46,5 @@ EXPOSE 5000
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:5000/health')" || exit 1 CMD python -c "import requests; requests.get('http://localhost:5000/health')" || exit 1
# Run the application # Run the application with Gunicorn for production
CMD ["python", "main.py"] CMD ["gunicorn", "-c", "gunicorn.conf.py", "main:app"]

262
README.md
View File

@@ -6,9 +6,11 @@ A modern Flask web application for generating and managing QR codes with authent
- **Multiple QR Code Types**: Text, URL, WiFi, Email, SMS, vCard - **Multiple QR Code Types**: Text, URL, WiFi, Email, SMS, vCard
- **Dynamic Link Pages**: Create collections of links accessible via QR codes - **Dynamic Link Pages**: Create collections of links accessible via QR codes
- **URL Shortener**: Generate shortened URLs with custom domains and QR codes
- **Admin Authentication**: Secure login with bcrypt password hashing - **Admin Authentication**: Secure login with bcrypt password hashing
- **Customizable Styling**: Different QR code styles (square, rounded, circle) - **Customizable Styling**: Different QR code styles (square, rounded, circle)
- **Logo Integration**: Add custom logos to QR codes - **Logo Integration**: Add custom logos to QR codes
- **Click Tracking**: Monitor short URL usage and statistics
- **Docker Deployment**: Production-ready containerization - **Docker Deployment**: Production-ready containerization
- **Responsive Design**: Modern web interface that works on all devices - **Responsive Design**: Modern web interface that works on all devices
@@ -43,6 +45,7 @@ qr-code_manager/
│ ├── auth.py # Authentication utilities │ ├── auth.py # Authentication utilities
│ ├── qr_generator.py # QR code generation │ ├── qr_generator.py # QR code generation
│ ├── link_manager.py # Dynamic link management │ ├── link_manager.py # Dynamic link management
│ ├── url_shortener.py # URL shortening utilities
│ └── data_manager.py # Data storage utilities │ └── data_manager.py # Data storage utilities
``` ```
@@ -89,7 +92,180 @@ qr-code_manager/
python main.py python main.py
``` ```
## 🔐 Authentication ## <EFBFBD> Production vs Development Modes
The QR Code Manager supports two distinct runtime modes with different behaviors and optimizations.
### 🛠️ Development Mode (Default)
**When it runs:**
- When `FLASK_ENV` is not set to "production"
- When running `python main.py` locally
- Default mode for local development
**Characteristics:**
- Uses Flask's built-in development server
- Debug mode enabled with auto-reload
- Detailed error messages and stack traces
- Console shows default login credentials
- Not suitable for production use
**How to run:**
```bash
# Method 1: Direct execution
python main.py
# Method 2: With explicit development environment
FLASK_ENV=development python main.py
# Method 3: Using environment file
echo "FLASK_ENV=development" > .env
python main.py
```
**Console output in development:**
```
🛠️ Starting QR Code Manager in DEVELOPMENT mode
🔐 Admin user: admin
🔑 Default password: admin123
🌐 Domain configured: localhost:5000
🔗 URL shortener available at: /s/
* Serving Flask app 'app'
* Debug mode: on
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
```
### 🚀 Production Mode
**When it runs:**
- When `FLASK_ENV=production` is set
- When deployed with Docker (automatic)
- For live/production deployments
**Characteristics:**
- Uses Gunicorn WSGI server (4 workers)
- Debug mode disabled
- Optimized for performance and security
- No default credentials shown
- Production-grade error handling
**How to run:**
#### Option 1: Docker (Recommended)
```bash
# Copy and edit production environment
cp .env.production .env
# Edit .env with your production settings
# Deploy with Docker
docker-compose up -d
```
#### Option 2: Manual Gunicorn
```bash
# Set production environment
export FLASK_ENV=production
export SECRET_KEY=your-super-secret-key
export ADMIN_USERNAME=your-admin-username
export ADMIN_PASSWORD=your-secure-password
export APP_DOMAIN=your-domain.com
# Run with Gunicorn
gunicorn -c gunicorn.conf.py main:app
```
#### Option 3: Environment File + Gunicorn
```bash
# Create production environment file
cp .env.production .env
# Edit .env with your settings:
# FLASK_ENV=production
# SECRET_KEY=your-super-secret-key
# ADMIN_USERNAME=your-admin-username
# ADMIN_PASSWORD=your-secure-password
# APP_DOMAIN=your-domain.com
# Run with Gunicorn
gunicorn -c gunicorn.conf.py main:app
```
**Console output in production:**
```
Admin user initialized: your-admin-username
Default password: your-secure-password
[2025-07-16 17:27:27 +0000] [1] [INFO] Starting gunicorn 21.2.0
[2025-07-16 17:27:27 +0000] [1] [INFO] Listening at: http://0.0.0.0:5000 (1)
[2025-07-16 17:27:27 +0000] [1] [INFO] Using worker: sync
[2025-07-16 17:27:27 +0000] [7] [INFO] Booting worker with pid: 7
[2025-07-16 17:27:27 +0000] [8] [INFO] Booting worker with pid: 8
[2025-07-16 17:27:27 +0000] [9] [INFO] Booting worker with pid: 9
[2025-07-16 17:27:27 +0000] [10] [INFO] Booting worker with pid: 10
```
### 🔧 Environment Configuration
Create a `.env` file in the project root with your configuration:
**For Development:**
```bash
# Development settings
FLASK_ENV=development
SECRET_KEY=dev-secret-key
ADMIN_USERNAME=admin
ADMIN_PASSWORD=admin123
APP_DOMAIN=localhost:5000
```
**For Production:**
```bash
# Production settings (copy from .env.production and customize)
FLASK_ENV=production
SECRET_KEY=your-super-secret-key-change-this
ADMIN_USERNAME=your-admin-username
ADMIN_PASSWORD=your-secure-password
APP_DOMAIN=your-domain.com
# Security settings
SESSION_COOKIE_SECURE=true # Set to true for HTTPS
SESSION_COOKIE_HTTPONLY=true
SESSION_COOKIE_SAMESITE=Lax
# Performance settings
UPLOAD_MAX_SIZE=10485760 # 10MB
CACHE_TIMEOUT=3600 # 1 hour
```
### 🛡️ Security Considerations
**Development Mode:**
- ⚠️ Never use in production
- Default credentials are visible
- Debug information exposed
- Single-threaded server
**Production Mode:**
- ✅ Use Gunicorn WSGI server
- ✅ Change default credentials
- ✅ Use strong SECRET_KEY
- ✅ Enable HTTPS when possible
- ✅ Set secure cookie flags
- ✅ Multiple worker processes
### 📊 Performance Comparison
| Feature | Development | Production |
|---------|-------------|------------|
| Server | Flask dev server | Gunicorn (4 workers) |
| Performance | Basic | Optimized |
| Concurrency | Single-threaded | Multi-worker |
| Auto-reload | Yes | No |
| Debug info | Full | Minimal |
| Error handling | Verbose | Secure |
| Session security | Basic | Enhanced |
## <20>🔐 Authentication
- **Default Credentials**: admin / admin123 - **Default Credentials**: admin / admin123
- **Environment Variables**: - **Environment Variables**:
@@ -114,6 +290,7 @@ The application is fully containerized with Docker:
SECRET_KEY=your-super-secret-key-here SECRET_KEY=your-super-secret-key-here
ADMIN_USERNAME=your-admin-username ADMIN_USERNAME=your-admin-username
ADMIN_PASSWORD=your-secure-password ADMIN_PASSWORD=your-secure-password
APP_DOMAIN=qr.moto-adv.com # Your custom domain for URL shortener
``` ```
2. **Deploy:** 2. **Deploy:**
@@ -121,6 +298,15 @@ The application is fully containerized with Docker:
docker-compose up -d docker-compose up -d
``` ```
### URL Shortener Configuration
The URL shortener feature uses the `APP_DOMAIN` environment variable to generate short URLs:
- **Development**: `APP_DOMAIN=localhost:5000`
- **Production**: `APP_DOMAIN=qr.moto-adv.com` or `APP_DOMAIN=https://qr.moto-adv.com`
Short URLs will be available at: `https://[your-domain]/s/[short-code]`
## 📱 Usage ## 📱 Usage
### Generating QR Codes ### Generating QR Codes
@@ -139,6 +325,18 @@ The application is fully containerized with Docker:
3. **Share the QR code** that points to your link page 3. **Share the QR code** that points to your link page
4. **Update links anytime** without changing the QR code 4. **Update links anytime** without changing the QR code
### URL Shortener
1. **Create shortened URLs** with optional custom codes
2. **Generate QR codes** for shortened URLs
3. **Track clicks** and monitor usage statistics
4. **Redirect seamlessly** from short URLs to original destinations
5. **Integrate with link pages** by enabling shortener for individual links
**Examples:**
- Original: `https://very-long-domain.com/extremely/long/path/to/resource`
- Short: `https://qr.moto-adv.com/s/abc123`
## 🛡️ Security Features ## 🛡️ Security Features
- **Password Hashing**: Uses bcrypt for secure password storage - **Password Hashing**: Uses bcrypt for secure password storage
@@ -168,15 +366,28 @@ The application follows a modular Flask structure:
## 📊 API Endpoints ## 📊 API Endpoints
### QR Code Management
- `POST /api/generate` - Generate QR code - `POST /api/generate` - Generate QR code
- `GET /api/qr_codes` - List all QR codes - `GET /api/qr_codes` - List all QR codes
- `GET /api/qr_codes/{id}` - Get specific QR code - `GET /api/qr_codes/{id}` - Get specific QR code
- `DELETE /api/qr_codes/{id}` - Delete QR code - `DELETE /api/qr_codes/{id}` - Delete QR code
### Dynamic Link Pages
- `POST /api/create_link_page` - Create dynamic link page - `POST /api/create_link_page` - Create dynamic link page
- `POST /api/link_pages/{id}/links` - Add link to page - `POST /api/link_pages/{id}/links` - Add link to page
- `PUT /api/link_pages/{id}/links/{link_id}` - Update link - `PUT /api/link_pages/{id}/links/{link_id}` - Update link
- `DELETE /api/link_pages/{id}/links/{link_id}` - Delete link - `DELETE /api/link_pages/{id}/links/{link_id}` - Delete link
### URL Shortener
- `POST /api/shorten` - Create standalone short URL
- `POST /api/generate_shortened_qr` - Generate QR code with short URL
- `GET /api/short_urls` - List all short URLs
- `GET /api/short_urls/{code}/stats` - Get short URL statistics
- `GET /s/{code}` - Redirect short URL to original
- `POST /api/link_pages/{id}/links` - Add link to page
- `PUT /api/link_pages/{id}/links/{link_id}` - Update link
- `DELETE /api/link_pages/{id}/links/{link_id}` - Delete link
## 🚨 Troubleshooting ## 🚨 Troubleshooting
### Common Issues ### Common Issues
@@ -205,6 +416,55 @@ This project is licensed under the MIT License - see the LICENSE file for detail
4. Add tests if applicable 4. Add tests if applicable
5. Submit a pull request 5. Submit a pull request
## 🧹 Data Cleanup for Deployment
When preparing for a fresh deployment or when you need to clear all data, use the provided cleanup scripts:
### Option 1: Python Script (Recommended)
```bash
python clean_data.py
```
### Option 2: Shell Script (Quick)
```bash
./clean_data.sh
```
### What Gets Cleaned
Both scripts will remove:
- **All QR codes and their data** - Clears the QR codes database and deletes all generated PNG images
- **All dynamic link pages** - Removes all link collections and their settings
- **All short URLs** - Clears the URL shortener database
- **All Flask sessions** - Removes user session files
- **All log files** - Deletes any application log files
- **Python cache files** - Removes `__pycache__` directories and `.pyc` files
### Safety Features
- **Confirmation prompt** - Both scripts require typing 'YES' to confirm the action
- **Directory preservation** - Required directories are recreated after cleanup
- **Error handling** - Scripts handle missing files/directories gracefully
### Post-Cleanup Steps
After running the cleanup script:
1. Start the application: `python main.py`
2. Login with default credentials: `admin` / `admin123`
3. **Important**: Change the default admin password immediately
4. Begin creating your QR codes and link pages
### Use Cases
- **Fresh deployment** - Clean slate for production deployment
- **Development reset** - Clear test data during development
- **Data migration** - Prepare for moving to a new system
- **Security cleanup** - Remove all data when decommissioning
## 📞 Support ## 📞 Support
For support, please open an issue on GitHub or contact the development team. For support, please open an issue on GitHub or contact the development team.

View File

@@ -7,7 +7,7 @@ import io
import base64 import base64
import uuid import uuid
from datetime import datetime from datetime import datetime
from flask import Blueprint, request, jsonify, send_file from flask import Blueprint, request, jsonify, send_file, redirect, Response, current_app
from app.utils.auth import login_required from app.utils.auth import login_required
from app.utils.qr_generator import QRCodeGenerator from app.utils.qr_generator import QRCodeGenerator
from app.utils.link_manager import LinkPageManager from app.utils.link_manager import LinkPageManager
@@ -20,9 +20,11 @@ qr_generator = QRCodeGenerator()
link_manager = LinkPageManager() link_manager = LinkPageManager()
data_manager = QRDataManager() data_manager = QRDataManager()
# Configuration for file uploads # Configuration for file uploads - use paths relative to app root
UPLOAD_FOLDER = 'app/static/qr_codes' UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), '..', 'static', 'qr_codes')
LOGOS_FOLDER = 'app/static/logos' 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(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(LOGOS_FOLDER, exist_ok=True) os.makedirs(LOGOS_FOLDER, exist_ok=True)
@@ -107,7 +109,7 @@ END:VCARD"""
@bp.route('/download/<qr_id>') @bp.route('/download/<qr_id>')
@login_required @login_required
def download_qr(qr_id): def download_qr(qr_id):
"""Download QR code""" """Download QR code in PNG format"""
try: try:
img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png') img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png')
if os.path.exists(img_path): if os.path.exists(img_path):
@@ -117,6 +119,32 @@ def download_qr(qr_id):
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 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') @bp.route('/qr_codes')
@login_required @login_required
def list_qr_codes(): def list_qr_codes():
@@ -195,8 +223,19 @@ def create_link_page():
# Create the link page # Create the link page
page_id = link_manager.create_link_page(title, description) page_id = link_manager.create_link_page(title, description)
# Create QR code pointing to the link page # Create the original page URL
page_url = f"{request.url_root}links/{page_id}" original_page_url = f"{request.url_root}links/{page_id}"
# Automatically create a short URL for the link page
short_result = link_manager.create_standalone_short_url(
original_page_url,
title=f"Link Page: {title}",
custom_code=None
)
short_page_url = short_result['short_url']
# Store the short URL info with the page
link_manager.set_page_short_url(page_id, short_page_url, short_result['short_code'])
settings = { settings = {
'size': data.get('size', 10), 'size': data.get('size', 10),
@@ -206,8 +245,8 @@ def create_link_page():
'style': data.get('style', 'square') 'style': data.get('style', 'square')
} }
# Generate QR code # Generate QR code pointing to the SHORT URL (not the original long URL)
qr_img = qr_generator.generate_qr_code(page_url, settings) qr_img = qr_generator.generate_qr_code(short_page_url, settings)
# Convert to base64 # Convert to base64
img_buffer = io.BytesIO() img_buffer = io.BytesIO()
@@ -215,8 +254,8 @@ def create_link_page():
img_buffer.seek(0) img_buffer.seek(0)
img_base64 = base64.b64encode(img_buffer.getvalue()).decode() img_base64 = base64.b64encode(img_buffer.getvalue()).decode()
# Save QR code record # Save QR code record with the short URL
qr_id = data_manager.save_qr_record('link_page', page_url, settings, img_base64, page_id) qr_id = data_manager.save_qr_record('link_page', short_page_url, settings, img_base64, page_id)
# Save image file # Save image file
img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png') img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png')
@@ -226,7 +265,9 @@ def create_link_page():
'success': True, 'success': True,
'qr_id': qr_id, 'qr_id': qr_id,
'page_id': page_id, 'page_id': page_id,
'page_url': page_url, '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}", 'edit_url': f"{request.url_root}edit/{page_id}",
'image_data': f'data:image/png;base64,{img_base64}', 'image_data': f'data:image/png;base64,{img_base64}',
'download_url': f'/api/download/{qr_id}' 'download_url': f'/api/download/{qr_id}'
@@ -244,11 +285,17 @@ def add_link_to_page(page_id):
title = data.get('title', '') title = data.get('title', '')
url = data.get('url', '') url = data.get('url', '')
description = data.get('description', '') 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: if not title or not url:
return jsonify({'error': 'Title and URL are required'}), 400 return jsonify({'error': 'Title and URL are required'}), 400
success = link_manager.add_link(page_id, title, url, description) success = link_manager.add_link(
page_id, title, url, description,
enable_shortener=enable_shortener,
custom_short_code=custom_short_code
)
if success: if success:
return jsonify({'success': True}) return jsonify({'success': True})
@@ -267,8 +314,14 @@ def update_link_in_page(page_id, link_id):
title = data.get('title') title = data.get('title')
url = data.get('url') url = data.get('url')
description = data.get('description') 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) success = link_manager.update_link(
page_id, link_id, title, url, description,
enable_shortener=enable_shortener,
custom_short_code=custom_short_code
)
if success: if success:
return jsonify({'success': True}) return jsonify({'success': True})
@@ -302,3 +355,108 @@ def get_link_page(page_id):
return jsonify(page_data) return jsonify(page_data)
else: else:
return jsonify({'error': 'Page not found'}), 404 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

View File

@@ -2,7 +2,7 @@
Main routes for QR Code Manager Main routes for QR Code Manager
""" """
from flask import Blueprint, render_template from flask import Blueprint, render_template, redirect, abort
from app.utils.auth import login_required from app.utils.auth import login_required
from app.utils.link_manager import LinkPageManager from app.utils.link_manager import LinkPageManager
@@ -44,3 +44,12 @@ def health_check():
from datetime import datetime from datetime import datetime
from flask import jsonify from flask import jsonify
return jsonify({'status': 'healthy', 'timestamp': datetime.now().isoformat()}) return jsonify({'status': 'healthy', 'timestamp': datetime.now().isoformat()})
@bp.route('/s/<short_code>')
def redirect_short_url(short_code):
"""Redirect short URL to original URL"""
original_url = link_manager.resolve_short_url(short_code)
if original_url:
return redirect(original_url)
else:
abort(404)

View File

@@ -141,12 +141,27 @@
border-radius: 8px; border-radius: 8px;
padding: 20px; padding: 20px;
margin-bottom: 15px; margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 15px;
} }
.link-item.editing { .link-item.editing {
border-color: #667eea; border-color: #667eea;
} }
.link-content {
flex: 1;
}
.link-logo {
width: 32px;
height: 32px;
border-radius: 6px;
flex-shrink: 0;
}
.link-display { .link-display {
display: block; display: block;
} }
@@ -253,6 +268,13 @@
<div class="header"> <div class="header">
<h1>✏️ Edit Links</h1> <h1>✏️ Edit Links</h1>
<p>Manage your link collection: {{ page.title }}</p> <p>Manage your link collection: {{ page.title }}</p>
{% if page.short_url %}
<div style="margin-top: 15px; padding: 12px; background: rgba(255,255,255,0.2); border-radius: 8px; font-size: 0.9em;">
<strong>🔗 Page Short URL:</strong>
<a href="{{ page.short_url }}" target="_blank" style="color: #fff; text-decoration: underline;">{{ page.short_url }}</a>
<button onclick="copyToClipboard('{{ page.short_url }}')" style="margin-left: 10px; padding: 4px 8px; background: rgba(255,255,255,0.3); color: white; border: 1px solid rgba(255,255,255,0.5); border-radius: 3px; cursor: pointer; font-size: 0.8em;">Copy</button>
</div>
{% endif %}
</div> </div>
<div class="alert alert-success" id="success-alert"> <div class="alert alert-success" id="success-alert">
@@ -292,12 +314,21 @@
{% if page.links %} {% if page.links %}
{% for link in page.links %} {% for link in page.links %}
<div class="link-item" data-link-id="{{ link.id }}"> <div class="link-item" data-link-id="{{ link.id }}">
<div class="link-content">
<div class="link-display"> <div class="link-display">
<div class="link-title">{{ link.title }}</div> <div class="link-title">{{ link.title }}</div>
{% if link.description %} {% if link.description %}
<div class="link-description">{{ link.description }}</div> <div class="link-description">{{ link.description }}</div>
{% endif %} {% endif %}
<div class="link-url">{{ link.url }}</div> <div class="link-url" data-url="{{ link.url }}">{{ link.url }}</div>
{% if link.short_url %}
<div class="short-url-display" style="margin-top: 8px; padding: 8px; background: #e3f2fd; border-radius: 5px; border-left: 3px solid #2196f3;">
<small style="color: #1976d2; font-weight: 600;">🔗 Short URL:</small>
<br>
<a href="{{ link.short_url }}" target="_blank" style="color: #1976d2; text-decoration: none; font-family: monospace;">{{ link.short_url }}</a>
<button class="btn-copy" onclick="copyToClipboard('{{ link.short_url }}')" style="margin-left: 10px; padding: 2px 8px; background: #2196f3; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8em;">Copy</button>
</div>
{% endif %}
<div class="link-actions"> <div class="link-actions">
<button class="btn btn-small btn-secondary" onclick="editLink('{{ link.id }}')">Edit</button> <button class="btn btn-small btn-secondary" onclick="editLink('{{ link.id }}')">Edit</button>
<button class="btn btn-small btn-danger" onclick="deleteLink('{{ link.id }}')">Delete</button> <button class="btn btn-small btn-danger" onclick="deleteLink('{{ link.id }}')">Delete</button>
@@ -322,6 +353,7 @@
<button class="btn btn-small btn-secondary" onclick="cancelEdit('{{ link.id }}')">Cancel</button> <button class="btn btn-small btn-secondary" onclick="cancelEdit('{{ link.id }}')">Cancel</button>
</div> </div>
</div> </div>
<img class="link-logo" src="" alt="" style="display: none;" onerror="this.style.display='none'">
</div> </div>
{% endfor %} {% endfor %}
{% else %} {% else %}
@@ -344,6 +376,87 @@
<script> <script>
const pageId = '{{ page.id }}'; const pageId = '{{ page.id }}';
// Social media and website logo detection
function getWebsiteLogo(url) {
try {
const urlObj = new URL(url.startsWith('http') ? url : 'https://' + url);
const domain = urlObj.hostname.toLowerCase().replace('www.', '');
// Logo mapping for popular sites
const logoMap = {
'facebook.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/facebook.svg',
'instagram.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/instagram.svg',
'twitter.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/twitter.svg',
'x.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/x.svg',
'tiktok.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/tiktok.svg',
'youtube.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/youtube.svg',
'linkedin.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/linkedin.svg',
'pinterest.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/pinterest.svg',
'snapchat.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/snapchat.svg',
'whatsapp.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/whatsapp.svg',
'telegram.org': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/telegram.svg',
'discord.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/discord.svg',
'reddit.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/reddit.svg',
'github.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/github.svg',
'gmail.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/gmail.svg',
'google.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/google.svg',
'amazon.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/amazon.svg',
'apple.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/apple.svg',
'microsoft.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/microsoft.svg',
'spotify.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/spotify.svg',
'netflix.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/netflix.svg',
'twitch.tv': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/twitch.svg',
'dropbox.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/dropbox.svg',
'zoom.us': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/zoom.svg'
};
if (logoMap[domain]) {
return logoMap[domain];
}
// Fallback to favicon
return `https://www.google.com/s2/favicons?domain=${domain}&sz=64`;
} catch (e) {
return null;
}
}
// Set logos for existing links
function setLogosForLinks() {
document.querySelectorAll('.link-url[data-url]').forEach(linkElement => {
const url = linkElement.getAttribute('data-url');
const logoImg = linkElement.closest('.link-item').querySelector('.link-logo');
const logoSrc = getWebsiteLogo(url);
if (logoSrc && logoImg) {
logoImg.src = logoSrc;
logoImg.style.display = 'block';
logoImg.alt = new URL(url.startsWith('http') ? url : 'https://' + url).hostname;
}
});
}
// Initialize logos when page loads
document.addEventListener('DOMContentLoaded', setLogosForLinks);
// Copy to clipboard function
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function() {
// Show temporary success message
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = 'Copied!';
btn.style.background = '#4caf50';
setTimeout(() => {
btn.textContent = originalText;
btn.style.background = '#2196f3';
}, 1500);
}).catch(function(err) {
console.error('Could not copy text: ', err);
alert('Failed to copy to clipboard');
});
}
// Add new link // Add new link
document.getElementById('add-link-form').addEventListener('submit', async function(e) { document.getElementById('add-link-form').addEventListener('submit', async function(e) {
e.preventDefault(); e.preventDefault();
@@ -358,7 +471,11 @@
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ title, url, description }) body: JSON.stringify({
title,
url,
description
})
}); });
const result = await response.json(); const result = await response.json();
@@ -399,7 +516,11 @@
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ title, url, description }) body: JSON.stringify({
title,
url,
description
})
}); });
const result = await response.json(); const result = await response.json();

View File

@@ -148,7 +148,8 @@
.link-page-fields, .link-page-fields,
.email-fields, .email-fields,
.sms-fields, .sms-fields,
.vcard-fields { .vcard-fields,
.url-shortener-fields {
display: none; display: none;
} }
@@ -156,7 +157,8 @@
.link-page-fields.active, .link-page-fields.active,
.email-fields.active, .email-fields.active,
.sms-fields.active, .sms-fields.active,
.vcard-fields.active { .vcard-fields.active,
.url-shortener-fields.active {
display: block; display: block;
} }
@@ -212,14 +214,9 @@
} }
.download-section { .download-section {
display: none;
gap: 10px; gap: 10px;
} }
.download-section.active {
display: flex;
}
.btn-secondary { .btn-secondary {
background: #6c757d; background: #6c757d;
flex: 1; flex: 1;
@@ -230,6 +227,15 @@
flex: 1; flex: 1;
} }
.btn-success {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
flex: 1;
}
.btn-success:hover {
background: linear-gradient(135deg, #20c997 0%, #17a2b8 100%);
}
.qr-history { .qr-history {
margin-top: 30px; margin-top: 30px;
padding: 25px; padding: 25px;
@@ -322,6 +328,7 @@
<option value="text">Text</option> <option value="text">Text</option>
<option value="url">URL/Website</option> <option value="url">URL/Website</option>
<option value="link_page">Dynamic Link Page</option> <option value="link_page">Dynamic Link Page</option>
<option value="url_shortener">URL Shortener</option>
<option value="wifi">WiFi</option> <option value="wifi">WiFi</option>
<option value="email">Email</option> <option value="email">Email</option>
<option value="phone">Phone</option> <option value="phone">Phone</option>
@@ -368,7 +375,32 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<p style="background: #e3f2fd; padding: 15px; border-radius: 8px; color: #1565c0; font-size: 0.9em;"> <p style="background: #e3f2fd; padding: 15px; border-radius: 8px; color: #1565c0; font-size: 0.9em;">
<strong>💡 Dynamic Link Page:</strong> This creates a QR code that points to a web page where you can add, edit, and manage links. The QR code stays the same, but you can update the links anytime! <strong>💡 Dynamic Link Page with Short URL:</strong> This creates a QR code with a short URL that points to a web page where you can add, edit, and manage links. The QR code stays the same, but you can update the links anytime!
<br><br><strong>Auto Short URL:</strong> Your link page will automatically get a short URL like <code>qr.moto-adv.com/s/abc123</code> making the QR code simpler and easier to scan!
</p>
</div>
</div>
<!-- URL Shortener fields -->
<div class="url-shortener-fields" id="url-shortener-fields">
<div class="form-group">
<label for="shortener-url">URL to Shorten</label>
<input type="url" id="shortener-url" placeholder="https://example.com/very/long/url">
</div>
<div class="form-group">
<label for="shortener-title">Title (optional)</label>
<input type="text" id="shortener-title" placeholder="My Website">
</div>
<div class="form-group">
<label for="shortener-custom-code">Custom Short Code (optional)</label>
<input type="text" id="shortener-custom-code" placeholder="mylink" maxlength="20">
<small style="color: #666; font-size: 0.8em;">
Leave empty for random code. Only letters and numbers allowed.
</small>
</div>
<div class="form-group">
<p style="background: #e3f2fd; padding: 15px; border-radius: 8px; color: #1565c0; font-size: 0.9em;">
<strong>🔗 URL Shortener:</strong> Creates a short URL that redirects to your original URL. The QR code will contain the short URL. Perfect for long URLs or tracking clicks!
</p> </p>
</div> </div>
</div> </div>
@@ -478,8 +510,9 @@
</div> </div>
<div class="download-section" id="download-section"> <div class="download-section" id="download-section">
<button class="btn btn-primary" onclick="downloadQR()">Download PNG</button> <button class="btn btn-primary" onclick="downloadQR('png')">📥 Download PNG</button>
<button class="btn btn-secondary" onclick="copyToClipboard()">Copy Image</button> <button class="btn btn-success" onclick="downloadQR('svg')">🎨 Download SVG</button>
<button class="btn btn-secondary" onclick="copyToClipboard()">📋 Copy Image</button>
</div> </div>
</div> </div>
</div> </div>
@@ -501,7 +534,7 @@
// Hide all specific fields // Hide all specific fields
document.getElementById('text-field').style.display = 'none'; document.getElementById('text-field').style.display = 'none';
document.querySelectorAll('.wifi-fields, .link-page-fields, .email-fields, .sms-fields, .vcard-fields').forEach(el => { document.querySelectorAll('.wifi-fields, .link-page-fields, .email-fields, .sms-fields, .vcard-fields, .url-shortener-fields').forEach(el => {
el.classList.remove('active'); el.classList.remove('active');
}); });
@@ -570,6 +603,12 @@
email: document.getElementById('vcard-email').value, email: document.getElementById('vcard-email').value,
website: document.getElementById('vcard-website').value website: document.getElementById('vcard-website').value
}; };
} else if (type === 'url_shortener') {
additionalData.shortener = {
url: document.getElementById('shortener-url').value,
title: document.getElementById('shortener-title').value,
custom_code: document.getElementById('shortener-custom-code').value
};
} }
const requestData = { const requestData = {
@@ -583,7 +622,13 @@
}; };
try { try {
const endpoint = type === 'link_page' ? '/api/create_link_page' : '/api/generate'; let endpoint = '/api/generate';
if (type === 'link_page') {
endpoint = '/api/create_link_page';
} else if (type === 'url_shortener') {
endpoint = '/api/generate_shortened_qr';
}
const response = await fetch(endpoint, { const response = await fetch(endpoint, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -606,10 +651,22 @@
if (type === 'link_page') { if (type === 'link_page') {
previewHTML += ` previewHTML += `
<div style="margin-top: 15px; padding: 15px; background: #e3f2fd; border-radius: 8px; text-align: left;"> <div style="margin-top: 15px; padding: 15px; background: #e3f2fd; border-radius: 8px; text-align: left;">
<h4 style="margin-bottom: 10px; color: #1565c0;">🎉 Dynamic Link Page Created!</h4> <h4 style="margin-bottom: 10px; color: #1565c0;">🎉 Dynamic Link Page Created with Short URL!</h4>
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>Public URL:</strong> <a href="${result.page_url}" target="_blank">${result.page_url}</a></p> <p style="margin-bottom: 10px; font-size: 0.9em;"><strong>🔗 Short URL:</strong> <a href="${result.page_url}" target="_blank">${result.page_url}</a>
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>Edit URL:</strong> <a href="${result.edit_url}" target="_blank">${result.edit_url}</a></p> <button onclick="copyToClipboard('${result.page_url}')" style="margin-left: 10px; padding: 4px 8px; background: #1565c0; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8em;">Copy</button></p>
<p style="font-size: 0.9em; color: #666;">Share the QR code - visitors will see your link collection. Use the edit URL to manage your links!</p> <p style="margin-bottom: 10px; font-size: 0.9em;"><strong>📝 Edit URL:</strong> <a href="${result.edit_url}" target="_blank">${result.edit_url}</a></p>
${result.original_url ? `<p style="margin-bottom: 10px; font-size: 0.8em; color: #666;"><strong>Original URL:</strong> ${result.original_url}</p>` : ''}
<p style="font-size: 0.9em; color: #666;">✨ QR code contains the short URL for easier scanning! Share it - visitors will see your link collection. Use the edit URL to manage your links!</p>
</div>
`;
} else if (type === 'url_shortener') {
previewHTML += `
<div style="margin-top: 15px; padding: 15px; background: #e8f5e8; border-radius: 8px; text-align: left;">
<h4 style="margin-bottom: 10px; color: #2e7d32;">🔗 Short URL Created!</h4>
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>Short URL:</strong> <a href="${result.short_url}" target="_blank">${result.short_url}</a></p>
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>Original URL:</strong> <a href="${result.original_url}" target="_blank">${result.original_url}</a></p>
<p style="font-size: 0.9em; color: #666;">The QR code contains your short URL. When scanned, it will redirect to your original URL!</p>
<button onclick="copyToClipboard('${result.short_url}')" style="margin-top: 10px; padding: 8px 15px; background: #2e7d32; color: white; border: none; border-radius: 5px; cursor: pointer;">Copy Short URL</button>
</div> </div>
`; `;
} }
@@ -629,11 +686,15 @@
} }
} }
async function downloadQR() { async function downloadQR(format = 'png') {
if (currentQRId) { if (currentQRId) {
if (format === 'svg') {
window.open(`/api/download/${currentQRId}/svg`, '_blank');
} else {
window.open(`/api/download/${currentQRId}`, '_blank'); window.open(`/api/download/${currentQRId}`, '_blank');
} }
} }
}
async function copyToClipboard() { async function copyToClipboard() {
if (currentQRData) { if (currentQRData) {
@@ -650,6 +711,23 @@
} }
} }
// Copy to clipboard function
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function() {
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = 'Copied!';
btn.style.background = '#4caf50';
setTimeout(() => {
btn.textContent = originalText;
btn.style.background = '#2e7d32';
}, 1500);
}).catch(function(err) {
console.error('Could not copy text: ', err);
alert('Failed to copy to clipboard');
});
}
async function loadQRHistory() { async function loadQRHistory() {
try { try {
const response = await fetch('/api/qr_codes'); const response = await fetch('/api/qr_codes');
@@ -670,9 +748,10 @@
<p>Created: ${new Date(qr.created_at).toLocaleDateString()}</p> <p>Created: ${new Date(qr.created_at).toLocaleDateString()}</p>
</div> </div>
<div class="qr-item-actions"> <div class="qr-item-actions">
<button class="btn btn-small btn-primary" onclick="downloadQRById('${qr.id}')">Download</button> <button class="btn btn-small btn-primary" onclick="downloadQRById('${qr.id}', 'png')" title="Download PNG">📥 PNG</button>
${qr.type === 'link_page' ? `<button class="btn btn-small" onclick="openLinkPage('${qr.id}')" style="background: #28a745;">Manage</button>` : ''} <button class="btn btn-small btn-success" onclick="downloadQRById('${qr.id}', 'svg')" title="Download SVG">🎨 SVG</button>
<button class="btn btn-small btn-secondary" onclick="deleteQR('${qr.id}')">Delete</button> ${qr.type === 'link_page' ? `<button class="btn btn-small" onclick="openLinkPage('${qr.id}')" style="background: #28a745;" title="Manage Links">📝 Manage</button>` : ''}
<button class="btn btn-small btn-secondary" onclick="deleteQR('${qr.id}')" title="Delete QR Code">🗑️</button>
</div> </div>
</div> </div>
`).join(''); `).join('');
@@ -681,9 +760,13 @@
} }
} }
async function downloadQRById(qrId) { async function downloadQRById(qrId, format = 'png') {
if (format === 'svg') {
window.open(`/api/download/${qrId}/svg`, '_blank');
} else {
window.open(`/api/download/${qrId}`, '_blank'); window.open(`/api/download/${qrId}`, '_blank');
} }
}
async function deleteQR(qrId) { async function deleteQR(qrId) {
if (confirm('Are you sure you want to delete this QR code?')) { if (confirm('Are you sure you want to delete this QR code?')) {

View File

@@ -90,7 +90,10 @@
transition: all 0.3s ease; transition: all 0.3s ease;
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
display: block; display: flex;
justify-content: space-between;
align-items: center;
gap: 15px;
color: inherit; color: inherit;
} }
@@ -100,14 +103,22 @@
border-color: #667eea; border-color: #667eea;
} }
.link-content {
flex: 1;
}
.link-title { .link-title {
font-size: 1.3em; font-size: 1.3em;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
margin-bottom: 8px; margin-bottom: 8px;
display: flex; }
align-items: center;
gap: 10px; .link-logo {
width: 36px;
height: 36px;
border-radius: 8px;
flex-shrink: 0;
} }
.link-icon { .link-icon {
@@ -153,31 +164,6 @@
color: #333; color: #333;
} }
.footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
border-top: 1px solid #e9ecef;
color: #666;
font-size: 0.9em;
}
.footer a {
color: #667eea;
text-decoration: none;
font-weight: 500;
}
.footer a:hover {
text-decoration: underline;
}
.last-updated {
margin-top: 10px;
font-size: 0.8em;
opacity: 0.7;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.header { .header {
padding: 30px 20px; padding: 30px 20px;
@@ -232,15 +218,19 @@
{% if page.links %} {% if page.links %}
<h2>📚 Available Links</h2> <h2>📚 Available Links</h2>
{% for link in page.links %} {% for link in page.links %}
<a href="{{ link.url }}" target="_blank" class="link-item"> <a href="{{ link.short_url if link.short_url else link.url }}" target="_blank" class="link-item" data-url="{{ link.url }}">
<div class="link-content">
<div class="link-title"> <div class="link-title">
<div class="link-icon">🔗</div>
{{ link.title }} {{ link.title }}
{% if link.short_url %}<span style="background: #2196f3; color: white; font-size: 0.7em; padding: 2px 6px; border-radius: 10px; margin-left: 8px;">SHORT</span>{% endif %}
</div> </div>
{% if link.description %} {% if link.description %}
<div class="link-description">{{ link.description }}</div> <div class="link-description">{{ link.description }}</div>
{% endif %} {% endif %}
<div class="link-url">{{ link.url }}</div> <div class="link-url">{{ link.short_url if link.short_url else link.url }}</div>
</div>
<img class="link-logo" src="" alt="" style="display: none;" onerror="this.style.display='none'">
<div class="link-icon" style="display: block;">🔗</div>
</a> </a>
{% endfor %} {% endfor %}
{% else %} {% else %}
@@ -252,18 +242,81 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="footer">
<p>Powered by <a href="/">QR Code Manager</a></p>
{% if page.updated_at %}
<div class="last-updated">
Last updated: {{ page.updated_at[:10] }} at {{ page.updated_at[11:19] }}
</div>
{% endif %}
</div>
</div> </div>
<script> <script>
// Social media and website logo detection
function getWebsiteLogo(url) {
try {
const urlObj = new URL(url.startsWith('http') ? url : 'https://' + url);
const domain = urlObj.hostname.toLowerCase().replace('www.', '');
// Logo mapping for popular sites
const logoMap = {
'facebook.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/facebook.svg',
'instagram.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/instagram.svg',
'twitter.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/twitter.svg',
'x.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/x.svg',
'tiktok.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/tiktok.svg',
'youtube.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/youtube.svg',
'linkedin.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/linkedin.svg',
'pinterest.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/pinterest.svg',
'snapchat.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/snapchat.svg',
'whatsapp.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/whatsapp.svg',
'telegram.org': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/telegram.svg',
'discord.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/discord.svg',
'reddit.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/reddit.svg',
'github.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/github.svg',
'gmail.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/gmail.svg',
'google.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/google.svg',
'amazon.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/amazon.svg',
'apple.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/apple.svg',
'microsoft.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/microsoft.svg',
'spotify.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/spotify.svg',
'netflix.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/netflix.svg',
'twitch.tv': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/twitch.svg',
'dropbox.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/dropbox.svg',
'zoom.us': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/zoom.svg'
};
if (logoMap[domain]) {
return logoMap[domain];
}
// Fallback to favicon
return `https://www.google.com/s2/favicons?domain=${domain}&sz=64`;
} catch (e) {
return null;
}
}
// Set logos for links
function setLogosForLinks() {
document.querySelectorAll('.link-item[data-url]').forEach(linkElement => {
const url = linkElement.getAttribute('data-url');
const logoImg = linkElement.querySelector('.link-logo');
const fallbackIcon = linkElement.querySelector('.link-icon');
const logoSrc = getWebsiteLogo(url);
if (logoSrc && logoImg) {
logoImg.src = logoSrc;
logoImg.style.display = 'block';
logoImg.alt = new URL(url.startsWith('http') ? url : 'https://' + url).hostname;
// Hide the fallback icon when logo is shown
logoImg.onload = function() {
if (fallbackIcon) fallbackIcon.style.display = 'none';
};
logoImg.onerror = function() {
this.style.display = 'none';
if (fallbackIcon) fallbackIcon.style.display = 'flex';
};
}
});
}
// Initialize logos when page loads
document.addEventListener('DOMContentLoaded', setLogosForLinks);
// Add click tracking (optional) // Add click tracking (optional)
document.querySelectorAll('.link-item').forEach(link => { document.querySelectorAll('.link-item').forEach(link => {
link.addEventListener('click', function() { link.addEventListener('click', function() {

View File

@@ -134,22 +134,6 @@
font-size: 0.9em; font-size: 0.9em;
} }
.default-credentials {
background: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
text-align: center;
font-size: 0.9em;
}
.default-credentials strong {
display: block;
margin-bottom: 5px;
}
@media (max-width: 480px) { @media (max-width: 480px) {
.login-container { .login-container {
margin: 10px; margin: 10px;
@@ -199,12 +183,6 @@
{% endif %} {% endif %}
{% endwith %} {% endwith %}
<div class="default-credentials">
<strong>Default Login Credentials:</strong>
Username: admin<br>
Password: admin123
</div>
<form method="POST"> <form method="POST">
<div class="form-group"> <div class="form-group">
<label for="username">Username</label> <label for="username">Username</label>
@@ -222,9 +200,6 @@
<div class="login-footer"> <div class="login-footer">
<p>🔒 Secure QR Code Management System</p> <p>🔒 Secure QR Code Management System</p>
<p style="margin-top: 5px; font-size: 0.8em; opacity: 0.7;">
Change default credentials in production
</p>
</div> </div>
</div> </div>

View File

@@ -4,8 +4,9 @@ Utility modules for QR Code Manager
from .auth import init_admin, login_required, verify_password, get_admin_credentials from .auth import init_admin, login_required, verify_password, get_admin_credentials
from .qr_generator import QRCodeGenerator from .qr_generator import QRCodeGenerator
from .link_manager import LinkPageManager, link_pages_db from .link_manager import LinkPageManager
from .data_manager import QRDataManager, qr_codes_db from .data_manager import QRDataManager
from .url_shortener import URLShortener
__all__ = [ __all__ = [
'init_admin', 'init_admin',
@@ -14,7 +15,6 @@ __all__ = [
'get_admin_credentials', 'get_admin_credentials',
'QRCodeGenerator', 'QRCodeGenerator',
'LinkPageManager', 'LinkPageManager',
'link_pages_db',
'QRDataManager', 'QRDataManager',
'qr_codes_db' 'URLShortener'
] ]

View File

@@ -3,14 +3,39 @@ Data storage utilities for QR codes
""" """
import uuid import uuid
import json
import os
from datetime import datetime from datetime import datetime
# In-memory storage for QR codes (in production, use a database) # Data storage directory
qr_codes_db = {} DATA_DIR = 'data'
QR_CODES_FILE = os.path.join(DATA_DIR, 'qr_codes.json')
# Ensure data directory exists
os.makedirs(DATA_DIR, exist_ok=True)
class QRDataManager: class QRDataManager:
def __init__(self): def __init__(self):
pass self.qr_codes_db = self._load_qr_codes()
def _load_qr_codes(self):
"""Load QR codes from JSON file"""
try:
if os.path.exists(QR_CODES_FILE):
with open(QR_CODES_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return {}
except Exception as e:
print(f"Error loading QR codes: {e}")
return {}
def _save_qr_codes(self):
"""Save QR codes to JSON file"""
try:
with open(QR_CODES_FILE, 'w', encoding='utf-8') as f:
json.dump(self.qr_codes_db, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error saving QR codes: {e}")
def save_qr_record(self, qr_type, content, settings, image_data, page_id=None): def save_qr_record(self, qr_type, content, settings, image_data, page_id=None):
"""Save QR code record to database""" """Save QR code record to database"""
@@ -27,32 +52,45 @@ class QRDataManager:
if page_id: if page_id:
qr_record['page_id'] = page_id qr_record['page_id'] = page_id
qr_codes_db[qr_id] = qr_record self.qr_codes_db[qr_id] = qr_record
self._save_qr_codes() # Persist to file
return qr_id return qr_id
def get_qr_record(self, qr_id): def get_qr_record(self, qr_id):
"""Get QR code record""" """Get QR code record"""
return qr_codes_db.get(qr_id) # Reload data from file to ensure we have the latest data
self.qr_codes_db = self._load_qr_codes()
return self.qr_codes_db.get(qr_id)
def get_qr_code(self, qr_id):
"""Get QR code record (alias for compatibility)"""
return self.get_qr_record(qr_id)
def delete_qr_record(self, qr_id): def delete_qr_record(self, qr_id):
"""Delete QR code record""" """Delete QR code record"""
if qr_id in qr_codes_db: if qr_id in self.qr_codes_db:
del qr_codes_db[qr_id] del self.qr_codes_db[qr_id]
self._save_qr_codes() # Persist to file
return True return True
return False return False
def list_qr_codes(self): def list_qr_codes(self):
"""List all QR codes""" """List all QR codes"""
# Reload data from file to ensure we have the latest data
self.qr_codes_db = self._load_qr_codes()
qr_list = [] qr_list = []
for qr_id, qr_data in qr_codes_db.items(): for qr_id, qr_data in self.qr_codes_db.items():
qr_list.append({ qr_list.append({
'id': qr_id, 'id': qr_id,
'type': qr_data['type'], 'type': qr_data['type'],
'created_at': qr_data['created_at'], 'created_at': qr_data['created_at'],
'preview': f'data:image/png;base64,{qr_data["image_data"]}' 'preview': f'data:image/png;base64,{qr_data["image_data"]}',
'page_id': qr_data.get('page_id') # Include page_id if it exists
}) })
return qr_list return qr_list
def qr_exists(self, qr_id): def qr_exists(self, qr_id):
"""Check if QR code exists""" """Check if QR code exists"""
return qr_id in qr_codes_db # Reload data from file to ensure we have the latest data
self.qr_codes_db = self._load_qr_codes()
return qr_id in self.qr_codes_db

View File

@@ -3,14 +3,41 @@ Dynamic Link Page Manager utilities
""" """
import uuid import uuid
import json
import os
from datetime import datetime from datetime import datetime
from .url_shortener import URLShortener
# In-memory storage for dynamic link pages (in production, use a database) # Data storage directory
link_pages_db = {} DATA_DIR = 'data'
LINK_PAGES_FILE = os.path.join(DATA_DIR, 'link_pages.json')
# Ensure data directory exists
os.makedirs(DATA_DIR, exist_ok=True)
class LinkPageManager: class LinkPageManager:
def __init__(self): def __init__(self):
pass self.url_shortener = URLShortener()
self.link_pages_db = self._load_link_pages()
def _load_link_pages(self):
"""Load link pages from JSON file"""
try:
if os.path.exists(LINK_PAGES_FILE):
with open(LINK_PAGES_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return {}
except Exception as e:
print(f"Error loading link pages: {e}")
return {}
def _save_link_pages(self):
"""Save link pages to JSON file"""
try:
with open(LINK_PAGES_FILE, 'w', encoding='utf-8') as f:
json.dump(self.link_pages_db, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error saving link pages: {e}")
def create_link_page(self, title="My Links", description="Collection of useful links"): def create_link_page(self, title="My Links", description="Collection of useful links"):
"""Create a new dynamic link page""" """Create a new dynamic link page"""
@@ -22,34 +49,70 @@ class LinkPageManager:
'links': [], 'links': [],
'created_at': datetime.now().isoformat(), 'created_at': datetime.now().isoformat(),
'updated_at': datetime.now().isoformat(), 'updated_at': datetime.now().isoformat(),
'view_count': 0 'view_count': 0,
'short_url': None, # Will be set when short URL is created
'short_code': None
} }
link_pages_db[page_id] = page_data self.link_pages_db[page_id] = page_data
self._save_link_pages() # Persist to file
return page_id return page_id
def add_link(self, page_id, title, url, description=""): def set_page_short_url(self, page_id, short_url, short_code):
"""Add a link to a page""" """Set the short URL for a link page"""
if page_id not in link_pages_db: if page_id in self.link_pages_db:
self.link_pages_db[page_id]['short_url'] = short_url
self.link_pages_db[page_id]['short_code'] = short_code
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
self._save_link_pages() # Persist to file
return True
return False return False
def add_link(self, page_id, title, url, description="", enable_shortener=False, custom_short_code=None):
"""Add a link to a page with optional URL shortening"""
if page_id not in self.link_pages_db:
return False
# Ensure URL has protocol
if not url.startswith(('http://', 'https://')):
url = f'https://{url}'
# Create the link data
link_data = { link_data = {
'id': str(uuid.uuid4()), 'id': str(uuid.uuid4()),
'title': title, 'title': title,
'url': url if url.startswith(('http://', 'https://')) else f'https://{url}', 'url': url,
'description': description, 'description': description,
'created_at': datetime.now().isoformat() 'created_at': datetime.now().isoformat(),
'short_url': None,
'short_code': None,
'clicks': 0
} }
link_pages_db[page_id]['links'].append(link_data) # Generate short URL if enabled
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat() if enable_shortener:
try:
short_result = self.url_shortener.create_short_url(
url,
custom_code=custom_short_code,
title=title
)
link_data['short_url'] = short_result['short_url']
link_data['short_code'] = short_result['short_code']
except Exception as e:
# If shortening fails, continue without it
print(f"URL shortening failed: {e}")
self.link_pages_db[page_id]['links'].append(link_data)
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
self._save_link_pages() # Persist to file
return True return True
def update_link(self, page_id, link_id, title=None, url=None, description=None): def update_link(self, page_id, link_id, title=None, url=None, description=None, enable_shortener=None, custom_short_code=None):
"""Update a specific link""" """Update a specific link with optional URL shortening"""
if page_id not in link_pages_db: if page_id not in self.link_pages_db:
return False return False
for link in link_pages_db[page_id]['links']: for link in self.link_pages_db[page_id]['links']:
if link['id'] == link_id: if link['id'] == link_id:
if title is not None: if title is not None:
link['title'] = title link['title'] = title
@@ -58,29 +121,79 @@ class LinkPageManager:
if description is not None: if description is not None:
link['description'] = description link['description'] = description
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat() # Handle URL shortening update
if enable_shortener is not None:
if enable_shortener and not link.get('short_url'):
# Create new short URL
try:
short_result = self.url_shortener.create_short_url(
link['url'],
custom_code=custom_short_code,
title=link['title']
)
link['short_url'] = short_result['short_url']
link['short_code'] = short_result['short_code']
except Exception as e:
print(f"URL shortening failed: {e}")
elif not enable_shortener and link.get('short_code'):
# Remove short URL
if link.get('short_code'):
self.url_shortener.delete_url(link['short_code'])
link['short_url'] = None
link['short_code'] = None
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
self._save_link_pages() # Persist to file
return True return True
return False return False
def delete_link(self, page_id, link_id): def delete_link(self, page_id, link_id):
"""Delete a specific link""" """Delete a specific link"""
if page_id not in link_pages_db: if page_id not in self.link_pages_db:
return False return False
links = link_pages_db[page_id]['links'] links = self.link_pages_db[page_id]['links']
link_pages_db[page_id]['links'] = [link for link in links if link['id'] != link_id] for link in links:
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat() if link['id'] == link_id and link.get('short_code'):
# Delete the short URL if it exists
self.url_shortener.delete_url(link['short_code'])
self.link_pages_db[page_id]['links'] = [link for link in links if link['id'] != link_id]
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
self._save_link_pages() # Persist to file
return True return True
def increment_view_count(self, page_id): def increment_view_count(self, page_id):
"""Increment view count for a page""" """Increment view count for a page"""
if page_id in link_pages_db: if page_id in self.link_pages_db:
link_pages_db[page_id]['view_count'] += 1 self.link_pages_db[page_id]['view_count'] += 1
self._save_link_pages() # Persist to file
def get_page(self, page_id): def get_page(self, page_id):
"""Get page data""" """Get page data"""
return link_pages_db.get(page_id) # Reload data from file to ensure we have the latest data
self.link_pages_db = self._load_link_pages()
return self.link_pages_db.get(page_id)
def page_exists(self, page_id): def page_exists(self, page_id):
"""Check if page exists""" """Check if page exists"""
return page_id in link_pages_db # Reload data from file to ensure we have the latest data
self.link_pages_db = self._load_link_pages()
return page_id in self.link_pages_db
# URL Shortener management methods
def create_standalone_short_url(self, url, title="", custom_code=None):
"""Create a standalone short URL (not tied to a link page)"""
return self.url_shortener.create_short_url(url, custom_code, title)
def get_short_url_stats(self, short_code):
"""Get statistics for a short URL"""
return self.url_shortener.get_url_stats(short_code)
def list_all_short_urls(self):
"""List all short URLs in the system"""
return self.url_shortener.list_urls()
def resolve_short_url(self, short_code):
"""Resolve a short URL to its original URL"""
return self.url_shortener.get_original_url(short_code)

View File

@@ -6,7 +6,9 @@ import os
import qrcode import qrcode
from qrcode.image.styledpil import StyledPilImage from qrcode.image.styledpil import StyledPilImage
from qrcode.image.styles.moduledrawers import RoundedModuleDrawer, CircleModuleDrawer, SquareModuleDrawer from qrcode.image.styles.moduledrawers import RoundedModuleDrawer, CircleModuleDrawer, SquareModuleDrawer
from qrcode.image.svg import SvgPathImage, SvgFragmentImage, SvgFillImage
from PIL import Image from PIL import Image
import io
class QRCodeGenerator: class QRCodeGenerator:
def __init__(self): def __init__(self):
@@ -19,8 +21,8 @@ class QRCodeGenerator:
'style': 'square' 'style': 'square'
} }
def generate_qr_code(self, data, settings=None): def generate_qr_code(self, data, settings=None, format='PNG'):
"""Generate QR code with custom settings""" """Generate QR code with custom settings in PNG or SVG format"""
if settings is None: if settings is None:
settings = self.default_settings.copy() settings = self.default_settings.copy()
else: else:
@@ -28,6 +30,13 @@ class QRCodeGenerator:
merged_settings.update(settings) merged_settings.update(settings)
settings = merged_settings settings = merged_settings
if format.upper() == 'SVG':
return self._generate_svg_qr_code(data, settings)
else:
return self._generate_png_qr_code(data, settings)
def _generate_png_qr_code(self, data, settings):
"""Generate PNG QR code (existing functionality)"""
# Create QR code instance # Create QR code instance
qr = qrcode.QRCode( qr = qrcode.QRCode(
version=1, version=1,
@@ -64,6 +73,47 @@ class QRCodeGenerator:
return img return img
def _generate_svg_qr_code(self, data, settings):
"""Generate SVG QR code"""
# Create QR code instance
qr = qrcode.QRCode(
version=1,
error_correction=settings['error_correction'],
box_size=settings['size'],
border=settings['border'],
)
qr.add_data(data)
qr.make(fit=True)
# Choose SVG image factory based on style
if settings['style'] == 'circle':
# Use SvgFillImage for better circle support
factory = SvgFillImage
else:
# Use SvgPathImage for square and rounded styles
factory = SvgPathImage
# Generate SVG image
img = qr.make_image(
image_factory=factory,
fill_color=settings['foreground_color'],
back_color=settings['background_color']
)
return img
def generate_qr_code_svg_string(self, data, settings=None):
"""Generate QR code as SVG string"""
svg_img = self.generate_qr_code(data, settings, format='SVG')
# Convert SVG image to string
svg_buffer = io.BytesIO()
svg_img.save(svg_buffer)
svg_buffer.seek(0)
return svg_buffer.getvalue().decode('utf-8')
def add_logo(self, qr_img, logo_path, logo_size_ratio=0.2): def add_logo(self, qr_img, logo_path, logo_size_ratio=0.2):
"""Add logo to QR code""" """Add logo to QR code"""
try: try:

122
app/utils/url_shortener.py Normal file
View File

@@ -0,0 +1,122 @@
"""
URL Shortener utilities for QR Code Manager
"""
import os
import uuid
import string
import random
import json
from datetime import datetime
# Data storage directory
DATA_DIR = 'data'
SHORT_URLS_FILE = os.path.join(DATA_DIR, 'short_urls.json')
# Ensure data directory exists
os.makedirs(DATA_DIR, exist_ok=True)
class URLShortener:
def __init__(self):
self.base_domain = os.environ.get('APP_DOMAIN', 'localhost:5000')
# Ensure we have the protocol
if not self.base_domain.startswith(('http://', 'https://')):
# Use HTTPS for production domains, HTTP for localhost
protocol = 'https://' if 'localhost' not in self.base_domain else 'http://'
self.base_domain = f"{protocol}{self.base_domain}"
self.short_urls_db = self._load_short_urls()
def _load_short_urls(self):
"""Load short URLs from JSON file"""
try:
if os.path.exists(SHORT_URLS_FILE):
with open(SHORT_URLS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return {}
except Exception as e:
print(f"Error loading short URLs: {e}")
return {}
def _save_short_urls(self):
"""Save short URLs to JSON file"""
try:
with open(SHORT_URLS_FILE, 'w', encoding='utf-8') as f:
json.dump(self.short_urls_db, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error saving short URLs: {e}")
def generate_short_code(self, length=6):
"""Generate a random short code"""
characters = string.ascii_letters + string.digits
while True:
short_code = ''.join(random.choice(characters) for _ in range(length))
# Ensure uniqueness
if short_code not in self.short_urls_db:
return short_code
def create_short_url(self, original_url, custom_code=None, title=""):
"""Create a shortened URL"""
# Generate or use custom short code
if custom_code and custom_code not in self.short_urls_db:
short_code = custom_code
else:
short_code = self.generate_short_code()
# Ensure original URL has protocol
if not original_url.startswith(('http://', 'https://')):
original_url = f'https://{original_url}'
# Create URL record
url_data = {
'id': str(uuid.uuid4()),
'short_code': short_code,
'original_url': original_url,
'title': title,
'clicks': 0,
'created_at': datetime.now().isoformat(),
'last_accessed': None
}
self.short_urls_db[short_code] = url_data
self._save_short_urls() # Persist to file
# Return the complete short URL
short_url = f"{self.base_domain}/s/{short_code}"
return {
'short_url': short_url,
'short_code': short_code,
'original_url': original_url,
'id': url_data['id']
}
def get_original_url(self, short_code):
"""Get original URL from short code and track click"""
if short_code in self.short_urls_db:
url_data = self.short_urls_db[short_code]
# Track click
url_data['clicks'] += 1
url_data['last_accessed'] = datetime.now().isoformat()
self._save_short_urls() # Persist to file
return url_data['original_url']
return None
def get_url_stats(self, short_code):
"""Get statistics for a short URL"""
return self.short_urls_db.get(short_code)
def list_urls(self):
"""List all short URLs"""
return list(self.short_urls_db.values())
def delete_url(self, short_code):
"""Delete a short URL"""
if short_code in self.short_urls_db:
del self.short_urls_db[short_code]
self._save_short_urls() # Persist to file
return True
return False
def url_exists(self, short_code):
"""Check if short URL exists"""
return short_code in self.short_urls_db

196
clean_data.py Executable file
View File

@@ -0,0 +1,196 @@
#!/usr/bin/env python3
"""
QR Code Manager - Data Cleanup Script
This script removes all persistent data to prepare for a clean deployment.
It will delete:
- All QR codes and their data
- All dynamic link pages
- All short URLs
- All uploaded QR code images
- Flask session files
Use this script when you want to start fresh or prepare for deployment.
"""
import os
import shutil
import json
from pathlib import Path
def clean_json_data():
"""Clean all JSON data files"""
data_dir = Path('data')
json_files = [
'qr_codes.json',
'link_pages.json',
'short_urls.json'
]
print("🗑️ Cleaning JSON data files...")
for json_file in json_files:
file_path = data_dir / json_file
if file_path.exists():
# Reset to empty object
with open(file_path, 'w', encoding='utf-8') as f:
json.dump({}, f, indent=2)
print(f" ✅ Cleared {json_file}")
else:
print(f" ⚠️ {json_file} not found")
def clean_qr_images():
"""Clean all QR code image files"""
qr_dir = Path('app/static/qr_codes')
print("🖼️ Cleaning QR code images...")
if qr_dir.exists():
# Count files before deletion
files = list(qr_dir.glob('*.png'))
count = len(files)
# Delete all PNG files
for file in files:
file.unlink()
print(f" ✅ Deleted {count} QR code images")
else:
print(" ⚠️ QR codes directory not found")
def clean_flask_sessions():
"""Clean Flask session files"""
session_dir = Path('flask_session')
print("🔐 Cleaning Flask sessions...")
if session_dir.exists():
# Count files before deletion
files = list(session_dir.iterdir())
count = len(files)
# Delete all session files
for file in files:
if file.is_file():
file.unlink()
print(f" ✅ Deleted {count} session files")
else:
print(" ⚠️ Flask session directory not found")
def clean_logs():
"""Clean any log files"""
print("📝 Cleaning log files...")
log_patterns = ['*.log', '*.log.*']
found_logs = False
for pattern in log_patterns:
for log_file in Path('.').glob(pattern):
log_file.unlink()
print(f" ✅ Deleted {log_file}")
found_logs = True
if not found_logs:
print(" ✅ No log files found")
def clean_pycache():
"""Clean Python cache files"""
print("🐍 Cleaning Python cache...")
cache_dirs = list(Path('.').rglob('__pycache__'))
pyc_files = list(Path('.').rglob('*.pyc'))
# Remove __pycache__ directories
for cache_dir in cache_dirs:
if cache_dir.is_dir():
shutil.rmtree(cache_dir)
# Remove .pyc files
for pyc_file in pyc_files:
pyc_file.unlink()
total_cleaned = len(cache_dirs) + len(pyc_files)
print(f" ✅ Cleaned {total_cleaned} cache files/directories")
def create_fresh_directories():
"""Ensure required directories exist"""
print("📁 Creating fresh directories...")
directories = [
'data',
'app/static/qr_codes',
'app/static/logos',
'flask_session'
]
for directory in directories:
Path(directory).mkdir(parents=True, exist_ok=True)
print(f" ✅ Ensured {directory} exists")
def main():
"""Main cleanup function"""
print("🧹 QR Code Manager - Data Cleanup Script")
print("=" * 50)
# Change to script directory
script_dir = Path(__file__).parent
os.chdir(script_dir)
print(f"📂 Working directory: {os.getcwd()}")
print()
# Ask for confirmation
print("⚠️ WARNING: This will delete ALL persistent data!")
print(" - All QR codes and their images")
print(" - All dynamic link pages")
print(" - All short URLs")
print(" - All Flask sessions")
print(" - All log files")
print(" - All Python cache files")
print()
confirm = input("Are you sure you want to continue? Type 'YES' to confirm: ")
if confirm != 'YES':
print("❌ Cleanup cancelled.")
return
print()
print("🚀 Starting cleanup process...")
print()
try:
# Clean different types of data
clean_json_data()
print()
clean_qr_images()
print()
clean_flask_sessions()
print()
clean_logs()
print()
clean_pycache()
print()
create_fresh_directories()
print()
print("✅ Cleanup completed successfully!")
print()
print("🎉 Your QR Code Manager is now ready for a fresh deployment!")
print(" Next steps:")
print(" 1. Start the application: python main.py")
print(" 2. Login with: admin / admin123")
print(" 3. Change the default password")
except Exception as e:
print(f"❌ Error during cleanup: {e}")
print("Please check the error and try again.")
if __name__ == '__main__':
main()

101
clean_data.sh Executable file
View File

@@ -0,0 +1,101 @@
#!/bin/bash
"""
QR Code Manager - Quick Data Cleanup Script (Shell Version)
A simple shell script to clean all persistent data for deployment.
"""
echo "🧹 QR Code Manager - Quick Data Cleanup"
echo "======================================"
echo ""
# Get script directory
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
cd "$SCRIPT_DIR"
echo "📂 Working directory: $(pwd)"
echo ""
echo "⚠️ WARNING: This will delete ALL persistent data!"
echo " - All QR codes and their images"
echo " - All dynamic link pages"
echo " - All short URLs"
echo " - All Flask sessions"
echo ""
read -p "Are you sure you want to continue? Type 'YES' to confirm: " confirm
if [ "$confirm" != "YES" ]; then
echo "❌ Cleanup cancelled."
exit 0
fi
echo ""
echo "🚀 Starting cleanup process..."
echo ""
# Clean JSON data files
echo "🗑️ Cleaning JSON data files..."
if [ -d "data" ]; then
echo '{}' > data/qr_codes.json 2>/dev/null && echo " ✅ Cleared qr_codes.json" || echo " ⚠️ qr_codes.json not found"
echo '{}' > data/link_pages.json 2>/dev/null && echo " ✅ Cleared link_pages.json" || echo " ⚠️ link_pages.json not found"
echo '{}' > data/short_urls.json 2>/dev/null && echo " ✅ Cleared short_urls.json" || echo " ⚠️ short_urls.json not found"
else
echo " ⚠️ Data directory not found"
fi
echo ""
# Clean QR code images
echo "🖼️ Cleaning QR code images..."
if [ -d "app/static/qr_codes" ]; then
COUNT=$(find app/static/qr_codes -name "*.png" | wc -l)
find app/static/qr_codes -name "*.png" -delete
echo " ✅ Deleted $COUNT QR code images"
else
echo " ⚠️ QR codes directory not found"
fi
echo ""
# Clean Flask sessions
echo "🔐 Cleaning Flask sessions..."
if [ -d "flask_session" ]; then
COUNT=$(find flask_session -type f | wc -l)
find flask_session -type f -delete
echo " ✅ Deleted $COUNT session files"
else
echo " ⚠️ Flask session directory not found"
fi
echo ""
# Clean log files
echo "📝 Cleaning log files..."
LOG_COUNT=$(find . -maxdepth 1 -name "*.log*" | wc -l)
if [ $LOG_COUNT -gt 0 ]; then
find . -maxdepth 1 -name "*.log*" -delete
echo " ✅ Deleted $LOG_COUNT log files"
else
echo " ✅ No log files found"
fi
echo ""
# Clean Python cache
echo "🐍 Cleaning Python cache..."
CACHE_COUNT=$(find . -name "__pycache__" -o -name "*.pyc" | wc -l)
find . -name "__pycache__" -exec rm -rf {} + 2>/dev/null
find . -name "*.pyc" -delete 2>/dev/null
echo " ✅ Cleaned $CACHE_COUNT cache files/directories"
echo ""
# Create fresh directories
echo "📁 Creating fresh directories..."
mkdir -p data app/static/qr_codes app/static/logos flask_session
echo " ✅ Ensured all directories exist"
echo ""
echo "✅ Cleanup completed successfully!"
echo ""
echo "🎉 Your QR Code Manager is now ready for a fresh deployment!"
echo " Next steps:"
echo " 1. Start the application: python main.py"
echo " 2. Login with: admin / admin123"
echo " 3. Change the default password"

1
data/link_pages.json Normal file
View File

@@ -0,0 +1 @@
{}

1
data/qr_codes.json Normal file
View File

@@ -0,0 +1 @@
{}

1
data/short_urls.json Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -16,6 +16,7 @@ services:
- qr_data:/app/app/static/qr_codes - qr_data:/app/app/static/qr_codes
- logo_data:/app/app/static/logos - logo_data:/app/app/static/logos
- session_data:/app/flask_session - session_data:/app/flask_session
- persistent_data:/app/data
healthcheck: healthcheck:
test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:5000/health')"] test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:5000/health')"]
interval: 30s interval: 30s
@@ -33,6 +34,8 @@ volumes:
driver: local driver: local
session_data: session_data:
driver: local driver: local
persistent_data:
driver: local
networks: networks:
default: default:

37
gunicorn.conf.py Normal file
View File

@@ -0,0 +1,37 @@
# Gunicorn configuration file
# Documentation: https://docs.gunicorn.org/en/stable/configure.html
# Server socket
bind = "0.0.0.0:5000"
backlog = 2048
# Worker processes
workers = 4
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 2
# Restart workers after this many requests, to prevent memory leaks
max_requests = 1000
max_requests_jitter = 50
# Logging
accesslog = "-"
errorlog = "-"
loglevel = "info"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
# Process naming
proc_name = "qr-code-manager"
# Server mechanics
preload_app = True
pidfile = "/tmp/gunicorn.pid"
user = "app"
group = "app"
tmp_upload_dir = None
# SSL (uncomment and configure for HTTPS)
# keyfile = "/path/to/keyfile"
# certfile = "/path/to/certfile"

20
main.py
View File

@@ -1,11 +1,13 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """ print("🚀 QR Code Manager - Production Mode")
QR Code Manager - Main Application Entry Point print(" This should be run with Gunicorn in production!")
print("🔧 Use: gunicorn -c gunicorn.conf.py main:app") Code Manager - Main Application Entry Point
A modern Flask web application for generating and managing QR codes with authentication. A modern Flask web application for generating and managing QR codes with authentication.
Features include: Features include:
- Multiple QR code types (text, URL, WiFi, email, SMS, vCard) - Multiple QR code types (text, URL, WiFi, email, SMS, vCard)
- Dynamic link pages for managing collections of links - Dynamic link pages for managing collections of links
- URL shortener functionality with custom domains
- Admin authentication with bcrypt password hashing - Admin authentication with bcrypt password hashing
- Docker deployment ready - Docker deployment ready
- Modern responsive web interface - Modern responsive web interface
@@ -20,14 +22,20 @@ app = create_app()
if __name__ == '__main__': if __name__ == '__main__':
# Production vs Development configuration # Production vs Development configuration
is_production = os.environ.get('FLASK_ENV') == 'production' is_production = os.environ.get('FLASK_ENV') == 'production'
app_domain = os.environ.get('APP_DOMAIN', 'localhost:5000')
if is_production: if is_production:
print("🚀 Starting QR Code Manager in PRODUCTION mode") print("🚀 QR Code Manager - Production Mode")
print("🔐 Admin user: admin") print(" This should be run with Gunicorn in production!")
print("🔒 Make sure to change default credentials!") print("<EFBFBD> Use: gunicorn -c gunicorn.conf.py main:app")
app.run(host='0.0.0.0', port=5000, debug=False) print(f"🌐 Domain configured: {app_domain}")
print("🔗 URL shortener available at: /s/")
# In production, this file is used by Gunicorn as WSGI application
# The Flask dev server should NOT be started in production
else: else:
print("🛠️ Starting QR Code Manager in DEVELOPMENT mode") print("🛠️ Starting QR Code Manager in DEVELOPMENT mode")
print("🔐 Admin user: admin") print("🔐 Admin user: admin")
print("🔑 Default password: admin123") print("🔑 Default password: admin123")
print(f"🌐 Domain configured: {app_domain}")
print("🔗 URL shortener available at: /s/")
app.run(host='0.0.0.0', port=5000, debug=True) app.run(host='0.0.0.0', port=5000, debug=True)

View File

@@ -7,3 +7,5 @@ requests==2.31.0
flask-session==0.5.0 flask-session==0.5.0
werkzeug==2.3.7 werkzeug==2.3.7
bcrypt==4.0.1 bcrypt==4.0.1
lxml==4.9.3
gunicorn==21.2.0