diff --git a/go-backend/cmd/api/main.go b/go-backend/cmd/api/main.go index c31ce98..942bcfe 100644 --- a/go-backend/cmd/api/main.go +++ b/go-backend/cmd/api/main.go @@ -201,7 +201,7 @@ func main() { users.DELETE("/:id/reject", userHandler.RejectFollowRequest) users.GET("/requests", userHandler.GetPendingFollowRequests) users.GET("/:id/posts", postHandler.GetProfilePosts) - users.GET("/me/saved", userHandler.GetSavedPosts) + users.GET("/:id/saved", userHandler.GetSavedPosts) users.GET("/me/liked", userHandler.GetLikedPosts) } diff --git a/go-backend/internal/handlers/post_handler.go b/go-backend/internal/handlers/post_handler.go index c144253..652f09e 100644 --- a/go-backend/internal/handlers/post_handler.go +++ b/go-backend/internal/handlers/post_handler.go @@ -279,13 +279,14 @@ func (h *PostHandler) GetProfilePosts(c *gin.Context) { limit := utils.GetQueryInt(c, "limit", 20) offset := utils.GetQueryInt(c, "offset", 0) + onlyChains := c.Query("chained") == "true" viewerID := "" if val, exists := c.Get("user_id"); exists { viewerID = val.(string) } - posts, err := h.postRepo.GetPostsByAuthor(c.Request.Context(), authorID, viewerID, limit, offset) + posts, err := h.postRepo.GetPostsByAuthor(c.Request.Context(), authorID, viewerID, limit, offset, onlyChains) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch profile posts", "details": err.Error()}) return diff --git a/go-backend/internal/handlers/user_handler.go b/go-backend/internal/handlers/user_handler.go index 6161c1f..e4de96c 100644 --- a/go-backend/internal/handlers/user_handler.go +++ b/go-backend/internal/handlers/user_handler.go @@ -10,6 +10,7 @@ import ( "github.com/patbritton/sojorn-backend/internal/models" "github.com/patbritton/sojorn-backend/internal/repository" "github.com/patbritton/sojorn-backend/internal/services" + "github.com/patbritton/sojorn-backend/pkg/utils" "github.com/rs/zerolog/log" ) @@ -231,23 +232,19 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) { } func (h *UserHandler) GetSavedPosts(c *gin.Context) { - userID := c.GetString("user_id") // Authenticated user + currentUserID := c.GetString("user_id") // Authenticated user + targetID := c.Param("id") - // Optional: allow viewing other's saved posts if public? For now assume "me" context from route usually, - // but if route is /users/:id/saved we would use param. - // The prompt asked for GET /users/me/saved, which implies authenticated user. + if targetID == "" || targetID == "me" { + targetID = currentUserID + } - // Check if a specific ID is requested in URL, otherwise use authenticated user - // However, usually saved posts are private. Let's assume me-only for now or strictly follow ID if provided. - // The prompt says GET /api/v1/users/me/saved + // TODO: Add privacy check here if viewing another user's saved posts - // If the route is /users/me/saved, we rely on the middleware setting user_id. + limit := utils.GetQueryInt(c, "limit", 20) + offset := utils.GetQueryInt(c, "offset", 0) - limit := 20 - offset := 0 - // simplified for brevity, in real app parse query params - - posts, err := h.postRepo.GetSavedPosts(c.Request.Context(), userID, limit, offset) + posts, err := h.postRepo.GetSavedPosts(c.Request.Context(), targetID, limit, offset) if err != nil { log.Error().Err(err).Msg("Failed to fetch saved posts") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch saved posts"}) diff --git a/go-backend/internal/middleware/cors.go b/go-backend/internal/middleware/cors.go index 0111df2..8606230 100644 --- a/go-backend/internal/middleware/cors.go +++ b/go-backend/internal/middleware/cors.go @@ -17,7 +17,7 @@ func CORS() gin.HandlerFunc { } c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") - c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With, X-Signature, X-Timestamp, X-Request-ID, X-Rate-Limit-Remaining, X-Rate-Limit-Reset") c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH, DELETE") if c.Request.Method == "OPTIONS" { diff --git a/go-backend/internal/repository/post_repository.go b/go-backend/internal/repository/post_repository.go index 4aff3ab..cf80cff 100644 --- a/go-backend/internal/repository/post_repository.go +++ b/go-backend/internal/repository/post_repository.go @@ -137,7 +137,9 @@ func (r *PostRepository) GetFeed(ctx context.Context, userID string, categorySlu pr.handle as author_handle, pr.display_name as author_display_name, COALESCE(pr.avatar_url, '') as author_avatar_url, COALESCE(m.like_count, 0) as like_count, COALESCE(m.comment_count, 0) as comment_count, CASE WHEN ($4::text) != '' THEN EXISTS(SELECT 1 FROM public.post_likes WHERE post_id = p.id AND user_id = $4::text::uuid) ELSE FALSE END as is_liked, - p.allow_chain, p.visibility + p.allow_chain, p.visibility, + COALESCE((SELECT jsonb_object_agg(emoji, count) FROM (SELECT emoji, COUNT(*) as count FROM public.post_reactions WHERE post_id = p.id GROUP BY emoji) r), '{}'::jsonb) as reaction_counts, + CASE WHEN ($4::text) != '' THEN COALESCE((SELECT jsonb_agg(emoji) FROM public.post_reactions WHERE post_id = p.id AND user_id = $4::text::uuid), '[]'::jsonb) ELSE '[]'::jsonb END as my_reactions FROM public.posts p JOIN public.profiles pr ON p.author_id = pr.id LEFT JOIN public.post_metrics m ON p.id = m.post_id @@ -169,7 +171,7 @@ func (r *PostRepository) GetFeed(ctx context.Context, userID string, categorySlu &p.ID, &p.AuthorID, &p.CategoryID, &p.Body, &p.ImageURL, &p.VideoURL, &p.ThumbnailURL, &p.DurationMS, &p.Tags, &p.CreatedAt, &p.AuthorHandle, &p.AuthorDisplayName, &p.AuthorAvatarURL, &p.LikeCount, &p.CommentCount, &p.IsLiked, - &p.AllowChain, &p.Visibility, + &p.AllowChain, &p.Visibility, &p.Reactions, &p.MyReactions, ) if err != nil { return nil, err @@ -205,7 +207,7 @@ func (r *PostRepository) GetCategories(ctx context.Context) ([]models.Category, return categories, nil } -func (r *PostRepository) GetPostsByAuthor(ctx context.Context, authorID string, viewerID string, limit int, offset int) ([]models.Post, error) { +func (r *PostRepository) GetPostsByAuthor(ctx context.Context, authorID string, viewerID string, limit int, offset int, onlyChains bool) ([]models.Post, error) { query := ` SELECT p.id, p.author_id, p.category_id, p.body, @@ -221,7 +223,10 @@ func (r *PostRepository) GetPostsByAuthor(ctx context.Context, authorID string, p.created_at, pr.handle as author_handle, pr.display_name as author_display_name, COALESCE(pr.avatar_url, '') as author_avatar_url, COALESCE(m.like_count, 0) as like_count, COALESCE(m.comment_count, 0) as comment_count, - CASE WHEN $4 != '' THEN EXISTS(SELECT 1 FROM public.post_likes WHERE post_id = p.id AND user_id = $4::uuid) ELSE FALSE END as is_liked + CASE WHEN $4 != '' THEN EXISTS(SELECT 1 FROM public.post_likes WHERE post_id = p.id AND user_id = $4::uuid) ELSE FALSE END as is_liked, + p.allow_chain, p.visibility, + COALESCE((SELECT jsonb_object_agg(emoji, count) FROM (SELECT emoji, COUNT(*) as count FROM public.post_reactions WHERE post_id = p.id GROUP BY emoji) r), '{}'::jsonb) as reaction_counts, + CASE WHEN ($4::text) != '' THEN COALESCE((SELECT jsonb_agg(emoji) FROM public.post_reactions WHERE post_id = p.id AND user_id = $4::text::uuid), '[]'::jsonb) ELSE '[]'::jsonb END as my_reactions FROM public.posts p JOIN public.profiles pr ON p.author_id = pr.id LEFT JOIN public.post_metrics m ON p.id = m.post_id @@ -234,10 +239,11 @@ func (r *PostRepository) GetPostsByAuthor(ctx context.Context, authorID string, WHERE f.follower_id = CASE WHEN $4 != '' THEN $4::uuid ELSE NULL END AND f.following_id = p.author_id AND f.status = 'accepted' ) ) + AND ($5 = FALSE OR p.chain_parent_id IS NOT NULL) ORDER BY p.created_at DESC LIMIT $2 OFFSET $3 ` - rows, err := r.pool.Query(ctx, query, authorID, limit, offset, viewerID) + rows, err := r.pool.Query(ctx, query, authorID, limit, offset, viewerID, onlyChains) if err != nil { return nil, err } @@ -249,7 +255,7 @@ func (r *PostRepository) GetPostsByAuthor(ctx context.Context, authorID string, err := rows.Scan( &p.ID, &p.AuthorID, &p.CategoryID, &p.Body, &p.ImageURL, &p.VideoURL, &p.ThumbnailURL, &p.DurationMS, &p.Tags, &p.CreatedAt, &p.AuthorHandle, &p.AuthorDisplayName, &p.AuthorAvatarURL, - &p.LikeCount, &p.CommentCount, &p.IsLiked, + &p.LikeCount, &p.CommentCount, &p.IsLiked, &p.AllowChain, &p.Visibility, &p.Reactions, &p.MyReactions, ) if err != nil { return nil, err diff --git a/go-backend/migrate-linux b/go-backend/migrate-linux index 99b74bd..0225188 100644 Binary files a/go-backend/migrate-linux and b/go-backend/migrate-linux differ diff --git a/go-backend/sojorn-api-linux b/go-backend/sojorn-api-linux index 0bed3ad..43220fc 100644 Binary files a/go-backend/sojorn-api-linux and b/go-backend/sojorn-api-linux differ