From fd991a1d4b73537160074a309b831ca328e898f8 Mon Sep 17 00:00:00 2001 From: Elijah Date: Sun, 24 May 2026 12:48:03 -0700 Subject: [PATCH] Add public OnlyOffice download endpoint to bypass auth middleware 401 issue --- backend/handlers/onlyoffice.go | 33 ++++++++++++++++++++ backend/main.go | 3 ++ frontend/src/components/OnlyOfficeEditor.tsx | 4 ++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/backend/handlers/onlyoffice.go b/backend/handlers/onlyoffice.go index ce721a4..309b248 100644 --- a/backend/handlers/onlyoffice.go +++ b/backend/handlers/onlyoffice.go @@ -78,6 +78,39 @@ func (h *OnlyOfficeHandler) Callback(c *fiber.Ctx) error { return c.JSON(fiber.Map{"error": 0}) } +// DownloadForEditor serves a file to the OnlyOffice Document Server. +// This is a public endpoint that validates the download token manually, +// bypassing the auth middleware (which can fail when requests are proxied +// through Next.js rewrites). +func (h *OnlyOfficeHandler) DownloadForEditor(c *fiber.Ctx) error { + path := c.Query("path") + token := c.Query("token") + + if path == "" { + return c.Status(400).JSON(fiber.Map{"error": "path required"}) + } + + // Validate the download token + if err := middleware.ValidateDownloadToken(token, h.Config.JWTSecret, ""); err != nil { + return c.Status(401).JSON(fiber.Map{"error": "unauthorized"}) + } + + // Jail the path to the storage directory + cleaned := filepath.Clean(path) + if filepath.IsAbs(cleaned) || cleaned == ".." || len(cleaned) > 2 && cleaned[:3] == ".." + string(filepath.Separator) { + return c.Status(403).JSON(fiber.Map{"error": "invalid path"}) + } + + fullPath := filepath.Join(h.Config.StorageDir, cleaned) + + info, err := os.Stat(fullPath) + if err != nil || info.IsDir() { + return c.Status(404).JSON(fiber.Map{"error": "file not found"}) + } + + return c.SendFile(fullPath) +} + // CreateBlank creates an empty document for Docs or Slides mode func (h *OnlyOfficeHandler) CreateBlank(c *fiber.Ctx) error { req := new(struct { diff --git a/backend/main.go b/backend/main.go index 2b9fb9d..61c2962 100644 --- a/backend/main.go +++ b/backend/main.go @@ -138,6 +138,9 @@ func main() { // OnlyOffice callback (public but validates query token) public.Post("/onlyoffice/callback", onlyOfficeHandler.Callback) + // OnlyOffice file download (public but validates query token — bypasses auth middleware + // since requests come from the Document Server via Next.js proxy which can drop query params) + public.Get("/onlyoffice/download", onlyOfficeHandler.DownloadForEditor) // Auth routes (with brute force protection) auth := app.Group("/api/auth") diff --git a/frontend/src/components/OnlyOfficeEditor.tsx b/frontend/src/components/OnlyOfficeEditor.tsx index 0ab1e75..0493833 100644 --- a/frontend/src/components/OnlyOfficeEditor.tsx +++ b/frontend/src/components/OnlyOfficeEditor.tsx @@ -41,9 +41,11 @@ export default function OnlyOfficeEditor({ file, type, onClose }: OnlyOfficeEdit // It's on the default bridge network and can reach the Unraid host IP, but NOT // the public domain (hairpin NAT) or Docker DNS names (different network). // Port 5827 is the frontend container (which proxies /api/* to the backend). + // We use dedicated public endpoints that validate tokens manually, bypassing + // the auth middleware which fails when requests come through the Next.js proxy. const INTERNAL_URL = 'http://192.168.50.81:5827/api'; - const fileUrl = `${INTERNAL_URL}/files/download/${encodeURIComponent(file.path)}?token=${downloadToken}`; + const fileUrl = `${INTERNAL_URL}/public/onlyoffice/download?path=${encodeURIComponent(file.path)}&token=${downloadToken}`; const callbackUrl = `${INTERNAL_URL}/public/onlyoffice/callback?path=${encodeURIComponent(file.path)}&token=${downloadToken}`; const baseConfig: any = {