From e7a09302df3faf85f5746186935451b302096623 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Thu, 4 Jun 2026 22:37:03 +0530 Subject: [PATCH 01/10] fix: allow author to comment --- handlers/faculty_post.go | 62 ++++++++++++++++++++++++++++++++++++++++ routes/post.go | 3 ++ 2 files changed, 65 insertions(+) diff --git a/handlers/faculty_post.go b/handlers/faculty_post.go index 2eb29cf..9771460 100644 --- a/handlers/faculty_post.go +++ b/handlers/faculty_post.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "log" + "strconv" "time" "github.com/ayush00git/cms-web/middleware" @@ -237,3 +238,64 @@ func (h *PostHandler) GetFacultyPosts(c *gin.Context) { c.JSON(200, gin.H{"success": "posts fetched successfully", "posts": posts}) } + + +// FacultyPostComment allows the author of the post to +// comment on the post +func (h *PostHandler) FacultyPostComment(c *gin.Context) { + // get email of the logged in user from gin context + email, _ := c.Get(middleware.EmailKey) + + // find the user + var faculty models.Faculty + result := h.DB.Where("email = ?", email).Take(&faculty) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.JSON(404, gin.H{"error": "user does not exists"}) + return + } + c.JSON(500, gin.H{"error": "internal server error"}); + return; + } + + // get post id from path parameters + postIDString := c.Param("post_id") + postIDU64, err := strconv.ParseUint(postIDString, 10, 64) + if err != nil { + c.JSON(500, gin.H{"error": "failed to parse post id"}) + return + } + + // read database for this postIDU64 + var post models.FacultyPost + result = h.DB.Where("id = ?", uint(postIDU64)).Take(&post) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.JSON(404, gin.H{"error": "post unavailable"}) + return + } + c.JSON(500, gin.H{"error": "internal server error"}) + return + } + + // validate the author of the post + if post.FacultyID != faculty.ID { + c.JSON(403, gin.H{"error": "you are not authorized to comment"}) + return + } + + // bind input to json + var inputs CommentType + if err := c.ShouldBindJSON(&inputs); err != nil { + c.JSON(401, gin.H{"error": "invalid request body"}) + return + } + + _ = models.Comment{ + CommentableID: uint(postIDU64), + CommentableType: "faculty_posts", + Content: inputs.Content, + AuthorID: faculty.ID, + } + +} diff --git a/routes/post.go b/routes/post.go index ea60f4c..514493d 100644 --- a/routes/post.go +++ b/routes/post.go @@ -27,4 +27,7 @@ func PostRoute(e *gin.Engine, h *handlers.PostHandler) { e.GET("/api/post/faculty", middleware.IsAuthenticated(), h.GetFacultyPosts) e.GET("/api/post/warden", middleware.IsAuthenticated(), h.GetWardenPosts) e.GET("/api/post/centrehead", middleware.IsAuthenticated(), h.GetCentreheadPosts) + + // APIs for comments on the posts + e.POST("/api/post/faculty/comment/:post_id", h.FacultyPostComment) } From 4054f96ce527de9d9c9bf78e1bc3cf3d24473b23 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 7 Jun 2026 19:18:35 +0530 Subject: [PATCH 02/10] fix: struct now drops Author admin model --- models/post.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/post.go b/models/post.go index 2f4718c..9acef3e 100644 --- a/models/post.go +++ b/models/post.go @@ -95,8 +95,8 @@ type Comment struct { CommentableID uint `gorm:"not null"` CommentableType string `gorm:"not null"` Content string `gorm:"type:text;not null" json:"comment_text"` - AuthorID uint `gorm:"not null" json:"author_id"` - Author Admin `gorm:"foreignKey:AuthorID"` + Email string `gorm:"not null" json:"email"` + Role string `gorm:"not null" json:"role"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } From bd2a14722dc0a27cf7573ee3da8f7ecb609a12c5 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 7 Jun 2026 20:07:52 +0530 Subject: [PATCH 03/10] feat: refined the preloaded author --- handlers/admin_comment.go | 3 ++- handlers/admin_post.go | 18 +++--------------- handlers/centrehead_post.go | 6 +----- handlers/faculty_post.go | 20 +++++++++++++------- handlers/warden_post.go | 6 +----- 5 files changed, 20 insertions(+), 33 deletions(-) diff --git a/handlers/admin_comment.go b/handlers/admin_comment.go index 6090d91..72947ee 100644 --- a/handlers/admin_comment.go +++ b/handlers/admin_comment.go @@ -89,7 +89,8 @@ func (h *AdminHandler) AdminPostComment (c *gin.Context) { CommentableID: uint(postID), CommentableType: postType, Content : inputs.Content, - AuthorID: admin.ID, + Email: admin.Email, + Role: string(admin.Position), CreatedAt: time.Now(), UpdatedAt: time.Now(), } diff --git a/handlers/admin_post.go b/handlers/admin_post.go index 0a143a9..8d4c00c 100644 --- a/handlers/admin_post.go +++ b/handlers/admin_post.go @@ -271,11 +271,7 @@ func (h *AdminHandler) AdminGetPost(c *gin.Context) { result := h.DB.Preload("Author", func (db *gorm.DB) (*gorm.DB) { return db.Select("id, email, name, house_number, department, phone_number, block, type") }). - Preload("Comments", func(db *gorm.DB) (*gorm.DB) { - return db.Preload("Author", func(d *gorm.DB) (*gorm.DB) { - return d.Select("id, email, position") - }) - }). + Preload("Comments"). Where("id = ?", postID).Take(&post) if result.Error != nil { c.JSON(404, gin.H{"error": "failed to fetch the post"}) @@ -287,11 +283,7 @@ func (h *AdminHandler) AdminGetPost(c *gin.Context) { result := h.DB.Preload("Author", func (db *gorm.DB) (*gorm.DB) { return db.Select("id, email, hostel, phone_number") }). - Preload("Comments", func(db *gorm.DB) (*gorm.DB) { - return db.Preload("Author", func(d *gorm.DB) (*gorm.DB) { - return d.Select("id, email, position") - }) - }). + Preload("Comments"). Where("id = ?", postID).Take(&post) if result.Error != nil { c.JSON(404, gin.H{"error": "failed to fetch the post"}) @@ -303,11 +295,7 @@ func (h *AdminHandler) AdminGetPost(c *gin.Context) { result := h.DB.Preload("Author", func (db *gorm.DB) (*gorm.DB) { return db.Select("id, email, building, phone_number") }). - Preload("Comments", func(db *gorm.DB) (*gorm.DB) { - return db.Preload("Author", func(d *gorm.DB) (*gorm.DB) { - return d.Select("id, email, position") - }) - }). + Preload("Comments"). Where("id = ?", postID).Take(&post) if result.Error != nil { c.JSON(404, gin.H{"error": "failed to fetch the post"}) diff --git a/handlers/centrehead_post.go b/handlers/centrehead_post.go index dba8fc3..c19c2ea 100644 --- a/handlers/centrehead_post.go +++ b/handlers/centrehead_post.go @@ -211,11 +211,7 @@ func (h *PostHandler) GetCentreheadPosts(c *gin.Context) { var posts []models.CentreheadPost result = h.DB. - Preload("Comments", func(db *gorm.DB) (*gorm.DB) { - return db.Preload("Author", func (d *gorm.DB) (*gorm.DB) { - return d.Select("id, email, position") - }) - }). + Preload("Comments"). Where("centrehead_id = ?", head.ID). Find(&posts) if result.Error != nil { diff --git a/handlers/faculty_post.go b/handlers/faculty_post.go index 9771460..17cc34f 100644 --- a/handlers/faculty_post.go +++ b/handlers/faculty_post.go @@ -224,11 +224,7 @@ func (h *PostHandler) GetFacultyPosts(c *gin.Context) { // return posts where author is faculty (the logged in user) var posts []models.FacultyPost result = h.DB. - Preload("Comments", func(db *gorm.DB) (*gorm.DB) { - return db.Preload("Author", func(d *gorm.DB) (*gorm.DB) { - return d.Select("id, email, position") - }) - }). + Preload("Comments"). Where("faculty_id = ?", faculty.ID). Find(&posts) if result.Error != nil { @@ -291,11 +287,21 @@ func (h *PostHandler) FacultyPostComment(c *gin.Context) { return } - _ = models.Comment{ + doc := models.Comment{ CommentableID: uint(postIDU64), CommentableType: "faculty_posts", Content: inputs.Content, - AuthorID: faculty.ID, + Email: faculty.Email, + Role: "faculty", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + result = h.DB.Create(&doc) + if result.Error != nil { + c.JSON(500, gin.H{"error": "failed to comment at the moment"}) + return } + c.JSON(201, gin.H{"success": "comment posted!"}) } diff --git a/handlers/warden_post.go b/handlers/warden_post.go index 51fede3..0142679 100644 --- a/handlers/warden_post.go +++ b/handlers/warden_post.go @@ -215,11 +215,7 @@ func (h *PostHandler) GetWardenPosts(c *gin.Context) { var posts []models.WardenPost result = h.DB. - Preload("Comments", func(db *gorm.DB) (*gorm.DB) { - return db.Preload("Author", func(d *gorm.DB) (*gorm.DB) { - return d.Select("id, email, position") - }) - }). + Preload("Comments"). Where("warden_id = ?", warden.ID). Find(&posts) if result.Error != nil { From 403bc8e13e101d7a74dc69405b86b41f45269983 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 7 Jun 2026 20:08:11 +0530 Subject: [PATCH 04/10] added tests --- test/admin_comment_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/admin_comment_test.go b/test/admin_comment_test.go index ec7b077..ad82ace 100644 --- a/test/admin_comment_test.go +++ b/test/admin_comment_test.go @@ -43,8 +43,11 @@ func TestAdminPostComment_Success(t *testing.T) { if doc.Content != "looks good" { t.Fatalf("expected content %q, got %q", "looks good", doc.Content) } - if doc.AuthorID != admin.ID { - t.Fatalf("expected author %d, got %d", admin.ID, doc.AuthorID) + if doc.Email != admin.Email { + t.Fatalf("expected author email %q, got %q", admin.Email, doc.Email) + } + if doc.Role != string(admin.Position) { + t.Fatalf("expected role %q, got %q", string(admin.Position), doc.Role) } } From 3c0eb0e007ad230b42c828eb2bb5af35e66d2f82 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 7 Jun 2026 20:53:44 +0530 Subject: [PATCH 05/10] feat: add warden and centrehead post comment handlers --- handlers/centrehead_post.go | 75 ++++++++++++++++++++++++++++++++++++- handlers/warden_post.go | 75 ++++++++++++++++++++++++++++++++++++- 2 files changed, 148 insertions(+), 2 deletions(-) diff --git a/handlers/centrehead_post.go b/handlers/centrehead_post.go index c19c2ea..747b01d 100644 --- a/handlers/centrehead_post.go +++ b/handlers/centrehead_post.go @@ -1,10 +1,11 @@ package handlers import ( + "errors" "fmt" "log" + "strconv" "time" - "errors" "github.com/ayush00git/cms-web/middleware" "github.com/ayush00git/cms-web/models" @@ -221,3 +222,75 @@ func (h *PostHandler) GetCentreheadPosts(c *gin.Context) { c.JSON(200, gin.H{"success": "posts fetched successfully", "posts": posts}) } + + +// CentreheadPostComment allows a user of type centrehead to post +// comment as the post's author +func (h *PostHandler) CentreheadPostComment(c *gin.Context) { + // get email of the logged in user from the gin context + email, _ := c.Get(middleware.EmailKey) + + // check if the user is a type centrehead role + var head models.Centrehead + result := h.DB.Where("email = ?", email).Take(&head) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.JSON(404, gin.H{"error": "user not found"}) + return + } + c.JSON(500, gin.H{"error": "internal server error"}) + return + } + + // get post_id from path parameters + postIDString := c.Param("post_id") + postIDU64, err := strconv.ParseUint(postIDString, 10, 64) + if err != nil { + c.JSON(500, gin.H{"error": "failed to parse post_id at the moment"}) + return + } + + // read this post from the db + postID := uint(postIDU64) + var post models.CentreheadPost + result = h.DB.Where("id = ?", postID).Take(&post) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.JSON(404, gin.H{"error": "post not found"}) + return + } + c.JSON(500, gin.H{"error": "internal server error"}) + return + } + + // verify the centrehead(logged in user) is the author of the post + if head.ID != post.CentreheadID { + c.JSON(403, gin.H{"error": "you are not authorized to comment"}) + return + } + + // bind the input + var inputs CommentType + if err := c.ShouldBindJSON(&inputs); err != nil { + c.JSON(401, gin.H{"error": "invalid request body"}) + return + } + + doc := models.Comment{ + CommentableID: postID, + CommentableType: "centrehead_posts", + Content: inputs.Content, + Email: head.Email, + Role: "centrehead", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + result = h.DB.Create(&doc) + if result.Error != nil { + c.JSON(500, gin.H{"error": "failed to comment at the moment"}) + return + } + + c.JSON(201, gin.H{"success": "comment posted!"}) +} diff --git a/handlers/warden_post.go b/handlers/warden_post.go index 0142679..d598044 100644 --- a/handlers/warden_post.go +++ b/handlers/warden_post.go @@ -1,10 +1,11 @@ package handlers import ( + "errors" "fmt" "log" + "strconv" "time" - "errors" "github.com/ayush00git/cms-web/middleware" "github.com/ayush00git/cms-web/models" @@ -225,3 +226,75 @@ func (h *PostHandler) GetWardenPosts(c *gin.Context) { c.JSON(200, gin.H{"success": "posts fetched successfully", "posts": posts}) } + + +// WardenPostComment allows a user of type warden to post +// comment as the post's author +func (h *PostHandler) WardenPostComment(c *gin.Context) { + // get email of the logged in user from the gin context + email, _ := c.Get(middleware.EmailKey) + + // check if the user is a type warden role + var warden models.Warden + result := h.DB.Where("email = ?", email).Take(&warden) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.JSON(404, gin.H{"error": "user not found"}) + return + } + c.JSON(500, gin.H{"error": "internal server error"}) + return + } + + // get post_id from path parameters + postIDString := c.Param("post_id") + postIDU64, err := strconv.ParseUint(postIDString, 10, 64) + if err != nil { + c.JSON(500, gin.H{"error": "failed to parse post_id at the moment"}) + return + } + + // read this post from the db + postID := uint(postIDU64) + var post models.WardenPost + result = h.DB.Where("id = ?", postID).Take(&post) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.JSON(404, gin.H{"error": "post not found"}) + return + } + c.JSON(500, gin.H{"error": "internal server error"}) + return + } + + // verify the warden(logged in user) is the author of the post + if warden.ID != post.WardenID { + c.JSON(403, gin.H{"error": "you are not authorized to comment"}) + return + } + + // bind the input + var inputs CommentType + if err := c.ShouldBindJSON(&inputs); err != nil { + c.JSON(401, gin.H{"error": "invalid request body"}) + return + } + + doc := models.Comment{ + CommentableID: postID, + CommentableType: "warden_posts", + Content: inputs.Content, + Email: warden.Email, + Role: "warden", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + result = h.DB.Create(&doc) + if result.Error != nil { + c.JSON(500, gin.H{"error": "failed to comment at the moment"}) + return + } + + c.JSON(201, gin.H{"success": "comment posted!"}) +} From c770c6c9f11ed002d79b07d313016ce6f698aea0 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 7 Jun 2026 20:54:07 +0530 Subject: [PATCH 06/10] feat: registered comment routes --- routes/post.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/routes/post.go b/routes/post.go index 514493d..16c9221 100644 --- a/routes/post.go +++ b/routes/post.go @@ -29,5 +29,7 @@ func PostRoute(e *gin.Engine, h *handlers.PostHandler) { e.GET("/api/post/centrehead", middleware.IsAuthenticated(), h.GetCentreheadPosts) // APIs for comments on the posts - e.POST("/api/post/faculty/comment/:post_id", h.FacultyPostComment) + e.POST("/api/post/faculty/comment/:post_id", middleware.IsAuthenticated() ,h.FacultyPostComment) + e.POST("/api/post/warden/comment/:post_id", middleware.IsAuthenticated(), h.WardenPostComment) + e.POST("/api/post/centrehead/comment/:post_id", middleware.IsAuthenticated(), h.CentreheadPostComment) } From 8ae5f3db4f434757e1c0a3124345734a63b9f6ed Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 7 Jun 2026 20:54:33 +0530 Subject: [PATCH 07/10] feat: add test cases --- test/helpers_test.go | 11 +- test/post_comment_test.go | 232 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 test/post_comment_test.go diff --git a/test/helpers_test.go b/test/helpers_test.go index a0881de..ec9a487 100644 --- a/test/helpers_test.go +++ b/test/helpers_test.go @@ -25,6 +25,7 @@ import ( "github.com/glebarez/sqlite" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" + "gorm.io/gorm/logger" ) // testPassword is the cleartext password every seeded user is created with, so @@ -74,7 +75,11 @@ func newTestDB(t *testing.T) *gorm.DB { // The unique name keeps tests isolated from one another, while the shared // cache keeps the schema alive across gorm's connection pool. dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) - db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) + db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{ + // Silence GORM's logger so expected "record not found" lookups in the + // not-found test cases don't spam the test output. + Logger: logger.Default.LogMode(logger.Silent), + }) if err != nil { t.Fatalf("failed to open in-memory sqlite: %v", err) } @@ -144,6 +149,10 @@ func newPostRouter(db *gorm.DB, auth gin.HandlerFunc) *gin.Engine { e.GET("/api/post/warden", auth, h.GetWardenPosts) e.GET("/api/post/centrehead", auth, h.GetCentreheadPosts) + e.POST("/api/post/faculty/comment/:post_id", auth, h.FacultyPostComment) + e.POST("/api/post/warden/comment/:post_id", auth, h.WardenPostComment) + e.POST("/api/post/centrehead/comment/:post_id", auth, h.CentreheadPostComment) + return e } diff --git a/test/post_comment_test.go b/test/post_comment_test.go new file mode 100644 index 0000000..f22a84f --- /dev/null +++ b/test/post_comment_test.go @@ -0,0 +1,232 @@ +package test + +import ( + "net/http" + "testing" + + "github.com/ayush00git/cms-web/handlers" + "github.com/ayush00git/cms-web/models" +) + +// Tests for the post-author comment APIs: FacultyPostComment, +// WardenPostComment and CentreheadPostComment. These let the author of a post +// comment on their own post; non-authors are rejected. + +// --- FacultyPostComment ----------------------------------------------------- + +func TestFacultyPostComment_Success(t *testing.T) { + db := newTestDB(t) + f := seedFaculty(t, db, "fac.cmt@iit.ac.in") + post := models.FacultyPost{FacultyID: f.ID, Place: models.PlaceDepartmental, TypeOfPost: models.TypeCivil, Title: "t", Description: "d"} + db.Create(&post) + + e := newPostRouter(db, authAs(f.ID, f.Email)) + rec := doRequest(t, e, http.MethodPost, "/api/post/faculty/comment/1", handlers.CommentType{Content: "my issue persists"}) + assertStatus(t, rec, 201) + + var doc models.Comment + if err := db.Where("commentable_type = ? AND commentable_id = ?", "faculty_posts", post.ID).Take(&doc).Error; err != nil { + t.Fatalf("expected comment to be persisted: %v", err) + } + if doc.Content != "my issue persists" { + t.Fatalf("expected content %q, got %q", "my issue persists", doc.Content) + } + if doc.Email != f.Email { + t.Fatalf("expected email %q, got %q", f.Email, doc.Email) + } + if doc.Role != "faculty" { + t.Fatalf("expected role %q, got %q", "faculty", doc.Role) + } +} + +func TestFacultyPostComment_NotAuthor(t *testing.T) { + db := newTestDB(t) + owner := seedFaculty(t, db, "fac.owner@iit.ac.in") + other := seedFaculty(t, db, "fac.other@iit.ac.in") + post := models.FacultyPost{FacultyID: owner.ID, Place: models.PlaceDepartmental, TypeOfPost: models.TypeCivil, Title: "t", Description: "d"} + db.Create(&post) + + e := newPostRouter(db, authAs(other.ID, other.Email)) + rec := doRequest(t, e, http.MethodPost, "/api/post/faculty/comment/1", handlers.CommentType{Content: "x"}) + assertStatus(t, rec, 403) +} + +func TestFacultyPostComment_UserNotFound(t *testing.T) { + db := newTestDB(t) + e := newPostRouter(db, authAs(999, "ghost@iit.ac.in")) + rec := doRequest(t, e, http.MethodPost, "/api/post/faculty/comment/1", handlers.CommentType{Content: "x"}) + assertStatus(t, rec, 404) +} + +func TestFacultyPostComment_PostNotFound(t *testing.T) { + db := newTestDB(t) + f := seedFaculty(t, db, "fac.pnf@iit.ac.in") + e := newPostRouter(db, authAs(f.ID, f.Email)) + rec := doRequest(t, e, http.MethodPost, "/api/post/faculty/comment/9999", handlers.CommentType{Content: "x"}) + assertStatus(t, rec, 404) +} + +func TestFacultyPostComment_BadPostID(t *testing.T) { + db := newTestDB(t) + f := seedFaculty(t, db, "fac.badpid@iit.ac.in") + e := newPostRouter(db, authAs(f.ID, f.Email)) + rec := doRequest(t, e, http.MethodPost, "/api/post/faculty/comment/not-a-number", handlers.CommentType{Content: "x"}) + assertStatus(t, rec, 500) +} + +func TestFacultyPostComment_InvalidBody(t *testing.T) { + db := newTestDB(t) + f := seedFaculty(t, db, "fac.badbody@iit.ac.in") + post := models.FacultyPost{FacultyID: f.ID, Place: models.PlaceDepartmental, TypeOfPost: models.TypeCivil, Title: "t", Description: "d"} + db.Create(&post) + + e := newPostRouter(db, authAs(f.ID, f.Email)) + rec := doRequestRaw(t, e, http.MethodPost, "/api/post/faculty/comment/1", "{not json") + assertStatus(t, rec, 401) +} + +// --- WardenPostComment ------------------------------------------------------ + +func TestWardenPostComment_Success(t *testing.T) { + db := newTestDB(t) + w := seedWarden(t, db, "war.cmt@iit.ac.in") + post := models.WardenPost{WardenID: w.ID, RoomNumber: "A-1", TypeOfPost: models.TypeCivil, Title: "t", Description: "d"} + db.Create(&post) + + e := newPostRouter(db, authAs(w.ID, w.Email)) + rec := doRequest(t, e, http.MethodPost, "/api/post/warden/comment/1", handlers.CommentType{Content: "still broken"}) + assertStatus(t, rec, 201) + + var doc models.Comment + if err := db.Where("commentable_type = ? AND commentable_id = ?", "warden_posts", post.ID).Take(&doc).Error; err != nil { + t.Fatalf("expected comment to be persisted: %v", err) + } + if doc.Content != "still broken" { + t.Fatalf("expected content %q, got %q", "still broken", doc.Content) + } + if doc.Email != w.Email { + t.Fatalf("expected email %q, got %q", w.Email, doc.Email) + } + if doc.Role != "warden" { + t.Fatalf("expected role %q, got %q", "warden", doc.Role) + } +} + +func TestWardenPostComment_NotAuthor(t *testing.T) { + db := newTestDB(t) + owner := seedWarden(t, db, "war.owner@iit.ac.in") + other := seedWarden(t, db, "war.other@iit.ac.in") + post := models.WardenPost{WardenID: owner.ID, RoomNumber: "A-1", TypeOfPost: models.TypeCivil, Title: "t", Description: "d"} + db.Create(&post) + + e := newPostRouter(db, authAs(other.ID, other.Email)) + rec := doRequest(t, e, http.MethodPost, "/api/post/warden/comment/1", handlers.CommentType{Content: "x"}) + assertStatus(t, rec, 403) +} + +func TestWardenPostComment_UserNotFound(t *testing.T) { + db := newTestDB(t) + e := newPostRouter(db, authAs(999, "ghost@iit.ac.in")) + rec := doRequest(t, e, http.MethodPost, "/api/post/warden/comment/1", handlers.CommentType{Content: "x"}) + assertStatus(t, rec, 404) +} + +func TestWardenPostComment_PostNotFound(t *testing.T) { + db := newTestDB(t) + w := seedWarden(t, db, "war.pnf@iit.ac.in") + e := newPostRouter(db, authAs(w.ID, w.Email)) + rec := doRequest(t, e, http.MethodPost, "/api/post/warden/comment/9999", handlers.CommentType{Content: "x"}) + assertStatus(t, rec, 404) +} + +func TestWardenPostComment_BadPostID(t *testing.T) { + db := newTestDB(t) + w := seedWarden(t, db, "war.badpid@iit.ac.in") + e := newPostRouter(db, authAs(w.ID, w.Email)) + rec := doRequest(t, e, http.MethodPost, "/api/post/warden/comment/not-a-number", handlers.CommentType{Content: "x"}) + assertStatus(t, rec, 500) +} + +func TestWardenPostComment_InvalidBody(t *testing.T) { + db := newTestDB(t) + w := seedWarden(t, db, "war.badbody@iit.ac.in") + post := models.WardenPost{WardenID: w.ID, RoomNumber: "A-1", TypeOfPost: models.TypeCivil, Title: "t", Description: "d"} + db.Create(&post) + + e := newPostRouter(db, authAs(w.ID, w.Email)) + rec := doRequestRaw(t, e, http.MethodPost, "/api/post/warden/comment/1", "{not json") + assertStatus(t, rec, 401) +} + +// --- CentreheadPostComment -------------------------------------------------- + +func TestCentreheadPostComment_Success(t *testing.T) { + db := newTestDB(t) + ch := seedCentrehead(t, db, "ch.cmt@iit.ac.in") + post := models.CentreheadPost{CentreheadID: ch.ID, TypeOfPost: models.TypeCivil, Title: "t", Description: "d"} + db.Create(&post) + + e := newPostRouter(db, authAs(ch.ID, ch.Email)) + rec := doRequest(t, e, http.MethodPost, "/api/post/centrehead/comment/1", handlers.CommentType{Content: "needs urgent fix"}) + assertStatus(t, rec, 201) + + var doc models.Comment + if err := db.Where("commentable_type = ? AND commentable_id = ?", "centrehead_posts", post.ID).Take(&doc).Error; err != nil { + t.Fatalf("expected comment to be persisted: %v", err) + } + if doc.Content != "needs urgent fix" { + t.Fatalf("expected content %q, got %q", "needs urgent fix", doc.Content) + } + if doc.Email != ch.Email { + t.Fatalf("expected email %q, got %q", ch.Email, doc.Email) + } + if doc.Role != "centrehead" { + t.Fatalf("expected role %q, got %q", "centrehead", doc.Role) + } +} + +func TestCentreheadPostComment_NotAuthor(t *testing.T) { + db := newTestDB(t) + owner := seedCentrehead(t, db, "ch.owner@iit.ac.in") + other := seedCentrehead(t, db, "ch.other@iit.ac.in") + post := models.CentreheadPost{CentreheadID: owner.ID, TypeOfPost: models.TypeCivil, Title: "t", Description: "d"} + db.Create(&post) + + e := newPostRouter(db, authAs(other.ID, other.Email)) + rec := doRequest(t, e, http.MethodPost, "/api/post/centrehead/comment/1", handlers.CommentType{Content: "x"}) + assertStatus(t, rec, 403) +} + +func TestCentreheadPostComment_UserNotFound(t *testing.T) { + db := newTestDB(t) + e := newPostRouter(db, authAs(999, "ghost@iit.ac.in")) + rec := doRequest(t, e, http.MethodPost, "/api/post/centrehead/comment/1", handlers.CommentType{Content: "x"}) + assertStatus(t, rec, 404) +} + +func TestCentreheadPostComment_PostNotFound(t *testing.T) { + db := newTestDB(t) + ch := seedCentrehead(t, db, "ch.pnf@iit.ac.in") + e := newPostRouter(db, authAs(ch.ID, ch.Email)) + rec := doRequest(t, e, http.MethodPost, "/api/post/centrehead/comment/9999", handlers.CommentType{Content: "x"}) + assertStatus(t, rec, 404) +} + +func TestCentreheadPostComment_BadPostID(t *testing.T) { + db := newTestDB(t) + ch := seedCentrehead(t, db, "ch.badpid@iit.ac.in") + e := newPostRouter(db, authAs(ch.ID, ch.Email)) + rec := doRequest(t, e, http.MethodPost, "/api/post/centrehead/comment/not-a-number", handlers.CommentType{Content: "x"}) + assertStatus(t, rec, 500) +} + +func TestCentreheadPostComment_InvalidBody(t *testing.T) { + db := newTestDB(t) + ch := seedCentrehead(t, db, "ch.badbody@iit.ac.in") + post := models.CentreheadPost{CentreheadID: ch.ID, TypeOfPost: models.TypeCivil, Title: "t", Description: "d"} + db.Create(&post) + + e := newPostRouter(db, authAs(ch.ID, ch.Email)) + rec := doRequestRaw(t, e, http.MethodPost, "/api/post/centrehead/comment/1", "{not json") + assertStatus(t, rec, 401) +} From 39d4f87da2d77a9e14834cd0eba3ae8a1add2db9 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 7 Jun 2026 21:18:18 +0530 Subject: [PATCH 08/10] fix: AdminGetPost now returns server-driven response --- handlers/admin_comment.go | 2 +- handlers/admin_post.go | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/handlers/admin_comment.go b/handlers/admin_comment.go index 72947ee..7c2026a 100644 --- a/handlers/admin_comment.go +++ b/handlers/admin_comment.go @@ -27,7 +27,7 @@ type CommentType struct { // AdminPost comment allow any admin comment on any type of post. // Common for all type of admins and posts. -func (h *AdminHandler) AdminPostComment (c *gin.Context) { +func (h *AdminHandler) AdminPostComment(c *gin.Context) { // verify the admin emailID, exists := c.Get(middleware.EmailKey) if !exists { diff --git a/handlers/admin_post.go b/handlers/admin_post.go index 8d4c00c..71ec26d 100644 --- a/handlers/admin_post.go +++ b/handlers/admin_post.go @@ -12,7 +12,7 @@ import ( // GetXENPosts fetch posts (all that were posted) where status of post is // Pending_XEN PENDING_AE PENDING_JE Resolved_JE Resolved Closed -func (h *AdminHandler) GetXENPosts (c *gin.Context) { +func (h *AdminHandler) GetXENPosts(c *gin.Context) { emailID, exists := c.Get(middleware.EmailKey) if !exists { @@ -86,7 +86,7 @@ func (h *AdminHandler) GetXENPosts (c *gin.Context) { // GetAEPosts fetch posts where status of post is // Pending_AE Pending_JE -func (h* AdminHandler) GetAEPosts (c *gin.Context) { +func (h* AdminHandler) GetAEPosts(c *gin.Context) { // get email of user from gin context email, exists := c.Get(middleware.EmailKey) @@ -161,7 +161,7 @@ func (h* AdminHandler) GetAEPosts (c *gin.Context) { // GetJEPosts fetch posts where status of Post is // Pending_JE Resolved_JE -func (h* AdminHandler) GetJEPosts (c *gin.Context) { +func (h* AdminHandler) GetJEPosts(c *gin.Context) { // get email of user from gin context email, exists := c.Get(middleware.EmailKey) @@ -307,5 +307,9 @@ func (h *AdminHandler) AdminGetPost(c *gin.Context) { return } - c.JSON(200, gin.H{"success": "post fetched", "post": reqPost}) + c.JSON(200, gin.H{ + "success": "post fetched", + "post": reqPost, + "position": admin.Position, + }) } From 65534f69398098d0c9e2ce96652f66e0daac2bed Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 7 Jun 2026 21:18:46 +0530 Subject: [PATCH 09/10] fix: api response refactored --- app/src/components/ComplaintCard.tsx | 17 +++++++---------- app/src/pages/admin/AdminPostView.tsx | 21 ++++++++++----------- app/src/pages/auth/StaffLogin.tsx | 1 - 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/app/src/components/ComplaintCard.tsx b/app/src/components/ComplaintCard.tsx index ed2ac8e..27b6dca 100644 --- a/app/src/components/ComplaintCard.tsx +++ b/app/src/components/ComplaintCard.tsx @@ -10,17 +10,11 @@ import { POST_PLACES } from '../constants/models'; export type Role = 'faculty' | 'warden' | 'centrehead'; -export interface CommentAuthor { - id: number; - email: string; - position: string; -} - export interface ComplaintComment { id: number; comment_text: string; - author_id: number; - Author?: CommentAuthor; + email: string; + role: string; created_at: string; } @@ -195,7 +189,7 @@ function CommentsList({ comments }: { comments: ComplaintComment[] }) { return (
{comments.map((c, idx) => { - const author = c.Author?.position ? roleLabel(c.Author.position) : `Admin #${c.author_id}`; + const author = c.role ? roleLabel(c.role) : 'Staff'; const initials = author.split(' ').map((w) => w[0]).slice(0, 2).join('').toUpperCase(); return (
@@ -208,7 +202,10 @@ function CommentsList({ comments }: { comments: ComplaintComment[] }) {
- {author} + + {author} + {c.email && {c.email}} + {formatDateTime(c.created_at)} diff --git a/app/src/pages/admin/AdminPostView.tsx b/app/src/pages/admin/AdminPostView.tsx index a39aa2d..bf24890 100644 --- a/app/src/pages/admin/AdminPostView.tsx +++ b/app/src/pages/admin/AdminPostView.tsx @@ -44,17 +44,11 @@ interface CentreHeadAuthor { phone_number: string; } -interface CommentAuthor { - id: number; - email: string; - position: string; -} - interface Comment { id: number; comment_text: string; - author_id: number; - Author?: CommentAuthor; + email: string; + role: string; created_at: string; } @@ -110,6 +104,7 @@ type Post = FacultyPost | WardenPost | CentreHeadPost; interface ApiResponse { success: string; post: Post; + position: string; } // ── Helpers ──────────────────────────────────────────────────────────────────── @@ -217,11 +212,13 @@ function Detail({ label, value }: { label: string; value?: string }) { export function AdminPostView() { const { role, post_id } = useParams<{ role: string; post_id: string }>(); - const adminPosition = sessionStorage.getItem('adminPosition') ?? ''; - const adminType = adminPosition.startsWith('XEN') ? 'xen' : adminPosition.startsWith('AE') ? 'ae' : adminPosition.startsWith('JE') ? 'je' : ''; const navigate = useNavigate(); const [post, setPost] = useState(null); + // Admin position comes from the server (AdminGetPost response) rather than + // per-tab sessionStorage, so action gating survives new tabs / direct nav. + const [adminPosition, setAdminPosition] = useState(''); + const adminType = adminPosition.startsWith('XEN') ? 'xen' : adminPosition.startsWith('AE') ? 'ae' : adminPosition.startsWith('JE') ? 'je' : ''; const [loading, setLoading] = useState(true); const [error, setError] = useState<{ message: string; status?: number } | null>(null); @@ -247,6 +244,7 @@ export function AdminPostView() { }) .then((json: ApiResponse) => { setPost(json.post); + setAdminPosition(json.position ?? ''); if (!silent) setLoading(false); }) .catch((err: Error & { status?: number }) => { @@ -463,11 +461,12 @@ export function AdminPostView() { ) : (
    {comments.map((c) => { - const who = c.Author?.position ? c.Author.position.replace(/_/g, ' ') : `Admin #${c.author_id}`; + const who = c.role ? c.role.replace(/_/g, ' ') : 'Staff'; return (
  • {who} + {c.email && {c.email}} {formatDateTime(c.created_at)}

    {c.comment_text}

    diff --git a/app/src/pages/auth/StaffLogin.tsx b/app/src/pages/auth/StaffLogin.tsx index dd6f88a..e856f6f 100644 --- a/app/src/pages/auth/StaffLogin.tsx +++ b/app/src/pages/auth/StaffLogin.tsx @@ -41,7 +41,6 @@ export function StaffLogin() { setStatus('error'); setMessage(`Unknown position "${data.position}" — contact admin.`); } else { - sessionStorage.setItem('adminPosition', data.position ?? ''); navigate(dest); } } else { From 357ffbbe40a7134aa7d63ba80f8b23b426e5b927 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 7 Jun 2026 22:21:47 +0530 Subject: [PATCH 10/10] fix: comment box for users to add comments --- app/src/components/ComplaintCard.tsx | 78 +++++++++++++++++++++++++++- app/src/pages/profile/Profile.tsx | 9 ++-- 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/app/src/components/ComplaintCard.tsx b/app/src/components/ComplaintCard.tsx index 27b6dca..477d98a 100644 --- a/app/src/components/ComplaintCard.tsx +++ b/app/src/components/ComplaintCard.tsx @@ -3,6 +3,7 @@ import { createPortal } from 'react-dom'; import { Zap, Hammer, Trash2, Pencil, X, Check, Calendar, MapPin, BedDouble, MessageSquare, Wrench, GitBranch, ChevronRight, UserCircle, Clock, + Send, AlertCircle, } from 'lucide-react'; import { POST_PLACES } from '../constants/models'; @@ -51,6 +52,9 @@ interface ComplaintCardProps { onCancelEdit: () => void; onSaveEdit: (postId: number) => void; onDelete: (postId: number) => void; + // Called after the author successfully posts a comment, so the parent can + // refetch and surface the new comment. + onCommentPosted?: () => void; } // ── Status config ────────────────────────────────────────────────────────────── @@ -222,6 +226,74 @@ function CommentsList({ comments }: { comments: ComplaintComment[] }) { ); } +// ── Comment composer (post author only) ────────────────────────────────────────── + +function CommentComposer({ role, postId, onPosted }: { role: Role; postId: number; onPosted?: () => void }) { + const [text, setText] = useState(''); + const [submitting, setBusy] = useState(false); + const [error, setError] = useState(null); + + const disabled = submitting || !text.trim(); + + async function submit() { + const content = text.trim(); + if (!content) return; + setBusy(true); + setError(null); + try { + const res = await fetch(`/api/post/${role}/comment/${postId}`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ Content: content }), + }); + if (!res.ok) { + let msg = `Failed to post comment (${res.status})`; + try { const b = await res.json(); if (b?.error) msg = b.error; } catch {} + throw new Error(msg); + } + setText(''); + onPosted?.(); + } catch (err) { + setError((err as Error).message); + } finally { + setBusy(false); + } + } + + return ( +
    +
    +