diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..7b1fede --- /dev/null +++ b/.air.toml @@ -0,0 +1,44 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = ".bin" + +[build] + args_bin = [] + bin = "./.bin/main" + cmd = "go build -o ./.bin/main ." + delay = 0 + exclude_dir = ["assets", ".bin", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/.env b/.env new file mode 100644 index 0000000..c07e3a4 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +PORT=8080 +MONGO_URL=mongodb+srv://root:root@cluster0.jgltqbs.mongodb.net/?retryWrites=true&w=majority +JWT_SECRET=secret \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index a6baac5..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Build - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - - build: - name: Build - runs-on: ubuntu-latest - steps: - - - name: Set up Go 1.x - uses: actions/setup-go@v2 - with: - go-version: ^1.13 - - - name: Check out code into the Go module directory - uses: actions/checkout@v2 - - - name: Get dependencies - run: | - go get -v -t -d ./... - if [ -f Gopkg.toml ]; then - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - dep ensure - fi - - - name: Build - run: go build -v . - -# - name: Test -# run: go test -v . diff --git a/.github/workflows/heroku-deploy-Prod.yml b/.github/workflows/heroku-deploy-Prod.yml new file mode 100644 index 0000000..322320d --- /dev/null +++ b/.github/workflows/heroku-deploy-Prod.yml @@ -0,0 +1,51 @@ +name: Prod - Heroku Deploy + +on: + # Job Trigger automatically if any PR is closed for main branch + pull_request: + branches: + - dev + types: [ closed ] + # Enabled Manual Trigger + workflow_dispatch: +jobs: + + build-deploy: + name: Deploy + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.19 + + - name: Cache + uses: actions/cache@v3 + with: + # In order: + # * Module download cache + # * Build cache (Linux) + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... + + - name: Deploy API -> Prod + uses: akhileshns/heroku-deploy@v3.12.12 + with: + heroku_api_key: ${{secrets.HEROKU_API_KEY}} + heroku_app_name: "challenge-api" + heroku_email: ${{secrets.HEROKU_EMAIL}} + branch: dev +# env_file: .env.default diff --git a/.github/workflows/heroku-deploy-QA.yml b/.github/workflows/heroku-deploy-QA.yml new file mode 100644 index 0000000..9b3d0a6 --- /dev/null +++ b/.github/workflows/heroku-deploy-QA.yml @@ -0,0 +1,50 @@ +name: QA - Heroku Deploy + +on: + # Job Trigger automatically if any code push/PR to QA branch + push: + branches: + - QA + # Enabled Manual Trigger + workflow_dispatch: + +jobs: + build-deploy: + name: Deploy + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.19 + + - name: Cache + uses: actions/cache@v3 + with: + # In order: + # * Module download cache + # * Build cache (Linux) + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... + + - name: Deploy API -> QA + uses: akhileshns/heroku-deploy@v3.12.12 + with: + heroku_api_key: ${{secrets.HEROKU_API_KEY}} + heroku_app_name: "challenge-api-qa" + heroku_email: ${{secrets.HEROKU_EMAIL}} + branch: QA +# env_file: .env diff --git a/.gitignore b/.gitignore index 0f87e70..d5e8410 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ main gin-bin -.env +# .env # exclude everything uploaded/* diff --git a/Dockerfile b/Dockerfile index bf9f6ab..af1d933 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,28 @@ +# FROM golang:latest + +# ENV GO111MODULE=on +# ENV PORT=8080 +# WORKDIR /app +# COPY go.mod /app +# COPY go.sum /app + +# RUN go mod download +# RUN go install -mod=mod github.com/githubnemo/CompileDaemon +# COPY . /app +# ENTRYPOINT CompileDaemon --build="go build -o main" --command=./main + FROM golang:latest -ENV GO111MODULE=on -ENV PORT=8080 -WORKDIR /app -COPY go.mod /app -COPY go.sum /app +RUN mkdir /golang + +RUN go install github.com/cosmtrek/air@latest + +ADD . /golang/ + +RUN go install github.com/cosmtrek/air@latest + +WORKDIR /golang RUN go mod download -RUN go get github.com/githubnemo/CompileDaemon -COPY . /app -ENTRYPOINT CompileDaemon --build="go build -o main" --command=./main \ No newline at end of file + +CMD ["air", "-c", ".air.toml"] \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..85926b9 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: challenge-platform-api diff --git a/controllers/betController.go b/controllers/betController.go new file mode 100644 index 0000000..afac450 --- /dev/null +++ b/controllers/betController.go @@ -0,0 +1,80 @@ +package controllers + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + middlewares "github.com/chattertechno/challenge-platform-api/handlers" + "github.com/chattertechno/challenge-platform-api/models" + "github.com/dgrijalva/jwt-go" + "github.com/gorilla/mux" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var AddBetChallenge = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + var bet models.Bet + err := json.NewDecoder(r.Body).Decode(&bet) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + ChallengeID, _ := primitive.ObjectIDFromHex(bet.ChallengeID) + + var challenge models.Challenge + collection := client.Database("challenge").Collection("challenges") + err = collection.FindOne(context.TODO(), bson.D{primitive.E{Key: "_id", Value: ChallengeID}}).Decode(&challenge) + if err != nil { + middlewares.ServerErrResponse(fmt.Sprintf("challenge %v not found", bet.ChallengeID), rw) + return + } + + if challenge.MinBetAmount > bet.Amount { + middlewares.ErrorResponse(fmt.Sprintf("bet must be greater than %v", challenge.MinBetAmount), rw) + return + } + + props, _ := r.Context().Value("props").(jwt.MapClaims) + identity := props["identity"].(string) + bet.Identity = identity + bet.CreatedAt = time.Now().UTC() + + betCollection := client.Database("challenge").Collection("bets") + result, err := betCollection.InsertOne(context.TODO(), bet) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + middlewares.SuccessRespond(result, rw) +}) + +var GetAllBetsForChallenge = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + var bets []*models.Bet + + betCollection := client.Database("challenge").Collection("bets") + + cursor, err := betCollection.Find(context.TODO(), bson.D{primitive.E{Key: "challenge_id", Value: params["id"]}}) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + for cursor.Next(context.TODO()) { + var bet models.Bet + err := cursor.Decode(&bet) + if err != nil { + log.Fatal(err) + } + bets = append(bets, &bet) + } + + if err := cursor.Err(); err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + middlewares.SuccessRespond(bets, rw) +}) diff --git a/controllers/challengeController.go b/controllers/challengeController.go index 41c96de..37902ff 100644 --- a/controllers/challengeController.go +++ b/controllers/challengeController.go @@ -3,23 +3,29 @@ package controllers import ( "context" "encoding/json" - "log" + "fmt" "net/http" + "strconv" "strings" "time" - middlewares "github.com/gaquarius/challenge-platform-api/handlers" - "github.com/gaquarius/challenge-platform-api/models" + middlewares "github.com/chattertechno/challenge-platform-api/handlers" + "github.com/chattertechno/challenge-platform-api/models" + "github.com/dgrijalva/jwt-go" "github.com/gorilla/mux" "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" ) // ListChallenge -> List all the challenges var ListChallenge = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { var challenges []*models.Challenge + opts := options.Find().SetSort(bson.D{primitive.E{Key: "created_at", Value: -1}}) + collection := client.Database("challenge").Collection("challenges") - cursor, err := collection.Find(context.TODO(), bson.D{}) + cursor, err := collection.Find(context.TODO(), bson.D{}, opts) if err != nil { middlewares.ServerErrResponse(err.Error(), rw) return @@ -29,7 +35,8 @@ var ListChallenge = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Reques var challenge models.Challenge err := cursor.Decode(&challenge) if err != nil { - log.Fatal(err) + middlewares.ServerErrResponse(err.Error(), rw) + return } challenges = append(challenges, &challenge) @@ -59,7 +66,8 @@ var GetChallenges = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Reques var challenge models.Challenge err := cursor.Decode(&challenge) if err != nil { - log.Fatal(err) + middlewares.ServerErrResponse(err.Error(), rw) + return } challenges = append(challenges, &challenge) } @@ -80,8 +88,13 @@ var CreateChallenge = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Requ middlewares.ServerErrResponse(err.Error(), rw) return } - challenge.CreatedAt = time.Now() - challenge.UpdatedAt = time.Now() + props, _ := r.Context().Value("props").(jwt.MapClaims) + challenge.Identity = props["identity"].(string) + + now := time.Now().UTC() + challenge.FundDeliveredFlag = false + challenge.CreatedAt = now + challenge.UpdatedAt = now collection := client.Database("challenge").Collection("challenges") result, err := collection.InsertOne(context.TODO(), challenge) if err != nil { @@ -89,7 +102,7 @@ var CreateChallenge = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Requ return } res, _ := json.Marshal(result.InsertedID) - middlewares.SuccessResponse(`Inserted at `+strings.Replace(string(res), `"`, ``, 2), rw) + middlewares.SuccessResponseWithData(`Inserted at `+strings.Replace(string(res), `"`, ``, 2), result.InsertedID, rw) }) // GetChallenge -> Get a challenge by id @@ -104,6 +117,31 @@ var GetChallenge = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request middlewares.ServerErrResponse(err.Error(), rw) return } + challengeJoined := client.Database("challenge").Collection("challengeJoined") + cursor, err := challengeJoined.Find(context.TODO(), bson.D{primitive.E{Key: "challenge_id", Value: params["id"]}}) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + + var participants []models.JoinChallenge + + for cursor.Next(context.TODO()) { + var participant models.JoinChallenge + err := cursor.Decode(&participant) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + participants = append(participants, participant) + } + + if err := cursor.Err(); err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + challenge.Participants = participants + middlewares.SuccessRespond(challenge, rw) }) @@ -136,3 +174,324 @@ var UpdateChallenge = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Requ var DeleteChallenge = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { }) + +var JoinChallenge = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + var req models.JoinChallenge + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + ChallengeID, _ := primitive.ObjectIDFromHex(req.ChallengeID) + + var challenge models.Challenge + + collection := client.Database("challenge").Collection("challenges") + err = collection.FindOne(context.TODO(), bson.D{primitive.E{Key: "_id", Value: ChallengeID}}).Decode(&challenge) + if err != nil { + if err == mongo.ErrNoDocuments { + middlewares.ErrorResponse("challenge does not exists", rw) + return + } + middlewares.ErrorResponse(err.Error(), rw) + return + } + + props, _ := r.Context().Value("props").(jwt.MapClaims) + identity := props["identity"].(string) + + challengeBet, err := strconv.ParseFloat(challenge.AddBet, 64) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + + if challengeBet > req.Bet { + middlewares.ErrorResponse(fmt.Sprintf("bet must be greater than %v", challengeBet), rw) + return + } + + req.Identity = identity + req.CreatedAt = time.Now().UTC() + + challengeJoined := client.Database("challenge").Collection("challengeJoined") + + filter := bson.M{ + "challenge_id": req.ChallengeID, + "identity": req.Identity, + } + + var getjoinedChallenge models.JoinChallenge + + err = challengeJoined.FindOne(context.TODO(), filter).Decode(&getjoinedChallenge) + if err != nil && err != mongo.ErrNoDocuments { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + + if getjoinedChallenge.ChallengeID != "" || getjoinedChallenge.Identity != "" { + if err != nil { + middlewares.ErrorResponse("you already joined this challenge", rw) + return + } + } + + result, err := challengeJoined.InsertOne(context.TODO(), req) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + + var bet models.Bet + bet.Identity = identity + bet.ChallengeID = req.ChallengeID + bet.Amount = req.Bet + + betCollection := client.Database("challenge").Collection("bets") + _, err = betCollection.InsertOne(context.TODO(), bet) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + res, _ := json.Marshal(result.InsertedID) + middlewares.SuccessResponse(`Inserted at `+strings.Replace(string(res), `"`, ``, 2), rw) +}) + +var UnJoinChallenge = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + + props, _ := r.Context().Value("props").(jwt.MapClaims) + identity := props["identity"].(string) + + filter := bson.M{ + "challenge_id": params["id"], + "identity": identity, + } + + challengeJoined := client.Database("challenge").Collection("challengeJoined") + + deleteResult, err := challengeJoined.DeleteOne(context.TODO(), filter) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + + if deleteResult.DeletedCount == 0 { + middlewares.ErrorResponse("challenge does not exists", rw) + return + } + + middlewares.SuccessResponse("unjoin challenge successfully", rw) +}) + +// ChallengeWinner -> Get all the winners of challenge +var ChallengeWinner = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + var challenges *models.Challenge + + id, _ := primitive.ObjectIDFromHex(params["id"]) + + collection := client.Database("challenge").Collection("challenges") + err := collection.FindOne(context.TODO(), bson.D{primitive.E{Key: "_id", Value: id}}).Decode(&challenges) + if err != nil { + if err == mongo.ErrNoDocuments { + middlewares.ServerErrResponse("challenge not found", rw) + return + } + middlewares.ServerErrResponse(err.Error(), rw) + return + } + + var steps []*models.Steps + + stepsCollection := client.Database("challenge").Collection("stepsDetails") + cursor, err := stepsCollection.Find(context.TODO(), bson.D{primitive.E{Key: "challenge_id", Value: params["id"]}}) + if err != nil { + if err == mongo.ErrNoDocuments { + middlewares.ServerErrResponse("users steps record not found for this challenge", rw) + return + } + middlewares.ServerErrResponse(err.Error(), rw) + return + } + for cursor.Next(context.TODO()) { + var step models.Steps + err := cursor.Decode(&step) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + steps = append(steps, &step) + } + if err := cursor.Err(); err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + + var bets []*models.Bet + + betCollection := client.Database("challenge").Collection("bets") + + cursor, err = betCollection.Find(context.TODO(), bson.D{primitive.E{Key: "challenge_id", Value: params["id"]}}) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + for cursor.Next(context.TODO()) { + var bet models.Bet + err := cursor.Decode(&bet) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + bets = append(bets, &bet) + } + + if err := cursor.Err(); err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + + checkChallengeWins := false + + goalThreshold, err := strconv.ParseFloat(challenges.GoalThreshold, 64) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + + for _, value := range steps { + stepsDistance, err := strconv.ParseFloat(value.StepsDistance, 64) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + if goalThreshold < stepsDistance { + checkChallengeWins = true + break + } + + } + + var winnerRecord []*models.Steps + + if checkChallengeWins { + + // if challenges.Goal == "distance" { + floatGoalThershold, err := strconv.ParseFloat(challenges.GoalThreshold, 32) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + for _, v := range steps { + floatUserDistance, err := strconv.ParseFloat(v.StepsDistance, 64) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + if floatUserDistance >= floatGoalThershold { + winnerRecord = append(winnerRecord, v) + } + } + // } + // else if challenges.Goal == "count" { + // intGoalThershold, err := strconv.ParseInt(challenges.GoalThreshold, 10, 64) + // if err != nil { + // middlewares.ServerErrResponse(err.Error(), rw) + // return + // } + // for _, v := range steps { + // if v.StepsCount > intGoalThershold { + // winnerRecord = append(winnerRecord, v) + // } + // } + // } + + var totalAmount float64 + + for _, bet := range bets { + totalAmount = totalAmount + bet.Amount + } + var count int64 + var winners []models.WinnerResponse + for _, winner := range winnerRecord { + for _, user := range bets { + var win models.WinnerResponse + if user.Identity == winner.Identity { + win.ChallengeID = user.ChallengeID + win.Identity = user.Identity + // win.Amount = averageAmount + count++ + winners = append(winners, win) + } + } + } + averageAmount := totalAmount / float64(count) + for i := range winners { + winners[i].Amount = averageAmount + } + middlewares.SuccessRespondWithCustomMessage(winners, "challenge winners", rw) + return + } + middlewares.SuccessRespondWithCustomMessage(bets, "no winner", rw) +}) + +var UpdateFlag = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + var challenge models.UpdateFlagRequest + + id, _ := primitive.ObjectIDFromHex(params["id"]) + challenge.FundDeliveredFlag = true + + collection := client.Database("challenge").Collection("challenges") + res, err := collection.UpdateOne(context.TODO(), bson.D{primitive.E{Key: "_id", Value: id}}, bson.D{primitive.E{Key: "$set", Value: challenge}}) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + + if res.MatchedCount == 0 { + middlewares.ErrorResponse("Challenge does not exist", rw) + return + } + + middlewares.SuccessResponse("Flag updated", rw) +}) + +var FinishedChallenges = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + var challenges []*models.GetChallenges + + collection := client.Database("challenge").Collection("challenges") + ctx := context.TODO() + currentDateTime := time.Now().Format("2006-01-02") + + filter := bson.M{ + "end_date": bson.M{"$gte": currentDateTime}, + "fund_delivered_flag": false, + } + + cursor, err := collection.Find(ctx, filter) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + defer cursor.Close(ctx) + + for cursor.Next(ctx) { + var challenge models.GetChallenges + err := cursor.Decode(&challenge) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + + challenges = append(challenges, &challenge) + } + + if err := cursor.Err(); err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + + middlewares.SuccessRespond(challenges, rw) +}) diff --git a/controllers/stepsController.go b/controllers/stepsController.go new file mode 100644 index 0000000..1871e9d --- /dev/null +++ b/controllers/stepsController.go @@ -0,0 +1,83 @@ +package controllers + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + middlewares "github.com/chattertechno/challenge-platform-api/handlers" + "github.com/chattertechno/challenge-platform-api/models" + "github.com/dgrijalva/jwt-go" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +var AddStepsChallenge = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + var steps models.Steps + err := json.NewDecoder(r.Body).Decode(&steps) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + ChallengeID, _ := primitive.ObjectIDFromHex(steps.ChallengeID) + + var challenge models.Challenge + collection := client.Database("challenge").Collection("challenges") + err = collection.FindOne(context.TODO(), bson.D{primitive.E{Key: "_id", Value: ChallengeID}}).Decode(&challenge) + if err != nil { + middlewares.ServerErrResponse(fmt.Sprintf("challenge %v not found", steps.ChallengeID), rw) + return + } + + stepsCollection := client.Database("challenge").Collection("stepsDetails") + + props, _ := r.Context().Value("props").(jwt.MapClaims) + identity := props["identity"].(string) + steps.Identity = identity + steps.CreatedAt = time.Now().UTC() + + filter := bson.M{ + "challenge_id": steps.ChallengeID, + "identity": steps.Identity, + } + var existedSteps models.Steps + err = stepsCollection.FindOne(context.TODO(), filter).Decode(&existedSteps) + if err != nil && err != mongo.ErrNoDocuments { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + if err == mongo.ErrNoDocuments { + res, err := stepsCollection.InsertOne(context.TODO(), steps) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + middlewares.SuccessRespond(res, rw) + return + } + if steps.StepsCount != 0 { + existedSteps.StepsCount = steps.StepsCount + } + if len(steps.StepsDistance) > 0 { + existedSteps.StepsDistance = steps.StepsDistance + } + if steps.MinimumStepsCount != 0 { + existedSteps.MinimumStepsCount = steps.MinimumStepsCount + } + if len(steps.MinimumStepsDistance) > 0 { + existedSteps.MinimumStepsDistance = steps.MinimumStepsDistance + } + if len(steps.Distance) > 0 { + existedSteps.Distance = steps.Distance + } + + _, err = stepsCollection.ReplaceOne(context.TODO(), bson.M{"_id": existedSteps.ID}, existedSteps) + if err != nil { + middlewares.ServerErrResponse(err.Error(), rw) + return + } + middlewares.SuccessRespond(fmt.Sprintf("steps updated at %v", existedSteps.ID.Hex()), rw) +}) diff --git a/controllers/userController.go b/controllers/userController.go index 395206a..7902536 100644 --- a/controllers/userController.go +++ b/controllers/userController.go @@ -9,11 +9,11 @@ import ( "os" "strings" + "github.com/chattertechno/challenge-platform-api/db" + middlewares "github.com/chattertechno/challenge-platform-api/handlers" + "github.com/chattertechno/challenge-platform-api/models" + "github.com/chattertechno/challenge-platform-api/validators" "github.com/dgrijalva/jwt-go" - "github.com/gaquarius/challenge-platform-api/db" - middlewares "github.com/gaquarius/challenge-platform-api/handlers" - "github.com/gaquarius/challenge-platform-api/models" - "github.com/gaquarius/challenge-platform-api/validators" "github.com/gorilla/mux" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" @@ -29,9 +29,10 @@ var RegisterUser = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request middlewares.ServerErrResponse(err.Error(), rw) return } + toLowerCase := strings.ToLower(user.Username) collection := client.Database("challenge").Collection("users") var existingUser models.User - err = collection.FindOne(r.Context(), bson.D{primitive.E{Key: "username", Value: user.Username}}).Decode(&existingUser) + err = collection.FindOne(r.Context(), bson.D{primitive.E{Key: "username", Value: toLowerCase}}).Decode(&existingUser) if err == nil { middlewares.ErrorResponse("Username is already taken.", rw) return @@ -41,11 +42,17 @@ var RegisterUser = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request middlewares.ErrorResponse("Identity is already in use.", rw) return } + err = collection.FindOne(r.Context(), bson.D{primitive.E{Key: "mnemonic", Value: user.Mnemonic}}).Decode(&existingUser) + if err == nil { + middlewares.ErrorResponse("Mnemonic Invalid", rw) + return + } passwordHash, err := middlewares.HashPassword(user.Password) if err != nil { middlewares.ServerErrResponse(err.Error(), rw) return } + user.Username = toLowerCase user.Password = passwordHash result, err := collection.InsertOne(r.Context(), user) if err != nil { @@ -64,9 +71,11 @@ var LoginUser = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { middlewares.ServerErrResponse(err.Error(), rw) return } + toLowerCase := strings.ToLower(user.Username) collection := client.Database("challenge").Collection("users") var existingUser models.User - err = collection.FindOne(r.Context(), bson.D{primitive.E{Key: "username", Value: user.Username}}).Decode(&existingUser) + err = collection.FindOne(r.Context(), bson.D{primitive.E{Key: "username", Value: toLowerCase}}).Decode(&existingUser) + if err != nil { middlewares.ErrorResponse("User doesn't exist", rw) return @@ -76,7 +85,7 @@ var LoginUser = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { middlewares.ErrorResponse("Password doesn't match", rw) return } - token, err := middlewares.GenerateJWT(user.Username) + token, err := middlewares.GenerateJWT(toLowerCase, existingUser.Identity, existingUser.PrivateKey) if err != nil { middlewares.ErrorResponse("Failed to generate JWT", rw) return @@ -153,7 +162,7 @@ var UpdateUser = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) return } - token, err := middlewares.GenerateJWT(newUser.Username) + token, err := middlewares.GenerateJWT(newUser.Username, user.Identity, user.PrivateKey) if err != nil { middlewares.ErrorResponse("Failed to generate JWT", rw) return diff --git a/db/db.go b/db/db.go index 0f9fde8..7738958 100644 --- a/db/db.go +++ b/db/db.go @@ -4,8 +4,8 @@ import ( "context" "log" + middlewares "github.com/chattertechno/challenge-platform-api/handlers" "github.com/fatih/color" - middlewares "github.com/gaquarius/challenge-platform-api/handlers" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) @@ -14,16 +14,17 @@ var client *mongo.Client // Dbconnect -> connects mongo func Dbconnect() *mongo.Client { + clientOptions := options.Client().ApplyURI(middlewares.DotEnvVariable("MONGO_URL")) client, err := mongo.Connect(context.TODO(), clientOptions) if err != nil { - log.Fatal("⛒ Connection Failed to Database") + log.Fatal("⛒ Connection Failed to Database 1") log.Fatal(err) } // Check the connection err = client.Ping(context.TODO(), nil) if err != nil { - log.Fatal("⛒ Connection Failed to Database") + log.Fatal("⛒ Connection Failed to Database 2") log.Fatal(err) } color.Green("⛁ Connected to Database") diff --git a/docker-compose.yml b/docker-compose.yml index aa70b85..6552b31 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,26 +1,35 @@ version: '3.3' services: + # api: + # build: . + # ports: + # - '3100:8080' + # volumes: + # - .:/app + # depends_on: + # - 'mongo' + # web: + # container_name: 'web' + # image: 'nginx:latest' + # ports: + # - '80:80' + # volumes: + # - ./nginx/dev.conf.d:/etc/nginx/conf.d + # depends_on: + # - 'api' + # mongo: + # image: 'mongo:latest' + # container_name: 'mongo' + # ports: + # - '27100:27017' + # volumes: + # - ./data/dev/mongo:/data/db api: - build: . + container_name: api + build: + context: . + dockerfile: Dockerfile ports: - - '3100:8080' + - 8080:8080 volumes: - - .:/app - depends_on: - - 'mongo' - web: - container_name: 'web' - image: 'nginx:latest' - ports: - - '80:80' - volumes: - - ./nginx/dev.conf.d:/etc/nginx/conf.d - depends_on: - - 'api' - mongo: - image: 'mongo:latest' - container_name: 'mongo' - ports: - - '27100:27017' - volumes: - - ./data/dev/mongo:/data/db + - ./:/api diff --git a/go.mod b/go.mod index 8bd6b94..2193009 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/gaquarius/challenge-platform-api +module github.com/chattertechno/challenge-platform-api // +heroku goVersion go1.16 go 1.15 diff --git a/handlers/response.go b/handlers/response.go index b121203..268ffdc 100644 --- a/handlers/response.go +++ b/handlers/response.go @@ -4,7 +4,7 @@ import ( "encoding/json" "net/http" - "github.com/gaquarius/challenge-platform-api/models" + "github.com/chattertechno/challenge-platform-api/models" ) // AuthorizationResponse -> response authorize @@ -80,6 +80,25 @@ func SuccessRespond(fields interface{}, writer http.ResponseWriter) { json.NewEncoder(writer).Encode(temp) } +// SuccessRespond -> response formatter +func SuccessRespondWithCustomMessage(fields interface{}, msg string, writer http.ResponseWriter) { + _, err := json.Marshal(fields) + type data struct { + Person interface{} `json:"data"` + Statuscode int `json:"status"` + Message string `json:"msg"` + } + temp := &data{Person: fields, Statuscode: 200, Message: msg} + if err != nil { + ServerErrResponse(err.Error(), writer) + } + + //Send header, status code and output to writer + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) + json.NewEncoder(writer).Encode(temp) +} + // SuccessResponse -> success formatter func SuccessResponse(msg string, writer http.ResponseWriter) { type errdata struct { @@ -94,6 +113,20 @@ func SuccessResponse(msg string, writer http.ResponseWriter) { json.NewEncoder(writer).Encode(temp) } +func SuccessResponseWithData(msg string, Id interface{}, writer http.ResponseWriter) { + type errdata struct { + Statuscode int `json:"status"` + Message string `json:"msg"` + Id interface{} `json:"id"` + } + temp := &errdata{Statuscode: 200, Message: msg, Id: Id} + + //Send header, status code and output to writer + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) + json.NewEncoder(writer).Encode(temp) +} + // ErrorResponse -> error formatter func ErrorResponse(error string, writer http.ResponseWriter) { type errdata struct { @@ -108,6 +141,20 @@ func ErrorResponse(error string, writer http.ResponseWriter) { json.NewEncoder(writer).Encode(temp) } +// ForbiddenResponse -> error formatter +func ForbiddenResponse(msg string, writer http.ResponseWriter) { + type errdata struct { + Statuscode int `json:"status"` + Message string `json:"msg"` + } + temp := &errdata{Statuscode: http.StatusForbidden, Message: msg} + + //Send header, status code and output to writer + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(writer).Encode(temp) +} + // ServerErrResponse -> server error formatter func ServerErrResponse(error string, writer http.ResponseWriter) { type servererrdata struct { diff --git a/handlers/verifyjwt.go b/handlers/verifyjwt.go index e603d9a..b68410f 100644 --- a/handlers/verifyjwt.go +++ b/handlers/verifyjwt.go @@ -13,7 +13,9 @@ import ( var JWT_SECRET = []byte(DotEnvVariable("JWT_SECRET")) type Claims struct { - Username string `json:"username"` + Username string `json:"username"` + Identity string `json:"identity"` + PrivateKey string `json:"private_key"` jwt.StandardClaims } @@ -21,6 +23,7 @@ type Claims struct { func IsAuthorized(next http.Handler) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader := strings.Split(r.Header.Get("Authorization"), "Bearer ") + //fmt.Println(authHeader) if len(authHeader) != 2 { AuthorizationResponse("Malformed JWT token", w) @@ -44,17 +47,20 @@ func IsAuthorized(next http.Handler) http.HandlerFunc { } // GenerateJWT -> generate jwt -func GenerateJWT(username string) (string, error) { +func GenerateJWT(username, identity, privateKey string) (string, error) { claims := &Claims{ - Username: username, + Username: username, + Identity: identity, + PrivateKey: privateKey, StandardClaims: jwt.StandardClaims{ - ExpiresAt: time.Now().Add(2 * time.Hour).Unix(), + ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString(JWT_SECRET) + //fmt.Println(`this is it`, tokenString) if err != nil { return "", err diff --git a/main.go b/main.go index 8592f20..171fbba 100644 --- a/main.go +++ b/main.go @@ -4,9 +4,9 @@ import ( "log" "net/http" + middlewares "github.com/chattertechno/challenge-platform-api/handlers" + "github.com/chattertechno/challenge-platform-api/routes" "github.com/fatih/color" - middlewares "github.com/gaquarius/challenge-platform-api/handlers" - "github.com/gaquarius/challenge-platform-api/routes" "github.com/rs/cors" ) diff --git a/models/models.go b/models/models.go index 835f13f..b4acf79 100644 --- a/models/models.go +++ b/models/models.go @@ -15,13 +15,15 @@ type Person struct { // User Model type User struct { - ID primitive.ObjectID `json:"_id,omitempty" bson:"_id,omitempty"` - Username string `json:"username,omitempty" bson:"username,omitempty"` - Role string `json:"role,omitempty" bson:"role,omitempty"` - Bio string `json:"bio,omitempty" bson:"bio,omitempty"` - Avatar string `json:"avatar,omitempty" bson:"avatar,omitempty"` - Identity string `json:"identity,omitempty" bson:"identity,omitempty"` - Password string `json:"password,omitempty" bson:"password,omitempty"` + ID primitive.ObjectID `json:"_id,omitempty" bson:"_id,omitempty"` + Username string `json:"username,omitempty" bson:"username,omitempty"` + Role string `json:"role,omitempty" bson:"role,omitempty"` + Bio string `json:"bio,omitempty" bson:"bio,omitempty"` + Avatar string `json:"avatar,omitempty" bson:"avatar,omitempty"` + Identity string `json:"identity,omitempty" bson:"identity,omitempty"` + Mnemonic string `json:"mnemonic,omitempty" bson:"mnemonic,omitempty"` + PrivateKey string `json:"private_key,omitempty" bson:"private_key,omitempty"` + Password string `json:"password,omitempty" bson:"password,omitempty"` } type ChallengeStatus string @@ -42,14 +44,22 @@ type Challenge struct { Goal string `json:"goal,omitempty" bson:"goal,omitempty"` GoalIncreaments string `json:"goal_increaments,omitempty" bson:"goal_increaments,omitempty"` GoalThreshold string `json:"goal_threshold,omitempty" bson:"goal_threshold,omitempty"` + AddBet string `json:"add_bet,omitempty" bson:"add_bet,omitempty"` Category []string `json:"category,omitempty" bson:"category,omitempty"` Name string `json:"name,omitempty" bson:"name,omitempty"` Description string `json:"description,omitempty" bson:"description,omitempty"` - Content string `json:"content,omitempty" bson:"content,omitempty"` - HeaderImage string `json:"header_image,omitempty" bson:"header_image,omitempty"` - Coordinator string `json:"coordinator,omitempty" bson:"coordinator,omitempty"` - CreatedAt time.Time `json:"created_at,omitempty" bson:"created_at,omitempty"` - UpdatedAt time.Time `json:"updated_at,omitempty" bson:"updated_at,omitempty"` + // Mnemonic string `json:"mnemonic,omitempty" bson:"mnemonic,omitempty"` + FundDeliveredFlag bool `json:"fund_delivered_flag" bson:"fund_delivered_flag"` + Content string `json:"content,omitempty" bson:"content,omitempty"` + HeaderImage string `json:"header_image,omitempty" bson:"header_image,omitempty"` + Coordinator string `json:"coordinator,omitempty" bson:"coordinator,omitempty"` + Identity string `json:"identity,omitempty" bson:"identity,omitempty"` + Visible bool `json:"visible,omitempty" bson:"visible,omitempty"` + RecipientAddress string `json:"recipient_address,omitempty" bson:"recipient_address,omitempty"` + MinBetAmount float64 `json:"min_bet_amount,omitempty" bson:"min_bet_amount,omitempty"` + Participants []JoinChallenge `json:"participant,omitempty" bson:"participant,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty" bson:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty" bson:"updated_at,omitempty"` } type EscrowStatus string @@ -85,3 +95,52 @@ type Activity struct { CreatedAt time.Time `json:"created_at,omitempty" bson:"created_at,omitempty"` UpdatedAt time.Time `json:"updated_at,omitempty" bson:"updated_at,omitempty"` } + +type Bet struct { + ID primitive.ObjectID `json:"_id,omitempty" bson:"_id,omitempty"` + Identity string `json:"identity,omitempty" bson:"identity,omitempty"` + Amount float64 `json:"amount,omitempty" bson:"amount,omitempty"` + ChallengeID string `json:"challenge_id,omitempty" bson:"challenge_id,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty" bson:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty" bson:"updated_at,omitempty"` +} + +type JoinChallenge struct { + ID primitive.ObjectID `json:"_id,omitempty" bson:"_id,omitempty"` + Identity string `json:"identity,omitempty" bson:"identity,omitempty"` + Bet float64 `json:"bet,omitempty" bson:"bet,omitempty"` + ChallengeID string `json:"challenge_id,omitempty" bson:"challenge_id,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty" bson:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty" bson:"updated_at,omitempty"` +} + +type Steps struct { + ID primitive.ObjectID `json:"_id,omitempty" bson:"_id,omitempty"` + Identity string `json:"identity,omitempty" bson:"identity,omitempty"` + StepsCount int64 `json:"steps_count,omitempty" bson:"steps_count,omitempty"` + StepsDistance string `json:"steps_distance,omitempty" bson:"steps_distance,omitempty"` + Distance string `json:"distance,omitempty" bson:"distance,omitempty"` + MinimumStepsCount int64 `json:"minimum_steps_count,omitempty" bson:"minimum_steps_count,omitempty"` + MinimumStepsDistance string `json:"minimum_steps_distance,omitempty" bson:"minimum_steps_distance,omitempty"` + ChallengeID string `json:"challenge_id,omitempty" bson:"challenge_id,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty" bson:"created_at,omitempty"` +} + +type WinnerResponse struct { + Identity string `json:"identity,omitempty" bson:"identity,omitempty"` + ChallengeID string `json:"challenge_id,omitempty" bson:"challenge_id,omitempty"` + Amount float64 `json:"amount,omitempty" bson:"amount,omitempty"` +} + +type UpdateFlagRequest struct { + FundDeliveredFlag bool `json:"fund_delivered_flag" bson:"fund_delivered_flag"` +} + +type GetChallenges struct { + ID primitive.ObjectID `json:"_id,omitempty" bson:"_id,omitempty"` + StartDate string `json:"start_date,omitempty" bson:"start_date,omitempty"` + EndDate string `json:"end_date,omitempty" bson:"end_date,omitempty"` + FundDeliveredFlag bool `json:"fund_delivered_flag" bson:"fund_delivered_flag"` + Coordinator string `json:"coordinator,omitempty" bson:"coordinator,omitempty"` + Identity string `json:"identity,omitempty" bson:"identity,omitempty"` +} diff --git a/routes/routes.go b/routes/routes.go index 9d39e99..1f09a87 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -3,8 +3,8 @@ package routes import ( "net/http" - "github.com/gaquarius/challenge-platform-api/controllers" - middlewares "github.com/gaquarius/challenge-platform-api/handlers" + "github.com/chattertechno/challenge-platform-api/controllers" + middlewares "github.com/chattertechno/challenge-platform-api/handlers" "github.com/gorilla/mux" ) @@ -32,6 +32,21 @@ func Routes() *mux.Router { challenge.HandleFunc("/{id}", middlewares.IsAuthorized(controllers.GetChallenge)).Methods("GET") challenge.HandleFunc("/{id}", middlewares.IsAuthorized(controllers.UpdateChallenge)).Methods("PUT") challenge.HandleFunc("/{id}", middlewares.IsAuthorized(controllers.DeleteChallenge)).Methods("DELETE") + challenge.HandleFunc("/join/", middlewares.IsAuthorized(controllers.JoinChallenge)).Methods("POST") + challenge.HandleFunc("/{id}/unjoin/", middlewares.IsAuthorized(controllers.UnJoinChallenge)).Methods("POST") + challenge.HandleFunc("/{id}/winner/", controllers.ChallengeWinner).Methods("GET") + + challenge.HandleFunc("/finished/", controllers.FinishedChallenges).Methods("GET") + challenge.HandleFunc("/update/flag/{id}", controllers.UpdateFlag).Methods("PUT") + + // Challenge bet routes + bet := challenge.PathPrefix("/bet").Subrouter() + bet.HandleFunc("/add/", middlewares.IsAuthorized(controllers.AddBetChallenge)).Methods("POST") + bet.HandleFunc("/{id}", middlewares.IsAuthorized(controllers.GetAllBetsForChallenge)).Methods("GET") + + // User steps routes + steps := challenge.PathPrefix("/user/steps").Subrouter() + steps.HandleFunc("/add/", middlewares.IsAuthorized(controllers.AddStepsChallenge)).Methods("PUT") api.HandleFunc("/person", controllers.CreatePersonEndpoint).Methods("POST") api.HandleFunc("/people", middlewares.IsAuthorized(controllers.GetPeopleEndpoint)).Methods("GET")