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}