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 main.py .
COPY gunicorn.conf.py .
COPY app/ ./app/
# 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
ENV FLASK_APP=main.py
@@ -45,5 +46,5 @@ EXPOSE 5000
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:5000/health')" || exit 1
# Run the application
CMD ["python", "main.py"]
# Run the application with Gunicorn for production
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
- **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
- **Customizable Styling**: Different QR code styles (square, rounded, circle)
- **Logo Integration**: Add custom logos to QR codes
- **Click Tracking**: Monitor short URL usage and statistics
- **Docker Deployment**: Production-ready containerization
- **Responsive Design**: Modern web interface that works on all devices
@@ -43,6 +45,7 @@ qr-code_manager/
│ ├── auth.py # Authentication utilities
│ ├── qr_generator.py # QR code generation
│ ├── link_manager.py # Dynamic link management
│ ├── url_shortener.py # URL shortening utilities
│ └── data_manager.py # Data storage utilities
```
@@ -89,7 +92,180 @@ qr-code_manager/
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
- **Environment Variables**:
@@ -114,6 +290,7 @@ The application is fully containerized with Docker:
SECRET_KEY=your-super-secret-key-here
ADMIN_USERNAME=your-admin-username
ADMIN_PASSWORD=your-secure-password
APP_DOMAIN=qr.moto-adv.com # Your custom domain for URL shortener
```
2. **Deploy:**
@@ -121,6 +298,15 @@ The application is fully containerized with Docker:
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
### 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
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
- **Password Hashing**: Uses bcrypt for secure password storage
@@ -168,15 +366,28 @@ The application follows a modular Flask structure:
## 📊 API Endpoints
### QR Code Management
- `POST /api/generate` - Generate QR code
- `GET /api/qr_codes` - List all QR codes
- `GET /api/qr_codes/{id}` - Get specific QR code
- `DELETE /api/qr_codes/{id}` - Delete QR code
### Dynamic Link Pages
- `POST /api/create_link_page` - Create dynamic link page
- `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
### 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
### 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
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
For support, please open an issue on GitHub or contact the development team.

View File

@@ -7,7 +7,7 @@ import io
import base64
import uuid
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.qr_generator import QRCodeGenerator
from app.utils.link_manager import LinkPageManager
@@ -20,9 +20,11 @@ qr_generator = QRCodeGenerator()
link_manager = LinkPageManager()
data_manager = QRDataManager()
# Configuration for file uploads
UPLOAD_FOLDER = 'app/static/qr_codes'
LOGOS_FOLDER = 'app/static/logos'
# 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)
@@ -107,7 +109,7 @@ END:VCARD"""
@bp.route('/download/<qr_id>')
@login_required
def download_qr(qr_id):
"""Download QR code"""
"""Download QR code in PNG format"""
try:
img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png')
if os.path.exists(img_path):
@@ -117,6 +119,32 @@ def download_qr(qr_id):
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():
@@ -195,8 +223,19 @@ def create_link_page():
# Create the link page
page_id = link_manager.create_link_page(title, description)
# Create QR code pointing to the link page
page_url = f"{request.url_root}links/{page_id}"
# Create the original page URL
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 = {
'size': data.get('size', 10),
@@ -206,8 +245,8 @@ def create_link_page():
'style': data.get('style', 'square')
}
# Generate QR code
qr_img = qr_generator.generate_qr_code(page_url, settings)
# 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()
@@ -215,8 +254,8 @@ def create_link_page():
img_buffer.seek(0)
img_base64 = base64.b64encode(img_buffer.getvalue()).decode()
# Save QR code record
qr_id = data_manager.save_qr_record('link_page', page_url, settings, img_base64, page_id)
# 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')
@@ -226,7 +265,9 @@ def create_link_page():
'success': True,
'qr_id': qr_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}",
'image_data': f'data:image/png;base64,{img_base64}',
'download_url': f'/api/download/{qr_id}'
@@ -244,11 +285,17 @@ def add_link_to_page(page_id):
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:
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:
return jsonify({'success': True})
@@ -267,8 +314,14 @@ def update_link_in_page(page_id, link_id):
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)
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})
@@ -302,3 +355,108 @@ def get_link_page(page_id):
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

View File

@@ -2,7 +2,7 @@
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.link_manager import LinkPageManager
@@ -44,3 +44,12 @@ def health_check():
from datetime import datetime
from flask import jsonify
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;
padding: 20px;
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 15px;
}
.link-item.editing {
border-color: #667eea;
}
.link-content {
flex: 1;
}
.link-logo {
width: 32px;
height: 32px;
border-radius: 6px;
flex-shrink: 0;
}
.link-display {
display: block;
}
@@ -253,6 +268,13 @@
<div class="header">
<h1>✏️ Edit Links</h1>
<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 class="alert alert-success" id="success-alert">
@@ -292,12 +314,21 @@
{% if page.links %}
{% for link in page.links %}
<div class="link-item" data-link-id="{{ link.id }}">
<div class="link-display">
<div class="link-title">{{ link.title }}</div>
{% if link.description %}
<div class="link-description">{{ link.description }}</div>
<div class="link-content">
<div class="link-display">
<div class="link-title">{{ link.title }}</div>
{% if link.description %}
<div class="link-description">{{ link.description }}</div>
{% endif %}
<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-url">{{ link.url }}</div>
<div class="link-actions">
<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>
@@ -322,6 +353,7 @@
<button class="btn btn-small btn-secondary" onclick="cancelEdit('{{ link.id }}')">Cancel</button>
</div>
</div>
<img class="link-logo" src="" alt="" style="display: none;" onerror="this.style.display='none'">
</div>
{% endfor %}
{% else %}
@@ -344,6 +376,87 @@
<script>
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
document.getElementById('add-link-form').addEventListener('submit', async function(e) {
e.preventDefault();
@@ -358,7 +471,11 @@
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ title, url, description })
body: JSON.stringify({
title,
url,
description
})
});
const result = await response.json();
@@ -399,7 +516,11 @@
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ title, url, description })
body: JSON.stringify({
title,
url,
description
})
});
const result = await response.json();

View File

@@ -148,7 +148,8 @@
.link-page-fields,
.email-fields,
.sms-fields,
.vcard-fields {
.vcard-fields,
.url-shortener-fields {
display: none;
}
@@ -156,7 +157,8 @@
.link-page-fields.active,
.email-fields.active,
.sms-fields.active,
.vcard-fields.active {
.vcard-fields.active,
.url-shortener-fields.active {
display: block;
}
@@ -212,14 +214,9 @@
}
.download-section {
display: none;
gap: 10px;
}
.download-section.active {
display: flex;
}
.btn-secondary {
background: #6c757d;
flex: 1;
@@ -230,6 +227,15 @@
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 {
margin-top: 30px;
padding: 25px;
@@ -322,6 +328,7 @@
<option value="text">Text</option>
<option value="url">URL/Website</option>
<option value="link_page">Dynamic Link Page</option>
<option value="url_shortener">URL Shortener</option>
<option value="wifi">WiFi</option>
<option value="email">Email</option>
<option value="phone">Phone</option>
@@ -368,7 +375,32 @@
</div>
<div class="form-group">
<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>
</div>
</div>
@@ -478,8 +510,9 @@
</div>
<div class="download-section" id="download-section">
<button class="btn btn-primary" onclick="downloadQR()">Download PNG</button>
<button class="btn btn-secondary" onclick="copyToClipboard()">Copy Image</button>
<button class="btn btn-primary" onclick="downloadQR('png')">📥 Download PNG</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>
@@ -501,7 +534,7 @@
// Hide all specific fields
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');
});
@@ -570,6 +603,12 @@
email: document.getElementById('vcard-email').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 = {
@@ -583,7 +622,13 @@
};
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, {
method: 'POST',
headers: {
@@ -606,10 +651,22 @@
if (type === 'link_page') {
previewHTML += `
<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>
<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>Edit URL:</strong> <a href="${result.edit_url}" target="_blank">${result.edit_url}</a></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>
<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>🔗 Short URL:</strong> <a href="${result.page_url}" target="_blank">${result.page_url}</a>
<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="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>
`;
}
@@ -629,9 +686,13 @@
}
}
async function downloadQR() {
async function downloadQR(format = 'png') {
if (currentQRId) {
window.open(`/api/download/${currentQRId}`, '_blank');
if (format === 'svg') {
window.open(`/api/download/${currentQRId}/svg`, '_blank');
} else {
window.open(`/api/download/${currentQRId}`, '_blank');
}
}
}
@@ -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() {
try {
const response = await fetch('/api/qr_codes');
@@ -670,9 +748,10 @@
<p>Created: ${new Date(qr.created_at).toLocaleDateString()}</p>
</div>
<div class="qr-item-actions">
<button class="btn btn-small btn-primary" onclick="downloadQRById('${qr.id}')">Download</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-secondary" onclick="deleteQR('${qr.id}')">Delete</button>
<button class="btn btn-small btn-primary" onclick="downloadQRById('${qr.id}', 'png')" title="Download PNG">📥 PNG</button>
<button class="btn btn-small btn-success" onclick="downloadQRById('${qr.id}', 'svg')" title="Download SVG">🎨 SVG</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>
`).join('');
@@ -681,8 +760,12 @@
}
}
async function downloadQRById(qrId) {
window.open(`/api/download/${qrId}`, '_blank');
async function downloadQRById(qrId, format = 'png') {
if (format === 'svg') {
window.open(`/api/download/${qrId}/svg`, '_blank');
} else {
window.open(`/api/download/${qrId}`, '_blank');
}
}
async function deleteQR(qrId) {

View File

@@ -90,7 +90,10 @@
transition: all 0.3s ease;
cursor: pointer;
text-decoration: none;
display: block;
display: flex;
justify-content: space-between;
align-items: center;
gap: 15px;
color: inherit;
}
@@ -100,14 +103,22 @@
border-color: #667eea;
}
.link-content {
flex: 1;
}
.link-title {
font-size: 1.3em;
font-weight: 600;
color: #333;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 10px;
}
.link-logo {
width: 36px;
height: 36px;
border-radius: 8px;
flex-shrink: 0;
}
.link-icon {
@@ -153,31 +164,6 @@
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) {
.header {
padding: 30px 20px;
@@ -232,15 +218,19 @@
{% if page.links %}
<h2>📚 Available Links</h2>
{% for link in page.links %}
<a href="{{ link.url }}" target="_blank" class="link-item">
<div class="link-title">
<div class="link-icon">🔗</div>
{{ link.title }}
<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">
{{ 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>
{% if link.description %}
<div class="link-description">{{ link.description }}</div>
{% endif %}
<div class="link-url">{{ link.short_url if link.short_url else link.url }}</div>
</div>
{% if link.description %}
<div class="link-description">{{ link.description }}</div>
{% endif %}
<div class="link-url">{{ link.url }}</div>
<img class="link-logo" src="" alt="" style="display: none;" onerror="this.style.display='none'">
<div class="link-icon" style="display: block;">🔗</div>
</a>
{% endfor %}
{% else %}
@@ -252,18 +242,81 @@
{% endif %}
</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>
<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)
document.querySelectorAll('.link-item').forEach(link => {
link.addEventListener('click', function() {

View File

@@ -134,22 +134,6 @@
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) {
.login-container {
margin: 10px;
@@ -199,12 +183,6 @@
{% endif %}
{% endwith %}
<div class="default-credentials">
<strong>Default Login Credentials:</strong>
Username: admin<br>
Password: admin123
</div>
<form method="POST">
<div class="form-group">
<label for="username">Username</label>
@@ -222,9 +200,6 @@
<div class="login-footer">
<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>

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 .qr_generator import QRCodeGenerator
from .link_manager import LinkPageManager, link_pages_db
from .data_manager import QRDataManager, qr_codes_db
from .link_manager import LinkPageManager
from .data_manager import QRDataManager
from .url_shortener import URLShortener
__all__ = [
'init_admin',
@@ -14,7 +15,6 @@ __all__ = [
'get_admin_credentials',
'QRCodeGenerator',
'LinkPageManager',
'link_pages_db',
'QRDataManager',
'qr_codes_db'
'URLShortener'
]

View File

@@ -3,14 +3,39 @@ Data storage utilities for QR codes
"""
import uuid
import json
import os
from datetime import datetime
# In-memory storage for QR codes (in production, use a database)
qr_codes_db = {}
# Data storage directory
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:
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):
"""Save QR code record to database"""
@@ -27,32 +52,45 @@ class QRDataManager:
if 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
def get_qr_record(self, qr_id):
"""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):
"""Delete QR code record"""
if qr_id in qr_codes_db:
del qr_codes_db[qr_id]
if qr_id in self.qr_codes_db:
del self.qr_codes_db[qr_id]
self._save_qr_codes() # Persist to file
return True
return False
def list_qr_codes(self):
"""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 = []
for qr_id, qr_data in qr_codes_db.items():
for qr_id, qr_data in self.qr_codes_db.items():
qr_list.append({
'id': qr_id,
'type': qr_data['type'],
'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
def qr_exists(self, qr_id):
"""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 json
import os
from datetime import datetime
from .url_shortener import URLShortener
# In-memory storage for dynamic link pages (in production, use a database)
link_pages_db = {}
# Data storage directory
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:
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"):
"""Create a new dynamic link page"""
@@ -22,34 +49,70 @@ class LinkPageManager:
'links': [],
'created_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
def add_link(self, page_id, title, url, description=""):
"""Add a link to a page"""
if page_id not in link_pages_db:
def set_page_short_url(self, page_id, short_url, short_code):
"""Set the short URL for a link page"""
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
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 = {
'id': str(uuid.uuid4()),
'title': title,
'url': url if url.startswith(('http://', 'https://')) else f'https://{url}',
'url': url,
'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)
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
# Generate short URL if enabled
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
def update_link(self, page_id, link_id, title=None, url=None, description=None):
"""Update a specific link"""
if page_id not in link_pages_db:
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 with optional URL shortening"""
if page_id not in self.link_pages_db:
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 title is not None:
link['title'] = title
@@ -58,29 +121,79 @@ class LinkPageManager:
if description is not None:
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 False
def delete_link(self, page_id, link_id):
"""Delete a specific link"""
if page_id not in link_pages_db:
if page_id not in self.link_pages_db:
return False
links = link_pages_db[page_id]['links']
link_pages_db[page_id]['links'] = [link for link in links if link['id'] != link_id]
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
links = self.link_pages_db[page_id]['links']
for link in links:
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
def increment_view_count(self, page_id):
"""Increment view count for a page"""
if page_id in link_pages_db:
link_pages_db[page_id]['view_count'] += 1
if page_id in self.link_pages_db:
self.link_pages_db[page_id]['view_count'] += 1
self._save_link_pages() # Persist to file
def get_page(self, page_id):
"""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):
"""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
from qrcode.image.styledpil import StyledPilImage
from qrcode.image.styles.moduledrawers import RoundedModuleDrawer, CircleModuleDrawer, SquareModuleDrawer
from qrcode.image.svg import SvgPathImage, SvgFragmentImage, SvgFillImage
from PIL import Image
import io
class QRCodeGenerator:
def __init__(self):
@@ -19,8 +21,8 @@ class QRCodeGenerator:
'style': 'square'
}
def generate_qr_code(self, data, settings=None):
"""Generate QR code with custom settings"""
def generate_qr_code(self, data, settings=None, format='PNG'):
"""Generate QR code with custom settings in PNG or SVG format"""
if settings is None:
settings = self.default_settings.copy()
else:
@@ -28,6 +30,13 @@ class QRCodeGenerator:
merged_settings.update(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
qr = qrcode.QRCode(
version=1,
@@ -64,6 +73,47 @@ class QRCodeGenerator:
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):
"""Add logo to QR code"""
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
- logo_data:/app/app/static/logos
- session_data:/app/flask_session
- persistent_data:/app/data
healthcheck:
test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:5000/health')"]
interval: 30s
@@ -33,6 +34,8 @@ volumes:
driver: local
session_data:
driver: local
persistent_data:
driver: local
networks:
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
"""
QR Code Manager - Main Application Entry Point
""" print("🚀 QR Code Manager - Production Mode")
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.
Features include:
- Multiple QR code types (text, URL, WiFi, email, SMS, vCard)
- Dynamic link pages for managing collections of links
- URL shortener functionality with custom domains
- Admin authentication with bcrypt password hashing
- Docker deployment ready
- Modern responsive web interface
@@ -20,14 +22,20 @@ app = create_app()
if __name__ == '__main__':
# Production vs Development configuration
is_production = os.environ.get('FLASK_ENV') == 'production'
app_domain = os.environ.get('APP_DOMAIN', 'localhost:5000')
if is_production:
print("🚀 Starting QR Code Manager in PRODUCTION mode")
print("🔐 Admin user: admin")
print("🔒 Make sure to change default credentials!")
app.run(host='0.0.0.0', port=5000, debug=False)
print("🚀 QR Code Manager - Production Mode")
print(" This should be run with Gunicorn in production!")
print("<EFBFBD> Use: gunicorn -c gunicorn.conf.py main:app")
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:
print("🛠️ Starting QR Code Manager in DEVELOPMENT mode")
print("🔐 Admin user: admin")
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)

View File

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