Compare commits

...

2 Commits

Author SHA1 Message Date
b94d2ebbd6 final stage of the app 2025-07-15 14:32:57 +03:00
94f006d458 updated to correct frature 2025-07-15 13:55:52 +03:00
27 changed files with 2146 additions and 523 deletions

76
.dockerignore Normal file
View File

@@ -0,0 +1,76 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.pyc
*.pyo
*.pyd
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# IDEs
.vscode/
.idea/
*.swp
*.swo
.DS_Store
Thumbs.db
# Git
.git/
.gitignore
# Documentation
README.md
*.md
docs/
# Test files
test_*.py
*_test.py
tests/
# Logs
*.log
logs/
# Temporary files
*.tmp
*.temp
.cache/
# Development files
.env.example
docker-compose.override.yml
# Generated QR codes and uploads (will be in volumes)
static/qr_codes/*.png
static/logos/*
!static/qr_codes/.gitkeep
!static/logos/.gitkeep
# Session data
flask_session/

24
.env.example Normal file
View File

@@ -0,0 +1,24 @@
# QR Code Manager Environment Configuration
# Copy this file to .env and customize as needed
# Security Settings
SECRET_KEY=your-super-secret-key-change-me-in-production
ADMIN_USERNAME=admin
ADMIN_PASSWORD=admin123
# Flask Configuration
FLASK_ENV=production
FLASK_DEBUG=false
# Server Configuration
HOST=0.0.0.0
PORT=5000
# Upload Configuration
MAX_CONTENT_LENGTH=16777216
# Default QR Code Settings
DEFAULT_QR_SIZE=10
DEFAULT_QR_BORDER=4
DEFAULT_FOREGROUND_COLOR=#000000
DEFAULT_BACKGROUND_COLOR=#FFFFFF

49
Dockerfile Normal file
View File

@@ -0,0 +1,49 @@
# Use Python 3.11 slim image
FROM python:3.11-slim
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
libjpeg-dev \
zlib1g-dev \
libfreetype6-dev \
liblcms2-dev \
libopenjp2-7-dev \
libtiff5-dev \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first for better caching
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY main.py .
COPY app/ ./app/
# Create necessary directories
RUN mkdir -p app/static/qr_codes app/static/logos flask_session
# Set environment variables
ENV FLASK_APP=main.py
ENV FLASK_ENV=production
ENV PYTHONUNBUFFERED=1
# Create non-root user for security
RUN useradd --create-home --shell /bin/bash app && \
chown -R app:app /app
USER app
# Expose port
EXPOSE 5000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:5000/health')" || exit 1
# Run the application
CMD ["python", "main.py"]

357
README.md
View File

@@ -1,189 +1,214 @@
# QR Code Manager
A comprehensive Python web application for creating, customizing, and managing QR codes. This application provides functionality similar to popular QR code generation websites with additional features for local management.
A modern Flask web application for generating and managing QR codes with authentication and dynamic link pages.
## Features
## 🚀 Features
### QR Code Types Supported
- **Text**: Plain text QR codes
- **URL/Website**: Direct links to websites
- **WiFi**: WiFi network connection details
- **Email**: Pre-filled email composition
- **Phone**: Direct phone number dialing
- **SMS**: Pre-filled SMS messages
- **vCard**: Digital contact cards
- **Multiple QR Code Types**: Text, URL, WiFi, Email, SMS, vCard
- **Dynamic Link Pages**: Create collections of links accessible via QR codes
- **Admin Authentication**: Secure login with bcrypt password hashing
- **Customizable Styling**: Different QR code styles (square, rounded, circle)
- **Logo Integration**: Add custom logos to QR codes
- **Docker Deployment**: Production-ready containerization
- **Responsive Design**: Modern web interface that works on all devices
### Customization Options
- **Colors**: Customizable foreground and background colors
- **Styles**: Square, rounded, or circular module styles
- **Size**: Adjustable module size (5-15px per module)
- **Logo**: Option to add custom logos to QR codes
### Management Features
- **Preview**: Real-time preview of generated QR codes
- **Download**: Export QR codes as PNG images
- **History**: View and manage previously generated QR codes
- **Delete**: Remove unwanted QR codes
- **Copy**: Copy QR codes to clipboard
## Installation
1. **Clone or navigate to the project directory**:
```bash
cd /home/pi/Desktop/qr-code_manager
```
2. **Install dependencies**:
```bash
pip install -r requirements.txt
```
Or if using the virtual environment that was created:
```bash
/home/pi/Desktop/qr-code_manager/.venv/bin/pip install -r requirements.txt
```
## Usage
1. **Start the server**:
```bash
python app.py
```
Or with the virtual environment:
```bash
/home/pi/Desktop/qr-code_manager/.venv/bin/python app.py
```
2. **Access the application**:
Open your web browser and navigate to `http://localhost:5000`
3. **Create QR codes**:
- Select the type of QR code you want to create
- Fill in the required information
- Customize the appearance (colors, style, size)
- Click "Generate QR Code"
- Download or copy your QR code
## API Endpoints
The application provides a RESTful API for programmatic access:
### Generate QR Code
- **POST** `/api/generate`
- **Body**: JSON with QR code parameters
- **Response**: Generated QR code data and download URL
### Download QR Code
- **GET** `/api/download/<qr_id>`
- **Response**: PNG image file
### List QR Codes
- **GET** `/api/qr_codes`
- **Response**: JSON array of all generated QR codes
### Get QR Code Details
- **GET** `/api/qr_codes/<qr_id>`
- **Response**: JSON with QR code details
### Delete QR Code
- **DELETE** `/api/qr_codes/<qr_id>`
- **Response**: Success/error status
### Upload Logo
- **POST** `/api/upload_logo`
- **Body**: Multipart form with logo file
- **Response**: Logo path for use in QR generation
## Example API Usage
### Generate a URL QR Code
```bash
curl -X POST http://localhost:5000/api/generate \
-H "Content-Type: application/json" \
-d '{
"type": "url",
"content": "https://github.com",
"foreground_color": "#000000",
"background_color": "#FFFFFF",
"style": "square",
"size": 10
}'
```
### Generate a WiFi QR Code
```bash
curl -X POST http://localhost:5000/api/generate \
-H "Content-Type: application/json" \
-d '{
"type": "wifi",
"wifi": {
"ssid": "MyNetwork",
"password": "MyPassword",
"security": "WPA"
},
"foreground_color": "#0066cc",
"background_color": "#ffffff"
}'
```
## File Structure
## 📁 Project Structure
```
qr-code_manager/
├── app.py # Main Flask application
├── requirements.txt # Python dependencies
├── README.md # This file
├── templates/
│ └── index.html # Web interface
├── static/
│ ├── qr_codes/ # Generated QR code images
│ └── logos/ # Uploaded logo files
└── .venv/ # Virtual environment (if created)
├── README.md # Project documentation
├── main.py # Application entry point
├── Dockerfile # Docker container definition
├── docker-compose.yml # Docker orchestration
├── requirements.txt # Python dependencies
├── .env.example # Environment variables template
├── deploy.sh # Deployment script
├── app/ # Main application package
│ ├── __init__.py # Flask app factory
│ ├── templates/ # Jinja2 templates
│ │ ├── index.html # Main dashboard
│ │ ├── login.html # Authentication page
│ │ ├── link_page.html # Public link display
│ │ └── edit_links.html # Link management interface
│ ├── static/ # Static files
│ │ ├── qr_codes/ # Generated QR code images
│ │ └── logos/ # Uploaded logo files
│ ├── routes/ # Route handlers
│ │ ├── __init__.py # Route package
│ │ ├── main.py # Main page routes
│ │ ├── auth.py # Authentication routes
│ │ └── api.py # API endpoints
│ └── utils/ # Utility modules
│ ├── __init__.py # Utils package
│ ├── auth.py # Authentication utilities
│ ├── qr_generator.py # QR code generation
│ ├── link_manager.py # Dynamic link management
│ └── data_manager.py # Data storage utilities
```
## Dependencies
## 🛠️ Quick Start
- **Flask**: Web framework
- **qrcode[pil]**: QR code generation with PIL support
- **Pillow**: Image processing
- **flask-cors**: Cross-Origin Resource Sharing support
- **python-dotenv**: Environment variable management
### Method 1: Docker (Recommended)
## Security Considerations
1. **Clone and navigate to the project:**
```bash
git clone <your-repo-url>
cd qr-code_manager
```
- The application runs in debug mode by default - disable for production
- File uploads are stored locally - implement proper validation and storage limits
- QR codes are stored in memory - consider using a database for persistence
- The server accepts connections from all interfaces (0.0.0.0) - restrict for production
2. **Create environment file:**
```bash
cp .env.example .env
# Edit .env with your preferred settings
```
## Customization
3. **Deploy with Docker:**
```bash
./deploy.sh
```
### Adding New QR Code Types
1. Add the new type to the HTML select options
2. Create corresponding form fields in the HTML
3. Add processing logic in the `/api/generate` endpoint
4. Update the JavaScript to handle the new type
4. **Access the application:**
- Open http://localhost:5000
- Login with: admin / admin123 (change in production!)
### Styling
The application uses embedded CSS for easy customization. Modify the `<style>` section in `templates/index.html` to change the appearance.
### Method 2: Local Development
### Storage
Currently uses in-memory storage. To persist data:
1. Install a database library (SQLite, PostgreSQL, etc.)
2. Replace the `qr_codes_db` dictionary with database operations
3. Add database initialization code
1. **Set up Python environment:**
```bash
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
```
## Troubleshooting
2. **Install dependencies:**
```bash
pip install -r requirements.txt
```
3. **Run the application:**
```bash
python main.py
```
## 🔐 Authentication
- **Default Credentials**: admin / admin123
- **Environment Variables**:
- `ADMIN_USERNAME`: Set custom admin username
- `ADMIN_PASSWORD`: Set custom admin password
- `SECRET_KEY`: Set Flask secret key for sessions
## 🐳 Docker Deployment
The application is fully containerized with Docker:
- **Build and run**: `docker-compose up -d`
- **View logs**: `docker-compose logs -f`
- **Stop**: `docker-compose down`
- **Full rebuild**: `docker-compose up --build -d`
### Production Configuration
1. **Set environment variables in .env:**
```bash
FLASK_ENV=production
SECRET_KEY=your-super-secret-key-here
ADMIN_USERNAME=your-admin-username
ADMIN_PASSWORD=your-secure-password
```
2. **Deploy:**
```bash
docker-compose up -d
```
## 📱 Usage
### Generating QR Codes
1. **Login** to the admin interface
2. **Select QR type**: Text, URL, WiFi, Email, SMS, or vCard
3. **Fill in the details** for your chosen type
4. **Customize appearance**: Size, colors, style, border
5. **Add logo** (optional): Upload custom logo for branding
6. **Generate and download** your QR code
### Dynamic Link Pages
1. **Create a link page** from the main interface
2. **Add links** to your collection via the edit interface
3. **Share the QR code** that points to your link page
4. **Update links anytime** without changing the QR code
## 🛡️ Security Features
- **Password Hashing**: Uses bcrypt for secure password storage
- **Session Management**: Secure Flask sessions with signing
- **Authentication Required**: All admin functions require login
- **Docker Security**: Non-root user in container
- **Environment Variables**: Sensitive data via environment configuration
## 🔧 Development
### Project Architecture
The application follows a modular Flask structure:
- **App Factory Pattern**: Clean application initialization
- **Blueprint Organization**: Separate route modules for different features
- **Utility Modules**: Reusable components for QR generation, auth, etc.
- **Template Organization**: Structured Jinja2 templates
- **Static File Management**: Organized asset storage
### Adding New Features
1. **New Routes**: Add to appropriate blueprint in `app/routes/`
2. **New Utilities**: Create modules in `app/utils/`
3. **New Templates**: Add to `app/templates/`
4. **New Dependencies**: Update `requirements.txt`
## 📊 API Endpoints
- `POST /api/generate` - Generate QR code
- `GET /api/qr_codes` - List all QR codes
- `GET /api/qr_codes/{id}` - Get specific QR code
- `DELETE /api/qr_codes/{id}` - Delete QR code
- `POST /api/create_link_page` - Create dynamic link page
- `POST /api/link_pages/{id}/links` - Add link to page
- `PUT /api/link_pages/{id}/links/{link_id}` - Update link
- `DELETE /api/link_pages/{id}/links/{link_id}` - Delete link
## 🚨 Troubleshooting
### Common Issues
1. **Port already in use**: Change the port in `app.py` or stop the conflicting service
2. **Permission errors**: Ensure the application has write permissions to the static directories
3. **Missing dependencies**: Reinstall requirements with `pip install -r requirements.txt`
### Debug Mode
The application runs in debug mode, which provides detailed error messages and auto-reload functionality. Disable for production by setting `debug=False` in the `app.run()` call.
1. **Permission Denied**: Ensure Docker has proper permissions
2. **Port 5000 in use**: Change port in docker-compose.yml
3. **Authentication Failed**: Check admin credentials in .env
4. **Image Generation Failed**: Verify PIL/Pillow installation
## License
### Health Check
This project is open source. Feel free to modify and distribute according to your needs.
Visit `/health` to check application status:
```bash
curl http://localhost:5000/health
```
## 📝 License
This project is licensed under the MIT License - see the LICENSE file for details.
## 🤝 Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests if applicable
5. Submit a pull request
## 📞 Support
For support, please open an issue on GitHub or contact the development team.
---
**Made with ❤️ for easy QR code management**

37
app/__init__.py Normal file
View File

@@ -0,0 +1,37 @@
"""
QR Code Manager Flask Application
A modern Flask web application for generating and managing QR codes with authentication
"""
import os
from flask import Flask
from flask_cors import CORS
from flask_session import Session
from app.utils.auth import init_admin
def create_app():
"""Create and configure the Flask application"""
app = Flask(__name__)
# Configuration
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'your-secret-key-change-in-production')
app.config['SESSION_TYPE'] = 'filesystem'
app.config['SESSION_PERMANENT'] = False
app.config['SESSION_USE_SIGNER'] = True
# Initialize CORS
CORS(app)
# Initialize session
Session(app)
# Initialize admin user
init_admin()
# Register blueprints
from app.routes import main, api, auth
app.register_blueprint(main.bp)
app.register_blueprint(api.bp, url_prefix='/api')
app.register_blueprint(auth.bp)
return app

7
app/routes/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
"""
Route modules for QR Code Manager
"""
from . import main, api, auth
__all__ = ['main', 'api', 'auth']

View File

@@ -1,123 +1,33 @@
"""
API routes for QR Code Manager
"""
import os
import io
import base64
import json
import uuid
from datetime import datetime
from PIL import Image, ImageDraw
import qrcode
from qrcode.image.styledpil import StyledPilImage
from qrcode.image.styles.moduledrawers import RoundedModuleDrawer, CircleModuleDrawer, SquareModuleDrawer
from qrcode.image.styles.colorfills import SolidFillColorMask
from flask import Flask, request, jsonify, send_file, render_template
from flask_cors import CORS
from flask import Blueprint, request, jsonify, send_file
from app.utils.auth import login_required
from app.utils.qr_generator import QRCodeGenerator
from app.utils.link_manager import LinkPageManager
from app.utils.data_manager import QRDataManager
app = Flask(__name__)
CORS(app)
bp = Blueprint('api', __name__)
# Configuration
UPLOAD_FOLDER = 'static/qr_codes'
LOGOS_FOLDER = 'static/logos'
# Initialize managers
qr_generator = QRCodeGenerator()
link_manager = LinkPageManager()
data_manager = QRDataManager()
# Configuration for file uploads
UPLOAD_FOLDER = 'app/static/qr_codes'
LOGOS_FOLDER = 'app/static/logos'
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(LOGOS_FOLDER, exist_ok=True)
# In-memory storage for QR codes (in production, use a database)
qr_codes_db = {}
class QRCodeGenerator:
def __init__(self):
self.default_settings = {
'size': 10,
'border': 4,
'error_correction': qrcode.constants.ERROR_CORRECT_M,
'foreground_color': '#000000',
'background_color': '#FFFFFF',
'style': 'square'
}
def generate_qr_code(self, data, settings=None):
"""Generate QR code with custom settings"""
if settings is None:
settings = self.default_settings.copy()
else:
merged_settings = self.default_settings.copy()
merged_settings.update(settings)
settings = merged_settings
# 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 module drawer based on style
module_drawer = None
if settings['style'] == 'rounded':
module_drawer = RoundedModuleDrawer()
elif settings['style'] == 'circle':
module_drawer = CircleModuleDrawer()
else:
module_drawer = SquareModuleDrawer()
# Create color mask
color_mask = SolidFillColorMask(
back_color=settings['background_color'],
front_color=settings['foreground_color']
)
# Generate the image
img = qr.make_image(
image_factory=StyledPilImage,
module_drawer=module_drawer,
color_mask=color_mask
)
return img
def add_logo(self, qr_img, logo_path, logo_size_ratio=0.2):
"""Add logo to QR code"""
try:
logo = Image.open(logo_path)
# Calculate logo size
qr_width, qr_height = qr_img.size
logo_size = int(min(qr_width, qr_height) * logo_size_ratio)
# Resize logo
logo = logo.resize((logo_size, logo_size), Image.Resampling.LANCZOS)
# Create a white background for the logo
logo_bg = Image.new('RGB', (logo_size + 20, logo_size + 20), 'white')
logo_bg.paste(logo, (10, 10))
# Calculate position to center the logo
logo_pos = (
(qr_width - logo_bg.width) // 2,
(qr_height - logo_bg.height) // 2
)
# Paste logo onto QR code
qr_img.paste(logo_bg, logo_pos)
return qr_img
except Exception as e:
print(f"Error adding logo: {e}")
return qr_img
# Initialize QR code generator
qr_generator = QRCodeGenerator()
@app.route('/')
def index():
"""Serve the main page"""
return render_template('index.html')
@app.route('/api/generate', methods=['POST'])
@bp.route('/generate', methods=['POST'])
@login_required
def generate_qr():
"""Generate QR code API endpoint"""
try:
@@ -178,16 +88,7 @@ END:VCARD"""
img_base64 = base64.b64encode(img_buffer.getvalue()).decode()
# Save QR code record
qr_id = str(uuid.uuid4())
qr_record = {
'id': qr_id,
'type': qr_type,
'content': qr_content,
'settings': settings,
'created_at': datetime.now().isoformat(),
'image_data': img_base64
}
qr_codes_db[qr_id] = qr_record
qr_id = data_manager.save_qr_record(qr_type, qr_content, settings, img_base64)
# Save image file
img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png')
@@ -203,7 +104,8 @@ END:VCARD"""
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/download/<qr_id>')
@bp.route('/download/<qr_id>')
@login_required
def download_qr(qr_id):
"""Download QR code"""
try:
@@ -215,34 +117,30 @@ def download_qr(qr_id):
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/qr_codes')
@bp.route('/qr_codes')
@login_required
def list_qr_codes():
"""List all generated QR codes"""
qr_list = []
for qr_id, qr_data in qr_codes_db.items():
qr_list.append({
'id': qr_id,
'type': qr_data['type'],
'created_at': qr_data['created_at'],
'preview': f'data:image/png;base64,{qr_data["image_data"]}'
})
return jsonify(qr_list)
return jsonify(data_manager.list_qr_codes())
@app.route('/api/qr_codes/<qr_id>')
@bp.route('/qr_codes/<qr_id>')
@login_required
def get_qr_code(qr_id):
"""Get specific QR code details"""
if qr_id in qr_codes_db:
return jsonify(qr_codes_db[qr_id])
qr_data = data_manager.get_qr_record(qr_id)
if qr_data:
return jsonify(qr_data)
else:
return jsonify({'error': 'QR code not found'}), 404
@app.route('/api/qr_codes/<qr_id>', methods=['DELETE'])
@bp.route('/qr_codes/<qr_id>', methods=['DELETE'])
@login_required
def delete_qr_code(qr_id):
"""Delete QR code"""
try:
if qr_id in qr_codes_db:
if data_manager.qr_exists(qr_id):
# Remove from database
del qr_codes_db[qr_id]
data_manager.delete_qr_record(qr_id)
# Remove image file
img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png')
@@ -255,7 +153,8 @@ def delete_qr_code(qr_id):
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/upload_logo', methods=['POST'])
@bp.route('/upload_logo', methods=['POST'])
@login_required
def upload_logo():
"""Upload logo for QR code"""
try:
@@ -282,5 +181,124 @@ def upload_logo():
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)
# Dynamic Link Pages API Routes
@bp.route('/create_link_page', methods=['POST'])
@login_required
def create_link_page():
"""Create a new dynamic link page and QR code"""
try:
data = request.json
title = data.get('title', 'My Links')
description = data.get('description', 'Collection of useful links')
# Create the link page
page_id = link_manager.create_link_page(title, description)
# Create QR code pointing to the link page
page_url = f"{request.url_root}links/{page_id}"
settings = {
'size': data.get('size', 10),
'border': data.get('border', 4),
'foreground_color': data.get('foreground_color', '#000000'),
'background_color': data.get('background_color', '#FFFFFF'),
'style': data.get('style', 'square')
}
# Generate QR code
qr_img = qr_generator.generate_qr_code(page_url, settings)
# Convert to base64
img_buffer = io.BytesIO()
qr_img.save(img_buffer, format='PNG')
img_buffer.seek(0)
img_base64 = base64.b64encode(img_buffer.getvalue()).decode()
# Save QR code record
qr_id = data_manager.save_qr_record('link_page', page_url, settings, img_base64, page_id)
# Save image file
img_path = os.path.join(UPLOAD_FOLDER, f'{qr_id}.png')
qr_img.save(img_path)
return jsonify({
'success': True,
'qr_id': qr_id,
'page_id': page_id,
'page_url': page_url,
'edit_url': f"{request.url_root}edit/{page_id}",
'image_data': f'data:image/png;base64,{img_base64}',
'download_url': f'/api/download/{qr_id}'
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/link_pages/<page_id>/links', methods=['POST'])
@login_required
def add_link_to_page(page_id):
"""Add a link to a page"""
try:
data = request.json
title = data.get('title', '')
url = data.get('url', '')
description = data.get('description', '')
if not title or not url:
return jsonify({'error': 'Title and URL are required'}), 400
success = link_manager.add_link(page_id, title, url, description)
if success:
return jsonify({'success': True})
else:
return jsonify({'error': 'Page not found'}), 404
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/link_pages/<page_id>/links/<link_id>', methods=['PUT'])
@login_required
def update_link_in_page(page_id, link_id):
"""Update a link in a page"""
try:
data = request.json
title = data.get('title')
url = data.get('url')
description = data.get('description')
success = link_manager.update_link(page_id, link_id, title, url, description)
if success:
return jsonify({'success': True})
else:
return jsonify({'error': 'Page or link not found'}), 404
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/link_pages/<page_id>/links/<link_id>', methods=['DELETE'])
@login_required
def delete_link_from_page(page_id, link_id):
"""Delete a link from a page"""
try:
success = link_manager.delete_link(page_id, link_id)
if success:
return jsonify({'success': True})
else:
return jsonify({'error': 'Page or link not found'}), 404
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/link_pages/<page_id>')
@login_required
def get_link_page(page_id):
"""Get link page data"""
page_data = link_manager.get_page(page_id)
if page_data:
return jsonify(page_data)
else:
return jsonify({'error': 'Page not found'}), 404

34
app/routes/auth.py Normal file
View File

@@ -0,0 +1,34 @@
"""
Authentication routes for QR Code Manager
"""
from flask import Blueprint, request, render_template, session, redirect, url_for, flash
from app.utils.auth import get_admin_credentials, verify_password
bp = Blueprint('auth', __name__)
@bp.route('/login', methods=['GET', 'POST'])
def login():
"""Login page"""
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
admin_username, admin_password_hash = get_admin_credentials()
if username == admin_username and verify_password(password, admin_password_hash):
session['logged_in'] = True
session['username'] = username
flash('Successfully logged in!', 'success')
return redirect(url_for('main.index'))
else:
flash('Invalid username or password!', 'error')
return render_template('login.html')
@bp.route('/logout')
def logout():
"""Logout and clear session"""
session.clear()
flash('You have been logged out!', 'info')
return redirect(url_for('auth.login'))

46
app/routes/main.py Normal file
View File

@@ -0,0 +1,46 @@
"""
Main routes for QR Code Manager
"""
from flask import Blueprint, render_template
from app.utils.auth import login_required
from app.utils.link_manager import LinkPageManager
bp = Blueprint('main', __name__)
# Initialize manager
link_manager = LinkPageManager()
@bp.route('/')
@login_required
def index():
"""Serve the main page"""
return render_template('index.html')
@bp.route('/links/<page_id>')
def view_link_page(page_id):
"""Display the public link page"""
if not link_manager.page_exists(page_id):
return "Page not found", 404
link_manager.increment_view_count(page_id)
page_data = link_manager.get_page(page_id)
return render_template('link_page.html', page=page_data)
@bp.route('/edit/<page_id>')
@login_required
def edit_link_page(page_id):
"""Display the edit interface for the link page"""
if not link_manager.page_exists(page_id):
return "Page not found", 404
page_data = link_manager.get_page(page_id)
return render_template('edit_links.html', page=page_data)
@bp.route('/health')
def health_check():
"""Health check endpoint for Docker"""
from datetime import datetime
from flask import jsonify
return jsonify({'status': 'healthy', 'timestamp': datetime.now().isoformat()})

View File

@@ -0,0 +1,454 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit {{ page.title }}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.2em;
margin-bottom: 10px;
}
.header p {
font-size: 1em;
opacity: 0.9;
}
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
padding: 30px;
}
.form-section {
background: #f8f9fa;
padding: 25px;
border-radius: 10px;
border: 1px solid #e9ecef;
}
.form-section h2 {
color: #333;
margin-bottom: 20px;
font-size: 1.5em;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #555;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 12px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #667eea;
}
.form-group textarea {
height: 80px;
resize: vertical;
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px 25px;
border: none;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
transition: transform 0.2s;
margin-right: 10px;
margin-bottom: 10px;
}
.btn:hover {
transform: translateY(-2px);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-success {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
}
.btn-danger {
background: linear-gradient(135deg, #dc3545 0%, #fd7e14 100%);
}
.btn-secondary {
background: linear-gradient(135deg, #6c757d 0%, #adb5bd 100%);
}
.links-section h2 {
margin-bottom: 20px;
}
.link-item {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
margin-bottom: 15px;
}
.link-item.editing {
border-color: #667eea;
}
.link-display {
display: block;
}
.link-edit {
display: none;
}
.link-item.editing .link-display {
display: none;
}
.link-item.editing .link-edit {
display: block;
}
.link-title {
font-size: 1.2em;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.link-description {
color: #666;
font-size: 0.9em;
margin-bottom: 8px;
}
.link-url {
color: #667eea;
font-size: 0.9em;
word-break: break-all;
}
.link-actions {
margin-top: 15px;
display: flex;
gap: 10px;
}
.btn-small {
padding: 8px 15px;
font-size: 12px;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #666;
background: white;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.empty-state .icon {
font-size: 3em;
margin-bottom: 15px;
opacity: 0.5;
}
.page-actions {
background: #f8f9fa;
padding: 20px;
text-align: center;
border-top: 1px solid #e9ecef;
display: flex;
justify-content: center;
gap: 15px;
}
.alert {
padding: 12px 20px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
}
.alert-success {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.alert-error {
background: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
@media (max-width: 768px) {
.main-content {
grid-template-columns: 1fr;
}
.page-actions {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>✏️ Edit Links</h1>
<p>Manage your link collection: {{ page.title }}</p>
</div>
<div class="alert alert-success" id="success-alert">
Operation completed successfully!
</div>
<div class="alert alert-error" id="error-alert">
An error occurred. Please try again.
</div>
<div class="main-content">
<div class="form-section">
<h2>Add New Link</h2>
<form id="add-link-form">
<div class="form-group">
<label for="link-title">Title *</label>
<input type="text" id="link-title" placeholder="Link title" required>
</div>
<div class="form-group">
<label for="link-url">URL *</label>
<input type="url" id="link-url" placeholder="https://example.com" required>
</div>
<div class="form-group">
<label for="link-description">Description</label>
<textarea id="link-description" placeholder="Optional description"></textarea>
</div>
<button type="submit" class="btn btn-success">Add Link</button>
</form>
</div>
<div class="links-section">
<h2>Current Links ({{ page.links|length }})</h2>
<div id="links-container">
{% if page.links %}
{% for link in page.links %}
<div class="link-item" data-link-id="{{ link.id }}">
<div class="link-display">
<div class="link-title">{{ link.title }}</div>
{% if link.description %}
<div class="link-description">{{ link.description }}</div>
{% endif %}
<div class="link-url">{{ link.url }}</div>
<div class="link-actions">
<button class="btn btn-small btn-secondary" onclick="editLink('{{ link.id }}')">Edit</button>
<button class="btn btn-small btn-danger" onclick="deleteLink('{{ link.id }}')">Delete</button>
</div>
</div>
<div class="link-edit">
<div class="form-group">
<label>Title</label>
<input type="text" class="edit-title" value="{{ link.title }}">
</div>
<div class="form-group">
<label>URL</label>
<input type="url" class="edit-url" value="{{ link.url }}">
</div>
<div class="form-group">
<label>Description</label>
<textarea class="edit-description">{{ link.description or '' }}</textarea>
</div>
<div class="link-actions">
<button class="btn btn-small btn-success" onclick="saveLink('{{ link.id }}')">Save</button>
<button class="btn btn-small btn-secondary" onclick="cancelEdit('{{ link.id }}')">Cancel</button>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<div class="icon">📝</div>
<h3>No links yet</h3>
<p>Add your first link using the form on the left.</p>
</div>
{% endif %}
</div>
</div>
</div>
<div class="page-actions">
<a href="/links/{{ page.id }}" target="_blank" class="btn">👁️ View Public Page</a>
<a href="/" class="btn btn-secondary">🏠 Back to Dashboard</a>
</div>
</div>
<script>
const pageId = '{{ page.id }}';
// Add new link
document.getElementById('add-link-form').addEventListener('submit', async function(e) {
e.preventDefault();
const title = document.getElementById('link-title').value;
const url = document.getElementById('link-url').value;
const description = document.getElementById('link-description').value;
try {
const response = await fetch(`/api/link_pages/${pageId}/links`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ title, url, description })
});
const result = await response.json();
if (result.success) {
showAlert('Link added successfully!', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showAlert(result.error || 'Failed to add link', 'error');
}
} catch (error) {
showAlert('Network error occurred', 'error');
}
});
// Edit link
function editLink(linkId) {
const linkItem = document.querySelector(`[data-link-id="${linkId}"]`);
linkItem.classList.add('editing');
}
// Cancel edit
function cancelEdit(linkId) {
const linkItem = document.querySelector(`[data-link-id="${linkId}"]`);
linkItem.classList.remove('editing');
}
// Save link
async function saveLink(linkId) {
const linkItem = document.querySelector(`[data-link-id="${linkId}"]`);
const title = linkItem.querySelector('.edit-title').value;
const url = linkItem.querySelector('.edit-url').value;
const description = linkItem.querySelector('.edit-description').value;
try {
const response = await fetch(`/api/link_pages/${pageId}/links/${linkId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ title, url, description })
});
const result = await response.json();
if (result.success) {
showAlert('Link updated successfully!', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showAlert(result.error || 'Failed to update link', 'error');
}
} catch (error) {
showAlert('Network error occurred', 'error');
}
}
// Delete link
async function deleteLink(linkId) {
if (!confirm('Are you sure you want to delete this link?')) {
return;
}
try {
const response = await fetch(`/api/link_pages/${pageId}/links/${linkId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showAlert('Link deleted successfully!', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showAlert(result.error || 'Failed to delete link', 'error');
}
} catch (error) {
showAlert('Network error occurred', 'error');
}
}
// Show alert
function showAlert(message, type) {
const alert = document.getElementById(`${type}-alert`);
alert.textContent = message;
alert.style.display = 'block';
setTimeout(() => {
alert.style.display = 'none';
}, 3000);
}
</script>
</body>
</html>

View File

@@ -145,6 +145,7 @@
}
.wifi-fields,
.link-page-fields,
.email-fields,
.sms-fields,
.vcard-fields {
@@ -152,6 +153,7 @@
}
.wifi-fields.active,
.link-page-fields.active,
.email-fields.active,
.sms-fields.active,
.vcard-fields.active {
@@ -303,6 +305,11 @@
<div class="header">
<h1>🎯 QR Code Manager</h1>
<p>Create, customize, and manage your QR codes with ease</p>
<div style="position: absolute; top: 20px; right: 20px;">
<a href="/logout" style="color: white; text-decoration: none; background: rgba(255,255,255,0.2); padding: 8px 15px; border-radius: 20px; font-size: 0.9em;">
👤 Logout
</a>
</div>
</div>
<div class="main-content">
@@ -314,6 +321,7 @@
<select id="qr-type" onchange="toggleFields()">
<option value="text">Text</option>
<option value="url">URL/Website</option>
<option value="link_page">Dynamic Link Page</option>
<option value="wifi">WiFi</option>
<option value="email">Email</option>
<option value="phone">Phone</option>
@@ -348,6 +356,23 @@
</div>
</div>
<!-- Link Page fields -->
<div class="link-page-fields" id="link-page-fields">
<div class="form-group">
<label for="page-title">Page Title</label>
<input type="text" id="page-title" placeholder="My Link Collection" value="My Links">
</div>
<div class="form-group">
<label for="page-description">Description</label>
<textarea id="page-description" placeholder="A collection of useful links">Collection of useful links</textarea>
</div>
<div class="form-group">
<p style="background: #e3f2fd; padding: 15px; border-radius: 8px; color: #1565c0; font-size: 0.9em;">
<strong>💡 Dynamic Link Page:</strong> This creates a QR code that points to a web page where you can add, edit, and manage links. The QR code stays the same, but you can update the links anytime!
</p>
</div>
</div>
<!-- Email fields -->
<div class="email-fields" id="email-fields">
<div class="form-group">
@@ -476,7 +501,7 @@
// Hide all specific fields
document.getElementById('text-field').style.display = 'none';
document.querySelectorAll('.wifi-fields, .email-fields, .sms-fields, .vcard-fields').forEach(el => {
document.querySelectorAll('.wifi-fields, .link-page-fields, .email-fields, .sms-fields, .vcard-fields').forEach(el => {
el.classList.remove('active');
});
@@ -492,7 +517,7 @@
contentField.placeholder = 'Enter your text...';
}
} else {
document.getElementById(`${type}-fields`).classList.add('active');
document.getElementById(`${type.replace('_', '-')}-fields`).classList.add('active');
}
}
@@ -517,6 +542,9 @@
// Get content based on type
if (type === 'text' || type === 'url' || type === 'phone') {
content = document.getElementById('content').value;
} else if (type === 'link_page') {
additionalData.title = document.getElementById('page-title').value;
additionalData.description = document.getElementById('page-description').value;
} else if (type === 'wifi') {
additionalData.wifi = {
ssid: document.getElementById('wifi-ssid').value,
@@ -555,7 +583,8 @@
};
try {
const response = await fetch('/api/generate', {
const endpoint = type === 'link_page' ? '/api/create_link_page' : '/api/generate';
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -571,7 +600,21 @@
// Show QR code
const preview = document.getElementById('qr-preview');
preview.innerHTML = `<img src="${result.image_data}" alt="Generated QR Code">`;
let previewHTML = `<img src="${result.image_data}" alt="Generated QR Code">`;
// Add special info for link pages
if (type === 'link_page') {
previewHTML += `
<div style="margin-top: 15px; padding: 15px; background: #e3f2fd; border-radius: 8px; text-align: left;">
<h4 style="margin-bottom: 10px; color: #1565c0;">🎉 Dynamic Link Page Created!</h4>
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>Public URL:</strong> <a href="${result.page_url}" target="_blank">${result.page_url}</a></p>
<p style="margin-bottom: 10px; font-size: 0.9em;"><strong>Edit URL:</strong> <a href="${result.edit_url}" target="_blank">${result.edit_url}</a></p>
<p style="font-size: 0.9em; color: #666;">Share the QR code - visitors will see your link collection. Use the edit URL to manage your links!</p>
</div>
`;
}
preview.innerHTML = previewHTML;
// Show download buttons
document.getElementById('download-section').classList.add('active');
@@ -623,11 +666,12 @@
<div class="qr-item">
<img src="${qr.preview}" alt="QR Code">
<div class="qr-item-info">
<h4>${qr.type.toUpperCase()}</h4>
<h4>${qr.type.toUpperCase()}${qr.type === 'link_page' ? ' 🔗' : ''}</h4>
<p>Created: ${new Date(qr.created_at).toLocaleDateString()}</p>
</div>
<div class="qr-item-actions">
<button class="btn btn-small btn-primary" onclick="downloadQRById('${qr.id}')">Download</button>
${qr.type === 'link_page' ? `<button class="btn btn-small" onclick="openLinkPage('${qr.id}')" style="background: #28a745;">Manage</button>` : ''}
<button class="btn btn-small btn-secondary" onclick="deleteQR('${qr.id}')">Delete</button>
</div>
</div>
@@ -659,6 +703,21 @@
}
}
async function openLinkPage(qrId) {
try {
const response = await fetch(`/api/qr_codes/${qrId}`);
const qrData = await response.json();
if (qrData.page_id) {
window.open(`/edit/${qrData.page_id}`, '_blank');
} else {
alert('Link page not found');
}
} catch (error) {
alert('Error opening link page: ' + error.message);
}
}
// Load history on page load
document.addEventListener('DOMContentLoaded', function() {
loadQRHistory();

View File

@@ -0,0 +1,281 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ page.title }}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px 30px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
font-weight: 300;
}
.header p {
font-size: 1.1em;
opacity: 0.9;
}
.stats {
background: rgba(255,255,255,0.1);
margin-top: 20px;
padding: 15px;
border-radius: 10px;
display: flex;
justify-content: center;
gap: 30px;
}
.stat-item {
text-align: center;
}
.stat-number {
font-size: 1.5em;
font-weight: bold;
}
.stat-label {
font-size: 0.9em;
opacity: 0.8;
margin-top: 5px;
}
.content {
padding: 30px;
}
.links-section h2 {
color: #333;
margin-bottom: 25px;
font-size: 1.8em;
text-align: center;
}
.link-item {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 12px;
padding: 20px;
margin-bottom: 15px;
transition: all 0.3s ease;
cursor: pointer;
text-decoration: none;
display: block;
color: inherit;
}
.link-item:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
border-color: #667eea;
}
.link-title {
font-size: 1.3em;
font-weight: 600;
color: #333;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 10px;
}
.link-icon {
width: 20px;
height: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
}
.link-description {
color: #666;
font-size: 0.95em;
line-height: 1.4;
margin-bottom: 10px;
}
.link-url {
color: #667eea;
font-size: 0.9em;
font-weight: 500;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #666;
}
.empty-state .icon {
font-size: 4em;
margin-bottom: 20px;
opacity: 0.5;
}
.empty-state h3 {
font-size: 1.5em;
margin-bottom: 10px;
color: #333;
}
.footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
border-top: 1px solid #e9ecef;
color: #666;
font-size: 0.9em;
}
.footer a {
color: #667eea;
text-decoration: none;
font-weight: 500;
}
.footer a:hover {
text-decoration: underline;
}
.last-updated {
margin-top: 10px;
font-size: 0.8em;
opacity: 0.7;
}
@media (max-width: 768px) {
.header {
padding: 30px 20px;
}
.header h1 {
font-size: 2em;
}
.content {
padding: 20px;
}
.stats {
flex-direction: column;
gap: 15px;
}
}
/* Link animation */
@keyframes linkPulse {
0% { transform: scale(1); }
50% { transform: scale(1.02); }
100% { transform: scale(1); }
}
.link-item:active {
animation: linkPulse 0.2s ease;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>{{ page.title }}</h1>
<p>{{ page.description }}</p>
<div class="stats">
<div class="stat-item">
<div class="stat-number">{{ page.links|length }}</div>
<div class="stat-label">Links</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ page.view_count }}</div>
<div class="stat-label">Views</div>
</div>
</div>
</div>
<div class="content">
<div class="links-section">
{% if page.links %}
<h2>📚 Available Links</h2>
{% for link in page.links %}
<a href="{{ link.url }}" target="_blank" class="link-item">
<div class="link-title">
<div class="link-icon">🔗</div>
{{ link.title }}
</div>
{% if link.description %}
<div class="link-description">{{ link.description }}</div>
{% endif %}
<div class="link-url">{{ link.url }}</div>
</a>
{% endfor %}
{% else %}
<div class="empty-state">
<div class="icon">📝</div>
<h3>No links yet</h3>
<p>This collection is empty. Check back later for new links!</p>
</div>
{% endif %}
</div>
</div>
<div class="footer">
<p>Powered by <a href="/">QR Code Manager</a></p>
{% if page.updated_at %}
<div class="last-updated">
Last updated: {{ page.updated_at[:10] }} at {{ page.updated_at[11:19] }}
</div>
{% endif %}
</div>
</div>
<script>
// Add click tracking (optional)
document.querySelectorAll('.link-item').forEach(link => {
link.addEventListener('click', function() {
// You could add analytics here
console.log('Link clicked:', this.querySelector('.link-title').textContent.trim());
});
});
// Auto-refresh every 30 seconds to get latest links
setInterval(() => {
window.location.reload();
}, 30000);
</script>
</body>
</html>

