Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
11 changes: 10 additions & 1 deletion backend/controllers/team_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,10 @@ func JoinTeam(c *gin.Context) {
for _, member := range team.Members {
totalElo += member.Elo
}
if len(team.Members) >= team.MaxSize {
c.JSON(http.StatusBadRequest, gin.H{"error": "Team is already full"})
return
}
totalElo += newMember.Elo
newAverageElo := totalElo / float64(len(team.Members)+1)

Expand Down Expand Up @@ -591,7 +595,12 @@ func GetTeamMemberProfile(c *gin.Context) {
func GetAvailableTeams(c *gin.Context) {
collection := db.GetCollection("teams")
cursor, err := collection.Find(context.Background(), bson.M{
"$expr": bson.M{"$lt": []interface{}{bson.M{"$size": "$members"}, 4}}, // Teams with less than 4 members
"$expr": bson.M{
"$lt": []interface{}{
bson.M{"$size": "$members"},
"$maxSize",
},
},
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve teams"})
Expand Down
49 changes: 36 additions & 13 deletions backend/controllers/team_matchmaking.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package controllers
import (
"context"
"net/http"
"time"

"arguehub/db"
"arguehub/models"
Expand Down Expand Up @@ -32,7 +33,9 @@ func JoinMatchmaking(c *gin.Context) {
// Get team
collection := db.GetCollection("teams")
var team models.Team
err = collection.FindOne(context.Background(), bson.M{"_id": objectID}).Decode(&team)
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()
err = collection.FindOne(ctx, bson.M{"_id": objectID}).Decode(&team)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Team not found"})
return
Expand Down Expand Up @@ -77,28 +80,49 @@ func LeaveMatchmaking(c *gin.Context) {
return
}

userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}

collection := db.GetCollection("teams")
var team models.Team
ctxLeave, cancelLeave := context.WithTimeout(c.Request.Context(), 5*time.Second)
defer cancelLeave()
err = collection.FindOne(ctxLeave, bson.M{"_id": objectID}).Decode(&team)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Team not found"})
return
}

if team.CaptainID != userID.(primitive.ObjectID) {
c.JSON(http.StatusForbidden, gin.H{"error": "Only the captain can leave matchmaking"})
return
}

services.RemoveFromMatchmaking(objectID)
c.JSON(http.StatusOK, gin.H{"message": "Team removed from matchmaking"})
}

// GetMatchmakingPool returns the current matchmaking pool for debugging
func GetMatchmakingPool(c *gin.Context) {
pool := services.GetMatchmakingPool()

// Convert to a more readable format
var poolInfo []gin.H
for teamID, entry := range pool {
poolInfo = append(poolInfo, gin.H{
"teamId": teamID,
"teamName": entry.Team.Name,
"captainId": entry.Team.CaptainID.Hex(),
"maxSize": entry.MaxSize,
"averageElo": entry.AverageElo,
"teamId": teamID,
"teamName": entry.Team.Name,
"captainId": entry.Team.CaptainID.Hex(),
"maxSize": entry.MaxSize,
"averageElo": entry.AverageElo,
"membersCount": len(entry.Team.Members),
"timestamp": entry.Timestamp.Format("2006-01-02 15:04:05"),
"timestamp": entry.Timestamp.Format("2006-01-02 15:04:05"),
})
}

c.JSON(http.StatusOK, gin.H{
"poolSize": len(pool),
"teams": poolInfo,
Expand All @@ -122,9 +146,8 @@ func GetMatchmakingStatus(c *gin.Context) {
}

c.JSON(http.StatusOK, gin.H{
"matched": true,
"team": matchingTeam,
"matchId": matchingTeam.ID.Hex(),
"matched": true,
"team": matchingTeam,
"matchId": matchingTeam.ID.Hex(),
})
}

25 changes: 19 additions & 6 deletions backend/middlewares/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"arguehub/db"
"arguehub/models"
"context"
"errors"
"fmt"
"log"
"net/http"
Expand All @@ -14,12 +15,13 @@ import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)

func AuthMiddleware(configPath string) gin.HandlerFunc {
return func(c *gin.Context) {
log.Printf("AuthMiddleware called for path: %s", c.Request.URL.Path)

cfg, err := config.LoadConfig(configPath)
if err != nil {
log.Printf("Failed to load config: %v", err)
Expand Down Expand Up @@ -51,16 +53,27 @@ func AuthMiddleware(configPath string) gin.HandlerFunc {
return
}

email := claims["sub"].(string)

email, ok := claims["sub"].(string)
if !ok || email == "" {
log.Printf("Invalid or missing 'sub' claim in JWT")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"})
c.Abort()
return
}

// Fetch user from database
dbCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
dbCtx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()

var user models.User
err = db.MongoDatabase.Collection("users").FindOne(dbCtx, bson.M{"email": email}).Decode(&user)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"})
if errors.Is(err, mongo.ErrNoDocuments) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"})
} else {
log.Printf("Failed to load user %s: %v", email, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Authentication lookup failed"})
}
c.Abort()
return
}
Expand Down
94 changes: 49 additions & 45 deletions backend/models/team.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ import (

// Team represents a debate team
type Team struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
Name string `bson:"name" json:"name"`
Code string `bson:"code" json:"code"` // Unique team code
CaptainID primitive.ObjectID `bson:"captainId" json:"captainId"`
CaptainEmail string `bson:"captainEmail" json:"captainEmail"`
Members []TeamMember `bson:"members" json:"members"`
MaxSize int `bson:"maxSize" json:"maxSize"` // Maximum team size for matching
AverageElo float64 `bson:"averageElo" json:"averageElo"`
CreatedAt time.Time `bson:"createdAt" json:"createdAt"`
UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"`
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
Name string `bson:"name" json:"name"`
Code string `bson:"code" json:"code"` // Unique team code
CaptainID primitive.ObjectID `bson:"captainId" json:"captainId"`
CaptainEmail string `bson:"captainEmail" json:"captainEmail"`
Members []TeamMember `bson:"members" json:"members"`
MaxSize int `bson:"maxSize" json:"maxSize"` // Maximum team size for matching
AverageElo float64 `bson:"averageElo" json:"averageElo"`
CreatedAt time.Time `bson:"createdAt" json:"createdAt"`
UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"`
}

// TeamMember represents a member of a team
Expand All @@ -28,7 +28,7 @@ type TeamMember struct {
DisplayName string `bson:"displayName" json:"displayName"`
AvatarURL string `bson:"avatarUrl,omitempty" json:"avatarUrl,omitempty"`
Elo float64 `bson:"elo" json:"elo"`
JoinedAt time.Time `bson:"joinedAt" json:"joinedAt"`
JoinedAt time.Time `bson:"joinedAt" json:"joinedAt"`
}

// MarshalJSON customizes JSON serialization for Team to convert ObjectIDs to hex strings
Expand Down Expand Up @@ -62,22 +62,22 @@ type TeamDebate struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
Team1ID primitive.ObjectID `bson:"team1Id" json:"team1Id"`
Team2ID primitive.ObjectID `bson:"team2Id" json:"team2Id"`
Team1Name string `bson:"team1Name" json:"team1Name"`
Team2Name string `bson:"team2Name" json:"team2Name"`
Team1Members []TeamMember `bson:"team1Members" json:"team1Members"`
Team2Members []TeamMember `bson:"team2Members" json:"team2Members"`
Topic string `bson:"topic" json:"topic"`
Team1Stance string `bson:"team1Stance" json:"team1Stance"` // "for" or "against"
Team2Stance string `bson:"team2Stance" json:"team2Stance"` // "for" or "against"
Status string `bson:"status" json:"status"` // "waiting", "active", "finished"
CurrentTurn string `bson:"currentTurn" json:"currentTurn"` // "team1" or "team2"
Team1Name string `bson:"team1Name" json:"team1Name"`
Team2Name string `bson:"team2Name" json:"team2Name"`
Team1Members []TeamMember `bson:"team1Members" json:"team1Members"`
Team2Members []TeamMember `bson:"team2Members" json:"team2Members"`
Topic string `bson:"topic" json:"topic"`
Team1Stance string `bson:"team1Stance" json:"team1Stance"` // "for" or "against"
Team2Stance string `bson:"team2Stance" json:"team2Stance"` // "for" or "against"
Status string `bson:"status" json:"status"` // "waiting", "active", "finished"
CurrentTurn string `bson:"currentTurn" json:"currentTurn"` // "team1" or "team2"
CurrentUserID primitive.ObjectID `bson:"currentUserId,omitempty" json:"currentUserId,omitempty"`
TurnCount int `bson:"turnCount" json:"turnCount"`
MaxTurns int `bson:"maxTurns" json:"maxTurns"`
Team1Elo float64 `bson:"team1Elo" json:"team1Elo"`
Team2Elo float64 `bson:"team2Elo" json:"team2Elo"`
CreatedAt time.Time `bson:"createdAt" json:"createdAt"`
UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"`
TurnCount int `bson:"turnCount" json:"turnCount"`
MaxTurns int `bson:"maxTurns" json:"maxTurns"`
Team1Elo float64 `bson:"team1Elo" json:"team1Elo"`
Team2Elo float64 `bson:"team2Elo" json:"team2Elo"`
CreatedAt time.Time `bson:"createdAt" json:"createdAt"`
UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"`
}

// TeamDebateMessage represents a message in a team debate
Expand All @@ -86,39 +86,44 @@ type TeamDebateMessage struct {
DebateID primitive.ObjectID `bson:"debateId" json:"debateId"`
TeamID primitive.ObjectID `bson:"teamId" json:"teamId"`
UserID primitive.ObjectID `bson:"userId" json:"userId"`
Email string `bson:"email" json:"email"`
DisplayName string `bson:"displayName" json:"displayName"`
AvatarURL string `bson:"avatarUrl,omitempty" json:"avatarUrl,omitempty"`
Message string `bson:"message" json:"message"`
Type string `bson:"type" json:"type"` // "user", "system"
Timestamp time.Time `bson:"timestamp" json:"timestamp"`
Email string `bson:"email" json:"email"`
DisplayName string `bson:"displayName" json:"displayName"`
AvatarURL string `bson:"avatarUrl,omitempty" json:"avatarUrl,omitempty"`
Message string `bson:"message" json:"message"`
Type string `bson:"type" json:"type"` // "user", "system"
Timestamp time.Time `bson:"timestamp" json:"timestamp"`
}

// TeamChatMessage represents a message in team chat
type TeamChatMessage struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
TeamID primitive.ObjectID `bson:"teamId" json:"teamId"`
UserID primitive.ObjectID `bson:"userId" json:"userId"`
Email string `bson:"email" json:"email"`
DisplayName string `bson:"displayName" json:"displayName"`
Message string `bson:"message" json:"message"`
Timestamp time.Time `bson:"timestamp" json:"timestamp"`
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
TeamID primitive.ObjectID `bson:"teamId" json:"teamId"`
UserID primitive.ObjectID `bson:"userId" json:"userId"`
Email string `bson:"email" json:"email"`
DisplayName string `bson:"displayName" json:"displayName"`
Message string `bson:"message" json:"message"`
Timestamp time.Time `bson:"timestamp" json:"timestamp"`
}

// MarshalJSON customizes JSON serialization for TeamDebate to convert ObjectIDs to hex strings
func (td TeamDebate) MarshalJSON() ([]byte, error) {
type Alias TeamDebate
var currentUserHex *string
if !td.CurrentUserID.IsZero() {
hex := td.CurrentUserID.Hex()
currentUserHex = &hex
}
return json.Marshal(&struct {
ID string `json:"id,omitempty"`
Team1ID string `json:"team1Id"`
Team2ID string `json:"team2Id"`
CurrentUserID string `json:"currentUserId,omitempty"`
ID string `json:"id,omitempty"`
Team1ID string `json:"team1Id"`
Team2ID string `json:"team2Id"`
CurrentUserID *string `json:"currentUserId,omitempty"`
*Alias
}{
ID: td.ID.Hex(),
Team1ID: td.Team1ID.Hex(),
Team2ID: td.Team2ID.Hex(),
CurrentUserID: td.CurrentUserID.Hex(),
CurrentUserID: currentUserHex,
Alias: (*Alias)(&td),
})
}
Expand Down Expand Up @@ -156,4 +161,3 @@ func (tcm TeamChatMessage) MarshalJSON() ([]byte, error) {
Alias: (*Alias)(&tcm),
})
}

Loading