RYP / cmd /server /discussions.go
Soumya79's picture
Upload 1361 files
f91a684 verified
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"
)
// ── Discussion document ──────────────────────────────────────────────
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"`
}
// ── Helpers ──────────────────────────────────────────────────────────
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
}
// ── Create indexes ──────────────────────────────────────────────────
func ensureDiscussionIndexes(coll *mongo.Collection) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Fast lookup by problemId, sorted by newest first
_, _ = coll.Indexes().CreateOne(ctx, mongo.IndexModel{
Keys: bson.D{{Key: "problemId", Value: 1}, {Key: "createdAt", Value: -1}},
})
// Fast duplicate check: same user + problem + language
_, _ = coll.Indexes().CreateOne(ctx, mongo.IndexModel{
Keys: bson.D{{Key: "userId", Value: 1}, {Key: "problemId", Value: 1}, {Key: "language", Value: 1}},
})
}
// ── GET /api/discussions?problemId=...&page=1&limit=10 ──────────────
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{}
}
// Ensure upvotedBy is never null in JSON
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,
})
}
}
// ── POST /api/discussions ───────────────────────────────────────────
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
}
// Try JWT first, fall back to body userId for local-demo users
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
}
// Determine username: try users collection first, then fall back to body
username := strings.TrimSpace(body.Username)
if jwtErr == nil {
// If we got a real JWT, try to fetch display name from DB
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()
// Upsert: same user + problem + language β†’ update, otherwise insert
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 {
// Fetch the existing doc ID
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,
})
}
}
// ── PATCH /api/discussions/upvote?id=... ────────────────────────────
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
}
// Try JWT first, fall back to query param for local-demo users
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()
// Check if user already upvoted
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 {
// Remove upvote
update = bson.M{
"$pull": bson.M{"upvotedBy": userID},
"$inc": bson.M{"upvotes": -1},
}
} else {
// Add upvote
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
}
// Re-fetch to return updated state
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,
})
}
}
// ── DELETE /api/discussions?id=... ──────────────────────────────────
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
}
// Try JWT first, fall back to query param for local-demo users
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()
// Ownership check
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,
})
}
}