diff --git a/backend/handlers/files.go b/backend/handlers/files.go index 530ad71..1af5bec 100644 --- a/backend/handlers/files.go +++ b/backend/handlers/files.go @@ -1026,7 +1026,12 @@ func (h *FSHandler) Download(c *fiber.Ctx) error { // Set appropriate headers for range requests c.Set("Accept-Ranges", "bytes") c.Set("Content-Type", mime.TypeByExtension(filepath.Ext(resolvedPath))) - c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", info.Name())) + + if c.Query("download") == "true" { + c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", info.Name())) + } else { + c.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", info.Name())) + } // Use Fiber's built-in SendFile which supports Range requests return c.SendFile(resolvedPath) diff --git a/backend/middleware/auth.go b/backend/middleware/auth.go index 557d6f3..c905e85 100644 --- a/backend/middleware/auth.go +++ b/backend/middleware/auth.go @@ -141,7 +141,7 @@ func GenerateEditToken(userID int, username, secret, filePath string) (string, e claims := jwt.MapClaims{ "sub": userID, "username": username, - "exp": time.Now().Add(24 * time.Hour).Unix(), + "exp": time.Now().Add(7 * 24 * time.Hour).Unix(), "iat": time.Now().Unix(), "type": "edit", } diff --git a/frontend/src/components/FileExplorer.tsx b/frontend/src/components/FileExplorer.tsx index 0087548..f064d5f 100644 --- a/frontend/src/components/FileExplorer.tsx +++ b/frontend/src/components/FileExplorer.tsx @@ -1395,7 +1395,27 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
{getFileIcon(file)}
- {getFileDisplayName(file.name, file.is_dir)} + {renaming === file.path ? ( + setNewName(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleRename(file.path, file.name, file.is_dir); + if (e.key === 'Escape') setRenaming(null); + }} + onBlur={() => handleRename(file.path, file.name, file.is_dir)} + autoFocus + onFocus={(e) => e.target.select()} + onClick={(e) => e.stopPropagation()} + className="flex-1 text-sm bg-transparent outline-none w-full" + style={{ color: 'var(--color-text-primary)', borderBottom: '1px solid var(--color-accent)' }} + /> + ) : ( + + {getFileDisplayName(file.name, file.is_dir)} + + )} )) )} @@ -2274,23 +2294,27 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { className="p-2 hover:bg-white/5 rounded-lg transition-colors flex items-center gap-2 text-sm" 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(downloadToken); - form.target = '_blank'; - - 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(); - document.body.removeChild(form); - setSelectedFiles(new Set()); + onClick={async () => { + try { + const token = await api.createDownloadToken(); + const form = document.createElement('form'); + form.method = 'POST'; + form.action = api.getBulkDownloadUrl(token); + form.target = '_blank'; + + 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(); + document.body.removeChild(form); + setSelectedFiles(new Set()); + } catch (err) { + console.error("Failed to download selected files", err); + } }} > @@ -2385,20 +2409,28 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { } label="Download Folder" - onClick={() => { - if (!downloadToken) return; - window.location.href = api.getDownloadFolderUrl(contextMenu.file.path, downloadToken); - setContextMenu(null); + onClick={async () => { + try { + const token = await api.createDownloadToken(); + window.location.href = api.getDownloadFolderUrl(contextMenu.file.path, token); + setContextMenu(null); + } catch (err) { + console.error("Failed to download folder", err); + } }} /> ) : ( } label="Download" - onClick={() => { - if (!downloadToken) return; - window.location.href = api.getDownloadUrl(contextMenu.file.path, downloadToken); - setContextMenu(null); + onClick={async () => { + try { + const token = await api.createDownloadToken(); + window.location.href = api.getDownloadUrl(contextMenu.file.path, token, true); + setContextMenu(null); + } catch (err) { + console.error("Failed to download file", err); + } }} /> )} diff --git a/frontend/src/components/FilePreview.tsx b/frontend/src/components/FilePreview.tsx index 4c7677b..d0a3946 100644 --- a/frontend/src/components/FilePreview.tsx +++ b/frontend/src/components/FilePreview.tsx @@ -67,6 +67,7 @@ export default function FilePreview({ file, onClose, downloadToken }: FilePrevie const previewType = getPreviewType(file); const downloadUrl = downloadToken ? api.getDownloadUrl(file.path, downloadToken) : ''; + const forceDownloadUrl = downloadToken ? api.getDownloadUrl(file.path, downloadToken, true) : ''; return ( @@ -103,7 +104,7 @@ export default function FilePreview({ file, onClose, downloadToken }: FilePrevie
console.error('Failed to load PDF:', error)} loading={
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index b1b92ad..4de9a85 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -80,6 +80,7 @@ class ApiClient { if (!retryResponse.ok && retryResponse.status === 401) { if (typeof window !== 'undefined') window.dispatchEvent(new Event('auth_error')); + this.clearTokens(); throw new Error(`HTTP 401`); } @@ -90,6 +91,7 @@ class ApiClient { if (!response.ok && endpoint !== '/api/auth/refresh') { if (response.status === 401) { if (typeof window !== 'undefined') window.dispatchEvent(new Event('auth_error')); + this.clearTokens(); } let errorMsg = `HTTP ${response.status}`; try { @@ -318,8 +320,9 @@ class ApiClient { return data.token; } - getDownloadUrl(path: string, token: string) { - return `${API_BASE}/api/files/download?path=${encodeURIComponent(path)}&token=${token}`; + getDownloadUrl(path: string, token: string, forceDownload: boolean = false) { + const url = `${API_BASE}/api/files/download?path=${encodeURIComponent(path)}&token=${token}`; + return forceDownload ? `${url}&download=true` : url; } getDownloadFolderUrl(path: string, token: string) {