From b668418951ff4f8db9da65182f69bb58bc1d8c4c Mon Sep 17 00:00:00 2001 From: Elijah Date: Sat, 23 May 2026 13:08:13 -0700 Subject: [PATCH] fix: resolve folder drag-and-drop, add remember me, fix pinned folder navigation --- backend/handlers/auth.go | 9 ++-- backend/middleware/auth.go | 9 +++- frontend/src/components/FileExplorer.tsx | 59 ++++++++++++++++++++++-- frontend/src/components/LoginPage.tsx | 22 ++++++++- frontend/src/lib/api.ts | 4 +- 5 files changed, 89 insertions(+), 14 deletions(-) diff --git a/backend/handlers/auth.go b/backend/handlers/auth.go index 917b8ea..25fec26 100644 --- a/backend/handlers/auth.go +++ b/backend/handlers/auth.go @@ -40,9 +40,10 @@ type SetupRequest struct { } type LoginRequest struct { - Username string `json:"username"` - Password string `json:"password"` - TOTPCode string `json:"totp_code,omitempty"` + Username string `json:"username"` + Password string `json:"password"` + TOTPCode string `json:"totp_code,omitempty"` + RememberMe bool `json:"remember_me"` } // Setup handles the first-run account creation wizard. @@ -150,7 +151,7 @@ func (h *AuthHandler) Login(c *fiber.Ctx) error { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to generate token"}) } - refreshToken, err := middleware.GenerateRefreshToken(id, req.Username, h.Config.JWTSecret) + refreshToken, err := middleware.GenerateRefreshToken(id, req.Username, h.Config.JWTSecret, req.RememberMe) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to generate refresh token"}) } diff --git a/backend/middleware/auth.go b/backend/middleware/auth.go index 777dea4..1850581 100644 --- a/backend/middleware/auth.go +++ b/backend/middleware/auth.go @@ -86,11 +86,16 @@ func GenerateAccessToken(userID int, username, secret string, expirySecs int) (s } // GenerateRefreshToken creates a longer-lived refresh token. -func GenerateRefreshToken(userID int, username, secret string) (string, error) { +func GenerateRefreshToken(userID int, username, secret string, rememberMe bool) (string, error) { + exp := time.Now().Add(7 * 24 * time.Hour).Unix() // 7 days + if rememberMe { + exp = time.Now().Add(21 * 24 * time.Hour).Unix() // 21 days (3 weeks) + } + claims := jwt.MapClaims{ "sub": userID, "username": username, - "exp": time.Now().Add(7 * 24 * time.Hour).Unix(), // 7 days + "exp": exp, "iat": time.Now().Unix(), "type": "refresh", } diff --git a/frontend/src/components/FileExplorer.tsx b/frontend/src/components/FileExplorer.tsx index ce9cff2..1d5789b 100644 --- a/frontend/src/components/FileExplorer.tsx +++ b/frontend/src/components/FileExplorer.tsx @@ -433,7 +433,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { if (section === 'files') { navigateTo('.'); - loadFiles(); + loadFiles('.'); } else if (section === 'trash') { loadTrash(); } @@ -748,11 +748,12 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { } } - async function uploadFile(file: File) { + async function uploadFile(file: File, targetDir?: string) { const id = `${Date.now()}-${file.name}`; setUploads(prev => [...prev, { id, name: file.name, progress: 0, status: 'uploading' }]); try { - await api.upload(file, currentPath, { + const uploadPath = targetDir ? (currentPath === '.' ? targetDir : `${currentPath}/${targetDir}`) : currentPath; + await api.upload(file, uploadPath, { onProgress: (percent) => { setUploads(prev => prev.map(u => u.id === id ? { ...u, progress: percent } : u)); }, @@ -770,8 +771,56 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { e.preventDefault(); setDragOver(false); if (draggedFile) return; - const droppedFiles = Array.from(e.dataTransfer.files); - await Promise.all(droppedFiles.map(file => uploadFile(file))); + + if (e.dataTransfer.items) { + const filesToUpload: { file: File; path: string }[] = []; + const promises: Promise[] = []; + + const processEntry = async (entry: any, path: string = '') => { + if (entry.isFile) { + const file = await new Promise((resolve) => entry.file(resolve)); + filesToUpload.push({ file, path }); + } else if (entry.isDirectory) { + const dirReader = entry.createReader(); + let entries: any[] = []; + + const readAllEntries = async () => { + const batch = await new Promise((resolve) => dirReader.readEntries(resolve)); + if (batch.length > 0) { + entries = entries.concat(batch); + await readAllEntries(); + } + }; + await readAllEntries(); + + for (const child of entries) { + await processEntry(child, `${path}${entry.name}/`); + } + } + }; + + for (let i = 0; i < e.dataTransfer.items.length; i++) { + const item = e.dataTransfer.items[i]; + if (item.kind === 'file') { + const entry = item.webkitGetAsEntry(); + if (entry) { + promises.push(processEntry(entry)); + } + } + } + + await Promise.all(promises); + + if (filesToUpload.length > 0) { + await Promise.all(filesToUpload.map(({file, path}) => + uploadFile(file, path.replace(/\/$/, '')) + )); + } + } else { + const droppedFiles = Array.from(e.dataTransfer.files); + await Promise.all(droppedFiles.map(file => uploadFile(file))); + } + loadFiles(undefined, true); } diff --git a/frontend/src/components/LoginPage.tsx b/frontend/src/components/LoginPage.tsx index 35054fb..f039aae 100644 --- a/frontend/src/components/LoginPage.tsx +++ b/frontend/src/components/LoginPage.tsx @@ -17,6 +17,7 @@ export default function LoginPage({ onSuccess }: LoginPageProps) { const [error, setError] = useState(''); const [loading, setLoading] = useState(false); const [needs2FA, setNeeds2FA] = useState(false); + const [rememberMe, setRememberMe] = useState(false); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); @@ -24,7 +25,7 @@ export default function LoginPage({ onSuccess }: LoginPageProps) { setLoading(true); try { - const { ok, status, data } = await api.login(username, password, needs2FA ? totpCode : undefined); + const { ok, status, data } = await api.login(username, password, needs2FA ? totpCode : undefined, rememberMe); if (ok) { onSuccess(); @@ -189,6 +190,25 @@ export default function LoginPage({ onSuccess }: LoginPageProps) { )} + {!needs2FA && ( + + )} + {error && (