updated to get card name
This commit is contained in:
35
Caddyfile
35
Caddyfile
@@ -8,6 +8,41 @@
|
|||||||
{$DOMAIN:localhost} {
|
{$DOMAIN:localhost} {
|
||||||
# Automatic HTTPS (Caddy handles Let's Encrypt automatically)
|
# 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 to Flask app
|
||||||
reverse_proxy digiserver:5000 {
|
reverse_proxy digiserver:5000 {
|
||||||
# Headers
|
# Headers
|
||||||
|
|||||||
@@ -5,15 +5,18 @@ FROM python:3.13-slim
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install system dependencies including LibreOffice for PPTX conversion
|
# Install system dependencies including LibreOffice for PPTX conversion
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
poppler-utils \
|
poppler-utils \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
libmagic1 \
|
libmagic1 \
|
||||||
sudo \
|
sudo \
|
||||||
fonts-noto-color-emoji \
|
fonts-noto-color-emoji \
|
||||||
libreoffice \
|
libreoffice-core \
|
||||||
libreoffice-impress \
|
libreoffice-impress \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
libreoffice-writer \
|
||||||
|
&& apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy requirements first for better caching
|
# Copy requirements first for better caching
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
|
|||||||
@@ -772,3 +772,79 @@ def upload_login_logo():
|
|||||||
flash(f'Error uploading logo: {str(e)}', 'danger')
|
flash(f'Error uploading logo: {str(e)}', 'danger')
|
||||||
|
|
||||||
return redirect(url_for('admin.customize_logos'))
|
return redirect(url_for('admin.customize_logos'))
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route('/editing-users')
|
||||||
|
@login_required
|
||||||
|
@admin_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
|
||||||
|
@admin_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
|
||||||
|
@admin_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'))
|
||||||
|
|||||||
@@ -794,13 +794,23 @@ def receive_edited_media():
|
|||||||
except:
|
except:
|
||||||
time_of_mod = datetime.utcnow()
|
time_of_mod = datetime.utcnow()
|
||||||
|
|
||||||
|
# Auto-create PlayerUser record if user code is provided
|
||||||
|
user_code = metadata.get('user')
|
||||||
|
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}')
|
||||||
|
|
||||||
edit_record = PlayerEdit(
|
edit_record = PlayerEdit(
|
||||||
player_id=player.id,
|
player_id=player.id,
|
||||||
content_id=content.id,
|
content_id=content.id,
|
||||||
original_name=original_name,
|
original_name=original_name,
|
||||||
new_name=new_filename,
|
new_name=new_filename,
|
||||||
version=version,
|
version=version,
|
||||||
user=metadata.get('user'),
|
user=user_code,
|
||||||
time_of_modification=time_of_mod,
|
time_of_modification=time_of_mod,
|
||||||
metadata_path=metadata_path,
|
metadata_path=metadata_path,
|
||||||
edited_file_path=edited_file_path
|
edited_file_path=edited_file_path
|
||||||
|
|||||||
@@ -343,6 +343,7 @@ def edited_media(player_id: int):
|
|||||||
|
|
||||||
# Get all edited media history from player
|
# Get all edited media history from player
|
||||||
from app.models.player_edit import PlayerEdit
|
from app.models.player_edit import PlayerEdit
|
||||||
|
|
||||||
edited_media = PlayerEdit.query.filter_by(player_id=player_id)\
|
edited_media = PlayerEdit.query.filter_by(player_id=player_id)\
|
||||||
.order_by(PlayerEdit.created_at.desc())\
|
.order_by(PlayerEdit.created_at.desc())\
|
||||||
.all()
|
.all()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from app.models.content import Content
|
|||||||
from app.models.server_log import ServerLog
|
from app.models.server_log import ServerLog
|
||||||
from app.models.player_feedback import PlayerFeedback
|
from app.models.player_feedback import PlayerFeedback
|
||||||
from app.models.player_edit import PlayerEdit
|
from app.models.player_edit import PlayerEdit
|
||||||
|
from app.models.player_user import PlayerUser
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'User',
|
'User',
|
||||||
@@ -17,6 +18,7 @@ __all__ = [
|
|||||||
'ServerLog',
|
'ServerLog',
|
||||||
'PlayerFeedback',
|
'PlayerFeedback',
|
||||||
'PlayerEdit',
|
'PlayerEdit',
|
||||||
|
'PlayerUser',
|
||||||
'group_content',
|
'group_content',
|
||||||
'playlist_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,
|
||||||
|
}
|
||||||
@@ -56,6 +56,9 @@
|
|||||||
<a href="{{ url_for('admin.user_management') }}" class="btn btn-primary">
|
<a href="{{ url_for('admin.user_management') }}" class="btn btn-primary">
|
||||||
Manage Users
|
Manage Users
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('admin.manage_editing_users') }}" class="btn btn-secondary" style="margin-left: 0.5rem;">
|
||||||
|
Manage Users That Edited Images
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
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 %}
|
||||||
@@ -4,8 +4,8 @@ services:
|
|||||||
digiserver:
|
digiserver:
|
||||||
build: .
|
build: .
|
||||||
container_name: digiserver-v2
|
container_name: digiserver-v2
|
||||||
expose:
|
ports:
|
||||||
- "5000"
|
- "8080:5000" # Expose for nginx reverse proxy
|
||||||
volumes:
|
volumes:
|
||||||
- ./instance:/app/instance
|
- ./instance:/app/instance
|
||||||
- ./app/static/uploads:/app/app/static/uploads
|
- ./app/static/uploads:/app/app/static/uploads
|
||||||
@@ -21,33 +21,37 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
networks:
|
# Commented out network when using external nginx proxy
|
||||||
- digiserver-network
|
# networks:
|
||||||
|
# - digiserver-network
|
||||||
|
|
||||||
caddy:
|
# Caddy reverse proxy (commented out when using external nginx)
|
||||||
image: caddy:2-alpine
|
# Uncomment the section below if you want standalone deployment with automatic HTTPS
|
||||||
container_name: digiserver-caddy
|
# caddy:
|
||||||
ports:
|
# image: caddy:2-alpine
|
||||||
- "80:80"
|
# container_name: digiserver-caddy
|
||||||
- "443:443"
|
# ports:
|
||||||
- "443:443/udp" # HTTP/3
|
# - "80:80"
|
||||||
volumes:
|
# - "443:443"
|
||||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
# - "443:443/udp" # HTTP/3
|
||||||
- caddy-data:/data
|
# volumes:
|
||||||
- caddy-config:/config
|
# - ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
environment:
|
# - caddy-data:/data
|
||||||
- DOMAIN=${DOMAIN:-localhost}
|
# - caddy-config:/config
|
||||||
- EMAIL=${EMAIL:-admin@localhost}
|
# environment:
|
||||||
depends_on:
|
# - DOMAIN=${DOMAIN:-localhost}
|
||||||
- digiserver
|
# - EMAIL=${EMAIL:-admin@localhost}
|
||||||
restart: unless-stopped
|
# depends_on:
|
||||||
networks:
|
# - digiserver
|
||||||
- digiserver-network
|
# restart: unless-stopped
|
||||||
|
# networks:
|
||||||
|
# - digiserver-network
|
||||||
|
|
||||||
networks:
|
# Commented out when using external nginx proxy
|
||||||
digiserver-network:
|
# networks:
|
||||||
driver: bridge
|
# digiserver-network:
|
||||||
|
# driver: bridge
|
||||||
|
|
||||||
volumes:
|
# volumes:
|
||||||
caddy-data:
|
# caddy-data:
|
||||||
caddy-config:
|
# caddy-config:
|
||||||
|
|||||||
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")
|
||||||
Reference in New Issue
Block a user