diff --git a/backend/config/config.go b/backend/config/config.go index 7960e0a..aafe61d 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -1,6 +1,7 @@ package config import ( + "log" "os" "strconv" ) @@ -49,6 +50,11 @@ func Load() *Config { cfg.VersionsDir = cfg.StorageDir + "/.versions" cfg.BackupDir = cfg.StorageDir + "/.backups" + if cfg.OnlyOfficeJWT == "" { + log.Println("WARNING: ONLYOFFICE_JWT_SECRET not set \u2014 Document Server requests will NOT be authenticated.") + log.Println(" Anyone with network access to the Document Server can open/edit documents.") + } + return cfg } diff --git a/backend/handlers/files.go b/backend/handlers/files.go index 3684aaa..bfb68f6 100644 --- a/backend/handlers/files.go +++ b/backend/handlers/files.go @@ -554,7 +554,7 @@ func (h *FSHandler) Delete(c *fiber.Ctx) error { isPermanent := c.Query("permanent") == "true" if isPermanent { // Ensure we don't delete the root - if resolvedPath == c.Locals("resolvedPath").(string) && relativePath == "." { + if resolvedPath == h.Config.StorageDir && relativePath == "." { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "cannot permanently delete root"}) } @@ -592,8 +592,14 @@ func (h *FSHandler) Delete(c *fiber.Ctx) error { if err := os.Rename(resolvedPath, trashPath); err != nil { fmt.Printf("Rename error %s -> %s: %v. Attempting copy & delete fallback...\n", resolvedPath, trashPath, err) - if err := copyFile(resolvedPath, trashPath); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to move to trash"}) + if isDir == 1 { + if err := copyDir(resolvedPath, trashPath); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to move directory to trash"}) + } + } else { + if err := copyFile(resolvedPath, trashPath); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to move file to trash"}) + } } safeRemovePath(h.Config.StorageDir, resolvedPath) } diff --git a/backend/handlers/sharing.go b/backend/handlers/sharing.go index 24c05ba..8910c52 100644 --- a/backend/handlers/sharing.go +++ b/backend/handlers/sharing.go @@ -119,13 +119,11 @@ func (h *ShareHandler) ListShares(c *fiber.Ctx) error { // Skip expired shares if expiresAt != nil { - t, err := time.Parse("2006-01-02 15:04:05-07:00", *expiresAt) - if err == nil && time.Now().After(t) { - // To handle different time formats, we could parse it more robustly - // but for now, we'll let frontend also check or we just check here. + t, err := time.Parse(time.RFC3339, *expiresAt) + if err != nil { + t, err = time.Parse("2006-01-02 15:04:05", *expiresAt) } - t2, err2 := time.Parse(time.RFC3339, *expiresAt) - if err2 == nil && time.Now().After(t2) { + if err == nil && time.Now().After(t) { continue } } diff --git a/backend/handlers/webdav.go b/backend/handlers/webdav.go index 37ca0b1..3a66a0b 100644 --- a/backend/handlers/webdav.go +++ b/backend/handlers/webdav.go @@ -21,6 +21,16 @@ import ( "golang.org/x/net/webdav" ) +type statusCapture struct { + http.ResponseWriter + statusCode int +} + +func (sc *statusCapture) WriteHeader(code int) { + sc.statusCode = code + sc.ResponseWriter.WriteHeader(code) +} + type WebDAVHandler struct { Handler *webdav.Handler DB *database.DB @@ -117,11 +127,11 @@ func (h *WebDAVHandler) Handle(c *fiber.Ctx) error { // Adapt Fiber's FastHTTP to standard net/http for the WebDAV handler fasthttpadaptor.NewFastHTTPHandlerFunc(func(w http.ResponseWriter, r *http.Request) { relPath := strings.TrimPrefix(r.URL.Path, "/webdav") - // Native WebDAV handles overwriting automatically - h.Handler.ServeHTTP(w, r) - // If it was a PUT request, we should sync it with our DB (checksum, size, etc.) - if r.Method == "PUT" && r.Response != nil && r.Response.StatusCode >= 200 && r.Response.StatusCode < 300 { + capture := &statusCapture{ResponseWriter: w, statusCode: 200} + h.Handler.ServeHTTP(capture, r) + + if r.Method == "PUT" && capture.statusCode >= 200 && capture.statusCode < 300 { h.syncToDB(relPath) } })(c.Context()) diff --git a/backend/main.go b/backend/main.go index b7f608f..774249c 100644 --- a/backend/main.go +++ b/backend/main.go @@ -104,9 +104,14 @@ func main() { return c.Next() }) app.Use(helmet.New()) + app.Use(func(c *fiber.Ctx) error { + c.Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' wss: https: http:;") + return c.Next() + }) allowedOrigins := os.Getenv("ALLOWED_ORIGINS") if allowedOrigins == "" { - allowedOrigins = "http://localhost:3000" + log.Println("WARNING: ALLOWED_ORIGINS not set \u2014 CORS will reject all cross-origin requests") + allowedOrigins = "https://no-origin-configured.invalid" } app.Use(cors.New(cors.Config{ diff --git a/backend/middleware/auth.go b/backend/middleware/auth.go index e125e43..2b0db6b 100644 --- a/backend/middleware/auth.go +++ b/backend/middleware/auth.go @@ -23,8 +23,17 @@ func AuthMiddleware(jwtSecret string) fiber.Handler { isQueryToken := false if tokenString == "" { - tokenString = c.Query("token") - isQueryToken = true + allowedPaths := []string{"/api/onlyoffice/callback", "/api/files/download", "/api/files/thumbnail"} + path := c.Path() + for _, p := range allowedPaths { + if strings.HasPrefix(path, p) { + tokenString = c.Query("token") + if tokenString != "" { + isQueryToken = true + } + break + } + } } if tokenString == "" { diff --git a/frontend/package.json b/frontend/package.json index 482cedf..b8edf55 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,8 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "postinstall": "node -e \"require('fs').copyFileSync('node_modules/pdfjs-dist/build/pdf.worker.min.mjs', 'public/pdf.worker.min.mjs')\"" }, "dependencies": { "@onlyoffice/document-editor-react": "^2.2.0", diff --git a/frontend/src/components/PdfViewer.tsx b/frontend/src/components/PdfViewer.tsx index f5f7b3b..c14caa9 100644 --- a/frontend/src/components/PdfViewer.tsx +++ b/frontend/src/components/PdfViewer.tsx @@ -7,7 +7,7 @@ import 'react-pdf/dist/Page/AnnotationLayer.css'; import 'react-pdf/dist/Page/TextLayer.css'; if (typeof window !== 'undefined') { - pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`; + pdfjs.GlobalWorkerOptions.workerSrc = `/pdf.worker.min.mjs`; } export default function PdfViewer({ url }: { url: string }) { diff --git a/frontend/src/components/SettingsPage.tsx b/frontend/src/components/SettingsPage.tsx index 18fc23b..5207a26 100644 --- a/frontend/src/components/SettingsPage.tsx +++ b/frontend/src/components/SettingsPage.tsx @@ -72,7 +72,14 @@ export default function SettingsPage({ onLogout }: SettingsPageProps) { setSuccess(false); try { - await api.updateSettings(settings); + const existing = await api.getSettings(); + const merged = { + ...(existing?.settings || existing || {}), + theme: settings.theme, + grid_size: settings.grid_size, + trash_auto_purge_days: settings.trash_auto_purge_days, + }; + await api.updateSettings(merged); setSuccess(true); setTimeout(() => setSuccess(false), 3000); applyTheme(settings.theme); diff --git a/frontend/src/components/ShareModal.tsx b/frontend/src/components/ShareModal.tsx index 696d086..b4af917 100644 --- a/frontend/src/components/ShareModal.tsx +++ b/frontend/src/components/ShareModal.tsx @@ -85,7 +85,8 @@ export default function ShareModal({ filePath, onClose }: ShareModalProps) { Password (Optional) setPassword(e.target.value)} placeholder="Leave blank for no password"