5d568f7f89
- Implemented 2FA management in admin panel with enable/disable options. - Added QR code display for 2FA setup and input for TOTP codes in login and pickup forms. - Introduced key management section for generating, loading, and clearing RSA key pairs. - Enhanced file upload and sharing functionality with optional 2FA. - Added buttons for switching between development and production modes in admin panel. - Updated API documentation to reflect new 2FA and key management features.
569 lines
29 KiB
HTML
569 lines
29 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<meta name="description" content="PacCrypt - Secure text and file encryption with password generation" />
|
|
<title>PacCrypt</title>
|
|
|
|
<!-- Favicon -->
|
|
<link rel="icon" href="{{ url_for('static', filename='img/PacCrypt.png') }}" type="image/png" />
|
|
|
|
<!-- Stylesheets -->
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
|
|
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
|
|
|
|
<!-- Scripts -->
|
|
<script type="module" src="{{ url_for('static', filename='js/main.js') }}" defer></script>
|
|
</head>
|
|
<body class="dark">
|
|
<!-- Header -->
|
|
<header class="card logo-header">
|
|
<div class="logo-container">
|
|
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
|
|
<div class="logo-text">
|
|
<h1>PACCRYPT</h1>
|
|
<p>Securely Share Text and Files</p>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Main Content -->
|
|
<main>
|
|
<!-- Password Generator Section -->
|
|
<section id="password-generator-section" class="card form-group">
|
|
<h2>Password Generator</h2>
|
|
<div class="form-group">
|
|
<input type="text" id="generated-password" readonly />
|
|
<div class="button-group">
|
|
<button type="button" id="generate-btn">Generate</button>
|
|
<button type="button" id="copy-btn">Copy Password</button>
|
|
</div>
|
|
<div id="password-copy-feedback" class="copy-feedback">Password copied to clipboard!</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Key Management Section -->
|
|
<section id="key-pairs-section" class="card form-group">
|
|
<h2>Key Management</h2>
|
|
<p style="color: #ccc; font-size: 0.9em; margin-bottom: 15px;">
|
|
Manage Key Pairs for the RSA Hybrid Algorithm.
|
|
</p>
|
|
|
|
<!-- Key Status Indicators -->
|
|
<div class="form-group">
|
|
<h3 style="margin-bottom: 10px; color: #00ff99;">Key Status</h3>
|
|
<div id="key-status-indicators" style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 15px;">
|
|
<div style="padding: 10px; border: 2px solid #333; border-radius: 5px; text-align: center;">
|
|
<div id="public-key-indicator" style="color: #ff6b6b; font-weight: bold;">🔓 No Public Key</div>
|
|
<div style="font-size: 0.8em; color: #888;">For Encryption</div>
|
|
</div>
|
|
<div style="padding: 10px; border: 2px solid #333; border-radius: 5px; text-align: center;">
|
|
<div id="private-key-indicator" style="color: #ff6b6b; font-weight: bold;">🔐 No Private Key</div>
|
|
<div style="font-size: 0.8em; color: #888;">For Decryption</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Key Management Buttons -->
|
|
<div class="form-group">
|
|
<div class="button-group">
|
|
<button type="button" id="generate-keypair-main-btn">Generate & Download Key Pair</button>
|
|
<button type="button" id="load-public-main-btn">Load Public Key</button>
|
|
<button type="button" id="load-private-main-btn">Load Private Key</button>
|
|
</div>
|
|
<div class="button-group" style="margin-top: 10px;">
|
|
<button type="button" id="clear-keys-btn" class="danger-button">Clear All Keys</button>
|
|
<button type="button" id="download-keys-btn" style="display: none;">Download Current Keys</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hidden File Inputs -->
|
|
<input type="file" id="public-key-main-input" accept=".pub,.pem" style="display: none;">
|
|
<input type="file" id="private-key-main-input" accept=".key,.pem" style="display: none;">
|
|
|
|
<!-- Key Information Display -->
|
|
<div id="key-info-display" style="display: none; margin-top: 15px; padding: 10px; border: 1px solid #00ff99; border-radius: 5px; background-color: #001100;">
|
|
<h4 style="color: #00ff99; margin-top: 0;">Loaded Keys Information</h4>
|
|
<div id="key-info-content" style="font-family: monospace; font-size: 0.8em; color: #ccc;"></div>
|
|
</div>
|
|
|
|
<!-- Copy Feedback -->
|
|
<div id="keypair-feedback" class="copy-feedback">Keys generated and downloaded!</div>
|
|
</section>
|
|
|
|
<!-- Pacman Game Section -->
|
|
<section id="pacman-section" class="card" style="display: none;">
|
|
<div class="pacman-wrapper">
|
|
<canvas id="pacmanCanvas" width="800" height="600"></canvas>
|
|
</div>
|
|
<audio id="chomp-sound" src="{{ url_for('static', filename='audio/chomp.mp3') }}"></audio>
|
|
<div class="button-group" style="margin-top: 6px;">
|
|
<button type="button" onclick="resetGame()">Restart Game</button>
|
|
<button type="button" onclick="exitGame()">Exit Game</button>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Encryption/Decryption Section -->
|
|
<section id="encoding-section" class="card form-group">
|
|
<h2>Encrypt & Decrypt</h2>
|
|
<form id="crypto-form" class="form-group">
|
|
<!-- Algorithm Selection -->
|
|
<div class="form-group" id="algorithm-selection">
|
|
<label for="algorithm">Encryption Algorithm:</label>
|
|
<select id="algorithm">
|
|
<!-- Options populated dynamically by JavaScript -->
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Key Pair Management (for RSA/PQ algorithms) -->
|
|
<div class="form-group" id="keypair-section" style="display: none;">
|
|
<div class="keypair-info">
|
|
<p><strong>🔐 Key Pair Required:</strong></p>
|
|
<p><strong>For Encryption:</strong> Use Public Key (.pub file)</p>
|
|
<p><strong>For Decryption:</strong> Use Private Key (.key file)</p>
|
|
</div>
|
|
<div style="padding: 10px; border: 1px solid #ffaa00; border-radius: 5px; background-color: #221100; margin: 10px 0;">
|
|
<p style="color: #ffaa00; margin: 0; text-align: center;">
|
|
<strong>💡 Manage your keys in the "Key Pairs Management" section above</strong>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Key status indicators -->
|
|
<div id="key-status" style="margin-top: 10px;">
|
|
<div id="public-key-status" style="display: none;">✅ Public key loaded</div>
|
|
<div id="private-key-status" style="display: none;">✅ Private key loaded</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Operation Toggle -->
|
|
<div class="toggle-container">
|
|
<span class="toggle-label">Encrypt</span>
|
|
<label class="material-switch">
|
|
<input type="checkbox" id="operation-toggle">
|
|
<span class="material-slider"></span>
|
|
</label>
|
|
<span class="toggle-label">Decrypt</span>
|
|
</div>
|
|
|
|
|
|
<!-- Text Input Section -->
|
|
<div id="text-section" class="form-group">
|
|
<textarea id="input-text" placeholder="Enter your message..."></textarea>
|
|
</div>
|
|
|
|
<!-- Password Input -->
|
|
<div id="password-input" class="form-group">
|
|
<input type="password" id="password" placeholder="Encryption/Decryption Password" />
|
|
</div>
|
|
|
|
<!-- File Input Section -->
|
|
<div id="file-section" class="form-group">
|
|
<input type="file" id="file-input" />
|
|
<button type="button" id="remove-file-btn">Remove File</button>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="button-group">
|
|
<button type="submit">Execute</button>
|
|
<button type="button" id="copy-output-btn">Copy Output</button>
|
|
</div>
|
|
|
|
<!-- Output Section -->
|
|
<textarea id="output-text" readonly placeholder="Encrypted/Decrypted Output"></textarea>
|
|
<div class="button-group">
|
|
<button type="button" id="clear-all-btn" class="danger-button">Clear All</button>
|
|
</div>
|
|
<div id="output-copy-feedback" class="copy-feedback">Text copied to clipboard!</div>
|
|
</form>
|
|
</section>
|
|
|
|
<!-- File Sharing Section -->
|
|
<section id="sharing-section" class="card form-group">
|
|
<h2 style="margin-bottom: unset;">PacShare</h2>
|
|
<p style="margin-top: unset;">Securely share encrypted files.</p>
|
|
|
|
<!-- Flash Messages -->
|
|
{% with messages = get_flashed_messages() %}
|
|
{% if messages %}
|
|
<ul style="color: lime; list-style: none; padding-left: 0;">
|
|
{% for message in messages %}
|
|
<li>
|
|
{{ message | safe }}
|
|
{% if "pickup" in message %}
|
|
<div class="share-link-container">
|
|
<a id="share-link" href="{{ message.split(' at ')[1] }}" target="_blank">{{ message.split(" at ")[1] }}</a>
|
|
<button type="button" onclick="copyShareLink()">Copy Link</button>
|
|
<div id="shared-link-feedback" class="copy-feedback">Link copied to clipboard!</div>
|
|
</div>
|
|
{% endif %}
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
<script>window.onload = () => window.scrollTo(0, document.body.scrollHeight);</script>
|
|
{% endif %}
|
|
{% endwith %}
|
|
|
|
|
|
<!-- File Upload Form -->
|
|
<!-- Share Link Container (initially hidden) -->
|
|
<div class="share-link-container" id="share-link-container" style="display: none;">
|
|
<a id="share-link" href="#" target="_blank"></a>
|
|
<button type="button" id="copy-share-btn">Copy Link</button>
|
|
<div id="shared-link-feedback" class="copy-feedback">Link copied to clipboard!</div>
|
|
</div>
|
|
|
|
<!-- 2FA Setup Container (initially hidden) -->
|
|
<div id="tfa-setup-container" style="display: none; margin-top: 20px; padding: 15px; border: 2px solid #ffaa00; border-radius: 8px; background-color: #332200;">
|
|
<h3 style="color: #ffaa00; margin-top: 0;">🔒 Important: Set Up 2FA Now!</h3>
|
|
<p style="color: #ccc;">You enabled 2FA for this file. <strong>Scan this QR code NOW</strong> with your authenticator app:</p>
|
|
<div style="text-align: center; margin: 15px 0;">
|
|
<img id="tfa-qr-image" src="" alt="2FA QR Code" style="max-width: 200px; border: 2px solid #00ff99;" />
|
|
</div>
|
|
|
|
<!-- 2FA String Container -->
|
|
<div style="margin-top: 15px; padding: 10px; border: 1px solid #00ff99; border-radius: 5px; background-color: #001100;">
|
|
<p style="color: #00ff99; margin: 5px 0; font-size: 0.9em;"><strong>Or manually enter this string:</strong></p>
|
|
<div class="share-link-container" style="margin: 0;">
|
|
<input type="text" id="tfa-string" readonly style="flex: 1; background: #111; color: #00ff99; border: 1px solid #333; padding: 8px; font-family: monospace; font-size: 0.8em;" />
|
|
<button type="button" id="copy-tfa-string-btn">Copy String</button>
|
|
<div id="tfa-string-feedback" class="copy-feedback">2FA string copied to clipboard!</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p style="color: #ff6b6b; font-weight: bold; margin-top: 15px;">⚠️ SAVE THIS QR CODE OR STRING NOW! It will not be shown again for security reasons.</p>
|
|
<p style="color: #ccc; font-size: 0.9em;">Recommended apps: Google Authenticator, Authy, Microsoft Authenticator</p>
|
|
<button type="button" onclick="closeTwoFactorSetup()">I've Saved the 2FA Information</button>
|
|
</div>
|
|
<form method="POST" enctype="multipart/form-data" class="form-group" id="upload-form">
|
|
<!-- Algorithm Selection for PacShare -->
|
|
<div class="form-group">
|
|
<label for="share-algorithm">Encryption Algorithm:</label>
|
|
<select id="share-algorithm" name="algorithm">
|
|
<!-- Options populated dynamically by JavaScript -->
|
|
</select>
|
|
</div>
|
|
<input type="file" name="file" id="upload-file" required />
|
|
<input type="password" name="enc_password" placeholder="Encryption/Decryption Password" required />
|
|
<input type="password" name="pickup_password" placeholder="Pickup Password" required />
|
|
|
|
<!-- 2FA Option -->
|
|
<div class="form-group">
|
|
<label>
|
|
<input type="checkbox" name="enable_2fa" id="enable-2fa" />
|
|
Enable 2FA (TOTP) - Adds extra security with Google Authenticator, Authy, etc.
|
|
</label>
|
|
</div>
|
|
|
|
<div class="button-group">
|
|
<button type="submit">Upload and Generate Link</button>
|
|
</div>
|
|
</form>
|
|
<p style="color: #9c0000;">BOTH PASSWORDS ARE REQUIRED FOR PICKUP</p>
|
|
<script>
|
|
document.getElementById('upload-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
|
|
try {
|
|
const response = await fetch('/', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.error) {
|
|
alert(data.error);
|
|
return;
|
|
}
|
|
|
|
if (data.success && data.pickup_url) {
|
|
const shareLink = document.getElementById('share-link');
|
|
const shareLinkContainer = document.getElementById('share-link-container');
|
|
|
|
shareLink.href = data.pickup_url;
|
|
shareLink.textContent = data.pickup_url;
|
|
shareLinkContainer.style.display = 'flex';
|
|
|
|
// If 2FA is enabled, show the QR code immediately
|
|
if (data.qr_code_url) {
|
|
showTwoFactorSetup(data.qr_code_url, data.service_name, data.totp_secret);
|
|
}
|
|
|
|
// Clear form fields
|
|
document.getElementById('upload-file').value = '';
|
|
document.getElementsByName('enc_password')[0].value = '';
|
|
document.getElementsByName('pickup_password')[0].value = '';
|
|
document.getElementById('enable-2fa').checked = false;
|
|
|
|
// Scroll to the share link
|
|
shareLinkContainer.scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
} catch (error) {
|
|
alert('Error uploading file: ' + error.message);
|
|
}
|
|
});
|
|
|
|
// 2FA Setup Functions
|
|
function showTwoFactorSetup(qrCodeUrl, serviceName, totpSecret) {
|
|
const container = document.getElementById('tfa-setup-container');
|
|
const qrImage = document.getElementById('tfa-qr-image');
|
|
const tfaString = document.getElementById('tfa-string');
|
|
|
|
qrImage.src = qrCodeUrl;
|
|
tfaString.value = totpSecret;
|
|
container.style.display = 'block';
|
|
|
|
// Scroll to the 2FA setup
|
|
container.scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
|
|
function closeTwoFactorSetup() {
|
|
const container = document.getElementById('tfa-setup-container');
|
|
container.style.display = 'none';
|
|
}
|
|
|
|
// Copy share link functionality
|
|
document.getElementById('copy-share-btn').addEventListener('click', () => {
|
|
const shareLink = document.getElementById('share-link');
|
|
const feedback = document.getElementById('shared-link-feedback');
|
|
|
|
navigator.clipboard.writeText(shareLink.href).then(() => {
|
|
feedback.style.display = 'block';
|
|
feedback.classList.add('show');
|
|
|
|
setTimeout(() => {
|
|
feedback.classList.remove('show');
|
|
setTimeout(() => {
|
|
feedback.style.display = 'none';
|
|
}, 300);
|
|
}, 2000);
|
|
});
|
|
});
|
|
|
|
// Copy 2FA string functionality
|
|
document.getElementById('copy-tfa-string-btn').addEventListener('click', () => {
|
|
const tfaString = document.getElementById('tfa-string');
|
|
const feedback = document.getElementById('tfa-string-feedback');
|
|
|
|
navigator.clipboard.writeText(tfaString.value).then(() => {
|
|
feedback.style.display = 'block';
|
|
feedback.classList.add('show');
|
|
|
|
setTimeout(() => {
|
|
feedback.classList.remove('show');
|
|
setTimeout(() => {
|
|
feedback.style.display = 'none';
|
|
}, 300);
|
|
}, 2000);
|
|
});
|
|
});
|
|
|
|
// Centralized Key Pairs Management
|
|
let globalKeys = {
|
|
publicKey: null,
|
|
privateKey: null,
|
|
algorithm: 'rsa_hybrid'
|
|
};
|
|
|
|
function updateKeyStatusIndicators() {
|
|
const publicIndicator = document.getElementById('public-key-indicator');
|
|
const privateIndicator = document.getElementById('private-key-indicator');
|
|
const keyInfoDisplay = document.getElementById('key-info-display');
|
|
const keyInfoContent = document.getElementById('key-info-content');
|
|
const downloadKeysBtn = document.getElementById('download-keys-btn');
|
|
|
|
// Update public key indicator
|
|
if (globalKeys.publicKey) {
|
|
publicIndicator.style.color = '#00ff99';
|
|
publicIndicator.textContent = '🔓 Public Key Loaded';
|
|
document.getElementById('public-key-status').style.display = 'block';
|
|
} else {
|
|
publicIndicator.style.color = '#ff6b6b';
|
|
publicIndicator.textContent = '🔓 No Public Key';
|
|
document.getElementById('public-key-status').style.display = 'none';
|
|
}
|
|
|
|
// Update private key indicator
|
|
if (globalKeys.privateKey) {
|
|
privateIndicator.style.color = '#00ff99';
|
|
privateIndicator.textContent = '🔐 Private Key Loaded';
|
|
document.getElementById('private-key-status').style.display = 'block';
|
|
} else {
|
|
privateIndicator.style.color = '#ff6b6b';
|
|
privateIndicator.textContent = '🔐 No Private Key';
|
|
document.getElementById('private-key-status').style.display = 'none';
|
|
}
|
|
|
|
// Show/hide key info and download button
|
|
if (globalKeys.publicKey || globalKeys.privateKey) {
|
|
keyInfoDisplay.style.display = 'block';
|
|
downloadKeysBtn.style.display = 'inline-block';
|
|
|
|
let info = `Algorithm: ${globalKeys.algorithm.toUpperCase()}\n`;
|
|
if (globalKeys.publicKey) {
|
|
const pubPreview = globalKeys.publicKey.substring(0, 50) + '...';
|
|
info += `Public Key: ${pubPreview}\n`;
|
|
}
|
|
if (globalKeys.privateKey) {
|
|
const privPreview = globalKeys.privateKey.substring(0, 50) + '...';
|
|
info += `Private Key: ${privPreview}\n`;
|
|
}
|
|
keyInfoContent.textContent = info;
|
|
} else {
|
|
keyInfoDisplay.style.display = 'none';
|
|
downloadKeysBtn.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Generate key pair
|
|
document.getElementById('generate-keypair-main-btn').addEventListener('click', async () => {
|
|
const algorithm = 'rsa_hybrid';
|
|
|
|
try {
|
|
const response = await fetch('/api/generate-keypair', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ algorithm: algorithm })
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (response.ok) {
|
|
globalKeys.publicKey = data.public_key;
|
|
globalKeys.privateKey = data.private_key;
|
|
globalKeys.algorithm = algorithm;
|
|
|
|
// Download keys
|
|
downloadKeyPair(data.public_key, data.private_key, algorithm);
|
|
|
|
updateKeyStatusIndicators();
|
|
showKeypairFeedback('Keys generated and downloaded!');
|
|
} else {
|
|
alert('Error: ' + data.error);
|
|
}
|
|
} catch (error) {
|
|
alert('Failed to generate key pair: ' + error.message);
|
|
}
|
|
});
|
|
|
|
// Load public key
|
|
document.getElementById('load-public-main-btn').addEventListener('click', () => {
|
|
document.getElementById('public-key-main-input').click();
|
|
});
|
|
|
|
document.getElementById('public-key-main-input').addEventListener('change', (e) => {
|
|
const file = e.target.files[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
globalKeys.publicKey = event.target.result;
|
|
updateKeyStatusIndicators();
|
|
showKeypairFeedback('Public key loaded!');
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
});
|
|
|
|
// Load private key
|
|
document.getElementById('load-private-main-btn').addEventListener('click', () => {
|
|
document.getElementById('private-key-main-input').click();
|
|
});
|
|
|
|
document.getElementById('private-key-main-input').addEventListener('change', (e) => {
|
|
const file = e.target.files[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
globalKeys.privateKey = event.target.result;
|
|
updateKeyStatusIndicators();
|
|
showKeypairFeedback('Private key loaded!');
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
});
|
|
|
|
// Clear keys
|
|
document.getElementById('clear-keys-btn').addEventListener('click', () => {
|
|
if (confirm('Are you sure you want to clear all loaded keys?')) {
|
|
globalKeys.publicKey = null;
|
|
globalKeys.privateKey = null;
|
|
updateKeyStatusIndicators();
|
|
showKeypairFeedback('All keys cleared!');
|
|
}
|
|
});
|
|
|
|
// Download current keys
|
|
document.getElementById('download-keys-btn').addEventListener('click', () => {
|
|
if (globalKeys.publicKey || globalKeys.privateKey) {
|
|
downloadKeyPair(globalKeys.publicKey, globalKeys.privateKey, globalKeys.algorithm);
|
|
showKeypairFeedback('Keys downloaded!');
|
|
}
|
|
});
|
|
|
|
function downloadKeyPair(publicKey, privateKey, algorithm) {
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
|
|
if (publicKey) {
|
|
const pubBlob = new Blob([publicKey], { type: 'text/plain' });
|
|
const pubUrl = URL.createObjectURL(pubBlob);
|
|
const pubLink = document.createElement('a');
|
|
pubLink.href = pubUrl;
|
|
pubLink.download = `${algorithm}_public_key_${timestamp}.pub`;
|
|
pubLink.click();
|
|
URL.revokeObjectURL(pubUrl);
|
|
}
|
|
|
|
if (privateKey) {
|
|
const privBlob = new Blob([privateKey], { type: 'text/plain' });
|
|
const privUrl = URL.createObjectURL(privBlob);
|
|
const privLink = document.createElement('a');
|
|
privLink.href = privUrl;
|
|
privLink.download = `${algorithm}_private_key_${timestamp}.key`;
|
|
privLink.click();
|
|
URL.revokeObjectURL(privUrl);
|
|
}
|
|
}
|
|
|
|
function showKeypairFeedback(message) {
|
|
const feedback = document.getElementById('keypair-feedback');
|
|
feedback.textContent = message;
|
|
feedback.style.display = 'block';
|
|
feedback.classList.add('show');
|
|
|
|
setTimeout(() => {
|
|
feedback.classList.remove('show');
|
|
setTimeout(() => {
|
|
feedback.style.display = 'none';
|
|
}, 300);
|
|
}, 2000);
|
|
}
|
|
|
|
// Make global keys available to other scripts
|
|
window.getGlobalKeys = () => globalKeys;
|
|
window.setGlobalKeys = (keys) => {
|
|
globalKeys = { ...globalKeys, ...keys };
|
|
updateKeyStatusIndicators();
|
|
};
|
|
|
|
// Initialize key status
|
|
updateKeyStatusIndicators();
|
|
</script>
|
|
|
|
<!-- File Limits Information -->
|
|
<p class="text-muted mt-3" style="font-size: 0.85em;">
|
|
Files expire after {{ settings.max_file_age_days }} days.<br />
|
|
Max file size: {{ settings.max_file_size_bytes // (1024 * 1024 * 1024) }} GB.
|
|
</p>
|
|
</section>
|
|
</main>
|
|
|
|
<!-- Footer -->
|
|
<footer>
|
|
<p>© 2025 UnNaturalll-Dev. All rights reserved.</p>
|
|
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
|
|
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
|
|
</a>
|
|
</footer>
|
|
</body>
|
|
</html>
|