package office import ( "encoding/json" "io" "net/http" "strings" "github.com/go-chi/chi/v5" "github.com/ultisuite/ulti-backend/internal/api/apiresponse" "github.com/ultisuite/ulti-backend/internal/api/apivalidate" "github.com/ultisuite/ulti-backend/internal/nextcloud" ) func (h *Handler) PublicShareRoutes() chi.Router { r := chi.NewRouter() h.RegisterPublicShareRoutes(r) return r } func (h *Handler) RegisterPublicShareRoutes(r chi.Router) { r.Post("/shares/{token}/office/session", h.PublicShareSession) r.Get("/shares/{token}/office/document", h.PublicShareDocument) r.Post("/shares/{token}/office/callback", h.PublicShareCallback) } type publicOfficeSessionRequest struct { Path string `json:"path"` Mode string `json:"mode"` Password string `json:"password"` GuestID string `json:"guest_id"` } func publicSharePassword(r *http.Request) string { return strings.TrimSpace(r.URL.Query().Get("password")) } func (h *Handler) PublicShareSession(w http.ResponseWriter, r *http.Request) { token := strings.TrimSpace(chi.URLParam(r, "token")) if token == "" { apivalidate.WriteNotFound(w, r, "not found") return } var req publicOfficeSessionRequest if err := apivalidate.DecodeJSON(w, r, 32<<10, &req); err != nil { return } if strings.TrimSpace(req.Path) == "" { apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError( apivalidate.FieldDetail{Field: "path", Message: "required"}, )) return } password := strings.TrimSpace(req.Password) if password == "" { password = publicSharePassword(r) } perms, err := h.svc.nc.GetPublicSharePathPermissions(r.Context(), token, req.Path, password) if err != nil { apivalidate.WriteInternal(w, r) return } if !nextcloud.PublicShareCanRead(perms) { http.Error(w, "forbidden", http.StatusForbidden) return } mode := strings.TrimSpace(req.Mode) if mode == "" { mode = "edit" } if mode == "edit" && !nextcloud.PublicShareCanUpdate(perms) { mode = "view" } cfg, err := h.svc.PublicEditorConfig(r.Context(), token, req.Path, mode, password, req.GuestID) if err != nil { h.logger.Error("public editor config", "error", err) apivalidate.WriteInternal(w, r) return } apiresponse.WriteJSON(w, http.StatusOK, map[string]any{ "config": cfg, "serverUrl": h.svc.PublicURL(), }) } func (h *Handler) PublicShareDocument(w http.ResponseWriter, r *http.Request) { token := strings.TrimSpace(chi.URLParam(r, "token")) filePath := strings.TrimSpace(r.URL.Query().Get("path")) password := publicSharePassword(r) sig := strings.TrimSpace(r.URL.Query().Get("sig")) if h.svc.Cfg.JWTSecret != "" && !VerifyPublicDocAccess(token, filePath, password, sig, h.svc.Cfg.JWTSecret) { http.Error(w, "forbidden", http.StatusForbidden) return } body, contentType, err := h.svc.OpenPublicDocument(r.Context(), PublicShareAccess{ Token: token, FilePath: filePath, Password: password, }) if err != nil { http.Error(w, "not found", http.StatusNotFound) return } defer body.Close() if contentType != "" { w.Header().Set("Content-Type", contentType) } _, _ = io.Copy(w, body) } func (h *Handler) PublicShareCallback(w http.ResponseWriter, r *http.Request) { token := strings.TrimSpace(chi.URLParam(r, "token")) filePath := strings.TrimSpace(r.URL.Query().Get("path")) password := publicSharePassword(r) sig := strings.TrimSpace(r.URL.Query().Get("sig")) if h.svc.Cfg.JWTSecret != "" && !VerifyPublicDocAccess(token, filePath, password, sig, h.svc.Cfg.JWTSecret) { apiresponse.WriteJSON(w, http.StatusOK, map[string]int{"error": 1}) return } var payload struct { Status int `json:"status"` URL string `json:"url"` } if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { apiresponse.WriteJSON(w, http.StatusOK, map[string]int{"error": 1}) return } if payload.Status == 2 || payload.Status == 6 { if payload.URL != "" { resp, err := http.Get(payload.URL) if err != nil { h.logger.Error("public office callback fetch", "error", err, "path", filePath, "status", payload.Status) } else if resp.StatusCode != http.StatusOK { h.logger.Error("public office callback fetch status", "status", resp.StatusCode, "path", filePath, "oo_status", payload.Status) resp.Body.Close() } else { defer resp.Body.Close() ct := resp.Header.Get("Content-Type") if err := h.svc.SavePublicDocument(r.Context(), PublicShareAccess{ Token: token, FilePath: filePath, Password: password, }, resp.Body, ct); err != nil { h.logger.Error("public office callback save", "error", err, "path", filePath, "status", payload.Status) } else if payload.Status == 2 { h.svc.RotatePublicDocumentKeyAfterSave(r.Context(), token, filePath, password) } } } } apiresponse.WriteJSON(w, http.StatusOK, map[string]int{"error": 0}) }