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 application code
|
||||||
COPY main.py .
|
COPY main.py .
|
||||||
|
COPY gunicorn.conf.py .
|
||||||
COPY app/ ./app/
|
COPY app/ ./app/
|
||||||
|
|
||||||
# Create necessary directories
|
# Create necessary directories
|
||||||
RUN mkdir -p app/static/qr_codes app/static/logos flask_session
|
RUN mkdir -p app/static/qr_codes app/static/logos flask_session data
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
ENV FLASK_APP=main.py
|
ENV FLASK_APP=main.py
|
||||||
@@ -45,5 +46,5 @@ EXPOSE 5000
|
|||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
CMD python -c "import requests; requests.get('http://localhost:5000/health')" || exit 1
|
CMD python -c "import requests; requests.get('http://localhost:5000/health')" || exit 1
|
||||||
|
|
||||||
# Run the application
|
# Run the application with Gunicorn for production
|
||||||
CMD ["python", "main.py"]
|
CMD ["gunicorn", "-c", "gunicorn.conf.py", "main:app"]
|
||||||
|
|||||||
262
README.md
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
|
- **Multiple QR Code Types**: Text, URL, WiFi, Email, SMS, vCard
|
||||||
- **Dynamic Link Pages**: Create collections of links accessible via QR codes
|
- **Dynamic Link Pages**: Create collections of links accessible via QR codes
|
||||||
|
- **URL Shortener**: Generate shortened URLs with custom domains and QR codes
|
||||||
- **Admin Authentication**: Secure login with bcrypt password hashing
|
- **Admin Authentication**: Secure login with bcrypt password hashing
|
||||||
- **Customizable Styling**: Different QR code styles (square, rounded, circle)
|
- **Customizable Styling**: Different QR code styles (square, rounded, circle)
|
||||||
- **Logo Integration**: Add custom logos to QR codes
|
- **Logo Integration**: Add custom logos to QR codes
|
||||||
|
- **Click Tracking**: Monitor short URL usage and statistics
|
||||||
- **Docker Deployment**: Production-ready containerization
|
- **Docker Deployment**: Production-ready containerization
|
||||||
- **Responsive Design**: Modern web interface that works on all devices
|
- **Responsive Design**: Modern web interface that works on all devices
|
||||||
|
|
||||||
@@ -43,6 +45,7 @@ qr-code_manager/
|
|||||||
│ ├── auth.py # Authentication utilities
|
│ ├── auth.py # Authentication utilities
|
||||||
│ ├── qr_generator.py # QR code generation
|
│ ├── qr_generator.py # QR code generation
|
||||||
│ ├── link_manager.py # Dynamic link management
|
│ ├── link_manager.py # Dynamic link management
|
||||||
|
│ ├── url_shortener.py # URL shortening utilities
|
||||||
│ └── data_manager.py # Data storage utilities
|
│ └── data_manager.py # Data storage utilities
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -89,7 +92,180 @@ qr-code_manager/
|
|||||||
python main.py
|
python main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔐 Authentication
|
## <EFBFBD> Production vs Development Modes
|
||||||
|
|
||||||
|
The QR Code Manager supports two distinct runtime modes with different behaviors and optimizations.
|
||||||
|
|
||||||
|
### 🛠️ Development Mode (Default)
|
||||||
|
|
||||||
|
**When it runs:**
|
||||||
|
- When `FLASK_ENV` is not set to "production"
|
||||||
|
- When running `python main.py` locally
|
||||||
|
- Default mode for local development
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
- Uses Flask's built-in development server
|
||||||
|
- Debug mode enabled with auto-reload
|
||||||
|
- Detailed error messages and stack traces
|
||||||
|
- Console shows default login credentials
|
||||||
|
- Not suitable for production use
|
||||||
|
|
||||||
|
**How to run:**
|
||||||
|
```bash
|
||||||
|
# Method 1: Direct execution
|
||||||
|
python main.py
|
||||||
|
|
||||||
|
# Method 2: With explicit development environment
|
||||||
|
FLASK_ENV=development python main.py
|
||||||
|
|
||||||
|
# Method 3: Using environment file
|
||||||
|
echo "FLASK_ENV=development" > .env
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Console output in development:**
|
||||||
|
```
|
||||||
|
🛠️ Starting QR Code Manager in DEVELOPMENT mode
|
||||||
|
🔐 Admin user: admin
|
||||||
|
🔑 Default password: admin123
|
||||||
|
🌐 Domain configured: localhost:5000
|
||||||
|
🔗 URL shortener available at: /s/
|
||||||
|
* Serving Flask app 'app'
|
||||||
|
* Debug mode: on
|
||||||
|
* Running on all addresses (0.0.0.0)
|
||||||
|
* Running on http://127.0.0.1:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🚀 Production Mode
|
||||||
|
|
||||||
|
**When it runs:**
|
||||||
|
- When `FLASK_ENV=production` is set
|
||||||
|
- When deployed with Docker (automatic)
|
||||||
|
- For live/production deployments
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
- Uses Gunicorn WSGI server (4 workers)
|
||||||
|
- Debug mode disabled
|
||||||
|
- Optimized for performance and security
|
||||||
|
- No default credentials shown
|
||||||
|
- Production-grade error handling
|
||||||
|
|
||||||
|
**How to run:**
|
||||||
|
|
||||||
|
#### Option 1: Docker (Recommended)
|
||||||
|
```bash
|
||||||
|
# Copy and edit production environment
|
||||||
|
cp .env.production .env
|
||||||
|
# Edit .env with your production settings
|
||||||
|
|
||||||
|
# Deploy with Docker
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option 2: Manual Gunicorn
|
||||||
|
```bash
|
||||||
|
# Set production environment
|
||||||
|
export FLASK_ENV=production
|
||||||
|
export SECRET_KEY=your-super-secret-key
|
||||||
|
export ADMIN_USERNAME=your-admin-username
|
||||||
|
export ADMIN_PASSWORD=your-secure-password
|
||||||
|
export APP_DOMAIN=your-domain.com
|
||||||
|
|
||||||
|
# Run with Gunicorn
|
||||||
|
gunicorn -c gunicorn.conf.py main:app
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option 3: Environment File + Gunicorn
|
||||||
|
```bash
|
||||||
|
# Create production environment file
|
||||||
|
cp .env.production .env
|
||||||
|
|
||||||
|
# Edit .env with your settings:
|
||||||
|
# FLASK_ENV=production
|
||||||
|
# SECRET_KEY=your-super-secret-key
|
||||||
|
# ADMIN_USERNAME=your-admin-username
|
||||||
|
# ADMIN_PASSWORD=your-secure-password
|
||||||
|
# APP_DOMAIN=your-domain.com
|
||||||
|
|
||||||
|
# Run with Gunicorn
|
||||||
|
gunicorn -c gunicorn.conf.py main:app
|
||||||
|
```
|
||||||
|
|
||||||
|
**Console output in production:**
|
||||||
|
```
|
||||||
|
Admin user initialized: your-admin-username
|
||||||
|
Default password: your-secure-password
|
||||||
|
[2025-07-16 17:27:27 +0000] [1] [INFO] Starting gunicorn 21.2.0
|
||||||
|
[2025-07-16 17:27:27 +0000] [1] [INFO] Listening at: http://0.0.0.0:5000 (1)
|
||||||
|
[2025-07-16 17:27:27 +0000] [1] [INFO] Using worker: sync
|
||||||
|
[2025-07-16 17:27:27 +0000] [7] [INFO] Booting worker with pid: 7
|
||||||
|
[2025-07-16 17:27:27 +0000] [8] [INFO] Booting worker with pid: 8
|
||||||
|
[2025-07-16 17:27:27 +0000] [9] [INFO] Booting worker with pid: 9
|
||||||
|
[2025-07-16 17:27:27 +0000] [10] [INFO] Booting worker with pid: 10
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔧 Environment Configuration
|
||||||
|
|
||||||
|
Create a `.env` file in the project root with your configuration:
|
||||||
|
|
||||||
|
**For Development:**
|
||||||
|
```bash
|
||||||
|
# Development settings
|
||||||
|
FLASK_ENV=development
|
||||||
|
SECRET_KEY=dev-secret-key
|
||||||
|
ADMIN_USERNAME=admin
|
||||||
|
ADMIN_PASSWORD=admin123
|
||||||
|
APP_DOMAIN=localhost:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
**For Production:**
|
||||||
|
```bash
|
||||||
|
# Production settings (copy from .env.production and customize)
|
||||||
|
FLASK_ENV=production
|
||||||
|
SECRET_KEY=your-super-secret-key-change-this
|
||||||
|
ADMIN_USERNAME=your-admin-username
|
||||||
|
ADMIN_PASSWORD=your-secure-password
|
||||||
|
APP_DOMAIN=your-domain.com
|
||||||
|
|
||||||
|
# Security settings
|
||||||
|
SESSION_COOKIE_SECURE=true # Set to true for HTTPS
|
||||||
|
SESSION_COOKIE_HTTPONLY=true
|
||||||
|
SESSION_COOKIE_SAMESITE=Lax
|
||||||
|
|
||||||
|
# Performance settings
|
||||||
|
UPLOAD_MAX_SIZE=10485760 # 10MB
|
||||||
|
CACHE_TIMEOUT=3600 # 1 hour
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🛡️ Security Considerations
|
||||||
|
|
||||||
|
**Development Mode:**
|
||||||
|
- ⚠️ Never use in production
|
||||||
|
- Default credentials are visible
|
||||||
|
- Debug information exposed
|
||||||
|
- Single-threaded server
|
||||||
|
|
||||||
|
**Production Mode:**
|
||||||
|
- ✅ Use Gunicorn WSGI server
|
||||||
|
- ✅ Change default credentials
|
||||||
|
- ✅ Use strong SECRET_KEY
|
||||||
|
- ✅ Enable HTTPS when possible
|
||||||
|
- ✅ Set secure cookie flags
|
||||||
|
- ✅ Multiple worker processes
|
||||||
|
|
||||||
|
### 📊 Performance Comparison
|
||||||
|
|
||||||
|
| Feature | Development | Production |
|
||||||
|
|---------|-------------|------------|
|
||||||
|
| Server | Flask dev server | Gunicorn (4 workers) |
|
||||||
|
| Performance | Basic | Optimized |
|
||||||
|
| Concurrency | Single-threaded | Multi-worker |
|
||||||
|
| Auto-reload | Yes | No |
|
||||||
|
| Debug info | Full | Minimal |
|
||||||
|
| Error handling | Verbose | Secure |
|
||||||
|
| Session security | Basic | Enhanced |
|
||||||
|
|
||||||
|
## <20>🔐 Authentication
|
||||||
|
|
||||||
- **Default Credentials**: admin / admin123
|
- **Default Credentials**: admin / admin123
|
||||||
- **Environment Variables**:
|
- **Environment Variables**:
|
||||||
@@ -114,6 +290,7 @@ The application is fully containerized with Docker:
|
|||||||
SECRET_KEY=your-super-secret-key-here
|
SECRET_KEY=your-super-secret-key-here
|
||||||
ADMIN_USERNAME=your-admin-username
|
ADMIN_USERNAME=your-admin-username
|
||||||
ADMIN_PASSWORD=your-secure-password
|
ADMIN_PASSWORD=your-secure-password
|
||||||
|
APP_DOMAIN=qr.moto-adv.com # Your custom domain for URL shortener
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Deploy:**
|
2. **Deploy:**
|
||||||
@@ -121,6 +298,15 @@ The application is fully containerized with Docker:
|
|||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### URL Shortener Configuration
|
||||||
|
|
||||||
|
The URL shortener feature uses the `APP_DOMAIN` environment variable to generate short URLs:
|
||||||
|
|
||||||
|
- **Development**: `APP_DOMAIN=localhost:5000`
|
||||||
|
- **Production**: `APP_DOMAIN=qr.moto-adv.com` or `APP_DOMAIN=https://qr.moto-adv.com`
|
||||||
|
|
||||||
|
Short URLs will be available at: `https://[your-domain]/s/[short-code]`
|
||||||
|
|
||||||
## 📱 Usage
|
## 📱 Usage
|
||||||
|
|
||||||
### Generating QR Codes
|
### Generating QR Codes
|
||||||
@@ -139,6 +325,18 @@ The application is fully containerized with Docker:
|
|||||||
3. **Share the QR code** that points to your link page
|
3. **Share the QR code** that points to your link page
|
||||||
4. **Update links anytime** without changing the QR code
|
4. **Update links anytime** without changing the QR code
|
||||||
|
|
||||||
|
### URL Shortener
|
||||||
|
|
||||||
|
1. **Create shortened URLs** with optional custom codes
|
||||||
|
2. **Generate QR codes** for shortened URLs
|
||||||
|
3. **Track clicks** and monitor usage statistics
|
||||||
|
4. **Redirect seamlessly** from short URLs to original destinations
|
||||||
|
5. **Integrate with link pages** by enabling shortener for individual links
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
- Original: `https://very-long-domain.com/extremely/long/path/to/resource`
|
||||||
|
- Short: `https://qr.moto-adv.com/s/abc123`
|
||||||
|
|
||||||
## 🛡️ Security Features
|
## 🛡️ Security Features
|
||||||
|
|
||||||
- **Password Hashing**: Uses bcrypt for secure password storage
|
- **Password Hashing**: Uses bcrypt for secure password storage
|
||||||
@@ -168,15 +366,28 @@ The application follows a modular Flask structure:
|
|||||||
|
|
||||||
## 📊 API Endpoints
|
## 📊 API Endpoints
|
||||||
|
|
||||||
|
### QR Code Management
|
||||||
- `POST /api/generate` - Generate QR code
|
- `POST /api/generate` - Generate QR code
|
||||||
- `GET /api/qr_codes` - List all QR codes
|
- `GET /api/qr_codes` - List all QR codes
|
||||||
- `GET /api/qr_codes/{id}` - Get specific QR code
|
- `GET /api/qr_codes/{id}` - Get specific QR code
|
||||||
- `DELETE /api/qr_codes/{id}` - Delete QR code
|
- `DELETE /api/qr_codes/{id}` - Delete QR code
|
||||||
|
|
||||||
|
### Dynamic Link Pages
|
||||||
- `POST /api/create_link_page` - Create dynamic link page
|
- `POST /api/create_link_page` - Create dynamic link page
|
||||||
- `POST /api/link_pages/{id}/links` - Add link to page
|
- `POST /api/link_pages/{id}/links` - Add link to page
|
||||||
- `PUT /api/link_pages/{id}/links/{link_id}` - Update link
|
- `PUT /api/link_pages/{id}/links/{link_id}` - Update link
|
||||||
- `DELETE /api/link_pages/{id}/links/{link_id}` - Delete link
|
- `DELETE /api/link_pages/{id}/links/{link_id}` - Delete link
|
||||||
|
|
||||||
|
### URL Shortener
|
||||||
|
- `POST /api/shorten` - Create standalone short URL
|
||||||
|
- `POST /api/generate_shortened_qr` - Generate QR code with short URL
|
||||||
|
- `GET /api/short_urls` - List all short URLs
|
||||||
|
- `GET /api/short_urls/{code}/stats` - Get short URL statistics
|
||||||
|
- `GET /s/{code}` - Redirect short URL to original
|
||||||
|
- `POST /api/link_pages/{id}/links` - Add link to page
|
||||||
|
- `PUT /api/link_pages/{id}/links/{link_id}` - Update link
|
||||||
|
- `DELETE /api/link_pages/{id}/links/{link_id}` - Delete link
|
||||||
|
|
||||||
## 🚨 Troubleshooting
|
## 🚨 Troubleshooting
|
||||||
|
|
||||||
### Common Issues
|
### Common Issues
|
||||||
@@ -205,6 +416,55 @@ This project is licensed under the MIT License - see the LICENSE file for detail
|
|||||||
4. Add tests if applicable
|
4. Add tests if applicable
|
||||||
5. Submit a pull request
|
5. Submit a pull request
|
||||||
|
|
||||||
|
## 🧹 Data Cleanup for Deployment
|
||||||
|
|
||||||
|
When preparing for a fresh deployment or when you need to clear all data, use the provided cleanup scripts:
|
||||||
|
|
||||||
|
### Option 1: Python Script (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python clean_data.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Shell Script (Quick)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./clean_data.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### What Gets Cleaned
|
||||||
|
|
||||||
|
Both scripts will remove:
|
||||||
|
|
||||||
|
- **All QR codes and their data** - Clears the QR codes database and deletes all generated PNG images
|
||||||
|
- **All dynamic link pages** - Removes all link collections and their settings
|
||||||
|
- **All short URLs** - Clears the URL shortener database
|
||||||
|
- **All Flask sessions** - Removes user session files
|
||||||
|
- **All log files** - Deletes any application log files
|
||||||
|
- **Python cache files** - Removes `__pycache__` directories and `.pyc` files
|
||||||
|
|
||||||
|
### Safety Features
|
||||||
|
|
||||||
|
- **Confirmation prompt** - Both scripts require typing 'YES' to confirm the action
|
||||||
|
- **Directory preservation** - Required directories are recreated after cleanup
|
||||||
|
- **Error handling** - Scripts handle missing files/directories gracefully
|
||||||
|
|
||||||
|
### Post-Cleanup Steps
|
||||||
|
|
||||||
|
After running the cleanup script:
|
||||||
|
|
||||||
|
1. Start the application: `python main.py`
|
||||||
|
2. Login with default credentials: `admin` / `admin123`
|
||||||
|
3. **Important**: Change the default admin password immediately
|
||||||
|
4. Begin creating your QR codes and link pages
|
||||||
|
|
||||||
|
### Use Cases
|
||||||
|
|
||||||
|
- **Fresh deployment** - Clean slate for production deployment
|
||||||
|
- **Development reset** - Clear test data during development
|
||||||
|
- **Data migration** - Prepare for moving to a new system
|
||||||
|
- **Security cleanup** - Remove all data when decommissioning
|
||||||
|
|
||||||
## 📞 Support
|
## 📞 Support
|
||||||
|
|
||||||
For support, please open an issue on GitHub or contact the development team.
|
For support, please open an issue on GitHub or contact the development team.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import io
|
|||||||
import base64
|
import base64
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask import Blueprint, request, jsonify, send_file
|
from flask import Blueprint, request, jsonify, send_file, redirect, Response, current_app
|
||||||
from app.utils.auth import login_required
|
from app.utils.auth import login_required
|
||||||
from app.utils.qr_generator import QRCodeGenerator
|
from app.utils.qr_generator import QRCodeGenerator
|
||||||
from app.utils.link_manager import LinkPageManager
|
from app.utils.link_manager import LinkPageManager
|
||||||
@@ -20,9 +20,11 @@ qr_generator = QRCodeGenerator()
|
|||||||
link_manager = LinkPageManager()
|
link_manager = LinkPageManager()
|
||||||
data_manager = QRDataManager()
|
data_manager = QRDataManager()
|
||||||
|
|
||||||
# Configuration for file uploads
|
# Configuration for file uploads - use paths relative to app root
|
||||||
UPLOAD_FOLDER = 'app/static/qr_codes'
|
UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), '..', 'static', 'qr_codes')
|
||||||
LOGOS_FOLDER = 'app/static/logos'
|
LOGOS_FOLDER = os.path.join(os.path.dirname(__file__), '..', 'static', 'logos')
|
||||||
|
UPLOAD_FOLDER = os.path.abspath(UPLOAD_FOLDER)
|
||||||
|
LOGOS_FOLDER = os.path.abspath(LOGOS_FOLDER)
|
||||||
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
||||||
os.makedirs(LOGOS_FOLDER, exist_ok=True)
|
os.makedirs(LOGOS_FOLDER, exist_ok=True)
|
||||||
|
|
||||||
@@ -107,7 +109,7 @@ END:VCARD"""
|
|||||||
@bp.route('/download/<qr_id>')
|
@bp.route('/download/<qr_id>')
|
||||||
@login_required
|
@login_required
|
||||||
def download_qr(qr_id):
|
def download_qr(qr_id):
|
||||||
"""Download QR code"""
|
"""Download QR code in PNG format"""
|
||||||
try:
|
try:
|
||||||
img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png')
|
img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png')
|
||||||
if os.path.exists(img_path):
|
if os.path.exists(img_path):
|
||||||
@@ -117,6 +119,32 @@ def download_qr(qr_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@bp.route('/download/<qr_id>/svg')
|
||||||
|
@login_required
|
||||||
|
def download_qr_svg(qr_id):
|
||||||
|
"""Download QR code in SVG format"""
|
||||||
|
try:
|
||||||
|
# Get QR code data from database
|
||||||
|
qr_data = data_manager.get_qr_code(qr_id)
|
||||||
|
if not qr_data:
|
||||||
|
return jsonify({'error': 'QR code not found'}), 404
|
||||||
|
|
||||||
|
# Regenerate QR code as SVG
|
||||||
|
settings = qr_data.get('settings', {})
|
||||||
|
content = qr_data.get('content', '')
|
||||||
|
|
||||||
|
# Generate SVG QR code
|
||||||
|
svg_string = qr_generator.generate_qr_code_svg_string(content, settings)
|
||||||
|
|
||||||
|
# Create a response with SVG content
|
||||||
|
response = Response(svg_string, mimetype='image/svg+xml')
|
||||||
|
response.headers['Content-Disposition'] = f'attachment; filename=qr_code_{qr_id}.svg'
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@bp.route('/qr_codes')
|
@bp.route('/qr_codes')
|
||||||
@login_required
|
@login_required
|
||||||
def list_qr_codes():
|
def list_qr_codes():
|
||||||
@@ -195,8 +223,19 @@ def create_link_page():
|
|||||||
# Create the link page
|
# Create the link page
|
||||||
page_id = link_manager.create_link_page(title, description)
|
page_id = link_manager.create_link_page(title, description)
|
||||||
|
|
||||||
# Create QR code pointing to the link page
|
# Create the original page URL
|
||||||
page_url = f"{request.url_root}links/{page_id}"
|
original_page_url = f"{request.url_root}links/{page_id}"
|
||||||
|
|
||||||
|
# Automatically create a short URL for the link page
|
||||||
|
short_result = link_manager.create_standalone_short_url(
|
||||||
|
original_page_url,
|
||||||
|
title=f"Link Page: {title}",
|
||||||
|
custom_code=None
|
||||||
|
)
|
||||||
|
short_page_url = short_result['short_url']
|
||||||
|
|
||||||
|
# Store the short URL info with the page
|
||||||
|
link_manager.set_page_short_url(page_id, short_page_url, short_result['short_code'])
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
'size': data.get('size', 10),
|
'size': data.get('size', 10),
|
||||||
@@ -206,8 +245,8 @@ def create_link_page():
|
|||||||
'style': data.get('style', 'square')
|
'style': data.get('style', 'square')
|
||||||
}
|
}
|
||||||
|
|
||||||
# Generate QR code
|
# Generate QR code pointing to the SHORT URL (not the original long URL)
|
||||||
qr_img = qr_generator.generate_qr_code(page_url, settings)
|
qr_img = qr_generator.generate_qr_code(short_page_url, settings)
|
||||||
|
|
||||||
# Convert to base64
|
# Convert to base64
|
||||||
img_buffer = io.BytesIO()
|
img_buffer = io.BytesIO()
|
||||||
@@ -215,8 +254,8 @@ def create_link_page():
|
|||||||
img_buffer.seek(0)
|
img_buffer.seek(0)
|
||||||
img_base64 = base64.b64encode(img_buffer.getvalue()).decode()
|
img_base64 = base64.b64encode(img_buffer.getvalue()).decode()
|
||||||
|
|
||||||
# Save QR code record
|
# Save QR code record with the short URL
|
||||||
qr_id = data_manager.save_qr_record('link_page', page_url, settings, img_base64, page_id)
|
qr_id = data_manager.save_qr_record('link_page', short_page_url, settings, img_base64, page_id)
|
||||||
|
|
||||||
# Save image file
|
# Save image file
|
||||||
img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png')
|
img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png')
|
||||||
@@ -226,7 +265,9 @@ def create_link_page():
|
|||||||
'success': True,
|
'success': True,
|
||||||
'qr_id': qr_id,
|
'qr_id': qr_id,
|
||||||
'page_id': page_id,
|
'page_id': page_id,
|
||||||
'page_url': page_url,
|
'page_url': short_page_url, # Return the short URL as the main page URL
|
||||||
|
'original_url': original_page_url, # Keep original for reference
|
||||||
|
'short_code': short_result['short_code'],
|
||||||
'edit_url': f"{request.url_root}edit/{page_id}",
|
'edit_url': f"{request.url_root}edit/{page_id}",
|
||||||
'image_data': f'data:image/png;base64,{img_base64}',
|
'image_data': f'data:image/png;base64,{img_base64}',
|
||||||
'download_url': f'/api/download/{qr_id}'
|
'download_url': f'/api/download/{qr_id}'
|
||||||
@@ -244,11 +285,17 @@ def add_link_to_page(page_id):
|
|||||||
title = data.get('title', '')
|
title = data.get('title', '')
|
||||||
url = data.get('url', '')
|
url = data.get('url', '')
|
||||||
description = data.get('description', '')
|
description = data.get('description', '')
|
||||||
|
enable_shortener = data.get('enable_shortener', False)
|
||||||
|
custom_short_code = data.get('custom_short_code', None)
|
||||||
|
|
||||||
if not title or not url:
|
if not title or not url:
|
||||||
return jsonify({'error': 'Title and URL are required'}), 400
|
return jsonify({'error': 'Title and URL are required'}), 400
|
||||||
|
|
||||||
success = link_manager.add_link(page_id, title, url, description)
|
success = link_manager.add_link(
|
||||||
|
page_id, title, url, description,
|
||||||
|
enable_shortener=enable_shortener,
|
||||||
|
custom_short_code=custom_short_code
|
||||||
|
)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
@@ -267,8 +314,14 @@ def update_link_in_page(page_id, link_id):
|
|||||||
title = data.get('title')
|
title = data.get('title')
|
||||||
url = data.get('url')
|
url = data.get('url')
|
||||||
description = data.get('description')
|
description = data.get('description')
|
||||||
|
enable_shortener = data.get('enable_shortener')
|
||||||
|
custom_short_code = data.get('custom_short_code')
|
||||||
|
|
||||||
success = link_manager.update_link(page_id, link_id, title, url, description)
|
success = link_manager.update_link(
|
||||||
|
page_id, link_id, title, url, description,
|
||||||
|
enable_shortener=enable_shortener,
|
||||||
|
custom_short_code=custom_short_code
|
||||||
|
)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
@@ -302,3 +355,108 @@ def get_link_page(page_id):
|
|||||||
return jsonify(page_data)
|
return jsonify(page_data)
|
||||||
else:
|
else:
|
||||||
return jsonify({'error': 'Page not found'}), 404
|
return jsonify({'error': 'Page not found'}), 404
|
||||||
|
|
||||||
|
# URL Shortener API Routes
|
||||||
|
|
||||||
|
@bp.route('/shorten', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def create_short_url():
|
||||||
|
"""Create a shortened URL"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
url = data.get('url', '')
|
||||||
|
title = data.get('title', '')
|
||||||
|
custom_code = data.get('custom_code', None)
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
return jsonify({'error': 'URL is required'}), 400
|
||||||
|
|
||||||
|
result = link_manager.create_standalone_short_url(url, title, custom_code)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'short_url': result['short_url'],
|
||||||
|
'short_code': result['short_code'],
|
||||||
|
'original_url': result['original_url']
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@bp.route('/short_urls')
|
||||||
|
@login_required
|
||||||
|
def list_short_urls():
|
||||||
|
"""List all shortened URLs"""
|
||||||
|
try:
|
||||||
|
urls = link_manager.list_all_short_urls()
|
||||||
|
return jsonify({'success': True, 'urls': urls})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@bp.route('/short_urls/<short_code>/stats')
|
||||||
|
@login_required
|
||||||
|
def get_short_url_stats(short_code):
|
||||||
|
"""Get statistics for a short URL"""
|
||||||
|
try:
|
||||||
|
stats = link_manager.get_short_url_stats(short_code)
|
||||||
|
if stats:
|
||||||
|
return jsonify({'success': True, 'stats': stats})
|
||||||
|
else:
|
||||||
|
return jsonify({'error': 'Short URL not found'}), 404
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@bp.route('/generate_shortened_qr', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def generate_shortened_qr():
|
||||||
|
"""Generate QR code for a shortened URL"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
shortener_data = data.get('shortener', {})
|
||||||
|
url = shortener_data.get('url', '')
|
||||||
|
title = shortener_data.get('title', '')
|
||||||
|
custom_code = shortener_data.get('custom_code', '').strip() or None
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
return jsonify({'error': 'URL is required'}), 400
|
||||||
|
|
||||||
|
# Create shortened URL
|
||||||
|
result = link_manager.create_standalone_short_url(url, title, custom_code)
|
||||||
|
short_url = result['short_url']
|
||||||
|
|
||||||
|
# Generate QR code for the short URL
|
||||||
|
settings = {
|
||||||
|
'size': data.get('size', 10),
|
||||||
|
'border': data.get('border', 4),
|
||||||
|
'foreground_color': data.get('foreground_color', '#000000'),
|
||||||
|
'background_color': data.get('background_color', '#FFFFFF'),
|
||||||
|
'style': data.get('style', 'square')
|
||||||
|
}
|
||||||
|
|
||||||
|
qr_img = qr_generator.generate_qr_code(short_url, settings)
|
||||||
|
|
||||||
|
# Convert to base64
|
||||||
|
img_buffer = io.BytesIO()
|
||||||
|
qr_img.save(img_buffer, format='PNG')
|
||||||
|
img_buffer.seek(0)
|
||||||
|
img_base64 = base64.b64encode(img_buffer.getvalue()).decode()
|
||||||
|
|
||||||
|
# Save QR code record
|
||||||
|
qr_id = data_manager.save_qr_record('url_shortener', short_url, settings, img_base64)
|
||||||
|
|
||||||
|
# Save image file
|
||||||
|
img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png')
|
||||||
|
qr_img.save(img_path)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'qr_id': qr_id,
|
||||||
|
'short_url': short_url,
|
||||||
|
'short_code': result['short_code'],
|
||||||
|
'original_url': result['original_url'],
|
||||||
|
'image_data': f'data:image/png;base64,{img_base64}',
|
||||||
|
'download_url': f'/api/download/{qr_id}'
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
Main routes for QR Code Manager
|
Main routes for QR Code Manager
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import Blueprint, render_template
|
from flask import Blueprint, render_template, redirect, abort
|
||||||
from app.utils.auth import login_required
|
from app.utils.auth import login_required
|
||||||
from app.utils.link_manager import LinkPageManager
|
from app.utils.link_manager import LinkPageManager
|
||||||
|
|
||||||
@@ -44,3 +44,12 @@ def health_check():
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
return jsonify({'status': 'healthy', 'timestamp': datetime.now().isoformat()})
|
return jsonify({'status': 'healthy', 'timestamp': datetime.now().isoformat()})
|
||||||
|
|
||||||
|
@bp.route('/s/<short_code>')
|
||||||
|
def redirect_short_url(short_code):
|
||||||
|
"""Redirect short URL to original URL"""
|
||||||
|
original_url = link_manager.resolve_short_url(short_code)
|
||||||
|
if original_url:
|
||||||
|
return redirect(original_url)
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
|||||||
@@ -141,12 +141,27 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-item.editing {
|
.link-item.editing {
|
||||||
border-color: #667eea;
|
border-color: #667eea;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-logo {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.link-display {
|
.link-display {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -253,6 +268,13 @@
|
|||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>✏️ Edit Links</h1>
|
<h1>✏️ Edit Links</h1>
|
||||||
<p>Manage your link collection: {{ page.title }}</p>
|
<p>Manage your link collection: {{ page.title }}</p>
|
||||||
|
{% if page.short_url %}
|
||||||
|
<div style="margin-top: 15px; padding: 12px; background: rgba(255,255,255,0.2); border-radius: 8px; font-size: 0.9em;">
|
||||||
|
<strong>🔗 Page Short URL:</strong>
|
||||||
|
<a href="{{ page.short_url }}" target="_blank" style="color: #fff; text-decoration: underline;">{{ page.short_url }}</a>
|
||||||
|
<button onclick="copyToClipboard('{{ page.short_url }}')" style="margin-left: 10px; padding: 4px 8px; background: rgba(255,255,255,0.3); color: white; border: 1px solid rgba(255,255,255,0.5); border-radius: 3px; cursor: pointer; font-size: 0.8em;">Copy</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-success" id="success-alert">
|
<div class="alert alert-success" id="success-alert">
|
||||||
@@ -292,12 +314,21 @@
|
|||||||
{% if page.links %}
|
{% if page.links %}
|
||||||
{% for link in page.links %}
|
{% for link in page.links %}
|
||||||
<div class="link-item" data-link-id="{{ link.id }}">
|
<div class="link-item" data-link-id="{{ link.id }}">
|
||||||
|
<div class="link-content">
|
||||||
<div class="link-display">
|
<div class="link-display">
|
||||||
<div class="link-title">{{ link.title }}</div>
|
<div class="link-title">{{ link.title }}</div>
|
||||||
{% if link.description %}
|
{% if link.description %}
|
||||||
<div class="link-description">{{ link.description }}</div>
|
<div class="link-description">{{ link.description }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="link-url">{{ link.url }}</div>
|
<div class="link-url" data-url="{{ link.url }}">{{ link.url }}</div>
|
||||||
|
{% if link.short_url %}
|
||||||
|
<div class="short-url-display" style="margin-top: 8px; padding: 8px; background: #e3f2fd; border-radius: 5px; border-left: 3px solid #2196f3;">
|
||||||
|
<small style="color: #1976d2; font-weight: 600;">🔗 Short URL:</small>
|
||||||
|
<br>
|
||||||
|
<a href="{{ link.short_url }}" target="_blank" style="color: #1976d2; text-decoration: none; font-family: monospace;">{{ link.short_url }}</a>
|
||||||
|
<button class="btn-copy" onclick="copyToClipboard('{{ link.short_url }}')" style="margin-left: 10px; padding: 2px 8px; background: #2196f3; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8em;">Copy</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="link-actions">
|
<div class="link-actions">
|
||||||
<button class="btn btn-small btn-secondary" onclick="editLink('{{ link.id }}')">Edit</button>
|
<button class="btn btn-small btn-secondary" onclick="editLink('{{ link.id }}')">Edit</button>
|
||||||
<button class="btn btn-small btn-danger" onclick="deleteLink('{{ link.id }}')">Delete</button>
|
<button class="btn btn-small btn-danger" onclick="deleteLink('{{ link.id }}')">Delete</button>
|
||||||
@@ -322,6 +353,7 @@
|
|||||||
<button class="btn btn-small btn-secondary" onclick="cancelEdit('{{ link.id }}')">Cancel</button>
|
<button class="btn btn-small btn-secondary" onclick="cancelEdit('{{ link.id }}')">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<img class="link-logo" src="" alt="" style="display: none;" onerror="this.style.display='none'">
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -344,6 +376,87 @@
|
|||||||
<script>
|
<script>
|
||||||
const pageId = '{{ page.id }}';
|
const pageId = '{{ page.id }}';
|
||||||
|
|
||||||
|
// Social media and website logo detection
|
||||||
|
function getWebsiteLogo(url) {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url.startsWith('http') ? url : 'https://' + url);
|
||||||
|
const domain = urlObj.hostname.toLowerCase().replace('www.', '');
|
||||||
|
|
||||||
|
// Logo mapping for popular sites
|
||||||
|
const logoMap = {
|
||||||
|
'facebook.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/facebook.svg',
|
||||||
|
'instagram.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/instagram.svg',
|
||||||
|
'twitter.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/twitter.svg',
|
||||||
|
'x.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/x.svg',
|
||||||
|
'tiktok.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/tiktok.svg',
|
||||||
|
'youtube.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/youtube.svg',
|
||||||
|
'linkedin.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/linkedin.svg',
|
||||||
|
'pinterest.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/pinterest.svg',
|
||||||
|
'snapchat.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/snapchat.svg',
|
||||||
|
'whatsapp.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/whatsapp.svg',
|
||||||
|
'telegram.org': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/telegram.svg',
|
||||||
|
'discord.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/discord.svg',
|
||||||
|
'reddit.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/reddit.svg',
|
||||||
|
'github.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/github.svg',
|
||||||
|
'gmail.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/gmail.svg',
|
||||||
|
'google.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/google.svg',
|
||||||
|
'amazon.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/amazon.svg',
|
||||||
|
'apple.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/apple.svg',
|
||||||
|
'microsoft.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/microsoft.svg',
|
||||||
|
'spotify.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/spotify.svg',
|
||||||
|
'netflix.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/netflix.svg',
|
||||||
|
'twitch.tv': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/twitch.svg',
|
||||||
|
'dropbox.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/dropbox.svg',
|
||||||
|
'zoom.us': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/zoom.svg'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (logoMap[domain]) {
|
||||||
|
return logoMap[domain];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to favicon
|
||||||
|
return `https://www.google.com/s2/favicons?domain=${domain}&sz=64`;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set logos for existing links
|
||||||
|
function setLogosForLinks() {
|
||||||
|
document.querySelectorAll('.link-url[data-url]').forEach(linkElement => {
|
||||||
|
const url = linkElement.getAttribute('data-url');
|
||||||
|
const logoImg = linkElement.closest('.link-item').querySelector('.link-logo');
|
||||||
|
const logoSrc = getWebsiteLogo(url);
|
||||||
|
|
||||||
|
if (logoSrc && logoImg) {
|
||||||
|
logoImg.src = logoSrc;
|
||||||
|
logoImg.style.display = 'block';
|
||||||
|
logoImg.alt = new URL(url.startsWith('http') ? url : 'https://' + url).hostname;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize logos when page loads
|
||||||
|
document.addEventListener('DOMContentLoaded', setLogosForLinks);
|
||||||
|
|
||||||
|
// Copy to clipboard function
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
navigator.clipboard.writeText(text).then(function() {
|
||||||
|
// Show temporary success message
|
||||||
|
const btn = event.target;
|
||||||
|
const originalText = btn.textContent;
|
||||||
|
btn.textContent = 'Copied!';
|
||||||
|
btn.style.background = '#4caf50';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = originalText;
|
||||||
|
btn.style.background = '#2196f3';
|
||||||
|
}, 1500);
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.error('Could not copy text: ', err);
|
||||||
|
alert('Failed to copy to clipboard');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Add new link
|
// Add new link
|
||||||
document.getElementById('add-link-form').addEventListener('submit', async function(e) {
|
document.getElementById('add-link-form').addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -358,7 +471,11 @@
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ title, url, description })
|
body: JSON.stringify({
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
description
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
@@ -399,7 +516,11 @@
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ title, url, description })
|
body: JSON.stringify({
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
description
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|||||||
@@ -148,7 +148,8 @@
|
|||||||
.link-page-fields,
|
.link-page-fields,
|
||||||
.email-fields,
|
.email-fields,
|
||||||
.sms-fields,
|
.sms-fields,
|
||||||
.vcard-fields {
|
.vcard-fields,
|
||||||
|
.url-shortener-fields {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +157,8 @@
|
|||||||
.link-page-fields.active,
|
.link-page-fields.active,
|
||||||
.email-fields.active,
|
.email-fields.active,
|
||||||
.sms-fields.active,
|
.sms-fields.active,
|
||||||
.vcard-fields.active {
|
.vcard-fields.active,
|
||||||
|
.url-shortener-fields.active {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,14 +214,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.download-section {
|
.download-section {
|
||||||
display: none;
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-section.active {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: #6c757d;
|
background: #6c757d;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -230,6 +227,15 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
background: linear-gradient(135deg, #20c997 0%, #17a2b8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
.qr-history {
|
.qr-history {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
padding: 25px;
|
padding: 25px;
|
||||||
@@ -322,6 +328,7 @@
|
|||||||
<option value="text">Text</option>
|
<option value="text">Text</option>
|
||||||
<option value="url">URL/Website</option>
|
<option value="url">URL/Website</option>
|
||||||
<option value="link_page">Dynamic Link Page</option>
|
<option value="link_page">Dynamic Link Page</option>
|
||||||
|
<option value="url_shortener">URL Shortener</option>
|
||||||
<option value="wifi">WiFi</option>
|
<option value="wifi">WiFi</option>
|
||||||
<option value="email">Email</option>
|
<option value="email">Email</option>
|
||||||
<option value="phone">Phone</option>
|
<option value="phone">Phone</option>
|
||||||
@@ -368,7 +375,32 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<p style="background: #e3f2fd; padding: 15px; border-radius: 8px; color: #1565c0; font-size: 0.9em;">
|
<p style="background: #e3f2fd; padding: 15px; border-radius: 8px; color: #1565c0; font-size: 0.9em;">
|
||||||
<strong>💡 Dynamic Link Page:</strong> This creates a QR code that points to a web page where you can add, edit, and manage links. The QR code stays the same, but you can update the links anytime!
|
<strong>💡 Dynamic Link Page with Short URL:</strong> This creates a QR code with a short URL that points to a web page where you can add, edit, and manage links. The QR code stays the same, but you can update the links anytime!
|
||||||
|
<br><br>✨ <strong>Auto Short URL:</strong> Your link page will automatically get a short URL like <code>qr.moto-adv.com/s/abc123</code> making the QR code simpler and easier to scan!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- URL Shortener fields -->
|
||||||
|
<div class="url-shortener-fields" id="url-shortener-fields">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="shortener-url">URL to Shorten</label>
|
||||||
|
<input type="url" id="shortener-url" placeholder="https://example.com/very/long/url">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="shortener-title">Title (optional)</label>
|
||||||
|
<input type="text" id="shortener-title" placeholder="My Website">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="shortener-custom-code">Custom Short Code (optional)</label>
|
||||||
|
<input type="text" id="shortener-custom-code" placeholder="mylink" maxlength="20">
|
||||||
|
<small style="color: #666; font-size: 0.8em;">
|
||||||
|
Leave empty for random code. Only letters and numbers allowed.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<p style="background: #e3f2fd; padding: 15px; border-radius: 8px; color: #1565c0; font-size: 0.9em;">
|
||||||
|
<strong>🔗 URL Shortener:</strong> Creates a short URL that redirects to your original URL. The QR code will contain the short URL. Perfect for long URLs or tracking clicks!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -478,8 +510,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="download-section" id="download-section">
|
<div class="download-section" id="download-section">
|
||||||
<button class="btn btn-primary" onclick="downloadQR()">Download PNG</button>
|
<button class="btn btn-primary" onclick="downloadQR('png')">📥 Download PNG</button>
|
||||||
<button class="btn btn-secondary" onclick="copyToClipboard()">Copy Image</button>
|
<button class="btn btn-success" onclick="downloadQR('svg')">🎨 Download SVG</button>
|
||||||
|
<button class="btn btn-secondary" onclick="copyToClipboard()">📋 Copy Image</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -501,7 +534,7 @@
|
|||||||
|
|
||||||
// Hide all specific fields
|
// Hide all specific fields
|
||||||
document.getElementById('text-field').style.display = 'none';
|
document.getElementById('text-field').style.display = 'none';
|
||||||
document.querySelectorAll('.wifi-fields, .link-page-fields, .email-fields, .sms-fields, .vcard-fields').forEach(el => {
|
document.querySelectorAll('.wifi-fields, .link-page-fields, .email-fields, .sms-fields, .vcard-fields, .url-shortener-fields').forEach(el => {
|
||||||
el.classList.remove('active');
|
el.classList.remove('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -570,6 +603,12 @@
|
|||||||
email: document.getElementById('vcard-email').value,
|
email: document.getElementById('vcard-email').value,
|
||||||
website: document.getElementById('vcard-website').value
|
website: document.getElementById('vcard-website').value
|
||||||
};
|
};
|
||||||
|
} else if (type === 'url_shortener') {
|
||||||
|
additionalData.shortener = {
|
||||||
|
url: document.getElementById('shortener-url').value,
|
||||||
|
title: document.getElementById('shortener-title').value,
|
||||||
|
custom_code: document.getElementById('shortener-custom-code').value
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestData = {
|
const requestData = {
|
||||||
@@ -583,7 +622,13 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const endpoint = type === 'link_page' ? '/api/create_link_page' : '/api/generate';
|
let endpoint = '/api/generate';
|
||||||
|
if (type === 'link_page') {
|
||||||
|
endpoint = '/api/create_link_page';
|
||||||
|
} else if (type === 'url_shortener') {
|
||||||
|
endpoint = '/api/generate_shortened_qr';
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
const response = await fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -606,10 +651,22 @@
|
|||||||
if (type === 'link_page') {
|
if (type === 'link_page') {
|
||||||
previewHTML += `
|
previewHTML += `
|
||||||
<div style="margin-top: 15px; padding: 15px; background: #e3f2fd; border-radius: 8px; text-align: left;">
|
<div style="margin-top: 15px; padding: 15px; background: #e3f2fd; border-radius: 8px; text-align: left;">
|
||||||
<h4 style="margin-bottom: 10px; color: #1565c0;">🎉 Dynamic Link Page Created!</h4>
|
<h4 style="margin-bottom: 10px; color: #1565c0;">🎉 Dynamic Link Page Created with Short URL!</h4>
|
||||||
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>Public URL:</strong> <a href="${result.page_url}" target="_blank">${result.page_url}</a></p>
|
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>🔗 Short URL:</strong> <a href="${result.page_url}" target="_blank">${result.page_url}</a>
|
||||||
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>Edit URL:</strong> <a href="${result.edit_url}" target="_blank">${result.edit_url}</a></p>
|
<button onclick="copyToClipboard('${result.page_url}')" style="margin-left: 10px; padding: 4px 8px; background: #1565c0; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8em;">Copy</button></p>
|
||||||
<p style="font-size: 0.9em; color: #666;">Share the QR code - visitors will see your link collection. Use the edit URL to manage your links!</p>
|
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>📝 Edit URL:</strong> <a href="${result.edit_url}" target="_blank">${result.edit_url}</a></p>
|
||||||
|
${result.original_url ? `<p style="margin-bottom: 10px; font-size: 0.8em; color: #666;"><strong>Original URL:</strong> ${result.original_url}</p>` : ''}
|
||||||
|
<p style="font-size: 0.9em; color: #666;">✨ QR code contains the short URL for easier scanning! Share it - visitors will see your link collection. Use the edit URL to manage your links!</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (type === 'url_shortener') {
|
||||||
|
previewHTML += `
|
||||||
|
<div style="margin-top: 15px; padding: 15px; background: #e8f5e8; border-radius: 8px; text-align: left;">
|
||||||
|
<h4 style="margin-bottom: 10px; color: #2e7d32;">🔗 Short URL Created!</h4>
|
||||||
|
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>Short URL:</strong> <a href="${result.short_url}" target="_blank">${result.short_url}</a></p>
|
||||||
|
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>Original URL:</strong> <a href="${result.original_url}" target="_blank">${result.original_url}</a></p>
|
||||||
|
<p style="font-size: 0.9em; color: #666;">The QR code contains your short URL. When scanned, it will redirect to your original URL!</p>
|
||||||
|
<button onclick="copyToClipboard('${result.short_url}')" style="margin-top: 10px; padding: 8px 15px; background: #2e7d32; color: white; border: none; border-radius: 5px; cursor: pointer;">Copy Short URL</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -629,11 +686,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadQR() {
|
async function downloadQR(format = 'png') {
|
||||||
if (currentQRId) {
|
if (currentQRId) {
|
||||||
|
if (format === 'svg') {
|
||||||
|
window.open(`/api/download/${currentQRId}/svg`, '_blank');
|
||||||
|
} else {
|
||||||
window.open(`/api/download/${currentQRId}`, '_blank');
|
window.open(`/api/download/${currentQRId}`, '_blank');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function copyToClipboard() {
|
async function copyToClipboard() {
|
||||||
if (currentQRData) {
|
if (currentQRData) {
|
||||||
@@ -650,6 +711,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy to clipboard function
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
navigator.clipboard.writeText(text).then(function() {
|
||||||
|
const btn = event.target;
|
||||||
|
const originalText = btn.textContent;
|
||||||
|
btn.textContent = 'Copied!';
|
||||||
|
btn.style.background = '#4caf50';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = originalText;
|
||||||
|
btn.style.background = '#2e7d32';
|
||||||
|
}, 1500);
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.error('Could not copy text: ', err);
|
||||||
|
alert('Failed to copy to clipboard');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function loadQRHistory() {
|
async function loadQRHistory() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/qr_codes');
|
const response = await fetch('/api/qr_codes');
|
||||||
@@ -670,9 +748,10 @@
|
|||||||
<p>Created: ${new Date(qr.created_at).toLocaleDateString()}</p>
|
<p>Created: ${new Date(qr.created_at).toLocaleDateString()}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="qr-item-actions">
|
<div class="qr-item-actions">
|
||||||
<button class="btn btn-small btn-primary" onclick="downloadQRById('${qr.id}')">Download</button>
|
<button class="btn btn-small btn-primary" onclick="downloadQRById('${qr.id}', 'png')" title="Download PNG">📥 PNG</button>
|
||||||
${qr.type === 'link_page' ? `<button class="btn btn-small" onclick="openLinkPage('${qr.id}')" style="background: #28a745;">Manage</button>` : ''}
|
<button class="btn btn-small btn-success" onclick="downloadQRById('${qr.id}', 'svg')" title="Download SVG">🎨 SVG</button>
|
||||||
<button class="btn btn-small btn-secondary" onclick="deleteQR('${qr.id}')">Delete</button>
|
${qr.type === 'link_page' ? `<button class="btn btn-small" onclick="openLinkPage('${qr.id}')" style="background: #28a745;" title="Manage Links">📝 Manage</button>` : ''}
|
||||||
|
<button class="btn btn-small btn-secondary" onclick="deleteQR('${qr.id}')" title="Delete QR Code">🗑️</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
@@ -681,9 +760,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadQRById(qrId) {
|
async function downloadQRById(qrId, format = 'png') {
|
||||||
|
if (format === 'svg') {
|
||||||
|
window.open(`/api/download/${qrId}/svg`, '_blank');
|
||||||
|
} else {
|
||||||
window.open(`/api/download/${qrId}`, '_blank');
|
window.open(`/api/download/${qrId}`, '_blank');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteQR(qrId) {
|
async function deleteQR(qrId) {
|
||||||
if (confirm('Are you sure you want to delete this QR code?')) {
|
if (confirm('Are you sure you want to delete this QR code?')) {
|
||||||
|
|||||||
@@ -90,7 +90,10 @@
|
|||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: block;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,14 +103,22 @@
|
|||||||
border-color: #667eea;
|
border-color: #667eea;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.link-title {
|
.link-title {
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
display: flex;
|
}
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
.link-logo {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-icon {
|
.link-icon {
|
||||||
@@ -153,31 +164,6 @@
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
border-top: 1px solid #e9ecef;
|
|
||||||
color: #666;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer a {
|
|
||||||
color: #667eea;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.last-updated {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 0.8em;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.header {
|
.header {
|
||||||
padding: 30px 20px;
|
padding: 30px 20px;
|
||||||
@@ -232,15 +218,19 @@
|
|||||||
{% if page.links %}
|
{% if page.links %}
|
||||||
<h2>📚 Available Links</h2>
|
<h2>📚 Available Links</h2>
|
||||||
{% for link in page.links %}
|
{% for link in page.links %}
|
||||||
<a href="{{ link.url }}" target="_blank" class="link-item">
|
<a href="{{ link.short_url if link.short_url else link.url }}" target="_blank" class="link-item" data-url="{{ link.url }}">
|
||||||
|
<div class="link-content">
|
||||||
<div class="link-title">
|
<div class="link-title">
|
||||||
<div class="link-icon">🔗</div>
|
|
||||||
{{ link.title }}
|
{{ link.title }}
|
||||||
|
{% if link.short_url %}<span style="background: #2196f3; color: white; font-size: 0.7em; padding: 2px 6px; border-radius: 10px; margin-left: 8px;">SHORT</span>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if link.description %}
|
{% if link.description %}
|
||||||
<div class="link-description">{{ link.description }}</div>
|
<div class="link-description">{{ link.description }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="link-url">{{ link.url }}</div>
|
<div class="link-url">{{ link.short_url if link.short_url else link.url }}</div>
|
||||||
|
</div>
|
||||||
|
<img class="link-logo" src="" alt="" style="display: none;" onerror="this.style.display='none'">
|
||||||
|
<div class="link-icon" style="display: block;">🔗</div>
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -252,18 +242,81 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
<p>Powered by <a href="/">QR Code Manager</a></p>
|
|
||||||
{% if page.updated_at %}
|
|
||||||
<div class="last-updated">
|
|
||||||
Last updated: {{ page.updated_at[:10] }} at {{ page.updated_at[11:19] }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Social media and website logo detection
|
||||||
|
function getWebsiteLogo(url) {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url.startsWith('http') ? url : 'https://' + url);
|
||||||
|
const domain = urlObj.hostname.toLowerCase().replace('www.', '');
|
||||||
|
|
||||||
|
// Logo mapping for popular sites
|
||||||
|
const logoMap = {
|
||||||
|
'facebook.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/facebook.svg',
|
||||||
|
'instagram.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/instagram.svg',
|
||||||
|
'twitter.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/twitter.svg',
|
||||||
|
'x.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/x.svg',
|
||||||
|
'tiktok.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/tiktok.svg',
|
||||||
|
'youtube.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/youtube.svg',
|
||||||
|
'linkedin.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/linkedin.svg',
|
||||||
|
'pinterest.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/pinterest.svg',
|
||||||
|
'snapchat.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/snapchat.svg',
|
||||||
|
'whatsapp.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/whatsapp.svg',
|
||||||
|
'telegram.org': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/telegram.svg',
|
||||||
|
'discord.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/discord.svg',
|
||||||
|
'reddit.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/reddit.svg',
|
||||||
|
'github.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/github.svg',
|
||||||
|
'gmail.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/gmail.svg',
|
||||||
|
'google.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/google.svg',
|
||||||
|
'amazon.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/amazon.svg',
|
||||||
|
'apple.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/apple.svg',
|
||||||
|
'microsoft.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/microsoft.svg',
|
||||||
|
'spotify.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/spotify.svg',
|
||||||
|
'netflix.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/netflix.svg',
|
||||||
|
'twitch.tv': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/twitch.svg',
|
||||||
|
'dropbox.com': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/dropbox.svg',
|
||||||
|
'zoom.us': 'https://cdn.jsdelivr.net/npm/simple-icons@v9/icons/zoom.svg'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (logoMap[domain]) {
|
||||||
|
return logoMap[domain];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to favicon
|
||||||
|
return `https://www.google.com/s2/favicons?domain=${domain}&sz=64`;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set logos for links
|
||||||
|
function setLogosForLinks() {
|
||||||
|
document.querySelectorAll('.link-item[data-url]').forEach(linkElement => {
|
||||||
|
const url = linkElement.getAttribute('data-url');
|
||||||
|
const logoImg = linkElement.querySelector('.link-logo');
|
||||||
|
const fallbackIcon = linkElement.querySelector('.link-icon');
|
||||||
|
const logoSrc = getWebsiteLogo(url);
|
||||||
|
|
||||||
|
if (logoSrc && logoImg) {
|
||||||
|
logoImg.src = logoSrc;
|
||||||
|
logoImg.style.display = 'block';
|
||||||
|
logoImg.alt = new URL(url.startsWith('http') ? url : 'https://' + url).hostname;
|
||||||
|
// Hide the fallback icon when logo is shown
|
||||||
|
logoImg.onload = function() {
|
||||||
|
if (fallbackIcon) fallbackIcon.style.display = 'none';
|
||||||
|
};
|
||||||
|
logoImg.onerror = function() {
|
||||||
|
this.style.display = 'none';
|
||||||
|
if (fallbackIcon) fallbackIcon.style.display = 'flex';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize logos when page loads
|
||||||
|
document.addEventListener('DOMContentLoaded', setLogosForLinks);
|
||||||
|
|
||||||
// Add click tracking (optional)
|
// Add click tracking (optional)
|
||||||
document.querySelectorAll('.link-item').forEach(link => {
|
document.querySelectorAll('.link-item').forEach(link => {
|
||||||
link.addEventListener('click', function() {
|
link.addEventListener('click', function() {
|
||||||
|
|||||||
@@ -134,22 +134,6 @@
|
|||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.default-credentials {
|
|
||||||
background: #fff3cd;
|
|
||||||
border: 1px solid #ffeaa7;
|
|
||||||
color: #856404;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.default-credentials strong {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.login-container {
|
.login-container {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
@@ -199,12 +183,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<div class="default-credentials">
|
|
||||||
<strong>Default Login Credentials:</strong>
|
|
||||||
Username: admin<br>
|
|
||||||
Password: admin123
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
@@ -222,9 +200,6 @@
|
|||||||
|
|
||||||
<div class="login-footer">
|
<div class="login-footer">
|
||||||
<p>🔒 Secure QR Code Management System</p>
|
<p>🔒 Secure QR Code Management System</p>
|
||||||
<p style="margin-top: 5px; font-size: 0.8em; opacity: 0.7;">
|
|
||||||
Change default credentials in production
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ Utility modules for QR Code Manager
|
|||||||
|
|
||||||
from .auth import init_admin, login_required, verify_password, get_admin_credentials
|
from .auth import init_admin, login_required, verify_password, get_admin_credentials
|
||||||
from .qr_generator import QRCodeGenerator
|
from .qr_generator import QRCodeGenerator
|
||||||
from .link_manager import LinkPageManager, link_pages_db
|
from .link_manager import LinkPageManager
|
||||||
from .data_manager import QRDataManager, qr_codes_db
|
from .data_manager import QRDataManager
|
||||||
|
from .url_shortener import URLShortener
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'init_admin',
|
'init_admin',
|
||||||
@@ -14,7 +15,6 @@ __all__ = [
|
|||||||
'get_admin_credentials',
|
'get_admin_credentials',
|
||||||
'QRCodeGenerator',
|
'QRCodeGenerator',
|
||||||
'LinkPageManager',
|
'LinkPageManager',
|
||||||
'link_pages_db',
|
|
||||||
'QRDataManager',
|
'QRDataManager',
|
||||||
'qr_codes_db'
|
'URLShortener'
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,14 +3,39 @@ Data storage utilities for QR codes
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
import json
|
||||||
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
# In-memory storage for QR codes (in production, use a database)
|
# Data storage directory
|
||||||
qr_codes_db = {}
|
DATA_DIR = 'data'
|
||||||
|
QR_CODES_FILE = os.path.join(DATA_DIR, 'qr_codes.json')
|
||||||
|
|
||||||
|
# Ensure data directory exists
|
||||||
|
os.makedirs(DATA_DIR, exist_ok=True)
|
||||||
|
|
||||||
class QRDataManager:
|
class QRDataManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
self.qr_codes_db = self._load_qr_codes()
|
||||||
|
|
||||||
|
def _load_qr_codes(self):
|
||||||
|
"""Load QR codes from JSON file"""
|
||||||
|
try:
|
||||||
|
if os.path.exists(QR_CODES_FILE):
|
||||||
|
with open(QR_CODES_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading QR codes: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _save_qr_codes(self):
|
||||||
|
"""Save QR codes to JSON file"""
|
||||||
|
try:
|
||||||
|
with open(QR_CODES_FILE, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(self.qr_codes_db, f, indent=2, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving QR codes: {e}")
|
||||||
|
|
||||||
def save_qr_record(self, qr_type, content, settings, image_data, page_id=None):
|
def save_qr_record(self, qr_type, content, settings, image_data, page_id=None):
|
||||||
"""Save QR code record to database"""
|
"""Save QR code record to database"""
|
||||||
@@ -27,32 +52,45 @@ class QRDataManager:
|
|||||||
if page_id:
|
if page_id:
|
||||||
qr_record['page_id'] = page_id
|
qr_record['page_id'] = page_id
|
||||||
|
|
||||||
qr_codes_db[qr_id] = qr_record
|
self.qr_codes_db[qr_id] = qr_record
|
||||||
|
self._save_qr_codes() # Persist to file
|
||||||
return qr_id
|
return qr_id
|
||||||
|
|
||||||
def get_qr_record(self, qr_id):
|
def get_qr_record(self, qr_id):
|
||||||
"""Get QR code record"""
|
"""Get QR code record"""
|
||||||
return qr_codes_db.get(qr_id)
|
# Reload data from file to ensure we have the latest data
|
||||||
|
self.qr_codes_db = self._load_qr_codes()
|
||||||
|
return self.qr_codes_db.get(qr_id)
|
||||||
|
|
||||||
|
def get_qr_code(self, qr_id):
|
||||||
|
"""Get QR code record (alias for compatibility)"""
|
||||||
|
return self.get_qr_record(qr_id)
|
||||||
|
|
||||||
def delete_qr_record(self, qr_id):
|
def delete_qr_record(self, qr_id):
|
||||||
"""Delete QR code record"""
|
"""Delete QR code record"""
|
||||||
if qr_id in qr_codes_db:
|
if qr_id in self.qr_codes_db:
|
||||||
del qr_codes_db[qr_id]
|
del self.qr_codes_db[qr_id]
|
||||||
|
self._save_qr_codes() # Persist to file
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def list_qr_codes(self):
|
def list_qr_codes(self):
|
||||||
"""List all QR codes"""
|
"""List all QR codes"""
|
||||||
|
# Reload data from file to ensure we have the latest data
|
||||||
|
self.qr_codes_db = self._load_qr_codes()
|
||||||
qr_list = []
|
qr_list = []
|
||||||
for qr_id, qr_data in qr_codes_db.items():
|
for qr_id, qr_data in self.qr_codes_db.items():
|
||||||
qr_list.append({
|
qr_list.append({
|
||||||
'id': qr_id,
|
'id': qr_id,
|
||||||
'type': qr_data['type'],
|
'type': qr_data['type'],
|
||||||
'created_at': qr_data['created_at'],
|
'created_at': qr_data['created_at'],
|
||||||
'preview': f'data:image/png;base64,{qr_data["image_data"]}'
|
'preview': f'data:image/png;base64,{qr_data["image_data"]}',
|
||||||
|
'page_id': qr_data.get('page_id') # Include page_id if it exists
|
||||||
})
|
})
|
||||||
return qr_list
|
return qr_list
|
||||||
|
|
||||||
def qr_exists(self, qr_id):
|
def qr_exists(self, qr_id):
|
||||||
"""Check if QR code exists"""
|
"""Check if QR code exists"""
|
||||||
return qr_id in qr_codes_db
|
# Reload data from file to ensure we have the latest data
|
||||||
|
self.qr_codes_db = self._load_qr_codes()
|
||||||
|
return qr_id in self.qr_codes_db
|
||||||
|
|||||||
@@ -3,14 +3,41 @@ Dynamic Link Page Manager utilities
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
import json
|
||||||
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from .url_shortener import URLShortener
|
||||||
|
|
||||||
# In-memory storage for dynamic link pages (in production, use a database)
|
# Data storage directory
|
||||||
link_pages_db = {}
|
DATA_DIR = 'data'
|
||||||
|
LINK_PAGES_FILE = os.path.join(DATA_DIR, 'link_pages.json')
|
||||||
|
|
||||||
|
# Ensure data directory exists
|
||||||
|
os.makedirs(DATA_DIR, exist_ok=True)
|
||||||
|
|
||||||
class LinkPageManager:
|
class LinkPageManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
self.url_shortener = URLShortener()
|
||||||
|
self.link_pages_db = self._load_link_pages()
|
||||||
|
|
||||||
|
def _load_link_pages(self):
|
||||||
|
"""Load link pages from JSON file"""
|
||||||
|
try:
|
||||||
|
if os.path.exists(LINK_PAGES_FILE):
|
||||||
|
with open(LINK_PAGES_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading link pages: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _save_link_pages(self):
|
||||||
|
"""Save link pages to JSON file"""
|
||||||
|
try:
|
||||||
|
with open(LINK_PAGES_FILE, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(self.link_pages_db, f, indent=2, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving link pages: {e}")
|
||||||
|
|
||||||
def create_link_page(self, title="My Links", description="Collection of useful links"):
|
def create_link_page(self, title="My Links", description="Collection of useful links"):
|
||||||
"""Create a new dynamic link page"""
|
"""Create a new dynamic link page"""
|
||||||
@@ -22,34 +49,70 @@ class LinkPageManager:
|
|||||||
'links': [],
|
'links': [],
|
||||||
'created_at': datetime.now().isoformat(),
|
'created_at': datetime.now().isoformat(),
|
||||||
'updated_at': datetime.now().isoformat(),
|
'updated_at': datetime.now().isoformat(),
|
||||||
'view_count': 0
|
'view_count': 0,
|
||||||
|
'short_url': None, # Will be set when short URL is created
|
||||||
|
'short_code': None
|
||||||
}
|
}
|
||||||
link_pages_db[page_id] = page_data
|
self.link_pages_db[page_id] = page_data
|
||||||
|
self._save_link_pages() # Persist to file
|
||||||
return page_id
|
return page_id
|
||||||
|
|
||||||
def add_link(self, page_id, title, url, description=""):
|
def set_page_short_url(self, page_id, short_url, short_code):
|
||||||
"""Add a link to a page"""
|
"""Set the short URL for a link page"""
|
||||||
if page_id not in link_pages_db:
|
if page_id in self.link_pages_db:
|
||||||
|
self.link_pages_db[page_id]['short_url'] = short_url
|
||||||
|
self.link_pages_db[page_id]['short_code'] = short_code
|
||||||
|
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
||||||
|
self._save_link_pages() # Persist to file
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def add_link(self, page_id, title, url, description="", enable_shortener=False, custom_short_code=None):
|
||||||
|
"""Add a link to a page with optional URL shortening"""
|
||||||
|
if page_id not in self.link_pages_db:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Ensure URL has protocol
|
||||||
|
if not url.startswith(('http://', 'https://')):
|
||||||
|
url = f'https://{url}'
|
||||||
|
|
||||||
|
# Create the link data
|
||||||
link_data = {
|
link_data = {
|
||||||
'id': str(uuid.uuid4()),
|
'id': str(uuid.uuid4()),
|
||||||
'title': title,
|
'title': title,
|
||||||
'url': url if url.startswith(('http://', 'https://')) else f'https://{url}',
|
'url': url,
|
||||||
'description': description,
|
'description': description,
|
||||||
'created_at': datetime.now().isoformat()
|
'created_at': datetime.now().isoformat(),
|
||||||
|
'short_url': None,
|
||||||
|
'short_code': None,
|
||||||
|
'clicks': 0
|
||||||
}
|
}
|
||||||
|
|
||||||
link_pages_db[page_id]['links'].append(link_data)
|
# Generate short URL if enabled
|
||||||
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
if enable_shortener:
|
||||||
|
try:
|
||||||
|
short_result = self.url_shortener.create_short_url(
|
||||||
|
url,
|
||||||
|
custom_code=custom_short_code,
|
||||||
|
title=title
|
||||||
|
)
|
||||||
|
link_data['short_url'] = short_result['short_url']
|
||||||
|
link_data['short_code'] = short_result['short_code']
|
||||||
|
except Exception as e:
|
||||||
|
# If shortening fails, continue without it
|
||||||
|
print(f"URL shortening failed: {e}")
|
||||||
|
|
||||||
|
self.link_pages_db[page_id]['links'].append(link_data)
|
||||||
|
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
||||||
|
self._save_link_pages() # Persist to file
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def update_link(self, page_id, link_id, title=None, url=None, description=None):
|
def update_link(self, page_id, link_id, title=None, url=None, description=None, enable_shortener=None, custom_short_code=None):
|
||||||
"""Update a specific link"""
|
"""Update a specific link with optional URL shortening"""
|
||||||
if page_id not in link_pages_db:
|
if page_id not in self.link_pages_db:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for link in link_pages_db[page_id]['links']:
|
for link in self.link_pages_db[page_id]['links']:
|
||||||
if link['id'] == link_id:
|
if link['id'] == link_id:
|
||||||
if title is not None:
|
if title is not None:
|
||||||
link['title'] = title
|
link['title'] = title
|
||||||
@@ -58,29 +121,79 @@ class LinkPageManager:
|
|||||||
if description is not None:
|
if description is not None:
|
||||||
link['description'] = description
|
link['description'] = description
|
||||||
|
|
||||||
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
# Handle URL shortening update
|
||||||
|
if enable_shortener is not None:
|
||||||
|
if enable_shortener and not link.get('short_url'):
|
||||||
|
# Create new short URL
|
||||||
|
try:
|
||||||
|
short_result = self.url_shortener.create_short_url(
|
||||||
|
link['url'],
|
||||||
|
custom_code=custom_short_code,
|
||||||
|
title=link['title']
|
||||||
|
)
|
||||||
|
link['short_url'] = short_result['short_url']
|
||||||
|
link['short_code'] = short_result['short_code']
|
||||||
|
except Exception as e:
|
||||||
|
print(f"URL shortening failed: {e}")
|
||||||
|
elif not enable_shortener and link.get('short_code'):
|
||||||
|
# Remove short URL
|
||||||
|
if link.get('short_code'):
|
||||||
|
self.url_shortener.delete_url(link['short_code'])
|
||||||
|
link['short_url'] = None
|
||||||
|
link['short_code'] = None
|
||||||
|
|
||||||
|
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
||||||
|
self._save_link_pages() # Persist to file
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_link(self, page_id, link_id):
|
def delete_link(self, page_id, link_id):
|
||||||
"""Delete a specific link"""
|
"""Delete a specific link"""
|
||||||
if page_id not in link_pages_db:
|
if page_id not in self.link_pages_db:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
links = link_pages_db[page_id]['links']
|
links = self.link_pages_db[page_id]['links']
|
||||||
link_pages_db[page_id]['links'] = [link for link in links if link['id'] != link_id]
|
for link in links:
|
||||||
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
if link['id'] == link_id and link.get('short_code'):
|
||||||
|
# Delete the short URL if it exists
|
||||||
|
self.url_shortener.delete_url(link['short_code'])
|
||||||
|
|
||||||
|
self.link_pages_db[page_id]['links'] = [link for link in links if link['id'] != link_id]
|
||||||
|
self.link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
|
||||||
|
self._save_link_pages() # Persist to file
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def increment_view_count(self, page_id):
|
def increment_view_count(self, page_id):
|
||||||
"""Increment view count for a page"""
|
"""Increment view count for a page"""
|
||||||
if page_id in link_pages_db:
|
if page_id in self.link_pages_db:
|
||||||
link_pages_db[page_id]['view_count'] += 1
|
self.link_pages_db[page_id]['view_count'] += 1
|
||||||
|
self._save_link_pages() # Persist to file
|
||||||
|
|
||||||
def get_page(self, page_id):
|
def get_page(self, page_id):
|
||||||
"""Get page data"""
|
"""Get page data"""
|
||||||
return link_pages_db.get(page_id)
|
# Reload data from file to ensure we have the latest data
|
||||||
|
self.link_pages_db = self._load_link_pages()
|
||||||
|
return self.link_pages_db.get(page_id)
|
||||||
|
|
||||||
def page_exists(self, page_id):
|
def page_exists(self, page_id):
|
||||||
"""Check if page exists"""
|
"""Check if page exists"""
|
||||||
return page_id in link_pages_db
|
# Reload data from file to ensure we have the latest data
|
||||||
|
self.link_pages_db = self._load_link_pages()
|
||||||
|
return page_id in self.link_pages_db
|
||||||
|
|
||||||
|
# URL Shortener management methods
|
||||||
|
def create_standalone_short_url(self, url, title="", custom_code=None):
|
||||||
|
"""Create a standalone short URL (not tied to a link page)"""
|
||||||
|
return self.url_shortener.create_short_url(url, custom_code, title)
|
||||||
|
|
||||||
|
def get_short_url_stats(self, short_code):
|
||||||
|
"""Get statistics for a short URL"""
|
||||||
|
return self.url_shortener.get_url_stats(short_code)
|
||||||
|
|
||||||
|
def list_all_short_urls(self):
|
||||||
|
"""List all short URLs in the system"""
|
||||||
|
return self.url_shortener.list_urls()
|
||||||
|
|
||||||
|
def resolve_short_url(self, short_code):
|
||||||
|
"""Resolve a short URL to its original URL"""
|
||||||
|
return self.url_shortener.get_original_url(short_code)
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import os
|
|||||||
import qrcode
|
import qrcode
|
||||||
from qrcode.image.styledpil import StyledPilImage
|
from qrcode.image.styledpil import StyledPilImage
|
||||||
from qrcode.image.styles.moduledrawers import RoundedModuleDrawer, CircleModuleDrawer, SquareModuleDrawer
|
from qrcode.image.styles.moduledrawers import RoundedModuleDrawer, CircleModuleDrawer, SquareModuleDrawer
|
||||||
|
from qrcode.image.svg import SvgPathImage, SvgFragmentImage, SvgFillImage
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
import io
|
||||||
|
|
||||||
class QRCodeGenerator:
|
class QRCodeGenerator:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -19,8 +21,8 @@ class QRCodeGenerator:
|
|||||||
'style': 'square'
|
'style': 'square'
|
||||||
}
|
}
|
||||||
|
|
||||||
def generate_qr_code(self, data, settings=None):
|
def generate_qr_code(self, data, settings=None, format='PNG'):
|
||||||
"""Generate QR code with custom settings"""
|
"""Generate QR code with custom settings in PNG or SVG format"""
|
||||||
if settings is None:
|
if settings is None:
|
||||||
settings = self.default_settings.copy()
|
settings = self.default_settings.copy()
|
||||||
else:
|
else:
|
||||||
@@ -28,6 +30,13 @@ class QRCodeGenerator:
|
|||||||
merged_settings.update(settings)
|
merged_settings.update(settings)
|
||||||
settings = merged_settings
|
settings = merged_settings
|
||||||
|
|
||||||
|
if format.upper() == 'SVG':
|
||||||
|
return self._generate_svg_qr_code(data, settings)
|
||||||
|
else:
|
||||||
|
return self._generate_png_qr_code(data, settings)
|
||||||
|
|
||||||
|
def _generate_png_qr_code(self, data, settings):
|
||||||
|
"""Generate PNG QR code (existing functionality)"""
|
||||||
# Create QR code instance
|
# Create QR code instance
|
||||||
qr = qrcode.QRCode(
|
qr = qrcode.QRCode(
|
||||||
version=1,
|
version=1,
|
||||||
@@ -64,6 +73,47 @@ class QRCodeGenerator:
|
|||||||
|
|
||||||
return img
|
return img
|
||||||
|
|
||||||
|
def _generate_svg_qr_code(self, data, settings):
|
||||||
|
"""Generate SVG QR code"""
|
||||||
|
# Create QR code instance
|
||||||
|
qr = qrcode.QRCode(
|
||||||
|
version=1,
|
||||||
|
error_correction=settings['error_correction'],
|
||||||
|
box_size=settings['size'],
|
||||||
|
border=settings['border'],
|
||||||
|
)
|
||||||
|
|
||||||
|
qr.add_data(data)
|
||||||
|
qr.make(fit=True)
|
||||||
|
|
||||||
|
# Choose SVG image factory based on style
|
||||||
|
if settings['style'] == 'circle':
|
||||||
|
# Use SvgFillImage for better circle support
|
||||||
|
factory = SvgFillImage
|
||||||
|
else:
|
||||||
|
# Use SvgPathImage for square and rounded styles
|
||||||
|
factory = SvgPathImage
|
||||||
|
|
||||||
|
# Generate SVG image
|
||||||
|
img = qr.make_image(
|
||||||
|
image_factory=factory,
|
||||||
|
fill_color=settings['foreground_color'],
|
||||||
|
back_color=settings['background_color']
|
||||||
|
)
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
def generate_qr_code_svg_string(self, data, settings=None):
|
||||||
|
"""Generate QR code as SVG string"""
|
||||||
|
svg_img = self.generate_qr_code(data, settings, format='SVG')
|
||||||
|
|
||||||
|
# Convert SVG image to string
|
||||||
|
svg_buffer = io.BytesIO()
|
||||||
|
svg_img.save(svg_buffer)
|
||||||
|
svg_buffer.seek(0)
|
||||||
|
|
||||||
|
return svg_buffer.getvalue().decode('utf-8')
|
||||||
|
|
||||||
def add_logo(self, qr_img, logo_path, logo_size_ratio=0.2):
|
def add_logo(self, qr_img, logo_path, logo_size_ratio=0.2):
|
||||||
"""Add logo to QR code"""
|
"""Add logo to QR code"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
122
app/utils/url_shortener.py
Normal file
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
|
- qr_data:/app/app/static/qr_codes
|
||||||
- logo_data:/app/app/static/logos
|
- logo_data:/app/app/static/logos
|
||||||
- session_data:/app/flask_session
|
- session_data:/app/flask_session
|
||||||
|
- persistent_data:/app/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:5000/health')"]
|
test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:5000/health')"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -33,6 +34,8 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
session_data:
|
session_data:
|
||||||
driver: local
|
driver: local
|
||||||
|
persistent_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
|
|||||||
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
|
#!/usr/bin/env python3
|
||||||
"""
|
""" print("🚀 QR Code Manager - Production Mode")
|
||||||
QR Code Manager - Main Application Entry Point
|
print("ℹ️ This should be run with Gunicorn in production!")
|
||||||
|
print("🔧 Use: gunicorn -c gunicorn.conf.py main:app") Code Manager - Main Application Entry Point
|
||||||
|
|
||||||
A modern Flask web application for generating and managing QR codes with authentication.
|
A modern Flask web application for generating and managing QR codes with authentication.
|
||||||
Features include:
|
Features include:
|
||||||
- Multiple QR code types (text, URL, WiFi, email, SMS, vCard)
|
- Multiple QR code types (text, URL, WiFi, email, SMS, vCard)
|
||||||
- Dynamic link pages for managing collections of links
|
- Dynamic link pages for managing collections of links
|
||||||
|
- URL shortener functionality with custom domains
|
||||||
- Admin authentication with bcrypt password hashing
|
- Admin authentication with bcrypt password hashing
|
||||||
- Docker deployment ready
|
- Docker deployment ready
|
||||||
- Modern responsive web interface
|
- Modern responsive web interface
|
||||||
@@ -20,14 +22,20 @@ app = create_app()
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Production vs Development configuration
|
# Production vs Development configuration
|
||||||
is_production = os.environ.get('FLASK_ENV') == 'production'
|
is_production = os.environ.get('FLASK_ENV') == 'production'
|
||||||
|
app_domain = os.environ.get('APP_DOMAIN', 'localhost:5000')
|
||||||
|
|
||||||
if is_production:
|
if is_production:
|
||||||
print("🚀 Starting QR Code Manager in PRODUCTION mode")
|
print("🚀 QR Code Manager - Production Mode")
|
||||||
print("🔐 Admin user: admin")
|
print("ℹ️ This should be run with Gunicorn in production!")
|
||||||
print("🔒 Make sure to change default credentials!")
|
print("<EFBFBD> Use: gunicorn -c gunicorn.conf.py main:app")
|
||||||
app.run(host='0.0.0.0', port=5000, debug=False)
|
print(f"🌐 Domain configured: {app_domain}")
|
||||||
|
print("🔗 URL shortener available at: /s/")
|
||||||
|
# In production, this file is used by Gunicorn as WSGI application
|
||||||
|
# The Flask dev server should NOT be started in production
|
||||||
else:
|
else:
|
||||||
print("🛠️ Starting QR Code Manager in DEVELOPMENT mode")
|
print("🛠️ Starting QR Code Manager in DEVELOPMENT mode")
|
||||||
print("🔐 Admin user: admin")
|
print("🔐 Admin user: admin")
|
||||||
print("🔑 Default password: admin123")
|
print("🔑 Default password: admin123")
|
||||||
|
print(f"🌐 Domain configured: {app_domain}")
|
||||||
|
print("🔗 URL shortener available at: /s/")
|
||||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
|
|||||||
@@ -7,3 +7,5 @@ requests==2.31.0
|
|||||||
flask-session==0.5.0
|
flask-session==0.5.0
|
||||||
werkzeug==2.3.7
|
werkzeug==2.3.7
|
||||||
bcrypt==4.0.1
|
bcrypt==4.0.1
|
||||||
|
lxml==4.9.3
|
||||||
|
gunicorn==21.2.0
|
||||||
|
|||||||
Reference in New Issue
Block a user