diff --git a/README.md b/README.md
index b154216..c546102 100644
--- a/README.md
+++ b/README.md
@@ -26,4 +26,4 @@ the official web interface of the Estate Office of NIT Hamirpur to manage compla
### Complaint status public dashboard
-
+
\ No newline at end of file
diff --git a/cms-web b/cms-web
new file mode 100755
index 0000000..e75446d
Binary files /dev/null and b/cms-web differ
diff --git a/config/db.go b/config/db.go
index 2d00774..2608198 100644
--- a/config/db.go
+++ b/config/db.go
@@ -33,7 +33,11 @@ func ConnectDB() {
&models.Faculty{},
&models.Warden{},
&models.CentreHead{},
+ &models.FacultyPost{},
+ &models.WardenPost{},
+ &models.CentreHeadPost{},
+ &models.Comment{},
)
log.Println("Database connected")
-}
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 49400a0..4e4ecb7 100644
--- a/go.mod
+++ b/go.mod
@@ -17,6 +17,7 @@ require (
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
@@ -33,11 +34,14 @@ require (
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
+ github.com/stretchr/testify v1.11.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
@@ -47,4 +51,6 @@ require (
golang.org/x/sys v0.44.0 // indirect
golang.org/x/text v0.37.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ gorm.io/driver/sqlite v1.6.0 // indirect
)
diff --git a/go.sum b/go.sum
index 4784c20..106910d 100644
--- a/go.sum
+++ b/go.sum
@@ -54,6 +54,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -108,5 +110,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
+gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
+gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
diff --git a/handlers/post_handler.go b/handlers/post_handler.go
new file mode 100644
index 0000000..4dcd04d
--- /dev/null
+++ b/handlers/post_handler.go
@@ -0,0 +1,466 @@
+package handlers
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/ayush00git/cms-web/models"
+ "github.com/gin-gonic/gin"
+ "gorm.io/gorm"
+)
+
+type PostHandler struct {
+ DB *gorm.DB
+}
+
+// Let faculty members submit a new post
+func (h *PostHandler) FacultyReportPost(c *gin.Context) {
+ var input struct {
+ FacultyID uint `json:"faculty_id" binding:"required"`
+ Place models.PostPlace `json:"place" binding:"required"`
+ TypeOfPost models.PostType `json:"type_of_post" binding:"required"`
+ Title string `json:"title" binding:"required"`
+ Description string `json:"description" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ post := models.FacultyPost{
+ FacultyID: &input.FacultyID,
+ Place: input.Place,
+ TypeOfPost: input.TypeOfPost,
+ Title: input.Title,
+ Description: input.Description,
+ Status: models.StatusPendingXEN,
+ Stage: models.StageXEN,
+ }
+
+ if h.DB != nil {
+ h.DB.Create(&post)
+ }
+
+ xenEmail := "xen_civil@nit.edu"
+ if input.TypeOfPost == models.TypeElectrical {
+ xenEmail = "xen_electrical@nit.edu"
+ }
+
+ fmt.Printf("Email to %s: New Faculty Post - A new %s post was reported: %s\n", xenEmail, input.TypeOfPost, input.Title)
+
+ c.JSON(http.StatusOK, gin.H{"message": "Post submitted successfully", "post": post})
+}
+
+// Let wardens submit a post for their hostel
+func (h *PostHandler) WardenReportPost(c *gin.Context) {
+ var input struct {
+ WardenID uint `json:"warden_id" binding:"required"`
+ TypeOfPost models.PostType `json:"type_of_post" binding:"required"`
+ RoomNumber string `json:"room_number" binding:"required"`
+ Title string `json:"title" binding:"required"`
+ Description string `json:"description" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ post := models.WardenPost{
+ WardenID: &input.WardenID,
+ RoomNumber: input.RoomNumber,
+ TypeOfPost: input.TypeOfPost,
+ Title: input.Title,
+ Description: input.Description,
+ Status: models.StatusPendingXEN,
+ Stage: models.StageXEN,
+ }
+
+ if h.DB != nil {
+ h.DB.Create(&post)
+ }
+
+ xenEmail := "xen_civil@nit.edu"
+ if input.TypeOfPost == models.TypeElectrical {
+ xenEmail = "xen_electrical@nit.edu"
+ }
+
+ fmt.Printf("Email to %s: New Warden Post - A new %s post was reported for room %s: %s\n", xenEmail, input.TypeOfPost, input.RoomNumber, input.Title)
+
+ c.JSON(http.StatusOK, gin.H{"message": "Post submitted successfully", "post": post})
+}
+
+// Let Centre Heads submit posts for their department
+func (h *PostHandler) CentreHeadReportPost(c *gin.Context) {
+ var input struct {
+ CentreHeadID uint `json:"centre_head_id" binding:"required"`
+ TypeOfPost models.PostType `json:"type_of_post" binding:"required"`
+ Title string `json:"title" binding:"required"`
+ Description string `json:"description" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ post := models.CentreHeadPost{
+ CentreHeadID: &input.CentreHeadID,
+ TypeOfPost: input.TypeOfPost,
+ Title: input.Title,
+ Description: input.Description,
+ Status: models.StatusPendingXEN,
+ Stage: models.StageXEN,
+ }
+
+ if h.DB != nil {
+ h.DB.Create(&post)
+ }
+
+ xenEmail := "xen_civil@nit.edu"
+ if input.TypeOfPost == models.TypeElectrical {
+ xenEmail = "xen_electrical@nit.edu"
+ }
+
+ fmt.Printf("Email to %s: New Centre Head Post - A new %s post was reported by Centre Head: %s\n", xenEmail, input.TypeOfPost, input.Title)
+
+ c.JSON(http.StatusOK, gin.H{"message": "Post submitted successfully", "post": post})
+}
+
+// Allow XEN to review the post and either pass it forward or reject it
+func (h *PostHandler) XENUpdateStatus(c *gin.Context) {
+ var input struct {
+ Source string `json:"source" binding:"required"` // "Faculty", "Warden", "CentreHead"
+ PostID uint `json:"post_id" binding:"required"`
+ Action string `json:"action" binding:"required"` // "pass" or "reject"
+ CommentText string `json:"comment" binding:"required"`
+ AdminID uint `json:"admin_id" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ if h.DB == nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection missing"})
+ return
+ }
+
+ var typeOfPost models.PostType
+ var stage models.PostStage
+ commentableType := input.Source + "Post"
+
+ // Fetch and update based on source
+ if input.Source == string(models.SourceFaculty) {
+ var post models.FacultyPost
+ if err := h.DB.First(&post, input.PostID).Error; err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
+ return
+ }
+ typeOfPost = post.TypeOfPost
+ stage = post.Stage
+ if stage != models.StageXEN {
+ c.JSON(http.StatusForbidden, gin.H{"error": "Post is not at XEN stage"})
+ return
+ }
+ if input.Action == "pass" {
+ post.Status = models.StatusPendingAE
+ post.Stage = models.StageAE
+ } else if input.Action == "reject" {
+ post.Status = models.StatusRejected
+ }
+ h.DB.Save(&post)
+
+ } else if input.Source == string(models.SourceWarden) {
+ var post models.WardenPost
+ if err := h.DB.First(&post, input.PostID).Error; err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
+ return
+ }
+ typeOfPost = post.TypeOfPost
+ stage = post.Stage
+ if stage != models.StageXEN {
+ c.JSON(http.StatusForbidden, gin.H{"error": "Post is not at XEN stage"})
+ return
+ }
+ if input.Action == "pass" {
+ post.Status = models.StatusPendingAE
+ post.Stage = models.StageAE
+ } else if input.Action == "reject" {
+ post.Status = models.StatusRejected
+ }
+ h.DB.Save(&post)
+
+ } else if input.Source == string(models.SourceCentreHead) {
+ var post models.CentreHeadPost
+ if err := h.DB.First(&post, input.PostID).Error; err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
+ return
+ }
+ typeOfPost = post.TypeOfPost
+ stage = post.Stage
+ if stage != models.StageXEN {
+ c.JSON(http.StatusForbidden, gin.H{"error": "Post is not at XEN stage"})
+ return
+ }
+ if input.Action == "pass" {
+ post.Status = models.StatusPendingAE
+ post.Stage = models.StageAE
+ } else if input.Action == "reject" {
+ post.Status = models.StatusRejected
+ }
+ h.DB.Save(&post)
+ } else {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid source"})
+ return
+ }
+
+ // Create the comment
+ comment := models.Comment{
+ CommentableID: input.PostID,
+ CommentableType: commentableType,
+ AdminID: input.AdminID,
+ CommentText: input.CommentText,
+ }
+ h.DB.Create(&comment)
+
+ if input.Action == "pass" {
+ aeEmail := "ae_civil@nit.edu"
+ if typeOfPost == models.TypeElectrical {
+ aeEmail = "ae_electrical@nit.edu"
+ }
+ fmt.Printf("Email to %s: Post Forwarded to AE - A post has been forwarded to you for action.\n", aeEmail)
+ } else if input.Action == "reject" {
+ fmt.Println("Email to filer@nit.edu: Post Rejected - Your post has been rejected by XEN.")
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Status updated by XEN"})
+}
+
+// Allow AE to review the post, and if passing, assign a specific JE
+func (h *PostHandler) AEUpdateStatus(c *gin.Context) {
+ var input struct {
+ Source string `json:"source" binding:"required"`
+ PostID uint `json:"post_id" binding:"required"`
+ Action string `json:"action" binding:"required"` // "pass" or "reject"
+ CommentText string `json:"comment" binding:"required"`
+ SelectJE_ID *uint `json:"select_je_id"` // required if pass
+ AdminID uint `json:"admin_id" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ if h.DB == nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection missing"})
+ return
+ }
+
+ var stage models.PostStage
+ commentableType := input.Source + "Post"
+
+ // Fetch and update based on source
+ if input.Source == string(models.SourceFaculty) {
+ var post models.FacultyPost
+ if err := h.DB.First(&post, input.PostID).Error; err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
+ return
+ }
+ stage = post.Stage
+ if stage != models.StageAE {
+ c.JSON(http.StatusForbidden, gin.H{"error": "Post is not at AE stage"})
+ return
+ }
+ if input.Action == "pass" && input.SelectJE_ID != nil {
+ post.Status = models.StatusPendingJE
+ post.Stage = models.StageJE
+ post.AssignedJE_ID = input.SelectJE_ID
+ } else if input.Action == "reject" {
+ post.Status = models.StatusRejected
+ }
+ h.DB.Save(&post)
+
+ } else if input.Source == string(models.SourceWarden) {
+ var post models.WardenPost
+ if err := h.DB.First(&post, input.PostID).Error; err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
+ return
+ }
+ stage = post.Stage
+ if stage != models.StageAE {
+ c.JSON(http.StatusForbidden, gin.H{"error": "Post is not at AE stage"})
+ return
+ }
+ if input.Action == "pass" && input.SelectJE_ID != nil {
+ post.Status = models.StatusPendingJE
+ post.Stage = models.StageJE
+ post.AssignedJE_ID = input.SelectJE_ID
+ } else if input.Action == "reject" {
+ post.Status = models.StatusRejected
+ }
+ h.DB.Save(&post)
+
+ } else if input.Source == string(models.SourceCentreHead) {
+ var post models.CentreHeadPost
+ if err := h.DB.First(&post, input.PostID).Error; err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
+ return
+ }
+ stage = post.Stage
+ if stage != models.StageAE {
+ c.JSON(http.StatusForbidden, gin.H{"error": "Post is not at AE stage"})
+ return
+ }
+ if input.Action == "pass" && input.SelectJE_ID != nil {
+ post.Status = models.StatusPendingJE
+ post.Stage = models.StageJE
+ post.AssignedJE_ID = input.SelectJE_ID
+ } else if input.Action == "reject" {
+ post.Status = models.StatusRejected
+ }
+ h.DB.Save(&post)
+ } else {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid source"})
+ return
+ }
+
+ // Create the comment
+ comment := models.Comment{
+ CommentableID: input.PostID,
+ CommentableType: commentableType,
+ AdminID: input.AdminID,
+ CommentText: input.CommentText,
+ }
+ h.DB.Create(&comment)
+
+ if input.Action == "pass" && input.SelectJE_ID != nil {
+ fmt.Println("Email to je@nit.edu: Post Assigned to JE - A new post has been assigned to you by AE.")
+ } else if input.Action == "reject" {
+ fmt.Println("Email to filer@nit.edu: Post Rejected - Your post has been rejected by AE.")
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Status updated by AE"})
+}
+
+// Allow JE to mark the post as finally resolved or reject it
+func (h *PostHandler) JEUpdateStatus(c *gin.Context) {
+ var input struct {
+ Source string `json:"source" binding:"required"`
+ PostID uint `json:"post_id" binding:"required"`
+ Action string `json:"action" binding:"required"` // "resolve" or "reject"
+ CommentText string `json:"comment" binding:"required"`
+ AdminID uint `json:"admin_id" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ if h.DB == nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection missing"})
+ return
+ }
+
+ var stage models.PostStage
+ commentableType := input.Source + "Post"
+
+ // Fetch and update based on source
+ if input.Source == string(models.SourceFaculty) {
+ var post models.FacultyPost
+ if err := h.DB.First(&post, input.PostID).Error; err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
+ return
+ }
+ stage = post.Stage
+ if stage != models.StageJE {
+ c.JSON(http.StatusForbidden, gin.H{"error": "Post is not at JE stage"})
+ return
+ }
+ if input.Action == "resolve" {
+ post.Status = models.StatusResolved
+ } else if input.Action == "reject" {
+ post.Status = models.StatusRejected
+ }
+ h.DB.Save(&post)
+
+ } else if input.Source == string(models.SourceWarden) {
+ var post models.WardenPost
+ if err := h.DB.First(&post, input.PostID).Error; err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
+ return
+ }
+ stage = post.Stage
+ if stage != models.StageJE {
+ c.JSON(http.StatusForbidden, gin.H{"error": "Post is not at JE stage"})
+ return
+ }
+ if input.Action == "resolve" {
+ post.Status = models.StatusResolved
+ } else if input.Action == "reject" {
+ post.Status = models.StatusRejected
+ }
+ h.DB.Save(&post)
+
+ } else if input.Source == string(models.SourceCentreHead) {
+ var post models.CentreHeadPost
+ if err := h.DB.First(&post, input.PostID).Error; err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
+ return
+ }
+ stage = post.Stage
+ if stage != models.StageJE {
+ c.JSON(http.StatusForbidden, gin.H{"error": "Post is not at JE stage"})
+ return
+ }
+ if input.Action == "resolve" {
+ post.Status = models.StatusResolved
+ } else if input.Action == "reject" {
+ post.Status = models.StatusRejected
+ }
+ h.DB.Save(&post)
+ } else {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid source"})
+ return
+ }
+
+ // Create the comment
+ comment := models.Comment{
+ CommentableID: input.PostID,
+ CommentableType: commentableType,
+ AdminID: input.AdminID,
+ CommentText: input.CommentText,
+ }
+ h.DB.Create(&comment)
+
+ if input.Action == "resolve" {
+ fmt.Println("Email to filer@nit.edu: Post Resolved - Your post has been successfully resolved.")
+ } else if input.Action == "reject" {
+ fmt.Println("Email to filer@nit.edu: Post Rejected - Your post has been rejected by JE.")
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Status updated by JE"})
+}
+
+// Fetch all posts and their comments to show on the public dashboard
+func (h *PostHandler) GetPublicDashboard(c *gin.Context) {
+ var facultyPosts []models.FacultyPost
+ var wardenPosts []models.WardenPost
+ var centreHeadPosts []models.CentreHeadPost
+
+ if h.DB != nil {
+ h.DB.Preload("Comments").Find(&facultyPosts)
+ h.DB.Preload("Comments").Find(&wardenPosts)
+ h.DB.Preload("Comments").Find(¢reHeadPosts)
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "faculty_posts": facultyPosts,
+ "warden_posts": wardenPosts,
+ "centre_head_posts": centreHeadPosts,
+ })
+}
diff --git a/helpers/env.go b/helpers/env.go
index 38c925f..2b82ec8 100644
--- a/helpers/env.go
+++ b/helpers/env.go
@@ -3,18 +3,12 @@ package helpers
import (
"log"
"os"
- "github.com/joho/godotenv"
)
-func GetEnv(target string) (string) {
- err := godotenv.Load()
- if err != nil {
- log.Fatal("Error while loading the environment variables")
- }
-
+func GetEnv(target string) string {
value := os.Getenv(target)
if value == "" {
- log.Fatalf("No value for %s exists in environment variables", value)
+ log.Fatalf("No value for %s exists in environment variables", target)
}
return value
}
diff --git a/helpers/jwt.go b/helpers/jwt.go
index 4efcad6..ac4b487 100644
--- a/helpers/jwt.go
+++ b/helpers/jwt.go
@@ -7,11 +7,8 @@ import (
"github.com/golang-jwt/jwt/v5"
)
-var secret = GetEnv("JWT_SECRET")
-// signing key is passed as slice of bytes
-var secretKey = []byte(secret)
-
func GenerateToken(email string) (string, error) {
+ secretKey := []byte(GetEnv("JWT_SECRET"))
// define the algorithm to sign the header and payload with
token := jwt.NewWithClaims(jwt.SigningMethodHS256,
@@ -30,6 +27,8 @@ func GenerateToken(email string) (string, error) {
}
func VerifyToken(tokenString string) (error) {
+ secretKey := []byte(GetEnv("JWT_SECRET"))
+
// parsing the jwt string
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error){
return secretKey, nil
diff --git a/main.go b/main.go
index e069657..6aab21a 100644
--- a/main.go
+++ b/main.go
@@ -6,13 +6,16 @@ import (
"github.com/ayush00git/cms-web/config"
"github.com/ayush00git/cms-web/handlers"
"github.com/ayush00git/cms-web/routes"
-
"github.com/gin-gonic/gin"
+ "github.com/joho/godotenv"
)
func main() {
-
- config.ConnectDB()
+ err := godotenv.Load()
+ if err != nil {
+ log.Fatal("Error while loading the environment variables")
+ }
+ config.ConnectDB()
r := gin.Default()
@@ -26,6 +29,13 @@ func main() {
routes.AuthRoute(r, authHandler)
+ postHandler := &handlers.PostHandler{
+ DB: config.DB,
+ }
+
+ // Setup application routes
+ routes.PostRoute(r, postHandler)
+
r.Run(":8080")
fmt.Println("Sevrer running on port 8080")
}
diff --git a/models/admin_auth.go b/models/admin_auth.go
index 7d4f183..958a738 100644
--- a/models/admin_auth.go
+++ b/models/admin_auth.go
@@ -4,21 +4,21 @@ import (
"time"
)
-type PostType string
+type PositionType string
const (
- TypeXENCivil PostType = "XEN_Civil"
- TypeAECivil PostType = "AE_Civil"
- TypeJECivil PostType = "JE_Civil"
- TypeXENElectrical PostType = "XEN_Electrical"
- TypeAEElectrical PostType = "AE_Electrical"
- TypeJEElectrical PostType = "JE_Electrical"
+ TypeXENCivil PositionType = "XEN_Civil"
+ TypeAECivil PositionType = "AE_Civil"
+ TypeJECivil PositionType = "JE_Civil"
+ TypeXENElectrical PositionType = "XEN_Electrical"
+ TypeAEElectrical PositionType = "AE_Electrical"
+ TypeJEElectrical PositionType = "JE_Electrical"
)
type Admin struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Email string `gorm:"uniqueIndex;not null" json:"email"`
Password string `gorm:"not null" json:"password"`
- Post PostType `gorm:"type:varchar(15);unique;not null" json:"post"`
+ Post PositionType `gorm:"type:varchar(15);unique;not null" json:"post"`
IsVerified bool `gorm:"default:false" json:"is_verified"`
CreatedAt time.Time `json:"created_at"`
}
diff --git a/models/post.go b/models/post.go
new file mode 100644
index 0000000..d8db113
--- /dev/null
+++ b/models/post.go
@@ -0,0 +1,101 @@
+package models
+
+import (
+ "time"
+)
+
+type PostSource string
+
+const (
+ SourceFaculty PostSource = "Faculty"
+ SourceWarden PostSource = "Warden"
+ SourceCentreHead PostSource = "CentreHead"
+)
+
+type PostPlace string
+
+const (
+ PlaceResidential PostPlace = "Residential"
+ PlaceDepartmental PostPlace = "Departmental"
+)
+
+type PostType string
+
+const (
+ TypeCivil PostType = "Civil"
+ TypeElectrical PostType = "Electrical"
+)
+
+type PostStatus string
+
+const (
+ StatusPendingXEN PostStatus = "Pending_XEN"
+ StatusPendingAE PostStatus = "Pending_AE"
+ StatusPendingJE PostStatus = "Pending_JE"
+ StatusResolved PostStatus = "Resolved"
+ StatusRejected PostStatus = "Rejected"
+)
+
+type PostStage string
+
+const (
+ StageXEN PostStage = "XEN"
+ StageAE PostStage = "AE"
+ StageJE PostStage = "JE"
+)
+
+type FacultyPost struct {
+ ID uint `gorm:"primaryKey;autoIncrement"`
+ FacultyID *uint // Link to Faculty
+ Place PostPlace `gorm:"type:varchar(20)"`
+ TypeOfPost PostType `gorm:"type:varchar(20);not null"`
+ Title string `gorm:"not null"`
+ Description string `gorm:"type:text;not null"`
+ Status PostStatus `gorm:"type:varchar(20);not null;default:'Pending_XEN'"`
+ Stage PostStage `gorm:"type:varchar(20);not null;default:'XEN'"`
+ AssignedJE_ID *uint // Populated when AE delegates to JE
+ CreatedAt time.Time
+ UpdatedAt time.Time
+
+ Comments []Comment `gorm:"polymorphic:Commentable;"`
+}
+
+type WardenPost struct {
+ ID uint `gorm:"primaryKey;autoIncrement"`
+ WardenID *uint // Link to Warden
+ RoomNumber string `gorm:"type:varchar(50)"`
+ TypeOfPost PostType `gorm:"type:varchar(20);not null"`
+ Title string `gorm:"not null"`
+ Description string `gorm:"type:text;not null"`
+ Status PostStatus `gorm:"type:varchar(20);not null;default:'Pending_XEN'"`
+ Stage PostStage `gorm:"type:varchar(20);not null;default:'XEN'"`
+ AssignedJE_ID *uint // Populated when AE delegates to JE
+ CreatedAt time.Time
+ UpdatedAt time.Time
+
+ Comments []Comment `gorm:"polymorphic:Commentable;"`
+}
+
+type CentreHeadPost struct {
+ ID uint `gorm:"primaryKey;autoIncrement"`
+ CentreHeadID *uint // Link to CentreHead
+ TypeOfPost PostType `gorm:"type:varchar(20);not null"`
+ Title string `gorm:"not null"`
+ Description string `gorm:"type:text;not null"`
+ Status PostStatus `gorm:"type:varchar(20);not null;default:'Pending_XEN'"`
+ Stage PostStage `gorm:"type:varchar(20);not null;default:'XEN'"`
+ AssignedJE_ID *uint // Populated when AE delegates to JE
+ CreatedAt time.Time
+ UpdatedAt time.Time
+
+ Comments []Comment `gorm:"polymorphic:Commentable;"`
+}
+
+type Comment struct {
+ ID uint `gorm:"primaryKey;autoIncrement"`
+ CommentableID uint `gorm:"not null"`
+ CommentableType string `gorm:"type:varchar(50);not null"`
+ AdminID uint `gorm:"not null"`
+ CommentText string `gorm:"type:text;not null"`
+ CreatedAt time.Time
+}
diff --git a/routes/post_routes.go b/routes/post_routes.go
new file mode 100644
index 0000000..3301469
--- /dev/null
+++ b/routes/post_routes.go
@@ -0,0 +1,26 @@
+package routes
+
+import (
+ "github.com/ayush00git/cms-web/handlers"
+ "github.com/gin-gonic/gin"
+)
+
+// PostRoute initializes all post-related endpoints
+func PostRoute(router *gin.Engine, h *handlers.PostHandler) {
+
+ // Faculty, Warden & Centre Head reporting routes
+ router.POST("/faculty/post/report", h.FacultyReportPost)
+ router.POST("/warden/post/report", h.WardenReportPost)
+ router.POST("/centrehead/post/report", h.CentreHeadReportPost)
+
+ // Admin update routes
+ admin := router.Group("/admin")
+ {
+ admin.POST("/xen/post/status", h.XENUpdateStatus)
+ admin.POST("/ae/post/status", h.AEUpdateStatus)
+ admin.POST("/je/post/status", h.JEUpdateStatus)
+ }
+
+ // Public dashboard
+ router.GET("/posts/public", h.GetPublicDashboard)
+}
diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh
new file mode 100755
index 0000000..dc531ae
--- /dev/null
+++ b/scripts/run_tests.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+# Ensure Go is in PATH
+export PATH=$PATH:/usr/local/go/bin
+
+# Set environment variables for testing
+export JWT_SECRET=testsecret
+export DB_USER=test
+export DB_NAME=test
+export DB_PASS=test
+echo "--------------------------------------"
+
+# Run tests
+go test ./tests -v
+
+if [ $? -eq 0 ]; then
+ echo "--------------------------------------"
+ echo "SUCCESS: All features are working correctly."
+else
+ echo "--------------------------------------"
+ echo "FAILURE: Some features failed the functionality check."
+ exit 1
+fi
diff --git a/server.log b/server.log
new file mode 100644
index 0000000..a565ede
--- /dev/null
+++ b/server.log
@@ -0,0 +1,55 @@
+nohup: ignoring input
+2026/05/12 02:55:47 Database connected and migrated
+[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
+
+[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
+ - using env: export GIN_MODE=release
+ - using code: gin.SetMode(gin.ReleaseMode)
+
+[GIN-debug] GET /health --> main.main.func1 (3 handlers)
+[GIN-debug] POST /faculty/complaint/report --> github.com/ayush00git/cms-web/controllers.FacultyReportComplaint (3 handlers)
+[GIN-debug] POST /warden/complaint/report --> github.com/ayush00git/cms-web/controllers.WardenReportComplaint (3 handlers)
+[GIN-debug] POST /centrehead/complaint/report --> github.com/ayush00git/cms-web/controllers.CentreHeadReportComplaint (3 handlers)
+[GIN-debug] POST /admin/xen/complaint/status --> github.com/ayush00git/cms-web/controllers.XENUpdateStatus (3 handlers)
+[GIN-debug] POST /admin/ae/complaint/status --> github.com/ayush00git/cms-web/controllers.AEUpdateStatus (3 handlers)
+[GIN-debug] POST /admin/je/complaint/status --> github.com/ayush00git/cms-web/controllers.JEUpdateStatus (3 handlers)
+[GIN-debug] GET /complaints/public --> github.com/ayush00git/cms-web/controllers.GetPublicDashboard (3 handlers)
+[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
+Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.
+[GIN-debug] Listening and serving HTTP on :8080
+======================================
+MOCK EMAIL DISPATCH
+To: xen_civil@nit.edu
+Subject: New Faculty Complaint
+Body: A new Civil complaint was reported: Leaking pipe
+======================================
+[GIN] 2026/05/12 - 02:55:58 | 200 | 36.69ms | ::1 | POST "/faculty/complaint/report"
+======================================
+MOCK EMAIL DISPATCH
+To: ae_civil@nit.edu
+Subject: Complaint Forwarded to AE
+Body: A complaint has been forwarded to you for action.
+======================================
+[GIN] 2026/05/12 - 02:56:08 | 200 | 73.26ms | ::1 | POST "/admin/xen/complaint/status"
+======================================
+MOCK EMAIL DISPATCH
+To: je@nit.edu
+Subject: Complaint Assigned to JE
+Body: A new complaint has been assigned to you by AE.
+======================================
+[GIN] 2026/05/12 - 02:56:26 | 200 | 37.73ms | ::1 | POST "/admin/ae/complaint/status"
+======================================
+MOCK EMAIL DISPATCH
+To: filer@nit.edu
+Subject: Complaint Resolved
+Body: Your complaint has been successfully resolved.
+======================================
+[GIN] 2026/05/12 - 02:56:36 | 200 | 38.08ms | ::1 | POST "/admin/je/complaint/status"
+[GIN] 2026/05/12 - 02:56:46 | 200 | 2.69ms | ::1 | GET "/complaints/public"
+======================================
+MOCK EMAIL DISPATCH
+To: xen_electrical@nit.edu
+Subject: New Centre Head Complaint
+Body: A new Electrical complaint was reported by Centre Head: Power outage
+======================================
+[GIN] 2026/05/12 - 02:56:53 | 200 | 31.86ms | ::1 | POST "/centrehead/complaint/report"
diff --git a/tests/auth_test.go b/tests/auth_test.go
new file mode 100644
index 0000000..e123932
--- /dev/null
+++ b/tests/auth_test.go
@@ -0,0 +1,101 @@
+package tests
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/ayush00git/cms-web/models"
+ "github.com/stretchr/testify/assert"
+ "golang.org/x/crypto/bcrypt"
+)
+
+func TestAuthFlows(t *testing.T) {
+ db := SetupTestDB()
+ router := SetupTestRouter(db)
+
+ t.Run("Faculty Signup and Login", func(t *testing.T) {
+ // Signup
+ signupData := map[string]string{
+ "name": "Dr. Smith",
+ "email": "smith@nit.edu",
+ "password": "password123",
+ "department": "CSE",
+ "house_number": "101",
+ "block": "A",
+ "type": "Type-1",
+ "phone_number": "1234567890",
+ }
+ body, _ := json.Marshal(signupData)
+ req, _ := http.NewRequest("POST", "/api/auth/faculty/signup", bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+ assert.Equal(t, http.StatusCreated, w.Code)
+
+ // Login
+ loginData := map[string]string{
+ "email": "smith@nit.edu",
+ "password": "password123",
+ }
+ body, _ = json.Marshal(loginData)
+ req, _ = http.NewRequest("POST", "/api/auth/faculty/login", bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ w = httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+ assert.Equal(t, http.StatusOK, w.Code)
+ assert.Contains(t, w.Header().Get("Set-Cookie"), "token=")
+ })
+
+ t.Run("Warden Signup and Login", func(t *testing.T) {
+ signupData := map[string]string{
+ "email": "warden@nit.edu",
+ "password": "warden123",
+ "hostel": "Kailash",
+ "phone_number": "0987654321",
+ }
+ body, _ := json.Marshal(signupData)
+ req, _ := http.NewRequest("POST", "/api/auth/warden/signup", bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+ assert.Equal(t, http.StatusCreated, w.Code)
+
+ loginData := map[string]string{
+ "email": "warden@nit.edu",
+ "password": "warden123",
+ }
+ body, _ = json.Marshal(loginData)
+ req, _ = http.NewRequest("POST", "/api/auth/warden/login", bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ w = httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+ assert.Equal(t, http.StatusOK, w.Code)
+ })
+
+ t.Run("Admin Login", func(t *testing.T) {
+ hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("password"), 10)
+ admin := models.Admin{
+ Email: "admin@nit.edu",
+ Password: string(hashedPassword),
+ Post: "XEN_Civil",
+ IsVerified: true,
+ }
+ db.Create(&admin)
+
+ loginData := map[string]string{
+ "email": "admin@nit.edu",
+ "password": "password",
+ }
+ body, _ := json.Marshal(loginData)
+ req, _ := http.NewRequest("POST", "/api/auth/admin/login", bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusOK, w.Code)
+ assert.Contains(t, w.Header().Get("Set-Cookie"), "token=")
+ })
+}
diff --git a/tests/post_test.go b/tests/post_test.go
new file mode 100644
index 0000000..b9c23f8
--- /dev/null
+++ b/tests/post_test.go
@@ -0,0 +1,120 @@
+package tests
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/ayush00git/cms-web/models"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPostWorkflow(t *testing.T) {
+ db := SetupTestDB()
+ router := SetupTestRouter(db)
+
+ var facultyPostID uint
+
+ t.Run("Faculty Report Post", func(t *testing.T) {
+ postData := map[string]interface{}{
+ "faculty_id": 1,
+ "place": "Departmental",
+ "type_of_post": "Civil",
+ "title": "Leaking Pipe",
+ "description": "The pipe in CSE lab is leaking.",
+ }
+ body, _ := json.Marshal(postData)
+ req, _ := http.NewRequest("POST", "/faculty/post/report", bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusOK, w.Code)
+
+ var resp map[string]interface{}
+ json.Unmarshal(w.Body.Bytes(), &resp)
+ post := resp["post"].(map[string]interface{})
+ facultyPostID = uint(post["ID"].(float64))
+ })
+
+ t.Run("XEN Pass Post to AE", func(t *testing.T) {
+ updateData := map[string]interface{}{
+ "source": "Faculty",
+ "post_id": facultyPostID,
+ "action": "pass",
+ "comment": "Forwarding to AE for technical review",
+ "admin_id": 1,
+ }
+ body, _ := json.Marshal(updateData)
+ req, _ := http.NewRequest("POST", "/admin/xen/post/status", bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusOK, w.Code)
+
+ var post models.FacultyPost
+ db.First(&post, facultyPostID)
+ assert.Equal(t, models.StatusPendingAE, post.Status)
+ assert.Equal(t, models.StageAE, post.Stage)
+ })
+
+ t.Run("AE Pass Post to JE", func(t *testing.T) {
+ updateData := map[string]interface{}{
+ "source": "Faculty",
+ "post_id": facultyPostID,
+ "action": "pass",
+ "comment": "Assigning to JE Sharma",
+ "select_je_id": 10,
+ "admin_id": 2,
+ }
+ body, _ := json.Marshal(updateData)
+ req, _ := http.NewRequest("POST", "/admin/ae/post/status", bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusOK, w.Code)
+
+ var post models.FacultyPost
+ db.First(&post, facultyPostID)
+ assert.Equal(t, models.StatusPendingJE, post.Status)
+ assert.Equal(t, models.StageJE, post.Stage)
+ assert.Equal(t, uint(10), *post.AssignedJE_ID)
+ })
+
+ t.Run("JE Resolve Post", func(t *testing.T) {
+ updateData := map[string]interface{}{
+ "source": "Faculty",
+ "post_id": facultyPostID,
+ "action": "resolve",
+ "comment": "Fixed the leak.",
+ "admin_id": 10,
+ }
+ body, _ := json.Marshal(updateData)
+ req, _ := http.NewRequest("POST", "/admin/je/post/status", bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusOK, w.Code)
+
+ var post models.FacultyPost
+ db.First(&post, facultyPostID)
+ assert.Equal(t, models.StatusResolved, post.Status)
+ })
+
+ t.Run("Public Dashboard", func(t *testing.T) {
+ req, _ := http.NewRequest("GET", "/posts/public", nil)
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusOK, w.Code)
+
+ var resp map[string]interface{}
+ json.Unmarshal(w.Body.Bytes(), &resp)
+ assert.NotNil(t, resp["faculty_posts"])
+ })
+}
diff --git a/tests/test_helper.go b/tests/test_helper.go
new file mode 100644
index 0000000..2e17b9a
--- /dev/null
+++ b/tests/test_helper.go
@@ -0,0 +1,43 @@
+package tests
+
+import (
+ "github.com/ayush00git/cms-web/handlers"
+ "github.com/ayush00git/cms-web/models"
+ "github.com/ayush00git/cms-web/routes"
+ "github.com/gin-gonic/gin"
+ "gorm.io/driver/sqlite"
+ "gorm.io/gorm"
+)
+
+func SetupTestDB() *gorm.DB {
+ db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
+ if err != nil {
+ panic("failed to connect database")
+ }
+
+ db.AutoMigrate(
+ &models.Admin{},
+ &models.Faculty{},
+ &models.Warden{},
+ &models.CentreHead{},
+ &models.FacultyPost{},
+ &models.WardenPost{},
+ &models.CentreHeadPost{},
+ &models.Comment{},
+ )
+
+ return db
+}
+
+func SetupTestRouter(db *gorm.DB) *gin.Engine {
+ gin.SetMode(gin.TestMode)
+ r := gin.Default()
+
+ authHandler := &handlers.AuthHandler{DB: db}
+ postHandler := &handlers.PostHandler{DB: db}
+
+ routes.AuthRoute(r, authHandler)
+ routes.PostRoute(r, postHandler)
+
+ return r
+}