fix: allow share-link users to view/edit and autosave via share token
Users without personal document access can still load/save state when a valid share token is present, matching the permission level granted by the share link. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7b5558bc94
commit
aa67446f7c
@@ -137,6 +137,15 @@ func (h *DocumentHandler) GetDocumentState(c *gin.Context) {
|
||||
respondInternalError(c, "Failed to check permissions", err)
|
||||
return
|
||||
}
|
||||
if !canView && shareToken != "" {
|
||||
// Logged-in user without personal permission: fall back to share link
|
||||
valid, err := h.store.ValidateShareToken(c.Request.Context(), id, shareToken)
|
||||
if err != nil {
|
||||
respondInternalError(c, "Failed to validate share token", err)
|
||||
return
|
||||
}
|
||||
canView = valid
|
||||
}
|
||||
if !canView {
|
||||
respondForbidden(c, "Access denied")
|
||||
return
|
||||
@@ -189,12 +198,28 @@ func (h *DocumentHandler) UpdateDocumentState(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check edit permission
|
||||
// Check edit permission (personal share OR edit-level share link)
|
||||
shareToken := c.Query("share")
|
||||
canEdit, err := h.store.CanEditDocument(c.Request.Context(), id, *userID)
|
||||
if err != nil {
|
||||
respondInternalError(c, "Failed to check permissions", err)
|
||||
return
|
||||
}
|
||||
if !canEdit && shareToken != "" {
|
||||
valid, err := h.store.ValidateShareToken(c.Request.Context(), id, shareToken)
|
||||
if err != nil {
|
||||
respondInternalError(c, "Failed to validate share token", err)
|
||||
return
|
||||
}
|
||||
if valid {
|
||||
perm, err := h.store.GetShareLinkPermission(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
respondInternalError(c, "Failed to get token permission", err)
|
||||
return
|
||||
}
|
||||
canEdit = perm == "edit"
|
||||
}
|
||||
}
|
||||
if !canEdit {
|
||||
respondForbidden(c, "Edit access denied")
|
||||
return
|
||||
|
||||
@@ -381,8 +381,6 @@ func (s *DocumentHandlerSuite) TestGetDocumentState_Success() {
|
||||
s.assertSuccessResponse(w, http.StatusOK)
|
||||
|
||||
s.Equal("application/octet-stream", w.Header().Get("Content-Type"))
|
||||
// State should be empty bytes for new document
|
||||
s.NotNil(w.Body.Bytes())
|
||||
}
|
||||
|
||||
func (s *DocumentHandlerSuite) TestGetDocumentState_EmptyState() {
|
||||
@@ -416,6 +414,17 @@ func (s *DocumentHandlerSuite) TestGetDocumentState_InvalidID() {
|
||||
s.assertErrorResponse(w, http.StatusBadRequest, "bad_request", "Invalid document ID")
|
||||
}
|
||||
|
||||
func (s *DocumentHandlerSuite) TestGetDocumentState_AuthenticatedWithShareToken() {
|
||||
// Charlie (logged in, not owner/shared) reads Alice's public doc via share link
|
||||
path := fmt.Sprintf("/api/documents/%s/state?share=%s", s.testData.AlicePublicDoc, s.testData.PublicShareToken)
|
||||
w, httpReq, err := s.makeAuthRequest("GET", path, nil, s.testData.CharlieID)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(w, httpReq)
|
||||
s.assertSuccessResponse(w, http.StatusOK)
|
||||
s.Equal("application/octet-stream", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// UpdateDocumentState Tests
|
||||
// ========================================
|
||||
@@ -458,6 +467,32 @@ func (s *DocumentHandlerSuite) TestUpdateDocumentState_ViewOnlyDenied() {
|
||||
s.router.ServeHTTP(w, httpReq)
|
||||
s.assertErrorResponse(w, http.StatusForbidden, "forbidden", "Edit access denied")
|
||||
}
|
||||
func (s *DocumentHandlerSuite) TestUpdateDocumentState_AuthenticatedWithEditShareToken() {
|
||||
// Give Alice's public doc an "edit" share link
|
||||
ctx := context.Background()
|
||||
editToken, err := s.store.GenerateShareToken(ctx, s.testData.AlicePublicDoc, "edit")
|
||||
s.Require().NoError(err)
|
||||
|
||||
// Charlie (logged in, not owner/shared) edits via edit share link
|
||||
req := models.UpdateStateRequest{State: []byte("shared edit")}
|
||||
path := fmt.Sprintf("/api/documents/%s/state?share=%s", s.testData.AlicePublicDoc, editToken)
|
||||
w, httpReq, err := s.makeAuthRequest("PUT", path, req, s.testData.CharlieID)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(w, httpReq)
|
||||
s.assertSuccessResponse(w, http.StatusOK)
|
||||
}
|
||||
func (s *DocumentHandlerSuite) TestUpdateDocumentState_AuthenticatedWithViewShareTokenDenied() {
|
||||
// Alice's public doc has a "view" share link (from seed).
|
||||
// Charlie (logged in) cannot write via a view-only share link.
|
||||
req := models.UpdateStateRequest{State: []byte("attempt write")}
|
||||
path := fmt.Sprintf("/api/documents/%s/state?share=%s", s.testData.AlicePublicDoc, s.testData.PublicShareToken)
|
||||
w, httpReq, err := s.makeAuthRequest("PUT", path, req, s.testData.CharlieID)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(w, httpReq)
|
||||
s.assertErrorResponse(w, http.StatusForbidden, "forbidden", "Edit access denied")
|
||||
}
|
||||
|
||||
func (s *DocumentHandlerSuite) TestUpdateDocumentState_Unauthorized() {
|
||||
req := models.UpdateStateRequest{
|
||||
|
||||
Reference in New Issue
Block a user