Implement admin-only features and server directory access

- Restrict /train route to admin users only (@admin_required)
- Add --server-dir command line argument for server directory configuration
- Show server local path input only for admin users when --server-dir is set
- Add /api/browse endpoint for secure file browsing (admin only)
- Implement file navigator popup with directory navigation and file selection
- Add security checks to prevent directory traversal attacks
- Update analyze template with conditional server path input and file browser modal
parent 1c4e0046
...@@ -32,6 +32,23 @@ ...@@ -32,6 +32,23 @@
.alert { padding: 0.75rem; border-radius: 8px; margin-bottom: 1rem; } .alert { padding: 0.75rem; border-radius: 8px; margin-bottom: 1rem; }
.alert-error { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; } .alert-error { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }
.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; } .alert-success { background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; }
/* File Browser Modal */
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); }
.modal-content { background-color: white; margin: 5% auto; padding: 0; width: 80%; max-width: 800px; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
.modal-header { padding: 1rem 2rem; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; }
.modal-body { padding: 2rem; max-height: 60vh; overflow-y: auto; }
.modal-footer { padding: 1rem 2rem; border-top: 1px solid #e5e7eb; text-align: right; }
.file-list { list-style: none; padding: 0; }
.file-item { display: flex; align-items: center; padding: 0.75rem; border: 1px solid #e5e7eb; border-radius: 8px; margin-bottom: 0.5rem; cursor: pointer; }
.file-item:hover { background: #f8fafc; }
.file-icon { margin-right: 1rem; color: #667eea; }
.file-info { flex: 1; }
.file-name { font-weight: 500; }
.file-size { color: #64748b; font-size: 0.9rem; }
.breadcrumb { margin-bottom: 1rem; padding: 0.75rem; background: #f8fafc; border-radius: 8px; }
.breadcrumb a { color: #667eea; text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; }
</style> </style>
<script> <script>
function updateStats() { function updateStats() {
...@@ -65,6 +82,91 @@ ...@@ -65,6 +82,91 @@
} }
setInterval(updateStats, 5000); setInterval(updateStats, 5000);
window.onload = updateStats; window.onload = updateStats;
function openFileBrowser() {
document.getElementById('fileBrowserModal').style.display = 'block';
loadDirectory('');
}
function closeFileBrowser() {
document.getElementById('fileBrowserModal').style.display = 'none';
}
function loadDirectory(path) {
fetch(`/api/browse?path=${encodeURIComponent(path)}`)
.then(response => response.json())
.then(data => {
if (data.error) {
alert(data.error);
return;
}
// Update breadcrumb
const breadcrumb = document.getElementById('breadcrumb');
const pathParts = path ? path.split('/') : [];
let breadcrumbHtml = '<a href="#" onclick="loadDirectory(\'\')">Home</a>';
let currentPath = '';
pathParts.forEach((part, index) => {
currentPath += (currentPath ? '/' : '') + part;
breadcrumbHtml += ` / <a href="#" onclick="loadDirectory('${currentPath}')">${part}</a>`;
});
breadcrumb.innerHTML = breadcrumbHtml;
// Update file list
const fileList = document.getElementById('fileList');
fileList.innerHTML = '';
data.items.forEach(item => {
const li = document.createElement('li');
li.className = 'file-item';
const icon = item.is_dir ? 'fas fa-folder' : 'fas fa-file';
const size = item.is_dir ? '' : ` (${formatFileSize(item.size)})`;
li.innerHTML = `
<i class="${icon} file-icon"></i>
<div class="file-info">
<div class="file-name">${item.name}</div>
<div class="file-size">${item.is_dir ? 'Directory' : 'File'}${size}</div>
</div>
`;
if (item.is_dir) {
li.onclick = () => loadDirectory(item.path);
} else {
li.onclick = () => selectFile(item.path);
}
fileList.appendChild(li);
});
})
.catch(error => {
console.error('Error loading directory:', error);
alert('Error loading directory');
});
}
function selectFile(path) {
document.getElementById('server_path').value = path;
closeFileBrowser();
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Close modal when clicking outside
window.onclick = function(event) {
const modal = document.getElementById('fileBrowserModal');
if (event.target == modal) {
closeFileBrowser();
}
}
</script> </script>
</head> </head>
<body> <body>
...@@ -111,9 +213,16 @@ ...@@ -111,9 +213,16 @@
<label>Upload File: <input type="file" name="file" accept="image/*,video/*"></label> <label>Upload File: <input type="file" name="file" accept="image/*,video/*"></label>
</div> </div>
{% if user.get('role') == 'admin' and server_dir %}
<div class="form-group"> <div class="form-group">
<label>Or Local Path: <input type="text" name="local_path" placeholder="/path/to/your/media/file.mp4"></label> <label>Server Local Path:
<div style="display: flex; gap: 0.5rem;">
<input type="text" id="server_path" name="local_path" placeholder="Click browse to select file..." readonly>
<button type="button" onclick="openFileBrowser()" class="btn" style="padding: 0.5rem 1rem; font-size: 0.9rem;">Browse</button>
</div>
</label>
</div> </div>
{% endif %}
<div class="form-group"> <div class="form-group">
<label>Frame Interval (seconds): <input type="number" name="interval" value="10" min="1"></label> <label>Frame Interval (seconds): <input type="number" name="interval" value="10" min="1"></label>
...@@ -140,5 +249,22 @@ ...@@ -140,5 +249,22 @@
<div id="stats" class="stats">Loading stats...</div> <div id="stats" class="stats">Loading stats...</div>
</div> </div>
</div> </div>
<!-- File Browser Modal -->
<div id="fileBrowserModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Select File</h3>
<span onclick="closeFileBrowser()" style="cursor: pointer; font-size: 1.5rem;">&times;</span>
</div>
<div class="modal-body">
<div id="breadcrumb" class="breadcrumb"></div>
<ul id="fileList" class="file-list"></ul>
</div>
<div class="modal-footer">
<button onclick="closeFileBrowser()" class="btn" style="background: #6b7280;">Cancel</button>
</div>
</div>
</div>
</body> </body>
</html> </html>
\ No newline at end of file
...@@ -24,6 +24,7 @@ import os ...@@ -24,6 +24,7 @@ import os
import json import json
import uuid import uuid
import time import time
import argparse
from .comm import SocketCommunicator, Message from .comm import SocketCommunicator, Message
from .config import get_all_settings, get_allow_registration from .config import get_all_settings, get_allow_registration
from .auth import login_user, logout_user, get_current_user, register_user, confirm_email, require_auth, require_admin from .auth import login_user, logout_user, get_current_user, register_user, confirm_email, require_auth, require_admin
...@@ -33,6 +34,9 @@ app = Flask(__name__, template_folder=os.path.join(os.path.dirname(__file__), '. ...@@ -33,6 +34,9 @@ app = Flask(__name__, template_folder=os.path.join(os.path.dirname(__file__), '.
app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'dev-secret-key-change-in-production') app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'dev-secret-key-change-in-production')
os.makedirs('static', exist_ok=True) os.makedirs('static', exist_ok=True)
# Global configuration
server_dir = None
# Communicator to backend (always TCP) # Communicator to backend (always TCP)
comm = SocketCommunicator(host='localhost', port=5001, comm_type='tcp') comm = SocketCommunicator(host='localhost', port=5001, comm_type='tcp')
...@@ -226,10 +230,11 @@ def analyze(): ...@@ -226,10 +230,11 @@ def analyze():
return render_template('analyze.html', return render_template('analyze.html',
user=user, user=user,
tokens=get_user_tokens(user["id"]), tokens=get_user_tokens(user["id"]),
result=result) result=result,
server_dir=server_dir)
@app.route('/train', methods=['GET', 'POST']) @app.route('/train', methods=['GET', 'POST'])
@login_required @admin_required
def train(): def train():
user = get_current_user_session() user = get_current_user_session()
message = None message = None
...@@ -442,5 +447,56 @@ def serve_static(filename): ...@@ -442,5 +447,56 @@ def serve_static(filename):
def serve_logo(): def serve_logo():
return send_from_directory('.', 'image.jpg') return send_from_directory('.', 'image.jpg')
@app.route('/api/browse')
@admin_required
def browse_files():
"""Browse files in server directory (admin only)."""
path = request.args.get('path', '')
if not server_dir:
return json.dumps({'error': 'Server directory not configured'})
# Ensure path is within server_dir
full_path = os.path.join(server_dir, path)
full_path = os.path.abspath(full_path)
# Security check: ensure path is within server_dir
if not full_path.startswith(server_dir):
return json.dumps({'error': 'Access denied'})
if not os.path.exists(full_path):
return json.dumps({'error': 'Path not found'})
try:
items = []
if os.path.isdir(full_path):
for item in os.listdir(full_path):
item_path = os.path.join(full_path, item)
items.append({
'name': item,
'path': os.path.join(path, item) if path else item,
'is_dir': os.path.isdir(item_path),
'size': os.path.getsize(item_path) if os.path.isfile(item_path) else 0
})
else:
return json.dumps({'error': 'Not a directory'})
return json.dumps({
'current_path': path,
'items': items
})
except Exception as e:
return json.dumps({'error': str(e)})
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description='VidAI Web Interface')
parser.add_argument('--server-dir', type=str, help='Server directory for local file access (admin only)')
args = parser.parse_args()
# Set global server directory
server_dir = args.server_dir
if server_dir:
server_dir = os.path.abspath(server_dir)
print(f"Server directory set to: {server_dir}")
app.run(host='0.0.0.0', debug=True) app.run(host='0.0.0.0', debug=True)
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment