Compare commits
5 Commits
328edebe3c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3829d98e91 | ||
|
|
88e24f8fec | ||
|
|
87709bab4d | ||
|
|
0dfeb0ef7f | ||
|
|
4a9616a0f7 |
78
Caddyfile
Normal file
78
Caddyfile
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
# Global options
|
||||
email {$EMAIL}
|
||||
# Uncomment for testing to avoid rate limits
|
||||
# acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
}
|
||||
|
||||
{$DOMAIN:localhost} {
|
||||
# Automatic HTTPS (Caddy handles Let's Encrypt automatically)
|
||||
|
||||
# Reverse proxy to Flask app
|
||||
reverse_proxy digiserver:5000 {
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Forwarded-For {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 {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-XSS-Protection "1; mode=block"
|
||||
}
|
||||
|
||||
# Logging
|
||||
log {
|
||||
output file /var/log/caddy/access.log
|
||||
}
|
||||
}
|
||||
|
||||
# Handle IP address access without automatic HTTPS
|
||||
http://192.168.0.206 {
|
||||
# Reverse proxy to Flask app
|
||||
reverse_proxy digiserver:5000 {
|
||||
# Headers
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Forwarded-For {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 {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-XSS-Protection "1; mode=block"
|
||||
}
|
||||
|
||||
# Logging
|
||||
log {
|
||||
output file /var/log/caddy/access.log
|
||||
}
|
||||
}
|
||||
13
Dockerfile
13
Dockerfile
@@ -4,16 +4,19 @@ FROM python:3.13-slim
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
# Note: LibreOffice is excluded from the base image to reduce size (~500MB)
|
||||
# It can be installed on-demand via the Admin Panel → System Dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
# Install system dependencies including LibreOffice for PPTX conversion
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
poppler-utils \
|
||||
ffmpeg \
|
||||
libmagic1 \
|
||||
sudo \
|
||||
fonts-noto-color-emoji \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
libreoffice-core \
|
||||
libreoffice-impress \
|
||||
libreoffice-writer \
|
||||
&& apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements first for better caching
|
||||
COPY requirements.txt .
|
||||
|
||||
@@ -31,7 +31,6 @@ def admin_required(f):
|
||||
|
||||
@admin_bp.route('/')
|
||||
@login_required
|
||||
@admin_required
|
||||
def admin_panel():
|
||||
"""Display admin panel with system overview."""
|
||||
try:
|
||||
@@ -351,7 +350,6 @@ def system_info():
|
||||
|
||||
@admin_bp.route('/leftover-media')
|
||||
@login_required
|
||||
@admin_required
|
||||
def leftover_media():
|
||||
"""Display leftover media files not assigned to any playlist."""
|
||||
from app.models.playlist import playlist_content
|
||||
@@ -374,12 +372,15 @@ def leftover_media():
|
||||
leftover_pdfs = [c for c in leftover_content if c.content_type == 'pdf']
|
||||
leftover_pptx = [c for c in leftover_content if c.content_type == 'pptx']
|
||||
|
||||
# Calculate storage
|
||||
total_leftover_size = sum(c.file_size for c in leftover_content)
|
||||
images_size = sum(c.file_size for c in leftover_images)
|
||||
videos_size = sum(c.file_size for c in leftover_videos)
|
||||
pdfs_size = sum(c.file_size for c in leftover_pdfs)
|
||||
pptx_size = sum(c.file_size for c in leftover_pptx)
|
||||
# Calculate storage (handle None values)
|
||||
def safe_file_size(content_list):
|
||||
return sum(c.file_size or 0 for c in content_list)
|
||||
|
||||
total_leftover_size = safe_file_size(leftover_content)
|
||||
images_size = safe_file_size(leftover_images)
|
||||
videos_size = safe_file_size(leftover_videos)
|
||||
pdfs_size = safe_file_size(leftover_pdfs)
|
||||
pptx_size = safe_file_size(leftover_pptx)
|
||||
|
||||
return render_template('admin/leftover_media.html',
|
||||
leftover_images=leftover_images,
|
||||
@@ -401,7 +402,6 @@ def leftover_media():
|
||||
|
||||
@admin_bp.route('/delete-leftover-images', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_leftover_images():
|
||||
"""Delete all leftover images that are not part of any playlist"""
|
||||
from app.models.playlist import playlist_content
|
||||
@@ -457,7 +457,6 @@ def delete_leftover_images():
|
||||
|
||||
@admin_bp.route('/delete-leftover-videos', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_leftover_videos():
|
||||
"""Delete all leftover videos that are not part of any playlist"""
|
||||
from app.models.playlist import playlist_content
|
||||
@@ -513,7 +512,6 @@ def delete_leftover_videos():
|
||||
|
||||
@admin_bp.route('/delete-single-leftover/<int:content_id>', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_single_leftover(content_id):
|
||||
"""Delete a single leftover content file"""
|
||||
try:
|
||||
@@ -772,3 +770,76 @@ def upload_login_logo():
|
||||
flash(f'Error uploading logo: {str(e)}', 'danger')
|
||||
|
||||
return redirect(url_for('admin.customize_logos'))
|
||||
|
||||
|
||||
@admin_bp.route('/editing-users')
|
||||
@login_required
|
||||
def manage_editing_users():
|
||||
"""Display and manage users that edit images on players."""
|
||||
try:
|
||||
from app.models.player_user import PlayerUser
|
||||
from app.models.player_edit import PlayerEdit
|
||||
|
||||
# Get all editing users
|
||||
users = PlayerUser.query.order_by(PlayerUser.created_at.desc()).all()
|
||||
|
||||
# Get edit counts for each user
|
||||
user_stats = {}
|
||||
for user in users:
|
||||
edit_count = PlayerEdit.query.filter_by(user=user.user_code).count()
|
||||
user_stats[user.user_code] = edit_count
|
||||
|
||||
return render_template('admin/editing_users.html',
|
||||
users=users,
|
||||
user_stats=user_stats)
|
||||
except Exception as e:
|
||||
log_action('error', f'Error loading editing users: {str(e)}')
|
||||
flash('Error loading editing users.', 'danger')
|
||||
return redirect(url_for('admin.admin_panel'))
|
||||
|
||||
|
||||
@admin_bp.route('/editing-users/<int:user_id>/update', methods=['POST'])
|
||||
@login_required
|
||||
def update_editing_user(user_id: int):
|
||||
"""Update editing user name."""
|
||||
try:
|
||||
from app.models.player_user import PlayerUser
|
||||
|
||||
user = PlayerUser.query.get_or_404(user_id)
|
||||
user_name = request.form.get('user_name', '').strip()
|
||||
|
||||
user.user_name = user_name if user_name else None
|
||||
user.updated_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
log_action('info', f'Updated editing user {user.user_code} name to: {user_name or "None"}')
|
||||
flash('User name updated successfully!', 'success')
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
log_action('error', f'Error updating editing user: {str(e)}')
|
||||
flash(f'Error updating user: {str(e)}', 'danger')
|
||||
|
||||
return redirect(url_for('admin.manage_editing_users'))
|
||||
|
||||
|
||||
@admin_bp.route('/editing-users/<int:user_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def delete_editing_user(user_id: int):
|
||||
"""Delete editing user."""
|
||||
try:
|
||||
from app.models.player_user import PlayerUser
|
||||
|
||||
user = PlayerUser.query.get_or_404(user_id)
|
||||
user_code = user.user_code
|
||||
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
|
||||
log_action('info', f'Deleted editing user: {user_code}')
|
||||
flash('User deleted successfully!', 'success')
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
log_action('error', f'Error deleting editing user: {str(e)}')
|
||||
flash(f'Error deleting user: {str(e)}', 'danger')
|
||||
|
||||
return redirect(url_for('admin.manage_editing_users'))
|
||||
|
||||
@@ -777,14 +777,10 @@ def receive_edited_media():
|
||||
with open(metadata_path, 'w') as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
|
||||
# Copy the versioned image to the main uploads folder
|
||||
import shutil
|
||||
versioned_upload_path = os.path.join(base_upload_dir, new_filename)
|
||||
shutil.copy2(edited_file_path, versioned_upload_path)
|
||||
|
||||
# Update the content record to reference the new versioned filename
|
||||
# Update the content record to reference the edited version path
|
||||
# Keep original filename unchanged, point to edited_media folder
|
||||
old_filename = content.filename
|
||||
content.filename = new_filename
|
||||
content.filename = f"edited_media/{content.id}/{new_filename}"
|
||||
|
||||
# Create edit record
|
||||
time_of_mod = None
|
||||
@@ -794,13 +790,28 @@ def receive_edited_media():
|
||||
except:
|
||||
time_of_mod = datetime.utcnow()
|
||||
|
||||
# Auto-create PlayerUser record if user code is provided
|
||||
user_code = metadata.get('user_card_data')
|
||||
log_action('debug', f'Metadata user code: {user_code}')
|
||||
if user_code:
|
||||
from app.models.player_user import PlayerUser
|
||||
existing_user = PlayerUser.query.filter_by(user_code=user_code).first()
|
||||
if not existing_user:
|
||||
new_user = PlayerUser(user_code=user_code)
|
||||
db.session.add(new_user)
|
||||
log_action('info', f'Auto-created PlayerUser record for code: {user_code}')
|
||||
else:
|
||||
log_action('debug', f'PlayerUser already exists for code: {user_code}')
|
||||
else:
|
||||
log_action('debug', 'No user code in metadata')
|
||||
|
||||
edit_record = PlayerEdit(
|
||||
player_id=player.id,
|
||||
content_id=content.id,
|
||||
original_name=original_name,
|
||||
new_name=new_filename,
|
||||
version=version,
|
||||
user=metadata.get('user'),
|
||||
user=user_code,
|
||||
time_of_modification=time_of_mod,
|
||||
metadata_path=metadata_path,
|
||||
edited_file_path=edited_file_path
|
||||
|
||||
@@ -343,6 +343,8 @@ def edited_media(player_id: int):
|
||||
|
||||
# Get all edited media history from player
|
||||
from app.models.player_edit import PlayerEdit
|
||||
from app.models.player_user import PlayerUser
|
||||
|
||||
edited_media = PlayerEdit.query.filter_by(player_id=player_id)\
|
||||
.order_by(PlayerEdit.created_at.desc())\
|
||||
.all()
|
||||
@@ -355,10 +357,21 @@ def edited_media(player_id: int):
|
||||
if content:
|
||||
content_files[edit.content_id] = content
|
||||
|
||||
# Get user mappings for display names
|
||||
user_mappings = {}
|
||||
for edit in edited_media:
|
||||
if edit.user and edit.user not in user_mappings:
|
||||
player_user = PlayerUser.query.filter_by(user_code=edit.user).first()
|
||||
if player_user:
|
||||
user_mappings[edit.user] = player_user.user_name or edit.user
|
||||
else:
|
||||
user_mappings[edit.user] = edit.user
|
||||
|
||||
return render_template('players/edited_media.html',
|
||||
player=player,
|
||||
edited_media=edited_media,
|
||||
content_files=content_files)
|
||||
content_files=content_files,
|
||||
user_mappings=user_mappings)
|
||||
except Exception as e:
|
||||
log_action('error', f'Error loading edited media for player {player_id}: {str(e)}')
|
||||
flash('Error loading edited media.', 'danger')
|
||||
|
||||
@@ -87,9 +87,6 @@ class ProductionConfig(Config):
|
||||
# Security
|
||||
SESSION_COOKIE_SECURE = True
|
||||
WTF_CSRF_ENABLED = True
|
||||
|
||||
# Rate Limiting
|
||||
RATELIMIT_STORAGE_URL = f"redis://{os.getenv('REDIS_HOST', 'redis')}:6379/1"
|
||||
|
||||
|
||||
class TestingConfig(Config):
|
||||
|
||||
@@ -7,6 +7,7 @@ from app.models.content import Content
|
||||
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
|
||||
|
||||
__all__ = [
|
||||
'User',
|
||||
@@ -17,6 +18,7 @@ __all__ = [
|
||||
'ServerLog',
|
||||
'PlayerFeedback',
|
||||
'PlayerEdit',
|
||||
'PlayerUser',
|
||||
'group_content',
|
||||
'playlist_content',
|
||||
]
|
||||
|
||||
37
app/models/player_user.py
Normal file
37
app/models/player_user.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Player user model for managing user codes and names."""
|
||||
from datetime import datetime
|
||||
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
class PlayerUser(db.Model):
|
||||
"""Player user model for managing user codes and names globally.
|
||||
|
||||
Attributes:
|
||||
id: Primary key
|
||||
user_code: User code received from player (unique)
|
||||
user_name: Display name for the user
|
||||
created_at: Record creation timestamp
|
||||
updated_at: Record update timestamp
|
||||
"""
|
||||
__tablename__ = 'player_user'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_code = db.Column(db.String(255), nullable=False, unique=True, index=True)
|
||||
user_name = db.Column(db.String(255), nullable=True)
|
||||
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)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of PlayerUser."""
|
||||
return f'<PlayerUser {self.user_code} -> {self.user_name or "Unnamed"}>'
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert to dictionary for API responses."""
|
||||
return {
|
||||
'id': self.id,
|
||||
'user_code': self.user_code,
|
||||
'user_name': self.user_name,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
@@ -48,7 +48,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Management Card -->
|
||||
{% if current_user.is_admin %}
|
||||
<!-- User Management Card (Admin Only) -->
|
||||
<div class="card management-card">
|
||||
<h2>👥 User Management</h2>
|
||||
<p>Manage application users, roles and permissions</p>
|
||||
@@ -58,6 +59,18 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Editing Users Card -->
|
||||
<div class="card management-card">
|
||||
<h2>✏️ Editing Users</h2>
|
||||
<p>Manage user codes from players that edit images on-screen</p>
|
||||
<div class="card-actions">
|
||||
<a href="{{ url_for('admin.manage_editing_users') }}" class="btn btn-primary">
|
||||
Manage Editing Users
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leftover Media Management Card -->
|
||||
<div class="card management-card">
|
||||
@@ -70,7 +83,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Dependencies Card -->
|
||||
{% if current_user.is_admin %}
|
||||
<!-- System Dependencies Card (Admin Only) -->
|
||||
<div class="card management-card" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
|
||||
<h2>🔧 System Dependencies</h2>
|
||||
<p>Check and install required software dependencies</p>
|
||||
@@ -81,7 +95,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logo Customization Card -->
|
||||
<!-- Logo Customization Card (Admin Only) -->
|
||||
<div class="card management-card" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);">
|
||||
<h2>🎨 Logo Customization</h2>
|
||||
<p>Upload custom logos for header and login page</p>
|
||||
@@ -91,6 +105,7 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Quick Actions Card -->
|
||||
<div class="card">
|
||||
|
||||
105
app/templates/admin/editing_users.html
Normal file
105
app/templates/admin/editing_users.html
Normal file
@@ -0,0 +1,105 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Manage Editing Users{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<a href="{{ url_for('admin.admin_panel') }}"
|
||||
class="btn"
|
||||
style="background: #6c757d; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 6px;">
|
||||
← Back to Admin
|
||||
</a>
|
||||
<h1 style="margin: 0;">👤 Manage Editing Users</h1>
|
||||
</div>
|
||||
</div>
|
||||
<p style="color: #6c757d;">Manage users who edit images on players. User codes are automatically created from player metadata.</p>
|
||||
</div>
|
||||
|
||||
{% if users %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 style="margin: 0;">Editing Users ({{ users|length }})</h3>
|
||||
</div>
|
||||
<div class="card-body" style="padding: 0;">
|
||||
<table class="table" style="margin: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 30%;">User Code</th>
|
||||
<th style="width: 30%;">Display Name</th>
|
||||
<th style="width: 15%;">Edits Count</th>
|
||||
<th style="width: 15%;">Created</th>
|
||||
<th style="width: 10%;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td style="font-family: monospace; font-weight: 600;">{{ user.user_code }}</td>
|
||||
<td>
|
||||
<form method="POST" action="{{ url_for('admin.update_editing_user', user_id=user.id) }}" style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<input type="text"
|
||||
name="user_name"
|
||||
value="{{ user.user_name or '' }}"
|
||||
placeholder="Enter display name"
|
||||
class="form-control"
|
||||
style="flex: 1;">
|
||||
<button type="submit" class="btn btn-sm btn-primary">Save</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-info">{{ user_stats.get(user.user_code, 0) }} edits</span>
|
||||
</td>
|
||||
<td>{{ user.created_at | localtime('%Y-%m-%d %H:%M') }}</td>
|
||||
<td>
|
||||
<form method="POST"
|
||||
action="{{ url_for('admin.delete_editing_user', user_id=user.id) }}"
|
||||
style="display: inline;"
|
||||
onsubmit="return confirm('Are you sure you want to delete this user? This will not delete their edit history.');">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card" style="text-align: center; padding: 4rem 2rem;">
|
||||
<div style="font-size: 4rem; margin-bottom: 1rem; opacity: 0.5;">👤</div>
|
||||
<h2 style="color: #6c757d; margin-bottom: 1rem;">No Editing Users Yet</h2>
|
||||
<p style="color: #6c757d; font-size: 1.1rem;">
|
||||
User codes will appear here automatically when players edit media files.
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<style>
|
||||
body.dark-mode .table {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode .table thead th {
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
body.dark-mode .table tbody tr {
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
body.dark-mode .table tbody tr:hover {
|
||||
background: #2d3748;
|
||||
}
|
||||
|
||||
body.dark-mode .form-control {
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
border-color: #4a5568;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
||||
@@ -393,9 +393,7 @@
|
||||
<img src="{{ url_for('static', filename='icons/playlist.svg') }}" alt="">
|
||||
Playlists
|
||||
</a>
|
||||
{% if current_user.is_admin %}
|
||||
<a href="{{ url_for('admin.admin_panel') }}">Admin</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('admin.admin_panel') }}">Admin</a>
|
||||
<a href="{{ url_for('auth.logout') }}">Logout ({{ current_user.username }})</a>
|
||||
<button class="dark-mode-toggle" onclick="toggleDarkMode()" title="Toggle Dark Mode">
|
||||
<img id="theme-icon" src="{{ url_for('static', filename='icons/moon.svg') }}" alt="Toggle theme">
|
||||
|
||||
@@ -376,7 +376,7 @@
|
||||
</div>
|
||||
<div class="preview-info-item">
|
||||
<span class="preview-info-label">👤 Edited by:</span>
|
||||
<span class="preview-info-value" id="info-user-{{ content_id }}">{{ latest.user or 'Unknown' }}</span>
|
||||
<span class="preview-info-value" id="info-user-{{ content_id }}">{{ user_mappings.get(latest.user, latest.user or 'Unknown') }}</span>
|
||||
</div>
|
||||
<div class="preview-info-item">
|
||||
<span class="preview-info-label">🕒 Modified:</span>
|
||||
@@ -398,7 +398,7 @@
|
||||
{% for edit in data.versions|sort(attribute='version', reverse=True) %}
|
||||
<div class="version-item {% if loop.first %}active{% endif %}"
|
||||
id="version-{{ content_id }}-{{ edit.version }}"
|
||||
onclick="event.stopPropagation(); selectVersion({{ content_id }}, {{ edit.version }}, '{{ edit.new_name }}', '{{ edit.user or 'Unknown' }}', '{{ edit.time_of_modification | localtime('%Y-%m-%d %H:%M') if edit.time_of_modification else 'N/A' }}', '{{ edit.created_at | localtime('%Y-%m-%d %H:%M') }}', '{{ url_for('static', filename='uploads/edited_media/' ~ edit.content_id ~ '/' ~ edit.new_name) }}')">
|
||||
onclick="event.stopPropagation(); selectVersion({{ content_id }}, {{ edit.version }}, '{{ edit.new_name }}', '{{ user_mappings.get(edit.user, edit.user or 'Unknown') }}', '{{ edit.time_of_modification | localtime('%Y-%m-%d %H:%M') if edit.time_of_modification else 'N/A' }}', '{{ edit.created_at | localtime('%Y-%m-%d %H:%M') }}', '{{ url_for('static', filename='uploads/edited_media/' ~ edit.content_id ~ '/' ~ edit.new_name) }}')">
|
||||
<div class="version-thumbnail">
|
||||
{% if edit.new_name.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')) %}
|
||||
<img src="{{ url_for('static', filename='uploads/edited_media/' ~ edit.content_id ~ '/' ~ edit.new_name) }}"
|
||||
|
||||
@@ -5,7 +5,7 @@ services:
|
||||
build: .
|
||||
container_name: digiserver-v2
|
||||
ports:
|
||||
- "80:5000"
|
||||
- "8080:5000" # Expose for nginx reverse proxy
|
||||
volumes:
|
||||
- ./instance:/app/instance
|
||||
- ./app/static/uploads:/app/app/static/uploads
|
||||
@@ -21,14 +21,37 @@ services:
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
# Commented out network when using external nginx proxy
|
||||
# networks:
|
||||
# - digiserver-network
|
||||
|
||||
# Optional: Redis for caching (uncomment if needed)
|
||||
# redis:
|
||||
# image: redis:7-alpine
|
||||
# container_name: digiserver-redis
|
||||
# restart: unless-stopped
|
||||
# Caddy reverse proxy (commented out when using external nginx)
|
||||
# Uncomment the section below if you want standalone deployment with automatic HTTPS
|
||||
# caddy:
|
||||
# image: caddy:2-alpine
|
||||
# container_name: digiserver-caddy
|
||||
# ports:
|
||||
# - "80:80"
|
||||
# - "443:443"
|
||||
# - "443:443/udp" # HTTP/3
|
||||
# volumes:
|
||||
# - redis-data:/data
|
||||
# - ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
# - caddy-data:/data
|
||||
# - caddy-config:/config
|
||||
# environment:
|
||||
# - DOMAIN=${DOMAIN:-localhost}
|
||||
# - EMAIL=${EMAIL:-admin@localhost}
|
||||
# depends_on:
|
||||
# - digiserver
|
||||
# restart: unless-stopped
|
||||
# networks:
|
||||
# - digiserver-network
|
||||
|
||||
# Commented out when using external nginx proxy
|
||||
# networks:
|
||||
# digiserver-network:
|
||||
# driver: bridge
|
||||
|
||||
# volumes:
|
||||
# redis-data:
|
||||
# caddy-data:
|
||||
# caddy-config:
|
||||
|
||||
25
fix_player_user_schema.py
Normal file
25
fix_player_user_schema.py
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fix player_user table schema by dropping and recreating it."""
|
||||
import sys
|
||||
sys.path.insert(0, '/app')
|
||||
|
||||
from app.app import create_app
|
||||
from app.extensions import db
|
||||
from app.models.player_user import PlayerUser
|
||||
|
||||
def main():
|
||||
app = create_app('production')
|
||||
with app.app_context():
|
||||
# Drop the old table
|
||||
print("Dropping player_user table...")
|
||||
db.session.execute(db.text('DROP TABLE IF EXISTS player_user'))
|
||||
db.session.commit()
|
||||
|
||||
# Recreate with new schema
|
||||
print("Creating player_user table with new schema...")
|
||||
PlayerUser.__table__.create(db.engine)
|
||||
|
||||
print("Done! player_user table recreated successfully.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
14
migrations/add_player_user_table.py
Normal file
14
migrations/add_player_user_table.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Add player_user table for user code mappings."""
|
||||
import sys
|
||||
sys.path.insert(0, '/app')
|
||||
|
||||
from app.app import create_app
|
||||
from app.extensions import db
|
||||
from app.models.player_user import PlayerUser
|
||||
|
||||
app = create_app()
|
||||
|
||||
with app.app_context():
|
||||
print("Creating player_user table...")
|
||||
db.create_all()
|
||||
print("✓ player_user table created successfully!")
|
||||
24
migrations/migrate_player_user_global.py
Normal file
24
migrations/migrate_player_user_global.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""Migrate player_user table to remove player_id and make user_code unique globally."""
|
||||
import sys
|
||||
sys.path.insert(0, '/app')
|
||||
|
||||
from app.app import create_app
|
||||
from app.extensions import db
|
||||
from sqlalchemy import text
|
||||
|
||||
app = create_app('production')
|
||||
|
||||
with app.app_context():
|
||||
print("Migrating player_user table...")
|
||||
|
||||
# Drop existing table and recreate with new schema
|
||||
db.session.execute(text('DROP TABLE IF EXISTS player_user'))
|
||||
db.session.commit()
|
||||
|
||||
# Create new table
|
||||
db.create_all()
|
||||
|
||||
print("✓ player_user table migrated successfully!")
|
||||
print(" - Removed player_id foreign key")
|
||||
print(" - Made user_code unique globally")
|
||||
print(" - user_name is now nullable")
|
||||
@@ -5,13 +5,13 @@ FLASK_ENV=development
|
||||
# Security
|
||||
SECRET_KEY=change-this-to-a-random-secret-key
|
||||
|
||||
# Domain & SSL (for HTTPS with Caddy)
|
||||
DOMAIN=your-domain.com
|
||||
EMAIL=admin@your-domain.com
|
||||
|
||||
# Database
|
||||
DATABASE_URL=sqlite:///instance/dev.db
|
||||
|
||||
# Redis (for production)
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
|
||||
# Admin User Credentials (used during initial Docker deployment)
|
||||
# These credentials are set when the database is first created
|
||||
ADMIN_USERNAME=admin
|
||||
75
old_code_documentation/HTTPS_SETUP.md
Normal file
75
old_code_documentation/HTTPS_SETUP.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# DigiServer v2 - HTTPS Setup with Caddy
|
||||
|
||||
This setup uses **Caddy** as a reverse proxy with automatic HTTPS via Let's Encrypt.
|
||||
|
||||
## Quick Setup
|
||||
|
||||
### 1. Configure Domain
|
||||
Create a `.env` file or edit the existing one:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env` and set:
|
||||
```
|
||||
DOMAIN=your-domain.com
|
||||
EMAIL=admin@your-domain.com
|
||||
```
|
||||
|
||||
### 2. Point Your Domain
|
||||
Make sure your domain's DNS A record points to your server's IP address.
|
||||
|
||||
### 3. Start Services
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
That's it! Caddy will **automatically**:
|
||||
- Obtain SSL certificates from Let's Encrypt
|
||||
- Renew certificates before expiration
|
||||
- Redirect HTTP to HTTPS
|
||||
- Enable HTTP/2 and HTTP/3
|
||||
|
||||
## Access Your Site
|
||||
|
||||
- **HTTP**: http://your-domain.com (redirects to HTTPS)
|
||||
- **HTTPS**: https://your-domain.com
|
||||
|
||||
## Testing Locally (Without Domain)
|
||||
|
||||
If you don't have a domain yet, leave DOMAIN as `localhost`:
|
||||
```
|
||||
DOMAIN=localhost
|
||||
```
|
||||
|
||||
Then access: http://localhost (no HTTPS, but app works)
|
||||
|
||||
## Certificate Storage
|
||||
|
||||
SSL certificates are stored in Docker volumes:
|
||||
- `caddy-data` - Certificate data
|
||||
- `caddy-config` - Caddy configuration
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Check Caddy logs:
|
||||
```bash
|
||||
docker logs digiserver-caddy
|
||||
```
|
||||
|
||||
### Verify certificates:
|
||||
```bash
|
||||
docker exec digiserver-caddy caddy list-certificates
|
||||
```
|
||||
|
||||
### Force certificate renewal:
|
||||
```bash
|
||||
docker exec digiserver-caddy caddy reload --config /etc/caddy/Caddyfile
|
||||
```
|
||||
|
||||
## Port Forwarding
|
||||
|
||||
Make sure your firewall/router allows:
|
||||
- Port 80 (HTTP - for Let's Encrypt challenge)
|
||||
- Port 443 (HTTPS)
|
||||
@@ -4,7 +4,12 @@
|
||||
|
||||
set -e
|
||||
|
||||
# Get the root directory of the application
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
APP_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
echo "🧹 Cleaning DigiServer v2 for deployment..."
|
||||
echo "📍 App root: $APP_ROOT"
|
||||
echo ""
|
||||
|
||||
# Confirm action
|
||||
@@ -18,6 +23,9 @@ fi
|
||||
echo ""
|
||||
echo "📦 Cleaning development data..."
|
||||
|
||||
# Change to app root directory
|
||||
cd "$APP_ROOT"
|
||||
|
||||
# Remove database files
|
||||
if [ -d "instance" ]; then
|
||||
echo " 🗄️ Removing database files..."
|
||||
@@ -16,9 +16,6 @@ Flask-Caching==2.1.0
|
||||
SQLAlchemy==2.0.37
|
||||
alembic==1.14.1
|
||||
|
||||
# Redis (for caching in production)
|
||||
redis==5.0.1
|
||||
|
||||
# Date parsing
|
||||
python-dateutil==2.9.0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user