Update saved posts route and handler

This commit is contained in:
Patrick Britton 2026-02-03 16:52:40 -06:00
parent a1d47f8e95
commit 78f43494a2
7 changed files with 26 additions and 22 deletions

View file

@ -201,7 +201,7 @@ func main() {
users.DELETE("/:id/reject", userHandler.RejectFollowRequest) users.DELETE("/:id/reject", userHandler.RejectFollowRequest)
users.GET("/requests", userHandler.GetPendingFollowRequests) users.GET("/requests", userHandler.GetPendingFollowRequests)
users.GET("/:id/posts", postHandler.GetProfilePosts) users.GET("/:id/posts", postHandler.GetProfilePosts)
users.GET("/me/saved", userHandler.GetSavedPosts) users.GET("/:id/saved", userHandler.GetSavedPosts)
users.GET("/me/liked", userHandler.GetLikedPosts) users.GET("/me/liked", userHandler.GetLikedPosts)
} }

View file

@ -279,13 +279,14 @@ func (h *PostHandler) GetProfilePosts(c *gin.Context) {
limit := utils.GetQueryInt(c, "limit", 20) limit := utils.GetQueryInt(c, "limit", 20)
offset := utils.GetQueryInt(c, "offset", 0) offset := utils.GetQueryInt(c, "offset", 0)
onlyChains := c.Query("chained") == "true"
viewerID := "" viewerID := ""
if val, exists := c.Get("user_id"); exists { if val, exists := c.Get("user_id"); exists {
viewerID = val.(string) 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 { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch profile posts", "details": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch profile posts", "details": err.Error()})
return return

View file

@ -10,6 +10,7 @@ import (
"github.com/patbritton/sojorn-backend/internal/models" "github.com/patbritton/sojorn-backend/internal/models"
"github.com/patbritton/sojorn-backend/internal/repository" "github.com/patbritton/sojorn-backend/internal/repository"
"github.com/patbritton/sojorn-backend/internal/services" "github.com/patbritton/sojorn-backend/internal/services"
"github.com/patbritton/sojorn-backend/pkg/utils"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -231,23 +232,19 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
} }
func (h *UserHandler) GetSavedPosts(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, if targetID == "" || targetID == "me" {
// but if route is /users/:id/saved we would use param. targetID = currentUserID
// The prompt asked for GET /users/me/saved, which implies authenticated user. }
// Check if a specific ID is requested in URL, otherwise use authenticated user // TODO: Add privacy check here if viewing another user's saved posts
// 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
// 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 posts, err := h.postRepo.GetSavedPosts(c.Request.Context(), targetID, limit, offset)
offset := 0
// simplified for brevity, in real app parse query params
posts, err := h.postRepo.GetSavedPosts(c.Request.Context(), userID, limit, offset)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to fetch saved posts") log.Error().Err(err).Msg("Failed to fetch saved posts")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch saved posts"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch saved posts"})

View file

@ -17,7 +17,7 @@ func CORS() gin.HandlerFunc {
} }
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") 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") c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH, DELETE")
if c.Request.Method == "OPTIONS" { if c.Request.Method == "OPTIONS" {

View file

@ -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, 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, 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, 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 FROM public.posts p
JOIN public.profiles pr ON p.author_id = pr.id JOIN public.profiles pr ON p.author_id = pr.id
LEFT JOIN public.post_metrics m ON p.id = m.post_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.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.AuthorHandle, &p.AuthorDisplayName, &p.AuthorAvatarURL,
&p.LikeCount, &p.CommentCount, &p.IsLiked, &p.LikeCount, &p.CommentCount, &p.IsLiked,
&p.AllowChain, &p.Visibility, &p.AllowChain, &p.Visibility, &p.Reactions, &p.MyReactions,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -205,7 +207,7 @@ func (r *PostRepository) GetCategories(ctx context.Context) ([]models.Category,
return categories, nil 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 := ` query := `
SELECT SELECT
p.id, p.author_id, p.category_id, p.body, 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, p.created_at,
pr.handle as author_handle, pr.display_name as author_display_name, COALESCE(pr.avatar_url, '') as author_avatar_url, 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, 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 FROM public.posts p
JOIN public.profiles pr ON p.author_id = pr.id JOIN public.profiles pr ON p.author_id = pr.id
LEFT JOIN public.post_metrics m ON p.id = m.post_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' 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 ORDER BY p.created_at DESC
LIMIT $2 OFFSET $3 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 { if err != nil {
return nil, err return nil, err
} }
@ -249,7 +255,7 @@ func (r *PostRepository) GetPostsByAuthor(ctx context.Context, authorID string,
err := rows.Scan( err := rows.Scan(
&p.ID, &p.AuthorID, &p.CategoryID, &p.Body, &p.ImageURL, &p.VideoURL, &p.ThumbnailURL, &p.DurationMS, &p.Tags, &p.CreatedAt, &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.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 { if err != nil {
return nil, err return nil, err

Binary file not shown.

Binary file not shown.