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