From d1b01aa5b2d4e406736bf14a2cf13518237d0a17 Mon Sep 17 00:00:00 2001 From: Patrick Britton Date: Fri, 6 Feb 2026 12:37:03 -0600 Subject: [PATCH] Content jailing: hide all posts/comments on ban/suspend, restore on activate --- go-backend/internal/handlers/admin_handler.go | 11 +++++++++++ go-backend/internal/middleware/auth.go | 6 +++++- go-backend/internal/services/content_filter.go | 7 +++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/go-backend/internal/handlers/admin_handler.go b/go-backend/internal/handlers/admin_handler.go index 2e7b479..f657dd8 100644 --- a/go-backend/internal/handlers/admin_handler.go +++ b/go-backend/internal/handlers/admin_handler.go @@ -470,6 +470,9 @@ func (h *AdminHandler) UpdateUserStatus(c *gin.Context) { } // Revoke ALL refresh tokens immediately h.pool.Exec(ctx, `UPDATE refresh_tokens SET revoked = true WHERE user_id = $1::uuid`, targetUserID) + // Jail all their content (hidden from feeds until restored) + h.pool.Exec(ctx, `UPDATE posts SET status = 'jailed' WHERE author_id = $1::uuid AND status = 'active' AND deleted_at IS NULL`, targetUserID) + h.pool.Exec(ctx, `UPDATE comments SET status = 'jailed' WHERE author_id = $1::uuid AND status = 'active' AND deleted_at IS NULL`, targetUserID) } else if req.Status == "suspended" { suspendUntil := time.Now().Add(7 * 24 * time.Hour) // Default 7 day suspension from admin _, err := h.pool.Exec(ctx, `UPDATE users SET status = 'suspended', suspended_until = $2 WHERE id = $1::uuid`, targetUserID, suspendUntil) @@ -477,12 +480,20 @@ func (h *AdminHandler) UpdateUserStatus(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user status"}) return } + // Jail all their content during suspension + h.pool.Exec(ctx, `UPDATE posts SET status = 'jailed' WHERE author_id = $1::uuid AND status = 'active' AND deleted_at IS NULL`, targetUserID) + h.pool.Exec(ctx, `UPDATE comments SET status = 'jailed' WHERE author_id = $1::uuid AND status = 'active' AND deleted_at IS NULL`, targetUserID) } else { _, err := h.pool.Exec(ctx, `UPDATE users SET status = $1, suspended_until = NULL WHERE id = $2::uuid`, req.Status, targetUserID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user status"}) return } + // If reactivating, restore any jailed content + if req.Status == "active" { + h.pool.Exec(ctx, `UPDATE posts SET status = 'active' WHERE author_id = $1::uuid AND status = 'jailed'`, targetUserID) + h.pool.Exec(ctx, `UPDATE comments SET status = 'active' WHERE author_id = $1::uuid AND status = 'jailed'`, targetUserID) + } } // Log status change diff --git a/go-backend/internal/middleware/auth.go b/go-backend/internal/middleware/auth.go index adb5d29..9386ae5 100644 --- a/go-backend/internal/middleware/auth.go +++ b/go-backend/internal/middleware/auth.go @@ -86,9 +86,13 @@ func AuthMiddleware(jwtSecret string, pool ...*pgxpool.Pool) gin.HandlerFunc { } if status == "suspended" { if suspendedUntil != nil && time.Now().After(*suspendedUntil) { - // Suspension expired — reactivate + // Suspension expired — reactivate and restore jailed content dbPool.Exec(context.Background(), `UPDATE users SET status = 'active', suspended_until = NULL WHERE id = $1::uuid`, userID) + dbPool.Exec(context.Background(), + `UPDATE posts SET status = 'active' WHERE author_id = $1::uuid AND status = 'jailed'`, userID) + dbPool.Exec(context.Background(), + `UPDATE comments SET status = 'active' WHERE author_id = $1::uuid AND status = 'jailed'`, userID) } else { c.JSON(http.StatusForbidden, gin.H{"error": "Your account is temporarily suspended.", "code": "suspended"}) c.Abort() diff --git a/go-backend/internal/services/content_filter.go b/go-backend/internal/services/content_filter.go index 8e65be8..a06e0be 100644 --- a/go-backend/internal/services/content_filter.go +++ b/go-backend/internal/services/content_filter.go @@ -165,6 +165,9 @@ func (cf *ContentFilter) RecordStrikeWithIP(ctx context.Context, userID uuid.UUI cf.pool.Exec(ctx, `UPDATE users SET status = 'banned' WHERE id = $1`, userID) // Revoke ALL refresh tokens immediately so the user is logged out cf.pool.Exec(ctx, `UPDATE refresh_tokens SET revoked = true WHERE user_id = $1`, userID) + // Jail all their content + cf.pool.Exec(ctx, `UPDATE posts SET status = 'jailed' WHERE author_id = $1 AND status = 'active' AND deleted_at IS NULL`, userID) + cf.pool.Exec(ctx, `UPDATE comments SET status = 'jailed' WHERE author_id = $1 AND status = 'active' AND deleted_at IS NULL`, userID) // Log IP for ban evasion prevention if clientIP != "" { cf.pool.Exec(ctx, ` @@ -177,11 +180,15 @@ func (cf *ContentFilter) RecordStrikeWithIP(ctx context.Context, userID uuid.UUI consequence = "suspend_7d" suspendUntil := time.Now().Add(7 * 24 * time.Hour) cf.pool.Exec(ctx, `UPDATE users SET status = 'suspended', suspended_until = $2 WHERE id = $1`, userID, suspendUntil) + cf.pool.Exec(ctx, `UPDATE posts SET status = 'jailed' WHERE author_id = $1 AND status = 'active' AND deleted_at IS NULL`, userID) + cf.pool.Exec(ctx, `UPDATE comments SET status = 'jailed' WHERE author_id = $1 AND status = 'active' AND deleted_at IS NULL`, userID) fmt.Printf("Content filter: user %s suspended 7 days (%d strikes)\n", userID, count) case count >= 3: consequence = "suspend_24h" suspendUntil := time.Now().Add(24 * time.Hour) cf.pool.Exec(ctx, `UPDATE users SET status = 'suspended', suspended_until = $2 WHERE id = $1`, userID, suspendUntil) + cf.pool.Exec(ctx, `UPDATE posts SET status = 'jailed' WHERE author_id = $1 AND status = 'active' AND deleted_at IS NULL`, userID) + cf.pool.Exec(ctx, `UPDATE comments SET status = 'jailed' WHERE author_id = $1 AND status = 'active' AND deleted_at IS NULL`, userID) fmt.Printf("Content filter: user %s suspended 24h (%d strikes)\n", userID, count) default: fmt.Printf("Content filter: user %s warning (%d strikes)\n", userID, count)