refactor frontend, add readme

This commit is contained in:
Gregor Lohaus
2026-02-25 16:43:51 +01:00
parent 0d34b632ac
commit e78ebb25c3
8 changed files with 140 additions and 108 deletions

View File

@@ -1,87 +1,69 @@
(async function () {
const fragment = location.hash.slice(1);
if (!fragment) {
showError('No decryption key found in URL.');
return;
}
const fragment = location.hash.slice(1);
if (!fragment) {
showError('No decryption key found in URL.');
} else try {
setStatus('Deriving key\u2026');
try {
setStatus('Deriving key\u2026');
const rawKeyBytes = decodeKey(fragment);
const id = await hashKey(rawKeyBytes);
// Decode base64url → raw key bytes
const base64 = fragment.replace(/-/g, '+').replace(/_/g, '/');
const rawKeyBytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
const key = await crypto.subtle.importKey(
'raw', rawKeyBytes, { name: 'AES-GCM' }, false, ['decrypt']
);
// Import key
const key = await crypto.subtle.importKey(
'raw', rawKeyBytes, { name: 'AES-GCM' }, false, ['decrypt']
);
setStatus('Downloading\u2026');
const response = await fetch('/download/' + id + '/data');
// Derive hash → verify it matches the file ID in the URL
const hash = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', rawKeyBytes)))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
const pathId = location.pathname.split('/').pop();
if (hash !== pathId) {
showError('Invalid link — key does not match file.');
return;
}
setStatus('Downloading\u2026');
const response = await fetch(location.pathname + '/data');
if (response.status === 410) {
showError('This file has expired or reached its download limit.');
return;
}
if (!response.ok) {
showError(`Download failed (${response.status}).`);
return;
}
// Extract filename from Content-Disposition header
if (response.status === 410) {
showError('This file has expired or reached its download limit.');
} else if (!response.ok) {
showError(`Download failed (${response.status}).`);
} else {
const disposition = response.headers.get('Content-Disposition') || '';
const filename = disposition.match(/filename="?([^"]+)"?/)?.[1] || 'download';
setStatus('Decrypting\u2026');
const encrypted = new Uint8Array(await response.arrayBuffer());
const iv = encrypted.slice(0, 12);
const ciphertext = encrypted.slice(12);
const plaintext = await decryptFile(await response.arrayBuffer(), key);
const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
// Trigger browser download
const url = URL.createObjectURL(new Blob([plaintext]));
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
showSuccess(filename);
htmx.swap(htmx.find('#download-state'), `
<div class="drop-zone-icon mb-3">&#x1F512;</div>
<div class="fw-medium mb-2">${filename}</div>
<button id="download-btn" class="btn btn-outline-success mt-2">Download</button>`,
{ swapStyle: 'innerHTML' });
} catch (err) {
showError('Decryption failed: ' + err.message);
htmx.on(htmx.find('#download-btn'), 'click', () => {
a.click();
URL.revokeObjectURL(url);
showSuccess(filename);
});
}
})();
} catch (err) {
showError('Decryption failed: ' + err.message);
}
function setStatus(msg) {
const el = document.getElementById('download-status');
const el = htmx.find('#download-status');
if (el) el.textContent = msg;
}
function showSuccess(filename) {
document.getElementById('download-state').innerHTML = `
htmx.swap(htmx.find('#download-state'), `
<div class="drop-zone-icon mb-3">&#x2705;</div>
<div class="fw-medium mb-1">${filename}</div>
<div class="drop-zone-text mb-3">Your download has started.</div>
<a href="/" class="btn btn-link drop-zone-text text-decoration-none">Send a file</a>`;
<a href="/" class="btn btn-link drop-zone-text text-decoration-none">Send a file</a>`,
{ swapStyle: 'innerHTML' });
}
function showError(msg) {
document.getElementById('download-state').innerHTML = `
htmx.swap(htmx.find('#download-state'), `
<div class="drop-zone-icon mb-3">&#x26A0;</div>
<div class="drop-zone-text mb-3">${msg}</div>
<a href="/" class="btn btn-link drop-zone-text text-decoration-none">Go home</a>`;
<a href="/" class="btn btn-link drop-zone-text text-decoration-none">Go home</a>`,
{ swapStyle: 'innerHTML' });
}