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

feat: add document invite handler

+79
+1
cmd/server/main.go
··· 100 100 mux.HandleFunc("POST /api/docs/{rkey}/save", h.APIDocumentSave) 101 101 mux.HandleFunc("PUT /api/docs/{rkey}/autosave", h.APIDocumentAutoSave) 102 102 mux.HandleFunc("DELETE /api/docs/{rkey}", h.APIDocumentDelete) 103 + mux.HandleFunc("POST /api/docs/{rkey}/invite", h.DocumentInvite) 103 104 104 105 // Middleware stack 105 106 stack := middleware.Logger(
+78
internal/handler/handler.go
··· 6 6 "html/template" 7 7 "log" 8 8 "net/http" 9 + "os" 9 10 "regexp" 10 11 "strings" 11 12 "time" 12 13 13 14 "github.com/limeleaf/diffdown/internal/atproto/xrpc" 14 15 "github.com/limeleaf/diffdown/internal/auth" 16 + "github.com/limeleaf/diffdown/internal/collaboration" 15 17 "github.com/limeleaf/diffdown/internal/db" 16 18 "github.com/limeleaf/diffdown/internal/model" 17 19 "github.com/limeleaf/diffdown/internal/render" ··· 419 421 } 420 422 421 423 h.jsonResponse(w, map[string]string{"status": "ok"}) 424 + } 425 + 426 + // DocumentInvite creates an invite link for a document. 427 + func (h *Handler) DocumentInvite(w http.ResponseWriter, r *http.Request) { 428 + user := h.currentUser(r) 429 + if user == nil { 430 + http.Redirect(w, r, "/auth/login", http.StatusSeeOther) 431 + return 432 + } 433 + 434 + rkey := r.PathValue("rkey") 435 + if rkey == "" { 436 + http.Error(w, "Invalid document", http.StatusBadRequest) 437 + return 438 + } 439 + 440 + client, err := h.xrpcClient(user.ID) 441 + if err != nil { 442 + log.Printf("DocumentInvite: xrpc client: %v", err) 443 + h.render(w, "document_edit.html", PageData{Error: "Not authenticated with ATProto"}) 444 + return 445 + } 446 + 447 + value, _, err := client.GetRecord(client.DID(), collectionDocument, rkey) 448 + if err != nil { 449 + http.Error(w, "Document not found", http.StatusNotFound) 450 + return 451 + } 452 + 453 + doc := &model.Document{} 454 + if err := json.Unmarshal(value, doc); err != nil { 455 + http.Error(w, "Invalid document", http.StatusInternalServerError) 456 + return 457 + } 458 + doc.RKey = rkey 459 + 460 + session, err := h.DB.GetATProtoSession(user.ID) 461 + if err != nil || session == nil { 462 + http.Error(w, "Unauthorized", http.StatusForbidden) 463 + return 464 + } 465 + 466 + ownerDID := strings.TrimPrefix(doc.URI, "at://") 467 + if idx := strings.Index(ownerDID, "/"); idx > 0 { 468 + ownerDID = ownerDID[:idx] 469 + } 470 + if ownerDID == "" || session.DID != ownerDID { 471 + http.Error(w, "Unauthorized", http.StatusForbidden) 472 + return 473 + } 474 + 475 + if len(doc.Collaborators) >= 5 { 476 + http.Error(w, "Maximum collaborators reached", http.StatusBadRequest) 477 + return 478 + } 479 + 480 + invite, err := collaboration.CreateInvite(h.DB, rkey, session.DID) 481 + if err != nil { 482 + log.Printf("DocumentInvite: create invite: %v", err) 483 + http.Error(w, "Failed to create invite", http.StatusInternalServerError) 484 + return 485 + } 486 + 487 + baseURL := os.Getenv("BASE_URL") 488 + if baseURL == "" { 489 + baseURL = "http://127.0.0.1:8080" 490 + } 491 + inviteLink := fmt.Sprintf("%s/doc/%s?invite=%s", baseURL, rkey, invite.Token) 492 + h.render(w, "document_edit.html", PageData{ 493 + Title: "Edit " + doc.Title, 494 + User: user, 495 + Content: map[string]interface{}{ 496 + "document": doc, 497 + "inviteLink": inviteLink, 498 + }, 499 + }) 422 500 } 423 501 424 502 // --- API: Render markdown ---