final app for testing and deployment
This commit is contained in:
41
.env.production
Normal file
41
.env.production
Normal 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
19
.env.sample
Normal 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
|
||||
@@ -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
262
README.md
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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'
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
122
app/utils/url_shortener.py
Normal 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
196
clean_data.py
Executable 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
101
clean_data.sh
Executable 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
1
data/link_pages.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
data/qr_codes.json
Normal file
1
data/qr_codes.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
data/short_urls.json
Normal file
1
data/short_urls.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -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:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
37
gunicorn.conf.py
Normal file
37
gunicorn.conf.py
Normal 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
20
main.py
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user