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}