From 1201ad85d3f4612547ad37706054ce4c94ccd800 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sat, 12 Sep 2026 13:37:26 +0530 Subject: [PATCH 1/3] fix: set GetPostById to public --- routes/post.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/routes/post.go b/routes/post.go index afc1858..3e16a8d 100644 --- a/routes/post.go +++ b/routes/post.go @@ -32,11 +32,14 @@ func PostRoute(e *gin.Engine, h *handlers.PostHandler) { posts.GET("/faculty", h.GetFacultyPosts) posts.GET("/warden", h.GetWardenPosts) posts.GET("/centrehead", h.GetCentreheadPosts) - posts.GET("/:role/:post_id", h.GetPostByID) // APIs for comments on the posts posts.POST("/faculty/comment/:post_id", h.FacultyPostComment) posts.POST("/warden/comment/:post_id", h.WardenPostComment) posts.POST("/centrehead/comment/:post_id", h.CentreheadPostComment) } + + // let this be public, anyone can read a single post, rate limited by IP. + publicPostLimiter := middleware.NewRateLimiter(200, 1.0/10.0) + e.GET("/api/posts/:role/:post_id", publicPostLimiter.Limit(), h.GetPostByID) } From 8369c016b67944825d94c518785909af378e77b9 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sat, 12 Sep 2026 13:38:06 +0530 Subject: [PATCH 2/3] fix(handler): remove middleware logic and preload author fields --- handlers/post.go | 89 +++++++++++++++--------------------------------- 1 file changed, 28 insertions(+), 61 deletions(-) diff --git a/handlers/post.go b/handlers/post.go index df9c405..b9fd902 100644 --- a/handlers/post.go +++ b/handlers/post.go @@ -4,20 +4,15 @@ import ( "errors" "strconv" - "github.com/ayush00git/cms-web/middleware" "github.com/ayush00git/cms-web/models" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // GetPostByID fetches a single post for the logged in user by role and post_id +// GetPostByID returns a single post with its comments. It is public, +// no authentication is required to read a post. func (h *PostHandler) GetPostByID(c *gin.Context) { - email, exists := c.Get(middleware.EmailKey) - if !exists { - c.JSON(401, gin.H{"error": "unauthenticated user"}) - return - } - role := c.Param("role") postIDString := c.Param("post_id") postIDU64, err := strconv.ParseUint(postIDString, 10, 64) @@ -27,65 +22,37 @@ func (h *PostHandler) GetPostByID(c *gin.Context) { } postID := uint(postIDU64) + author := func(db *gorm.DB) *gorm.DB { + return db.Select("id, name, email") + } + + var post any + var result *gorm.DB switch role { case "faculty": - var faculty models.Faculty - result := h.DB.Where("email = ?", email).Take(&faculty) - if result.Error != nil { - c.JSON(401, gin.H{"error": "user not found"}) - return - } - var post models.FacultyPost - result = h.DB.Preload("Comments").Where("id = ? AND faculty_id = ?", postID, faculty.ID).Take(&post) - if result.Error != nil { - if errors.Is(result.Error, gorm.ErrRecordNotFound) { - c.JSON(404, gin.H{"error": "requested entry no longer exists"}) - return - } - c.JSON(500, gin.H{"error": "internal server error"}) - return - } - c.JSON(200, gin.H{"success": "post fetched successfully", "post": post}) - + var p models.FacultyPost + result = h.DB.Preload("Comments").Preload("Author", author).Where("id = ?", postID).Take(&p) + post = p case "warden": - var warden models.Warden - result := h.DB.Where("email = ?", email).Take(&warden) - if result.Error != nil { - c.JSON(401, gin.H{"error": "user not found"}) - return - } - var post models.WardenPost - result = h.DB.Preload("Comments").Where("id = ? AND warden_id = ?", postID, warden.ID).Take(&post) - if result.Error != nil { - if errors.Is(result.Error, gorm.ErrRecordNotFound) { - c.JSON(404, gin.H{"error": "requested entry no longer exists"}) - return - } - c.JSON(500, gin.H{"error": "internal server error"}) - return - } - c.JSON(200, gin.H{"success": "post fetched successfully", "post": post}) - + var p models.WardenPost + result = h.DB.Preload("Comments").Preload("Author", author).Where("id = ?", postID).Take(&p) + post = p case "centrehead": - var head models.Centrehead - result := h.DB.Where("email = ?", email).Take(&head) - if result.Error != nil { - c.JSON(401, gin.H{"error": "user not found"}) - return - } - var post models.CentreheadPost - result = h.DB.Preload("Comments").Where("id = ? AND centrehead_id = ?", postID, head.ID).Take(&post) - if result.Error != nil { - if errors.Is(result.Error, gorm.ErrRecordNotFound) { - c.JSON(404, gin.H{"error": "requested entry no longer exists"}) - return - } - c.JSON(500, gin.H{"error": "internal server error"}) - return - } - c.JSON(200, gin.H{"success": "post fetched successfully", "post": post}) - + var p models.CentreheadPost + result = h.DB.Preload("Comments").Preload("Author", author).Where("id = ?", postID).Take(&p) + post = p default: c.JSON(400, gin.H{"error": "undefined role"}) + return + } + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.JSON(404, gin.H{"error": "requested entry no longer exists"}) + return + } + c.JSON(500, gin.H{"error": "internal server error"}) + return } + c.JSON(200, gin.H{"success": "post fetched successfully", "post": post}) } From 0d20f877a7c76a1df984408979e4e1dd68efa91f Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sat, 12 Sep 2026 13:39:01 +0530 Subject: [PATCH 3/3] fix(app): set /posts/:role/:id to public and add author --- app/src/context/auth-context.ts | 1 + app/src/pages/post/PostView.tsx | 79 ++++++++++++++++++++++++++++++--- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/app/src/context/auth-context.ts b/app/src/context/auth-context.ts index 770857c..551027b 100644 --- a/app/src/context/auth-context.ts +++ b/app/src/context/auth-context.ts @@ -1,6 +1,7 @@ import { createContext, useContext } from 'react'; export interface ProfileData { + id?: number; name?: string; email?: string; is_verified?: boolean; diff --git a/app/src/pages/post/PostView.tsx b/app/src/pages/post/PostView.tsx index b9e05ff..7c923fb 100644 --- a/app/src/pages/post/PostView.tsx +++ b/app/src/pages/post/PostView.tsx @@ -3,12 +3,14 @@ import { useParams, useNavigate, Link } from 'react-router-dom'; import { Zap, Hammer, Trash2, Pencil, X, Check, Calendar, MapPin, BedDouble, MessageSquare, Wrench, ArrowLeft, AlertCircle, - Clock, Users, + Clock, Users, LogIn, UserCircle2, Mail, } from 'lucide-react'; import { MainLayout } from '../../components/layout/MainLayout'; import { POST_PLACES } from '../../constants/models'; import { CommentBox } from '../../components/CommentBox'; import { Loader } from '../../components/Loader'; +import { useAuth } from '../../context/auth-context'; +import type { ProfileData } from '../../context/auth-context'; type Role = 'faculty' | 'warden' | 'centrehead'; @@ -40,8 +42,30 @@ interface ComplaintPost { comments?: ComplaintComment[] | null; status_audit_logs?: StatusAudit[] | null; people_in_thread?: string[] | null; + // author foreign key, one of these depending on the post's role + faculty_id?: number; + warden_id?: number; + centrehead_id?: number; + // preloaded author; the Go struct field has no json tag, so the key is "Author" + Author?: { id: number; name: string; email: string }; } +// roleOf derives the complaint-side role of a logged-in profile from the +// role-specific field the profile API returns; admins and super admins get null. +function roleOf(profile: ProfileData | null): Role | null { + if (!profile || profile.position || profile.role === 'superadmin') return null; + if (profile.department !== undefined) return 'faculty'; + if (profile.hostel !== undefined) return 'warden'; + if (profile.building !== undefined) return 'centrehead'; + return null; +} + +const LOGIN_PATH: Record = { + faculty: '/faculty/login', + warden: '/warden/login', + centrehead: '/centre-head/login', +}; + interface EditForm { title: string; description: string; @@ -87,6 +111,8 @@ function formatDateTime(iso: string) { export function PostView() { const { role, post_id } = useParams<{ role: Role; post_id: string }>(); const navigate = useNavigate(); + const { status: authStatus, profile } = useAuth(); + const isLoggedIn = authStatus === 'authenticated'; const [post, setPost] = useState(null); const [loading, setLoading] = useState(true); @@ -190,6 +216,12 @@ export function PostView() { const comments = post.comments ?? []; const editExpired = isEditWindowExpired(post.created_at); + // Only the post's author may edit or delete it. The API enforces this too; + // this just keeps the buttons away from everyone else. + const authorId = role === 'faculty' ? post.faculty_id : role === 'warden' ? post.warden_id : post.centrehead_id; + const isOwner = isLoggedIn && roleOf(profile) === role && profile?.id !== undefined && profile.id === authorId; + const canManage = isOwner && !editExpired; + const editBase = isFaculty ? '/api/posts/faculty/edit' : isWarden ? '/api/posts/warden/edit' : '/api/posts/centrehead/edit'; const deleteBase = isFaculty ? '/api/posts/faculty/delete' : isWarden ? '/api/posts/warden/delete' : '/api/posts/centrehead/delete'; @@ -289,13 +321,13 @@ export function PostView() { {/* Back Link */}
- - Back to Dashboard + + {isLoggedIn ? 'Back to Dashboard' : 'Back to Home'} {/* Action buttons */}
- {!isEditing && !editExpired && ( + {!isEditing && canManage && (