diff --git a/api/middleware/auth/paseto/paseto.go b/api/middleware/auth/paseto/paseto.go index 59c2b1fd..e449155b 100644 --- a/api/middleware/auth/paseto/paseto.go +++ b/api/middleware/auth/paseto/paseto.go @@ -108,8 +108,9 @@ func PASETO(authOptional bool) func(*gin.Context) { c.AbortWithStatus(http.StatusInternalServerError) return } + c.Set(CTX_WALLET_ADDRES, userFetch.WalletAddress) - c.Set(CTX_USER_ID, cc.UserId) + c.Set(CTX_USER_ID, userFetch.UserId) c.Next() } } diff --git a/api/v1/report/create.go b/api/v1/report/create.go new file mode 100644 index 00000000..e13a6d58 --- /dev/null +++ b/api/v1/report/create.go @@ -0,0 +1,66 @@ +package report + +import ( + "fmt" + "net/http" + "time" + + "github.com/NetSepio/gateway/api/middleware/auth/paseto" + "github.com/NetSepio/gateway/config/dbconfig" + "github.com/NetSepio/gateway/models" + "github.com/NetSepio/gateway/util/pkg/logwrapper" + "github.com/TheLazarusNetwork/go-helpers/httpo" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "gorm.io/gorm" +) + +func postReport(c *gin.Context) { + var request ReportRequest + if err := c.BindJSON(&request); err != nil { + httpo.NewErrorResponse(http.StatusBadRequest, fmt.Sprintf("Invalid request body: %s", err)).SendD(c) + return + } + + db := dbconfig.GetDb() + userId := c.GetString(paseto.CTX_USER_ID) // Get user ID from context + newReport := models.Report{ + ID: uuid.NewString(), + Title: request.Title, + Description: request.Description, + Document: request.Document, + ProjectName: request.ProjectName, + ProjectDomain: request.ProjectDomain, + CreatedBy: userId, + EndTime: time.Now().Add(time.Second * 300), + } + err := db.Transaction(func(tx *gorm.DB) error { + if err := tx.Create(&newReport).Error; err != nil { + return fmt.Errorf("failed to insert report: %w", err) + } + + // Insert tags + for _, tag := range request.Tags { + if err := tx.Create(&models.ReportTag{ReportID: newReport.ID, Tag: tag}).Error; err != nil { + return fmt.Errorf("failed to insert tag for report: %w", err) + } + } + + // Insert images + for _, imageURL := range request.Images { + if err := tx.Create(&models.ReportImage{ReportID: newReport.ID, ImageURL: imageURL}).Error; err != nil { + return fmt.Errorf("failed to insert image for report: %w", err) + } + } + + return nil + }) + + if err != nil { + logwrapper.Errorf("failed to create report: %s", err) + httpo.NewErrorResponse(http.StatusInternalServerError, "Failed to create report").SendD(c) + return + } + + httpo.NewSuccessResponseP(200, "Report created successfully", newReport).SendD(c) +} diff --git a/api/v1/report/query.go b/api/v1/report/query.go new file mode 100644 index 00000000..75c8498c --- /dev/null +++ b/api/v1/report/query.go @@ -0,0 +1,70 @@ +package report + +import ( + "net/http" + "time" + + "github.com/NetSepio/gateway/api/middleware/auth/paseto" + "github.com/NetSepio/gateway/config/dbconfig" + "github.com/NetSepio/gateway/models" + "github.com/TheLazarusNetwork/go-helpers/httpo" + "github.com/gin-gonic/gin" +) + +// getReports fetches reports with optional filters +func getReports(c *gin.Context) { + var filter ReportFilter + if err := c.ShouldBindQuery(&filter); err != nil { + httpo.NewErrorResponse(http.StatusBadRequest, "Invalid query parameters").SendD(c) + return + } + + db := dbconfig.GetDb() + userId := c.GetString(paseto.CTX_USER_ID) + + // Query with vote counts + query := db.Debug().Model(&models.Report{}). + Select(`reports.*, + COUNT(DISTINCT CASE WHEN report_votes.vote_type = 'upvote' THEN report_votes.voter_id END) as upvotes, + COUNT(DISTINCT CASE WHEN report_votes.vote_type = 'downvote' THEN report_votes.voter_id END) as downvotes, + COUNT(DISTINCT CASE WHEN report_votes.vote_type = 'notsure' THEN report_votes.voter_id END) as notSure, + COUNT(DISTINCT report_votes.voter_id) as totalVotes, + reports.end_time, + (SELECT vote_type FROM report_votes WHERE report_id = reports.id AND voter_id = ?) as user_vote`, userId). + Joins("LEFT JOIN report_votes ON report_votes.report_id = reports.id"). + Group("reports.id") + + // Apply filters + if filter.Title != "" { + query = query.Where("title ILIKE ?", "%"+filter.Title+"%") + } + if filter.ProjectDomain != "" { + query = query.Where("project_domain = ?", filter.ProjectDomain) + } + if filter.ProjectName != "" { + query = query.Where("project_name = ?", filter.ProjectName) + } + + // Execute query + var reportsWithVotes []ReportWithVotes + if err := query.Find(&reportsWithVotes).Error; err != nil { + httpo.NewErrorResponse(http.StatusInternalServerError, "Failed to fetch reports").SendD(c) + return + } + + // Calculate status for each report + for i := range reportsWithVotes { + report := &reportsWithVotes[i] + if time.Now().After(report.EndTime) { + if float64(report.Upvotes)/float64(report.TotalVotes) >= 0.51 { + report.Status = "accepted" + } else { + report.Status = "rejected" + } + } else { + report.Status = "running" + } + } + + httpo.NewSuccessResponseP(http.StatusOK, "Reports fetched successfully", reportsWithVotes).SendD(c) +} diff --git a/api/v1/report/report.go b/api/v1/report/report.go new file mode 100644 index 00000000..0a0ec66e --- /dev/null +++ b/api/v1/report/report.go @@ -0,0 +1,17 @@ +package report + +import ( + "github.com/NetSepio/gateway/api/middleware/auth/paseto" + "github.com/gin-gonic/gin" +) + +func ApplyRoutes(r *gin.RouterGroup) { + report := r.Group("/report") + { + report.Use(paseto.PASETO(false)) + + report.POST("/", postReport) // Endpoint for creating a new report + report.GET("/", getReports) // Endpoint for retrieving reports with optional filters + report.POST("/vote", postReportVote) // Endpoint for voting on a report + } +} diff --git a/api/v1/report/types.go b/api/v1/report/types.go new file mode 100644 index 00000000..69881890 --- /dev/null +++ b/api/v1/report/types.go @@ -0,0 +1,45 @@ +package report + +import ( + "time" + + "github.com/google/uuid" +) + +// ReportRequest defines the structure for report creation request +type ReportRequest struct { + Title string `json:"title" binding:"required"` + Description string `json:"description"` + Images []string `json:"image"` + Document string `json:"document"` + Category string `json:"category"` + Tags []string `json:"tags"` + ProjectName string `json:"projectName"` + ProjectDomain string `json:"projectDomain"` +} + +// ReportFilter for query parameters +type ReportFilter struct { + Title string `form:"title"` + ProjectDomain string `form:"projectDomain"` + ProjectName string `form:"projectName"` + Accepted *bool `form:"accepted"` +} + +// ReportWithVotes and calculated status +type ReportWithVotes struct { + ID uuid.UUID `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Document string `json:"document"` + ProjectName string `json:"projectName"` + ProjectDomain string `json:"projectDomain"` + CreatedBy uuid.UUID `json:"createdBy"` + EndTime time.Time `json:"endTime"` + Upvotes int `json:"upvotes"` + Downvotes int `json:"downvotes"` + NotSure int `json:"notSure"` + TotalVotes int `json:"totalVotes"` + Status string `json:"status"` // Calculated status + UserVote string `json:"userVote"` +} diff --git a/api/v1/report/vote.go b/api/v1/report/vote.go new file mode 100644 index 00000000..1bb50edc --- /dev/null +++ b/api/v1/report/vote.go @@ -0,0 +1,66 @@ +package report + +import ( + "errors" + "fmt" + "net/http" + "time" + + "github.com/NetSepio/gateway/api/middleware/auth/paseto" + "github.com/NetSepio/gateway/config/dbconfig" + "github.com/NetSepio/gateway/models" + "github.com/NetSepio/gateway/util/pkg/logwrapper" + "github.com/TheLazarusNetwork/go-helpers/httpo" + "github.com/gin-gonic/gin" + "github.com/jackc/pgconn" +) + +type ReportVoteRequest struct { + ReportID string `json:"reportId" binding:"required"` + VoteType string `json:"voteType" binding:"required,oneof=upvote downvote notsure"` +} + +func postReportVote(c *gin.Context) { + var request ReportVoteRequest + if err := c.BindJSON(&request); err != nil { + httpo.NewErrorResponse(http.StatusBadRequest, fmt.Sprintf("Invalid request body: %s", err)).SendD(c) + return + } + + db := dbconfig.GetDb() + userId := c.GetString(paseto.CTX_USER_ID) // Assuming user ID is in the context + + // Check if the voting period has ended + var report models.Report + if err := db.Where("id = ?", request.ReportID).First(&report).Error; err != nil { + httpo.NewErrorResponse(http.StatusInternalServerError, "Failed to vote").SendD(c) + return + } + + if time.Now().After(report.EndTime) { + httpo.NewErrorResponse(http.StatusBadRequest, "Voting period has ended").SendD(c) + return + } + + // Insert or update the vote + newVote := models.ReportVote{ + ReportID: request.ReportID, + VoterID: userId, + VoteType: request.VoteType, + } + err := db.Create(&newVote).Error + if err != nil { + var pgError *pgconn.PgError + if errors.As(err, &pgError) { + if pgError.Code == "23505" && pgError.ConstraintName == "report_votes_pkey" { + httpo.NewErrorResponse(http.StatusBadRequest, "You have already voted on this report").SendD(c) + return + } + } + logwrapper.Errorf("failed to record vote: %s", err) + httpo.NewErrorResponse(http.StatusInternalServerError, "Failed to record vote").SendD(c) + return + } + + httpo.NewSuccessResponse(http.StatusOK, "Vote recorded successfully").SendD(c) +} diff --git a/api/v1/v1.go b/api/v1/v1.go index 89489807..96cd2f84 100644 --- a/api/v1/v1.go +++ b/api/v1/v1.go @@ -11,6 +11,7 @@ import ( "github.com/NetSepio/gateway/api/v1/getreviewerdetails" "github.com/NetSepio/gateway/api/v1/getreviews" "github.com/NetSepio/gateway/api/v1/profile" + "github.com/NetSepio/gateway/api/v1/report" "github.com/NetSepio/gateway/api/v1/sotreus" "github.com/NetSepio/gateway/api/v1/stats" "github.com/NetSepio/gateway/api/v1/status" @@ -36,6 +37,7 @@ func ApplyRoutes(r *gin.RouterGroup) { getreviewerdetails.ApplyRoutes(v1) sotreus.ApplyRoutes(v1) domain.ApplyRoutes(v1) + report.ApplyRoutes(v1) account.ApplyRoutes(v1) } } diff --git a/main.go b/main.go index 878826c2..95d237af 100644 --- a/main.go +++ b/main.go @@ -14,7 +14,6 @@ import ( "github.com/NetSepio/gateway/models/claims" "github.com/NetSepio/gateway/util/pkg/auth" "github.com/NetSepio/gateway/util/pkg/logwrapper" - "github.com/google/uuid" ) func main() { @@ -26,7 +25,7 @@ func main() { if os.Getenv("DEBUG_MODE") == "true" { newUser := &models.User{ WalletAddress: strings.ToLower("0x984185d39c67c954bd058beb619faf8929bb9349ef33c15102bdb982cbf7f18f"), - UserId: uuid.NewString(), + UserId: "fc8fe270-ce16-4df9-a17f-979bcd824e98", } if err := db.Create(newUser).Error; err != nil { logwrapper.Warn(err) diff --git a/migrations/000009_create_report.up.sql b/migrations/000009_create_report.up.sql new file mode 100644 index 00000000..17eb87c4 --- /dev/null +++ b/migrations/000009_create_report.up.sql @@ -0,0 +1,32 @@ +CREATE TABLE public.reports ( + id uuid PRIMARY KEY, + title text NOT NULL, + description text, + document text, + project_name text, + project_domain text, + status text CHECK (status IN ('accepted', 'rejected', 'running')), + created_by uuid REFERENCES public.users(user_id), + end_time timestamp with time zone, + created_at timestamp with time zone DEFAULT current_timestamp +); + +CREATE TABLE public.report_tags ( + report_id uuid REFERENCES public.reports(id), + tag text, + UNIQUE(report_id, tag) +); + +CREATE TABLE public.report_images ( + report_id uuid REFERENCES public.reports(id), + image_url text, + UNIQUE(report_id, image_url) +); + +CREATE TABLE public.report_votes ( + report_id uuid REFERENCES public.reports(id), + voter_id uuid REFERENCES public.users(user_id), + vote_type text CHECK (vote_type IN ('upvote', 'downvote', 'notsure')), + created_at timestamp with time zone DEFAULT current_timestamp, + PRIMARY KEY (report_id, voter_id) +); \ No newline at end of file diff --git a/models/Report.go b/models/Report.go new file mode 100644 index 00000000..84887c12 --- /dev/null +++ b/models/Report.go @@ -0,0 +1,34 @@ +package models + +import ( + "time" +) + +type Report struct { + ID string `gorm:"type:uuid;primary_key;"` + Title string `gorm:"type:text;not null"` + Description string `gorm:"type:text"` + Document string `gorm:"type:text"` + ProjectName string `gorm:"type:text"` + ProjectDomain string `gorm:"type:text"` + CreatedBy string `gorm:"type:uuid"` + CreatedAt time.Time `gorm:"type:timestamp"` + EndTime time.Time `gorm:"type:timestamp"` +} + +type ReportVote struct { + ReportID string `gorm:"type:uuid;primaryKey;"` + VoterID string `gorm:"type:uuid;primaryKey;"` + VoteType string `gorm:"type:text"` + CreatedAt time.Time `gorm:"type:timestamp with time zone"` +} + +type ReportTag struct { + ReportID string `gorm:"type:uuid;"` + Tag string `gorm:"type:text;"` +} + +type ReportImage struct { + ReportID string `gorm:"type:uuid;"` + ImageURL string `gorm:"type:text;"` +} diff --git a/models/claims/Claim.go b/models/claims/Claim.go index b6bc0795..b827389a 100644 --- a/models/claims/Claim.go +++ b/models/claims/Claim.go @@ -1,6 +1,7 @@ package claims import ( + "fmt" "time" "github.com/NetSepio/gateway/config/dbconfig" @@ -21,6 +22,7 @@ func (c CustomClaims) Valid() error { if err := c.RegisteredClaims.Valid(); err != nil { return err } + fmt.Printf("c.UserId: %s\n", c.UserId) err := db.Model(&models.User{}).Where("user_id = ?", c.UserId).First(&models.User{}).Error if err != nil { return err