From 3c03caaf6026d5d1b0e4eed0a8b3aad78054619d Mon Sep 17 00:00:00 2001 From: Dionisio Date: Thu, 12 Feb 2026 11:16:58 +0100 Subject: [PATCH] fix: resolve file viewer auth issues and add text file viewing support - Fix file viewer not sending JWT auth tokens when loading files - inlineViewer.js: already used XHR with auth (images/PDFs worked) - fileViewer.js: was setting img.src/iframe.src directly without auth headers, now uses fetch with Bearer token and blob URLs - ui.js/contextMenus.js/fileRenderer.js/recent.js/favorites.js: replaced all window.location.href = /api/files/... (unauthenticated navigation) with authenticated viewer or fileOps.downloadFile() - Add text file viewing support (text/*, application/json, etc.) - New createTextViewer() in inlineViewer.js with authenticated fetch - New loadTextViewer() in fileViewer.js with authenticated fetch - New isViewableFile() helper in ui.js used across all entry points - CSS styles for .inline-viewer-text-content and .file-viewer-text-content - Translate remaining Spanish strings to English in viewer files Fixes: text files showing 'Token not provided', images failing to load, and text files not being previewable at all. --- static/css/fileViewer.css | 19 ++++ static/css/inlineViewer.css | 19 ++++ static/js/contextMenus.js | 5 +- static/js/favorites.js | 16 +++- static/js/fileRenderer.js | 12 ++- static/js/fileViewer.js | 178 +++++++++++++++++++++++++++++------- static/js/inlineViewer.js | 78 +++++++++++++++- static/js/recent.js | 16 +++- static/js/ui.js | 32 +++++-- 9 files changed, 313 insertions(+), 62 deletions(-) diff --git a/static/css/fileViewer.css b/static/css/fileViewer.css index 5a6f5037..51f3e51c 100644 --- a/static/css/fileViewer.css +++ b/static/css/fileViewer.css @@ -123,6 +123,25 @@ border: none; } +/* Text viewer styles */ +.file-viewer-text-content { + width: 100%; + height: 100%; + margin: 0; + padding: 16px 24px; + font-family: 'Courier New', Consolas, Monaco, monospace; + font-size: 14px; + line-height: 1.6; + color: #2d3748; + background-color: #fff; + overflow: auto; + white-space: pre-wrap; + word-wrap: break-word; + box-sizing: border-box; + text-align: left; + tab-size: 4; +} + /* Loader */ .file-viewer-loader { position: absolute; diff --git a/static/css/inlineViewer.css b/static/css/inlineViewer.css index 0f8ee1d0..2dcb0bc9 100644 --- a/static/css/inlineViewer.css +++ b/static/css/inlineViewer.css @@ -193,6 +193,25 @@ margin: 0 0 16px; } +/* Text viewer */ +.inline-viewer-text-content { + width: 100%; + height: 100%; + margin: 0; + padding: 16px 24px; + font-family: 'Courier New', Consolas, Monaco, monospace; + font-size: 14px; + line-height: 1.6; + color: #2d3748; + background-color: #fff; + overflow: auto; + white-space: pre-wrap; + word-wrap: break-word; + box-sizing: border-box; + text-align: left; + tab-size: 4; +} + /* Responsive adjustments */ @media (max-width: 768px) { .inline-viewer-content { diff --git a/static/js/contextMenus.js b/static/js/contextMenus.js index 50f9a313..08281a4e 100644 --- a/static/js/contextMenus.js +++ b/static/js/contextMenus.js @@ -88,9 +88,8 @@ const contextMenus = { }) .then(response => response.json()) .then(fileDetails => { - // Check if viewable file type - if ((fileDetails.mime_type && fileDetails.mime_type.startsWith('image/')) || - (fileDetails.mime_type && fileDetails.mime_type === 'application/pdf')) { + // Check if viewable file type (images, PDFs, text files) + if (window.ui && window.ui.isViewableFile(fileDetails)) { // Open with inline viewer if (window.inlineViewer) { window.inlineViewer.openFile(fileDetails); diff --git a/static/js/favorites.js b/static/js/favorites.js index 5d37ac17..08ea4842 100644 --- a/static/js/favorites.js +++ b/static/js/favorites.js @@ -609,9 +609,13 @@ const favorites = {
Modified ${formattedDate.split(' ')[0]}
`; - // Download on click + // View or download on click fileGridElement.addEventListener('click', () => { - window.location.href = `/api/files/${file.id}`; + if (window.ui && window.ui.isViewableFile(file) && window.inlineViewer) { + window.inlineViewer.openFile(file); + } else if (window.fileOps) { + window.fileOps.downloadFile(file.id, file.name); + } }); // Context menu @@ -654,9 +658,13 @@ const favorites = {
${formattedDate}
`; - // Download on click + // View or download on click fileListElement.addEventListener('click', () => { - window.location.href = `/api/files/${file.id}`; + if (window.ui && window.ui.isViewableFile(file) && window.inlineViewer) { + window.inlineViewer.openFile(file); + } else if (window.fileOps) { + window.fileOps.downloadFile(file.id, file.name); + } }); // Context menu diff --git a/static/js/fileRenderer.js b/static/js/fileRenderer.js index 4282f60e..27c1a28a 100644 --- a/static/js/fileRenderer.js +++ b/static/js/fileRenderer.js @@ -304,8 +304,10 @@ class FileRenderer { window.inlineViewer.openFile(item); } else { console.warn('Inline viewer not available, downloading directly'); - // Fallback to direct download if viewer is not available - window.location.href = `/api/files/${item.id}`; + // Fallback to authenticated download if viewer is not available + if (window.fileOps) { + window.fileOps.downloadFile(item.id, item.name); + } } }); } @@ -448,8 +450,10 @@ class FileRenderer { window.inlineViewer.openFile(item); } else { console.warn('Inline viewer not available, downloading directly'); - // Fallback to direct download if viewer is not available - window.location.href = `/api/files/${item.id}`; + // Fallback to authenticated download if viewer is not available + if (window.fileOps) { + window.fileOps.downloadFile(item.id, item.name); + } } }); } diff --git a/static/js/fileViewer.js b/static/js/fileViewer.js index b214c399..3b6a2d84 100644 --- a/static/js/fileViewer.js +++ b/static/js/fileViewer.js @@ -128,6 +128,9 @@ class FileViewer { } else if (fileData.mime_type && fileData.mime_type === 'application/pdf') { console.log('FileViewer: Loading PDF viewer'); this.loadPdfViewer(fileData.id, viewerArea); + } else if (fileData.mime_type && this.isTextViewable(fileData.mime_type)) { + console.log('FileViewer: Loading text viewer'); + this.loadTextViewer(fileData.id, viewerArea); } else { console.log('FileViewer: Unsupported file type', fileData.mime_type); // For unsupported files, show download prompt @@ -141,25 +144,37 @@ class FileViewer { * @param {HTMLElement} container - Container element to render into */ loadImageViewer(fileId, container) { - // Create image element - const img = document.createElement('img'); - img.className = 'file-viewer-image'; - img.src = `/api/files/${fileId}`; - img.alt = this.fileData.name; - // Create loader const loader = document.createElement('div'); loader.className = 'file-viewer-loader'; loader.innerHTML = ''; container.appendChild(loader); - // When image loads, remove loader - img.onload = () => { - container.removeChild(loader); - }; - - // Add image to container - container.appendChild(img); + // Fetch image with auth header and create blob URL + this.fetchFileAsBlob(fileId).then(blob => { + const blobUrl = URL.createObjectURL(blob); + this.currentBlobUrl = blobUrl; + + const img = document.createElement('img'); + img.className = 'file-viewer-image'; + img.src = blobUrl; + img.alt = this.fileData.name; + + img.onload = () => { + if (loader.parentNode) container.removeChild(loader); + }; + + img.onerror = () => { + if (loader.parentNode) container.removeChild(loader); + this.showErrorMessage(container); + }; + + container.appendChild(img); + }).catch(error => { + console.error('Error loading image:', error); + if (loader.parentNode) container.removeChild(loader); + this.showErrorMessage(container); + }); // Add zoom controls to toolbar const toolbar = this.viewerContainer.querySelector('.file-viewer-toolbar'); @@ -220,25 +235,87 @@ class FileViewer { * @param {HTMLElement} container - Container element to render into */ loadPdfViewer(fileId, container) { - // Create iframe for PDF viewer - const iframe = document.createElement('iframe'); - iframe.className = 'file-viewer-pdf'; - iframe.src = `/api/files/${fileId}`; - iframe.title = this.fileData.name; - // Create loader const loader = document.createElement('div'); loader.className = 'file-viewer-loader'; loader.innerHTML = ''; container.appendChild(loader); - // When iframe loads, remove loader - iframe.onload = () => { - container.removeChild(loader); - }; + // Fetch PDF with auth header and create blob URL + this.fetchFileAsBlob(fileId).then(blob => { + const blobUrl = URL.createObjectURL(blob); + this.currentBlobUrl = blobUrl; + + const iframe = document.createElement('iframe'); + iframe.className = 'file-viewer-pdf'; + iframe.src = blobUrl; + iframe.title = this.fileData.name; + + iframe.onload = () => { + if (loader.parentNode) container.removeChild(loader); + }; + + container.appendChild(iframe); + }).catch(error => { + console.error('Error loading PDF:', error); + if (loader.parentNode) container.removeChild(loader); + this.showErrorMessage(container); + }); + } + + /** + * Load the text viewer + */ + async loadTextViewer(fileId, container) { + const loader = document.createElement('div'); + loader.className = 'file-viewer-loader'; + loader.innerHTML = ''; + container.appendChild(loader); - // Add iframe to container - container.appendChild(iframe); + try { + const token = localStorage.getItem('oxicloud_token'); + const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; + const response = await fetch(`/api/files/${fileId}?inline=true`, { headers }); + + if (!response.ok) throw new Error(`HTTP ${response.status}`); + + const text = await response.text(); + if (loader.parentNode) container.removeChild(loader); + + const pre = document.createElement('pre'); + pre.className = 'file-viewer-text-content'; + pre.textContent = text; + container.appendChild(pre); + } catch (error) { + console.error('Error loading text:', error); + if (loader.parentNode) container.removeChild(loader); + this.showErrorMessage(container); + } + } + + /** + * Check if a MIME type is text-viewable + */ + isTextViewable(mimeType) { + if (!mimeType) return false; + if (mimeType.startsWith('text/')) return true; + const textTypes = [ + 'application/json', 'application/xml', 'application/javascript', + 'application/x-sh', 'application/x-yaml', 'application/toml', + 'application/x-toml', 'application/sql', + ]; + return textTypes.includes(mimeType); + } + + /** + * Fetch a file as blob with auth headers + */ + async fetchFileAsBlob(fileId) { + const token = localStorage.getItem('oxicloud_token'); + const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; + const response = await fetch(`/api/files/${fileId}?inline=true`, { headers }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.blob(); } /** @@ -251,10 +328,10 @@ class FileViewer { message.innerHTML = ` -

${window.i18n ? window.i18n.t('viewer.unsupported_file') : 'Este tipo de archivo no se puede previsualizar.'}

+

${window.i18n ? window.i18n.t('viewer.unsupported_file') : 'This file type cannot be previewed.'}

`; @@ -272,14 +349,39 @@ class FileViewer { downloadFile() { if (!this.fileData) return; - // Create a link and simulate click - const link = document.createElement('a'); - link.href = `/api/files/${this.fileData.id}`; - link.download = this.fileData.name; - link.target = '_blank'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + // Download with auth headers + const token = localStorage.getItem('oxicloud_token'); + const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; + + fetch(`/api/files/${this.fileData.id}`, { headers }) + .then(res => { + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.blob(); + }) + .then(blob => { + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = this.fileData.name; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + }) + .catch(err => console.error('Download error:', err)); + } + + /** + * Show error message + */ + showErrorMessage(container) { + const message = document.createElement('div'); + message.className = 'file-viewer-unsupported'; + message.innerHTML = ` + +

Error loading the file. Try downloading it directly.

+ `; + container.appendChild(message); } /** @@ -290,6 +392,12 @@ class FileViewer { this.fileData = null; this.viewerContainer.classList.remove('active'); + // Clean up blob URL if exists + if (this.currentBlobUrl) { + URL.revokeObjectURL(this.currentBlobUrl); + this.currentBlobUrl = null; + } + // Reset toolbar (remove zoom controls) const toolbar = this.viewerContainer.querySelector('.file-viewer-toolbar'); const downloadBtn = toolbar.querySelector('.file-viewer-download'); diff --git a/static/js/inlineViewer.js b/static/js/inlineViewer.js index 7ca728b7..a605f5bd 100644 --- a/static/js/inlineViewer.js +++ b/static/js/inlineViewer.js @@ -133,6 +133,19 @@ class InlineViewer { // Create PDF viewer using object tag with blob URL this.createBlobUrlViewer(file, 'pdf', container, loader); } + else if (file.mime_type && this.isTextViewable(file.mime_type)) { + // Hide zoom controls for text files + controls.style.display = 'none'; + + // Show loading indicator + const loader = document.createElement('div'); + loader.className = 'inline-viewer-loader'; + loader.innerHTML = ''; + container.appendChild(loader); + + // Create text viewer using authenticated fetch + this.createTextViewer(file, container, loader); + } else { // Hide zoom controls for unsupported files controls.style.display = 'none'; @@ -143,8 +156,8 @@ class InlineViewer { message.innerHTML = `
-

Este tipo de archivo no puede ser previsualizado.

-

Haz clic en "Descargar" para obtener el archivo.

+

This file type cannot be previewed.

+

Click "Download" to get the file.

`; container.appendChild(message); @@ -154,6 +167,63 @@ class InlineViewer { modal.classList.add('active'); } + // Check if a MIME type is text-viewable + isTextViewable(mimeType) { + if (!mimeType) return false; + if (mimeType.startsWith('text/')) return true; + const textTypes = [ + 'application/json', + 'application/xml', + 'application/javascript', + 'application/x-sh', + 'application/x-yaml', + 'application/toml', + 'application/x-toml', + 'application/sql', + ]; + return textTypes.includes(mimeType); + } + + // Creates a text viewer using authenticated fetch + async createTextViewer(file, container, loader) { + try { + console.log('Creating text viewer for:', file.name); + + const token = localStorage.getItem('oxicloud_token'); + const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; + + const response = await fetch(`/api/files/${file.id}?inline=true`, { headers }); + + if (!response.ok) { + throw new Error(`Error fetching file: ${response.status} ${response.statusText}`); + } + + const text = await response.text(); + + // Remove loader + if (loader && loader.parentNode) { + loader.parentNode.removeChild(loader); + } + + // Create text viewer element + const pre = document.createElement('pre'); + pre.className = 'inline-viewer-text-content'; + pre.textContent = text; + container.appendChild(pre); + + console.log('Text viewer created successfully'); + } catch (error) { + console.error('Error creating text viewer:', error); + + // Remove loader + if (loader && loader.parentNode) { + loader.parentNode.removeChild(loader); + } + + this.showErrorMessage(container); + } + } + // Creates a viewer using a Blob URL to avoid content-disposition header async createBlobUrlViewer(file, type, container, loader) { try { @@ -269,8 +339,8 @@ class InlineViewer { message.innerHTML = `
-

Error al cargar el archivo.

-

Intenta descargarlo directamente.

+

Error loading the file.

+

Try downloading it directly.

`; container.appendChild(message); diff --git a/static/js/recent.js b/static/js/recent.js index 8ecac23a..097d8e52 100644 --- a/static/js/recent.js +++ b/static/js/recent.js @@ -207,9 +207,13 @@ const recent = {
Accessed ${formattedDate.split(' ')[0]}
`; - // Download on click + // View or download on click fileGridElement.addEventListener('click', () => { - window.location.href = `/api/files/${file.id}`; + if (window.ui && window.ui.isViewableFile(file) && window.inlineViewer) { + window.inlineViewer.openFile(file); + } else if (window.fileOps) { + window.fileOps.downloadFile(file.id, file.name); + } // Dispatch custom event to update recent files document.dispatchEvent(new CustomEvent('file-accessed', { @@ -257,9 +261,13 @@ const recent = {
${formattedDate}
`; - // Download on click + // View or download on click fileListElement.addEventListener('click', () => { - window.location.href = `/api/files/${file.id}`; + if (window.ui && window.ui.isViewableFile(file) && window.inlineViewer) { + window.inlineViewer.openFile(file); + } else if (window.fileOps) { + window.fileOps.downloadFile(file.id, file.name); + } // Dispatch custom event to update recent files document.dispatchEvent(new CustomEvent('file-accessed', { diff --git a/static/js/ui.js b/static/js/ui.js index daefedf0..4ccabb1e 100644 --- a/static/js/ui.js +++ b/static/js/ui.js @@ -434,6 +434,24 @@ const ui = { } }, + /** + * Check if a file can be previewed in the viewer + * @param {Object} file - File object with mime_type property + * @returns {boolean} + */ + isViewableFile(file) { + if (!file || !file.mime_type) return false; + if (file.mime_type.startsWith('image/')) return true; + if (file.mime_type === 'application/pdf') return true; + if (file.mime_type.startsWith('text/')) return true; + const textTypes = [ + 'application/json', 'application/xml', 'application/javascript', + 'application/x-sh', 'application/x-yaml', 'application/toml', + 'application/x-toml', 'application/sql', + ]; + return textTypes.includes(file.mime_type); + }, + /** * Show notification * @param {string} title - Notification title @@ -957,17 +975,16 @@ const ui = { } // Check if it's a viewable file type - if ((file.mime_type && file.mime_type.startsWith('image/')) || - (file.mime_type && file.mime_type === 'application/pdf')) { + if (this.isViewableFile(file)) { if (window.inlineViewer) { window.inlineViewer.openFile(file); } else if (window.fileViewer) { window.fileViewer.open(file); } else { - window.location.href = `/api/files/${file.id}`; + window.fileOps.downloadFile(file.id, file.name); } } else { - window.location.href = `/api/files/${file.id}`; + window.fileOps.downloadFile(file.id, file.name); } }); @@ -1051,8 +1068,7 @@ const ui = { } // Check if it's a viewable file type - if ((file.mime_type && file.mime_type.startsWith('image/')) || - (file.mime_type && file.mime_type === 'application/pdf')) { + if (this.isViewableFile(file)) { // Open in the inline viewer if (window.inlineViewer) { window.inlineViewer.openFile(file); @@ -1061,11 +1077,11 @@ const ui = { window.fileViewer.open(file); } else { // No viewer available, download directly - window.location.href = `/api/files/${file.id}`; + window.fileOps.downloadFile(file.id, file.name); } } else { // For other file types, download directly - window.location.href = `/api/files/${file.id}`; + window.fileOps.downloadFile(file.id, file.name); } });