diff --git a/backend/config/config.go b/backend/config/config.go index 4738243..313d529 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -18,6 +18,7 @@ type Config struct { BackupDir string MaxLoginAttempts int LockoutSeconds int + OnlyOfficeJWT string } func Load() *Config { @@ -29,6 +30,7 @@ func Load() *Config { JWTExpirySecs: getEnvInt("JWT_EXPIRY_SECS", 900), // 15 minutes MaxLoginAttempts: getEnvInt("MAX_LOGIN_ATTEMPTS", 5), LockoutSeconds: getEnvInt("LOCKOUT_SECONDS", 300), // 5 minutes + OnlyOfficeJWT: getEnv("ONLYOFFICE_JWT_SECRET", ""), } cfg.DBPath = cfg.DataDir + "/drive.db" diff --git a/backend/handlers/onlyoffice.go b/backend/handlers/onlyoffice.go index 2f78e1c..10b99a2 100644 --- a/backend/handlers/onlyoffice.go +++ b/backend/handlers/onlyoffice.go @@ -10,6 +10,7 @@ import ( "git.elijahkuntz.com/Elijah/drive/config" "git.elijahkuntz.com/Elijah/drive/middleware" "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" ) type OnlyOfficeHandler struct { @@ -127,3 +128,28 @@ func (h *OnlyOfficeHandler) CreateBlank(c *fiber.Ctx) error { "name": filepath.Base(finalPath), }) } + +// SignConfig takes the OnlyOffice configuration payload and signs it with the JWT secret +func (h *OnlyOfficeHandler) SignConfig(c *fiber.Ctx) error { + if h.Config.OnlyOfficeJWT == "" { + return c.Status(500).JSON(fiber.Map{"error": "JWT secret is not configured on the server"}) + } + + var payload map[string]interface{} + if err := c.BodyParser(&payload); err != nil { + return c.Status(400).JSON(fiber.Map{"error": "invalid json payload"}) + } + + // Create a new JWT token using the HS256 algorithm + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(payload)) + + // Sign the token with our secret + signedToken, err := token.SignedString([]byte(h.Config.OnlyOfficeJWT)) + if err != nil { + return c.Status(500).JSON(fiber.Map{"error": "failed to sign token"}) + } + + return c.JSON(fiber.Map{ + "token": signedToken, + }) +} diff --git a/backend/main.go b/backend/main.go index f08785b..2b9fb9d 100644 --- a/backend/main.go +++ b/backend/main.go @@ -188,8 +188,9 @@ func main() { protected.Post("/files/upload", fsHandler.Upload) protected.Post("/files/check-conflicts", fsHandler.CheckConflicts) - // OnlyOffice blanks + // OnlyOffice protected.Post("/onlyoffice/create", onlyOfficeHandler.CreateBlank) + protected.Post("/onlyoffice/sign", onlyOfficeHandler.SignConfig) protected.Get("/files/folders-tree", fsHandler.GetFolderTree) diff --git a/frontend/src/components/OnlyOfficeEditor.tsx b/frontend/src/components/OnlyOfficeEditor.tsx index 9907cbd..2e8e14c 100644 --- a/frontend/src/components/OnlyOfficeEditor.tsx +++ b/frontend/src/components/OnlyOfficeEditor.tsx @@ -22,15 +22,75 @@ interface OnlyOfficeEditorProps { export default function OnlyOfficeEditor({ file, type, onClose }: OnlyOfficeEditorProps) { const [downloadToken, setDownloadToken] = useState(null); const [loadError, setLoadError] = useState(null); + const [config, setConfig] = useState(null); useEffect(() => { // We need a short-lived download token to pass to the Document Server api.createDownloadToken() .then((token: string) => setDownloadToken(token)) - .catch(console.error); + .catch(err => { + setLoadError("Failed to get download token"); + console.error(err); + }); }, [file.path]); - if (!downloadToken) { + useEffect(() => { + if (!downloadToken) return; + + const getApiBase = () => { + if (process.env.NEXT_PUBLIC_API_URL) return process.env.NEXT_PUBLIC_API_URL; + if (typeof window !== 'undefined') return window.location.origin; + return 'http://localhost:8080'; + }; + + const API_BASE = getApiBase(); + + // Use the internal backend URL for OnlyOffice server-to-server communication + // to avoid Hairpin NAT/DNS issues from inside the Docker network. + const INTERNAL_BACKEND_URL = 'http://192.168.50.81:5827'; + + // URL that OnlyOffice will use to download the file + const fileUrl = `${INTERNAL_BACKEND_URL}/api/files/download/?path=${encodeURIComponent(file.path)}&token=${downloadToken}`; + + // Callback URL that OnlyOffice will post to when saving + const callbackUrl = `${INTERNAL_BACKEND_URL}/api/public/onlyoffice/callback?path=${encodeURIComponent(file.path)}&token=${downloadToken}`; + + const baseConfig: any = { + document: { + fileType: file.name.split('.').pop() || 'docx', + key: `${file.path.replace(/[^a-zA-Z0-9\-_=]/g, '_')}_${new Date(file.mod_time).getTime()}`.substring(0, 128), + title: file.name, + url: fileUrl, + }, + documentType: type, + editorConfig: { + callbackUrl: callbackUrl, + mode: 'edit', + customization: { + forcesave: true, + goback: { + url: '', + } + } + }, + }; + + // Sign the config with our backend + api.signOnlyOfficeConfig(baseConfig) + .then((res: any) => { + if (res.token) { + baseConfig.token = res.token; + } + setConfig(baseConfig); + }) + .catch((err) => { + console.warn("Failed to sign config (JWT secret might not be set). Falling back to unsigned config.", err); + setConfig(baseConfig); + }); + + }, [downloadToken, file, type]); + + if (!config) { return (
Loading Editor...
@@ -39,41 +99,7 @@ export default function OnlyOfficeEditor({ file, type, onClose }: OnlyOfficeEdit ); } - const getApiBase = () => { - if (process.env.NEXT_PUBLIC_API_URL) return process.env.NEXT_PUBLIC_API_URL; - if (typeof window !== 'undefined') return window.location.origin; - return 'http://localhost:8080'; - }; - - const API_BASE = getApiBase(); - const docServerUrl = 'https://office.elijahkuntz.com/'; - - // URL that OnlyOffice will use to download the file - const fileUrl = `${API_BASE}/api/files/download/?path=${encodeURIComponent(file.path)}&token=${downloadToken}`; - - // Callback URL that OnlyOffice will post to when saving - const callbackUrl = `${API_BASE}/api/public/onlyoffice/callback?path=${encodeURIComponent(file.path)}&token=${downloadToken}`; - - const config = { - document: { - fileType: file.name.split('.').pop() || 'docx', - key: `${file.path.replace(/[^a-zA-Z0-9\-_=]/g, '_')}_${new Date(file.mod_time).getTime()}`.substring(0, 128), - title: file.name, - url: fileUrl, - }, - documentType: type, - editorConfig: { - callbackUrl: callbackUrl, - mode: 'edit', - customization: { - forcesave: true, - goback: { - url: '', - } - } - }, - }; const onDocumentReady = function (event: any) { console.log("Document is loaded"); @@ -109,7 +135,7 @@ export default function OnlyOfficeEditor({ file, type, onClose }: OnlyOfficeEdit diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 080b818..4047581 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -471,9 +471,17 @@ class ApiClient { // --- OnlyOffice --- async createOnlyOfficeFile(type: 'word' | 'slide') { - const res = await this.request('/api/onlyoffice/create', { + const res = await this.request('/onlyoffice/create', { method: 'POST', - body: { type }, + body: JSON.stringify({ type }) + }); + return res.json(); + } + + async signOnlyOfficeConfig(config: any) { + const res = await this.request('/onlyoffice/sign', { + method: 'POST', + body: JSON.stringify(config) }); return res.json(); }