206 lines
6.2 KiB
Go
206 lines
6.2 KiB
Go
package richtext
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"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"
|
|
)
|
|
|
|
type publicSessionRequest struct {
|
|
Path string `json:"path"`
|
|
Mode string `json:"mode"`
|
|
Password string `json:"password"`
|
|
GuestID string `json:"guest_id"`
|
|
GuestName string `json:"guest_name"`
|
|
DisplayName string `json:"display_name"`
|
|
}
|
|
|
|
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 publicSessionRequest
|
|
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)
|
|
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"
|
|
}
|
|
|
|
guestID := strings.TrimSpace(req.GuestID)
|
|
if guestID == "" {
|
|
guestID = "public-guest"
|
|
} else {
|
|
guestID = "public:" + guestID
|
|
}
|
|
guestName := strings.TrimSpace(req.GuestName)
|
|
if guestName == "" {
|
|
guestName = "Invité"
|
|
}
|
|
|
|
result, err := h.svc.CreatePublicSession(r.Context(), token, req.Path, mode, password, guestID, guestName, strings.TrimSpace(req.DisplayName))
|
|
if err != nil {
|
|
h.logger.Error("public richtext session", "error", err)
|
|
apivalidate.WriteInternal(w, r)
|
|
return
|
|
}
|
|
result.Mode = mode
|
|
apiresponse.WriteJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
type publicImportRequest struct {
|
|
Path string `json:"path"`
|
|
SourcePath string `json:"source_path"`
|
|
Password string `json:"password"`
|
|
DisplayName string `json:"display_name"`
|
|
Content json.RawMessage `json:"content"`
|
|
}
|
|
|
|
func (h *Handler) PublicShareImport(w http.ResponseWriter, r *http.Request) {
|
|
token := strings.TrimSpace(chi.URLParam(r, "token"))
|
|
if token == "" {
|
|
apivalidate.WriteNotFound(w, r, "not found")
|
|
return
|
|
}
|
|
var req publicImportRequest
|
|
if err := apivalidate.DecodeJSON(w, r, 32<<20, &req); err != nil {
|
|
return
|
|
}
|
|
password := strings.TrimSpace(req.Password)
|
|
source := strings.TrimSpace(req.SourcePath)
|
|
if source == "" {
|
|
source = strings.TrimSpace(req.Path)
|
|
}
|
|
if source == "" {
|
|
apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(
|
|
apivalidate.FieldDetail{Field: "source_path", Message: "required"},
|
|
))
|
|
return
|
|
}
|
|
perms, err := h.svc.nc.EffectivePublicSharePermissions(r.Context(), token, source, password)
|
|
if err != nil || !nextcloud.PublicShareCanUpdate(perms) {
|
|
http.Error(w, "forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
canonical, err := h.svc.ImportPublicDocument(r.Context(), token, password, strings.TrimSpace(req.DisplayName), ImportRequest{
|
|
SourcePath: source,
|
|
Content: req.Content,
|
|
})
|
|
if err != nil {
|
|
apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, err.Error(), nil)
|
|
return
|
|
}
|
|
apiresponse.WriteJSON(w, http.StatusOK, map[string]string{"canonical_path": canonical})
|
|
}
|
|
|
|
func (h *Handler) PublicShareDocument(w http.ResponseWriter, r *http.Request) {
|
|
token := strings.TrimSpace(chi.URLParam(r, "token"))
|
|
path := strings.TrimSpace(r.URL.Query().Get("path"))
|
|
password := strings.TrimSpace(r.URL.Query().Get("password"))
|
|
sig := strings.TrimSpace(r.URL.Query().Get("sig"))
|
|
if h.svc.Cfg.HocuspocusSecret != "" && !verifyPublicDocAccess(token, path, password, sig, h.svc.Cfg.HocuspocusSecret) {
|
|
http.Error(w, "forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
body, err := h.svc.LoadPublicDocumentLegacy(r.Context(), token, path, password)
|
|
if err != nil {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write(body)
|
|
}
|
|
|
|
func (h *Handler) PublicSharePutDocument(w http.ResponseWriter, r *http.Request) {
|
|
token := strings.TrimSpace(chi.URLParam(r, "token"))
|
|
path := strings.TrimSpace(r.URL.Query().Get("path"))
|
|
password := strings.TrimSpace(r.URL.Query().Get("password"))
|
|
sig := strings.TrimSpace(r.URL.Query().Get("sig"))
|
|
if h.svc.Cfg.HocuspocusSecret != "" && !verifyPublicDocAccess(token, path, password, sig, h.svc.Cfg.HocuspocusSecret) {
|
|
http.Error(w, "forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
perms, err := h.svc.nc.EffectivePublicSharePermissions(r.Context(), token, path, password)
|
|
if err != nil || !nextcloud.PublicShareCanUpdate(perms) {
|
|
http.Error(w, "forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
raw, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
apivalidate.WriteInternal(w, r)
|
|
return
|
|
}
|
|
if err := h.svc.SavePublicDocumentLegacy(r.Context(), token, path, password, raw); err != nil {
|
|
http.Error(w, "save failed", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func verifyDocAccessSig(sig, user, path, secret string) (map[string]any, error) {
|
|
if secret == "" {
|
|
return map[string]any{}, nil
|
|
}
|
|
return verifyJWT(sig, secret)
|
|
}
|
|
|
|
func verifyPublicDocAccess(token, filePath, password, sig, secret string) bool {
|
|
if secret == "" {
|
|
return true
|
|
}
|
|
payload, err := verifyJWT(sig, secret)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if payload["token"] != strings.TrimSpace(token) || payload["path"] != normalizePath(filePath) {
|
|
return false
|
|
}
|
|
if pw, _ := payload["password"].(string); pw != password {
|
|
return false
|
|
}
|
|
if exp, ok := payload["exp"].(float64); ok && int64(exp) < time.Now().Unix() {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func signPublicDocAccess(token, filePath, password, secret string) (string, error) {
|
|
payload := map[string]any{
|
|
"token": strings.TrimSpace(token),
|
|
"path": normalizePath(filePath),
|
|
"password": password,
|
|
"exp": time.Now().Add(2 * time.Hour).Unix(),
|
|
}
|
|
return signJWT(payload, secret)
|
|
}
|