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

fix: add collaborator edit route and fix edit link for shared documents

+51 -16
+1
cmd/server/main.go
··· 103 103 mux.HandleFunc("GET /docs/{rkey}/edit", h.DocumentEdit) 104 104 // Collaborator-scoped routes (owner DID in path) 105 105 mux.HandleFunc("GET /docs/{did}/{rkey}", h.CollaboratorDocumentView) 106 + mux.HandleFunc("GET /docs/{did}/{rkey}/edit", h.CollaboratorDocumentEdit) 106 107 107 108 // API 108 109 mux.HandleFunc("POST /api/render", h.APIRender)
+46 -16
internal/handler/handler.go
··· 299 299 type DocumentViewData struct { 300 300 Doc *model.Document 301 301 Rendered template.HTML 302 + OwnerDID string // non-empty when viewing a collaborator's document 302 303 } 303 304 h.render(w, "document_view.html", PageData{ 304 305 Title: doc.Title, 305 306 User: user, 306 - Content: DocumentViewData{Doc: doc, Rendered: template.HTML(rendered)}, 307 + Content: DocumentViewData{Doc: doc, Rendered: template.HTML(rendered), OwnerDID: ownerDID}, 307 308 }) 308 309 } 309 310 ··· 345 346 h.documentView(w, r, ownerUser.ID, ownerDID, rkey) 346 347 } 347 348 348 - // DocumentEdit renders the editor for a document. 349 - func (h *Handler) DocumentEdit(w http.ResponseWriter, r *http.Request) { 350 - user := h.currentUser(r) 351 - if user == nil { 352 - http.Redirect(w, r, "/auth/atproto", http.StatusSeeOther) 353 - return 354 - } 355 - 356 - rkey := r.PathValue("rkey") 357 - client, err := h.xrpcClient(user.ID) 349 + // documentEdit is the shared implementation for the edit page. 350 + // ownerUserID/ownerDID identify whose PDS holds the document; isOwner is true for the creator. 351 + func (h *Handler) documentEdit(w http.ResponseWriter, r *http.Request, user *model.User, ownerUserID, ownerDID, rkey string, isOwner bool) { 352 + ownerClient, err := h.xrpcClient(ownerUserID) 358 353 if err != nil { 359 354 http.Error(w, "Could not connect to PDS", 500) 360 355 return 361 356 } 362 357 363 - value, _, err := client.GetRecord(client.DID(), collectionDocument, rkey) 358 + value, _, err := ownerClient.GetRecord(ownerDID, collectionDocument, rkey) 364 359 if err != nil { 365 360 http.NotFound(w, r) 366 361 return ··· 373 368 } 374 369 doc.RKey = rkey 375 370 376 - // Fetch ATProto session to pass access token to the WebSocket client. 377 - // The user is always the owner here since we fetched the doc via client.DID(). 378 - editData := &DocumentEditData{Document: doc, IsOwner: true} 371 + editData := &DocumentEditData{Document: doc, IsOwner: isOwner} 379 372 if session, err := h.DB.GetATProtoSession(user.ID); err == nil && session != nil { 380 373 editData.AccessToken = session.AccessToken 381 - // Check if this user is listed as a collaborator on the doc. 382 374 userDID := session.DID 383 375 for _, did := range doc.Collaborators { 384 376 if did == userDID { ··· 393 385 User: user, 394 386 Content: editData, 395 387 }) 388 + } 389 + 390 + // DocumentEdit renders the editor for a document (owner). 391 + func (h *Handler) DocumentEdit(w http.ResponseWriter, r *http.Request) { 392 + user := h.currentUser(r) 393 + if user == nil { 394 + http.Redirect(w, r, "/auth/atproto", http.StatusSeeOther) 395 + return 396 + } 397 + 398 + rkey := r.PathValue("rkey") 399 + client, err := h.xrpcClient(user.ID) 400 + if err != nil { 401 + http.Error(w, "Could not connect to PDS", 500) 402 + return 403 + } 404 + 405 + h.documentEdit(w, r, user, user.ID, client.DID(), rkey, true) 406 + } 407 + 408 + // CollaboratorDocumentEdit renders the editor for a document owned by another user. 409 + func (h *Handler) CollaboratorDocumentEdit(w http.ResponseWriter, r *http.Request) { 410 + user := h.currentUser(r) 411 + if user == nil { 412 + http.Redirect(w, r, "/auth/atproto", http.StatusSeeOther) 413 + return 414 + } 415 + 416 + ownerDID := r.PathValue("did") 417 + rkey := r.PathValue("rkey") 418 + 419 + ownerUser, err := h.DB.GetUserByDID(ownerDID) 420 + if err != nil { 421 + http.NotFound(w, r) 422 + return 423 + } 424 + 425 + h.documentEdit(w, r, user, ownerUser.ID, ownerDID, rkey, false) 396 426 } 397 427 398 428 // APIDocumentSave saves a document to the PDS.
+4
templates/document_view.html
··· 13 13 {{else if .Doc.CreatedAt}}<time>Created {{.Doc.CreatedAt | fmtdate}}</time>{{end}} 14 14 </div> 15 15 <div class="file-actions"> 16 + {{if .OwnerDID}} 17 + <a href="/docs/{{.OwnerDID}}/{{.Doc.RKey}}/edit" class="btn btn-sm">Edit</a> 18 + {{else}} 16 19 <a href="/docs/{{.Doc.RKey}}/edit" class="btn btn-sm">Edit</a> 17 20 <button class="btn btn-sm btn-outline btn-danger" onclick="deleteDocument('{{.Doc.RKey}}')">Delete</button> 21 + {{end}} 18 22 </div> 19 23 </div> 20 24 <div class="markdown-body">