fix: hide blocked user content

This commit is contained in:
Krispire Admin
2026-06-13 00:46:51 +05:30
parent 0b24ab4b45
commit 1edee92865
5 changed files with 119 additions and 22 deletions
+46 -1
View File
@@ -593,6 +593,32 @@ func (h *ProfileHandler) Get(w http.ResponseWriter, r *http.Request) {
})
}
// gateByBlock resolves the profile owner by handle and returns ErrBlocked
// (→403) when a block exists in either direction between the signed-in
// viewer and the owner — so a blocked viewer can't read the owner's feed,
// tagged posts, or follower lists, even by hitting the API directly. It
// returns the resolved owner so callers needn't look it up again, and
// propagates ErrUserNotFound (→404) for an unknown handle. Anonymous
// viewers and owners viewing their own profile are never gated.
func (h *ProfileHandler) gateByBlock(r *http.Request, handle string) (*model.User, error) {
owner, err := h.users.FindByHandle(r.Context(), handle)
if err != nil {
return nil, err
}
viewerID := middleware.UserID(r.Context())
if viewerID == "" || viewerID == owner.ID {
return owner, nil
}
blocked, err := h.graph.IsBlockedBetween(r.Context(), viewerID, owner.ID)
if err != nil {
return nil, err
}
if blocked {
return nil, service.ErrBlocked
}
return owner, nil
}
// Feed returns posts for the public feed. Same payload as ListMine but
// looked up by handle so it's reachable anonymously. When an
// authenticated viewer hits this route (via OptionalAuth) each post
@@ -600,8 +626,15 @@ func (h *ProfileHandler) Get(w http.ResponseWriter, r *http.Request) {
// button. Anonymous viewers get `false` for every post at zero cost.
func (h *ProfileHandler) Feed(w http.ResponseWriter, r *http.Request) {
handle := chi.URLParam(r, "handle")
owner, err := h.gateByBlock(r, handle)
if err != nil {
mapSocialErr(w, err)
return
}
limit, offset := parseLimitOffset(r)
posts, total, err := h.posts.ListByHandle(r.Context(), handle, limit, offset)
// gateByBlock already resolved the owner, so list by id rather than
// re-resolving the handle.
posts, total, err := h.posts.ListByUser(r.Context(), owner.ID, limit, offset)
if err != nil {
mapSocialErr(w, err)
return
@@ -617,6 +650,10 @@ func (h *ProfileHandler) Feed(w http.ResponseWriter, r *http.Request) {
// user, newest first. Public (OptionalAuth) with viewer-state hydration.
func (h *ProfileHandler) MentionsFeed(w http.ResponseWriter, r *http.Request) {
handle := chi.URLParam(r, "handle")
if _, err := h.gateByBlock(r, handle); err != nil {
mapSocialErr(w, err)
return
}
limit, offset := parseLimitOffset(r)
posts, total, err := h.posts.ListMentioningByHandle(r.Context(), handle, limit, offset)
if err != nil {
@@ -679,6 +716,10 @@ type followListResp struct {
// name/handle search. Public (OptionalAuth).
func (h *ProfileHandler) Followers(w http.ResponseWriter, r *http.Request) {
handle := chi.URLParam(r, "handle")
if _, err := h.gateByBlock(r, handle); err != nil {
mapSocialErr(w, err)
return
}
limit, offset := parseLimitOffset(r)
q := strings.TrimSpace(r.URL.Query().Get("q"))
users, total, err := h.graph.Followers(r.Context(), handle, q, limit, offset)
@@ -692,6 +733,10 @@ func (h *ProfileHandler) Followers(w http.ResponseWriter, r *http.Request) {
// Following — GET /u/{handle}/following?limit=&offset=&q=.
func (h *ProfileHandler) Following(w http.ResponseWriter, r *http.Request) {
handle := chi.URLParam(r, "handle")
if _, err := h.gateByBlock(r, handle); err != nil {
mapSocialErr(w, err)
return
}
limit, offset := parseLimitOffset(r)
q := strings.TrimSpace(r.URL.Query().Get("q"))
users, total, err := h.graph.Following(r.Context(), handle, q, limit, offset)
+25 -10
View File
@@ -94,21 +94,31 @@ type userSearchItem struct {
// the query carries an "@" prefix.
func (h *UserHandler) Search(w http.ResponseWriter, r *http.Request) {
q := strings.TrimSpace(r.URL.Query().Get("q"))
if q == "" {
response.JSON(w, http.StatusOK, map[string]any{
"query": "",
"results": []userSearchItem{},
})
return
}
limit := 20
if v := r.URL.Query().Get("limit"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 50 {
limit = n
}
}
if q == "" {
response.JSON(w, http.StatusOK, map[string]any{
"query": "",
"results": []userSearchItem{},
"total": 0,
"page": 1,
"limit": limit,
"total_pages": 0,
})
return
}
page := 1
if v := r.URL.Query().Get("page"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 1 {
page = n
}
}
users, err := h.svc.Search(r.Context(), q, limit)
users, total, err := h.svc.Search(r.Context(), q, limit, (page-1)*limit)
if err != nil {
response.Error(w, http.StatusInternalServerError, "search failed")
return
@@ -122,9 +132,14 @@ func (h *UserHandler) Search(w http.ResponseWriter, r *http.Request) {
AvatarURL: u.AvatarURL,
})
}
totalPages := (total + limit - 1) / limit
response.JSON(w, http.StatusOK, map[string]any{
"query": q,
"results": items,
"query": q,
"results": items,
"total": total,
"page": page,
"limit": limit,
"total_pages": totalPages,
})
}