security: remediate vulnerabilities and issues from codebase audit
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m15s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m15s
This commit is contained in:
parent
b60a09196a
commit
fb3f3a3393
15 changed files with 465 additions and 208 deletions
|
|
@ -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)} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Reference in a new issue