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
127 lines
5.4 KiB
JavaScript
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 + ' · ' + date +
|
|
(doc.description ? ' · ' + 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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
// 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');
|
|
})();
|