ultisuite-backend/internal/api/office/public_handlers.go
2026-06-09 14:30:34 +02:00

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})
}