diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml
new file mode 100644
index 0000000..074f19e
--- /dev/null
+++ b/.github/workflows/deploy-prod.yml
@@ -0,0 +1,76 @@
+name: deploy
+permissions:
+ id-token: write
+ contents: read
+on:
+ push:
+ tags:
+ - "*"
+jobs:
+ deploy-prod:
+ environment:
+ name: production
+ url: https://dashboard-api.string-api.xyz
+ if: startsWith(github.ref, 'refs/tags/')
+ name: deploy to ECS - Production
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: setup go
+ uses: actions/setup-go@v3
+ with:
+ go-version-file: go.mod
+ cache: true
+ cache-dependency-path: go.sum
+ - name: install deps
+ run: |
+ go mod download
+
+ - name: configure aws credentials
+ uses: aws-actions/configure-aws-credentials@v1.7.0
+ with:
+ aws-region: us-west-2
+ role-to-assume: ${{ secrets.PROD_ASSUME_ROLE }}
+
+ - name: login to Amazon ECR
+ id: login-ecr
+ uses: aws-actions/amazon-ecr-login@v1
+
+ - name: Extract tag
+ id: extract_tag
+ run: |
+ echo ::set-output name=tag::${GITHUB_REF#refs/tags/}
+
+ - name: build,tag and push to Amazon ECR
+ id: image-builder
+ env:
+ ECR_REPO: ${{ secrets.PROD_AWS_ACCT }}.dkr.ecr.us-west-2.amazonaws.com
+ SERVICE: dashboard-api
+ IMAGE_TAG: ${{ steps.extract_tag.tag }}
+ run: |
+ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./cmd/app/main ./cmd/app/main.go
+ docker build --platform linux/amd64 -t $ECR_REPO/$SERVICE:$IMAGE_TAG ./cmd/app/
+ docker push $ECR_REPO/$SERVICE:$IMAGE_TAG
+ echo "image=$ECR_REPO/$SERVICE:$IMAGE_TAG" >> $GITHUB_OUTPUT
+
+ - name: get latest task definition
+ run: |
+ aws ecs describe-task-definition --task-definition dashboard-api --query taskDefinition > task-definition.json
+
+ - name: update task definition
+ id: task
+ uses: aws-actions/amazon-ecs-render-task-definition@v1
+ with:
+ task-definition: task-definition.json
+ container-name: dashboard-api
+ image: ${{ steps.image-builder.outputs.image }}
+
+ - name: deploy
+ uses: aws-actions/amazon-ecs-deploy-task-definition@v1
+ with:
+ task-definition: ${{ steps.task.outputs.task-definition }}
+ cluster: core
+ service: dashboard-api
+ wait-for-service-stability: true
diff --git a/api/handler/apikey.go b/api/handler/apikey.go
index 8ddf25d..3313447 100644
--- a/api/handler/apikey.go
+++ b/api/handler/apikey.go
@@ -8,9 +8,9 @@ import (
"github.com/String-xyz/dashboard-api/pkg/model"
"github.com/String-xyz/dashboard-api/pkg/service"
"github.com/String-xyz/go-lib/v2/common"
- httperror "github.com/String-xyz/go-lib/v2/httperror"
+ "github.com/String-xyz/go-lib/v2/httperror"
serror "github.com/String-xyz/go-lib/v2/stringerror"
- validator "github.com/String-xyz/go-lib/v2/validator"
+ "github.com/String-xyz/go-lib/v2/validator"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
)
diff --git a/api/handler/common.go b/api/handler/common.go
index 8ab56d5..521dd83 100644
--- a/api/handler/common.go
+++ b/api/handler/common.go
@@ -9,7 +9,7 @@ import (
"github.com/String-xyz/dashboard-api/config"
"github.com/String-xyz/dashboard-api/pkg/service"
"github.com/String-xyz/go-lib/v2/common"
- httperror "github.com/String-xyz/go-lib/v2/httperror"
+ "github.com/String-xyz/go-lib/v2/httperror"
serror "github.com/String-xyz/go-lib/v2/stringerror"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/sha3"
diff --git a/api/handler/contract.go b/api/handler/contract.go
index a058fd7..78d5586 100644
--- a/api/handler/contract.go
+++ b/api/handler/contract.go
@@ -2,16 +2,18 @@ package handler
import (
"net/http"
+ "strconv"
"github.com/String-xyz/go-lib/v2/common"
- httperror "github.com/String-xyz/go-lib/v2/httperror"
+ "github.com/String-xyz/go-lib/v2/httperror"
serror "github.com/String-xyz/go-lib/v2/stringerror"
- validator "github.com/String-xyz/go-lib/v2/validator"
+ "github.com/String-xyz/go-lib/v2/validator"
"github.com/pkg/errors"
+ "github.com/labstack/echo/v4"
+
"github.com/String-xyz/dashboard-api/pkg/model"
"github.com/String-xyz/dashboard-api/pkg/service"
- "github.com/labstack/echo/v4"
)
type Contract interface {
@@ -57,11 +59,17 @@ func (a contract) Create(c echo.Context) error {
}
body := model.RequestContractCreate{}
+
if err := c.Bind(&body); err != nil {
common.LogStringError(c, err, "contract: create bind")
return httperror.BadRequest400(c)
}
+ if err := c.Validate(body); err != nil {
+ common.LogStringError(c, err, "contract: create validate")
+ return httperror.InvalidPayload400(c, err)
+ }
+
SanitizeChecksums(&body.Address)
m, err := a.service.Create(c.Request().Context(), body, callerId, organizationId)
@@ -94,7 +102,28 @@ func (a contract) GetAll(c echo.Context) error {
platformId := c.QueryParam("platformId")
- m, err := a.service.GetAll(c.Request().Context(), platformId, organizationId)
+ var err error
+ var limit int
+ var offset int
+ limitStr := c.QueryParam("limit")
+ offsetStr := c.QueryParam("offset")
+
+ if limitStr != "" {
+ limit, err = strconv.Atoi(limitStr)
+ if err != nil {
+ common.LogStringError(c, err, "contract get all: invalid limit")
+ return httperror.BadRequest400(c, "invalid limit")
+ }
+ }
+ if offsetStr != "" {
+ offset, err = strconv.Atoi(offsetStr)
+ if err != nil {
+ common.LogStringError(c, err, "contract get all: invalid offset")
+ return httperror.BadRequest400(c, "invalid offset")
+ }
+ }
+
+ m, err := a.service.GetAll(c.Request().Context(), platformId, organizationId, limit, offset)
if err != nil && errors.Cause(err) != serror.NOT_FOUND {
return DefaultErrorHandler(c, err, "contract: get all")
}
@@ -227,11 +256,17 @@ func (a contract) Update(c echo.Context) error {
}
body := model.RequestContractUpdate{}
- err := c.Bind(&body)
- if err != nil {
+
+ if err := c.Bind(&body); err != nil {
common.LogStringError(c, err, "contract: update bind")
return httperror.BadRequest400(c)
}
+
+ if err := c.Validate(body); err != nil {
+ common.LogStringError(c, err, "contract: update validate")
+ return httperror.InvalidPayload400(c, err)
+ }
+
if body.Address != nil {
SanitizeChecksums(body.Address)
}
diff --git a/api/handler/invite.go b/api/handler/invite.go
index 80c0316..3bee89a 100644
--- a/api/handler/invite.go
+++ b/api/handler/invite.go
@@ -2,15 +2,16 @@ package handler
import (
"net/http"
+ "strconv"
"strings"
"github.com/String-xyz/go-lib/v2/common"
"github.com/String-xyz/dashboard-api/pkg/model"
"github.com/String-xyz/dashboard-api/pkg/service"
- httperror "github.com/String-xyz/go-lib/v2/httperror"
+ "github.com/String-xyz/go-lib/v2/httperror"
serror "github.com/String-xyz/go-lib/v2/stringerror"
- validator "github.com/String-xyz/go-lib/v2/validator"
+ "github.com/String-xyz/go-lib/v2/validator"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
)
@@ -55,8 +56,8 @@ func (i invite) Send(c echo.Context) error {
}
body := model.RequestInviteSend{}
- err := c.Bind(&body)
- if err != nil {
+
+ if err := c.Bind(&body); err != nil {
common.LogStringError(c, err, "invite: send bind")
return httperror.BadRequest400(c, "invalid payload")
}
@@ -90,8 +91,8 @@ func (i invite) Accept(c echo.Context) error {
id := c.Param("id")
body := model.RequestInviteAcceptance{}
- err := c.Bind(&body)
- if err != nil {
+
+ if err := c.Bind(&body); err != nil {
common.LogStringError(c, err, "invite: accept bind")
return httperror.BadRequest400(c)
}
@@ -143,8 +144,29 @@ func (i invite) List(c echo.Context) error {
return httperror.Internal500(c, "missing or invalid organizationId")
}
+ var err error
+ var limit int
+ var offset int
+ limitStr := c.QueryParam("limit")
+ offsetStr := c.QueryParam("offset")
+
+ if limitStr != "" {
+ limit, err = strconv.Atoi(limitStr)
+ if err != nil {
+ common.LogStringError(c, err, "invite get all: invalid limit")
+ return httperror.BadRequest400(c, "invalid limit")
+ }
+ }
+ if offsetStr != "" {
+ offset, err = strconv.Atoi(offsetStr)
+ if err != nil {
+ common.LogStringError(c, err, "invite get all: invalid offset")
+ return httperror.BadRequest400(c, "invalid offset")
+ }
+ }
+
status := c.QueryParam("status")
- m, err := i.service.List(c.Request().Context(), status, organizationId)
+ m, err := i.service.List(c.Request().Context(), status, organizationId, limit, offset)
if err != nil && errors.Cause(err) != serror.NOT_FOUND {
return DefaultErrorHandler(c, err, "invite: list")
}
@@ -166,12 +188,17 @@ func (i invite) Resend(c echo.Context) error {
return httperror.Internal500(c, "missing or invalid callerId")
}
+ organizationId, ok := c.Get("organizationId").(string)
+ if !ok {
+ return httperror.Internal500(c, "missing or invalid organizationId")
+ }
+
id := c.Param("id")
if !validator.IsUUID(id) {
return httperror.BadRequest400(c, "invalid id")
}
- m, err := i.service.Resend(c.Request().Context(), id, callerId)
+ m, err := i.service.Resend(c.Request().Context(), id, callerId, organizationId)
if err != nil {
common.LogStringError(c, err, "invite: resend")
@@ -209,8 +236,8 @@ func (i invite) Update(c echo.Context) error {
}
body := model.RequestInviteUpdate{}
- err := c.Bind(&body)
- if err != nil {
+
+ if err := c.Bind(&body); err != nil {
common.LogStringError(c, err, "invite: update bind")
return httperror.BadRequest400(c)
}
diff --git a/api/handler/login.go b/api/handler/login.go
index 815b6dc..298e31e 100644
--- a/api/handler/login.go
+++ b/api/handler/login.go
@@ -7,7 +7,7 @@ import (
"github.com/String-xyz/dashboard-api/pkg/model"
"github.com/String-xyz/dashboard-api/pkg/service"
"github.com/String-xyz/go-lib/v2/common"
- httperror "github.com/String-xyz/go-lib/v2/httperror"
+ "github.com/String-xyz/go-lib/v2/httperror"
serror "github.com/String-xyz/go-lib/v2/stringerror"
"github.com/labstack/echo/v4"
)
diff --git a/api/handler/member.go b/api/handler/member.go
index 7b753cb..afdeeaf 100644
--- a/api/handler/member.go
+++ b/api/handler/member.go
@@ -2,12 +2,13 @@ package handler
import (
"net/http"
+ "strconv"
"strings"
"github.com/String-xyz/go-lib/v2/common"
- httperror "github.com/String-xyz/go-lib/v2/httperror"
+ "github.com/String-xyz/go-lib/v2/httperror"
serror "github.com/String-xyz/go-lib/v2/stringerror"
- validator "github.com/String-xyz/go-lib/v2/validator"
+ "github.com/String-xyz/go-lib/v2/validator"
"github.com/pkg/errors"
"github.com/String-xyz/dashboard-api/pkg/model"
@@ -47,7 +48,28 @@ func (a member) GetAll(c echo.Context) error {
return httperror.Internal500(c, "missing or invalid organizationId")
}
- m, err := a.service.GetAll(c.Request().Context(), organizationId)
+ var err error
+ var limit int
+ var offset int
+ limitStr := c.QueryParam("limit")
+ offsetStr := c.QueryParam("offset")
+
+ if limitStr != "" {
+ limit, err = strconv.Atoi(limitStr)
+ if err != nil {
+ common.LogStringError(c, err, "member get all: invalid limit")
+ return httperror.BadRequest400(c, "invalid limit")
+ }
+ }
+ if offsetStr != "" {
+ offset, err = strconv.Atoi(offsetStr)
+ if err != nil {
+ common.LogStringError(c, err, "member get all: invalid offset")
+ return httperror.BadRequest400(c, "invalid offset")
+ }
+ }
+
+ m, err := a.service.GetAll(c.Request().Context(), organizationId, limit, offset)
if err != nil && errors.Cause(err) != serror.NOT_FOUND {
common.LogStringError(c, err, "member: get all")
return httperror.Internal500(c)
diff --git a/api/handler/network.go b/api/handler/network.go
index 18a13fd..2b987e5 100644
--- a/api/handler/network.go
+++ b/api/handler/network.go
@@ -2,9 +2,11 @@ package handler
import (
"net/http"
+ "strconv"
"github.com/String-xyz/dashboard-api/pkg/service"
"github.com/String-xyz/go-lib/v2/common"
+ "github.com/String-xyz/go-lib/v2/httperror"
"github.com/labstack/echo/v4"
)
@@ -30,7 +32,28 @@ func NewNetwork(service service.Network) Network {
// @Failure 500 {object} error
// @Router /networks [get]
func (a network) GetAll(c echo.Context) error {
- m, err := a.service.GetAll(c.Request().Context())
+ var err error
+ var limit int
+ var offset int
+ limitStr := c.QueryParam("limit")
+ offsetStr := c.QueryParam("offset")
+
+ if limitStr != "" {
+ limit, err = strconv.Atoi(limitStr)
+ if err != nil {
+ common.LogStringError(c, err, "network get all: invalid limit")
+ return httperror.BadRequest400(c, "invalid limit")
+ }
+ }
+ if offsetStr != "" {
+ offset, err = strconv.Atoi(offsetStr)
+ if err != nil {
+ common.LogStringError(c, err, "network get all: invalid offset")
+ return httperror.BadRequest400(c, "invalid offset")
+ }
+ }
+
+ m, err := a.service.GetAll(c.Request().Context(), limit, offset)
if err != nil {
common.LogStringError(c, err, "network: get all")
return DefaultErrorHandler(c, err, "network: get all")
diff --git a/api/handler/organization.go b/api/handler/organization.go
index 365a74c..97ac540 100644
--- a/api/handler/organization.go
+++ b/api/handler/organization.go
@@ -7,7 +7,7 @@ import (
"github.com/String-xyz/dashboard-api/pkg/model"
"github.com/String-xyz/dashboard-api/pkg/service"
"github.com/String-xyz/go-lib/v2/common"
- httperror "github.com/String-xyz/go-lib/v2/httperror"
+ "github.com/String-xyz/go-lib/v2/httperror"
serror "github.com/String-xyz/go-lib/v2/stringerror"
"github.com/labstack/echo/v4"
)
diff --git a/api/handler/platform.go b/api/handler/platform.go
index c4dded3..1c26d2b 100644
--- a/api/handler/platform.go
+++ b/api/handler/platform.go
@@ -2,13 +2,14 @@ package handler
import (
"net/http"
+ "strconv"
"github.com/String-xyz/dashboard-api/pkg/model"
"github.com/String-xyz/dashboard-api/pkg/service"
"github.com/String-xyz/go-lib/v2/common"
- httperror "github.com/String-xyz/go-lib/v2/httperror"
+ "github.com/String-xyz/go-lib/v2/httperror"
serror "github.com/String-xyz/go-lib/v2/stringerror"
- validator "github.com/String-xyz/go-lib/v2/validator"
+ "github.com/String-xyz/go-lib/v2/validator"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
)
@@ -111,10 +112,30 @@ func (p platform) GetAll(c echo.Context) error {
if !ok {
return httperror.Internal500(c, "missing or invalid organizationId")
}
+ var err error
+ var limit int
+ var offset int
+ limitStr := c.QueryParam("limit")
+ offsetStr := c.QueryParam("offset")
+
+ if limitStr != "" {
+ limit, err = strconv.Atoi(limitStr)
+ if err != nil {
+ common.LogStringError(c, err, "platform get all: invalid limit")
+ return httperror.BadRequest400(c, "invalid limit")
+ }
+ }
+ if offsetStr != "" {
+ offset, err = strconv.Atoi(offsetStr)
+ if err != nil {
+ common.LogStringError(c, err, "platform get all: invalid offset")
+ return httperror.BadRequest400(c, "invalid offset")
+ }
+ }
- m, err := p.service.GetAll(c.Request().Context(), callerId, organizationId)
+ m, err := p.service.GetAll(c.Request().Context(), callerId, organizationId, limit, offset)
if err != nil && errors.Cause(err) != serror.NOT_FOUND {
- return DefaultErrorHandler(c, err, "apikey: get all")
+ return DefaultErrorHandler(c, err, "platform: get all")
}
return c.JSON(http.StatusOK, m)
diff --git a/api/middleware/middleware.go b/api/middleware/middleware.go
index 2668f6e..a5c5b91 100644
--- a/api/middleware/middleware.go
+++ b/api/middleware/middleware.go
@@ -3,12 +3,12 @@ package middleware
import (
"strings"
- validator "github.com/String-xyz/go-lib/v2/validator"
+ "github.com/String-xyz/go-lib/v2/validator"
"github.com/String-xyz/dashboard-api/config"
"github.com/String-xyz/dashboard-api/pkg/service"
libcommon "github.com/String-xyz/go-lib/v2/common"
- httperror "github.com/String-xyz/go-lib/v2/httperror"
+ "github.com/String-xyz/go-lib/v2/httperror"
"github.com/golang-jwt/jwt"
"github.com/labstack/echo/v4"
echoMiddleware "github.com/labstack/echo/v4/middleware"
diff --git a/go.mod b/go.mod
index 368e218..ef884e1 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/String-xyz/dashboard-api
go 1.19
require (
- github.com/String-xyz/go-lib/v2 v2.0.3
+ github.com/String-xyz/go-lib/v2 v2.1.1
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/uuid v1.3.0
github.com/jmoiron/sqlx v1.3.5
diff --git a/go.sum b/go.sum
index f9d81b9..31d001a 100644
--- a/go.sum
+++ b/go.sum
@@ -19,8 +19,8 @@ github.com/DataDog/sketches-go v1.2.1/go.mod h1:1xYmPLY1So10AwxV6MJV0J53XVH+WL9A
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY=
github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
-github.com/String-xyz/go-lib/v2 v2.0.3 h1:Ls+kgTuZfhjv3J5zDK61ZZ/nFyip3+0AnIRk+Ciq1Ps=
-github.com/String-xyz/go-lib/v2 v2.0.3/go.mod h1:ifMnKUbxfLatYJjFMcrhgjdMicJNAlCIa1vNOoqc24w=
+github.com/String-xyz/go-lib/v2 v2.1.1 h1:fHVsH5konF17O8c/hX71vE1LxlMjDZ3dTLUYuDilQNs=
+github.com/String-xyz/go-lib/v2 v2.1.1/go.mod h1:ifMnKUbxfLatYJjFMcrhgjdMicJNAlCIa1vNOoqc24w=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
diff --git a/infra/sandbox/variables.tf b/infra/sandbox/variables.tf
index 4d7fb4d..1b24041 100644
--- a/infra/sandbox/variables.tf
+++ b/infra/sandbox/variables.tf
@@ -16,7 +16,7 @@ locals {
variable "versioning" {
type = string
- default = "v1.1.0"
+ default = "v1.1.3"
}
locals {
diff --git a/pkg/internal/emailer/emailer.go b/pkg/internal/emailer/emailer.go
index 4c0e331..b808b4d 100644
--- a/pkg/internal/emailer/emailer.go
+++ b/pkg/internal/emailer/emailer.go
@@ -14,7 +14,7 @@ import (
)
type Emailer interface {
- SendInviteEmail(ctx context.Context, email string, token string, inviteId string, userName string) error
+ SendInviteEmail(ctx context.Context, email string, token string, inviteId string, userName string, orgName string) error
SendPasswordResetEmail(ctx context.Context, email string, token string, userName string) error
}
@@ -46,12 +46,12 @@ func (e emailer) SendPasswordResetEmail(ctx context.Context, email string, token
}
from := mail.NewEmail("String API", config.Var.AUTH_EMAIL_ADDRESS)
- subject := "String API Password Reset"
+ subject := "String Dashboard Password Reset Request"
to := mail.NewEmail(userName, email)
return sendEmail(ctx, from, subject, to, "", buf.String())
}
-func (e emailer) SendInviteEmail(ctx context.Context, email string, token string, inviteId string, userName string) error {
+func (e emailer) SendInviteEmail(ctx context.Context, email string, token string, inviteId string, userName string, orgName string) error {
link := config.Var.BASE_DASHBOARD_URL + "/invite/" + inviteId + "?token=" + token
tmpl, err := template.ParseFS(templatesFS, "templates/invite.tpl")
@@ -63,13 +63,14 @@ func (e emailer) SendInviteEmail(ctx context.Context, email string, token string
err = tmpl.ExecuteTemplate(&buf, "invite.tpl", map[string]interface{}{
"link": link,
"userName": userName,
+ "orgName": orgName,
})
if err != nil {
return err
}
from := mail.NewEmail("String API", config.Var.AUTH_EMAIL_ADDRESS)
- subject := "New String API User"
+ subject := "Invite to " + orgName + " on String Dashboard"
to := mail.NewEmail(userName, email)
return sendEmail(ctx, from, subject, to, "", buf.String())
}
diff --git a/pkg/internal/emailer/templates/invite.tpl b/pkg/internal/emailer/templates/invite.tpl
index 221f7a2..ca1773a 100644
--- a/pkg/internal/emailer/templates/invite.tpl
+++ b/pkg/internal/emailer/templates/invite.tpl
@@ -1,7 +1,61 @@
-
-
-
-You have been invited to use the String API
- Dear {{.userName}},
- Thank you for signing up to use the String API. Please click the link below to set your password and complete your registration process:
-Accept Invitation
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ You have been invited to {{.orgName}} on the String Dashboard
+
+
+
+
+
+ Dear {{.userName}},
+
+
+ Please click the button below to set your password and complete your registration process.
+
+
+
+
+
+ Accept Invitation
+
+
+
+
+ If you do not recognize this organization, please disregard this email.
+
+
+
+
+
+
+
+
+
+
+ Powered by
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pkg/internal/emailer/templates/password_reset.tpl b/pkg/internal/emailer/templates/password_reset.tpl
index 87d1d0d..924c963 100644
--- a/pkg/internal/emailer/templates/password_reset.tpl
+++ b/pkg/internal/emailer/templates/password_reset.tpl
@@ -1,7 +1,61 @@
-
-
-
-You have requested a password reset for the String API
- Dear {{.userName}},
- If you have forgotten your password, click the link below to reset it:
-Reset Password
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Password Reset Request
+
+
+
+
+
+ Dear {{.userName}},
+
+
+ Please click the button below to reset your password.
+
+
+
+
+
+ Reset Password
+
+
+
+
+ If you did not request this, please disregard this email.
+
+
+
+
+
+
+
+
+
+
+ Powered by
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pkg/model/entity.go b/pkg/model/entity.go
index 0f7f18d..8147a10 100644
--- a/pkg/model/entity.go
+++ b/pkg/model/entity.go
@@ -94,9 +94,10 @@ type Contract struct {
Name string `json:"name" db:"name"`
Address string `json:"address" db:"address"`
Functions pq.StringArray `json:"functions" db:"functions" swaggertype:"array,string"`
+ Type string `json:"type" db:"type"`
NetworkId string `json:"networkId" db:"network_id"`
- PlatformIds pq.StringArray `json:"platformIds" db:"platform_ids" swaggertype:"array,string"`
OrganizationId string `json:"organizationId" db:"organization_id"`
+ PlatformIds pq.StringArray `json:"platformIds" db:"platform_ids" swaggertype:"array,string"`
}
type NetworkFull struct {
diff --git a/pkg/model/request.go b/pkg/model/request.go
index a4fb366..9709179 100644
--- a/pkg/model/request.go
+++ b/pkg/model/request.go
@@ -3,14 +3,14 @@ package model
import "github.com/lib/pq"
type RequestOrganizationCreate struct {
- OrganizationName string `json:"organizationName" validate:"required"`
+ OrganizationName string `json:"organizationName" validate:"required,max=100"`
Email string `json:"email" validate:"required,email"`
- Name string `json:"name" validate:"required"`
+ Name string `json:"name" validate:"required,max=100"`
}
type RequestOrganizationUpdate struct {
- Name *string `json:"organizationName" db:"name"`
- Description *string `json:"description" db:"description"`
+ Name *string `json:"organizationName" db:"name" validate:"omitempty,max=100"`
+ Description *string `json:"description" db:"description" validate:"omitempty,max=144"`
}
type RequestPlatformCreate struct {
@@ -56,8 +56,8 @@ type RequestInviteAcceptance struct {
type RequestMemberUpdateSelf struct {
Name *string `json:"name" db:"name"`
- OldPassword *string `json:"oldPassword" validate:"required_with=NewPassword,min=8,max=100"`
- NewPassword *string `json:"newPassword" validate:"required_with=OldPassword,min=8,max=100"`
+ OldPassword *string `json:"oldPassword" validate:"required_with_all=OldPassword,NewPassword,min=8,max=100"`
+ NewPassword *string `json:"newPassword" validate:"required_with_all=NewPassword,OldPassword,min=8,max=100"`
}
type RequestMemberUpdateOther struct {
@@ -86,17 +86,20 @@ type RequestPasswordReset struct {
}
type RequestContractCreate struct {
- Name string `json:"name" db:"name"`
- Address string `json:"address" db:"address"`
- Functions pq.StringArray `json:"functions" db:"functions" swaggertype:"array,string"`
- NetworkId string `json:"networkId" db:"network_id"`
- PlatformIds pq.StringArray `json:"platformIds" db:"platform_ids" swaggertype:"array,string"`
+ Name string `json:"name" db:"name" validate:"required,max=100"`
+ Address string `json:"address" db:"address" validate:"required,eth_addr"`
+ Functions pq.StringArray `json:"functions" db:"functions" validate:"required" swaggertype:"array,string" `
+ Type string `json:"type" db:"type" validate:"required,oneof=NFT TOKEN NFT_AND_TOKEN"`
+ NetworkId string `json:"networkId" db:"network_id" validate:"required,uuid"`
+ PlatformIds pq.StringArray `json:"platformIds" db:"platform_ids" validate:"required,min=1" swaggertype:"array,string"`
OrganizationId string `json:"-" db:"organization_id"`
}
type RequestContractUpdate struct {
- Name *string `json:"name" db:"name"`
- Address *string `json:"address" db:"address"`
- Functions *pq.StringArray `json:"functions" db:"functions" swaggertype:"array,string"`
- NetworkId *string `json:"networkId" db:"network_id"`
+ Name *string `json:"name" validate:"omitempty,max=100"`
+ Address *string `json:"address" validate:"omitempty,eth_addr"`
+ Functions pq.StringArray `json:"functions" swaggertype:"array,string"`
+ Type *string `json:"type" validate:"omitempty,oneof=NFT TOKEN NFT_AND_TOKEN"`
+ NetworkId *string `json:"networkId" validate:"omitempty,uuid"`
+ PlatformIds pq.StringArray `json:"platformIds" validate:"omitempty,min=1" swaggertype:"array,string"`
}
diff --git a/pkg/repository/contract.go b/pkg/repository/contract.go
index 63d54c5..602aae1 100644
--- a/pkg/repository/contract.go
+++ b/pkg/repository/contract.go
@@ -5,13 +5,13 @@ import (
"database/sql"
"errors"
"fmt"
- "strings"
"github.com/String-xyz/go-lib/v2/common"
libcommon "github.com/String-xyz/go-lib/v2/common"
"github.com/String-xyz/go-lib/v2/database"
"github.com/String-xyz/go-lib/v2/repository"
serror "github.com/String-xyz/go-lib/v2/stringerror"
+ "github.com/lib/pq"
"github.com/String-xyz/dashboard-api/pkg/model"
)
@@ -47,15 +47,14 @@ func NewContract(db database.Queryable) Contract {
// The result is a single contract record with an array of platform ids.
func (c contract[T]) Create(ctx context.Context, request model.RequestContractCreate) (contract model.Contract, err error) {
rows, err := c.Store.QueryxContext(ctx, `
- WITH ins_contract AS (
- INSERT INTO contract (name, address, functions, network_id, organization_id)
- VALUES ($1, $2, $3, $4, $5)
- ON CONFLICT (address, organization_id, network_id) DO NOTHING
- RETURNING *
+ WITH platforms AS (
+ SELECT UNNEST($7::uuid[]) AS platform_id
+ WHERE EXISTS (SELECT 1 FROM platform WHERE organization_id = $6)
),
- platforms AS (
- SELECT UNNEST($6::uuid[]) AS platform_id
- WHERE EXISTS (SELECT 1 FROM platform WHERE organization_id = $5)
+ ins_contract AS (
+ INSERT INTO contract (name, address, functions, type, network_id, organization_id)
+ VALUES ($1, $2, $3, $4, $5, $6)
+ RETURNING *
),
ins_ctp AS (
INSERT INTO contract_to_platform (platform_id, contract_id)
@@ -68,10 +67,16 @@ func (c contract[T]) Create(ctx context.Context, request model.RequestContractCr
FROM ins_contract
JOIN ins_ctp ON ins_contract.id = ins_ctp.contract_id
GROUP BY ins_contract.id, ins_contract.name, ins_contract.address, ins_contract.functions,
- ins_contract.network_id, ins_contract.organization_id, ins_contract.created_at,
+ ins_contract.type, ins_contract.network_id, ins_contract.organization_id, ins_contract.created_at,
ins_contract.updated_at, ins_contract.deleted_at, ins_contract.deactivated_at, ins_contract.deleted_at
- `, request.Name, request.Address, request.Functions, request.NetworkId, request.OrganizationId, request.PlatformIds)
+ `, request.Name, request.Address, request.Functions, request.Type, request.NetworkId, request.OrganizationId, request.PlatformIds)
if err != nil {
+ var pgErr *pq.Error
+ if errors.As(err, &pgErr) {
+ if pgErr.Code == "23505" { // unique violation
+ return contract, common.StringError(serror.ALREADY_IN_USE)
+ }
+ }
return contract, libcommon.StringError(err)
}
for rows.Next() {
@@ -80,10 +85,7 @@ func (c contract[T]) Create(ctx context.Context, request model.RequestContractCr
return contract, libcommon.StringError(err)
}
}
- // If the contract id is empty, it means the contract already exists.
- if contract.Id == "" {
- return contract, common.StringError(serror.ALREADY_IN_USE)
- }
+
defer rows.Close()
return contract, nil
}
@@ -201,7 +203,7 @@ func (c contract[T]) Deactivate(ctx context.Context, id string, organizationId s
SELECT uc.*, array_agg(jctp.platform_id) AS platform_ids
FROM updated_contract uc
JOIN contract_to_platform jctp ON uc.id = jctp.contract_id
- GROUP BY uc.id
+ GROUP BY uc.id, uc.name, uc.address, uc.organization_id, uc.functions, uc.type, uc.network_id, uc.created_at, uc.updated_at, uc.deactivated_at, uc.deleted_at
`, id, organizationId)
return model, libcommon.StringError(err)
@@ -225,38 +227,54 @@ func (c contract[T]) Activate(ctx context.Context, id string, organizationId str
SELECT uc.*, array_agg(jctp.platform_id) AS platform_ids
FROM updated_contract uc
JOIN contract_to_platform jctp ON uc.id = jctp.contract_id
- GROUP BY uc.id
+ GROUP BY uc.id, uc.name, uc.address, uc.organization_id, uc.functions, uc.type, uc.network_id, uc.created_at, uc.updated_at, uc.deactivated_at, uc.deleted_at
`, id, organizationId)
return model, libcommon.StringError(err)
}
func (c contract[T]) Update(ctx context.Context, id string, organizationId string, updates model.RequestContractUpdate) (model model.Contract, err error) {
- names, keyToUpdate := libcommon.KeysAndValues(updates)
- if len(names) == 0 {
- return model, libcommon.StringError(errors.New("no fields to update"))
- }
- query := fmt.Sprintf(`
+ query := `
WITH updated_contract AS (
- UPDATE contract SET %s
- WHERE EXISTS (
- SELECT 1
- FROM contract_to_platform
- JOIN platform ON contract_to_platform.platform_id = platform.id
- WHERE contract_to_platform.contract_id = contract.id
- AND platform.organization_id = %s
- )
- AND contract.id = %s AND deleted_at IS NULL
- RETURNING *
+ UPDATE contract
+ SET name = COALESCE($1, name),
+ address = COALESCE($2, address),
+ functions = COALESCE($3, functions),
+ type = COALESCE($4, type),
+ network_id = COALESCE($5, network_id)
+ WHERE id = $6
+ AND EXISTS (
+ SELECT 1
+ FROM contract_to_platform
+ JOIN platform ON contract_to_platform.platform_id = platform.id
+ WHERE contract_to_platform.contract_id = contract.id
+ AND platform.organization_id = $7
+ )
+ AND deleted_at IS NULL
+ RETURNING *
+ ),
+ valid_platforms AS (
+ SELECT UNNEST($8::uuid[]) AS platform_id
+ FROM platform
+ WHERE organization_id = $7
+ ),
+ new_ctp AS (
+ INSERT INTO contract_to_platform (platform_id, contract_id)
+ SELECT platform_id, id FROM valid_platforms, updated_contract
+ ON CONFLICT (platform_id, contract_id) DO NOTHING
+ RETURNING platform_id, contract_id
)
- SELECT uc.*, array_agg(jctp.platform_id) AS platform_ids
+ SELECT uc.*, array_agg(ctp.platform_id) AS platform_ids
FROM updated_contract uc
- JOIN contract_to_platform jctp ON uc.id = jctp.contract_id
- GROUP BY uc.id
- `, strings.Join(names, ", "), organizationId, id)
- err = c.Store.GetContext(ctx, &model, query, keyToUpdate)
+ JOIN contract_to_platform ctp ON uc.id = ctp.contract_id
+ GROUP BY uc.id, uc.name, uc.address, uc.organization_id, uc.functions, uc.type, uc.network_id, uc.created_at, uc.updated_at, uc.deactivated_at, uc.deleted_at
+ `
+
+ err = c.Store.QueryRowxContext(ctx, query,
+ updates.Name, updates.Address, updates.Functions, updates.Type, updates.NetworkId, id, organizationId, updates.PlatformIds).StructScan(&model)
if err != nil {
- return model, err
+ return model, libcommon.StringError(err)
}
- return model, err
+
+ return model, nil
}
diff --git a/pkg/repository/member_invite.go b/pkg/repository/member_invite.go
index 683c251..447e598 100644
--- a/pkg/repository/member_invite.go
+++ b/pkg/repository/member_invite.go
@@ -49,7 +49,7 @@ type MemberInvite interface {
GetById(ctx context.Context, id string) (invite MemberInviteInfo, err error)
List(ctx context.Context, limit int, offset int) (invites []model.MemberInvite, err error)
Update(ctx context.Context, id string, updates any) error
- GetByOrganization(ctx context.Context, organizationId string) (invites []MemberInviteInfo, err error)
+ GetByOrganization(ctx context.Context, organizationId string, limit int, offset int) (invites []MemberInviteInfo, err error)
GetByEmail(ctx context.Context, email string) (invite MemberInviteInfo, err error)
SoftDelete(ctx context.Context, id string) error
}
@@ -89,8 +89,11 @@ func (i memberInvite[T]) Create(ctx context.Context, request model.MemberInvite)
return invite, nil
}
-func (i memberInvite[T]) GetByOrganization(ctx context.Context, organizationId string) (invites []MemberInviteInfo, err error) {
- err = i.Store.Select(&invites, getBaseQuery()+`WHERE member_invite.organization_id = $1 AND member_invite.deleted_at IS NULL`, organizationId)
+func (i memberInvite[T]) GetByOrganization(ctx context.Context, organizationId string, limit int, offset int) (invites []MemberInviteInfo, err error) {
+ if limit == 0 {
+ limit = 100
+ }
+ err = i.Store.Select(&invites, getBaseQuery()+`WHERE member_invite.organization_id = $1 AND member_invite.deleted_at IS NULL LIMIT $2 OFFSET $3`, organizationId, limit, offset)
if err != nil && err == sql.ErrNoRows {
return []MemberInviteInfo{}, common.StringError(serror.NOT_FOUND)
diff --git a/pkg/service/contract.go b/pkg/service/contract.go
index c274a6e..358f81e 100644
--- a/pkg/service/contract.go
+++ b/pkg/service/contract.go
@@ -11,7 +11,7 @@ import (
type Contract interface {
Create(ctx context.Context, create model.RequestContractCreate, callerId string, organizationId string) (model.Contract, error)
- GetAll(ctx context.Context, platformId string, organizationId string) ([]model.Contract, error)
+ GetAll(ctx context.Context, platformId string, organizationId string, limit int, offset int) ([]model.Contract, error)
Get(ctx context.Context, contractId string, organizationId string) (model.Contract, error)
Deactivate(ctx context.Context, contractId string, callerId string, organizationId string) (model.Contract, error)
Reactivate(ctx context.Context, contractId string, callerId string, organizationId string) (model.Contract, error)
@@ -45,17 +45,17 @@ func (c contract) Create(ctx context.Context, create model.RequestContractCreate
return contract, nil
}
-func (c contract) GetAll(ctx context.Context, platformId string, organizationId string) (contracts []model.Contract, err error) {
+func (c contract) GetAll(ctx context.Context, platformId string, organizationId string, limit int, offset int) (contracts []model.Contract, err error) {
_, finish := Span(ctx, "service.contract.GetAll", SpanTag{"organizationId": organizationId})
defer finish()
if platformId != "" {
- contracts, err = c.repos.Contract.ListByPlatform(ctx, platformId, 0, 0)
+ contracts, err = c.repos.Contract.ListByPlatform(ctx, platformId, limit, offset)
if err != nil {
return contracts, common.StringError(err)
}
} else {
- contracts, err = c.repos.Contract.ListByOrganization(ctx, organizationId, 0, 0)
+ contracts, err = c.repos.Contract.ListByOrganization(ctx, organizationId, limit, offset)
if err != nil {
return contracts, common.StringError(err)
}
diff --git a/pkg/service/invite.go b/pkg/service/invite.go
index e2ff63c..88deb6d 100644
--- a/pkg/service/invite.go
+++ b/pkg/service/invite.go
@@ -19,8 +19,8 @@ import (
type Invite interface {
Send(ctx context.Context, request model.RequestInviteSend, callerId *string, organizationId string) (repository.MemberInviteInfo, error)
Accept(ctx context.Context, inviteId string, requestBody model.RequestInviteAcceptance) (model.OrganizationMember, JWT, error)
- List(ctx context.Context, status string, organizationId string) ([]repository.MemberInviteInfo, error)
- Resend(ctx context.Context, inviteId string, callerId string) (repository.MemberInviteInfo, error)
+ List(ctx context.Context, status string, organizationId string, limit int, offset int) ([]repository.MemberInviteInfo, error)
+ Resend(ctx context.Context, inviteId string, callerId string, organizationId string) (repository.MemberInviteInfo, error)
Update(ctx context.Context, request model.RequestInviteUpdate, inviteId string, callerId string) (repository.MemberInviteInfo, error)
Revoke(ctx context.Context, inviteId string, callerId string) error
Get(ctx context.Context, id string) (repository.MemberInviteInfo, error)
@@ -63,10 +63,16 @@ func (i invite) Send(ctx context.Context, request model.RequestInviteSend, calle
if err != nil {
return repository.MemberInviteInfo{}, common.StringError(err)
}
- return i.Resend(ctx, newInvite.Id, *callerId)
+ return i.Resend(ctx, newInvite.Id, *callerId, organizationId)
}
roleId := GetRoleId(request.Role)
+
+ organization, err := i.repos.Organization.GetById(ctx, organizationId)
+ if err != nil {
+ return repository.MemberInviteInfo{}, common.StringError(err)
+ }
+
// TODO: VULNERABILITY! Ensure Owner can only be set as role if no other users exist!
invite, err := i.repos.MemberInvite.Create(ctx, model.MemberInvite{Email: request.Email, InvitedBy: callerId, OrganizationId: organizationId, Name: request.Name, RoleId: roleId})
if err != nil {
@@ -82,7 +88,7 @@ func (i invite) Send(ctx context.Context, request model.RequestInviteSend, calle
token = url.QueryEscape(token) // make
emailer := emailer.New()
- err = emailer.SendInviteEmail(ctx, request.Email, token, invite.Id, request.Name)
+ err = emailer.SendInviteEmail(ctx, request.Email, token, invite.Id, request.Name, organization.Name)
if err != nil {
return invite, common.StringError(err)
}
@@ -170,11 +176,11 @@ func (i invite) Accept(ctx context.Context, inviteId string, requestBody model.R
return member, jwt, nil
}
-func (i invite) List(ctx context.Context, status string, organizationId string) ([]repository.MemberInviteInfo, error) {
+func (i invite) List(ctx context.Context, status string, organizationId string, limit int, offset int) ([]repository.MemberInviteInfo, error) {
_, finish := Span(ctx, "service.invite.List", SpanTag{"organizationId": organizationId})
defer finish()
- result, err := i.repos.MemberInvite.GetByOrganization(ctx, organizationId)
+ result, err := i.repos.MemberInvite.GetByOrganization(ctx, organizationId, limit, offset)
if err != nil {
return result, common.StringError(err)
}
@@ -182,14 +188,22 @@ func (i invite) List(ctx context.Context, status string, organizationId string)
return result, nil
}
-func (i invite) Resend(ctx context.Context, inviteId string, callerId string) (repository.MemberInviteInfo, error) {
+func (i invite) Resend(ctx context.Context, inviteId string, callerId string, organizationId string) (repository.MemberInviteInfo, error) {
_, finish := Span(ctx, "service.invite.Resend")
defer finish()
+
+ organization, err := i.repos.Organization.GetById(ctx, organizationId)
+ if err != nil {
+ return repository.MemberInviteInfo{}, common.StringError(err)
+ }
+
result := repository.MemberInviteInfo{}
- err := RequireAuthority(i.repos, callerId, "Admin", "Owner")
+
+ err = RequireAuthority(i.repos, callerId, "Admin", "Owner")
if err != nil {
return result, common.StringError(err)
}
+
result, err = i.repos.MemberInvite.GetById(ctx, inviteId)
if err != nil {
return result, common.StringError(err)
@@ -205,7 +219,7 @@ func (i invite) Resend(ctx context.Context, inviteId string, callerId string) (r
token = url.QueryEscape(token)
emailer := emailer.New()
- err = emailer.SendInviteEmail(ctx, result.Email, token, result.Id, result.Name)
+ err = emailer.SendInviteEmail(ctx, result.Email, token, result.Id, result.Name, organization.Name)
if err != nil {
return result, common.StringError(err)
}
diff --git a/pkg/service/member.go b/pkg/service/member.go
index 82e42ec..456f297 100644
--- a/pkg/service/member.go
+++ b/pkg/service/member.go
@@ -21,7 +21,7 @@ type MemberCreateResponse struct {
}
type Member interface {
- GetAll(ctx context.Context, organizationId string) ([]repository.OrganizationMemberWithRole, error)
+ GetAll(ctx context.Context, organizationId string, limit int, offset int) ([]repository.OrganizationMemberWithRole, error)
Get(ctx context.Context, callerId string, organizationId string, memberId string) (repository.OrganizationMemberWithRole, error)
UpdateMember(ctx context.Context, request model.RequestMemberUpdateOther, callerId string, memberId string) (repository.OrganizationMemberWithRole, error)
UpdateSelf(ctx context.Context, request model.RequestMemberUpdateSelf, callerId string) (repository.OrganizationMemberWithRole, error)
@@ -40,11 +40,11 @@ func NewMember(repos repository.Repositories) Member {
return &member{repos}
}
-func (m member) GetAll(ctx context.Context, organizationId string) ([]repository.OrganizationMemberWithRole, error) {
+func (m member) GetAll(ctx context.Context, organizationId string, limit int, offset int) ([]repository.OrganizationMemberWithRole, error) {
_, finish := Span(ctx, "service.member.GetAll", SpanTag{"organizationId": organizationId})
defer finish()
- result, err := m.repos.OrganizationMember.List(ctx, organizationId, 0, 0)
+ result, err := m.repos.OrganizationMember.List(ctx, organizationId, limit, offset)
if err != nil {
return result, common.StringError(err)
}
diff --git a/pkg/service/network.go b/pkg/service/network.go
index 6b70baf..7c3438d 100644
--- a/pkg/service/network.go
+++ b/pkg/service/network.go
@@ -9,7 +9,7 @@ import (
)
type Network interface {
- GetAll(ctx context.Context) ([]model.NetworkData, error)
+ GetAll(ctx context.Context, limit int, offset int) ([]model.NetworkData, error)
}
type network struct {
@@ -20,11 +20,11 @@ func NewNetwork(repos repository.Repositories) Network {
return &network{repos}
}
-func (n network) GetAll(ctx context.Context) ([]model.NetworkData, error) {
+func (n network) GetAll(ctx context.Context, limit int, offset int) ([]model.NetworkData, error) {
_, finish := Span(ctx, "service.network.GetAll")
defer finish()
- networks, err := n.repos.Network.List(ctx, 0, 0)
+ networks, err := n.repos.Network.List(ctx, limit, offset)
if err != nil {
return nil, common.StringError(err)
}
diff --git a/pkg/service/platform.go b/pkg/service/platform.go
index 67310e0..04882c3 100644
--- a/pkg/service/platform.go
+++ b/pkg/service/platform.go
@@ -14,7 +14,7 @@ import (
type Platform interface {
Create(ctx context.Context, request model.RequestPlatformCreate, organizationId string) (platform model.Platform, err error)
Get(ctx context.Context, platformId string, organizationId string) (platform model.Platform, err error)
- GetAll(ctx context.Context, callerId string, organizationId string) (platforms []model.Platform, err error)
+ GetAll(ctx context.Context, callerId string, organizationId string, limit int, offset int) (platforms []model.Platform, err error)
Update(ctx context.Context, request model.RequestPlatformUpdate, platformId string, callerId string, organizationId string) (platform model.Platform, err error)
Deactivate(ctx context.Context, platformId string, callerId string, organizationId string) (model.Platform, error)
Reactivate(ctx context.Context, platformId string, callerId string, organizationId string) (model.Platform, error)
@@ -57,11 +57,11 @@ func (p platform) Get(ctx context.Context, platformId string, organizationId str
return platform, nil
}
-func (p platform) GetAll(ctx context.Context, callerId string, organizationId string) (platforms []model.Platform, err error) {
+func (p platform) GetAll(ctx context.Context, callerId string, organizationId string, limit int, offset int) (platforms []model.Platform, err error) {
_, finish := Span(ctx, "service.platform.GetAll", SpanTag{"organizationId": organizationId})
defer finish()
- platforms, err = p.repos.Platform.List(ctx, organizationId, 0, 0)
+ platforms, err = p.repos.Platform.List(ctx, organizationId, limit, offset)
if err != nil {
return platforms, common.StringError(err)
}