Files
DocNest/backend/internal/handlers/document.go

343 lines
9.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
// Read binary data directly from request body
state, err := c.GetRawData()
if err != nil {
respondBadRequest(c, "Failed to read request body")
return
}
if len(state) == 0 {
respondBadRequest(c, "Empty state data")
return
}
if err := h.store.UpdateDocumentState(id, 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"})
}
// GetDocumentPermission returns the user's permission level for a document
func (h *DocumentHandler) GetDocumentPermission(c *gin.Context) {
documentID, err := uuid.Parse(c.Param("id"))
if err != nil {
respondBadRequest(c, "Invalid document ID format")
return
}
// 1. 先检查文档是否存在 (Good practice)
_, err = h.store.GetDocument(documentID)
if err != nil {
respondNotFound(c, "document")
return
}
userID := auth.GetUserFromContext(c)
shareToken := c.Query("share")
// 定义两个临时变量,用来存两边的结果
var userPerm string // 存 document_shares 的结果
var tokenPerm string // 存 share_token 的结果
// ====================================================
// 步骤 A: 检查个人权限 (Base Permission)
// ====================================================
if userID != nil {
perm, err := h.store.GetUserPermission(c.Request.Context(), documentID, *userID)
if err != nil {
respondInternalError(c, "Failed to get user permission", err)
return
}
userPerm = perm
// ⚠️ 注意:如果 perm 是空,这里不报错!继续往下走!
}
// ====================================================
// 步骤 B: 检查 Token 权限 (Upgrade Permission)
// ====================================================
if shareToken != "" {
// 先验证 Token 是否有效
valid, err := h.store.ValidateShareToken(c.Request.Context(), documentID, shareToken)
if err != nil {
respondInternalError(c, "Failed to validate token", err)
return
}
// 只有 Token 有效才去取权限
if valid {
p, err := h.store.GetShareLinkPermission(c.Request.Context(), documentID)
if err != nil {
respondInternalError(c, "Failed to get token permission", err)
return
}
tokenPerm = p
// 处理数据库老数据的 fallback
if tokenPerm == "" { tokenPerm = "view" }
}
}
// ====================================================
// 步骤 C: ⚡️ 权限合并与计算 (The Brain)
// ====================================================
finalPermission := ""
role := "viewer" // 默认角色
// 1. 如果是 Owner无敌直接返回
if userPerm == "owner" {
finalPermission = "edit"
role = "owner"
// 直接返回,不用看 Token 了
c.JSON(http.StatusOK, models.PermissionResponse{
Permission: finalPermission,
Role: role,
})
return
}
// 2. 比较 User 和 Token取最大值
// 逻辑:只要任意一边给了 "edit",那就是 "edit"
if userPerm == "edit" || tokenPerm == "edit" {
finalPermission = "edit"
role = "editor"
} else if userPerm == "view" || tokenPerm == "view" {
finalPermission = "view"
role = "viewer"
}
// ====================================================
// 步骤 D: 最终判决
// ====================================================
if finalPermission == "" {
// 既没个人权限Token 也不对(或者没 Token
if userID == nil {
respondUnauthorized(c, "Authentication required") // 没登录且没Token
} else {
respondForbidden(c, "You don't have permission") // 登录了但没权限
}
return
}
c.JSON(http.StatusOK, models.PermissionResponse{
Permission: finalPermission,
Role: role,
})
}