Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com

fix: route collaborator saves to owner's PDS using ownerDID from request

+37 -9
+34 -7
internal/handler/handler.go
··· 56 56 IsOwner bool 57 57 // IsCollaborator is true when the current user is in the collaborators list. 58 58 IsCollaborator bool 59 + // OwnerDID is the document owner's ATProto DID. Empty when IsOwner is true. 60 + // Used by collaborators to route save requests to the owner's PDS. 61 + OwnerDID string 59 62 } 60 63 61 64 func (h *Handler) currentUser(r *http.Request) *model.User { ··· 376 379 doc.RKey = rkey 377 380 378 381 editData := &DocumentEditData{Document: doc, IsOwner: isOwner} 382 + if !isOwner { 383 + editData.OwnerDID = ownerDID 384 + } 379 385 if session, err := h.DB.GetATProtoSession(user.ID); err == nil && session != nil { 380 386 editData.AccessToken = session.AccessToken 381 387 userDID := session.DID ··· 442 448 443 449 rkey := r.PathValue("rkey") 444 450 var req struct { 445 - Content string `json:"content"` 446 - Title string `json:"title"` 451 + Content string `json:"content"` 452 + Title string `json:"title"` 453 + OwnerDID string `json:"ownerDID"` // non-empty when saving on behalf of another user (collaborator) 447 454 } 448 455 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 449 456 http.Error(w, "Bad request", 400) 450 457 return 451 458 } 452 459 453 - client, err := h.xrpcClient(user.ID) 454 - if err != nil { 455 - http.Error(w, "Could not connect to PDS", 500) 456 - return 460 + // For collaborators, save to the document owner's PDS, not the collaborator's. 461 + var client *xrpc.Client 462 + var repoDID string 463 + if req.OwnerDID != "" { 464 + ownerUser, err := h.DB.GetUserByDID(req.OwnerDID) 465 + if err != nil { 466 + log.Printf("APIDocumentSave: get owner by DID %s: %v", req.OwnerDID, err) 467 + http.Error(w, "Document owner not found", 404) 468 + return 469 + } 470 + client, err = h.xrpcClient(ownerUser.ID) 471 + if err != nil { 472 + http.Error(w, "Could not connect to owner PDS", 500) 473 + return 474 + } 475 + repoDID = req.OwnerDID 476 + } else { 477 + var err error 478 + client, err = h.xrpcClient(user.ID) 479 + if err != nil { 480 + http.Error(w, "Could not connect to PDS", 500) 481 + return 482 + } 483 + repoDID = client.DID() 457 484 } 458 485 459 486 // Fetch existing record to preserve fields 460 - value, _, err := client.GetRecord(client.DID(), collectionDocument, rkey) 487 + value, _, err := client.GetRecord(repoDID, collectionDocument, rkey) 461 488 if err != nil { 462 489 http.Error(w, "Document not found", 404) 463 490 return
+3 -2
templates/document_edit.html
··· 108 108 const rkey = '{{.Content.RKey}}'; 109 109 const accessToken = '{{.Content.AccessToken}}'; 110 110 const isCollaborator = {{if .Content.IsCollaborator}}true{{else}}false{{end}}; 111 + const ownerDID = '{{.Content.OwnerDID}}'; // empty string when current user is owner 111 112 112 113 const STORAGE_KEY = 'editor-mode'; 113 114 let currentMode = localStorage.getItem(STORAGE_KEY) || 'rich'; // 'rich' | 'source' ··· 135 136 await fetch(`/api/docs/${rkey}/autosave`, { 136 137 method: 'PUT', 137 138 headers: {'Content-Type': 'application/json'}, 138 - body: JSON.stringify({content, title: titleInput.value}), 139 + body: JSON.stringify({content, title: titleInput.value, ownerDID}), 139 140 }); 140 141 saveStatus.textContent = 'Auto-saved'; 141 142 saveStatus.className = 'status-saved'; ··· 331 332 const resp = await fetch(`/api/docs/${rkey}/save`, { 332 333 method: 'POST', 333 334 headers: {'Content-Type': 'application/json'}, 334 - body: JSON.stringify({content, title: titleInput.value}), 335 + body: JSON.stringify({content, title: titleInput.value, ownerDID}), 335 336 }); 336 337 if (resp.ok) { 337 338 saveStatus.textContent = 'Saved!';