Files
DocNest/backend/internal/handlers/document.go
M1ngdaXie 8ae7fd96e8 feat: Implement error handling and response structure for API
- Added standardized error response structure in `errors.go` for consistent error handling across the API.
- Implemented specific response functions for various HTTP status codes (400, 401, 403, 404, 500) to enhance error reporting.
- Introduced validation error handling to provide detailed feedback on input validation issues.

test: Add comprehensive tests for share handler functionality

- Created a suite of tests for share handler endpoints, covering scenarios for creating, listing, deleting shares, and managing share links.
- Included tests for permission checks, validation errors, and edge cases such as unauthorized access and invalid document IDs.

chore: Set up test utilities and database for integration tests

- Established a base handler suite for common setup tasks in tests, including database initialization and teardown.
- Implemented test data seeding to facilitate consistent testing across different scenarios.

migration: Add public sharing support in the database schema

- Modified the `documents` table to include `share_token` and `is_public` columns for managing public document sharing.
- Added constraints to ensure data integrity, preventing public documents from lacking a share token.
2026-01-05 15:25:46 -08:00

230 lines
5.5 KiB
Go

package handlers
import (
"fmt"
"net/http"
"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 DocumentHandler struct {
store *store.PostgresStore
}
func NewDocumentHandler(s *store.PostgresStore) *DocumentHandler {
return &DocumentHandler{store: s}
}
// CreateDocument creates a new document (requires auth)
func (h *DocumentHandler) CreateDocument(c *gin.Context) {
userID := auth.GetUserFromContext(c)
if userID == nil {
respondUnauthorized(c, "Authentication required")
return
}
var req models.CreateDocumentRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondWithValidationError(c, err)
return
}
// Create document with owner_id
doc, err := h.store.CreateDocumentWithOwner(req.Name, req.Type, userID)
if err != nil {
respondInternalError(c, "Failed to create document", err)
return
}
c.JSON(http.StatusCreated, doc)
}
func (h *DocumentHandler) ListDocuments(c *gin.Context) {
userID := auth.GetUserFromContext(c)
fmt.Println("Getting userId, which is : ")
fmt.Println(userID)
if userID == nil {
respondUnauthorized(c, "Authentication required to list documents")
return
}
// Authenticated: show owned + shared documents
docs, err := h.store.ListUserDocuments(c.Request.Context(), *userID)
if err != nil {
respondInternalError(c, "Failed to list documents", err)
return
}
c.JSON(http.StatusOK, models.DocumentListResponse{
Documents: docs,
Total: len(docs),
})
}
func (h *DocumentHandler) GetDocument(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
respondBadRequest(c, "Invalid document ID format")
return
}
// First, check if document exists (404 takes precedence over 403)
doc, err := h.store.GetDocument(id)
if err != nil {
respondNotFound(c, "document")
return
}
userID := auth.GetUserFromContext(c)
// Check permission if authenticated
if userID != nil {
canView, err := h.store.CanViewDocument(c.Request.Context(), id, *userID)
if err != nil {
respondInternalError(c, "Failed to check permissions", err)
return
}
if !canView {
respondForbidden(c, "Access denied")
return
}
} else {
// Unauthenticated users can only access public documents
if !doc.Is_Public {
respondForbidden(c, "This document is not public. Please sign in to access.")
return
}
}
c.JSON(http.StatusOK, doc)
}
// GetDocumentState returns the Yjs state for a document
// GetDocumentState retrieves document state (requires view permission)
func (h *DocumentHandler) GetDocumentState(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
respondBadRequest(c, "Invalid document ID format")
return
}
userID := auth.GetUserFromContext(c)
// Check permission if authenticated
if userID != nil {
canView, err := h.store.CanViewDocument(c.Request.Context(), id, *userID)
if err != nil {
respondInternalError(c, "Failed to check permissions", err)
return
}
if !canView {
respondForbidden(c, "Access denied")
return
}
}
doc, err := h.store.GetDocument(id)
if err != nil {
respondNotFound(c, "document")
return
}
// Return empty byte slice if state is nil (new document)
state := doc.YjsState
if state == nil {
state = []byte{}
}
c.Data(http.StatusOK, "application/octet-stream", state)
}
// UpdateDocumentState updates document state (requires edit permission)
func (h *DocumentHandler) UpdateDocumentState(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
respondBadRequest(c, "Invalid document ID format")
return
}
userID := auth.GetUserFromContext(c)
if userID == nil {
respondUnauthorized(c, "Authentication required")
return
}
// First, check if document exists (404 takes precedence over 403)
_, err = h.store.GetDocument(id)
if err != nil {
respondNotFound(c, "document")
return
}
// Check edit permission
canEdit, err := h.store.CanEditDocument(c.Request.Context(), id, *userID)
if err != nil {
respondInternalError(c, "Failed to check permissions", err)
return
}
if !canEdit {
respondForbidden(c, "Edit access denied")
return
}
var req models.UpdateStateRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondWithValidationError(c, err)
return
}
if err := h.store.UpdateDocumentState(id, req.State); err != nil {
respondInternalError(c, "Failed to update state", err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "State updated successfully"})
}
// DeleteDocument deletes a document (owner only)
func (h *DocumentHandler) DeleteDocument(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
respondBadRequest(c, "Invalid document ID format")
return
}
userID := auth.GetUserFromContext(c)
if userID == nil {
respondUnauthorized(c, "Authentication required")
return
}
// First, check if document exists (404 takes precedence over 403)
_, err = h.store.GetDocument(id)
if err != nil {
respondNotFound(c, "document")
return
}
// Check ownership
isOwner, err := h.store.IsDocumentOwner(c.Request.Context(), id, *userID)
if err != nil {
respondInternalError(c, "Failed to check ownership", err)
return
}
if !isOwner {
respondForbidden(c, "Only document owner can delete documents")
return
}
if err := h.store.DeleteDocument(id); err != nil {
respondInternalError(c, "Failed to delete document", err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Document deleted successfully"})
}