253
app/templates/login.html Normal file
View File

@@ -0,0 +1,253 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QR Code Manager - Login</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-container {
background: white;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
width: 100%;
max-width: 400px;
}
.login-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px 30px;
text-align: center;
}
.login-header h1 {
font-size: 2.5em;
margin-bottom: 10px;
font-weight: 300;
}
.login-header p {
font-size: 1.1em;
opacity: 0.9;
}
.login-form {
padding: 40px 30px;
}
.form-group {
margin-bottom: 25px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #555;
}
.form-group input {
width: 100%;
padding: 15px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
background: #f8f9fa;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
background: white;
}
.login-btn {
width: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px;
border: none;
border-radius: 8px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
}
.login-btn:hover {
transform: translateY(-2px);
}
.login-btn:active {
transform: translateY(0);
}
.alert {
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
text-align: center;
font-weight: 500;
}
.alert-error {
background: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.alert-success {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.alert-info {
background: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
}
.login-footer {
padding: 20px 30px;
background: #f8f9fa;
text-align: center;
color: #666;
font-size: 0.9em;
}
.default-credentials {
background: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
text-align: center;
font-size: 0.9em;
}
.default-credentials strong {
display: block;
margin-bottom: 5px;
}
@media (max-width: 480px) {
.login-container {
margin: 10px;
}
.login-header {
padding: 30px 20px;
}
.login-header h1 {
font-size: 2em;
}
.login-form {
padding: 30px 20px;
}
}
/* Security icon animation */
.security-icon {
font-size: 3em;
margin-bottom: 15px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<div class="security-icon">🔐</div>
<h1>QR Manager</h1>
<p>Secure Admin Access</p>
</div>
<div class="login-form">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="default-credentials">
<strong>Default Login Credentials:</strong>
Username: admin<br>
Password: admin123
</div>
<form method="POST">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autocomplete="username">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
</div>
<button type="submit" class="login-btn">🚀 Login</button>
</form>
</div>
<div class="login-footer">
<p>🔒 Secure QR Code Management System</p>
<p style="margin-top: 5px; font-size: 0.8em; opacity: 0.7;">
Change default credentials in production
</p>
</div>
</div>
<script>
// Auto-focus on username field
document.getElementById('username').focus();
// Handle form submission
document.querySelector('form').addEventListener('submit', function(e) {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
if (!username || !password) {
e.preventDefault();
alert('Please enter both username and password');
return;
}
// Show loading state
const btn = document.querySelector('.login-btn');
btn.innerHTML = '🔄 Logging in...';
btn.disabled = true;
});
</script>
</body>
</html>

20
app/utils/__init__.py Normal file
View File

@@ -0,0 +1,20 @@
"""
Utility modules for QR Code Manager
"""
from .auth import init_admin, login_required, verify_password, get_admin_credentials
from .qr_generator import QRCodeGenerator
from .link_manager import LinkPageManager, link_pages_db
from .data_manager import QRDataManager, qr_codes_db
__all__ = [
'init_admin',
'login_required',
'verify_password',
'get_admin_credentials',
'QRCodeGenerator',
'LinkPageManager',
'link_pages_db',
'QRDataManager',
'qr_codes_db'
]

39
app/utils/auth.py Normal file
View File

@@ -0,0 +1,39 @@
"""
Authentication utilities for QR Code Manager
"""
import os
import bcrypt
from functools import wraps
from flask import session, redirect, url_for, request, jsonify
# Admin configuration
ADMIN_USERNAME = os.environ.get('ADMIN_USERNAME', 'admin')
ADMIN_PASSWORD_HASH = None
def init_admin():
"""Initialize admin user with password from environment or default"""
global ADMIN_PASSWORD_HASH
admin_password = os.environ.get('ADMIN_PASSWORD', 'admin123')
ADMIN_PASSWORD_HASH = bcrypt.hashpw(admin_password.encode('utf-8'), bcrypt.gensalt())
print(f"Admin user initialized: {ADMIN_USERNAME}")
print(f"Default password: {admin_password if admin_password == 'admin123' else '***'}")
def verify_password(password, hashed):
"""Verify a password against its hash"""
return bcrypt.checkpw(password.encode('utf-8'), hashed)
def login_required(f):
"""Authentication decorator"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'logged_in' not in session:
if request.endpoint and request.endpoint.startswith('api'):
return jsonify({'error': 'Authentication required'}), 401
return redirect(url_for('auth.login'))
return f(*args, **kwargs)
return decorated_function
def get_admin_credentials():
"""Get admin credentials for authentication"""
return ADMIN_USERNAME, ADMIN_PASSWORD_HASH

58
app/utils/data_manager.py Normal file
View File

@@ -0,0 +1,58 @@
"""
Data storage utilities for QR codes
"""
import uuid
from datetime import datetime
# In-memory storage for QR codes (in production, use a database)
qr_codes_db = {}
class QRDataManager:
def __init__(self):
pass
def save_qr_record(self, qr_type, content, settings, image_data, page_id=None):
"""Save QR code record to database"""
qr_id = str(uuid.uuid4())
qr_record = {
'id': qr_id,
'type': qr_type,
'content': content,
'settings': settings,
'created_at': datetime.now().isoformat(),
'image_data': image_data
}
if page_id:
qr_record['page_id'] = page_id
qr_codes_db[qr_id] = qr_record
return qr_id
def get_qr_record(self, qr_id):
"""Get QR code record"""
return qr_codes_db.get(qr_id)
def delete_qr_record(self, qr_id):
"""Delete QR code record"""
if qr_id in qr_codes_db:
del qr_codes_db[qr_id]
return True
return False
def list_qr_codes(self):
"""List all QR codes"""
qr_list = []
for qr_id, qr_data in qr_codes_db.items():
qr_list.append({
'id': qr_id,
'type': qr_data['type'],
'created_at': qr_data['created_at'],
'preview': f'data:image/png;base64,{qr_data["image_data"]}'
})
return qr_list
def qr_exists(self, qr_id):
"""Check if QR code exists"""
return qr_id in qr_codes_db

86
app/utils/link_manager.py Normal file
View File

@@ -0,0 +1,86 @@
"""
Dynamic Link Page Manager utilities
"""
import uuid
from datetime import datetime
# In-memory storage for dynamic link pages (in production, use a database)
link_pages_db = {}
class LinkPageManager:
def __init__(self):
pass
def create_link_page(self, title="My Links", description="Collection of useful links"):
"""Create a new dynamic link page"""
page_id = str(uuid.uuid4())
page_data = {
'id': page_id,
'title': title,
'description': description,
'links': [],
'created_at': datetime.now().isoformat(),
'updated_at': datetime.now().isoformat(),
'view_count': 0
}
link_pages_db[page_id] = page_data
return page_id
def add_link(self, page_id, title, url, description=""):
"""Add a link to a page"""
if page_id not in link_pages_db:
return False
link_data = {
'id': str(uuid.uuid4()),
'title': title,
'url': url if url.startswith(('http://', 'https://')) else f'https://{url}',
'description': description,
'created_at': datetime.now().isoformat()
}
link_pages_db[page_id]['links'].append(link_data)
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
return True
def update_link(self, page_id, link_id, title=None, url=None, description=None):
"""Update a specific link"""
if page_id not in link_pages_db:
return False
for link in link_pages_db[page_id]['links']:
if link['id'] == link_id:
if title is not None:
link['title'] = title
if url is not None:
link['url'] = url if url.startswith(('http://', 'https://')) else f'https://{url}'
if description is not None:
link['description'] = description
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
return True
return False
def delete_link(self, page_id, link_id):
"""Delete a specific link"""
if page_id not in link_pages_db:
return False
links = link_pages_db[page_id]['links']
link_pages_db[page_id]['links'] = [link for link in links if link['id'] != link_id]
link_pages_db[page_id]['updated_at'] = datetime.now().isoformat()
return True
def increment_view_count(self, page_id):
"""Increment view count for a page"""
if page_id in link_pages_db:
link_pages_db[page_id]['view_count'] += 1
def get_page(self, page_id):
"""Get page data"""
return link_pages_db.get(page_id)
def page_exists(self, page_id):
"""Check if page exists"""
return page_id in link_pages_db

95
app/utils/qr_generator.py Normal file
View File

@@ -0,0 +1,95 @@
"""
QR Code generation utilities
"""
import os
import qrcode
from qrcode.image.styledpil import StyledPilImage
from qrcode.image.styles.moduledrawers import RoundedModuleDrawer, CircleModuleDrawer, SquareModuleDrawer
from PIL import Image
class QRCodeGenerator:
def __init__(self):
self.default_settings = {
'size': 10,
'border': 4,
'error_correction': qrcode.constants.ERROR_CORRECT_M,
'foreground_color': '#000000',
'background_color': '#FFFFFF',
'style': 'square'
}
def generate_qr_code(self, data, settings=None):
"""Generate QR code with custom settings"""
if settings is None:
settings = self.default_settings.copy()
else:
merged_settings = self.default_settings.copy()
merged_settings.update(settings)
settings = merged_settings
# 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)
# For styled QR codes with custom module drawer
if settings['style'] != 'square':
# Choose module drawer based on style
module_drawer = None
if settings['style'] == 'rounded':
module_drawer = RoundedModuleDrawer()
elif settings['style'] == 'circle':
module_drawer = CircleModuleDrawer()
# Generate the styled image
img = qr.make_image(
image_factory=StyledPilImage,
module_drawer=module_drawer,
fill_color=settings['foreground_color'],
back_color=settings['background_color']
)
else:
# Generate standard image with custom colors
img = qr.make_image(
fill_color=settings['foreground_color'],
back_color=settings['background_color']
)
return img
def add_logo(self, qr_img, logo_path, logo_size_ratio=0.2):
"""Add logo to QR code"""
try:
logo = Image.open(logo_path)
# Calculate logo size
qr_width, qr_height = qr_img.size
logo_size = int(min(qr_width, qr_height) * logo_size_ratio)
# Resize logo
logo = logo.resize((logo_size, logo_size), Image.Resampling.LANCZOS)
# Create a white background for the logo
logo_bg = Image.new('RGB', (logo_size + 20, logo_size + 20), 'white')
logo_bg.paste(logo, (10, 10))
# Calculate position to center the logo
logo_pos = (
(qr_width - logo_bg.width) // 2,
(qr_height - logo_bg.height) // 2
)
# Paste logo onto QR code
qr_img.paste(logo_bg, logo_pos)
return qr_img
except Exception as e:
print(f"Error adding logo: {e}")
return qr_img

98
deploy.sh Executable file
View File

@@ -0,0 +1,98 @@
#!/bin/bash
# QR Code Manager - Docker Build and Test Script
set -e # Exit on any error
echo "🐳 QR Code Manager - Docker Build & Test"
echo "========================================"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
echo -e "${GREEN}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
# Check prerequisites
echo "🔍 Checking prerequisites..."
if ! command -v docker &> /dev/null; then
print_error "Docker is not installed"
exit 1
fi
if ! docker compose version &> /dev/null; then
print_error "Docker Compose is not available"
exit 1
fi
print_status "Docker and Docker Compose are available"
# Create environment file if it doesn't exist
if [ ! -f .env ]; then
print_warning "Creating .env file from template"
cp .env.example .env
print_warning "Please review and update .env file with secure credentials!"
fi
# Build the Docker image
echo "🏗️ Building Docker image..."
if docker compose build; then
print_status "Docker image built successfully"
else
print_error "Docker build failed"
exit 1
fi
# Start the application
echo "🚀 Starting application..."
if docker compose up -d; then
print_status "Application started successfully"
else
print_error "Failed to start application"
exit 1
fi
# Wait for application to be ready
echo "⏳ Waiting for application to be ready..."
sleep 10
# Check if application is responding
if curl -f http://localhost:5000/health > /dev/null 2>&1; then
print_status "Application is healthy and responding"
else
print_warning "Health check failed, checking logs..."
docker compose logs qr-manager
fi
# Show status
echo "📊 Application Status:"
docker compose ps
echo ""
echo "🎉 Deployment Complete!"
echo "========================================"
echo "Application URL: http://localhost:5000"
echo "Default Login:"
echo " Username: admin"
echo " Password: admin123"
echo ""
echo "Management Commands:"
echo " View logs: docker compose logs -f"
echo " Stop app: docker compose down"
echo " Restart: docker compose restart"
echo ""
print_warning "Remember to change default credentials in production!"

39
docker-compose.yml Normal file
View File

@@ -0,0 +1,39 @@
version: '3.8'
services:
qr-manager:
build: .
container_name: qr-code-manager
restart: unless-stopped
ports:
- "5000:5000"
environment:
- FLASK_ENV=production
- SECRET_KEY=${SECRET_KEY:-your-super-secret-key-change-me}
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin123}
volumes:
- qr_data:/app/app/static/qr_codes
- logo_data:/app/app/static/logos
- session_data:/app/flask_session
healthcheck:
test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:5000/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
labels:
- "com.example.description=QR Code Manager Application"
- "com.example.service=qr-manager"
volumes:
qr_data:
driver: local
logo_data:
driver: local
session_data:
driver: local
networks:
default:
name: qr-manager-network

Binary file not shown.

Binary file not shown.

33
main.py Normal file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""
QR Code Manager - Main Application Entry Point
A modern Flask web application for generating and managing QR codes with authentication.
Features include:
- Multiple QR code types (text, URL, WiFi, email, SMS, vCard)
- Dynamic link pages for managing collections of links
- Admin authentication with bcrypt password hashing
- Docker deployment ready
- Modern responsive web interface
"""
import os
from app import create_app
# Create Flask application
app = create_app()
if __name__ == '__main__':
# Production vs Development configuration
is_production = os.environ.get('FLASK_ENV') == 'production'
if is_production:
print("🚀 Starting QR Code Manager in PRODUCTION mode")
print("🔐 Admin user: admin")
print("🔒 Make sure to change default credentials!")
app.run(host='0.0.0.0', port=5000, debug=False)
else:
print("🛠️ Starting QR Code Manager in DEVELOPMENT mode")
print("🔐 Admin user: admin")
print("🔑 Default password: admin123")
app.run(host='0.0.0.0', port=5000, debug=True)

View File

@@ -3,3 +3,7 @@ qrcode[pil]==7.4.2
Pillow==10.0.1
flask-cors==4.0.0
python-dotenv==1.0.0
requests==2.31.0
flask-session==0.5.0
werkzeug==2.3.7
bcrypt==4.0.1

View File

@@ -1,2 +0,0 @@
# This file ensures the directory is tracked by git
# Uploaded logo files will be stored here

View File

@@ -1,2 +0,0 @@
# This file ensures the directory is tracked by git
# Generated QR code images will be stored here

View File

@@ -1,208 +0,0 @@
#!/usr/bin/env python3
"""
Test script for QR Code Manager API
Run this script while the server is running to test API functionality
"""
import requests
import json
import base64
from PIL import Image
import io
# Server URL
BASE_URL = "http://localhost:5000"
def test_text_qr():
"""Test generating a text QR code"""
print("Testing text QR code generation...")
data = {
"type": "text",
"content": "Hello, World! This is a test QR code.",
"foreground_color": "#000000",
"background_color": "#FFFFFF",
"style": "square",
"size": 10
}
response = requests.post(f"{BASE_URL}/api/generate", json=data)
if response.status_code == 200:
result = response.json()
if result['success']:
print(f"✅ Text QR generated successfully! ID: {result['qr_id']}")
return result['qr_id']
else:
print(f"❌ Error: {result['error']}")
else:
print(f"❌ HTTP Error: {response.status_code}")
return None
def test_url_qr():
"""Test generating a URL QR code"""
print("Testing URL QR code generation...")
data = {
"type": "url",
"content": "https://github.com",
"foreground_color": "#0066cc",
"background_color": "#ffffff",
"style": "rounded",
"size": 12
}
response = requests.post(f"{BASE_URL}/api/generate", json=data)
if response.status_code == 200:
result = response.json()
if result['success']:
print(f"✅ URL QR generated successfully! ID: {result['qr_id']}")
return result['qr_id']
else:
print(f"❌ Error: {result['error']}")
else:
print(f"❌ HTTP Error: {response.status_code}")
return None
def test_wifi_qr():
"""Test generating a WiFi QR code"""
print("Testing WiFi QR code generation...")
data = {
"type": "wifi",
"wifi": {
"ssid": "TestNetwork",
"password": "TestPassword123",
"security": "WPA"
},
"foreground_color": "#ff6600",
"background_color": "#ffffff",
"style": "circle",
"size": 8
}
response = requests.post(f"{BASE_URL}/api/generate", json=data)
if response.status_code == 200:
result = response.json()
if result['success']:
print(f"✅ WiFi QR generated successfully! ID: {result['qr_id']}")
return result['qr_id']
else:
print(f"❌ Error: {result['error']}")
else:
print(f"❌ HTTP Error: {response.status_code}")
return None
def test_list_qr_codes():
"""Test listing all QR codes"""
print("Testing QR codes listing...")
response = requests.get(f"{BASE_URL}/api/qr_codes")
if response.status_code == 200:
qr_codes = response.json()
print(f"✅ Found {len(qr_codes)} QR codes")
for qr in qr_codes:
print(f" - {qr['id']}: {qr['type']} (created: {qr['created_at']})")
return qr_codes
else:
print(f"❌ HTTP Error: {response.status_code}")
return []
def test_download_qr(qr_id):
"""Test downloading a QR code"""
print(f"Testing QR code download for ID: {qr_id}")
response = requests.get(f"{BASE_URL}/api/download/{qr_id}")
if response.status_code == 200:
print(f"✅ QR code downloaded successfully! Size: {len(response.content)} bytes")
# Verify it's a valid PNG image
try:
image = Image.open(io.BytesIO(response.content))
print(f" Image format: {image.format}, Size: {image.size}")
except Exception as e:
print(f"❌ Invalid image data: {e}")
else:
print(f"❌ HTTP Error: {response.status_code}")
def test_get_qr_details(qr_id):
"""Test getting QR code details"""
print(f"Testing QR code details for ID: {qr_id}")
response = requests.get(f"{BASE_URL}/api/qr_codes/{qr_id}")
if response.status_code == 200:
qr_data = response.json()
print(f"✅ QR code details retrieved!")
print(f" Type: {qr_data['type']}")
print(f" Created: {qr_data['created_at']}")
print(f" Settings: {qr_data['settings']}")
else:
print(f"❌ HTTP Error: {response.status_code}")
def test_delete_qr(qr_id):
"""Test deleting a QR code"""
print(f"Testing QR code deletion for ID: {qr_id}")
response = requests.delete(f"{BASE_URL}/api/qr_codes/{qr_id}")
if response.status_code == 200:
result = response.json()
if result['success']:
print(f"✅ QR code deleted successfully!")
else:
print(f"❌ Error: {result['error']}")
else:
print(f"❌ HTTP Error: {response.status_code}")
def main():
"""Run all tests"""
print("🚀 Starting QR Code Manager API Tests")
print("=" * 50)
# Test generating different types of QR codes
text_qr_id = test_text_qr()
print()
url_qr_id = test_url_qr()
print()
wifi_qr_id = test_wifi_qr()
print()
# Test listing QR codes
qr_codes = test_list_qr_codes()
print()
# Test operations on generated QR codes
if text_qr_id:
test_download_qr(text_qr_id)
print()
test_get_qr_details(text_qr_id)
print()
# Test deletion (only delete one to keep some for manual testing)
if wifi_qr_id:
test_delete_qr(wifi_qr_id)
print()
print("🏁 Tests completed!")
print("You can now open http://localhost:5000 in your browser to test the web interface.")
if __name__ == "__main__":
try:
main()
except requests.exceptions.ConnectionError:
print("❌ Cannot connect to the server. Make sure the QR Code Manager is running on localhost:5000")
print("Start the server with: python app.py")
except Exception as e:
print(f"❌ Unexpected error: {e}")