Add HTTPS configuration management system
- Add HTTPSConfig model for managing HTTPS settings - Add admin routes for HTTPS configuration management - Add beautiful admin template for HTTPS configuration - Add database migration for https_config table - Add CLI utility for HTTPS management - Add setup script for automated configuration - Add Caddy configuration generator and manager - Add comprehensive documentation (3 guides) - Add HTTPS Configuration card to admin dashboard - Implement input validation and security features - Add admin-only access control with audit trail - Add real-time configuration preview - Integrate with existing Caddy reverse proxy Features: - Enable/disable HTTPS from web interface - Configure domain, hostname, IP address, port - Automatic SSL certificate management via Let's Encrypt - Real-time Caddyfile generation and reload - Full audit trail with admin username and timestamps - Support for HTTPS and HTTP fallback access points - Beautiful, mobile-responsive UI Modified files: - app/models/__init__.py (added HTTPSConfig import) - app/blueprints/admin.py (added HTTPS routes) - app/templates/admin/admin.html (added HTTPS card) - docker-compose.yml (added Caddyfile mount and admin port) New files: - app/models/https_config.py - app/blueprints/https_config.html - app/utils/caddy_manager.py - https_manager.py - setup_https.sh - migrations/add_https_config_table.py - migrations/add_email_to_https_config.py - HTTPS_STATUS.txt - Documentation files (3 markdown guides)
0
app/app.py
Normal file → Executable file
0
app/blueprints/__init__.py
Normal file → Executable file
155
app/blueprints/admin.py
Normal file → Executable file
@@ -8,8 +8,9 @@ from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from app.extensions import db, bcrypt
|
||||
from app.models import User, Player, Group, Content, ServerLog, Playlist
|
||||
from app.models import User, Player, Group, Content, ServerLog, Playlist, HTTPSConfig
|
||||
from app.utils.logger import log_action
|
||||
from app.utils.caddy_manager import CaddyConfigGenerator
|
||||
|
||||
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
|
||||
@@ -843,3 +844,155 @@ def delete_editing_user(user_id: int):
|
||||
flash(f'Error deleting user: {str(e)}', 'danger')
|
||||
|
||||
return redirect(url_for('admin.manage_editing_users'))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# HTTPS Configuration Management Routes
|
||||
# ============================================================================
|
||||
|
||||
@admin_bp.route('/https-config', methods=['GET'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def https_config():
|
||||
"""Display HTTPS configuration management page."""
|
||||
try:
|
||||
config = HTTPSConfig.get_config()
|
||||
|
||||
return render_template('admin/https_config.html',
|
||||
config=config)
|
||||
except Exception as e:
|
||||
log_action('error', f'Error loading HTTPS config page: {str(e)}')
|
||||
flash('Error loading HTTPS configuration page.', 'danger')
|
||||
return redirect(url_for('admin.admin_panel'))
|
||||
|
||||
|
||||
@admin_bp.route('/https-config/update', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def update_https_config():
|
||||
"""Update HTTPS configuration."""
|
||||
try:
|
||||
https_enabled = request.form.get('https_enabled') == 'on'
|
||||
hostname = request.form.get('hostname', '').strip()
|
||||
domain = request.form.get('domain', '').strip()
|
||||
ip_address = request.form.get('ip_address', '').strip()
|
||||
email = request.form.get('email', '').strip()
|
||||
port = request.form.get('port', '443').strip()
|
||||
|
||||
# Validation
|
||||
errors = []
|
||||
|
||||
if https_enabled:
|
||||
if not hostname:
|
||||
errors.append('Hostname is required when HTTPS is enabled.')
|
||||
if not domain:
|
||||
errors.append('Domain name is required when HTTPS is enabled.')
|
||||
if not ip_address:
|
||||
errors.append('IP address is required when HTTPS is enabled.')
|
||||
if not email:
|
||||
errors.append('Email address is required when HTTPS is enabled.')
|
||||
|
||||
# Validate domain format (basic)
|
||||
if domain and '.' not in domain:
|
||||
errors.append('Please enter a valid domain name (e.g., example.com).')
|
||||
|
||||
# Validate IP format (basic)
|
||||
if ip_address:
|
||||
ip_parts = ip_address.split('.')
|
||||
if len(ip_parts) != 4:
|
||||
errors.append('Please enter a valid IPv4 address (e.g., 10.76.152.164).')
|
||||
else:
|
||||
try:
|
||||
for part in ip_parts:
|
||||
num = int(part)
|
||||
if num < 0 or num > 255:
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
errors.append('Please enter a valid IPv4 address.')
|
||||
|
||||
# Validate email format (basic)
|
||||
if email and '@' not in email:
|
||||
errors.append('Please enter a valid email address.')
|
||||
|
||||
# Validate port
|
||||
try:
|
||||
port_num = int(port)
|
||||
if port_num < 1 or port_num > 65535:
|
||||
errors.append('Port must be between 1 and 65535.')
|
||||
port = port_num
|
||||
except ValueError:
|
||||
errors.append('Port must be a valid number.')
|
||||
else:
|
||||
port = 443
|
||||
|
||||
if errors:
|
||||
for error in errors:
|
||||
flash(error, 'warning')
|
||||
return redirect(url_for('admin.https_config'))
|
||||
|
||||
# Update configuration
|
||||
config = HTTPSConfig.create_or_update(
|
||||
https_enabled=https_enabled,
|
||||
hostname=hostname if https_enabled else None,
|
||||
domain=domain if https_enabled else None,
|
||||
ip_address=ip_address if https_enabled else None,
|
||||
email=email if https_enabled else None,
|
||||
port=port if https_enabled else 443,
|
||||
updated_by=current_user.username
|
||||
)
|
||||
|
||||
# Generate and update Caddyfile
|
||||
try:
|
||||
caddyfile_content = CaddyConfigGenerator.generate_caddyfile(config)
|
||||
if CaddyConfigGenerator.write_caddyfile(caddyfile_content):
|
||||
# Reload Caddy configuration
|
||||
if CaddyConfigGenerator.reload_caddy():
|
||||
caddy_status = '✅ Caddy configuration updated successfully!'
|
||||
log_action('info', f'Caddy configuration reloaded by {current_user.username}')
|
||||
else:
|
||||
caddy_status = '⚠️ Caddyfile updated but reload failed. Please restart containers.'
|
||||
log_action('warning', f'Caddy reload failed for {current_user.username}')
|
||||
else:
|
||||
caddy_status = '⚠️ Configuration saved but Caddyfile update failed.'
|
||||
log_action('warning', f'Caddyfile write failed for {current_user.username}')
|
||||
except Exception as caddy_error:
|
||||
caddy_status = f'⚠️ Configuration saved but Caddy update failed: {str(caddy_error)}'
|
||||
log_action('error', f'Caddy update error: {str(caddy_error)}')
|
||||
|
||||
if https_enabled:
|
||||
log_action('info', f'HTTPS enabled by {current_user.username}: domain={domain}, hostname={hostname}, ip={ip_address}, email={email}')
|
||||
flash(f'✅ HTTPS configuration saved successfully!\n{caddy_status}\nServer available at https://{domain}', 'success')
|
||||
else:
|
||||
log_action('info', f'HTTPS disabled by {current_user.username}')
|
||||
flash(f'✅ HTTPS has been disabled. Server running on HTTP only.\n{caddy_status}', 'success')
|
||||
|
||||
return redirect(url_for('admin.https_config'))
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
log_action('error', f'Error updating HTTPS config: {str(e)}')
|
||||
flash(f'Error updating HTTPS configuration: {str(e)}', 'danger')
|
||||
return redirect(url_for('admin.https_config'))
|
||||
|
||||
|
||||
@admin_bp.route('/https-config/status')
|
||||
@login_required
|
||||
@admin_required
|
||||
def https_config_status():
|
||||
"""Get current HTTPS configuration status as JSON."""
|
||||
try:
|
||||
config = HTTPSConfig.get_config()
|
||||
|
||||
if config:
|
||||
return jsonify(config.to_dict())
|
||||
else:
|
||||
return jsonify({
|
||||
'https_enabled': False,
|
||||
'hostname': None,
|
||||
'domain': None,
|
||||
'ip_address': None,
|
||||
'port': 443,
|
||||
})
|
||||
except Exception as e:
|
||||
log_action('error', f'Error getting HTTPS status: {str(e)}')
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
0
app/blueprints/api.py
Normal file → Executable file
0
app/blueprints/auth.py
Normal file → Executable file
0
app/blueprints/content.py
Normal file → Executable file
0
app/blueprints/content_old.py
Normal file → Executable file
0
app/blueprints/groups.py
Normal file → Executable file
0
app/blueprints/main.py
Normal file → Executable file
0
app/blueprints/players.py
Normal file → Executable file
0
app/blueprints/playlist.py
Normal file → Executable file
0
app/config.py
Normal file → Executable file
0
app/extensions.py
Normal file → Executable file
2
app/models/__init__.py
Normal file → Executable file
@@ -8,6 +8,7 @@ from app.models.server_log import ServerLog
|
||||
from app.models.player_feedback import PlayerFeedback
|
||||
from app.models.player_edit import PlayerEdit
|
||||
from app.models.player_user import PlayerUser
|
||||
from app.models.https_config import HTTPSConfig
|
||||
|
||||
__all__ = [
|
||||
'User',
|
||||
@@ -19,6 +20,7 @@ __all__ = [
|
||||
'PlayerFeedback',
|
||||
'PlayerEdit',
|
||||
'PlayerUser',
|
||||
'HTTPSConfig',
|
||||
'group_content',
|
||||
'playlist_content',
|
||||
]
|
||||
|
||||
0
app/models/content.py
Normal file → Executable file
0
app/models/group.py
Normal file → Executable file
104
app/models/https_config.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""HTTPS Configuration model."""
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
class HTTPSConfig(db.Model):
|
||||
"""HTTPS configuration model for managing secure connections.
|
||||
|
||||
Attributes:
|
||||
id: Primary key
|
||||
https_enabled: Whether HTTPS is enabled
|
||||
hostname: Server hostname (e.g., 'digiserver')
|
||||
domain: Full domain name (e.g., 'digiserver.sibiusb.harting.intra')
|
||||
ip_address: IP address for direct access
|
||||
email: Email address for SSL certificate notifications
|
||||
port: HTTPS port (default 443)
|
||||
created_at: Creation timestamp
|
||||
updated_at: Last update timestamp
|
||||
updated_by: User who made the last update
|
||||
"""
|
||||
__tablename__ = 'https_config'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
https_enabled = db.Column(db.Boolean, default=False, nullable=False)
|
||||
hostname = db.Column(db.String(255), nullable=True)
|
||||
domain = db.Column(db.String(255), nullable=True)
|
||||
ip_address = db.Column(db.String(45), nullable=True) # Support IPv6
|
||||
email = db.Column(db.String(255), nullable=True)
|
||||
port = db.Column(db.Integer, default=443, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow,
|
||||
onupdate=datetime.utcnow, nullable=False)
|
||||
updated_by = db.Column(db.String(255), nullable=True)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of HTTPSConfig."""
|
||||
status = 'ENABLED' if self.https_enabled else 'DISABLED'
|
||||
return f'<HTTPSConfig [{status}] {self.domain or "N/A"}>'
|
||||
|
||||
@classmethod
|
||||
def get_config(cls) -> Optional['HTTPSConfig']:
|
||||
"""Get the current HTTPS configuration.
|
||||
|
||||
Returns:
|
||||
HTTPSConfig instance or None if not configured
|
||||
"""
|
||||
return cls.query.first()
|
||||
|
||||
@classmethod
|
||||
def create_or_update(cls, https_enabled: bool, hostname: str = None,
|
||||
domain: str = None, ip_address: str = None,
|
||||
email: str = None, port: int = 443,
|
||||
updated_by: str = None) -> 'HTTPSConfig':
|
||||
"""Create or update HTTPS configuration.
|
||||
|
||||
Args:
|
||||
https_enabled: Whether HTTPS is enabled
|
||||
hostname: Server hostname
|
||||
domain: Full domain name
|
||||
ip_address: IP address
|
||||
email: Email for SSL certificates
|
||||
port: HTTPS port
|
||||
updated_by: Username of who made the update
|
||||
|
||||
Returns:
|
||||
HTTPSConfig instance
|
||||
"""
|
||||
config = cls.get_config()
|
||||
if not config:
|
||||
config = cls()
|
||||
|
||||
config.https_enabled = https_enabled
|
||||
config.hostname = hostname
|
||||
config.domain = domain
|
||||
config.ip_address = ip_address
|
||||
config.email = email
|
||||
config.port = port
|
||||
config.updated_by = updated_by
|
||||
config.updated_at = datetime.utcnow()
|
||||
|
||||
db.session.add(config)
|
||||
db.session.commit()
|
||||
return config
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert configuration to dictionary.
|
||||
|
||||
Returns:
|
||||
Dictionary representation of config
|
||||
"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'https_enabled': self.https_enabled,
|
||||
'hostname': self.hostname,
|
||||
'domain': self.domain,
|
||||
'ip_address': self.ip_address,
|
||||
'email': self.email,
|
||||
'port': self.port,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||||
'updated_by': self.updated_by,
|
||||
}
|
||||
0
app/models/player.py
Normal file → Executable file
0
app/models/player_edit.py
Normal file → Executable file
0
app/models/player_feedback.py
Normal file → Executable file
0
app/models/player_user.py
Normal file → Executable file
0
app/models/playlist.py
Normal file → Executable file
0
app/models/server_log.py
Normal file → Executable file
0
app/models/user.py
Normal file → Executable file
0
app/static/icons/edit.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 294 B After Width: | Height: | Size: 294 B |
0
app/static/icons/home.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 311 B After Width: | Height: | Size: 311 B |
0
app/static/icons/info.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 329 B After Width: | Height: | Size: 329 B |
0
app/static/icons/monitor.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 301 B After Width: | Height: | Size: 301 B |
0
app/static/icons/moon.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 257 B After Width: | Height: | Size: 257 B |
0
app/static/icons/playlist.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 259 B After Width: | Height: | Size: 259 B |
0
app/static/icons/sun.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 651 B After Width: | Height: | Size: 651 B |
0
app/static/icons/trash.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 334 B After Width: | Height: | Size: 334 B |
0
app/static/icons/upload.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 345 B After Width: | Height: | Size: 345 B |
0
app/static/icons/warning.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 396 B After Width: | Height: | Size: 396 B |
0
app/static/uploads/.gitkeep
Normal file → Executable file
11
app/templates/admin/admin.html
Normal file → Executable file
@@ -105,6 +105,17 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HTTPS Configuration Card (Admin Only) -->
|
||||
<div class="card management-card" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<h2>🔒 HTTPS Configuration</h2>
|
||||
<p>Manage SSL/HTTPS settings, domain, and access points</p>
|
||||
<div class="card-actions">
|
||||
<a href="{{ url_for('admin.https_config') }}" class="btn btn-primary">
|
||||
Configure HTTPS
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Quick Actions Card -->
|
||||
|
||||
0
app/templates/admin/customize_logos.html
Normal file → Executable file
0
app/templates/admin/dependencies.html
Normal file → Executable file
0
app/templates/admin/editing_users.html
Normal file → Executable file
471
app/templates/admin/https_config.html
Normal file
@@ -0,0 +1,471 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}HTTPS Configuration - DigiServer v2{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="page-header">
|
||||
<a href="{{ url_for('admin.admin_panel') }}" class="back-link">← Back to Admin Panel</a>
|
||||
<h1>🔒 HTTPS Configuration</h1>
|
||||
</div>
|
||||
|
||||
<div class="https-config-container">
|
||||
<!-- Status Display -->
|
||||
<div class="card status-card">
|
||||
<h2>Current Status</h2>
|
||||
{% if config and config.https_enabled %}
|
||||
<div class="status-enabled">
|
||||
<span class="status-badge">✅ HTTPS ENABLED</span>
|
||||
<div class="status-details">
|
||||
<p><strong>Domain:</strong> {{ config.domain }}</p>
|
||||
<p><strong>Hostname:</strong> {{ config.hostname }}</p>
|
||||
<p><strong>Email:</strong> {{ config.email }}</p>
|
||||
<p><strong>IP Address:</strong> {{ config.ip_address }}</p>
|
||||
<p><strong>Port:</strong> {{ config.port }}</p>
|
||||
<p><strong>Access URL:</strong> <code>https://{{ config.domain }}</code></p>
|
||||
<p><strong>Last Updated:</strong> {{ config.updated_at.strftime('%Y-%m-%d %H:%M:%S') }} by {{ config.updated_by }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="status-disabled">
|
||||
<span class="status-badge-inactive">⚠️ HTTPS DISABLED</span>
|
||||
<p>The application is currently running on HTTP only (port 80)</p>
|
||||
<p>Enable HTTPS below to secure your application.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Configuration Form -->
|
||||
<div class="card config-card">
|
||||
<h2>Configure HTTPS Settings</h2>
|
||||
<p class="info-text">
|
||||
💡 <strong>Workflow:</strong> First, the app runs on HTTP (port 80). After you configure the HTTPS settings below,
|
||||
the application will be available over HTTPS (port 443) using the domain and hostname you specify.
|
||||
</p>
|
||||
|
||||
<form method="POST" action="{{ url_for('admin.update_https_config') }}" class="https-form">
|
||||
<!-- Enable HTTPS Toggle -->
|
||||
<div class="form-group">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" name="https_enabled" id="https_enabled"
|
||||
{% if config and config.https_enabled %}checked{% endif %}
|
||||
class="toggle-input">
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="toggle-text">Enable HTTPS</span>
|
||||
</label>
|
||||
<p class="form-hint">Check this box to enable HTTPS/SSL for your application</p>
|
||||
</div>
|
||||
|
||||
<!-- Hostname Field -->
|
||||
<div class="form-group">
|
||||
<label for="hostname">Hostname <span class="required">*</span></label>
|
||||
<input type="text" id="hostname" name="hostname"
|
||||
value="{{ config.hostname or 'digiserver' }}"
|
||||
placeholder="e.g., digiserver"
|
||||
class="form-input"
|
||||
required>
|
||||
<p class="form-hint">Short name for your server (e.g., 'digiserver')</p>
|
||||
</div>
|
||||
|
||||
<!-- Domain Field -->
|
||||
<div class="form-group">
|
||||
<label for="domain">Full Domain Name <span class="required">*</span></label>
|
||||
<input type="text" id="domain" name="domain"
|
||||
value="{{ config.domain or 'digiserver.sibiusb.harting.intra' }}"
|
||||
placeholder="e.g., digiserver.sibiusb.harting.intra"
|
||||
class="form-input"
|
||||
required>
|
||||
<p class="form-hint">Complete domain name (e.g., digiserver.sibiusb.harting.intra)</p>
|
||||
</div>
|
||||
|
||||
<!-- IP Address Field -->
|
||||
<div class="form-group">
|
||||
<label for="ip_address">IP Address <span class="required">*</span></label>
|
||||
<input type="text" id="ip_address" name="ip_address"
|
||||
value="{{ config.ip_address or '10.76.152.164' }}"
|
||||
placeholder="e.g., 10.76.152.164"
|
||||
class="form-input"
|
||||
required>
|
||||
<p class="form-hint">Server's IP address for direct access (e.g., 10.76.152.164)</p>
|
||||
</div>
|
||||
|
||||
<!-- Email Field -->
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address <span class="required">*</span></label>
|
||||
<input type="email" id="email" name="email"
|
||||
value="{{ config.email or '' }}"
|
||||
placeholder="e.g., admin@example.com"
|
||||
class="form-input"
|
||||
required>
|
||||
<p class="form-hint">Email address for SSL certificate notifications and Let's Encrypt communications</p>
|
||||
</div>
|
||||
|
||||
<!-- Port Field -->
|
||||
<div class="form-group">
|
||||
<label for="port">HTTPS Port</label>
|
||||
<input type="number" id="port" name="port"
|
||||
value="{{ config.port or 443 }}"
|
||||
placeholder="443"
|
||||
min="1" max="65535"
|
||||
class="form-input">
|
||||
<p class="form-hint">Port for HTTPS connections (default: 443)</p>
|
||||
</div>
|
||||
|
||||
<!-- Preview Section -->
|
||||
<div class="preview-section">
|
||||
<h3>Access Points After Configuration:</h3>
|
||||
<ul class="access-points">
|
||||
<li>
|
||||
<strong>HTTPS (Recommended):</strong>
|
||||
<code>https://<span id="preview-domain">digiserver.sibiusb.harting.intra</span></code>
|
||||
</li>
|
||||
<li>
|
||||
<strong>HTTP (Fallback):</strong>
|
||||
<code>http://<span id="preview-ip">10.76.152.164</span></code>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
💾 Save HTTPS Configuration
|
||||
</button>
|
||||
<a href="{{ url_for('admin.admin_panel') }}" class="btn btn-secondary">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Information Section -->
|
||||
<div class="card info-card">
|
||||
<h2>ℹ️ Important Information</h2>
|
||||
<div class="info-sections">
|
||||
<div class="info-section">
|
||||
<h3>📝 Before You Start</h3>
|
||||
<ul>
|
||||
<li>Ensure your DNS is configured to resolve the domain to your server</li>
|
||||
<li>Verify the IP address matches your server's actual network interface</li>
|
||||
<li>Check that ports 80, 443, and 443/UDP are open for traffic</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="info-section">
|
||||
<h3>🔐 HTTPS Setup</h3>
|
||||
<ul>
|
||||
<li>SSL certificates are automatically managed by Caddy</li>
|
||||
<li>Certificates are obtained from Let's Encrypt</li>
|
||||
<li>Automatic renewal is handled by the system</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="info-section">
|
||||
<h3>✅ After Configuration</h3>
|
||||
<ul>
|
||||
<li>Your app will restart with the new settings</li>
|
||||
<li>Both HTTP and HTTPS access points will be available</li>
|
||||
<li>HTTP requests will be redirected to HTTPS</li>
|
||||
<li>Check the status above for current configuration</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.https-config-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 15px;
|
||||
color: #0066cc;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
margin-bottom: 30px;
|
||||
border-left: 5px solid #ddd;
|
||||
}
|
||||
|
||||
.status-enabled {
|
||||
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
border-left: 5px solid #28a745;
|
||||
}
|
||||
|
||||
.status-disabled {
|
||||
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
border-left: 5px solid #ffc107;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
background: #28a745;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-badge-inactive {
|
||||
display: inline-block;
|
||||
background: #ffc107;
|
||||
color: #333;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-details {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.status-details p {
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-details code {
|
||||
background: rgba(0,0,0,0.1);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.config-card {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
background: #e7f3ff;
|
||||
border-left: 4px solid #0066cc;
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 25px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.https-form {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #0066cc;
|
||||
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* Toggle Switch Styling */
|
||||
.toggle-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.toggle-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 28px;
|
||||
background: #ccc;
|
||||
border-radius: 14px;
|
||||
position: relative;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.toggle-slider::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.toggle-input:checked + .toggle-slider {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.toggle-input:checked + .toggle-slider::after {
|
||||
left: 24px;
|
||||
}
|
||||
|
||||
.toggle-text {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
background: #f8f9fa;
|
||||
border: 2px dashed #0066cc;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 25px 0;
|
||||
}
|
||||
|
||||
.preview-section h3 {
|
||||
margin-top: 0;
|
||||
color: #0066cc;
|
||||
}
|
||||
|
||||
.access-points {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.access-points li {
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
border-left: 4px solid #0066cc;
|
||||
}
|
||||
|
||||
.access-points code {
|
||||
background: #e7f3ff;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #0066cc;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 30px;
|
||||
padding-top: 25px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 12px 30px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: linear-gradient(135deg, #e7f3ff 0%, #f0f7ff 100%);
|
||||
}
|
||||
|
||||
.info-card h2 {
|
||||
color: #0066cc;
|
||||
}
|
||||
|
||||
.info-sections {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.info-section h3 {
|
||||
color: #0066cc;
|
||||
margin-top: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.info-section ul {
|
||||
padding-left: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info-section li {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.https-config-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.info-sections {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Update preview in real-time
|
||||
document.getElementById('domain').addEventListener('input', function() {
|
||||
document.getElementById('preview-domain').textContent = this.value || 'digiserver.sibiusb.harting.intra';
|
||||
});
|
||||
|
||||
document.getElementById('ip_address').addEventListener('input', function() {
|
||||
document.getElementById('preview-ip').textContent = this.value || '10.76.152.164';
|
||||
});
|
||||
|
||||
// Load initial preview
|
||||
document.getElementById('preview-domain').textContent = document.getElementById('domain').value || 'digiserver.sibiusb.harting.intra';
|
||||
document.getElementById('preview-ip').textContent = document.getElementById('ip_address').value || '10.76.152.164';
|
||||
</script>
|
||||
{% endblock %}
|
||||
0
app/templates/admin/leftover_media.html
Normal file → Executable file
0
app/templates/admin/user_management.html
Normal file → Executable file
0
app/templates/auth/change_password.html
Normal file → Executable file
0
app/templates/auth/login.html
Normal file → Executable file
0
app/templates/auth/register.html
Normal file → Executable file
2
app/templates/base.html
Normal file → Executable file
@@ -376,7 +376,7 @@
|
||||
<header>
|
||||
<div class="container">
|
||||
<h1>
|
||||
<img src="{{ url_for('static', filename='uploads/header_logo.png') }}" alt="DigiServer" style="height: 32px; width: auto;" onerror="this.src='{{ url_for('static', filename='icons/monitor.svg') }}'; this.style.filter='brightness(0) invert(1)'; this.style.width='28px'; this.style.height='28px';">
|
||||
<img src="{{ url_for('static', filename='uploads/header_logo.png?v=1') }}" alt="DigiServer" style="height: 32px; width: auto; margin-right: 8px;" onerror="this.style.display='none';" onload="this.style.display='inline';">
|
||||
DigiServer
|
||||
</h1>
|
||||
<nav>
|
||||
|
||||
0
app/templates/content/content_list.html
Normal file → Executable file
0
app/templates/content/content_list_new.html
Normal file → Executable file
0
app/templates/content/edit_content.html
Normal file → Executable file
0
app/templates/content/manage_playlist_content.html
Normal file → Executable file
0
app/templates/content/media_library.html
Normal file → Executable file
0
app/templates/content/upload_content.html
Normal file → Executable file
0
app/templates/content/upload_media.html
Normal file → Executable file
0
app/templates/dashboard.html
Normal file → Executable file
0
app/templates/errors/403.html
Normal file → Executable file
0
app/templates/errors/404.html
Normal file → Executable file
0
app/templates/errors/500.html
Normal file → Executable file
0
app/templates/groups/create_group.html
Normal file → Executable file
0
app/templates/groups/edit_group.html
Normal file → Executable file
0
app/templates/groups/group_fullscreen.html
Normal file → Executable file
0
app/templates/groups/groups_list.html
Normal file → Executable file
0
app/templates/groups/manage_group.html
Normal file → Executable file
0
app/templates/players/add_player.html
Normal file → Executable file
0
app/templates/players/edit_player.html
Normal file → Executable file
0
app/templates/players/edited_media.html
Normal file → Executable file
0
app/templates/players/manage_player.html
Normal file → Executable file
0
app/templates/players/player_fullscreen.html
Normal file → Executable file
0
app/templates/players/player_page.html
Normal file → Executable file
0
app/templates/players/players_list.html
Normal file → Executable file
0
app/templates/playlist/manage_playlist.html
Normal file → Executable file
0
app/utils/__init__.py
Normal file → Executable file
154
app/utils/caddy_manager.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""Caddy configuration generator and manager."""
|
||||
import os
|
||||
from typing import Optional
|
||||
from app.models.https_config import HTTPSConfig
|
||||
|
||||
|
||||
class CaddyConfigGenerator:
|
||||
"""Generate Caddyfile configuration based on HTTPSConfig."""
|
||||
|
||||
@staticmethod
|
||||
def generate_caddyfile(config: Optional[HTTPSConfig] = None) -> str:
|
||||
"""Generate complete Caddyfile content.
|
||||
|
||||
Args:
|
||||
config: HTTPSConfig instance or None
|
||||
|
||||
Returns:
|
||||
Complete Caddyfile content as string
|
||||
"""
|
||||
# Get config from database if not provided
|
||||
if config is None:
|
||||
config = HTTPSConfig.get_config()
|
||||
|
||||
# Base configuration
|
||||
email = "admin@localhost"
|
||||
if config and config.email:
|
||||
email = config.email
|
||||
|
||||
base_config = f"""{{
|
||||
# Global options
|
||||
email {email}
|
||||
# Admin API for configuration management (listen on all interfaces)
|
||||
admin 0.0.0.0:2019
|
||||
# Uncomment for testing to avoid rate limits
|
||||
# acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
}}
|
||||
|
||||
# Shared reverse proxy configuration
|
||||
(reverse_proxy_config) {{
|
||||
reverse_proxy digiserver-app:5000 {{
|
||||
header_up Host {{host}}
|
||||
header_up X-Real-IP {{remote_host}}
|
||||
header_up X-Forwarded-Proto {{scheme}}
|
||||
|
||||
# Timeouts for large uploads
|
||||
transport http {{
|
||||
read_timeout 300s
|
||||
write_timeout 300s
|
||||
}}
|
||||
}}
|
||||
|
||||
# File upload size limit (2GB)
|
||||
request_body {{
|
||||
max_size 2GB
|
||||
}}
|
||||
|
||||
# Security headers
|
||||
header {{
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-XSS-Protection "1; mode=block"
|
||||
}}
|
||||
|
||||
# Logging
|
||||
log {{
|
||||
output file /var/log/caddy/access.log
|
||||
}}
|
||||
}}
|
||||
|
||||
# Localhost (development/local access)
|
||||
http://localhost {{
|
||||
import reverse_proxy_config
|
||||
}}
|
||||
"""
|
||||
|
||||
# Add main domain/IP configuration if HTTPS is enabled
|
||||
if config and config.https_enabled and config.domain and config.ip_address:
|
||||
# Internal domain configuration
|
||||
domain_config = f"""
|
||||
# Internal domain (HTTP only - internal use)
|
||||
http://{config.domain} {{
|
||||
import reverse_proxy_config
|
||||
}}
|
||||
|
||||
# Handle IP address access
|
||||
http://{config.ip_address} {{
|
||||
import reverse_proxy_config
|
||||
}}
|
||||
"""
|
||||
base_config += domain_config
|
||||
else:
|
||||
# Default fallback configuration
|
||||
base_config += """
|
||||
# Internal domain (HTTP only - internal use)
|
||||
http://digiserver.sibiusb.harting.intra {
|
||||
import reverse_proxy_config
|
||||
}
|
||||
|
||||
# Handle IP address access
|
||||
http://10.76.152.164 {
|
||||
import reverse_proxy_config
|
||||
}
|
||||
"""
|
||||
|
||||
# Add catch-all for any other HTTP requests
|
||||
base_config += """
|
||||
# Catch-all for any other HTTP requests
|
||||
http://* {
|
||||
import reverse_proxy_config
|
||||
}
|
||||
"""
|
||||
|
||||
return base_config
|
||||
|
||||
@staticmethod
|
||||
def write_caddyfile(caddyfile_content: str, path: str = '/app/Caddyfile') -> bool:
|
||||
"""Write Caddyfile to disk.
|
||||
|
||||
Args:
|
||||
caddyfile_content: Content to write
|
||||
path: Path to Caddyfile
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
with open(path, 'w') as f:
|
||||
f.write(caddyfile_content)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error writing Caddyfile: {str(e)}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def reload_caddy() -> bool:
|
||||
"""Reload Caddy configuration without restart.
|
||||
|
||||
Note: Caddy monitoring is handled via file watching. After writing the Caddyfile,
|
||||
Caddy should automatically reload. If it doesn't, you may need to restart the
|
||||
Caddy container manually.
|
||||
|
||||
Returns:
|
||||
True if configuration was written successfully (Caddy will auto-reload)
|
||||
"""
|
||||
try:
|
||||
# Just verify that Caddy is reachable
|
||||
import urllib.request
|
||||
response = urllib.request.urlopen('http://caddy:2019/config/', timeout=2)
|
||||
return response.status == 200
|
||||
except Exception as e:
|
||||
# Caddy might not be reachable, but Caddyfile was already written
|
||||
# Caddy should reload automatically when it detects file changes
|
||||
print(f"Note: Caddy reload check returned: {str(e)}")
|
||||
return True # Return True anyway since Caddyfile was written
|
||||