ultisuite-backend/internal/api/ultidraw/handlers.go
R3D347HR4Y 0466a1c169
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
wow
2026-06-11 01:22:52 +02:00

179 lines
5.2 KiB
Go

package ultidraw
import (
"encoding/json"
"log/slog"
"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/api/middleware"
"github.com/ultisuite/ulti-backend/internal/permission"
)
type Handler struct {
svc *Service
logger *slog.Logger
}
func NewHandler(svc *Service) *Handler {
return &Handler{
svc: svc,
logger: slog.Default().With("component", "ultidraw-api"),
}
}
func (h *Handler) Routes(authMiddleware func(http.Handler) http.Handler) chi.Router {
r := chi.NewRouter()
r.Post("/hooks/store", h.HookStore)
r.Get("/internal/document", h.InternalLoadDocument)
r.Group(func(pr chi.Router) {
pr.Use(authMiddleware)
read := middleware.RequirePermission(permission.ResourceDrive, permission.LevelRead)
pr.With(read).Post("/session", h.CreateSession)
})
return r
}
func (h *Handler) RegisterPublicShareRoutes(r chi.Router) {
r.Post("/shares/{token}/ultidraw/session", h.PublicShareSession)
r.Get("/shares/{token}/ultidraw/document", h.PublicShareDocument)
r.Put("/shares/{token}/ultidraw/document", h.PublicSharePutDocument)
}
type sessionRequest struct {
Path string `json:"path"`
Mode string `json:"mode"`
}
func (h *Handler) CreateSession(w http.ResponseWriter, r *http.Request) {
claims := middleware.ClaimsFromContext(r.Context())
ncUser, err := h.svc.EnsureNextcloudUser(r.Context(), claims)
if err != nil {
apivalidate.WriteInternal(w, r)
return
}
var req sessionRequest
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
}
mode := strings.TrimSpace(req.Mode)
if mode == "" {
mode = "edit"
}
result, err := h.svc.CreateSession(r.Context(), ncUser, req.Path, mode, claims.Sub, claims.Name)
if err != nil {
h.logger.Error("ultidraw session", "error", err)
apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, err.Error(), nil)
return
}
apiresponse.WriteJSON(w, http.StatusOK, result)
}
func (h *Handler) InternalLoadDocument(w http.ResponseWriter, r *http.Request) {
secret := r.Header.Get("X-Hocuspocus-Secret")
if h.svc.Cfg.HocuspocusSecret != "" && secret != h.svc.Cfg.HocuspocusSecret {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
user := strings.TrimSpace(r.URL.Query().Get("user"))
path := strings.TrimSpace(r.URL.Query().Get("path"))
body, err := h.svc.LoadDocumentForUser(r.Context(), user, path)
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(body)
}
type hookStorePayload struct {
Room string `json:"room"`
Path string `json:"path"`
User string `json:"user"`
Sub string `json:"sub"`
YjsState string `json:"yjsState"`
Document json.RawMessage `json:"document,omitempty"`
}
func (h *Handler) HookStore(w http.ResponseWriter, r *http.Request) {
secret := r.Header.Get("X-Hocuspocus-Secret")
if h.svc.Cfg.HocuspocusSecret != "" && secret != h.svc.Cfg.HocuspocusSecret {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
var payload hookStorePayload
if err := apivalidate.DecodeJSON(w, r, 32<<20, &payload); err != nil {
return
}
path := normalizePath(payload.Path)
existingRaw, _ := h.svc.LoadDocumentForUser(r.Context(), payload.User, path)
var existing UltiDrawDoc
if len(existingRaw) > 0 {
if parsed, err := ParseUltiDrawDoc(existingRaw); err == nil {
existing = parsed
}
}
var raw []byte
if len(payload.Document) > 0 {
doc, err := ApplyUltiDrawPatch(existing, payload.Document)
if err != nil {
apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(
apivalidate.FieldDetail{Field: "document", Message: "invalid JSON"},
))
return
}
if !isEmptyElements(doc.Elements) {
doc.YjsState = payload.YjsState
}
if isEmptyElements(doc.Elements) && len(existingRaw) > 0 && !isEmptyElements(existing.Elements) {
w.WriteHeader(http.StatusNoContent)
return
}
raw, err = doc.Marshal()
if err != nil {
apivalidate.WriteInternal(w, r)
return
}
} else if payload.YjsState != "" {
doc := existing
if doc.Type == "" {
doc = NewUltiDrawDoc(nil, nil, nil)
}
if isEmptyElements(doc.Elements) {
doc.YjsState = payload.YjsState
}
if isEmptyElements(doc.Elements) && len(existingRaw) > 0 && !isEmptyElements(existing.Elements) {
w.WriteHeader(http.StatusNoContent)
return
}
var err error
raw, err = doc.Marshal()
if err != nil {
apivalidate.WriteInternal(w, r)
return
}
} else {
apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(
apivalidate.FieldDetail{Field: "document", Message: "required"},
))
return
}
if err := h.svc.SaveDocument(r.Context(), payload.User, path, raw, payload.Sub); err != nil {
h.logger.Error("hook store", "error", err, "path", path)
apivalidate.WriteInternal(w, r)
return
}
w.WriteHeader(http.StatusNoContent)
}