security: remediate vulnerabilities and issues from codebase audit
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m15s

This commit is contained in:
Elijah 2026-05-23 10:04:08 -07:00
parent b60a09196a
commit fb3f3a3393
15 changed files with 465 additions and 208 deletions

View file

@ -159,7 +159,7 @@ function Tooltip({ children, text }: { children: React.ReactNode; text: string }
);
}
function ThumbnailImage({ checksum, fallbackIcon }: { checksum: string; fallbackIcon: React.ReactNode }) {
function ThumbnailImage({ checksum, fallbackIcon, downloadToken }: { checksum: string; fallbackIcon: React.ReactNode; downloadToken: string }) {
const [errorCount, setErrorCount] = useState(0);
const [key, setKey] = useState(0);
const [showFallback, setShowFallback] = useState(false);
@ -168,7 +168,7 @@ function ThumbnailImage({ checksum, fallbackIcon }: { checksum: string; fallback
<>
<img
key={key}
src={`${api.getThumbnailUrl(checksum)}${errorCount > 0 ? `?t=${key}` : ''}`}
src={downloadToken ? `${api.getThumbnailUrl(checksum, downloadToken)}${errorCount > 0 ? `&t=${key}` : ''}` : ''}
alt=""
className="w-full h-full object-cover"
style={{ display: showFallback ? 'none' : 'block' }}
@ -248,6 +248,26 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
const [selectionBox, setSelectionBox] = useState<{ startX: number; startY: number; currentX: number; currentY: number } | null>(null);
const [initialSelectedFiles, setInitialSelectedFiles] = useState<Set<string>>(new Set());
const searchTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [downloadToken, setDownloadToken] = useState<string>('');
// Fetch download token loop
useEffect(() => {
let mounted = true;
const fetchToken = async () => {
try {
const token = await api.createDownloadToken();
if (mounted) setDownloadToken(token);
} catch (e) {
console.error('Failed to refresh download token', e);
}
};
fetchToken();
const interval = setInterval(fetchToken, 4 * 60 * 1000); // 4 minutes
return () => {
mounted = false;
clearInterval(interval);
};
}, []);
const mainContainerRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
@ -810,7 +830,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
if (isMedia && file.checksum && viewMode === 'grid') {
return (
<div className="relative w-full h-full flex items-center justify-center overflow-hidden rounded-md">
<ThumbnailImage checksum={file.checksum} fallbackIcon={fallbackIcon} />
<ThumbnailImage checksum={file.checksum} fallbackIcon={fallbackIcon} downloadToken={downloadToken} />
</div>
);
}
@ -1496,18 +1516,17 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
style={{ color: 'var(--color-text-primary)' }}
title="Download Selected"
onClick={() => {
if (!downloadToken) return;
const form = document.createElement('form');
form.method = 'POST';
form.action = api.getBulkDownloadUrl();
form.action = api.getBulkDownloadUrl(downloadToken);
form.target = '_blank';
Array.from(selectedFiles).forEach(path => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'paths';
input.value = path;
form.appendChild(input);
});
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'paths';
input.value = JSON.stringify(Array.from(selectedFiles));
form.appendChild(input);
document.body.appendChild(form);
form.submit();
@ -1598,17 +1617,27 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
setContextMenu(null);
}}
/>
<ContextMenuItem
icon={<Download className="w-4 h-4" />}
label="Download"
onClick={() => {
const downloadUrl = contextMenu.file.is_dir
? api.getDownloadFolderUrl(contextMenu.file.path)
: api.getDownloadUrl(contextMenu.file.path);
window.open(downloadUrl, '_blank');
setContextMenu(null);
}}
/>
{contextMenu.file.is_dir ? (
<ContextMenuItem
icon={<Download className="w-4 h-4" />}
label="Download Folder"
onClick={() => {
if (!downloadToken) return;
window.location.href = api.getDownloadFolderUrl(contextMenu.file.path, downloadToken);
setContextMenu(null);
}}
/>
) : (
<ContextMenuItem
icon={<Download className="w-4 h-4" />}
label="Download"
onClick={() => {
if (!downloadToken) return;
window.location.href = api.getDownloadUrl(contextMenu.file.path, downloadToken);
setContextMenu(null);
}}
/>
)}
<ContextMenuItem
icon={<Pencil className="w-4 h-4" />}
label="Rename"
@ -1767,7 +1796,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
</AnimatePresence>
<TaskManager />
<FilePreview file={previewFile} onClose={() => setPreviewFile(null)} />
<FilePreview file={previewFile} onClose={() => setPreviewFile(null)} downloadToken={downloadToken} />
<AnimatePresence>
{sharingFile && (
<ShareModal filePath={sharingFile} onClose={() => setSharingFile(null)} />

View file

@ -18,6 +18,7 @@ interface FilePreviewProps {
mime_type: string;
} | null;
onClose: () => void;
downloadToken?: string;
}
function getFileDisplayName(name: string) {
@ -26,7 +27,7 @@ function getFileDisplayName(name: string) {
return name;
}
export default function FilePreview({ file, onClose }: FilePreviewProps) {
export default function FilePreview({ file, onClose, downloadToken }: FilePreviewProps) {
const [content, setContent] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@ -53,7 +54,8 @@ export default function FilePreview({ file, onClose }: FilePreviewProps) {
setLoading(true);
setError(null);
fetch(api.getDownloadUrl(file.path))
if (!downloadToken) return;
fetch(api.getDownloadUrl(file.path, downloadToken))
.then((res) => {
if (!res.ok) throw new Error('Failed to load file content');
return res.text();
@ -67,7 +69,7 @@ export default function FilePreview({ file, onClose }: FilePreviewProps) {
if (!file) return null;
const previewType = getPreviewType(file);
const downloadUrl = api.getDownloadUrl(file.path);
const downloadUrl = downloadToken ? api.getDownloadUrl(file.path, downloadToken) : '';
return (
<AnimatePresence>

View file

@ -227,24 +227,32 @@ class ApiClient {
});
}
getDownloadUrl(path: string) {
const url = `${API_BASE}/api/files/download/?path=${encodeURIComponent(path)}`;
return this.accessToken ? `${url}&token=${this.accessToken}` : url;
async createDownloadToken(): Promise<string> {
const res = await fetch(`${API_BASE}/api/auth/download-token`, {
method: 'GET',
headers: {
Authorization: `Bearer ${this.accessToken}`,
},
});
if (!res.ok) throw new Error('Failed to fetch download token');
const data = await res.json();
return data.token;
}
getDownloadFolderUrl(path: string) {
const url = `${API_BASE}/api/files/download-folder?path=${encodeURIComponent(path)}`;
return this.accessToken ? `${url}&token=${this.accessToken}` : url;
getDownloadUrl(path: string, token: string) {
return `${API_BASE}/api/files/download/?path=${encodeURIComponent(path)}&token=${token}`;
}
getBulkDownloadUrl() {
const url = `${API_BASE}/api/files/download-bulk`;
return this.accessToken ? `${url}?token=${this.accessToken}` : url;
getDownloadFolderUrl(path: string, token: string) {
return `${API_BASE}/api/files/download-folder?path=${encodeURIComponent(path)}&token=${token}`;
}
getThumbnailUrl(checksum: string) {
const url = `${API_BASE}/api/files/thumbnail/${encodeURIComponent(checksum)}`;
return this.accessToken ? `${url}?token=${this.accessToken}` : url;
getBulkDownloadUrl(token: string) {
return `${API_BASE}/api/files/download-bulk?token=${token}`;
}
getThumbnailUrl(checksum: string, token: string) {
return `${API_BASE}/api/files/thumbnail/${encodeURIComponent(checksum)}?token=${token}`;
}
getStreamHeaders(): Record<string, string> {
@ -379,10 +387,21 @@ class ApiClient {
return res.json();
}
getPublicDownloadUrl(id: string, password?: string, path?: string) {
async createPublicShareToken(id: string, password?: string) {
const res = await fetch(`${API_BASE}/api/public/share/${id}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: password || '' }),
});
if (!res.ok) throw await res.json();
const data = await res.json();
return data.token;
}
getPublicDownloadUrl(id: string, token: string, path?: string) {
let url = `${API_BASE}/api/public/download/${id}`;
const params = new URLSearchParams();
if (password) params.set('pwd', password);
if (token) params.set('token', token);
if (path) params.set('path', path);
const qs = params.toString();
if (qs) {