···
4
+
"Coves/internal/api/middleware"
5
+
"Coves/internal/core/posts"
12
+
// CreateHandler handles post creation requests
13
+
type CreateHandler struct {
14
+
service posts.Service
17
+
// NewCreateHandler creates a new create handler
18
+
func NewCreateHandler(service posts.Service) *CreateHandler {
19
+
return &CreateHandler{
24
+
// HandleCreate handles POST /xrpc/social.coves.post.create
25
+
// Creates a new post in a community's repository
26
+
func (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)
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)
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)")
46
+
writeError(w, http.StatusBadRequest, "InvalidRequest", "Invalid request body")
50
+
// 4. Extract authenticated user DID from request context (injected by auth middleware)
51
+
userDID := middleware.GetUserDID(r)
53
+
writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required")
57
+
// 5. Validate required fields
58
+
if req.Community == "" {
59
+
writeError(w, http.StatusBadRequest, "InvalidRequest", "community is required")
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
70
+
// We only reject obviously invalid formats here (no prefix, no dots, no @ for !)
71
+
// The service layer (ResolveCommunityIdentifier) does comprehensive validation
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)")
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")
88
+
// 7. Set author from authenticated user context
89
+
req.AuthorDID = userDID
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)
95
+
handleServiceError(w, err)
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)