···
4
+
"Coves/internal/api/middleware"
5
+
"Coves/internal/core/aggregators"
"Coves/internal/core/communities"
···
type postService struct {
17
-
communityService communities.Service
19
+
communityService communities.Service
20
+
aggregatorService aggregators.Service
// NewPostService creates a new post service
25
+
// aggregatorService can be nil if aggregator support is not needed (e.g., in tests or minimal setups)
communityService communities.Service,
29
+
aggregatorService aggregators.Service, // Optional: can be nil
29
-
communityService: communityService,
34
+
communityService: communityService,
35
+
aggregatorService: aggregatorService,
// CreatePost creates a new post in a community
37
-
// 2. Resolve community at-identifier (handle or DID) to DID
38
-
// 3. Fetch community from AppView
39
-
// 4. Ensure community has fresh PDS credentials
43
+
// 2. Check if author is an aggregator (server-side validation using DID from JWT)
44
+
// 3. If aggregator: validate authorization and rate limits, skip membership checks
45
+
// 4. If user: resolve community and perform membership/ban validation
// 6. Write to community's PDS repository
42
-
// 7. Return URI/CID (AppView indexes asynchronously via Jetstream)
48
+
// 7. If aggregator: record post for rate limiting
49
+
// 8. Return URI/CID (AppView indexes asynchronously via Jetstream)
func (s *postService) CreatePost(ctx context.Context, req CreatePostRequest) (*CreatePostResponse, error) {
44
-
// 1. Validate basic input
51
+
// 1. SECURITY: Extract authenticated DID from context (set by JWT middleware)
52
+
// Defense-in-depth: verify service layer receives correct DID even if handler is bypassed
53
+
authenticatedDID := middleware.GetAuthenticatedDID(ctx)
54
+
if authenticatedDID == "" {
55
+
return nil, fmt.Errorf("no authenticated DID in context - authentication required")
58
+
// SECURITY: Verify request DID matches authenticated DID from JWT
59
+
// This prevents DID spoofing where a malicious client or compromised handler
60
+
// could provide a different DID than what was authenticated
61
+
if authenticatedDID != req.AuthorDID {
62
+
log.Printf("[SECURITY] DID mismatch: authenticated=%s, request=%s", authenticatedDID, req.AuthorDID)
63
+
return nil, fmt.Errorf("authenticated DID does not match author DID")
66
+
// 2. Validate basic input
if err := s.validateCreateRequest(req); err != nil {
49
-
// 2. Resolve community at-identifier (handle or DID) to DID
71
+
// 3. SECURITY: Check if the authenticated DID is a registered aggregator
72
+
// This is server-side verification - we query the database to confirm
73
+
// the DID from the JWT corresponds to a registered aggregator service
74
+
// If aggregatorService is nil (tests or environments without aggregators), treat all posts as user posts
75
+
isAggregator := false
76
+
if s.aggregatorService != nil {
78
+
isAggregator, err = s.aggregatorService.IsAggregator(ctx, req.AuthorDID)
80
+
return nil, fmt.Errorf("failed to check if author is aggregator: %w", err)
84
+
// 4. Resolve community at-identifier (handle or DID) to DID
// This accepts both formats per atProto best practices:
// - Handles: !gardening.communities.coves.social
// - DIDs: did:plc:abc123 or did:web:coves.social
···
return nil, fmt.Errorf("failed to resolve community identifier: %w", err)
68
-
// 3. Fetch community from AppView (includes all metadata)
103
+
// 5. Fetch community from AppView (includes all metadata)
community, err := s.communityService.GetByDID(ctx, communityDID)
if communities.IsNotFound(err) {
···
return nil, fmt.Errorf("failed to fetch community: %w", err)
77
-
// 4. Check community visibility (Alpha: public/unlisted only)
78
-
// Beta will add membership checks for private communities
79
-
if community.Visibility == "private" {
80
-
return nil, ErrNotAuthorized
112
+
// 6. Apply validation based on actor type (aggregator vs user)
114
+
// AGGREGATOR VALIDATION FLOW
115
+
// Following Bluesky's pattern: feed generators and labelers are authorized services
116
+
log.Printf("[POST-CREATE] Aggregator detected: %s posting to community: %s", req.AuthorDID, communityDID)
118
+
// Check authorization exists and is enabled, and verify rate limits
119
+
if err := s.aggregatorService.ValidateAggregatorPost(ctx, req.AuthorDID, communityDID); err != nil {
120
+
if aggregators.IsUnauthorized(err) {
121
+
return nil, ErrNotAuthorized
123
+
if aggregators.IsRateLimited(err) {
124
+
return nil, ErrRateLimitExceeded
126
+
return nil, fmt.Errorf("aggregator validation failed: %w", err)
129
+
// Aggregators skip membership checks and visibility restrictions
130
+
// They are authorized services, not community members
132
+
// USER VALIDATION FLOW
133
+
// Check community visibility (Alpha: public/unlisted only)
134
+
// Beta will add membership checks for private communities
135
+
if community.Visibility == "private" {
136
+
return nil, ErrNotAuthorized
83
-
// 5. Ensure community has fresh PDS credentials (token refresh if needed)
140
+
// 7. Ensure community has fresh PDS credentials (token refresh if needed)
community, err = s.communityService.EnsureFreshToken(ctx, community)
return nil, fmt.Errorf("failed to refresh community credentials: %w", err)
89
-
// 6. Build post record for PDS
146
+
// 8. Build post record for PDS
postRecord := PostRecord{
Type: "social.coves.post.record",
···
CreatedAt: time.Now().UTC().Format(time.RFC3339),
105
-
// 7. Write to community's PDS repository
162
+
// 9. Write to community's PDS repository
uri, cid, err := s.createPostOnPDS(ctx, community, postRecord)
return nil, fmt.Errorf("failed to write post to PDS: %w", err)
111
-
// 8. Return response (AppView will index via Jetstream consumer)
112
-
log.Printf("[POST-CREATE] Author: %s, Community: %s, URI: %s", req.AuthorDID, communityDID, uri)
168
+
// 10. If aggregator, record post for rate limiting and statistics
169
+
if isAggregator && s.aggregatorService != nil {
170
+
if err := s.aggregatorService.RecordAggregatorPost(ctx, req.AuthorDID, communityDID, uri, cid); err != nil {
171
+
// Log error but don't fail the request (post was already created on PDS)
172
+
log.Printf("[POST-CREATE] Warning: failed to record aggregator post for rate limiting: %v", err)
176
+
// 11. Return response (AppView will index via Jetstream consumer)
177
+
log.Printf("[POST-CREATE] Author: %s (aggregator=%v), Community: %s, URI: %s",
178
+
req.AuthorDID, isAggregator, communityDID, uri)
return &CreatePostResponse{