pediatric-ai-scribe-v3/public/js/documents.js
Daniel 5a700a2a27 Hybrid auth: cookie-only on web, Keychain Bearer on mobile
Runtime split driven by window.Capacitor.isNativePlatform():

  Web browser
    - No token in localStorage / sessionStorage — XSS can't read it
    - Server-set httpOnly cookie carries the session
    - fetch() default credentials='same-origin' sends the cookie
    - getAuthHeaders() returns Content-Type only, no Authorization
    - Middleware already falls back to cookie when Bearer is absent

  Capacitor native (iOS / Android)
    - Unchanged — Bearer token lives in Keychain / Keystore via the
      capacitor-secure-storage-plugin SecureStorage wrapper
    - Bearer header still sent on every request

enterApp() / clearSession() / getAuthHeaders() all now branch on
isNativeApp(). Legacy localStorage entries from the dual-mode era
are wiped on clearSession() for users migrating in.

Rollback: git reset --hard pre-httponly-only-2026-04-14
2026-04-14 04:11:55 +02:00

127 lines
5.4 KiB
JavaScript

// ============================================================
// DOCUMENTS.JS — S3 document upload & management UI
// ============================================================
(function() {
function loadDocuments() {
var container = document.getElementById('documents-list');
if (!container) return;
fetch('/api/documents', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.s3_configured) {
container.innerHTML = '<p style="color:var(--g400);font-size:13px;">S3 storage not configured. Set S3_BUCKET in server environment.</p>';
var uploadArea = document.getElementById('doc-upload-area');
if (uploadArea) uploadArea.style.display = 'none';
return;
}
var docs = data.documents || [];
if (docs.length === 0) {
container.innerHTML = '<p style="color:var(--g400);font-size:13px;">No documents uploaded yet.</p>';
return;
}
container.innerHTML = docs.map(function(doc) {
var sizeStr = doc.size_bytes < 1024 ? doc.size_bytes + ' B' :
doc.size_bytes < 1048576 ? Math.round(doc.size_bytes / 1024) + ' KB' :
(doc.size_bytes / 1048576).toFixed(1) + ' MB';
var date = doc.created_at ? new Date(doc.created_at).toLocaleDateString() : '';
return '<div class="saved-enc-item" style="padding:8px 12px;">' +
'<div style="flex:1;min-width:0;">' +
'<div style="font-weight:600;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' +
'<i class="fas fa-file" style="margin-right:4px;color:var(--g400);"></i>' + esc(doc.filename) +
'</div>' +
'<div style="font-size:11px;color:var(--g500);">' + sizeStr + ' &middot; ' + date +
(doc.description ? ' &middot; ' + esc(doc.description) : '') +
'</div>' +
'</div>' +
'<button class="btn-sm btn-primary doc-download-btn" data-id="' + doc.id + '"><i class="fas fa-download"></i></button>' +
'<button class="btn-sm btn-ghost doc-delete-btn" data-id="' + doc.id + '" style="color:var(--red);"><i class="fas fa-trash"></i></button>' +
'</div>';
}).join('');
container.querySelectorAll('.doc-download-btn').forEach(function(btn) {
btn.addEventListener('click', function() { downloadDocument(btn.dataset.id); });
});
container.querySelectorAll('.doc-delete-btn').forEach(function(btn) {
btn.addEventListener('click', function() { deleteDocument(btn.dataset.id); });
});
})
.catch(function() {
container.innerHTML = '<p style="color:var(--g400);font-size:13px;">Failed to load documents.</p>';
});
}
function uploadDocument() {
var fileInput = document.getElementById('doc-file-input');
var descInput = document.getElementById('doc-description');
if (!fileInput || !fileInput.files[0]) { showToast('Select a file first', 'error'); return; }
var formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('description', descInput ? descInput.value : '');
showLoading('Uploading document...');
fetch('/api/documents/upload', {
method: 'POST',
headers: window.IS_NATIVE_APP
? { 'Authorization': 'Bearer ' + (window.AUTH_TOKEN || (window.SecureStorage && window.SecureStorage.getSync('ped_scribe_token')) || '') }
: {}, // web: httpOnly cookie sent automatically via credentials:'same-origin' default
credentials: 'same-origin',
body: formData
})
.then(function(r) { return r.json(); })
.then(function(data) {
hideLoading();
if (data.success) {
showToast('Document uploaded: ' + data.filename, 'success');
fileInput.value = '';
if (descInput) descInput.value = '';
loadDocuments();
} else {
showToast(data.error || 'Upload failed', 'error');
}
})
.catch(function(err) { hideLoading(); showToast('Upload failed: ' + err.message, 'error'); });
}
function downloadDocument(id) {
fetch('/api/documents/' + id + '/download', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success && data.url) {
window.open(data.url, '_blank');
} else {
showToast(data.error || 'Download failed', 'error');
}
})
.catch(function() { showToast('Download failed', 'error'); });
}
function deleteDocument(id) {
showConfirm('Delete this document permanently?', function() {
fetch('/api/documents/' + id, { method: 'DELETE', headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) { showToast('Document deleted', 'info'); loadDocuments(); }
else showToast(data.error || 'Delete failed', 'error');
})
.catch(function() { showToast('Delete failed', 'error'); });
}, { danger: true, confirmText: 'Delete' });
}
function esc(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// Expose for settings tab load
window.loadDocuments = loadDocuments;
// Wire upload button
document.addEventListener('click', function(e) {
if (e.target.closest('#btn-doc-upload')) uploadDocument();
});
console.log('Documents module loaded');
})();