Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions .env.example

This file was deleted.

43 changes: 0 additions & 43 deletions backend/config/config.prod.sample.yml

This file was deleted.

136 changes: 136 additions & 0 deletions backend/controllers/vote_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package controllers

import (
"context"
"net/http"
"time"

"arguehub/db"
"arguehub/models"

"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)

// SubmitVote handles a spectator's vote on a debate
func SubmitVote(c *gin.Context) {
debateIDHex := c.Param("id")
debateID, err := primitive.ObjectIDFromHex(debateIDHex)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid debate ID"})
return
}

var req struct {
Vote string `json:"vote" binding:"required"` // "User" or "Bot"
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"})
return
}
Comment on lines +25 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing validation for vote value.

The req.Vote field accepts any string, but the Vote model comment indicates only "User" or "Bot" are valid (per backend/models/vote.go). Invalid values will be stored and silently ignored in aggregation, corrupting vote counts.

🛡️ Proposed fix
 	if err := c.ShouldBindJSON(&req); err != nil {
 		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"})
 		return
 	}
+
+	if req.Vote != "User" && req.Vote != "Bot" {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "Vote must be 'User' or 'Bot'"})
+		return
+	}

Also applies to: 63-68

🤖 Prompt for AI Agents
In `@backend/controllers/vote_controller.go` around lines 25 - 31, The request
binding currently accepts any string for req.Vote which risks storing invalid
values; after binding JSON into the local req struct (req.Vote) validate that
the value is exactly "User" or "Bot" and return a 400 with a clear error message
if it is not; apply the same validation in the other handler occurrence handling
req.Vote (lines ~63-68) so only allowed Vote values per the Vote model
(backend/models/vote.go) are persisted and used in aggregation.


// Validate debate exists and is in a votable state (has an outcome)
var debate models.DebateVsBot
err = db.DebateVsBotCollection.FindOne(context.Background(), bson.M{"_id": debateID}).Decode(&debate)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Debate not found"})
return
}

if debate.Outcome == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Debate must be finalized before voting"})
return
}

// Basic duplicate prevention using IP
voterID := c.ClientIP()

// Check if this voter has already voted for this debate
count, err := db.VotesCollection.CountDocuments(context.Background(), bson.M{
"debateId": debateID,
"voterId": voterID,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check existing votes"})
return
}
if count > 0 {
c.JSON(http.StatusConflict, gin.H{"error": "You have already voted on this debate"})
return
}

vote := models.Vote{
ID: primitive.NewObjectID(),
DebateID: debateID,
Vote: req.Vote,
VoterID: voterID,
Timestamp: time.Now(),
}

_, err = db.VotesCollection.InsertOne(context.Background(), vote)
Comment on lines +49 to +71
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Race condition in duplicate vote prevention (TOCTOU).

The check-then-insert pattern (CountDocumentsInsertOne) allows duplicate votes if two requests arrive concurrently for the same voter/debate. Between the count check and the insert, another request could complete its insert.

Consider using a unique index on (debateId, voterId) in MongoDB to enforce uniqueness atomically, then handle the duplicate key error.

🔒 Proposed approach
  1. Create unique index in backend/db/db.go during initialization:
indexModel := mongo.IndexModel{
    Keys:    bson.D{{Key: "debateId", Value: 1}, {Key: "voterId", Value: 1}},
    Options: options.Index().SetUnique(true),
}
VotesCollection.Indexes().CreateOne(ctx, indexModel)
  1. Update the controller to handle duplicate key errors:
-	count, err := db.VotesCollection.CountDocuments(context.Background(), bson.M{
-		"debateId": debateID,
-		"voterId":  voterID,
-	})
-	if err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check existing votes"})
-		return
-	}
-	if count > 0 {
-		c.JSON(http.StatusConflict, gin.H{"error": "You have already voted on this debate"})
-		return
-	}
-
 	vote := models.Vote{
 		ID:        primitive.NewObjectID(),
 		DebateID:  debateID,
 		Vote:      req.Vote,
 		VoterID:   voterID,
 		Timestamp: time.Now(),
 	}

 	_, err = db.VotesCollection.InsertOne(context.Background(), vote)
 	if err != nil {
+		if mongo.IsDuplicateKeyError(err) {
+			c.JSON(http.StatusConflict, gin.H{"error": "You have already voted on this debate"})
+			return
+		}
 		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to submit vote"})
 		return
 	}
🤖 Prompt for AI Agents
In `@backend/controllers/vote_controller.go` around lines 49 - 71, The current
CountDocuments → InsertOne flow in VoteCreate (uses
db.VotesCollection.CountDocuments and db.VotesCollection.InsertOne with
models.Vote) is vulnerable to a TOCTOU race; add a unique compound index on
(debateId, voterId) during DB initialization (use
VotesCollection.Indexes().CreateOne with a mongo.IndexModel keyed by "debateId"
and "voterId" and SetUnique(true)) and then remove the pre-insert count-check in
the controller, instead attempting InsertOne directly and catching duplicate-key
errors (check for mongo.IsDuplicateKeyError or error code 11000) to return HTTP
409 when a duplicate vote is attempted.

if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to submit vote"})
return
}

c.JSON(http.StatusOK, gin.H{"message": "Vote submitted successfully"})
}

// GetVerdicts combines AI and People's Choice verdicts
func GetVerdicts(c *gin.Context) {
debateIDHex := c.Param("id")
debateID, err := primitive.ObjectIDFromHex(debateIDHex)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid debate ID"})
return
}

