mirror of
https://github.com/mattintech/simplefileupload-server.git
synced 2026-07-11 14:21:53 +00:00
- Store client key in SQLite database instead of environment variable - Add database migration from CLIENT_KEY environment variable to preserve existing keys - Add admin UI with tabbed interface (Configuration and QR Code tabs) - Implement QR code generation containing server config (address, port, key) for Android app - Add functionality to regenerate client key with warning dialog - Add buttons to download QR code as PNG and copy QR image to clipboard - Add manual input fields for server address and port configuration - Update requirements.txt with pyotp and qrcode dependencies
735 lines
22 KiB
HTML
735 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Admin Dashboard - Simple File Server</title>
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
margin: 0;
|
|
padding: 0;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
min-height: 100vh;
|
|
color: #2d3748;
|
|
padding: 2rem 1rem;
|
|
}
|
|
|
|
.container {
|
|
background: rgba(255, 255, 255, 0.95);
|
|
padding: 2rem;
|
|
border-radius: 1rem;
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
width: 90%;
|
|
max-width: 900px;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
border-bottom: 2px solid #e2e8f0;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.tab {
|
|
padding: 0.75rem 1.5rem;
|
|
cursor: pointer;
|
|
border: none;
|
|
background: transparent;
|
|
color: #718096;
|
|
font-weight: 500;
|
|
font-size: 0.95rem;
|
|
transition: all 0.2s;
|
|
border-bottom: 3px solid transparent;
|
|
margin-bottom: -2px;
|
|
}
|
|
|
|
.tab:hover {
|
|
color: #667eea;
|
|
}
|
|
|
|
.tab.active {
|
|
color: #667eea;
|
|
border-bottom-color: #667eea;
|
|
}
|
|
|
|
.tab-content {
|
|
display: none;
|
|
}
|
|
|
|
.tab-content.active {
|
|
display: block;
|
|
}
|
|
|
|
.cards-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.cards-grid {
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
}
|
|
|
|
.header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 2rem;
|
|
padding-bottom: 1.5rem;
|
|
border-bottom: 2px solid #e2e8f0;
|
|
}
|
|
|
|
.header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.icon {
|
|
color: #667eea;
|
|
font-size: 2.5rem;
|
|
}
|
|
|
|
.header-text h1 {
|
|
color: #2d3748;
|
|
font-size: 1.75rem;
|
|
font-weight: 600;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.header-text .username {
|
|
color: #718096;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.logout-btn {
|
|
background: transparent;
|
|
color: #718096;
|
|
padding: 0.5rem 1rem;
|
|
border: 2px solid #e2e8f0;
|
|
border-radius: 0.5rem;
|
|
font-size: 0.9rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.logout-btn:hover {
|
|
border-color: #e53e3e;
|
|
color: #e53e3e;
|
|
background: #fff5f5;
|
|
}
|
|
|
|
.card {
|
|
background: #f7fafc;
|
|
padding: 1.25rem;
|
|
border-radius: 0.75rem;
|
|
border-left: 4px solid #667eea;
|
|
}
|
|
|
|
.card-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-weight: 600;
|
|
color: #2d3748;
|
|
margin-bottom: 1rem;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.card-title .material-icons {
|
|
color: #667eea;
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.key-container {
|
|
background: white;
|
|
padding: 1rem;
|
|
border-radius: 0.5rem;
|
|
border: 2px solid #e2e8f0;
|
|
position: relative;
|
|
}
|
|
|
|
.key-display {
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 0.95rem;
|
|
word-break: break-all;
|
|
color: #2d3748;
|
|
padding-right: 3rem;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.copy-btn {
|
|
position: absolute;
|
|
top: 0.75rem;
|
|
right: 0.75rem;
|
|
background: #667eea;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 0.375rem;
|
|
padding: 0.5rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.copy-btn:hover {
|
|
background: #5a67d8;
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.copy-btn:active {
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
.copy-btn .material-icons {
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.info-box {
|
|
background: #ebf8ff;
|
|
color: #2c5282;
|
|
padding: 0.75rem;
|
|
border-radius: 0.5rem;
|
|
border-left: 3px solid #4299e1;
|
|
font-size: 0.8rem;
|
|
line-height: 1.5;
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.info-box .material-icons {
|
|
color: #4299e1;
|
|
font-size: 1.5rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.success-message {
|
|
background: #f0fff4;
|
|
color: #2f855a;
|
|
padding: 0.75rem 1rem;
|
|
border-radius: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
display: none;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
animation: slideIn 0.3s ease;
|
|
}
|
|
|
|
.success-message .material-icons {
|
|
color: #48bb78;
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
.btn {
|
|
flex: 1;
|
|
padding: 0.75rem;
|
|
border: none;
|
|
border-radius: 0.5rem;
|
|
font-size: 0.9rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: white;
|
|
color: #667eea;
|
|
border: 2px solid #667eea;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: #f7fafc;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.form-group:last-of-type {
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
font-weight: 500;
|
|
margin-bottom: 0.5rem;
|
|
color: #2d3748;
|
|
}
|
|
|
|
.form-input {
|
|
width: 100%;
|
|
padding: 0.75rem;
|
|
border: 2px solid #e2e8f0;
|
|
border-radius: 0.5rem;
|
|
font-size: 0.95rem;
|
|
transition: border-color 0.2s;
|
|
}
|
|
|
|
.form-input:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
}
|
|
|
|
.form-hint {
|
|
display: block;
|
|
color: #718096;
|
|
font-size: 0.85rem;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.key-actions {
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.btn-danger {
|
|
background: #fff5f5;
|
|
color: #e53e3e;
|
|
border: 2px solid #e53e3e;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background: #e53e3e;
|
|
color: white;
|
|
}
|
|
|
|
.qr-container {
|
|
display: flex;
|
|
justify-content: center;
|
|
padding: 1rem;
|
|
background: white;
|
|
border-radius: 0.5rem;
|
|
border: 2px solid #e2e8f0;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.qr-image {
|
|
max-width: 250px;
|
|
width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
.error-message {
|
|
background: #fff5f5;
|
|
color: #c53030;
|
|
padding: 0.75rem 1rem;
|
|
border-radius: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
animation: slideIn 0.3s ease;
|
|
}
|
|
|
|
.error-message .material-icons {
|
|
color: #e53e3e;
|
|
}
|
|
|
|
.qr-actions {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.btn-icon {
|
|
flex: 1;
|
|
padding: 0.75rem;
|
|
border: none;
|
|
border-radius: 0.5rem;
|
|
font-size: 0.9rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.btn-download {
|
|
background: #667eea;
|
|
color: white;
|
|
}
|
|
|
|
.btn-download:hover {
|
|
background: #5a67d8;
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.btn-copy-config {
|
|
background: white;
|
|
color: #667eea;
|
|
border: 2px solid #667eea;
|
|
}
|
|
|
|
.btn-copy-config:hover {
|
|
background: #f7fafc;
|
|
}
|
|
</style>
|
|
<script>
|
|
function copyToClipboard() {
|
|
const keyText = document.querySelector('.key-display').textContent;
|
|
navigator.clipboard.writeText(keyText).then(() => {
|
|
const successMsg = document.getElementById('copy-success');
|
|
successMsg.style.display = 'flex';
|
|
setTimeout(() => {
|
|
successMsg.style.display = 'none';
|
|
}, 3000);
|
|
});
|
|
}
|
|
|
|
function validateConfig() {
|
|
const address = document.getElementById('server_address').value.trim();
|
|
const port = parseInt(document.getElementById('server_port').value);
|
|
|
|
if (!address) {
|
|
alert('Server address is required');
|
|
return false;
|
|
}
|
|
|
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
alert('Port must be between 1 and 65535');
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function confirmRegenerate() {
|
|
const confirmed = confirm(
|
|
'WARNING: Regenerating the client key will immediately invalidate the current key.\n\n' +
|
|
'All Android apps and clients using the old key will be unable to upload files until they are updated with the new key.\n\n' +
|
|
'Are you sure you want to continue?'
|
|
);
|
|
|
|
if (confirmed) {
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = '/admin/regenerate-key';
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
}
|
|
}
|
|
|
|
function switchTab(tabName) {
|
|
// Hide all tab contents
|
|
const tabContents = document.querySelectorAll('.tab-content');
|
|
tabContents.forEach(content => content.classList.remove('active'));
|
|
|
|
// Remove active class from all tabs
|
|
const tabs = document.querySelectorAll('.tab');
|
|
tabs.forEach(tab => tab.classList.remove('active'));
|
|
|
|
// Show selected tab content
|
|
const selectedContent = document.getElementById(tabName + '-tab');
|
|
if (selectedContent) {
|
|
selectedContent.classList.add('active');
|
|
}
|
|
|
|
// Add active class to clicked tab
|
|
event.target.closest('.tab').classList.add('active');
|
|
}
|
|
|
|
function downloadQRCode() {
|
|
const qrImage = document.querySelector('.qr-image');
|
|
if (!qrImage) return;
|
|
|
|
// Convert base64 to blob and download
|
|
const base64Data = qrImage.src.split(',')[1];
|
|
const byteCharacters = atob(base64Data);
|
|
const byteNumbers = new Array(byteCharacters.length);
|
|
for (let i = 0; i < byteCharacters.length; i++) {
|
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
}
|
|
const byteArray = new Uint8Array(byteNumbers);
|
|
const blob = new Blob([byteArray], { type: 'image/png' });
|
|
|
|
const link = document.createElement('a');
|
|
link.href = URL.createObjectURL(blob);
|
|
link.download = 'server-config-qr.png';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(link.href);
|
|
|
|
// Show success message
|
|
showCopySuccess('QR code downloaded!');
|
|
}
|
|
|
|
async function copyQRConfig() {
|
|
const qrImage = document.querySelector('.qr-image');
|
|
if (!qrImage) return;
|
|
|
|
try {
|
|
// Convert base64 to blob
|
|
const base64Data = qrImage.src.split(',')[1];
|
|
const byteCharacters = atob(base64Data);
|
|
const byteNumbers = new Array(byteCharacters.length);
|
|
for (let i = 0; i < byteCharacters.length; i++) {
|
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
}
|
|
const byteArray = new Uint8Array(byteNumbers);
|
|
const blob = new Blob([byteArray], { type: 'image/png' });
|
|
|
|
// Copy to clipboard
|
|
await navigator.clipboard.write([
|
|
new ClipboardItem({
|
|
'image/png': blob
|
|
})
|
|
]);
|
|
|
|
showCopySuccess('QR code copied to clipboard!');
|
|
} catch (err) {
|
|
console.error('Failed to copy QR code:', err);
|
|
alert('Failed to copy QR code to clipboard. Your browser may not support this feature.');
|
|
}
|
|
}
|
|
|
|
function showCopySuccess(message) {
|
|
const successMsg = document.getElementById('copy-success');
|
|
const messageSpan = successMsg.querySelector('span:last-child');
|
|
if (messageSpan) {
|
|
const originalMessage = messageSpan.textContent;
|
|
messageSpan.textContent = message;
|
|
successMsg.style.display = 'flex';
|
|
setTimeout(() => {
|
|
successMsg.style.display = 'none';
|
|
messageSpan.textContent = originalMessage;
|
|
}, 3000);
|
|
}
|
|
}
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<div class="header-left">
|
|
<span class="material-icons icon">dashboard</span>
|
|
<div class="header-text">
|
|
<h1>Admin Dashboard</h1>
|
|
<div class="username">Logged in as {{ username }}</div>
|
|
</div>
|
|
</div>
|
|
<a href="/admin/logout" class="logout-btn">
|
|
<span class="material-icons" style="font-size: 1.25rem;">logout</span>
|
|
Logout
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Error Message -->
|
|
{% if error %}
|
|
<div class="error-message">
|
|
<span class="material-icons">error</span>
|
|
{{ error }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Success Message -->
|
|
{% if success %}
|
|
<div class="success-message" style="display: flex;">
|
|
<span class="material-icons">check_circle</span>
|
|
{{ success }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="success-message" id="copy-success">
|
|
<span class="material-icons">check_circle</span>
|
|
<span>Client key copied to clipboard!</span>
|
|
</div>
|
|
|
|
<!-- Tabs Navigation -->
|
|
<div class="tabs">
|
|
<button class="tab active" onclick="switchTab('config')">
|
|
<span class="material-icons" style="font-size: 1.2rem; vertical-align: middle;">settings</span>
|
|
Configuration
|
|
</button>
|
|
<button class="tab" onclick="switchTab('qrcode')" {% if not qr_code %}disabled style="opacity: 0.5; cursor: not-allowed;"{% endif %}>
|
|
<span class="material-icons" style="font-size: 1.2rem; vertical-align: middle;">qr_code_2</span>
|
|
QR Code
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Configuration Tab -->
|
|
<div id="config-tab" class="tab-content active">
|
|
<div class="cards-grid">
|
|
<!-- Server Configuration Card -->
|
|
<div class="card">
|
|
<div class="card-title">
|
|
<span class="material-icons">settings</span>
|
|
Server Configuration
|
|
</div>
|
|
|
|
<form method="POST" action="/admin/config" onsubmit="return validateConfig()">
|
|
<div class="form-group">
|
|
<label for="server_address">Server Address</label>
|
|
<input type="text"
|
|
id="server_address"
|
|
name="server_address"
|
|
class="form-input"
|
|
value="{{ server_address }}"
|
|
placeholder="e.g., 192.168.1.100 or myserver.com"
|
|
required>
|
|
<small class="form-hint">IP address or hostname where the server is accessible</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="server_port">Port</label>
|
|
<input type="number"
|
|
id="server_port"
|
|
name="server_port"
|
|
class="form-input"
|
|
value="{{ server_port }}"
|
|
min="1"
|
|
max="65535"
|
|
required>
|
|
<small class="form-hint">Port number (default: 7777)</small>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary">
|
|
<span class="material-icons">save</span>
|
|
Save Configuration
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Client Key Card -->
|
|
<div class="card">
|
|
<div class="card-title">
|
|
<span class="material-icons">vpn_key</span>
|
|
Client Key
|
|
</div>
|
|
<div class="key-container">
|
|
<div class="key-display">{{ client_key }}</div>
|
|
<button class="copy-btn" onclick="copyToClipboard()" title="Copy to clipboard">
|
|
<span class="material-icons">content_copy</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="info-box">
|
|
<span class="material-icons">info</span>
|
|
<div>
|
|
<strong>What is the Client Key?</strong><br>
|
|
This key is used by clients to authenticate API requests to the file server.
|
|
It should be included in the <code>X-Client-Key</code> header for all upload operations.
|
|
Keep this key secure and don't share it publicly.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="key-actions">
|
|
<button type="button" class="btn btn-danger" onclick="confirmRegenerate()">
|
|
<span class="material-icons">refresh</span>
|
|
Regenerate Key
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div><!-- end cards-grid -->
|
|
</div><!-- end config-tab -->
|
|
|
|
<!-- QR Code Tab -->
|
|
<div id="qrcode-tab" class="tab-content">
|
|
{% if qr_code %}
|
|
<div class="card">
|
|
<div class="card-title">
|
|
<span class="material-icons">qr_code_2</span>
|
|
QR Code for Android App
|
|
</div>
|
|
|
|
<div class="qr-container">
|
|
<img src="data:image/png;base64,{{ qr_code }}" alt="Configuration QR Code" class="qr-image">
|
|
</div>
|
|
|
|
<div class="info-box">
|
|
<span class="material-icons">info</span>
|
|
<div>
|
|
Scan this QR code with the Simple File Upload Android app to automatically configure:
|
|
<ul style="margin: 0.5rem 0 0 1.5rem;">
|
|
<li>Server address: <strong>{{ server_address }}</strong></li>
|
|
<li>Port: <strong>{{ server_port }}</strong></li>
|
|
<li>Client key: <strong>(hidden)</strong></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="qr-actions">
|
|
<button class="btn-icon btn-download" onclick="downloadQRCode()">
|
|
<span class="material-icons">download</span>
|
|
Download PNG
|
|
</button>
|
|
<button class="btn-icon btn-copy-config" onclick="copyQRConfig()">
|
|
<span class="material-icons">content_copy</span>
|
|
Copy QR Code
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="info-box" style="background: #fef5e7; border-left-color: #f39c12; color: #7d6608;">
|
|
<span class="material-icons" style="color: #f39c12;">warning</span>
|
|
<div>
|
|
<strong>Configure server settings to generate QR code</strong><br>
|
|
Enter your server address and port above to generate a QR code for the Android app.
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div><!-- end qrcode-tab -->
|
|
|
|
<div class="actions">
|
|
<a href="/" class="btn btn-secondary">
|
|
<span class="material-icons">home</span>
|
|
Home
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|