178 lines
5.1 KiB
Go
178 lines
5.1 KiB
Go
package office
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"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/drive"
|
|
"github.com/ultisuite/ulti-backend/internal/api/middleware"
|
|
"github.com/ultisuite/ulti-backend/internal/permission"
|
|
)
|
|
|
|
type Handler struct {
|
|
svc *Service
|
|
drive *drive.Service
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func NewHandler(svc *Service, driveSvc *drive.Service) *Handler {
|
|
return &Handler{
|
|
svc: svc,
|
|
drive: driveSvc,
|
|
logger: slog.Default().With("component", "office-api"),
|
|
}
|
|
}
|
|
|
|
func (h *Handler) PublicRoutes() chi.Router {
|
|
r := chi.NewRouter()
|
|
r.Get("/document", h.ServeDocument)
|
|
r.Post("/callback", h.Callback)
|
|
return r
|
|
}
|
|
|
|
func (h *Handler) ProtectedRoutes() chi.Router {
|
|
r := chi.NewRouter()
|
|
read := middleware.RequirePermission(permission.ResourceDrive, permission.LevelRead)
|
|
write := middleware.RequirePermission(permission.ResourceDrive, permission.LevelWrite)
|
|
r.With(read).Post("/session", h.CreateSession)
|
|
r.With(write).Post("/create", h.CreateDocument)
|
|
return r
|
|
}
|
|
|
|
// Routes registers public OnlyOffice callbacks and authenticated session endpoints on one router.
|
|
func (h *Handler) Routes(authMiddleware func(http.Handler) http.Handler) chi.Router {
|
|
r := chi.NewRouter()
|
|
r.Get("/document", h.ServeDocument)
|
|
r.Post("/callback", h.Callback)
|
|
r.Group(func(pr chi.Router) {
|
|
pr.Use(authMiddleware)
|
|
read := middleware.RequirePermission(permission.ResourceDrive, permission.LevelRead)
|
|
write := middleware.RequirePermission(permission.ResourceDrive, permission.LevelWrite)
|
|
pr.With(read).Post("/session", h.CreateSession)
|
|
pr.With(write).Post("/create", h.CreateDocument)
|
|
})
|
|
return r
|
|
}
|
|
|
|
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 {
|
|
h.logger.Error("ensure nextcloud user", "error", err)
|
|
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"
|
|
}
|
|
|
|
cfg, err := h.svc.EditorConfig(r.Context(), ncUser, req.Path, mode, claims.Name)
|
|
if err != nil {
|
|
h.logger.Error("editor config", "error", err)
|
|
apivalidate.WriteInternal(w, r)
|
|
return
|
|
}
|
|
apiresponse.WriteJSON(w, http.StatusOK, map[string]any{
|
|
"config": cfg,
|
|
"serverUrl": h.svc.PublicURL(),
|
|
})
|
|
}
|
|
|
|
type createRequest struct {
|
|
ParentPath string `json:"parent_path"`
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind"`
|
|
}
|
|
|
|
func (h *Handler) CreateDocument(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 createRequest
|
|
if err := apivalidate.DecodeJSON(w, r, 32<<10, &req); err != nil {
|
|
return
|
|
}
|
|
kind := drive.NewFileKind(strings.TrimSpace(strings.ToLower(req.Kind)))
|
|
target, err := h.drive.CreateNewFile(r.Context(), ncUser, req.ParentPath, req.Name, kind)
|
|
if err != nil {
|
|
apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, err.Error(), nil)
|
|
return
|
|
}
|
|
apiresponse.WriteJSON(w, http.StatusCreated, map[string]string{"path": target})
|
|
}
|
|
|
|
func (h *Handler) ServeDocument(w http.ResponseWriter, r *http.Request) {
|
|
ncUser := r.URL.Query().Get("user")
|
|
filePath := r.URL.Query().Get("path")
|
|
sig := r.URL.Query().Get("sig")
|
|
if h.svc.Cfg.JWTSecret != "" && !VerifyDocAccess(ncUser, filePath, sig, h.svc.Cfg.JWTSecret) {
|
|
http.Error(w, "forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
body, contentType, err := h.svc.OpenDocument(r.Context(), ncUser, filePath)
|
|
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) Callback(w http.ResponseWriter, r *http.Request) {
|
|
ncUser := r.URL.Query().Get("user")
|
|
filePath := r.URL.Query().Get("path")
|
|
|
|
var payload struct {
|
|
Status int `json:"status"`
|
|
URL string `json:"url"`
|
|
Key string `json:"key"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
apiresponse.WriteJSON(w, http.StatusOK, map[string]int{"error": 1})
|
|
return
|
|
}
|
|
|
|
// status 2 = ready for saving, 6 = must force save
|
|
if payload.Status == 2 || payload.Status == 6 {
|
|
if payload.URL != "" {
|
|
resp, err := http.Get(payload.URL)
|
|
if err == nil && resp.StatusCode == http.StatusOK {
|
|
defer resp.Body.Close()
|
|
ct := resp.Header.Get("Content-Type")
|
|
_ = h.svc.SaveDocument(r.Context(), ncUser, filePath, resp.Body, ct)
|
|
}
|
|
}
|
|
}
|
|
apiresponse.WriteJSON(w, http.StatusOK, map[string]int{"error": 0})
|
|
}
|