// Fetch debate for AI outcome
var debate models.DebateVsBot
err = db.DebateVsBotCollection.FindOne(context.Background(), bson.M{"_id": debateID}).Decode(&debate)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Debate not found"})
return
}

// Aggregate People's Choice votes
cursor, err := db.VotesCollection.Find(context.Background(), bson.M{"debateId": debateID})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch votes"})
return
}
defer cursor.Close(context.Background())

userVotes := 0
botVotes := 0
for cursor.Next(context.Background()) {
var vote models.Vote
if err := cursor.Decode(&vote); err == nil {
if vote.Vote == "User" {
userVotes++
} else if vote.Vote == "Bot" {
botVotes++
}
}
}

peoplesChoice := "Draw"
if userVotes > botVotes {
peoplesChoice = "User"
} else if botVotes > userVotes {
peoplesChoice = "Bot"
}

c.JSON(http.StatusOK, gin.H{
"debateId": debateIDHex,
"aiVerdict": debate.Outcome,
"peoplesChoice": gin.H{
"winner": peoplesChoice,
"counts": gin.H{
"user": userVotes,
"bot": botVotes,
},
},
})
}
4 changes: 3 additions & 1 deletion backend/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
var MongoClient *mongo.Client
var MongoDatabase *mongo.Database
var DebateVsBotCollection *mongo.Collection
var VotesCollection *mongo.Collection
var RedisClient *redis.Client

// GetCollection returns a collection by name
Expand Down Expand Up @@ -57,6 +58,7 @@ func ConnectMongoDB(uri string) error {

MongoDatabase = client.Database(dbName)
DebateVsBotCollection = MongoDatabase.Collection("debates_vs_bot")
VotesCollection = MongoDatabase.Collection("votes")
return nil
}

Expand Down Expand Up @@ -115,4 +117,4 @@ func ConnectRedis(addr, password string, db int) error {

log.Println("Connected to Redis")
return nil
}
}
16 changes: 16 additions & 0 deletions backend/models/vote.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package models

import (
"time"

"go.mongodb.org/mongo-driver/bson/primitive"
)

// Vote represents a spectator's vote on a debate outcome
type Vote struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
DebateID primitive.ObjectID `json:"debateId" bson:"debateId"`
Vote string `json:"vote" bson:"vote"` // "User", "Bot", or "for", "against"
VoterID string `json:"voterId" bson:"voterId"` // IP address or fingerprint
Timestamp time.Time `json:"timestamp" bson:"timestamp"`
Comment on lines +11 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid storing raw IPs in VoterID (PII).
The field comment suggests persisting client IPs; that’s sensitive data. For best‑effort duplicate prevention, store a hashed/obfuscated identifier (e.g., HMAC(IP, server‑secret) with optional truncation/TTL). SubmitVote currently uses c.ClientIP() (backend/controllers/vote_controller.go lines 36‑47), so that path should hash before storing and querying.

🔒 Suggested model adjustment (also update controller logic)
 type Vote struct {
 	ID        primitive.ObjectID `json:"id" bson:"_id,omitempty"`
 	DebateID  primitive.ObjectID `json:"debateId" bson:"debateId"`
 	Vote      string             `json:"vote" bson:"vote"` // "User", "Bot", or "for", "against"
-	VoterID   string             `json:"voterId" bson:"voterId"` // IP address or fingerprint
+	VoterHash string             `json:"voterHash" bson:"voterHash"` // HMAC(IP, secret) or anonymized fingerprint
 	Timestamp time.Time          `json:"timestamp" bson:"timestamp"`
 }
🤖 Prompt for AI Agents
In `@backend/models/vote.go` around lines 11 - 15, The Vote model currently stores
raw client IPs in VoterID; change the usage so VoterID stores a
hashed/obfuscated identifier instead (keep type string but document it as
HMAC(IP, serverSecret) or similar). Update SubmitVote in
backend/controllers/vote_controller.go (which currently reads c.ClientIP()) to
compute a stable hash (e.g., hashVoterID or computeVoterIDHash using a server
secret from env/config) before any DB queries or when setting Vote.VoterID, and
use that hashed value for duplicate checks and persistence; ensure hashing uses
HMAC with a secret and consider optional truncation/TTL if desired.

}
2 changes: 2 additions & 0 deletions backend/routes/debatevsbot.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ func SetupDebateVsBotRoutes(router *gin.RouterGroup) {
vsbot.POST("/debate", controllers.SendDebateMessage)
vsbot.POST("/judge", controllers.JudgeDebate)
vsbot.POST("/concede", controllers.ConcedeDebate)
vsbot.POST("/debate/:id/vote", controllers.SubmitVote)
vsbot.GET("/debate/:id/verdicts", controllers.GetVerdicts)
}
}
4 changes: 2 additions & 2 deletions backend/services/coach.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Provide ONLY the JSON output without additional text or markdown formatting.`,
)

ctx := context.Background()
response, err := generateDefaultModelText(ctx, prompt)
response, err := generateDefaultModelText(ctx, "", prompt)
if err != nil {
return models.WeakStatement{}, fmt.Errorf("failed to generate weak statement: %v", err)
}
Expand Down Expand Up @@ -98,7 +98,7 @@ Provide ONLY the JSON output without additional text or markdown formatting.`,
)

ctx := context.Background()
response, err := generateDefaultModelText(ctx, prompt)
response, err := generateDefaultModelText(ctx, "", prompt)
if err != nil {
return models.Evaluation{}, fmt.Errorf("failed to evaluate argument: %v", err)
}
Expand Down
Loading