github.com/DapperCollectives/CAST/backend@v0.0.0-20230921221157-1350c8be7c96/main/server/controllers.go (about) 1 package server 2 3 import ( 4 "encoding/json" 5 "errors" 6 "io" 7 "net/http" 8 "strconv" 9 10 "github.com/DapperCollectives/CAST/backend/main/models" 11 "github.com/DapperCollectives/CAST/backend/main/shared" 12 "github.com/gorilla/mux" 13 "github.com/rs/zerolog/log" 14 ) 15 16 type errorResponse struct { 17 StatusCode int `json:"statusCode,string"` 18 ErrorCode string `json:"errorCode"` 19 Message string `json:"message"` 20 Details string `json:"details"` 21 } 22 23 var ( 24 errIncompleteRequest = errorResponse{ 25 StatusCode: http.StatusBadRequest, 26 ErrorCode: "ERR_1001", 27 Message: "Error", 28 Details: "There was an error trying to complete your request", 29 } 30 31 errCreateCommunity = errorResponse{ 32 StatusCode: http.StatusBadRequest, 33 ErrorCode: "ERR_1002", 34 Message: "Error", 35 Details: "There was an error trying to create your community", 36 } 37 38 errFetchingBalance = errorResponse{ 39 StatusCode: http.StatusBadRequest, 40 ErrorCode: "ERR_1003", 41 Message: "Error Fetching Balance", 42 Details: `While confirming your balance, we've encountered an error 43 connecting to the Flow Blockchain.`, 44 } 45 46 errInsufficientBalance = errorResponse{ 47 StatusCode: http.StatusUnauthorized, 48 ErrorCode: "ERR_1004", 49 Message: "Insufficient Balance", 50 Details: `In order to vote on this proposal you must have a minimum 51 balance of %f %s tokens in your wallet.`, 52 } 53 54 errForbidden = errorResponse{ 55 StatusCode: http.StatusForbidden, 56 ErrorCode: "ERR_1005", 57 Message: "Forbidden", 58 Details: "You are not authorized to perform this action.", 59 } 60 61 errCreateProposal = errorResponse{ 62 StatusCode: http.StatusForbidden, 63 ErrorCode: "ERR_1006", 64 Message: "Error", 65 Details: "There was an error trying to create your proposal", 66 } 67 68 errUpdateCommunity = errorResponse{ 69 StatusCode: http.StatusForbidden, 70 ErrorCode: "ERR_1007", 71 Message: "Error", 72 Details: "There was an error trying to update your community", 73 } 74 75 errStrategyNotFound = errorResponse{ 76 StatusCode: http.StatusNotFound, 77 ErrorCode: "ERR_1008", 78 Message: "Strategy Not Found", 79 Details: "The strategy name you are trying to use no longer exists.", 80 } 81 82 errAlreadyVoted = errorResponse{ 83 StatusCode: http.StatusBadRequest, 84 ErrorCode: "ERR_1009", 85 Message: "Error", 86 Details: "Address %s has already voted for proposal %d.", 87 } 88 89 errInactiveProposal = errorResponse{ 90 StatusCode: http.StatusBadRequest, 91 ErrorCode: "ERR_1010", 92 Message: "Error", 93 Details: "Cannot vote on an inactive proposal.", 94 } 95 96 errGetCommunity = errorResponse{ 97 StatusCode: http.StatusInternalServerError, 98 ErrorCode: "ERR_1011", 99 Message: "Error", 100 Details: "There was an error retrieving the community.", 101 } 102 103 errCreateVote = errorResponse{ 104 StatusCode: http.StatusInternalServerError, 105 ErrorCode: "ERR_1012", 106 Message: "Error", 107 Details: "There was an error creating the vote.", 108 } 109 110 nilErr = errorResponse{} 111 ) 112 113 func (a *App) health(w http.ResponseWriter, r *http.Request) { 114 respondWithJSON(w, http.StatusOK, "OK!!") 115 } 116 117 func (a *App) upload(w http.ResponseWriter, r *http.Request) { 118 r.Body = http.MaxBytesReader(w, r.Body, maxFileSize) 119 if err := r.ParseMultipartForm(maxFileSize); err != nil { 120 log.Error().Err(err).Msgf("File cannot be larger than max file size of %v.\n", maxFileSize) 121 respondWithError(w, errIncompleteRequest) 122 return 123 } 124 125 resp, err := helpers.uploadFile(r) 126 if err != nil { 127 log.Error().Err(err).Msg("Error uploading file.") 128 respondWithError(w, errIncompleteRequest) 129 return 130 } 131 132 respondWithJSON(w, http.StatusOK, resp) 133 } 134 135 // Votes 136 func (a *App) getResultsForProposal(w http.ResponseWriter, r *http.Request) { 137 vars := mux.Vars(r) 138 proposal, err := helpers.fetchProposal(vars, "proposalId") 139 140 votes, err := models.GetAllVotesForProposal(a.DB, proposal.ID, *proposal.Strategy) 141 if err != nil { 142 log.Error().Err(err).Msg("Error getting votes for proposal.") 143 respondWithError(w, errIncompleteRequest) 144 return 145 } 146 147 results, err := helpers.useStrategyTally(proposal, votes) 148 if err != nil { 149 log.Error().Err(err).Msg("Error tallying votes.") 150 respondWithError(w, errIncompleteRequest) 151 return 152 } 153 154 if *proposal.Computed_status == "closed" && !proposal.Achievements_done { 155 if err := models.AddWinningVoteAchievement(a.DB, votes, results); err != nil { 156 log.Error().Err(err).Msg("Error calculating winning votes") 157 respondWithError(w, errIncompleteRequest) 158 } 159 } 160 161 respondWithJSON(w, http.StatusOK, results) 162 } 163 164 func (a *App) getVotesForProposal(w http.ResponseWriter, r *http.Request) { 165 vars := mux.Vars(r) 166 proposal, err := helpers.fetchProposal(vars, "proposalId") 167 if err != nil { 168 log.Error().Err(err).Msg("Invalid Proposal ID.") 169 respondWithError(w, errIncompleteRequest) 170 return 171 } 172 173 votes, order, err := helpers.getPaginatedVotes(r, proposal) 174 if err != nil { 175 log.Error().Err(err).Msg("error getting paginated votes") 176 respondWithError(w, errIncompleteRequest) 177 return 178 } 179 180 votesWithWeights, err := helpers.useStrategyGetVotes(proposal, votes) 181 if err != nil { 182 log.Error().Err(err).Msg("error calling useStrategyGetVotes") 183 respondWithError(w, errIncompleteRequest) 184 return 185 } 186 187 response := shared.GetPaginatedResponseWithPayload(votesWithWeights, order) 188 respondWithJSON(w, http.StatusOK, response) 189 } 190 191 func (a *App) getVoteForAddress(w http.ResponseWriter, r *http.Request) { 192 vars := mux.Vars(r) 193 addr := vars["addr"] 194 195 proposal, err := helpers.fetchProposal(vars, "proposalId") 196 if err != nil { 197 log.Error().Err(err).Msg("Invalid Proposal ID.") 198 respondWithError(w, errIncompleteRequest) 199 return 200 } 201 202 vote, err := helpers.processVote(addr, proposal) 203 if err != nil { 204 log.Error().Err(err).Msg("Error processing vote.") 205 respondWithError(w, errIncompleteRequest) 206 return 207 } 208 209 respondWithJSON(w, http.StatusOK, vote) 210 } 211 212 func (a *App) getVotesForAddress(w http.ResponseWriter, r *http.Request) { 213 var proposalIds []int 214 215 vars := mux.Vars(r) 216 addr := vars["addr"] 217 218 err := json.Unmarshal([]byte(r.FormValue("proposalIds")), &proposalIds) 219 if err != nil { 220 log.Error().Err(err).Msg("Error unmarshalling proposalIds") 221 respondWithError(w, errIncompleteRequest) 222 return 223 } 224 225 pageParams := getPageParams(*r, 25) 226 227 votes, pageParams, err := helpers.processVotes(addr, proposalIds, pageParams) 228 if err != nil { 229 log.Error().Err(err).Msg("Error processing votes.") 230 respondWithError(w, errIncompleteRequest) 231 return 232 } 233 234 response := shared.GetPaginatedResponseWithPayload(votes, pageParams) 235 respondWithJSON(w, http.StatusOK, response) 236 } 237 238 func (a *App) createVoteForProposal(w http.ResponseWriter, r *http.Request) { 239 vars := mux.Vars(r) 240 241 proposal, err := helpers.fetchProposal(vars, "proposalId") 242 if err != nil { 243 log.Error().Err(err).Msg("Invalid Proposal ID.") 244 respondWithError(w, errIncompleteRequest) 245 return 246 } 247 248 vote, errResponse := helpers.createVote(r, proposal) 249 if errResponse != nilErr { 250 log.Error().Err(err).Msg("Error creating vote.") 251 respondWithError(w, errResponse) 252 return 253 } 254 255 respondWithJSON(w, http.StatusCreated, vote) 256 } 257 258 // Proposals 259 func (a *App) getProposalsForCommunity(w http.ResponseWriter, r *http.Request) { 260 vars := mux.Vars(r) 261 communityId, err := strconv.Atoi(vars["communityId"]) 262 263 if err != nil { 264 log.Error().Err(err).Msg("Invalid Community ID") 265 respondWithError(w, errIncompleteRequest) 266 return 267 } 268 269 pageParams := getPageParams(*r, 25) 270 status := r.FormValue("status") 271 272 proposals, totalRecords, err := models.GetProposalsForCommunity( 273 a.DB, 274 communityId, 275 status, 276 pageParams, 277 ) 278 if err != nil { 279 log.Error().Err(err).Msg("Error getting proposals for community.") 280 respondWithError(w, errIncompleteRequest) 281 return 282 } 283 284 pageParams.TotalRecords = totalRecords 285 286 response := shared.GetPaginatedResponseWithPayload(proposals, pageParams) 287 respondWithJSON(w, http.StatusOK, response) 288 } 289 290 func (a *App) getProposal(w http.ResponseWriter, r *http.Request) { 291 vars := mux.Vars(r) 292 p, err := helpers.fetchProposal(vars, "id") 293 if err != nil { 294 log.Error().Err(err).Msg("Invalid Proposal ID.") 295 respondWithError(w, errIncompleteRequest) 296 return 297 } 298 299 c, err := helpers.fetchCommunity(p.Community_id) 300 if err != nil { 301 log.Error().Err(err).Msg("error fetching community") 302 respondWithError(w, errIncompleteRequest) 303 return 304 } 305 306 _, err = models.MatchStrategyByProposal(*c.Strategies, *p.Strategy) 307 if err != nil { 308 log.Error().Err(err).Msg("error getting strategy by proposal") 309 respondWithError(w, errIncompleteRequest) 310 return 311 } 312 313 respondWithJSON(w, http.StatusOK, p) 314 } 315 316 func (a *App) createProposal(w http.ResponseWriter, r *http.Request) { 317 vars := mux.Vars(r) 318 communityId, err := strconv.Atoi(vars["communityId"]) 319 if err != nil { 320 log.Error().Err(err).Msg("Invalid Community ID") 321 respondWithError(w, errIncompleteRequest) 322 return 323 } 324 325 var p models.Proposal 326 p.Community_id = communityId 327 328 if err := validatePayload(r.Body, &p); err != nil { 329 log.Error().Err(err).Msg("Error validating payload") 330 respondWithError(w, errIncompleteRequest) 331 return 332 } 333 334 proposal, errResponse := helpers.createProposal(p) 335 if errResponse != nilErr { 336 log.Error().Err(err).Msg("Error creating proposal") 337 respondWithError(w, errResponse) 338 return 339 } 340 341 respondWithJSON(w, http.StatusCreated, proposal) 342 } 343 344 func (a *App) updateProposal(w http.ResponseWriter, r *http.Request) { 345 vars := mux.Vars(r) 346 p, err := helpers.fetchProposal(vars, "id") 347 if err != nil { 348 log.Error().Err(err).Msg("Invalid Proposal ID.") 349 respondWithError(w, errIncompleteRequest) 350 return 351 } 352 353 var payload models.UpdateProposalRequestPayload 354 if err := validatePayload(r.Body, &payload); err != nil { 355 log.Error().Err(err).Msg("Error validating payload") 356 respondWithError(w, errIncompleteRequest) 357 return 358 } 359 360 // Check that status update is valid 361 // For now we are assuming proposals are creating with 362 // status 'published' and may be cancelled. 363 if payload.Status != "cancelled" { 364 log.Error().Err(err).Msg("Invalid status update") 365 respondWithError(w, errIncompleteRequest) 366 return 367 } 368 369 if payload.Voucher != nil { 370 if err := helpers.validateUserWithRoleViaVoucher( 371 payload.Signing_addr, 372 payload.Voucher, 373 p.Community_id, 374 "author"); err != nil { 375 log.Error().Err(err).Msg("Error validating user with role via voucher") 376 respondWithError(w, errForbidden) 377 return 378 } 379 } else { 380 if err := helpers.validateUserWithRole( 381 payload.Signing_addr, 382 payload.Timestamp, 383 payload.Composite_signatures, 384 p.Community_id, 385 "author"); err != nil { 386 log.Error().Err(err).Msg("Error validating user with role") 387 respondWithError(w, errForbidden) 388 return 389 } 390 } 391 392 p.Status = &payload.Status 393 p.Cid, err = helpers.pinJSONToIpfs(p) 394 if err != nil { 395 log.Error().Err(err).Msg("Error pinning proposal to IPFS") 396 respondWithError(w, errIncompleteRequest) 397 return 398 } 399 400 if err := p.UpdateProposal(a.DB); err != nil { 401 log.Error().Err(err).Msg("Error updating proposal") 402 respondWithError(w, errIncompleteRequest) 403 return 404 } 405 406 respondWithJSON(w, http.StatusOK, p) 407 } 408 409 // Communities 410 func (a *App) getCommunities(w http.ResponseWriter, r *http.Request) { 411 pageParams := getPageParams(*r, 25) 412 413 communities, totalRecords, err := models.GetCommunities(a.DB, pageParams) 414 if err != nil { 415 log.Error().Err(err).Msg("Error fetching communities") 416 respondWithError(w, errIncompleteRequest) 417 return 418 } 419 420 pageParams.TotalRecords = totalRecords 421 response := shared.GetPaginatedResponseWithPayload(communities, pageParams) 422 423 respondWithJSON(w, http.StatusOK, response) 424 } 425 426 func (a *App) searchCommunities(w http.ResponseWriter, r *http.Request) { 427 pageParams := getPageParams(*r, 25) 428 filters := r.FormValue("filters") 429 searchText := r.FormValue("text") 430 431 results, totalRecords, categories, err := helpers.searchCommunities( 432 searchText, 433 filters, 434 pageParams, 435 ) 436 if err != nil { 437 log.Error().Err(err).Msg("Error searching communities") 438 respondWithError(w, errIncompleteRequest) 439 } 440 441 pageParams.TotalRecords = totalRecords 442 443 paginatedResults, err := helpers.appendFiltersToResponse( 444 results, 445 pageParams, 446 categories, 447 ) 448 if err != nil { 449 log.Error().Err(err).Msg("Error appending filters to response") 450 respondWithError(w, errIncompleteRequest) 451 } 452 453 respondWithJSON(w, http.StatusOK, paginatedResults) 454 } 455 456 func (a *App) getCommunity(w http.ResponseWriter, r *http.Request) { 457 vars := mux.Vars(r) 458 id, err := strconv.Atoi(vars["id"]) 459 if err != nil { 460 log.Error().Err(err).Msg("Invalid Community ID") 461 respondWithError(w, errIncompleteRequest) 462 return 463 } 464 465 c, err := helpers.fetchCommunity(id) 466 if err != nil { 467 log.Error().Err(err).Msg("Error fetching community") 468 respondWithError(w, errIncompleteRequest) 469 return 470 } 471 472 respondWithJSON(w, http.StatusOK, c) 473 } 474 475 func (a *App) getCommunitiesForHomePage(w http.ResponseWriter, r *http.Request) { 476 pageParams := getPageParams(*r, 25) 477 isSearch := false 478 479 communities, totalRecords, err := models.GetDefaultCommunities( 480 a.DB, 481 pageParams, 482 []string{}, 483 isSearch, 484 ) 485 if err != nil { 486 log.Error().Err(err).Msg("Error fetching communities for home page") 487 respondWithError(w, errIncompleteRequest) 488 return 489 } 490 491 pageParams.TotalRecords = totalRecords 492 493 response := shared.GetPaginatedResponseWithPayload(communities, pageParams) 494 respondWithJSON(w, http.StatusOK, response) 495 } 496 497 func (a *App) createCommunity(w http.ResponseWriter, r *http.Request) { 498 var err error 499 var c models.Community 500 var payload models.CreateCommunityRequestPayload 501 502 if err := validatePayload(r.Body, &payload); err != nil { 503 log.Error().Err(err).Msg("Error validating payload") 504 respondWithError(w, errIncompleteRequest) 505 return 506 } 507 508 //Validate Strategies & Proposal Thresholds 509 if payload.Strategies != nil { 510 err = validateContractThreshold(*payload.Strategies) 511 if err != nil { 512 log.Error().Err(err).Msg("Error validating contract threshold") 513 respondWithError(w, errIncompleteRequest) 514 return 515 } 516 } 517 if payload.Proposal_threshold != nil && payload.Only_authors_to_submit != nil { 518 err = validateProposalThreshold(*payload.Proposal_threshold, *payload.Only_authors_to_submit) 519 if err != nil { 520 log.Error().Err(err).Msg("Error validating proposal threshold") 521 respondWithError(w, errIncompleteRequest) 522 } 523 } 524 525 c, err = helpers.createCommunity(payload) 526 if err != nil { 527 log.Error().Err(err).Msg("Error creating community") 528 respondWithError(w, errIncompleteRequest) 529 return 530 } 531 532 respondWithJSON(w, http.StatusCreated, c) 533 } 534 535 func (a *App) updateCommunity(w http.ResponseWriter, r *http.Request) { 536 vars := mux.Vars(r) 537 id, err := strconv.Atoi(vars["id"]) 538 if err != nil { 539 log.Error().Err(err).Msg("Invalid Community ID") 540 respondWithError(w, errIncompleteRequest) 541 return 542 } 543 var payload models.UpdateCommunityRequestPayload 544 545 if err := validatePayload(r.Body, &payload); err != nil { 546 log.Error().Err(err).Msg("Error validating payload") 547 respondWithError(w, errIncompleteRequest) 548 return 549 } 550 551 //Validate Contract Thresholds 552 if payload.Strategies != nil { 553 err = validateContractThreshold(*payload.Strategies) 554 if err != nil { 555 log.Error().Err(err).Msg("Error validating contract threshold") 556 respondWithError(w, errIncompleteRequest) 557 return 558 } 559 } 560 561 if payload.Proposal_threshold != nil && payload.Only_authors_to_submit != nil { 562 err = validateProposalThreshold(*payload.Proposal_threshold, *payload.Only_authors_to_submit) 563 if err != nil { 564 log.Error().Err(err).Msg("Error validating proposal threshold") 565 respondWithError(w, errIncompleteRequest) 566 } 567 } 568 569 c, err := helpers.updateCommunity(id, payload) 570 if err != nil { 571 log.Error().Err(err).Msg("Error updating community") 572 respondWithError(w, errIncompleteRequest) 573 return 574 } 575 576 respondWithJSON(w, http.StatusOK, c) 577 } 578 579 func validateConractThreshold(s []models.Strategy) error { 580 for _, s := range s { 581 if s.Threshold != nil { 582 if *s.Threshold < 1 { 583 return errors.New("Contract Threshold Cannot Be < 1.") 584 } 585 } 586 } 587 return nil 588 } 589 590 // Voting Strategies 591 func (a *App) getVotingStrategies(w http.ResponseWriter, r *http.Request) { 592 vs, err := models.GetVotingStrategies(a.DB) 593 594 // Add custom scripts for the custom-script strategy 595 for _, strategy := range vs { 596 if strategy.Key == "custom-script" { 597 strategy.Scripts = customScripts 598 } 599 } 600 601 if err != nil { 602 log.Error().Err(err).Msg("Error fetching voting strategies") 603 respondWithError(w, errIncompleteRequest) 604 return 605 } 606 607 respondWithJSON(w, http.StatusOK, vs) 608 } 609 610 func (a *App) getCommunityCategories(w http.ResponseWriter, r *http.Request) { 611 vs, err := models.GetCommunityTypes(a.DB) 612 if err != nil { 613 log.Error().Err(err).Msg("Error fetching community categories") 614 respondWithError(w, errIncompleteRequest) 615 return 616 } 617 618 respondWithJSON(w, http.StatusOK, vs) 619 } 620 621 func (a *App) getActiveStrategiesForCommunity(w http.ResponseWriter, r *http.Request) { 622 vars := mux.Vars(r) 623 communityId, err := strconv.Atoi(vars["communityId"]) 624 625 if err != nil { 626 log.Error().Err(err).Msg("Invalid Community ID") 627 respondWithError(w, errIncompleteRequest) 628 return 629 } 630 631 strategies, err := models.GetActiveStrategiesForCommunity(a.DB, communityId) 632 if err != nil { 633 log.Error().Err(err).Msg("Error fetching active strategies for community") 634 respondWithError(w, errIncompleteRequest) 635 return 636 } 637 638 respondWithJSON(w, http.StatusOK, strategies) 639 } 640 641 //////////// 642 // Lists // 643 /////////// 644 645 func (a *App) getListsForCommunity(w http.ResponseWriter, r *http.Request) { 646 vars := mux.Vars(r) 647 communityId, err := strconv.Atoi(vars["communityId"]) 648 if err != nil { 649 log.Error().Err(err).Msg("Invalid Community ID") 650 respondWithError(w, errIncompleteRequest) 651 return 652 } 653 654 lists, err := models.GetListsForCommunity(a.DB, communityId) 655 if err != nil { 656 log.Error().Err(err).Msg("Error getting lists for community") 657 respondWithError(w, errIncompleteRequest) 658 return 659 } 660 661 respondWithJSON(w, http.StatusOK, lists) 662 } 663 664 func (a *App) getList(w http.ResponseWriter, r *http.Request) { 665 vars := mux.Vars(r) 666 id, err := strconv.Atoi(vars["id"]) 667 668 if err != nil { 669 log.Error().Err(err).Msg("Invalid List ID") 670 respondWithError(w, errIncompleteRequest) 671 return 672 } 673 list := models.List{ID: id} 674 675 if err = list.GetListById(a.DB); err != nil { 676 log.Error().Err(err).Msg("Error getting list") 677 respondWithError(w, errIncompleteRequest) 678 return 679 } 680 681 respondWithJSON(w, http.StatusOK, list) 682 } 683 684 func (a *App) createListForCommunity(w http.ResponseWriter, r *http.Request) { 685 vars := mux.Vars(r) 686 communityId, err := strconv.Atoi(vars["communityId"]) 687 if err != nil { 688 log.Error().Err(err).Msg("Invalid Community ID") 689 respondWithError(w, errIncompleteRequest) 690 return 691 } 692 693 payload := models.ListPayload{} 694 payload.Community_id = communityId 695 696 if err := validatePayload(r.Body, &payload); err != nil { 697 log.Error().Err(err).Msg("Error validating payload") 698 respondWithError(w, errIncompleteRequest) 699 return 700 } 701 702 l, httpStatus, err := helpers.createListForCommunity(payload) 703 if err != nil { 704 log.Error().Err(err).Msg("Error creating list for community") 705 errIncompleteRequest.StatusCode = httpStatus 706 respondWithError(w, errIncompleteRequest) 707 return 708 } 709 710 respondWithJSON(w, http.StatusCreated, l) 711 } 712 713 func (a *App) addAddressesToList(w http.ResponseWriter, r *http.Request) { 714 vars := mux.Vars(r) 715 id, err := strconv.Atoi(vars["id"]) 716 if err != nil { 717 log.Error().Err(err).Msg("Invalid List ID") 718 respondWithError(w, errIncompleteRequest) 719 return 720 } 721 722 payload := models.ListUpdatePayload{} 723 if err := validatePayload(r.Body, &payload); err != nil { 724 log.Error().Err(err).Msg("Error validating payload") 725 respondWithError(w, errIncompleteRequest) 726 return 727 } 728 729 httpStatus, err := helpers.updateAddressesInList(id, payload, "add") 730 if err != nil { 731 log.Error().Err(err).Msg("Error adding addresses to list") 732 errIncompleteRequest.StatusCode = httpStatus 733 respondWithError(w, errCreateCommunity) 734 return 735 } 736 737 respondWithJSON(w, http.StatusCreated, "OK") 738 } 739 740 func (a *App) removeAddressesFromList(w http.ResponseWriter, r *http.Request) { 741 vars := mux.Vars(r) 742 id, err := strconv.Atoi(vars["id"]) 743 if err != nil { 744 log.Error().Err(err).Msg("Invalid List ID") 745 respondWithError(w, errIncompleteRequest) 746 return 747 } 748 749 payload := models.ListUpdatePayload{} 750 if err := validatePayload(r.Body, &payload); err != nil { 751 log.Error().Err(err).Msg("Error validating payload") 752 respondWithError(w, errIncompleteRequest) 753 return 754 } 755 756 httpStatus, err := helpers.updateAddressesInList(id, payload, "remove") 757 if err != nil { 758 log.Error().Err(err).Msg("Error removing addresses from list") 759 errIncompleteRequest.StatusCode = httpStatus 760 respondWithError(w, errIncompleteRequest) 761 return 762 } 763 764 respondWithJSON(w, http.StatusOK, "OK") 765 } 766 767 ////////////// 768 // Accounts // 769 ////////////// 770 771 func (a *App) getAccountAtBlockHeight(w http.ResponseWriter, r *http.Request) { 772 vars := mux.Vars(r) 773 addr := vars["addr"] 774 var blockHeight uint64 775 blockHeight, err := strconv.ParseUint(vars["blockHeight"], 10, 64) 776 if err != nil { 777 log.Error().Err(err).Msg("Error parsing blockHeight param.") 778 respondWithError(w, errFetchingBalance) 779 return 780 } 781 782 flowToken := "FlowToken" 783 784 b := shared.FTBalanceResponse{} 785 acc, err := a.FlowAdapter.GetAccountAtBlockHeight(addr, blockHeight) 786 if err != nil { 787 log.Error().Err(err).Msgf("Error getting account %s at blockheight %d.", addr, blockHeight) 788 } 789 790 //TODO: @bluesign add locked tokens 791 b.Balance = acc.Balance 792 b.Addr = addr 793 b.BlockHeight = blockHeight 794 b.FungibleTokenID = flowToken 795 796 respondWithJSON(w, http.StatusOK, b) 797 } 798 799 func (a *App) getAdminList(w http.ResponseWriter, r *http.Request) { 800 respondWithJSON(w, http.StatusOK, a.AdminAllowlist.Addresses) 801 } 802 803 func (a *App) getCommunityBlocklist(w http.ResponseWriter, r *http.Request) { 804 respondWithJSON(w, http.StatusOK, a.CommunityBlocklist.Addresses) 805 } 806 807 /////////// 808 // Users // 809 /////////// 810 811 func (a *App) createCommunityUser(w http.ResponseWriter, r *http.Request) { 812 vars := mux.Vars(r) 813 communityId, err := strconv.Atoi(vars["communityId"]) 814 if err != nil { 815 log.Error().Err(err).Msg("Invalid Community ID") 816 respondWithError(w, errIncompleteRequest) 817 return 818 } 819 820 payload := models.CommunityUserPayload{} 821 payload.Community_id = communityId 822 823 if err := validatePayload(r.Body, &payload); err != nil { 824 log.Error().Err(err).Msg("Error validating payload") 825 respondWithError(w, errIncompleteRequest) 826 return 827 } 828 829 httpStatus, err := helpers.createCommunityUser(payload) 830 if err != nil { 831 log.Error().Err(err).Msg("Error creating community user") 832 errCreateCommunity.StatusCode = httpStatus 833 respondWithError(w, errCreateCommunity) 834 return 835 } 836 837 respondWithJSON(w, http.StatusCreated, "OK") 838 } 839 840 func (a *App) getCommunityUsers(w http.ResponseWriter, r *http.Request) { 841 vars := mux.Vars(r) 842 communityId, err := strconv.Atoi(vars["communityId"]) 843 844 if err != nil { 845 log.Error().Err(err).Msg("Invalid Community ID") 846 respondWithError(w, errIncompleteRequest) 847 return 848 } 849 850 pageParams := getPageParams(*r, 100) 851 852 users, totalRecords, err := models.GetUsersForCommunity(a.DB, communityId, pageParams) 853 if err != nil { 854 log.Error().Err(err).Msg("Error getting community users") 855 respondWithError(w, errIncompleteRequest) 856 return 857 } 858 859 pageParams.TotalRecords = totalRecords 860 861 response := shared.GetPaginatedResponseWithPayload(users, pageParams) 862 respondWithJSON(w, http.StatusOK, response) 863 864 } 865 866 func (a *App) getCommunityUsersByType(w http.ResponseWriter, r *http.Request) { 867 vars := mux.Vars(r) 868 communityId, err := strconv.Atoi(vars["communityId"]) 869 870 if err != nil { 871 log.Error().Err(err).Msg("Invalid Community ID") 872 respondWithError(w, errIncompleteRequest) 873 return 874 } 875 876 userType := vars["userType"] 877 if !models.EnsureValidRole(userType) { 878 log.Error().Err(err).Msg("Invalid User Type") 879 respondWithError(w, errIncompleteRequest) 880 return 881 } 882 883 pageParams := getPageParams(*r, 100) 884 users, totalRecords, err := models.GetUsersForCommunityByType( 885 a.DB, 886 communityId, 887 userType, 888 pageParams, 889 ) 890 if err != nil { 891 log.Error().Err(err).Msg("Error getting community users") 892 respondWithError(w, errIncompleteRequest) 893 return 894 } 895 pageParams.TotalRecords = totalRecords 896 897 response := shared.GetPaginatedResponseWithPayload(users, pageParams) 898 respondWithJSON(w, http.StatusOK, response) 899 } 900 901 func (a *App) getCommunityLeaderboard(w http.ResponseWriter, r *http.Request) { 902 vars := mux.Vars(r) 903 communityId, err := strconv.Atoi(vars["communityId"]) 904 905 if err != nil { 906 log.Error().Err(err).Msg("Invalid Community ID") 907 respondWithError(w, errIncompleteRequest) 908 return 909 } 910 911 addr := r.FormValue("addr") 912 pageParams := getPageParams(*r, 100) 913 914 leaderboard, totalRecords, err := models.GetCommunityLeaderboard(a.DB, communityId, addr, pageParams) 915 if err != nil { 916 log.Error().Err(err).Msg("Error getting community leaderboard") 917 respondWithError(w, errIncompleteRequest) 918 return 919 } 920 pageParams.TotalRecords = totalRecords 921 922 response := shared.GetPaginatedResponseWithPayload(leaderboard.Users, pageParams) 923 response.Data = leaderboard 924 925 respondWithJSON(w, http.StatusOK, response) 926 } 927 928 func (a *App) getUserCommunities(w http.ResponseWriter, r *http.Request) { 929 vars := mux.Vars(r) 930 addr := vars["addr"] 931 932 pageParams := getPageParams(*r, 100) 933 934 communities, totalRecords, err := models.GetCommunitiesForUser(a.DB, addr, pageParams) 935 if err != nil { 936 log.Error().Err(err).Msg("Error getting user communities") 937 respondWithError(w, errIncompleteRequest) 938 return 939 } 940 941 pageParams.TotalRecords = totalRecords 942 response := shared.GetPaginatedResponseWithPayload(communities, pageParams) 943 944 respondWithJSON(w, http.StatusOK, response) 945 946 } 947 948 func (a *App) removeUserRole(w http.ResponseWriter, r *http.Request) { 949 vars := mux.Vars(r) 950 addr := vars["addr"] 951 userType := vars["userType"] 952 communityId, err := strconv.Atoi(vars["communityId"]) 953 954 if err != nil { 955 log.Error().Err(err).Msg("Invalid Community ID") 956 respondWithError(w, errIncompleteRequest) 957 return 958 } 959 960 payload := models.CommunityUserPayload{} 961 payload.Community_id = communityId 962 payload.Addr = addr 963 payload.User_type = userType 964 965 if err := validatePayload(r.Body, &payload); err != nil { 966 log.Error().Err(err).Msg("Error validating payload") 967 respondWithError(w, errIncompleteRequest) 968 return 969 } 970 971 _, err = helpers.removeUserRole(payload) 972 if err != nil { 973 log.Error().Err(err).Msg("Error removing user role") 974 respondWithError(w, errIncompleteRequest) 975 return 976 } 977 978 respondWithJSON(w, http.StatusOK, "OK") 979 } 980 981 ///////////// 982 // HELPERS // 983 ///////////// 984 985 func respondWithError(w http.ResponseWriter, err errorResponse) { 986 respondWithJSON(w, err.StatusCode, map[string]string{ 987 "statusCode": strconv.Itoa(err.StatusCode), 988 "errorCode": err.ErrorCode, 989 "message": err.Message, 990 "details": err.Details, 991 }) 992 } 993 994 func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { 995 response, _ := json.Marshal(payload) 996 w.Header().Set("Content-Type", "application/json") 997 w.WriteHeader(code) 998 w.Write(response) 999 } 1000 1001 func validatePayload(body io.ReadCloser, data interface{}) error { 1002 decoder := json.NewDecoder(body) 1003 if err := decoder.Decode(&data); err != nil { 1004 errMsg := "Invalid request payload." 1005 log.Error().Err(err).Msg(errMsg) 1006 return errors.New(errMsg) 1007 } 1008 1009 defer body.Close() 1010 1011 return nil 1012 } 1013 1014 func getPageParams(r http.Request, defaultCount int) shared.PageParams { 1015 s, _ := strconv.Atoi(r.FormValue("start")) 1016 c, _ := strconv.Atoi(r.FormValue("count")) 1017 o := r.FormValue("order") 1018 1019 if o == "" { 1020 o = "desc" 1021 } 1022 1023 if c > defaultCount || c < 1 { 1024 c = defaultCount 1025 } 1026 if s < 0 { 1027 s = 0 1028 } 1029 1030 return shared.PageParams{ 1031 Start: s, 1032 Count: c, 1033 Order: o, 1034 } 1035 }