153 lines
4.8 KiB
Go
153 lines
4.8 KiB
Go
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"`
|
|
GuestName string `json:"guest_name"`
|
|
}
|
|
|
|
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.EffectivePublicSharePermissions(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, req.GuestName)
|
|
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(),
|
|
"mode": mode,
|
|
})
|
|
}
|
|
|
|
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})
|
|
}
|