feat: Add landing page and version history functionality
- Implemented ConditionalHome component to show LandingPage for guests and Home for authenticated users. - Created LandingPage with login options for Google and GitHub. - Added VersionHistoryPanel component for managing document versions. - Integrated version history functionality into EditorPage. - Updated API client to handle FormData correctly. - Added styles for LandingPage and VersionHistoryPanel. - Created version management API methods for creating, listing, restoring, and fetching document versions.
This commit is contained in:
258
backend/internal/handlers/version.go
Normal file
258
backend/internal/handlers/version.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/M1ngdaXie/realtime-collab/internal/auth"
|
||||
"github.com/M1ngdaXie/realtime-collab/internal/models"
|
||||
"github.com/M1ngdaXie/realtime-collab/internal/store"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type VersionHandler struct {
|
||||
store *store.PostgresStore
|
||||
}
|
||||
|
||||
func NewVersionHandler(s *store.PostgresStore) *VersionHandler {
|
||||
return &VersionHandler{store: s}
|
||||
}
|
||||
|
||||
// CreateVersion creates a manual snapshot (requires edit permission)
|
||||
func (h *VersionHandler) CreateVersion(c *gin.Context) {
|
||||
userID := auth.GetUserFromContext(c)
|
||||
if userID == nil {
|
||||
respondUnauthorized(c, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
documentID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
respondBadRequest(c, "Invalid document ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Check edit permission (only editors can create versions)
|
||||
canEdit, err := h.store.CanEditDocument(c.Request.Context(), documentID, *userID)
|
||||
if err != nil {
|
||||
respondInternalError(c, "Failed to check permissions", err)
|
||||
return
|
||||
}
|
||||
if !canEdit {
|
||||
respondForbidden(c, "Edit permission required to create versions")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse multipart form data
|
||||
if err := c.Request.ParseMultipartForm(10 << 20); err != nil { // 10MB limit
|
||||
respondBadRequest(c, "Invalid multipart form")
|
||||
return
|
||||
}
|
||||
|
||||
// Get version label (optional)
|
||||
versionLabel := c.PostForm("version_label")
|
||||
var labelPtr *string
|
||||
if versionLabel != "" {
|
||||
labelPtr = &versionLabel
|
||||
}
|
||||
|
||||
// Get text preview (required)
|
||||
textPreview := c.PostForm("text_preview")
|
||||
if textPreview == "" {
|
||||
respondBadRequest(c, "text_preview is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Get Yjs snapshot binary (required)
|
||||
file, _, err := c.Request.FormFile("yjs_snapshot")
|
||||
if err != nil {
|
||||
respondBadRequest(c, "yjs_snapshot file is required")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
snapshotData, err := io.ReadAll(file)
|
||||
if err != nil || len(snapshotData) == 0 {
|
||||
respondBadRequest(c, "Failed to read snapshot data")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate snapshot size (max 10MB)
|
||||
if len(snapshotData) > 10*1024*1024 {
|
||||
respondBadRequest(c, "Snapshot too large (max 10MB)")
|
||||
return
|
||||
}
|
||||
|
||||
// Create version (manual snapshot)
|
||||
version, err := h.store.CreateDocumentVersion(
|
||||
c.Request.Context(),
|
||||
documentID,
|
||||
*userID,
|
||||
snapshotData,
|
||||
&textPreview,
|
||||
labelPtr,
|
||||
false, // is_auto_generated = false
|
||||
)
|
||||
if err != nil {
|
||||
respondInternalError(c, "Failed to create version", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, version)
|
||||
}
|
||||
|
||||
// ListVersions returns paginated version history (requires view permission)
|
||||
func (h *VersionHandler) ListVersions(c *gin.Context) {
|
||||
userID := auth.GetUserFromContext(c)
|
||||
if userID == nil {
|
||||
respondUnauthorized(c, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
documentID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
respondBadRequest(c, "Invalid document ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Check view permission
|
||||
canView, err := h.store.CanViewDocument(c.Request.Context(), documentID, *userID)
|
||||
if err != nil {
|
||||
respondInternalError(c, "Failed to check permissions", err)
|
||||
return
|
||||
}
|
||||
if !canView {
|
||||
respondForbidden(c, "View permission required")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse pagination params
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
|
||||
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
||||
|
||||
if limit > 100 {
|
||||
limit = 100 // Max limit
|
||||
}
|
||||
|
||||
versions, total, err := h.store.ListDocumentVersions(c.Request.Context(), documentID, limit, offset)
|
||||
if err != nil {
|
||||
respondInternalError(c, "Failed to list versions", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.VersionListResponse{
|
||||
Versions: versions,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
// GetVersionSnapshot returns the Yjs binary snapshot for a specific version
|
||||
func (h *VersionHandler) GetVersionSnapshot(c *gin.Context) {
|
||||
userID := auth.GetUserFromContext(c)
|
||||
if userID == nil {
|
||||
respondUnauthorized(c, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
versionID, err := uuid.Parse(c.Param("versionId"))
|
||||
if err != nil {
|
||||
respondBadRequest(c, "Invalid version ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Get version
|
||||
version, err := h.store.GetDocumentVersion(c.Request.Context(), versionID)
|
||||
if err != nil {
|
||||
respondNotFound(c, "version")
|
||||
return
|
||||
}
|
||||
|
||||
// Check permission on parent document
|
||||
canView, err := h.store.CanViewDocument(c.Request.Context(), version.DocumentID, *userID)
|
||||
if err != nil {
|
||||
respondInternalError(c, "Failed to check permissions", err)
|
||||
return
|
||||
}
|
||||
if !canView {
|
||||
respondForbidden(c, "View permission required")
|
||||
return
|
||||
}
|
||||
|
||||
// Return binary snapshot
|
||||
c.Data(http.StatusOK, "application/octet-stream", version.YjsSnapshot)
|
||||
}
|
||||
|
||||
// RestoreVersion creates a new version from an old snapshot (non-destructive)
|
||||
func (h *VersionHandler) RestoreVersion(c *gin.Context) {
|
||||
userID := auth.GetUserFromContext(c)
|
||||
if userID == nil {
|
||||
respondUnauthorized(c, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
documentID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
respondBadRequest(c, "Invalid document ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Check edit permission
|
||||
canEdit, err := h.store.CanEditDocument(c.Request.Context(), documentID, *userID)
|
||||
if err != nil {
|
||||
respondInternalError(c, "Failed to check permissions", err)
|
||||
return
|
||||
}
|
||||
if !canEdit {
|
||||
respondForbidden(c, "Edit permission required to restore versions")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.RestoreVersionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
respondWithValidationError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the version to restore
|
||||
oldVersion, err := h.store.GetDocumentVersion(c.Request.Context(), req.VersionID)
|
||||
if err != nil {
|
||||
respondNotFound(c, "version")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify version belongs to this document
|
||||
if oldVersion.DocumentID != documentID {
|
||||
respondBadRequest(c, "Version does not belong to this document")
|
||||
return
|
||||
}
|
||||
|
||||
// Update current document state with old snapshot
|
||||
if err := h.store.UpdateDocumentState(documentID, oldVersion.YjsSnapshot); err != nil {
|
||||
respondInternalError(c, "Failed to restore document state", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create new version entry marking it as a restore
|
||||
restoreLabel := fmt.Sprintf("Restored from version %d", oldVersion.VersionNumber)
|
||||
newVersion, err := h.store.CreateDocumentVersion(
|
||||
c.Request.Context(),
|
||||
documentID,
|
||||
*userID,
|
||||
oldVersion.YjsSnapshot,
|
||||
oldVersion.TextPreview,
|
||||
&restoreLabel,
|
||||
false, // Manual restore
|
||||
)
|
||||
if err != nil {
|
||||
respondInternalError(c, "Failed to create restore version", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Version restored successfully",
|
||||
"new_version": newVersion,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user