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.
This commit is contained in:
Dionisio
2026-02-12 11:16:58 +01:00
parent 6ca1ac4294
commit 3c03caaf60
9 changed files with 313 additions and 62 deletions
+19
View File
@@ -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;
+19
View File
@@ -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 {
+2 -3
View File
@@ -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);
+12 -4
View File
@@ -609,9 +609,13 @@ const favorites = {
<div class="file-info">Modified ${formattedDate.split(' ')[0]}</div>
`;
// 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 = {
<div class="date-cell">${formattedDate}</div>
`;
// 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
+8 -4
View File
@@ -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);
}
}
});
}
+143 -35
View File
@@ -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 = '<i class="fas fa-spinner fa-spin"></i>';
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 = '<i class="fas fa-spinner fa-spin"></i>';
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 = '<i class="fas fa-spinner fa-spin"></i>';
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 = `
<i class="fas fa-file-download"></i>
<p>${window.i18n ? window.i18n.t('viewer.unsupported_file') : 'Este tipo de archivo no se puede previsualizar.'}</p>
<p>${window.i18n ? window.i18n.t('viewer.unsupported_file') : 'This file type cannot be previewed.'}</p>
<button class="btn btn-primary download-btn">
<i class="fas fa-download"></i>
${window.i18n ? window.i18n.t('viewer.download_file') : 'Descargar archivo'}
${window.i18n ? window.i18n.t('viewer.download_file') : 'Download file'}
</button>
`;
@@ -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 = `
<i class="fas fa-exclamation-triangle"></i>
<p>Error loading the file. Try downloading it directly.</p>
`;
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');
+74 -4
View File
@@ -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 = '<i class="fas fa-spinner fa-spin"></i>';
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 = `
<div class="inline-viewer-icon"><i class="fas fa-file"></i></div>
<div class="inline-viewer-text">
<p>Este tipo de archivo no puede ser previsualizado.</p>
<p>Haz clic en "Descargar" para obtener el archivo.</p>
<p>This file type cannot be previewed.</p>
<p>Click "Download" to get the file.</p>
</div>
`;
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 = `
<div class="inline-viewer-icon"><i class="fas fa-exclamation-triangle"></i></div>
<div class="inline-viewer-text">
<p>Error al cargar el archivo.</p>
<p>Intenta descargarlo directamente.</p>
<p>Error loading the file.</p>
<p>Try downloading it directly.</p>
</div>
`;
container.appendChild(message);
+12 -4
View File
@@ -207,9 +207,13 @@ const recent = {
<div class="file-info">Accessed ${formattedDate.split(' ')[0]}</div>
`;
// 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 = {
<div class="date-cell">${formattedDate}</div>
`;
// 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', {
+24 -8
View File
@@ -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);
}
});