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

feat: add accept invite handler

+88
+1
cmd/server/main.go
··· 105 105 mux.HandleFunc("PUT /api/docs/{rkey}/autosave", h.APIDocumentAutoSave) 106 106 mux.HandleFunc("DELETE /api/docs/{rkey}", h.APIDocumentDelete) 107 107 mux.HandleFunc("POST /api/docs/{rkey}/invite", h.DocumentInvite) 108 + mux.HandleFunc("GET /docs/{rkey}/accept", h.AcceptInvite) 108 109 109 110 // Middleware stack 110 111 stack := middleware.Logger(
+22
internal/atproto/xrpc/client.go
··· 291 291 } 292 292 return nil 293 293 } 294 + 295 + const collectionDocument = "com.diffdown.document" 296 + 297 + // GetDocument fetches a document by its rkey. 298 + func (c *Client) GetDocument(rkey string) (*model.Document, error) { 299 + value, _, err := c.GetRecord(c.session.DID, collectionDocument, rkey) 300 + if err != nil { 301 + return nil, err 302 + } 303 + 304 + var doc model.Document 305 + if err := json.Unmarshal(value, &doc); err != nil { 306 + return nil, fmt.Errorf("unmarshal document: %w", err) 307 + } 308 + doc.RKey = rkey 309 + return &doc, nil 310 + } 311 + 312 + // PutDocument creates or updates a document. 313 + func (c *Client) PutDocument(rkey string, doc *model.Document) (string, string, error) { 314 + return c.PutRecord(collectionDocument, rkey, doc) 315 + }
+5
internal/db/db.go
··· 175 175 } 176 176 return &invite, nil 177 177 } 178 + 179 + func (db *DB) DeleteInvite(token string) error { 180 + _, err := db.Exec(`DELETE FROM invites WHERE token = ?`, token) 181 + return err 182 + }
+60
internal/handler/handler.go
··· 490 490 h.jsonResponse(w, map[string]string{"inviteLink": inviteLink}, http.StatusOK) 491 491 } 492 492 493 + // AcceptInvite handles an invite acceptance. 494 + func (h *Handler) AcceptInvite(w http.ResponseWriter, r *http.Request) { 495 + user := h.currentUser(r) 496 + if user == nil { 497 + http.Redirect(w, r, "/auth/login", http.StatusSeeOther) 498 + return 499 + } 500 + 501 + rKey := r.PathValue("rkey") 502 + inviteToken := r.URL.Query().Get("invite") 503 + if inviteToken == "" { 504 + http.Error(w, "Invalid invite", http.StatusBadRequest) 505 + return 506 + } 507 + 508 + invite, err := collaboration.ValidateInvite(h.DB, inviteToken, rKey) 509 + if err != nil { 510 + http.Error(w, err.Error(), http.StatusBadRequest) 511 + return 512 + } 513 + 514 + session, err := h.DB.GetATProtoSession(user.ID) 515 + if err != nil || session == nil { 516 + http.Redirect(w, r, "/auth/atproto", http.StatusSeeOther) 517 + return 518 + } 519 + 520 + client, err := h.xrpcClient(user.ID) 521 + if err != nil { 522 + log.Printf("AcceptInvite: xrpc client: %v", err) 523 + http.Error(w, "Failed to connect to ATProto", http.StatusInternalServerError) 524 + return 525 + } 526 + 527 + doc, err := client.GetDocument(rKey) 528 + if err != nil { 529 + http.Error(w, "Document not found", http.StatusNotFound) 530 + return 531 + } 532 + 533 + for _, c := range doc.Collaborators { 534 + if c == session.DID { 535 + http.Redirect(w, r, "/docs/"+rKey, http.StatusSeeOther) 536 + return 537 + } 538 + } 539 + 540 + doc.Collaborators = append(doc.Collaborators, session.DID) 541 + _, _, err = client.PutDocument(rKey, doc) 542 + if err != nil { 543 + log.Printf("AcceptInvite: add collaborator: %v", err) 544 + http.Error(w, "Failed to add collaborator", http.StatusInternalServerError) 545 + return 546 + } 547 + 548 + h.DB.DeleteInvite(invite.Token) 549 + 550 + http.Redirect(w, r, "/docs/"+rKey, http.StatusSeeOther) 551 + } 552 + 493 553 // --- API: Render markdown --- 494 554 495 555 func (h *Handler) APIRender(w http.ResponseWriter, r *http.Request) {