| package main |
|
|
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "math" |
| "net/http" |
| "strconv" |
| "strings" |
| "time" |
|
|
| "github.com/golang-jwt/jwt/v5" |
| "go.mongodb.org/mongo-driver/bson" |
| "go.mongodb.org/mongo-driver/bson/primitive" |
| "go.mongodb.org/mongo-driver/mongo" |
| "go.mongodb.org/mongo-driver/mongo/options" |
| ) |
|
|
| |
|
|
| type discussionDoc struct { |
| ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` |
| ProblemID string `bson:"problemId" json:"problemId"` |
| ProblemTitle string `bson:"problemTitle" json:"problemTitle"` |
| UserID string `bson:"userId" json:"userId"` |
| Username string `bson:"username" json:"username"` |
| Language string `bson:"language" json:"language"` |
| Code string `bson:"code" json:"code"` |
| Approach string `bson:"approach" json:"approach"` |
| Upvotes int `bson:"upvotes" json:"upvotes"` |
| UpvotedBy []string `bson:"upvotedBy" json:"upvotedBy"` |
| Verdict string `bson:"verdict" json:"verdict"` |
| ExecutionTime string `bson:"executionTime" json:"executionTime"` |
| MemoryUsed string `bson:"memoryUsed" json:"memoryUsed"` |
| CreatedAt time.Time `bson:"createdAt" json:"createdAt"` |
| Tags []string `bson:"tags" json:"tags"` |
| } |
|
|
| |
|
|
| func extractUserIDFromJWT(r *http.Request) (string, error) { |
| raw := readBearer(r) |
| if raw == "" { |
| return "", fmt.Errorf("not authenticated") |
| } |
| tok, err := jwt.ParseWithClaims(raw, &jwt.RegisteredClaims{}, func(t *jwt.Token) (any, error) { |
| if t.Method != jwt.SigningMethodHS256 { |
| return nil, fmt.Errorf("unexpected signing method") |
| } |
| return []byte(jwtSecret()), nil |
| }) |
| if err != nil || !tok.Valid { |
| return "", fmt.Errorf("invalid or expired session") |
| } |
| claims, ok := tok.Claims.(*jwt.RegisteredClaims) |
| if !ok || claims.Subject == "" { |
| return "", fmt.Errorf("invalid or expired session") |
| } |
| return claims.Subject, nil |
| } |
|
|
| |
|
|
| func ensureDiscussionIndexes(coll *mongo.Collection) { |
| ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) |
| defer cancel() |
|
|
| |
| _, _ = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ |
| Keys: bson.D{{Key: "problemId", Value: 1}, {Key: "createdAt", Value: -1}}, |
| }) |
|
|
| |
| _, _ = coll.Indexes().CreateOne(ctx, mongo.IndexModel{ |
| Keys: bson.D{{Key: "userId", Value: 1}, {Key: "problemId", Value: 1}, {Key: "language", Value: 1}}, |
| }) |
| } |
|
|
| |
|
|
| func handleGetDiscussions(coll *mongo.Collection) http.HandlerFunc { |
| return func(w http.ResponseWriter, r *http.Request) { |
| if r.Method != http.MethodGet { |
| jsonError(w, http.StatusMethodNotAllowed, "Method not allowed") |
| return |
| } |
|
|
| problemID := r.URL.Query().Get("problemId") |
| if problemID == "" { |
| jsonError(w, http.StatusBadRequest, "problemId is required") |
| return |
| } |
|
|
| page, _ := strconv.Atoi(r.URL.Query().Get("page")) |
| if page < 1 { |
| page = 1 |
| } |
| limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) |
| if limit < 1 || limit > 50 { |
| limit = 10 |
| } |
| skip := int64((page - 1) * limit) |
|
|
| ctx := r.Context() |
| filter := bson.M{"problemId": problemID, "verdict": "Accepted"} |
|
|
| total, err := coll.CountDocuments(ctx, filter) |
| if err != nil { |
| jsonError(w, http.StatusInternalServerError, "Failed to count discussions") |
| return |
| } |
| totalPages := int(math.Ceil(float64(total) / float64(limit))) |
|
|
| findOpts := options.Find(). |
| SetSort(bson.D{{Key: "createdAt", Value: -1}}). |
| SetSkip(skip). |
| SetLimit(int64(limit)). |
| SetProjection(bson.M{"__v": 0}) |
|
|
| cursor, err := coll.Find(ctx, filter, findOpts) |
| if err != nil { |
| jsonError(w, http.StatusInternalServerError, "Failed to fetch discussions") |
| return |
| } |
| defer cursor.Close(ctx) |
|
|
| var discussions []discussionDoc |
| if err := cursor.All(ctx, &discussions); err != nil { |
| jsonError(w, http.StatusInternalServerError, "Failed to parse discussions") |
| return |
| } |
| if discussions == nil { |
| discussions = []discussionDoc{} |
| } |
|
|
| |
| for i := range discussions { |
| if discussions[i].UpvotedBy == nil { |
| discussions[i].UpvotedBy = []string{} |
| } |
| if discussions[i].Tags == nil { |
| discussions[i].Tags = []string{} |
| } |
| } |
|
|
| w.Header().Set("Cache-Control", "public, max-age=30") |
| jsonWrite(w, http.StatusOK, map[string]any{ |
| "posts": discussions, |
| "totalPages": totalPages, |
| "currentPage": page, |
| }) |
| } |
| } |
|
|
| |
|
|
| func handlePostDiscussion(coll *mongo.Collection, usersColl *mongo.Collection) http.HandlerFunc { |
| return func(w http.ResponseWriter, r *http.Request) { |
| if r.Method != http.MethodPost { |
| jsonError(w, http.StatusMethodNotAllowed, "Method not allowed") |
| return |
| } |
|
|
| var body struct { |
| ProblemID string `json:"problemId"` |
| ProblemTitle string `json:"problemTitle"` |
| UserID string `json:"userId"` |
| Username string `json:"username"` |
| Language string `json:"language"` |
| Code string `json:"code"` |
| Approach string `json:"approach"` |
| ExecutionTime string `json:"executionTime"` |
| MemoryUsed string `json:"memoryUsed"` |
| Tags []string `json:"tags"` |
| } |
| if err := json.NewDecoder(maxBody(w, r.Body)).Decode(&body); err != nil { |
| jsonError(w, http.StatusBadRequest, "Invalid JSON body") |
| return |
| } |
|
|
| |
| userID, jwtErr := extractUserIDFromJWT(r) |
| if jwtErr != nil { |
| userID = strings.TrimSpace(body.UserID) |
| } |
| if userID == "" { |
| jsonError(w, http.StatusUnauthorized, "Not authenticated") |
| return |
| } |
|
|
| if strings.TrimSpace(body.ProblemID) == "" || strings.TrimSpace(body.Language) == "" || strings.TrimSpace(body.Code) == "" { |
| jsonError(w, http.StatusBadRequest, "problemId, language, and code are required") |
| return |
| } |
|
|
| |
| username := strings.TrimSpace(body.Username) |
| if jwtErr == nil { |
| |
| oid, err := primitive.ObjectIDFromHex(userID) |
| if err == nil { |
| var uDoc userDoc |
| if e := usersColl.FindOne(r.Context(), bson.M{"_id": oid}).Decode(&uDoc); e == nil { |
| username = uDoc.DisplayName |
| } |
| } |
| } |
| if username == "" { |
| username = "User" |
| } |
|
|
| if body.Tags == nil { |
| body.Tags = []string{} |
| } |
|
|
| now := time.Now().UTC() |
|
|
| |
| filter := bson.M{ |
| "userId": userID, |
| "problemId": body.ProblemID, |
| "language": body.Language, |
| } |
|
|
| update := bson.M{ |
| "$set": bson.M{ |
| "problemTitle": body.ProblemTitle, |
| "username": username, |
| "code": body.Code, |
| "approach": body.Approach, |
| "verdict": "Accepted", |
| "executionTime": body.ExecutionTime, |
| "memoryUsed": body.MemoryUsed, |
| "tags": body.Tags, |
| "createdAt": now, |
| }, |
| "$setOnInsert": bson.M{ |
| "userId": userID, |
| "problemId": body.ProblemID, |
| "language": body.Language, |
| "upvotes": 0, |
| "upvotedBy": []string{}, |
| }, |
| } |
|
|
| opts := options.Update().SetUpsert(true) |
| result, err := coll.UpdateOne(r.Context(), filter, update, opts) |
| if err != nil { |
| jsonError(w, http.StatusInternalServerError, "Failed to save discussion") |
| return |
| } |
|
|
| var postID string |
| if result.UpsertedID != nil { |
| postID = result.UpsertedID.(primitive.ObjectID).Hex() |
| } else { |
| |
| var existing discussionDoc |
| if e := coll.FindOne(r.Context(), filter).Decode(&existing); e == nil { |
| postID = existing.ID.Hex() |
| } |
| } |
|
|
| jsonWrite(w, http.StatusCreated, map[string]any{ |
| "success": true, |
| "postId": postID, |
| }) |
| } |
| } |
|
|
| |
|
|
| func handleUpvoteDiscussion(coll *mongo.Collection) http.HandlerFunc { |
| return func(w http.ResponseWriter, r *http.Request) { |
| if r.Method != http.MethodPatch { |
| jsonError(w, http.StatusMethodNotAllowed, "Method not allowed") |
| return |
| } |
|
|
| |
| userID, jwtErr := extractUserIDFromJWT(r) |
| if jwtErr != nil { |
| userID = strings.TrimSpace(r.URL.Query().Get("userId")) |
| } |
| if userID == "" { |
| jsonError(w, http.StatusUnauthorized, "Not authenticated") |
| return |
| } |
|
|
| postIDHex := r.URL.Query().Get("id") |
| postOID, err := primitive.ObjectIDFromHex(postIDHex) |
| if err != nil { |
| jsonError(w, http.StatusBadRequest, "Invalid post ID") |
| return |
| } |
|
|
| ctx := r.Context() |
|
|
| |
| var doc discussionDoc |
| if err := coll.FindOne(ctx, bson.M{"_id": postOID}).Decode(&doc); err != nil { |
| jsonError(w, http.StatusNotFound, "Discussion not found") |
| return |
| } |
|
|
| alreadyUpvoted := false |
| for _, uid := range doc.UpvotedBy { |
| if uid == userID { |
| alreadyUpvoted = true |
| break |
| } |
| } |
|
|
| var update bson.M |
| if alreadyUpvoted { |
| |
| update = bson.M{ |
| "$pull": bson.M{"upvotedBy": userID}, |
| "$inc": bson.M{"upvotes": -1}, |
| } |
| } else { |
| |
| update = bson.M{ |
| "$addToSet": bson.M{"upvotedBy": userID}, |
| "$inc": bson.M{"upvotes": 1}, |
| } |
| } |
|
|
| _, err = coll.UpdateOne(ctx, bson.M{"_id": postOID}, update) |
| if err != nil { |
| jsonError(w, http.StatusInternalServerError, "Failed to toggle upvote") |
| return |
| } |
|
|
| |
| if err := coll.FindOne(ctx, bson.M{"_id": postOID}).Decode(&doc); err != nil { |
| jsonError(w, http.StatusInternalServerError, "Failed to fetch updated discussion") |
| return |
| } |
|
|
| if doc.UpvotedBy == nil { |
| doc.UpvotedBy = []string{} |
| } |
|
|
| jsonWrite(w, http.StatusOK, map[string]any{ |
| "upvotes": doc.Upvotes, |
| "upvotedBy": doc.UpvotedBy, |
| }) |
| } |
| } |
|
|
| |
|
|
| func handleDeleteDiscussion(coll *mongo.Collection) http.HandlerFunc { |
| return func(w http.ResponseWriter, r *http.Request) { |
| if r.Method != http.MethodDelete { |
| jsonError(w, http.StatusMethodNotAllowed, "Method not allowed") |
| return |
| } |
|
|
| |
| userID, jwtErr := extractUserIDFromJWT(r) |
| if jwtErr != nil { |
| userID = strings.TrimSpace(r.URL.Query().Get("userId")) |
| } |
| if userID == "" { |
| jsonError(w, http.StatusUnauthorized, "Not authenticated") |
| return |
| } |
|
|
| postIDHex := r.URL.Query().Get("id") |
| postOID, err := primitive.ObjectIDFromHex(postIDHex) |
| if err != nil { |
| jsonError(w, http.StatusBadRequest, "Invalid post ID") |
| return |
| } |
|
|
| ctx := r.Context() |
|
|
| |
| var doc discussionDoc |
| if err := coll.FindOne(ctx, bson.M{"_id": postOID}).Decode(&doc); err != nil { |
| jsonError(w, http.StatusNotFound, "Discussion not found") |
| return |
| } |
|
|
| if doc.UserID != userID { |
| jsonError(w, http.StatusForbidden, "You can only delete your own submissions") |
| return |
| } |
|
|
| _, err = coll.DeleteOne(ctx, bson.M{"_id": postOID}) |
| if err != nil { |
| jsonError(w, http.StatusInternalServerError, "Failed to delete discussion") |
| return |
| } |
|
|
| jsonWrite(w, http.StatusOK, map[string]any{ |
| "success": true, |
| }) |
| } |
| } |
|
|