A community based topic aggregation platform built on atproto
1package post 2 3import ( 4 "Coves/internal/api/middleware" 5 "Coves/internal/core/posts" 6 "encoding/json" 7 "log" 8 "net/http" 9 "strings" 10) 11 12// CreateHandler handles post creation requests 13type CreateHandler struct { 14 service posts.Service 15} 16 17// NewCreateHandler creates a new create handler 18func NewCreateHandler(service posts.Service) *CreateHandler { 19 return &CreateHandler{ 20 service: service, 21 } 22} 23 24// HandleCreate handles POST /xrpc/social.coves.post.create 25// Creates a new post in a community's repository 26func (h *CreateHandler) HandleCreate(w http.ResponseWriter, r *http.Request) { 27 // 1. Check HTTP method 28 if r.Method != http.MethodPost { 29 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 30 return 31 } 32 33 // 2. Limit request body size to prevent DoS attacks 34 // 1MB allows for large content + embeds while preventing abuse 35 r.Body = http.MaxBytesReader(w, r.Body, 1*1024*1024) 36 37 // 3. Parse request body 38 var req posts.CreatePostRequest 39 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 40 // Check if error is due to body size limit 41 if err.Error() == "http: request body too large" { 42 writeError(w, http.StatusRequestEntityTooLarge, "RequestTooLarge", 43 "Request body too large (max 1MB)") 44 return 45 } 46 writeError(w, http.StatusBadRequest, "InvalidRequest", "Invalid request body") 47 return 48 } 49 50 // 4. Extract authenticated user DID from request context (injected by auth middleware) 51 userDID := middleware.GetUserDID(r) 52 if userDID == "" { 53 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") 54 return 55 } 56 57 // 5. Validate required fields 58 if req.Community == "" { 59 writeError(w, http.StatusBadRequest, "InvalidRequest", "community is required") 60 return 61 } 62 63 // 5b. Basic format validation for better UX (fail fast on obviously invalid input) 64 // Valid formats accepted by resolver: 65 // - DID: did:plc:xyz, did:web:example.com 66 // - Scoped handle: !name@instance 67 // - Canonical handle: name.community.instance 68 // - @-prefixed handle: @name.community.instance 69 // 70 // We only reject obviously invalid formats here (no prefix, no dots, no @ for !) 71 // The service layer (ResolveCommunityIdentifier) does comprehensive validation 72 73 // Scoped handles must include @ symbol 74 if strings.HasPrefix(req.Community, "!") && !strings.Contains(req.Community, "@") { 75 writeError(w, http.StatusBadRequest, "InvalidRequest", 76 "scoped handle must include @ symbol (!name@instance)") 77 return 78 } 79 80 // 6. SECURITY: Reject client-provided authorDid 81 // This prevents users from impersonating other users 82 if req.AuthorDID != "" { 83 writeError(w, http.StatusBadRequest, "InvalidRequest", 84 "authorDid must not be provided - derived from authenticated user") 85 return 86 } 87 88 // 7. Set author from authenticated user context 89 req.AuthorDID = userDID 90 91 // 8. Call service to create post (write-forward to PDS) 92 // Note: Service layer will resolve community at-identifier (handle or DID) to DID 93 response, err := h.service.CreatePost(r.Context(), req) 94 if err != nil { 95 handleServiceError(w, err) 96 return 97 } 98 99 // 9. Return success response matching lexicon output 100 w.Header().Set("Content-Type", "application/json") 101 w.WriteHeader(http.StatusOK) 102 if err := json.NewEncoder(w).Encode(response); err != nil { 103 // Log encoding errors but don't return error response (headers already sent) 104 log.Printf("Failed to encode post creation response: %v", err) 105 } 106}