├── app.yaml
├── config
├── files
│ ├── config.dev.yaml
│ ├── config.prod.yaml
│ ├── config.test-int.yaml
│ ├── config.test.yaml
│ ├── config.staging.yaml
│ └── config.test-e2e.yaml
├── site.go
├── mail.go
├── magic.go
├── twilio.go
├── config.go
├── jwt.go
└── postgres.go
├── mockgopg
├── build_insert.go
├── build_query.go
├── formatter.go
├── row_result.go
├── mock.go
└── orm.go
├── public
├── assets
│ └── img
│ │ ├── header_logo.png
│ │ ├── header_logo2.png
│ │ ├── verify_email.png
│ │ ├── welcome_page.png
│ │ ├── failed_approval.png
│ │ ├── forgot_password.png
│ │ ├── social_icons_fb.png
│ │ ├── social_icons_twitter.png
│ │ ├── social_icons_instagram.png
│ │ ├── submitted_application.png
│ │ ├── forgot_password.svg
│ │ ├── verify_email.svg
│ │ ├── failed_approval.svg
│ │ ├── welcome_page.svg
│ │ └── submitted_application.svg
├── MA.svg
├── 2140998d-7f62-46f2-a9b2-e44350bd4807.svg
├── FB.svg
├── fc6a5dcd-4a70-4b8d-b64f-d83a6dae9ba4.svg
├── TSLA.svg
├── 8ccae427-5dd0-45b3-b5fe-7ba5e422c766.svg
├── AMZN.svg
├── f801f835-bfe6-4a9d-a6b1-ccbb84bfd75f.svg
├── GOOG.svg
├── GOOGL.svg
├── V.svg
├── 69b15845-7c63-4586-b274-1cfdfe9df3d8.svg
├── AAPL.svg
├── f30d734c-2806-4d0d-b145-f9fade61432b.svg
├── 4f5baf1e-0e9b-4d85-b88a-d874dc4a3c42.svg
├── b0b6dd9d-8b9b-48a9-ba46-b9d54906e415.svg
├── NFLX.svg
├── bb2a26c0-4c77-4801-8afc-82e8142ac7b8.svg
├── GE.svg
├── 57c36644-876b-437c-b913-3cdb58b18fd3.svg
├── SNAP.svg
├── 83e52ac1-bb18-4e9f-b68d-dda5a8af3ec0.svg
├── TME.svg
└── 662a919f-1455-497c-90e7-f76248e6d3a6.svg
├── NOTICE
├── CaddyFile
├── route
├── custom_route.go
└── route.go
├── mobile
├── mobile_interface.go
└── mobile.go
├── generate-ssl.sh
├── middleware
├── middleware.go
├── middleware_test.go
├── jwt_test.go
└── jwt.go
├── .mergify.yml
├── secret
├── secret_interface.go
├── secret.go
└── cryptorandom.go
├── initdb.sh
├── model
├── plaid.go
├── verification.go
├── reward.go
├── coins_transactions.go
├── bank_account.go
├── user_reward.go
├── role.go
├── asset.go
├── user_test.go
├── model_test.go
├── auth.go
└── model.go
├── mock
├── middleware.go
├── auth.go
├── mobile.go
├── magic.go
├── secret.go
├── rbac.go
├── mail.go
├── mock.go
└── mockdb
│ ├── user.go
│ └── account.go
├── Dockerfile
├── magic
├── magic_interface.go
└── magic.go
├── e2e
├── e2e.go
├── mobile_i_test.go
├── signupemail_i_test.go
├── login_i_test.go
└── e2e_i_test.go
├── entry
└── main.go
├── repository
├── platform
│ ├── query
│ │ └── query.go
│ └── structs
│ │ └── structs.go
├── transfer
│ └── transfer.go
├── role.go
├── assets
│ └── assets.go
├── rbac.go
├── user
│ └── user.go
├── account
│ └── account.go
├── rbac_i_test.go
├── asset.go
└── user_test.go
├── .vscode
└── launch.json
├── mail
└── mail_interface.go
├── manager
├── createschema.go
├── createdbuser.go
├── createdb.go
└── manager.go
├── cmd
├── migrate_reset.go
├── migrate_down.go
├── generate_secret.go
├── migrate_version.go
├── migrate_init.go
├── migrate.go
├── migrate_up.go
├── migrate_set_version.go
├── create_db.go
├── create_schema.go
├── create_superadmin.go
├── sync_assets.go
└── root.go
├── test.sh
├── alpaca.go
├── docker-compose.yml
├── request
├── request.go
├── bank_account.go
├── auth.go
├── signup.go
└── user.go
├── testhelper
└── testhelper.go
├── templates
└── terms_conditions_old.html
├── go.mod
├── .gitignore
├── .env.sample
├── server
└── server.go
├── service
├── user.go
└── plaid.go
├── .github
└── workflows
│ └── codeql-analysis.yml
├── apperr
└── apperr.go
├── migration
└── migration.go
├── k8-cluster.yaml
└── countries.csv
/app.yaml:
--------------------------------------------------------------------------------
1 | application: github.com/alpacahq/ribbit-backend
--------------------------------------------------------------------------------
/config/files/config.dev.yaml:
--------------------------------------------------------------------------------
1 | server:
2 | mode: "debug"
--------------------------------------------------------------------------------
/config/files/config.prod.yaml:
--------------------------------------------------------------------------------
1 | server:
2 | mode: "release"
--------------------------------------------------------------------------------
/config/files/config.test-int.yaml:
--------------------------------------------------------------------------------
1 | server:
2 | mode: test
--------------------------------------------------------------------------------
/config/files/config.test.yaml:
--------------------------------------------------------------------------------
1 | server:
2 | mode: "test"
--------------------------------------------------------------------------------
/config/files/config.staging.yaml:
--------------------------------------------------------------------------------
1 | server:
2 | mode: "release"
--------------------------------------------------------------------------------
/config/files/config.test-e2e.yaml:
--------------------------------------------------------------------------------
1 | server:
2 | mode: "test"
--------------------------------------------------------------------------------
/mockgopg/build_insert.go:
--------------------------------------------------------------------------------
1 | package mockgopg
2 |
3 | type buildInsert struct {
4 | insert string
5 | err error
6 | }
7 |
--------------------------------------------------------------------------------
/public/assets/img/header_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alpacahq/ribbit-backend/HEAD/public/assets/img/header_logo.png
--------------------------------------------------------------------------------
/public/assets/img/header_logo2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alpacahq/ribbit-backend/HEAD/public/assets/img/header_logo2.png
--------------------------------------------------------------------------------
/public/assets/img/verify_email.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alpacahq/ribbit-backend/HEAD/public/assets/img/verify_email.png
--------------------------------------------------------------------------------
/public/assets/img/welcome_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alpacahq/ribbit-backend/HEAD/public/assets/img/welcome_page.png
--------------------------------------------------------------------------------
/public/assets/img/failed_approval.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alpacahq/ribbit-backend/HEAD/public/assets/img/failed_approval.png
--------------------------------------------------------------------------------
/public/assets/img/forgot_password.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alpacahq/ribbit-backend/HEAD/public/assets/img/forgot_password.png
--------------------------------------------------------------------------------
/public/assets/img/social_icons_fb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alpacahq/ribbit-backend/HEAD/public/assets/img/social_icons_fb.png
--------------------------------------------------------------------------------
/public/assets/img/social_icons_twitter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alpacahq/ribbit-backend/HEAD/public/assets/img/social_icons_twitter.png
--------------------------------------------------------------------------------
/public/assets/img/social_icons_instagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alpacahq/ribbit-backend/HEAD/public/assets/img/social_icons_instagram.png
--------------------------------------------------------------------------------
/public/assets/img/submitted_application.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alpacahq/ribbit-backend/HEAD/public/assets/img/submitted_application.png
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | Ribbit
2 | Copyright 2021 AlpacaDB, Inc.
3 |
4 | This product includes software developed at
5 | AlpacaDB, Inc. (https://alpaca.markets/)
6 |
--------------------------------------------------------------------------------
/CaddyFile:
--------------------------------------------------------------------------------
1 | ribbit.com:443 {
2 | tls /root/certs/ribbit-public.com.pem /root/certs/ribbit.com.pem
3 |
4 | reverse_proxy http://ribbit:8080
5 | }
6 |
--------------------------------------------------------------------------------
/route/custom_route.go:
--------------------------------------------------------------------------------
1 | package route
2 |
3 | // ServicesI is the interface for our user-defined custom routes and related services
4 | type ServicesI interface {
5 | SetupRoutes()
6 | }
7 |
--------------------------------------------------------------------------------
/mockgopg/build_query.go:
--------------------------------------------------------------------------------
1 | package mockgopg
2 |
3 | type buildQuery struct {
4 | funcName string
5 | query string
6 | params []interface{}
7 | result *OrmResult
8 | err error
9 | }
10 |
--------------------------------------------------------------------------------
/mobile/mobile_interface.go:
--------------------------------------------------------------------------------
1 | package mobile
2 |
3 | // Service is the interface to our mobile service
4 | type Service interface {
5 | GenerateSMSToken(countryCode, mobile string) error
6 | CheckCode(countryCode, mobile, code string) error
7 | }
8 |
--------------------------------------------------------------------------------
/generate-ssl.sh:
--------------------------------------------------------------------------------
1 | mkcert -key-file ribbit.com.pem -cert-file ribbit-public.com.pem ribbit.com
2 |
3 | # for client request encryption
4 | openssl genrsa -out private_key.pem 1024
5 | openssl rsa -in private_key.pem -outform PEM -pubout -out public_key.pem
6 |
--------------------------------------------------------------------------------
/middleware/middleware.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import "github.com/gin-gonic/gin"
4 |
5 | // Add adds middlewares to gin engine
6 | func Add(r *gin.Engine, h ...gin.HandlerFunc) {
7 | for _, v := range h {
8 | r.Use(v)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.mergify.yml:
--------------------------------------------------------------------------------
1 | pull_request_rules:
2 | - name: automatic merge for Dependabot pull requests
3 | conditions:
4 | - author=dependabot[bot]
5 | - status-success=Circle CI - Pull Request
6 | actions:
7 | merge:
8 | method: merge
9 |
--------------------------------------------------------------------------------
/secret/secret_interface.go:
--------------------------------------------------------------------------------
1 | package secret
2 |
3 | // Service is the interface to our secret service
4 | type Service interface {
5 | HashPassword(password string) string
6 | HashMatchesPassword(hash, password string) bool
7 | HashRandomPassword() (string, error)
8 | }
9 |
--------------------------------------------------------------------------------
/initdb.sh:
--------------------------------------------------------------------------------
1 | source .env
2 | export JWT_SECRET=$(openssl rand -base64 256)
3 | echo $JWT_SECRET
4 |
5 | go run ./entry create_db
6 | go run ./entry create_schema
7 | go run ./entry create_superadmin -e test_super_admin@gmail.com -p password
8 |
9 | go run ./entry/main.go
10 |
--------------------------------------------------------------------------------
/model/plaid.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | type PlaidAuthToken struct {
4 | LinkToken string `json:"link_token"`
5 | }
6 |
7 | type AccessToken struct {
8 | ID int `json:"id"`
9 | PublicToken string `json:"public_token"`
10 | ItemID string `json:"item_id"`
11 | }
12 |
--------------------------------------------------------------------------------
/middleware/middleware_test.go:
--------------------------------------------------------------------------------
1 | package middleware_test
2 |
3 | import (
4 | "testing"
5 |
6 | mw "github.com/alpacahq/ribbit-backend/middleware"
7 |
8 | "github.com/gin-gonic/gin"
9 | )
10 |
11 | func TestAdd(t *testing.T) {
12 | gin.SetMode(gin.TestMode)
13 | r := gin.New()
14 | mw.Add(r, gin.Logger())
15 | }
16 |
--------------------------------------------------------------------------------
/model/verification.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | func init() {
4 | Register(&Verification{})
5 | }
6 |
7 | // Verification stores randomly generated tokens that can be redeemed
8 | type Verification struct {
9 | Base
10 | ID int `json:"id"`
11 | Token string `json:"token"`
12 | UserID int `json:"user_id"`
13 | }
14 |
--------------------------------------------------------------------------------
/mock/middleware.go:
--------------------------------------------------------------------------------
1 | package mock
2 |
3 | import "github.com/alpacahq/ribbit-backend/model"
4 |
5 | // JWT mock
6 | type JWT struct {
7 | GenerateTokenFn func(*model.User) (string, string, error)
8 | }
9 |
10 | // GenerateToken mock
11 | func (j *JWT) GenerateToken(u *model.User) (string, string, error) {
12 | return j.GenerateTokenFn(u)
13 | }
14 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.16-alpine
2 | RUN apk update && apk add --virtual build-dependencies build-base gcc wget git openssl
3 |
4 | WORKDIR /app
5 |
6 | COPY ./ ./
7 | RUN go mod download
8 | RUN cp .env.sample .env
9 | RUN chmod 100 ./initdb.sh
10 | # RUN go build -o mvp ./entry/
11 |
12 | EXPOSE 8080
13 |
14 | CMD [ "go", "run", "./entry/main.go" ]
--------------------------------------------------------------------------------
/magic/magic_interface.go:
--------------------------------------------------------------------------------
1 | package magic
2 |
3 | import (
4 | mag "github.com/magiclabs/magic-admin-go"
5 | "github.com/magiclabs/magic-admin-go/token"
6 | )
7 |
8 | // Service is the interface to our magic service
9 | type Service interface {
10 | IsValidToken(string) (*token.Token, error)
11 | GetIssuer(*token.Token) (*mag.UserInfo, error)
12 | }
13 |
--------------------------------------------------------------------------------
/mock/auth.go:
--------------------------------------------------------------------------------
1 | package mock
2 |
3 | import (
4 | "github.com/alpacahq/ribbit-backend/model"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | // Auth mock
10 | type Auth struct {
11 | UserFn func(*gin.Context) *model.AuthUser
12 | }
13 |
14 | // User mock
15 | func (a *Auth) User(c *gin.Context) *model.AuthUser {
16 | return a.UserFn(c)
17 | }
18 |
--------------------------------------------------------------------------------
/model/reward.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | func init() {
4 | Register(&Reward{})
5 | }
6 |
7 | type Reward struct {
8 | Base
9 | ID int `json:"id"`
10 | PerAccountLimit int `json:"per_account_limit"`
11 | ReferralKycReward float64 `json:"referral_kyc_reward"`
12 | ReferralSignupReward float64 `json:"referral_signup_reward"`
13 | ReferreKycReward float64 `json:"referre_Kyc_reward"`
14 | }
15 |
--------------------------------------------------------------------------------
/mockgopg/formatter.go:
--------------------------------------------------------------------------------
1 | package mockgopg
2 |
3 | import "github.com/go-pg/pg/v9/orm"
4 |
5 | // Formatter implements orm.Formatter
6 | type Formatter struct {
7 | }
8 |
9 | // FormatQuery formats our query and params to byte
10 | func (f *Formatter) FormatQuery(b []byte, query string, params ...interface{}) []byte {
11 | formatter := new(orm.Formatter)
12 | got := formatter.FormatQuery(b, query, params...)
13 | return got
14 | }
15 |
--------------------------------------------------------------------------------
/model/coins_transactions.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | func init() {
4 | Register(&CoinStatement{})
5 | }
6 |
7 | // Verification stores randomly generated tokens that can be redeemed
8 | type CoinStatement struct {
9 | Base
10 | ID int `json:"id"`
11 | UserID int `json:"user_id"`
12 | Coins int `json:"coins"`
13 | Type string `json:"type"`
14 | Reason string `json:"reason"`
15 | Status bool `json:"status"`
16 | }
17 |
--------------------------------------------------------------------------------
/e2e/e2e.go:
--------------------------------------------------------------------------------
1 | package e2e
2 |
3 | import (
4 | "github.com/alpacahq/ribbit-backend/manager"
5 | "github.com/alpacahq/ribbit-backend/model"
6 | )
7 |
8 | // SetupDatabase creates the schema, populates it with data and returns with superadmin user
9 | func SetupDatabase(m *manager.Manager) (*model.User, error) {
10 | models := manager.GetModels()
11 | m.CreateSchema(models...)
12 | m.CreateRoles()
13 | return m.CreateSuperAdmin("superuser@example.org", "testpassword")
14 | }
15 |
--------------------------------------------------------------------------------
/entry/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | alpaca "github.com/alpacahq/ribbit-backend"
7 | )
8 |
9 | func main() {
10 | alpaca.New().
11 | WithRoutes(&MyServices{}).
12 | Run()
13 | }
14 |
15 | // MyServices implements github.com/alpacahq/ribbit-backend/route.ServicesI
16 | type MyServices struct{}
17 |
18 | // SetupRoutes is our implementation of custom routes
19 | func (s *MyServices) SetupRoutes() {
20 | fmt.Println("set up our custom routes!")
21 | }
22 |
--------------------------------------------------------------------------------
/repository/platform/query/query.go:
--------------------------------------------------------------------------------
1 | package query
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/alpacahq/ribbit-backend/apperr"
7 | "github.com/alpacahq/ribbit-backend/model"
8 | )
9 |
10 | // List prepares data for list queries
11 | func List(u *model.AuthUser) (*model.ListQuery, error) {
12 | switch true {
13 | case int(u.Role) <= 2: // user is SuperAdmin or Admin
14 | return nil, nil
15 | default:
16 | return nil, apperr.New(http.StatusForbidden, "Forbidden")
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Launch",
9 | "type": "go",
10 | "request": "launch",
11 | "mode": "auto",
12 | "program": "${fileDirname}",
13 | "env": {},
14 | "args": []
15 | }
16 | ]
17 | }
--------------------------------------------------------------------------------
/mail/mail_interface.go:
--------------------------------------------------------------------------------
1 | package mail
2 |
3 | import "github.com/alpacahq/ribbit-backend/model"
4 |
5 | // Service is the interface to access our Mail
6 | type Service interface {
7 | Send(subject string, toName string, toEmail string, content string, HTMLContent string) error
8 | SendWithDefaults(subject, toEmail, content string, HTMLContent string) error
9 | SendVerificationEmail(toEmail string, v *model.Verification) error
10 | SendForgotVerificationEmail(toEmail string, v *model.Verification) error
11 | }
12 |
--------------------------------------------------------------------------------
/manager/createschema.go:
--------------------------------------------------------------------------------
1 | package manager
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/go-pg/pg/v9"
7 | "github.com/go-pg/pg/v9/orm"
8 | )
9 |
10 | // CreateSchema creates the tables for given models
11 | func CreateSchema(db *pg.DB, models ...interface{}) {
12 | for _, model := range models {
13 | opt := &orm.CreateTableOptions{
14 | IfNotExists: true,
15 | FKConstraints: true,
16 | }
17 | err := db.CreateTable(model, opt)
18 | if err != nil {
19 | log.Fatal(err)
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/model/bank_account.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | func init() {
4 | Register(&BankAccount{})
5 | }
6 |
7 | // Verification stores randomly generated tokens that can be redeemed
8 | type BankAccount struct {
9 | Base
10 | ID int `json:"id"`
11 | UserID int `json:"user_id"`
12 | AccessToken string `json:"access_token"`
13 | AccountID string `json:"account_id"`
14 | BankName string `json:"bank_name"`
15 | AccountName string `json:"account_name"`
16 | Status bool `json:"status"`
17 | }
18 |
--------------------------------------------------------------------------------
/mock/mobile.go:
--------------------------------------------------------------------------------
1 | package mock
2 |
3 | // Mobile mock
4 | type Mobile struct {
5 | GenerateSMSTokenFn func(string, string) error
6 | CheckCodeFn func(string, string, string) error
7 | }
8 |
9 | // GenerateSMSToken mock
10 | func (m *Mobile) GenerateSMSToken(countryCode, mobile string) error {
11 | return m.GenerateSMSTokenFn(countryCode, mobile)
12 | }
13 |
14 | // CheckCode mock
15 | func (m *Mobile) CheckCode(countryCode, mobile, code string) error {
16 | return m.CheckCodeFn(countryCode, mobile, code)
17 | }
18 |
--------------------------------------------------------------------------------
/mock/magic.go:
--------------------------------------------------------------------------------
1 | package mock
2 |
3 | import (
4 | mag "github.com/magiclabs/magic-admin-go"
5 | "github.com/magiclabs/magic-admin-go/token"
6 | )
7 |
8 | type Magic struct {
9 | IsValidtokenFn func(string) (*token.Token, error)
10 | GetIssuerFn func(*token.Token) (*mag.UserInfo, error)
11 | }
12 |
13 | func (m *Magic) IsValidToken(token string) (*token.Token, error) {
14 | return m.IsValidtokenFn(token)
15 | }
16 |
17 | func (m *Magic) GetIssuer(tok *token.Token) (*mag.UserInfo, error) {
18 | return m.GetIssuerFn(tok)
19 | }
20 |
--------------------------------------------------------------------------------
/model/user_reward.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | func init() {
4 | Register(&UserReward{})
5 | }
6 |
7 | type UserReward struct {
8 | Base
9 | ID int `json:"id"`
10 | UserID int `json:"user_id"`
11 | JournalID string `json:"journal_id"`
12 | ReferredBy int `json:"referred_by"`
13 | RewardValue float32 `json:"reward_value"`
14 | RewardType string `json:"reward_type"`
15 | RewardTransferStatus bool `json:"reward_transfer_status"`
16 | ErrorResponse string `json:"error_response"`
17 | }
18 |
--------------------------------------------------------------------------------
/cmd/migrate_reset.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "log"
6 |
7 | "github.com/alpacahq/ribbit-backend/migration"
8 |
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | // resetCmd represents the reset command
13 | var resetCmd = &cobra.Command{
14 | Use: "reset",
15 | Short: "reset all migrations",
16 | Long: `reset all migrations`,
17 | Run: func(cmd *cobra.Command, args []string) {
18 | fmt.Println("reset called")
19 | err := migration.Run("reset")
20 | if err != nil {
21 | log.Fatal(err)
22 | }
23 | },
24 | }
25 |
26 | func init() {
27 | migrateCmd.AddCommand(resetCmd)
28 | }
29 |
--------------------------------------------------------------------------------
/cmd/migrate_down.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "log"
6 |
7 | "github.com/alpacahq/ribbit-backend/migration"
8 |
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | // downCmd represents the down command
13 | var downCmd = &cobra.Command{
14 | Use: "down",
15 | Short: "reverts the last migration",
16 | Long: `reverts the last migration`,
17 | Run: func(cmd *cobra.Command, args []string) {
18 | fmt.Println("down called")
19 | err := migration.Run("down")
20 | if err != nil {
21 | log.Fatal(err)
22 | }
23 | },
24 | }
25 |
26 | func init() {
27 | migrateCmd.AddCommand(downCmd)
28 | }
29 |
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | case "$1" in
4 | -s | --short)
5 | case "$2" in
6 | -c | --coverage) echo "Run only unit tests (with coverage)"
7 | go test -v -coverprofile c.out -short ./...
8 | go tool cover -html=c.out
9 | ;;
10 | *) echo "Run only unit tests"
11 | go test -v -short ./...
12 | ;;
13 | esac
14 | ;;
15 | -i | --integration) echo "Run only integration tests"
16 | go test -v -run Integration ./...
17 | ;;
18 | *) echo "Run all tests (with coverage)"
19 | go test -coverprofile c.out ./...
20 | go tool cover -html=c.out
21 | ;;
22 | esac
--------------------------------------------------------------------------------
/cmd/generate_secret.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "log"
6 |
7 | "github.com/alpacahq/ribbit-backend/secret"
8 |
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | // createCmd represents the migrate command
13 | var generateSecretCmd = &cobra.Command{
14 | Use: "generate_secret",
15 | Short: "generate_secret",
16 | Long: `generate_secret`,
17 | Run: func(cmd *cobra.Command, args []string) {
18 | s, err := secret.GenerateRandomString(256)
19 | if err != nil {
20 | log.Fatal(err)
21 | }
22 | fmt.Printf("\nJWT_SECRET=%s\n\n", s)
23 | },
24 | }
25 |
26 | func init() {
27 | rootCmd.AddCommand(generateSecretCmd)
28 | }
29 |
--------------------------------------------------------------------------------
/cmd/migrate_version.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "log"
6 |
7 | "github.com/alpacahq/ribbit-backend/migration"
8 |
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | // versionCmd represents the version command
13 | var versionCmd = &cobra.Command{
14 | Use: "version",
15 | Short: "version prints current db version",
16 | Long: `version prints current db version`,
17 | Run: func(cmd *cobra.Command, args []string) {
18 | fmt.Println("version called")
19 | err := migration.Run("version")
20 | if err != nil {
21 | log.Fatal(err)
22 | }
23 | },
24 | }
25 |
26 | func init() {
27 | migrateCmd.AddCommand(versionCmd)
28 | }
29 |
--------------------------------------------------------------------------------
/cmd/migrate_init.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "log"
6 |
7 | "github.com/alpacahq/ribbit-backend/migration"
8 |
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | // initCmd represents the init command
13 | var initCmd = &cobra.Command{
14 | Use: "init",
15 | Short: "init creates version info table in the database",
16 | Long: `init creates version info table in the database`,
17 | Run: func(cmd *cobra.Command, args []string) {
18 | fmt.Println("init called")
19 | err := migration.Run("init")
20 | if err != nil {
21 | log.Fatal(err)
22 | }
23 | },
24 | }
25 |
26 | func init() {
27 | migrateCmd.AddCommand(initCmd)
28 | }
29 |
--------------------------------------------------------------------------------
/public/MA.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/mock/secret.go:
--------------------------------------------------------------------------------
1 | package mock
2 |
3 | // Password mock
4 | type Password struct {
5 | HashPasswordFn func(string) string
6 | HashMatchesPasswordFn func(hash, password string) bool
7 | HashRandomPasswordFn func() (string, error)
8 | }
9 |
10 | // HashPassword mock
11 | func (p *Password) HashPassword(password string) string {
12 | return p.HashPasswordFn(password)
13 | }
14 |
15 | // HashMatchesPassword mock
16 | func (p *Password) HashMatchesPassword(hash, password string) bool {
17 | return p.HashMatchesPasswordFn(hash, password)
18 | }
19 |
20 | // HashRandomPassword mock
21 | func (p *Password) HashRandomPassword() (string, error) {
22 | return p.HashRandomPasswordFn()
23 | }
24 |
--------------------------------------------------------------------------------
/model/role.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | func init() {
4 | Register(&Role{})
5 | }
6 |
7 | // AccessRole represents access role type
8 | type AccessRole int8
9 |
10 | const (
11 | // SuperAdminRole has all permissions
12 | SuperAdminRole AccessRole = iota + 1
13 |
14 | // AdminRole has admin specific permissions
15 | AdminRole
16 |
17 | // UserRole is a standard user
18 | UserRole
19 | )
20 |
21 | // Role model
22 | type Role struct {
23 | ID int `json:"id"`
24 | AccessLevel AccessRole `json:"access_level"`
25 | Name string `json:"name"`
26 | }
27 |
28 | // RoleRepo represents the database interface
29 | type RoleRepo interface {
30 | CreateRoles() error
31 | }
32 |
--------------------------------------------------------------------------------
/public/2140998d-7f62-46f2-a9b2-e44350bd4807.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/FB.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/alpaca.go:
--------------------------------------------------------------------------------
1 | package alpaca
2 |
3 | import (
4 | "github.com/alpacahq/ribbit-backend/cmd"
5 | "github.com/alpacahq/ribbit-backend/route"
6 | )
7 |
8 | // New creates a new Alpaca instance
9 | func New() *Alpaca {
10 | return &Alpaca{}
11 | }
12 |
13 | // Alpaca allows us to specify customizations, such as custom route services
14 | type Alpaca struct {
15 | RouteServices []route.ServicesI
16 | }
17 |
18 | // WithRoutes is the builder method for us to add in custom route services
19 | func (g *Alpaca) WithRoutes(RouteServices ...route.ServicesI) *Alpaca {
20 | return &Alpaca{RouteServices}
21 | }
22 |
23 | // Run executes our alpaca functions or servers
24 | func (g *Alpaca) Run() {
25 | cmd.Execute(g.RouteServices)
26 | }
27 |
--------------------------------------------------------------------------------
/public/fc6a5dcd-4a70-4b8d-b64f-d83a6dae9ba4.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/TSLA.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | caddy:
4 | image: "caddy:latest"
5 | volumes:
6 | - ./ribbit.com.pem:/root/certs/ribbit.com.pem
7 | - ./ribbit-public.com.pem:/root/certs/ribbit-public.com.pem
8 | - ./Caddyfile:/etc/caddy/Caddyfile # to mount custom Caddyfile
9 | ports:
10 | - "443:443"
11 | depends_on:
12 | - ribbit
13 |
14 | ribbit:
15 | depends_on:
16 | - database
17 | build: .
18 | entrypoint: ["sh", "-c", "./initdb.sh"]
19 |
20 | database:
21 | image: "postgres:14.0"
22 | ports:
23 | - "5432:5432"
24 | # volumes:
25 | # add local volume mount if needed
26 | # - ./data:/var/lib/postgresql/data/pgdata
27 | environment:
28 | POSTGRES_PASSWORD: password
29 |
--------------------------------------------------------------------------------
/public/8ccae427-5dd0-45b3-b5fe-7ba5e422c766.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/repository/transfer/transfer.go:
--------------------------------------------------------------------------------
1 | package transfer
2 |
3 | import (
4 | "github.com/alpacahq/ribbit-backend/model"
5 | "github.com/go-pg/pg/v9/orm"
6 | "go.uber.org/zap"
7 | )
8 |
9 | // NewAuthService creates new auth service
10 | func NewTransferService(userRepo model.UserRepo, accountRepo model.AccountRepo, jwt JWT, db orm.DB, log *zap.Logger) *Service {
11 | return &Service{userRepo, accountRepo, jwt, db, log}
12 | }
13 |
14 | // Service represents the auth application service
15 | type Service struct {
16 | userRepo model.UserRepo
17 | accountRepo model.AccountRepo
18 | jwt JWT
19 | db orm.DB
20 | log *zap.Logger
21 | }
22 |
23 | // JWT represents jwt interface
24 | type JWT interface {
25 | GenerateToken(*model.User) (string, string, error)
26 | }
27 |
--------------------------------------------------------------------------------
/manager/createdbuser.go:
--------------------------------------------------------------------------------
1 | package manager
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/alpacahq/ribbit-backend/config"
7 |
8 | "github.com/go-pg/pg/v9"
9 | )
10 |
11 | // CreateDatabaseUserIfNotExist creates a database user
12 | func CreateDatabaseUserIfNotExist(db *pg.DB, p *config.PostgresConfig) {
13 | statement := fmt.Sprintf(`SELECT * FROM pg_roles WHERE rolname = '%s';`, p.User)
14 | res, _ := db.Exec(statement)
15 | if res.RowsReturned() == 0 {
16 | statement = fmt.Sprintf(`CREATE USER %s WITH PASSWORD '%s';`, p.User, p.Password)
17 | _, err := db.Exec(statement)
18 | if err != nil {
19 | fmt.Println(err)
20 | } else {
21 | fmt.Printf(`Created user %s`, p.User)
22 | }
23 | } else {
24 | fmt.Printf("Database user %s already exists\n", p.User)
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/cmd/migrate.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | )
6 |
7 | // migrateCmd represents the migrate command
8 | var migrateCmd = &cobra.Command{
9 | Use: "migrate",
10 | Short: "migrate runs schema migration",
11 | Long: `migrate runs schema migration`,
12 | }
13 |
14 | func init() {
15 | rootCmd.AddCommand(migrateCmd)
16 |
17 | // Here you will define your flags and configuration settings.
18 |
19 | // Cobra supports Persistent Flags which will work for this command
20 | // and all subcommands, e.g.:
21 | // migrateCmd.PersistentFlags().String("foo", "", "A help for foo")
22 |
23 | // Cobra supports local flags which will only run when this command
24 | // is called directly, e.g.:
25 | // migrateCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
26 | }
27 |
--------------------------------------------------------------------------------
/model/asset.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | func init() {
4 | Register(&Asset{})
5 | }
6 |
7 | type Asset struct {
8 | Base
9 | ID string `json:"id"`
10 | Class string `json:"class"`
11 | Exchange string `json:"exchange"`
12 | Symbol string `json:"symbol"`
13 | Name string `json:"name"`
14 | Status string `json:"status"`
15 | Tradable bool `json:"tradable"`
16 | Marginable bool `json:"marginable"`
17 | Shortable bool `json:"shortable"`
18 | EasyToBorrow bool `json:"easy_to_borrow"`
19 | Fractionable bool `json:"fractionable"`
20 | IsWatchlisted bool `json:"is_watchlisted"`
21 | }
22 |
23 | type AssetsRepo interface {
24 | CreateOrUpdate(*Asset) (*Asset, error)
25 | UpdateAsset(*Asset) error
26 | Search(string) ([]Asset, error)
27 | }
28 |
--------------------------------------------------------------------------------
/model/user_test.go:
--------------------------------------------------------------------------------
1 | package model_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/alpacahq/ribbit-backend/model"
7 | )
8 |
9 | func TestUpdateLastLogin(t *testing.T) {
10 | user := &model.User{
11 | FirstName: "TestGuy",
12 | }
13 | user.UpdateLastLogin()
14 | if user.LastLogin.IsZero() {
15 | t.Errorf("Last login time was not changed")
16 | }
17 | }
18 |
19 | func TestUpdateUpdatedAt(t *testing.T) {
20 | user := &model.User{
21 | FirstName: "TestGal",
22 | }
23 | user.Update()
24 | if user.UpdatedAt.IsZero() {
25 | t.Errorf("updated_at is not changed")
26 | }
27 | }
28 |
29 | func TestUpdateDeletedAt(t *testing.T) {
30 | user := &model.User{
31 | FirstName: "TestGod",
32 | }
33 | user.Delete()
34 | if user.DeletedAt.IsZero() {
35 | t.Errorf("deleted_at is not changed")
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/config/site.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "path"
6 | "path/filepath"
7 | "runtime"
8 |
9 | "github.com/caarlos0/env/v6"
10 | "github.com/joho/godotenv"
11 | )
12 |
13 | // SiteConfig persists global configs needed for our application
14 | type SiteConfig struct {
15 | ExternalURL string `env:"EXTERNAL_URL" envDefault:"http://localhost:8080"`
16 | }
17 |
18 | // GetSiteConfig returns a SiteConfig pointer with the correct Site Config values
19 | func GetSiteConfig() *SiteConfig {
20 | c := SiteConfig{}
21 |
22 | _, b, _, _ := runtime.Caller(0)
23 | d := path.Join(path.Dir(b))
24 | projectRoot := filepath.Dir(d)
25 | dotenvPath := path.Join(projectRoot, ".env")
26 | _ = godotenv.Load(dotenvPath)
27 |
28 | if err := env.Parse(&c); err != nil {
29 | fmt.Printf("%+v\n", err)
30 | }
31 | return &c
32 | }
33 |
--------------------------------------------------------------------------------
/config/mail.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "path"
6 | "path/filepath"
7 | "runtime"
8 |
9 | "github.com/caarlos0/env/v6"
10 | "github.com/joho/godotenv"
11 | )
12 |
13 | // MailConfig persists the config for our PostgreSQL database connection
14 | type MailConfig struct {
15 | Name string `env:"DEFAULT_NAME"`
16 | Email string `env:"DEFAULT_EMAIL"`
17 | }
18 |
19 | // GetMailConfig returns a MailConfig pointer with the correct Mail Config values
20 | func GetMailConfig() *MailConfig {
21 | c := MailConfig{}
22 |
23 | _, b, _, _ := runtime.Caller(0)
24 | d := path.Join(path.Dir(b))
25 | projectRoot := filepath.Dir(d)
26 | dotenvPath := path.Join(projectRoot, ".env")
27 | _ = godotenv.Load(dotenvPath)
28 |
29 | if err := env.Parse(&c); err != nil {
30 | fmt.Printf("%+v\n", err)
31 | }
32 | return &c
33 | }
34 |
--------------------------------------------------------------------------------
/config/magic.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "path"
6 | "path/filepath"
7 | "runtime"
8 |
9 | "github.com/caarlos0/env/v6"
10 | "github.com/joho/godotenv"
11 | )
12 |
13 | // MagicConfig persists the config for our Magic services
14 | type MagicConfig struct {
15 | Key string `env:"MAGIC_API_KEY"`
16 | Secret string `env:"MAGIC_API_SECRET"`
17 | }
18 |
19 | // GetMagicConfig returns a MagicConfig pointer with the correct Magic.link Config values
20 | func GetMagicConfig() *MagicConfig {
21 | c := MagicConfig{}
22 |
23 | _, b, _, _ := runtime.Caller(0)
24 | d := path.Join(path.Dir(b))
25 | projectRoot := filepath.Dir(d)
26 | dotenvPath := path.Join(projectRoot, ".env")
27 | _ = godotenv.Load(dotenvPath)
28 |
29 | if err := env.Parse(&c); err != nil {
30 | fmt.Printf("%+v\n", err)
31 | }
32 | return &c
33 | }
34 |
--------------------------------------------------------------------------------
/cmd/migrate_up.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "log"
5 | "strconv"
6 |
7 | "github.com/alpacahq/ribbit-backend/migration"
8 |
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | // upCmd represents the up command
13 | var upCmd = &cobra.Command{
14 | Use: "up [target]",
15 | Short: "runs all available migrations or up to the target if provided",
16 | Long: `runs all available migrations or up to the target if provided`,
17 | Run: func(cmd *cobra.Command, args []string) {
18 | var passthrough = []string{"up"}
19 | if len(args) > 0 {
20 | _, err := strconv.Atoi(args[0])
21 | if err != nil {
22 | passthrough = append(passthrough, args[0])
23 | }
24 | }
25 |
26 | err := migration.Run(passthrough...)
27 | if err != nil {
28 | log.Fatal(err)
29 | }
30 | },
31 | }
32 |
33 | func init() {
34 | migrateCmd.AddCommand(upCmd)
35 | }
36 |
--------------------------------------------------------------------------------
/manager/createdb.go:
--------------------------------------------------------------------------------
1 | package manager
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/alpacahq/ribbit-backend/config"
7 |
8 | "github.com/go-pg/pg/v9"
9 | )
10 |
11 | // CreateDatabaseIfNotExist creates our postgresql database from postgres config
12 | func CreateDatabaseIfNotExist(db *pg.DB, p *config.PostgresConfig) {
13 | statement := fmt.Sprintf(`SELECT 1 AS result FROM pg_database WHERE datname = '%s';`, p.Database)
14 | res, _ := db.Exec(statement)
15 | if res.RowsReturned() == 0 {
16 | fmt.Println("creating database")
17 | statement = fmt.Sprintf(`CREATE DATABASE %s WITH OWNER %s;`, p.Database, p.User)
18 | _, err := db.Exec(statement)
19 | if err != nil {
20 | fmt.Println(err)
21 | } else {
22 | fmt.Printf(`Created database %s`, p.Database)
23 | }
24 | } else {
25 | fmt.Printf("Database named %s already exists\n", p.Database)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/cmd/migrate_set_version.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "log"
5 | "strconv"
6 |
7 | "github.com/alpacahq/ribbit-backend/migration"
8 |
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | // setVersionCmd represents the version command
13 | var setVersionCmd = &cobra.Command{
14 | Use: "set_version [version]",
15 | Short: "sets db version without running migrations",
16 | Long: `sets db version without running migrations`,
17 | Run: func(cmd *cobra.Command, args []string) {
18 | var passthrough = []string{"set_version"}
19 | if len(args) > 0 {
20 | _, err := strconv.Atoi(args[0])
21 | if err != nil {
22 | passthrough = append(passthrough, args[0])
23 | }
24 | }
25 |
26 | err := migration.Run(passthrough...)
27 | if err != nil {
28 | log.Fatal(err)
29 | }
30 | },
31 | }
32 |
33 | func init() {
34 | migrateCmd.AddCommand(setVersionCmd)
35 | }
36 |
--------------------------------------------------------------------------------
/repository/role.go:
--------------------------------------------------------------------------------
1 | package repository
2 |
3 | import (
4 | "github.com/alpacahq/ribbit-backend/model"
5 |
6 | "github.com/go-pg/pg/v9"
7 | "go.uber.org/zap"
8 | )
9 |
10 | // NewRoleRepo returns a Role Repo instance
11 | func NewRoleRepo(db *pg.DB, log *zap.Logger) *RoleRepo {
12 | return &RoleRepo{db, log}
13 | }
14 |
15 | // RoleRepo represents the client for the role table
16 | type RoleRepo struct {
17 | db *pg.DB
18 | log *zap.Logger
19 | }
20 |
21 | // CreateRoles creates role objects in our database
22 | func (r *RoleRepo) CreateRoles() error {
23 | role := new(model.Role)
24 | sql := `INSERT INTO roles (id, access_level, name) VALUES (?, ?, ?) ON CONFLICT DO NOTHING`
25 | r.db.Query(role, sql, 1, model.SuperAdminRole, "superadmin")
26 | r.db.Query(role, sql, 2, model.AdminRole, "admin")
27 | r.db.Query(role, sql, 3, model.UserRole, "user")
28 | return nil
29 | }
30 |
--------------------------------------------------------------------------------
/public/AMZN.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/f801f835-bfe6-4a9d-a6b1-ccbb84bfd75f.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config/twilio.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "path"
6 | "path/filepath"
7 | "runtime"
8 |
9 | "github.com/caarlos0/env/v6"
10 | "github.com/joho/godotenv"
11 | )
12 |
13 | // TwilioConfig persists the config for our Twilio services
14 | type TwilioConfig struct {
15 | Account string `env:"TWILIO_ACCOUNT"`
16 | Token string `env:"TWILIO_TOKEN"`
17 | VerifyName string `env:"TWILIO_VERIFY_NAME"`
18 | Verify string `env:"TWILIO_VERIFY"`
19 | }
20 |
21 | // GetTwilioConfig returns a TwilioConfig pointer with the correct Mail Config values
22 | func GetTwilioConfig() *TwilioConfig {
23 | c := TwilioConfig{}
24 |
25 | _, b, _, _ := runtime.Caller(0)
26 | d := path.Join(path.Dir(b))
27 | projectRoot := filepath.Dir(d)
28 | dotenvPath := path.Join(projectRoot, ".env")
29 | _ = godotenv.Load(dotenvPath)
30 |
31 | if err := env.Parse(&c); err != nil {
32 | fmt.Printf("%+v\n", err)
33 | }
34 | return &c
35 | }
36 |
--------------------------------------------------------------------------------
/public/GOOG.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/GOOGL.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/V.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/69b15845-7c63-4586-b274-1cfdfe9df3d8.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/AAPL.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/f30d734c-2806-4d0d-b145-f9fade61432b.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/cmd/create_db.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/alpacahq/ribbit-backend/config"
7 | "github.com/alpacahq/ribbit-backend/manager"
8 |
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | // createCmd represents the migrate command
13 | var createdbCmd = &cobra.Command{
14 | Use: "create_db",
15 | Short: "create_db creates a database user and database from database parameters declared in config",
16 | Long: `create_db creates a database user and database from database parameters declared in config`,
17 | Run: func(cmd *cobra.Command, args []string) {
18 | fmt.Println("create_db called")
19 | p := config.GetPostgresConfig()
20 |
21 | // connection to db as postgres superuser
22 | dbSuper := config.GetPostgresSuperUserConnection()
23 | defer dbSuper.Close()
24 |
25 | manager.CreateDatabaseUserIfNotExist(dbSuper, p)
26 | manager.CreateDatabaseIfNotExist(dbSuper, p)
27 | },
28 | }
29 |
30 | func init() {
31 | rootCmd.AddCommand(createdbCmd)
32 | }
33 |
--------------------------------------------------------------------------------
/mock/rbac.go:
--------------------------------------------------------------------------------
1 | package mock
2 |
3 | import (
4 | "github.com/alpacahq/ribbit-backend/model"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | // RBAC Mock
10 | type RBAC struct {
11 | EnforceRoleFn func(*gin.Context, model.AccessRole) bool
12 | EnforceUserFn func(*gin.Context, int) bool
13 | AccountCreateFn func(*gin.Context, int) bool
14 | IsLowerRoleFn func(*gin.Context, model.AccessRole) bool
15 | }
16 |
17 | // EnforceRole mock
18 | func (a *RBAC) EnforceRole(c *gin.Context, role model.AccessRole) bool {
19 | return a.EnforceRoleFn(c, role)
20 | }
21 |
22 | // EnforceUser mock
23 | func (a *RBAC) EnforceUser(c *gin.Context, id int) bool {
24 | return a.EnforceUserFn(c, id)
25 | }
26 |
27 | // AccountCreate mock
28 | func (a *RBAC) AccountCreate(c *gin.Context, roleID int) bool {
29 | return a.AccountCreateFn(c, roleID)
30 | }
31 |
32 | // IsLowerRole mock
33 | func (a *RBAC) IsLowerRole(c *gin.Context, role model.AccessRole) bool {
34 | return a.IsLowerRoleFn(c, role)
35 | }
36 |
--------------------------------------------------------------------------------
/model/model_test.go:
--------------------------------------------------------------------------------
1 | package model_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/alpacahq/ribbit-backend/mock"
7 | "github.com/alpacahq/ribbit-backend/model"
8 | )
9 |
10 | func TestBeforeInsert(t *testing.T) {
11 | base := &model.Base{}
12 | base.BeforeInsert(nil)
13 | if base.CreatedAt.IsZero() {
14 | t.Errorf("CreatedAt was not changed")
15 | }
16 | if base.UpdatedAt.IsZero() {
17 | t.Errorf("UpdatedAt was not changed")
18 | }
19 | }
20 |
21 | func TestBeforeUpdate(t *testing.T) {
22 | base := &model.Base{
23 | CreatedAt: mock.TestTime(2000),
24 | }
25 | base.BeforeUpdate(nil)
26 | if base.UpdatedAt == mock.TestTime(2001) {
27 | t.Errorf("UpdatedAt was not changed")
28 | }
29 |
30 | }
31 |
32 | func TestDelete(t *testing.T) {
33 | baseModel := &model.Base{
34 | CreatedAt: mock.TestTime(2000),
35 | UpdatedAt: mock.TestTime(2001),
36 | }
37 | baseModel.Delete()
38 | if baseModel.DeletedAt.IsZero() {
39 | t.Errorf("DeletedAt not changed")
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/public/4f5baf1e-0e9b-4d85-b88a-d874dc4a3c42.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/b0b6dd9d-8b9b-48a9-ba46-b9d54906e415.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/mockgopg/row_result.go:
--------------------------------------------------------------------------------
1 | package mockgopg
2 |
3 | import "github.com/go-pg/pg/v9/orm"
4 |
5 | // OrmResult struct to implements orm.Result
6 | type OrmResult struct {
7 | rowsAffected int
8 | rowsReturned int
9 | model interface{}
10 | }
11 |
12 | // Model implements an orm.Model
13 | func (o *OrmResult) Model() orm.Model {
14 | if o.model == nil {
15 | return nil
16 | }
17 |
18 | model, err := orm.NewModel(o.model)
19 | if err != nil {
20 | return nil
21 | }
22 |
23 | return model
24 | }
25 |
26 | // RowsAffected returns the number of rows affected in the data table
27 | func (o *OrmResult) RowsAffected() int {
28 | return o.rowsAffected
29 | }
30 |
31 | // RowsReturned returns the number of rows
32 | func (o *OrmResult) RowsReturned() int {
33 | return o.rowsReturned
34 | }
35 |
36 | // NewResult implements orm.Result in go-pg package
37 | func NewResult(rowAffected, rowReturned int, model interface{}) *OrmResult {
38 | return &OrmResult{
39 | rowsAffected: rowAffected,
40 | rowsReturned: rowReturned,
41 | model: model,
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/secret/secret.go:
--------------------------------------------------------------------------------
1 | package secret
2 |
3 | import (
4 | "golang.org/x/crypto/bcrypt"
5 | )
6 |
7 | // New returns a password object
8 | func New() *Password {
9 | return &Password{}
10 | }
11 |
12 | // Password is our secret service implementation
13 | type Password struct{}
14 |
15 | // HashPassword hashes the password using bcrypt
16 | func (p *Password) HashPassword(password string) string {
17 | hashedPW, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
18 | return string(hashedPW)
19 | }
20 |
21 | // HashMatchesPassword matches hash with password. Returns true if hash and password match.
22 | func (p *Password) HashMatchesPassword(hash, password string) bool {
23 | return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
24 | }
25 |
26 | // HashRandomPassword creates a random password for passwordless mobile signup
27 | func (p *Password) HashRandomPassword() (string, error) {
28 | randomPassword, err := GenerateRandomString(16)
29 | if err != nil {
30 | return "", err
31 | }
32 | r := p.HashPassword(randomPassword)
33 | return r, nil
34 | }
35 |
--------------------------------------------------------------------------------
/mock/mail.go:
--------------------------------------------------------------------------------
1 | package mock
2 |
3 | import "github.com/alpacahq/ribbit-backend/model"
4 |
5 | // Mail mock
6 | type Mail struct {
7 | ExternalURL string
8 | SendFn func(string, string, string, string, string) error
9 | SendWithDefaultsFn func(string, string, string, string) error
10 | SendVerificationEmailFn func(string, *model.Verification) error
11 | SendForgotVerificationEmailFn func(string, *model.Verification) error
12 | }
13 |
14 | // Send mock
15 | func (m *Mail) Send(subject, toName, toEmail, content, html string) error {
16 | return m.SendFn(subject, toName, toEmail, content, html)
17 | }
18 |
19 | // SendWithDefaults mock
20 | func (m *Mail) SendWithDefaults(subject, toEmail, content, html string) error {
21 | return m.SendWithDefaultsFn(subject, toEmail, content, html)
22 | }
23 |
24 | // SendVerificationEmail mock
25 | func (m *Mail) SendVerificationEmail(toEmail string, v *model.Verification) error {
26 | return m.SendVerificationEmailFn(toEmail, v)
27 | }
28 |
29 | func (m *Mail) SendForgotVerificationEmail(toEmail string, v *model.Verification) error {
30 | return m.SendForgotVerificationEmailFn(toEmail, v)
31 | }
32 |
--------------------------------------------------------------------------------
/public/assets/img/forgot_password.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/cmd/create_schema.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/alpacahq/ribbit-backend/config"
7 | "github.com/alpacahq/ribbit-backend/manager"
8 | "github.com/alpacahq/ribbit-backend/repository"
9 | "github.com/alpacahq/ribbit-backend/secret"
10 |
11 | "github.com/spf13/cobra"
12 | "go.uber.org/zap"
13 | )
14 |
15 | // createschemaCmd represents the createschema command
16 | var createSchemaCmd = &cobra.Command{
17 | Use: "create_schema",
18 | Short: "create_schema creates the initial database schema for the existing database",
19 | Long: `create_schema creates the initial database schema for the existing database`,
20 | Run: func(cmd *cobra.Command, args []string) {
21 | fmt.Println("createschema called")
22 |
23 | db := config.GetConnection()
24 | log, _ := zap.NewDevelopment()
25 | defer log.Sync()
26 | accountRepo := repository.NewAccountRepo(db, log, secret.New())
27 | roleRepo := repository.NewRoleRepo(db, log)
28 |
29 | m := manager.NewManager(accountRepo, roleRepo, db)
30 | models := manager.GetModels()
31 | m.CreateSchema(models...)
32 | m.CreateRoles()
33 | },
34 | }
35 |
36 | func init() {
37 | rootCmd.AddCommand(createSchemaCmd)
38 | }
39 |
--------------------------------------------------------------------------------
/public/NFLX.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/model/auth.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | )
6 |
7 | // AuthToken holds authentication token details with refresh token
8 | type AuthToken struct {
9 | Token string `json:"token"`
10 | Expires string `json:"expires"`
11 | RefreshToken string `json:"refresh_token"`
12 | }
13 |
14 | // LoginResponseWithToken holds authentication token details with refresh token
15 | type LoginResponseWithToken struct {
16 | Token string `json:"token"`
17 | Expires string `json:"expires"`
18 | RefreshToken string `json:"refresh_token"`
19 | User User `json:"user"`
20 | }
21 |
22 | // RefreshToken holds authentication token details
23 | type RefreshToken struct {
24 | Token string `json:"token"`
25 | Expires string `json:"expires"`
26 | }
27 |
28 | // AuthService represents authentication service interface
29 | type AuthService interface {
30 | User(*gin.Context) *AuthUser
31 | }
32 |
33 | // RBACService represents role-based access control service interface
34 | type RBACService interface {
35 | EnforceRole(*gin.Context, AccessRole) bool
36 | EnforceUser(*gin.Context, int) bool
37 | AccountCreate(*gin.Context, int) bool
38 | IsLowerRole(*gin.Context, AccessRole) bool
39 | }
40 |
--------------------------------------------------------------------------------
/request/request.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | import (
4 | "net/http"
5 | "strconv"
6 |
7 | "github.com/alpacahq/ribbit-backend/apperr"
8 |
9 | "github.com/gin-gonic/gin"
10 | )
11 |
12 | const (
13 | defaultLimit = 100
14 | maxLimit = 1000
15 | )
16 |
17 | // Pagination contains pagination request
18 | type Pagination struct {
19 | Limit int `form:"limit"`
20 | Page int `form:"page" binding:"min=0"`
21 | Offset int `json:"-"`
22 | }
23 |
24 | // Paginate validates pagination requests
25 | func Paginate(c *gin.Context) (*Pagination, error) {
26 | p := new(Pagination)
27 | if err := c.ShouldBindQuery(p); err != nil {
28 | apperr.Response(c, err)
29 | return nil, err
30 | }
31 | if p.Limit < 1 {
32 | p.Limit = defaultLimit
33 | }
34 | if p.Limit > 1000 {
35 | p.Limit = maxLimit
36 | }
37 | p.Offset = p.Limit * p.Page
38 | return p, nil
39 | }
40 |
41 | // ID returns id url parameter.
42 | // In case of conversion error to int, request will be aborted with StatusBadRequest.
43 | func ID(c *gin.Context) (int, error) {
44 | id, err := strconv.Atoi(c.Param("id"))
45 | if err != nil {
46 | c.AbortWithStatus(http.StatusBadRequest)
47 | return 0, apperr.New(http.StatusBadRequest, "Bad request")
48 | }
49 | return id, nil
50 | }
51 |
--------------------------------------------------------------------------------
/testhelper/testhelper.go:
--------------------------------------------------------------------------------
1 | package testhelper
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | )
7 |
8 | // GetFreePort asks the kernel for a free open port that is ready to use.
9 | func GetFreePort(host string, preferredPort uint32) (int, error) {
10 | address := host + ":" + fmt.Sprint(preferredPort)
11 | addr, err := net.ResolveTCPAddr("tcp", address)
12 | if err != nil {
13 | return 0, err
14 | }
15 |
16 | l, err := net.ListenTCP("tcp", addr)
17 | if err != nil {
18 | return 0, err
19 | }
20 | defer l.Close()
21 | return l.Addr().(*net.TCPAddr).Port, nil
22 | }
23 |
24 | // AllocatePort returns a port that is available, given host and a preferred port
25 | // if none of the preferred ports are available, it will keep searching by adding 1 to the port number
26 | func AllocatePort(host string, preferredPort uint32) uint32 {
27 | preferredPortStr := fmt.Sprint(preferredPort)
28 | allocatedPort, err := GetFreePort(host, preferredPort)
29 | for err != nil {
30 | preferredPort = preferredPort + 1
31 | allocatedPort, err = GetFreePort(host, preferredPort)
32 | if err != nil {
33 | fmt.Println("Failed to connect to", preferredPortStr)
34 | }
35 | }
36 | fmt.Println("Allocated port", allocatedPort)
37 | return uint32(allocatedPort)
38 | }
39 |
--------------------------------------------------------------------------------
/public/bb2a26c0-4c77-4801-8afc-82e8142ac7b8.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/img/verify_email.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "log"
5 | "path/filepath"
6 | "runtime"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/spf13/viper"
10 | )
11 |
12 | // Load returns Configuration struct
13 | func Load(env string) *Configuration {
14 | _, filePath, _, _ := runtime.Caller(0)
15 | configName := "config." + env + ".yaml"
16 | configPath := filePath[:len(filePath)-9] + "files" + string(filepath.Separator)
17 |
18 | viper.SetConfigName(configName)
19 | viper.AddConfigPath(configPath)
20 | viper.SetConfigType("yaml")
21 |
22 | err := viper.ReadInConfig()
23 | if err != nil {
24 | log.Fatal(err)
25 | }
26 |
27 | var config Configuration
28 | viper.Unmarshal(&config)
29 | setGinMode(config.Server.Mode)
30 |
31 | return &config
32 | }
33 |
34 | // Configuration holds data necessery for configuring application
35 | type Configuration struct {
36 | Server *Server `yaml:"server"`
37 | }
38 |
39 | // Server holds data necessary for server configuration
40 | type Server struct {
41 | Mode string `yaml:"mode"`
42 | }
43 |
44 | func setGinMode(mode string) {
45 | switch mode {
46 | case "release":
47 | gin.SetMode(gin.ReleaseMode)
48 | break
49 | case "test":
50 | gin.SetMode(gin.TestMode)
51 | break
52 | default:
53 | gin.SetMode(gin.DebugMode)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/repository/platform/structs/structs.go:
--------------------------------------------------------------------------------
1 | package structs
2 |
3 | import "reflect"
4 |
5 | // Merge receives two structs, and merges them excluding fields with tag name: `structs`, value "-"
6 | func Merge(dst, src interface{}) {
7 | s := reflect.ValueOf(src)
8 | d := reflect.ValueOf(dst)
9 | if s.Kind() != reflect.Ptr || d.Kind() != reflect.Ptr {
10 | return
11 | }
12 | for i := 0; i < s.Elem().NumField(); i++ {
13 | v := s.Elem().Field(i)
14 | fieldName := s.Elem().Type().Field(i).Name
15 | skip := s.Elem().Type().Field(i).Tag.Get("structs")
16 | if skip == "-" {
17 | continue
18 | }
19 | if v.Kind() > reflect.Float64 &&
20 | v.Kind() != reflect.String &&
21 | v.Kind() != reflect.Struct &&
22 | v.Kind() != reflect.Ptr &&
23 | v.Kind() != reflect.Slice {
24 | continue
25 | }
26 | if v.Kind() == reflect.Ptr {
27 | // Field is pointer check if it's nil or set
28 | if !v.IsNil() {
29 | // Field is set assign it to dest
30 |
31 | if d.Elem().FieldByName(fieldName).Kind() == reflect.Ptr {
32 | d.Elem().FieldByName(fieldName).Set(v)
33 | continue
34 | }
35 | f := d.Elem().FieldByName(fieldName)
36 | if f.IsValid() {
37 | f.Set(v.Elem())
38 | }
39 | }
40 | continue
41 | }
42 | d.Elem().FieldByName(fieldName).Set(v)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/repository/assets/assets.go:
--------------------------------------------------------------------------------
1 | package assets
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/alpacahq/ribbit-backend/model"
7 | "github.com/go-pg/pg/v9/orm"
8 | "go.uber.org/zap"
9 |
10 | "github.com/joho/godotenv"
11 | )
12 |
13 | func init() {
14 | err := godotenv.Load()
15 |
16 | if err != nil {
17 | panic(fmt.Errorf("unexpected error while initializing plaid client %w", err))
18 | }
19 | }
20 |
21 | // NewAuthService creates new auth service
22 | func NewAssetsService(userRepo model.UserRepo, accountRepo model.AccountRepo, assetRepo model.AssetsRepo, jwt JWT, db orm.DB, log *zap.Logger) *Service {
23 | return &Service{userRepo, assetRepo, accountRepo, jwt, db, log}
24 | }
25 |
26 | // Service represents the auth application service
27 | type Service struct {
28 | userRepo model.UserRepo
29 | assetRepo model.AssetsRepo
30 | accountRepo model.AccountRepo
31 | jwt JWT
32 | db orm.DB
33 | log *zap.Logger
34 | }
35 |
36 | // JWT represents jwt interface
37 | type JWT interface {
38 | GenerateToken(*model.User) (string, string, error)
39 | }
40 |
41 | // SearchAssets changes user's avatar
42 | func (a *Service) SearchAssets(query string) ([]model.Asset, error) {
43 | assets, err := a.assetRepo.Search(query)
44 | if err != nil {
45 | return nil, err
46 | }
47 | return assets, nil
48 | }
49 |
--------------------------------------------------------------------------------
/templates/terms_conditions_old.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Page Title
5 |
6 |
7 |
8 |
9 |
10 |
11 |
20 |
21 |
22 | This following sets out the terms and conditions on which you may use the content on business-standard.com website, business-standard.com's mobile browser site, Business Standard instore Applications and other digital publishing services (www.smartinvestor.in, www.bshindi.com and www.bsmotoring,com) owned by Business Standard Private Limited, all the services herein will be referred to as Business Standard Content Services)
23 |
24 | Registration Access and Use We welcome users to register on our digital platforms. We offer the below mentioned registration services which may be subject to change in the future. All changes will be appended in the terms and conditions page and communicated to existing users by email.
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/public/assets/img/failed_approval.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/alpacahq/ribbit-backend
2 |
3 | go 1.13
4 |
5 | require (
6 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
7 | github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013
8 | github.com/caarlos0/env/v6 v6.5.0
9 | github.com/dgrijalva/jwt-go v3.2.0+incompatible
10 | github.com/fergusstrange/embedded-postgres v1.4.0
11 | github.com/gertd/go-pluralize v0.1.7
12 | github.com/gin-contrib/cors v1.3.1
13 | github.com/gin-gonic/gin v1.7.0
14 | github.com/go-pg/migrations/v7 v7.1.11
15 | github.com/go-pg/pg/v9 v9.2.0
16 | github.com/joho/godotenv v1.3.0
17 | github.com/lithammer/shortuuid/v3 v3.0.6
18 | github.com/magiclabs/magic-admin-go v0.1.0
19 | github.com/mcuadros/go-defaults v1.2.0
20 | github.com/mitchellh/go-homedir v1.1.0
21 | github.com/plaid/plaid-go v0.0.0-20210216195344-700b8cfc627d
22 | github.com/rs/xid v1.2.1
23 | github.com/satori/go.uuid v1.2.0
24 | github.com/sendgrid/rest v2.4.1+incompatible // indirect
25 | github.com/sendgrid/sendgrid-go v3.8.0+incompatible
26 | github.com/spf13/cobra v1.1.3
27 | github.com/spf13/viper v1.7.1
28 | github.com/stretchr/testify v1.7.0
29 | github.com/swaggo/gin-swagger v1.3.0
30 | github.com/swaggo/swag v1.7.0
31 | go.uber.org/zap v1.16.0
32 | go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
33 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
34 | gopkg.in/go-playground/validator.v8 v8.18.2
35 | )
36 |
--------------------------------------------------------------------------------
/model/model.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "context"
5 | "time"
6 | )
7 |
8 | // Models hold registered models in-memory
9 | var Models []interface{}
10 |
11 | // Base contains common fields for all tables
12 | type Base struct {
13 | CreatedAt time.Time `json:"created_at"`
14 | UpdatedAt time.Time `json:"updated_at"`
15 | DeletedAt *time.Time `json:"deleted_at,omitempty"`
16 | }
17 |
18 | // Pagination holds pagination's data
19 | type Pagination struct {
20 | Limit int
21 | Offset int
22 | }
23 |
24 | // ListQuery holds company/location data used for list db queries
25 | type ListQuery struct {
26 | Query string
27 | ID int
28 | }
29 |
30 | // BeforeInsert hooks into insert operations, setting createdAt and updatedAt to current time
31 | func (b *Base) BeforeInsert(ctx context.Context) (context.Context, error) {
32 | now := time.Now()
33 | if b.CreatedAt.IsZero() {
34 | b.CreatedAt = now
35 | }
36 | if b.UpdatedAt.IsZero() {
37 | b.UpdatedAt = now
38 | }
39 | return ctx, nil
40 | }
41 |
42 | // BeforeUpdate hooks into update operations, setting updatedAt to current time
43 | func (b *Base) BeforeUpdate(ctx context.Context) (context.Context, error) {
44 | b.UpdatedAt = time.Now()
45 | return ctx, nil
46 | }
47 |
48 | // Delete sets deleted_at time to current_time
49 | func (b *Base) Delete() {
50 | t := time.Now()
51 | b.DeletedAt = &t
52 | }
53 |
54 | // Register is used for registering models
55 | func Register(m interface{}) {
56 | Models = append(Models, m)
57 | }
58 |
--------------------------------------------------------------------------------
/public/assets/img/welcome_page.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/public/assets/img/submitted_application.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/mock/mock.go:
--------------------------------------------------------------------------------
1 | package mock
2 |
3 | import (
4 | "net/http/httptest"
5 | "time"
6 |
7 | "github.com/gin-gonic/gin"
8 | )
9 |
10 | // TestTime is used for testing time fields
11 | func TestTime(year int) time.Time {
12 | return time.Date(year, time.May, 19, 1, 2, 3, 4, time.UTC)
13 | }
14 |
15 | // TestTimePtr is used for testing pointer time fields
16 | func TestTimePtr(year int) *time.Time {
17 | t := time.Date(year, time.May, 19, 1, 2, 3, 4, time.UTC)
18 | return &t
19 | }
20 |
21 | // Str2Ptr converts string to pointer
22 | func Str2Ptr(s string) *string {
23 | return &s
24 | }
25 |
26 | // GinCtxWithKeys returns new gin context with keys
27 | func GinCtxWithKeys(keys []string, values ...interface{}) *gin.Context {
28 | w := httptest.NewRecorder()
29 | gin.SetMode(gin.TestMode)
30 | c, _ := gin.CreateTestContext(w)
31 | for i, k := range keys {
32 | c.Set(k, values[i])
33 | }
34 | return c
35 | }
36 |
37 | // HeaderValid is used for jwt testing
38 | func HeaderValid() string {
39 | return "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidSI6ImpvaG5kb2UiLCJlIjoiam9obmRvZUBtYWlsLmNvbSIsInIiOjEsImMiOjEsImwiOjEsImV4cCI6NDEwOTMyMDg5NCwiaWF0IjoxNTE2MjM5MDIyfQ.8Fa8mhshx3tiQVzS5FoUXte5lHHC4cvaa_tzvcel38I"
40 | }
41 |
42 | // HeaderInvalid is used for jwt testing
43 | func HeaderInvalid() string {
44 | return "Bearer eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidSI6ImpvaG5kb2UiLCJlIjoiam9obmRvZUBtYWlsLmNvbSIsInIiOjEsImMiOjEsImwiOjEsImV4cCI6NDEwOTMyMDg5NCwiaWF0IjoxNTE2MjM5MDIyfQ.7uPfVeZBkkyhICZSEINZfPo7ZsaY0NNeg0ebEGHuAvNjFvoKNn8dWYTKaZrqE1X4"
45 | }
46 |
--------------------------------------------------------------------------------
/e2e/mobile_i_test.go:
--------------------------------------------------------------------------------
1 | package e2e_test
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "log"
8 | "net/http"
9 | "net/http/httptest"
10 |
11 | "github.com/alpacahq/ribbit-backend/request"
12 |
13 | "github.com/stretchr/testify/assert"
14 | )
15 |
16 | func (suite *E2ETestSuite) TestSignupMobile() {
17 | t := suite.T()
18 | ts := httptest.NewServer(suite.r)
19 | defer ts.Close()
20 |
21 | urlSignupMobile := ts.URL + "/mobile"
22 |
23 | req := &request.MobileSignup{
24 | CountryCode: "+65",
25 | Mobile: "91919191",
26 | }
27 | b, err := json.Marshal(req)
28 | if err != nil {
29 | log.Fatal(err)
30 | }
31 | resp, err := http.Post(urlSignupMobile, "application/json", bytes.NewBuffer(b))
32 | if err != nil {
33 | log.Fatal(err)
34 | }
35 | defer resp.Body.Close()
36 |
37 | assert.Equal(t, http.StatusCreated, resp.StatusCode)
38 | assert.Nil(t, err)
39 |
40 | // the sms code will be separately sms-ed to user's mobile phone, trigger above
41 | // we now test against the /mobile/verify
42 |
43 | url := ts.URL + "/mobile/verify"
44 | req2 := &request.MobileVerify{
45 | CountryCode: "+65",
46 | Mobile: "91919191",
47 | Code: "123456",
48 | Signup: true,
49 | }
50 | b, err = json.Marshal(req2)
51 | if err != nil {
52 | log.Fatal(err)
53 | }
54 | resp, err = http.Post(url, "application/json", bytes.NewBuffer(b))
55 | if err != nil {
56 | log.Fatal(err)
57 | }
58 | defer resp.Body.Close()
59 |
60 | fmt.Println("Verify Code")
61 | assert.Equal(t, http.StatusOK, resp.StatusCode)
62 | assert.Nil(t, err)
63 | }
64 |
--------------------------------------------------------------------------------
/request/bank_account.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | import (
4 | "net/http"
5 | "strconv"
6 |
7 | "github.com/alpacahq/ribbit-backend/apperr"
8 |
9 | "github.com/gin-gonic/gin"
10 | )
11 |
12 | // Credentials stores the username and password provided in the request
13 | type SetAccessToken struct {
14 | PublicToken string `json:"public_token" binding:"required"`
15 | AccountID string `json:"account_id" binding:"required"`
16 | }
17 |
18 | // Login parses out the username and password in gin's request context, into Credentials
19 | func SetAccessTokenbody(c *gin.Context) (*SetAccessToken, error) {
20 | data := new(SetAccessToken)
21 | if err := c.ShouldBindJSON(data); err != nil {
22 | apperr.Response(c, err)
23 | return nil, err
24 | }
25 | return data, nil
26 | }
27 |
28 | // Credentials stores the username and password provided in the request
29 | type Charge struct {
30 | Amount string `json:"amount" binding:"required"`
31 | AccountID string `json:"account_id" binding:"required"`
32 | }
33 |
34 | // Login parses out the username and password in gin's request context, into Credentials
35 | func ChargeBody(c *gin.Context) (*Charge, error) {
36 | data := new(Charge)
37 | if err := c.ShouldBindJSON(data); err != nil {
38 | apperr.Response(c, err)
39 | return nil, err
40 | }
41 | return data, nil
42 | }
43 |
44 | func AccountID(c *gin.Context) (int, error) {
45 | id, err := strconv.Atoi(c.Param("account_id"))
46 | if err != nil {
47 | c.AbortWithStatus(http.StatusBadRequest)
48 | return 0, apperr.New(http.StatusBadRequest, "Account ID isn't valid")
49 | }
50 | return id, nil
51 | }
52 |
--------------------------------------------------------------------------------
/repository/rbac.go:
--------------------------------------------------------------------------------
1 | package repository
2 |
3 | import (
4 | "github.com/alpacahq/ribbit-backend/model"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | // NewRBACService creates new RBAC service
10 | func NewRBACService(userRepo model.UserRepo) *RBACService {
11 | return &RBACService{
12 | userRepo: userRepo,
13 | }
14 | }
15 |
16 | // RBACService is RBAC application service
17 | type RBACService struct {
18 | userRepo model.UserRepo
19 | }
20 |
21 | // EnforceRole authorizes request by AccessRole
22 | func (s *RBACService) EnforceRole(c *gin.Context, r model.AccessRole) bool {
23 | return !(c.MustGet("role").(int8) > int8(r))
24 | }
25 |
26 | // EnforceUser checks whether the request to change user data is done by the same user
27 | func (s *RBACService) EnforceUser(c *gin.Context, ID int) bool {
28 | // TODO: Implement querying db and checking the requested user's company_id/location_id
29 | // to allow company/location admins to view the user
30 | return (c.GetInt("id") == ID) || s.isAdmin(c)
31 | }
32 |
33 | func (s *RBACService) isAdmin(c *gin.Context) bool {
34 | return !(c.MustGet("role").(int8) > int8(model.AdminRole))
35 | }
36 |
37 | // AccountCreate performs auth check when creating a new account
38 | // Location admin cannot create accounts, needs to be fixed on EnforceLocation function
39 | func (s *RBACService) AccountCreate(c *gin.Context, roleID int) bool {
40 | roleCheck := s.EnforceRole(c, model.AccessRole(roleID))
41 | return roleCheck && s.IsLowerRole(c, model.AccessRole(roleID))
42 | }
43 |
44 | // IsLowerRole checks whether the requesting user has higher role than the user it wants to change
45 | // Used for account creation/deletion
46 | func (s *RBACService) IsLowerRole(c *gin.Context, r model.AccessRole) bool {
47 | return !(c.MustGet("role").(int8) >= int8(r))
48 | }
49 |
--------------------------------------------------------------------------------
/secret/cryptorandom.go:
--------------------------------------------------------------------------------
1 | package secret
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/base64"
6 | )
7 |
8 | // GenerateRandomBytes returns securely generated random bytes.
9 | // It will return an error if the system's secure random
10 | // number generator fails to function correctly, in which
11 | // case the caller should not continue.
12 | func GenerateRandomBytes(n int) ([]byte, error) {
13 | b := make([]byte, n)
14 | _, err := rand.Read(b)
15 | // Note that err == nil only if we read len(b) bytes.
16 | if err != nil {
17 | return nil, err
18 | }
19 |
20 | return b, nil
21 | }
22 |
23 | // GenerateRandomString returns a securely generated random string.
24 | // It will return an error if the system's secure random
25 | // number generator fails to function correctly, in which
26 | // case the caller should not continue.
27 | // Example: this will give us a 32 byte output
28 | // token, err = GenerateRandomString(32)
29 | func GenerateRandomString(n int) (string, error) {
30 | const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
31 | bytes, err := GenerateRandomBytes(n)
32 | if err != nil {
33 | return "", err
34 | }
35 | for i, b := range bytes {
36 | bytes[i] = letters[b%byte(len(letters))]
37 | }
38 | return string(bytes), nil
39 | }
40 |
41 | // GenerateRandomStringURLSafe returns a URL-safe, base64 encoded
42 | // securely generated random string.
43 | // It will return an error if the system's secure random
44 | // number generator fails to function correctly, in which
45 | // case the caller should not continue.
46 | // Example: this will give us a 44 byte, base64 encoded output
47 | // token, err := GenerateRandomStringURLSafe(32)
48 | func GenerateRandomStringURLSafe(n int) (string, error) {
49 | b, err := GenerateRandomBytes(n)
50 | return base64.URLEncoding.EncodeToString(b), err
51 | }
52 |
--------------------------------------------------------------------------------
/config/jwt.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "log"
7 | "path"
8 | "path/filepath"
9 | "runtime"
10 | "strings"
11 |
12 | "github.com/alpacahq/ribbit-backend/secret"
13 |
14 | "github.com/joho/godotenv"
15 | "github.com/mcuadros/go-defaults"
16 | "github.com/spf13/viper"
17 | )
18 |
19 | // LoadJWT returns our JWT with env variables and relevant defaults
20 | func LoadJWT(env string) *JWT {
21 | jwt := new(JWT)
22 | defaults.SetDefaults(jwt)
23 |
24 | _, b, _, _ := runtime.Caller(0)
25 | d := path.Join(path.Dir(b))
26 | projectRoot := filepath.Dir(d)
27 | suffix := ""
28 | if env != "" {
29 | suffix = suffix + "." + env
30 | }
31 | dotenvPath := path.Join(projectRoot, ".env"+suffix)
32 | _ = godotenv.Load(dotenvPath)
33 |
34 | viper.AutomaticEnv()
35 |
36 | jwt.Secret = viper.GetString("JWT_SECRET")
37 | if jwt.Secret == "" {
38 | if strings.HasPrefix(env, "test") {
39 | // generate jwt secret and write into file
40 | s, err := secret.GenerateRandomString(256)
41 | if err != nil {
42 | log.Fatal(err)
43 | }
44 | jwtString := fmt.Sprintf("JWT_SECRET=%s\n", s)
45 | err = ioutil.WriteFile(dotenvPath, []byte(jwtString), 0644)
46 | if err != nil {
47 | log.Fatal(err)
48 | }
49 | } else {
50 | log.Fatalf("Failed to set your environment variable JWT_SECRET. \n" +
51 | "Please do so via \n" +
52 | "go run . generate_secret\n" +
53 | "export JWT_SECRET=[the generated secret]")
54 | }
55 | }
56 |
57 | return jwt
58 | }
59 |
60 | // JWT holds data necessary for JWT configuration
61 | type JWT struct {
62 | Realm string `default:"jwtrealm"`
63 | Secret string `default:""`
64 | Duration int `default:"15"`
65 | RefreshDuration int `default:"10"`
66 | MaxRefresh int `default:"10"`
67 | SigningAlgorithm string `default:"HS256"`
68 | }
69 |
--------------------------------------------------------------------------------
/request/auth.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | import (
4 | "github.com/alpacahq/ribbit-backend/apperr"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | // Credentials stores the username and password provided in the request
10 | type Credentials struct {
11 | Email string `json:"email" binding:"required"`
12 | Password string `json:"password" binding:"required"`
13 | }
14 |
15 | // Login parses out the username and password in gin's request context, into Credentials
16 | func Login(c *gin.Context) (*Credentials, error) {
17 | cred := new(Credentials)
18 | if err := c.ShouldBindJSON(cred); err != nil {
19 | apperr.Response(c, err)
20 | return nil, err
21 | }
22 | return cred, nil
23 | }
24 |
25 | // ForgotPayload stores the email provided in the request
26 | type ForgotPayload struct {
27 | Email string `json:"email" binding:"required"`
28 | }
29 |
30 | // Forgot parses out the email in gin's request context, into ForgotPayload
31 | func Forgot(c *gin.Context) (*ForgotPayload, error) {
32 | fgt := new(ForgotPayload)
33 | if err := c.ShouldBindJSON(fgt); err != nil {
34 | apperr.Response(c, err)
35 | return nil, err
36 | }
37 | return fgt, nil
38 | }
39 |
40 | // RecoverPasswordPayload stores the data provided in the request
41 | type RecoverPasswordPayload struct {
42 | Email string `json:"email" binding:"required"`
43 | OTP string `json:"otp" binding:"required"`
44 | Password string `json:"password" binding:"required"`
45 | ConfirmPassword string `json:"confrim_password" binding:"required"`
46 | }
47 |
48 | // RecoverPassword parses out the data in gin's request context, into RecoverPasswordPayload
49 | func RecoverPassword(c *gin.Context) (*RecoverPasswordPayload, error) {
50 | rpp := new(RecoverPasswordPayload)
51 | if err := c.ShouldBindJSON(rpp); err != nil {
52 | apperr.Response(c, err)
53 | return nil, err
54 | }
55 | return rpp, nil
56 | }
57 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/go
3 | # Edit at https://www.gitignore.io/?templates=go
4 |
5 | ### Go ###
6 | # Binaries for programs and plugins
7 | *.exe
8 | *.exe~
9 | *.dll
10 | *.so
11 | *.dylib
12 |
13 | # Test binary, built with `go test -c`
14 | *.test
15 |
16 | # Output of the go coverage tool, specifically when used with LiteIDE
17 | *.out
18 |
19 | # Dependency directories (remove the comment below to include it)
20 | # vendor/
21 |
22 | ### Go Patch ###
23 | /vendor/
24 | /Godeps/
25 |
26 | # End of https://www.gitignore.io/api/go
27 |
28 | gin-go-pg
29 | cc-test-reporter
30 | .env*
31 | !.env.sample
32 | bin
33 | # Created by https://www.gitignore.io/api/visualstudiocode
34 | # Edit at https://www.gitignore.io/?templates=visualstudiocode
35 |
36 | ### VisualStudioCode ###
37 | .vscode/*
38 | !.vscode/settings.json
39 | !.vscode/tasks.json
40 | !.vscode/launch.json
41 | !.vscode/extensions.json
42 |
43 | ### VisualStudioCode Patch ###
44 | # Ignore all local history of files
45 | .history
46 |
47 | # End of https://www.gitignore.io/api/visualstudiocode
48 |
49 | # Created by https://www.gitignore.io/api/macos
50 | # Edit at https://www.gitignore.io/?templates=macos
51 |
52 | ### macOS ###
53 | # General
54 | .DS_Store
55 | .AppleDouble
56 | .LSOverride
57 |
58 | # Icon must end with two \r
59 | Icon
60 |
61 | # Thumbnails
62 | ._*
63 |
64 | # Files that might appear in the root of a volume
65 | .DocumentRevisions-V100
66 | .fseventsd
67 | .Spotlight-V100
68 | .TemporaryItems
69 | .Trashes
70 | .VolumeIcon.icns
71 | .com.apple.timemachine.donotpresent
72 |
73 | # Directories potentially created on remote AFP share
74 | .AppleDB
75 | .AppleDesktop
76 | Network Trash Folder
77 | Temporary Items
78 | .apdisk
79 |
80 | # End of https://www.gitignore.io/api/macos
81 |
82 | tmp
83 | tmp2
84 | private_key.pem
85 | public_key.pem
86 | ribbit-public.com.pem
87 | ribbit.com.pem
88 |
--------------------------------------------------------------------------------
/e2e/signupemail_i_test.go:
--------------------------------------------------------------------------------
1 | package e2e_test
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io/ioutil"
8 | "log"
9 | "net/http"
10 | "net/http/httptest"
11 |
12 | "github.com/alpacahq/ribbit-backend/request"
13 |
14 | "github.com/stretchr/testify/assert"
15 | )
16 |
17 | func (suite *E2ETestSuite) TestSignupEmail() {
18 |
19 | t := suite.T()
20 |
21 | ts := httptest.NewServer(suite.r)
22 | defer ts.Close()
23 |
24 | urlSignup := ts.URL + "/signup"
25 |
26 | req := &request.EmailSignup{
27 | Email: "user@example.org",
28 | Password: "userpassword1",
29 | }
30 | b, err := json.Marshal(req)
31 | if err != nil {
32 | log.Fatal(err)
33 | }
34 |
35 | resp, err := http.Post(urlSignup, "application/json", bytes.NewBuffer(b))
36 | if err != nil {
37 | log.Fatal(err)
38 | }
39 | defer resp.Body.Close()
40 |
41 | assert.Equal(t, http.StatusCreated, resp.StatusCode)
42 |
43 | body, err := ioutil.ReadAll(resp.Body)
44 | if err != nil {
45 | log.Fatal(err)
46 | }
47 | fmt.Println(string(body))
48 |
49 | assert.Nil(t, err)
50 | }
51 |
52 | func (suite *E2ETestSuite) TestVerification() {
53 | t := suite.T()
54 | v := suite.v
55 | // verify that we can retrieve our test verification token
56 | assert.NotNil(t, v)
57 |
58 | ts := httptest.NewServer(suite.r)
59 | defer ts.Close()
60 |
61 | url := ts.URL + "/verification/" + v.Token
62 | fmt.Println("This is our verification url", url)
63 |
64 | resp, err := http.Get(url)
65 | if err != nil {
66 | log.Fatal(err)
67 | }
68 | defer resp.Body.Close()
69 | assert.Equal(t, http.StatusOK, resp.StatusCode)
70 |
71 | body, err := ioutil.ReadAll(resp.Body)
72 | if err != nil {
73 | log.Fatal(err)
74 | }
75 | fmt.Println(string(body))
76 | assert.Nil(t, err)
77 |
78 | // The second time we call our verification url, it should return not found
79 | resp, err = http.Get(url)
80 | assert.Equal(t, http.StatusNotFound, resp.StatusCode)
81 | assert.Nil(t, err)
82 | }
83 |
--------------------------------------------------------------------------------
/cmd/create_superadmin.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 |
7 | "github.com/alpacahq/ribbit-backend/config"
8 | "github.com/alpacahq/ribbit-backend/manager"
9 | "github.com/alpacahq/ribbit-backend/repository"
10 | "github.com/alpacahq/ribbit-backend/secret"
11 |
12 | "github.com/spf13/cobra"
13 | "go.uber.org/zap"
14 | )
15 |
16 | var email string
17 | var password string
18 | var createSuperAdminCmd = &cobra.Command{
19 | Use: "create_superadmin",
20 | Short: "create_superadmin creates a superadmin user that has access to manage all other users in the system",
21 | Long: `create_superadmin creates a superadmin user that has access to manage all other users in the system`,
22 | Run: func(cmd *cobra.Command, args []string) {
23 | fmt.Println("create_superadmin called")
24 |
25 | email, _ = cmd.Flags().GetString("email")
26 | fmt.Println(email)
27 | if !validateEmail(email) {
28 | fmt.Println("Invalid email provided; superadmin user not created")
29 | return
30 | }
31 |
32 | password, _ = cmd.Flags().GetString("password")
33 | if password == "" {
34 | password, _ = secret.GenerateRandomString(16)
35 | }
36 |
37 | db := config.GetConnection()
38 | log, _ := zap.NewDevelopment()
39 | defer log.Sync()
40 | accountRepo := repository.NewAccountRepo(db, log, secret.New())
41 | roleRepo := repository.NewRoleRepo(db, log)
42 |
43 | m := manager.NewManager(accountRepo, roleRepo, db)
44 | m.CreateSuperAdmin(email, password)
45 | },
46 | }
47 |
48 | func init() {
49 | localFlags := createSuperAdminCmd.Flags()
50 | localFlags.StringVarP(&email, "email", "e", "", "SuperAdmin user's email")
51 | localFlags.StringVarP(&password, "password", "p", "", "SuperAdmin user's password")
52 | createSuperAdminCmd.MarkFlagRequired("email")
53 | rootCmd.AddCommand(createSuperAdminCmd)
54 | }
55 |
56 | func validateEmail(email string) bool {
57 | Re := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
58 | return Re.MatchString(email)
59 | }
60 |
--------------------------------------------------------------------------------
/magic/magic.go:
--------------------------------------------------------------------------------
1 | package magic
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strings"
7 |
8 | "github.com/alpacahq/ribbit-backend/apperr"
9 | "github.com/alpacahq/ribbit-backend/config"
10 | "github.com/magiclabs/magic-admin-go"
11 | "github.com/magiclabs/magic-admin-go/client"
12 | "github.com/magiclabs/magic-admin-go/token"
13 | )
14 |
15 | // NewMagic creates a new magic service implementation
16 | func NewMagic(config *config.MagicConfig) *Magic {
17 | return &Magic{config}
18 | }
19 |
20 | // Magic provides a magic service implementation
21 | type Magic struct {
22 | config *config.MagicConfig
23 | }
24 |
25 | // IsValidToken validates a token with magic link
26 | func (m *Magic) IsValidToken(tkn string) (*token.Token, error) {
27 | authBearer := "Bearer"
28 | fmt.Printf("%s", authBearer)
29 | if tkn == "" {
30 | return nil, apperr.New(http.StatusUnauthorized, "Bearer token is required")
31 | }
32 |
33 | if !strings.HasPrefix(tkn, authBearer) {
34 | return nil, apperr.New(http.StatusUnauthorized, "Bearer token is required")
35 | }
36 |
37 | did := tkn[len(authBearer)+1:]
38 | if did == "" {
39 | return nil, apperr.New(http.StatusUnauthorized, "DID token is required")
40 | }
41 |
42 | tk, err := token.NewToken(did)
43 | if err != nil {
44 |
45 | return nil, apperr.New(http.StatusUnauthorized, "Malformed DID token error: "+err.Error())
46 | }
47 |
48 | if err := tk.Validate(); err != nil {
49 | return nil, apperr.New(http.StatusUnauthorized, "DID token failed validation: "+err.Error())
50 | }
51 |
52 | return tk, nil
53 | }
54 |
55 | // GetIssuer retrieves the issuer from token
56 | // func (m *Magic) GetIssuer(c *gin.Context) error
57 | func (m *Magic) GetIssuer(tk *token.Token) (*magic.UserInfo, error) {
58 | client := client.New(m.config.Secret, magic.NewDefaultClient())
59 | userInfo, err := client.User.GetMetadataByIssuer(tk.GetIssuer())
60 | if err != nil {
61 | return nil, apperr.New(http.StatusBadRequest, "Bad request")
62 | }
63 |
64 | return userInfo, nil
65 | }
66 |
--------------------------------------------------------------------------------
/.env.sample:
--------------------------------------------------------------------------------
1 | # change to localhost when running on local machine and not docker
2 | export POSTGRES_HOST=database # for local development
3 | # export POSTGRES_HOST=postgres # for docker/kubernetes
4 | export POSTGRES_PORT=5432
5 | export POSTGRES_USER=test_user
6 | export POSTGRES_PASSWORD=test_password
7 | export POSTGRES_DB=test_db
8 |
9 | # DATABASE_URL will be used in preference if it exists
10 | # export DATABASE_URL=postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DB
11 | export DATABASE_URL=$DATABASE_URL
12 |
13 | # these are needed to create the database
14 | # and create the postgres user/password initially
15 | # if they are not set in env, these are the default values
16 | export POSTGRES_SUPERUSER=postgres
17 | # change this to empty string when running on local machine and not docker
18 | export POSTGRES_SUPERUSER_PASSWORD=password
19 | export POSTGRES_SUPERUSER_DB=postgres
20 |
21 | # for transactional emails
22 | export SENDGRID_API_KEY=
23 | export DEFAULT_NAME=
24 | export DEFAULT_EMAIL=
25 |
26 | # Change this to a FQDN as needed
27 | export EXTERNAL_URL="https://localhost:8080"
28 |
29 | export TWILIO_ACCOUNT="your Account SID from twil.io/console"
30 | export TWILIO_TOKEN="your Token from twil.io/console"
31 |
32 | export TWILIO_VERIFY_NAME="calvinx"
33 | export TWILIO_VERIFY="servicetoken"
34 |
35 | export MAGIC_API_KEY=""
36 | export MAGIC_API_SECRET=""
37 |
38 | export PLAID_CLIENT_ID=
39 | export PLAID_SECRET=
40 | export PLAID_ENV=
41 | # app expects comma separated strings
42 | export PLAID_PRODUCTS=
43 | # app expects comma separated strings
44 | export PLAID_COUNTRY_CODES=
45 |
46 | # BROKER TOKEN must be in the format "Basic
--------------------------------------------------------------------------------
/mock/mockdb/user.go:
--------------------------------------------------------------------------------
1 | package mockdb
2 |
3 | import (
4 | "github.com/alpacahq/ribbit-backend/model"
5 | )
6 |
7 | // User database mock
8 | type User struct {
9 | ViewFn func(int) (*model.User, error)
10 | FindByReferralCodeFn func(string) (*model.ReferralCodeVerifyResponse, error)
11 | FindByUsernameFn func(string) (*model.User, error)
12 | FindByEmailFn func(string) (*model.User, error)
13 | FindByMobileFn func(string, string) (*model.User, error)
14 | FindByTokenFn func(string) (*model.User, error)
15 | UpdateLoginFn func(*model.User) error
16 | ListFn func(*model.ListQuery, *model.Pagination) ([]model.User, error)
17 | DeleteFn func(*model.User) error
18 | UpdateFn func(*model.User) (*model.User, error)
19 | }
20 |
21 | // View mock
22 | func (u *User) View(id int) (*model.User, error) {
23 | return u.ViewFn(id)
24 | }
25 |
26 | // FindByReferralCode mock
27 | func (u *User) FindByReferralCode(username string) (*model.ReferralCodeVerifyResponse, error) {
28 | return u.FindByReferralCodeFn(username)
29 | }
30 |
31 | // FindByUsername mock
32 | func (u *User) FindByUsername(username string) (*model.User, error) {
33 | return u.FindByUsernameFn(username)
34 | }
35 |
36 | // FindByEmail mock
37 | func (u *User) FindByEmail(email string) (*model.User, error) {
38 | return u.FindByEmailFn(email)
39 | }
40 |
41 | // FindByMobile mock
42 | func (u *User) FindByMobile(countryCode, mobile string) (*model.User, error) {
43 | return u.FindByMobileFn(countryCode, mobile)
44 | }
45 |
46 | // FindByToken mock
47 | func (u *User) FindByToken(token string) (*model.User, error) {
48 | return u.FindByTokenFn(token)
49 | }
50 |
51 | // UpdateLogin mock
52 | func (u *User) UpdateLogin(usr *model.User) error {
53 | return u.UpdateLoginFn(usr)
54 | }
55 |
56 | // List mock
57 | func (u *User) List(lq *model.ListQuery, p *model.Pagination) ([]model.User, error) {
58 | return u.ListFn(lq, p)
59 | }
60 |
61 | // Delete mock
62 | func (u *User) Delete(usr *model.User) error {
63 | return u.DeleteFn(usr)
64 | }
65 |
66 | // Update mock
67 | func (u *User) Update(usr *model.User) (*model.User, error) {
68 | return u.UpdateFn(usr)
69 | }
70 |
--------------------------------------------------------------------------------
/server/server.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/alpacahq/ribbit-backend/config"
7 | "github.com/alpacahq/ribbit-backend/mail"
8 | mw "github.com/alpacahq/ribbit-backend/middleware"
9 | "github.com/alpacahq/ribbit-backend/mobile"
10 | "github.com/alpacahq/ribbit-backend/route"
11 |
12 | "github.com/gin-gonic/gin"
13 |
14 | "go.uber.org/zap"
15 | )
16 |
17 | // Server holds all the routes and their services
18 | type Server struct {
19 | RouteServices []route.ServicesI
20 | }
21 |
22 | func CORSMiddleware() gin.HandlerFunc {
23 | return func(c *gin.Context) {
24 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
25 | c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
26 | c.Writer.Header().Set("Access-Control-Allow-Headers", "Access-Control-Allow-Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
27 | c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH")
28 |
29 | if c.Request.Method == "OPTIONS" {
30 | c.AbortWithStatus(204)
31 | return
32 | }
33 |
34 | c.Next()
35 | }
36 | }
37 |
38 | // Run runs our API server
39 | func (server *Server) Run(env string) error {
40 |
41 | // load configuration
42 | j := config.LoadJWT(env)
43 |
44 | r := gin.Default()
45 | r.LoadHTMLGlob("templates/*")
46 |
47 | // middleware
48 | mw.Add(r, CORSMiddleware())
49 | jwt := mw.NewJWT(j)
50 | m := mail.NewMail(config.GetMailConfig(), config.GetSiteConfig())
51 | mobile := mobile.NewMobile(config.GetTwilioConfig())
52 | db := config.GetConnection()
53 | log, _ := zap.NewDevelopment()
54 | defer log.Sync()
55 |
56 | // setup default routes
57 | rsDefault := &route.Services{
58 | DB: db,
59 | Log: log,
60 | JWT: jwt,
61 | Mail: m,
62 | Mobile: mobile,
63 | R: r}
64 | rsDefault.SetupV1Routes()
65 |
66 | // setup all custom/user-defined route services
67 | for _, rs := range server.RouteServices {
68 | rs.SetupRoutes()
69 | }
70 |
71 | port, ok := os.LookupEnv("PORT")
72 | if !ok {
73 | port = "8080"
74 | }
75 |
76 | // run with port from config
77 | return r.Run(":" + port)
78 | }
79 |
--------------------------------------------------------------------------------
/public/57c36644-876b-437c-b913-3cdb58b18fd3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/service/user.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/alpacahq/ribbit-backend/apperr"
7 | "github.com/alpacahq/ribbit-backend/model"
8 | "github.com/alpacahq/ribbit-backend/repository/user"
9 | "github.com/alpacahq/ribbit-backend/request"
10 |
11 | "github.com/gin-gonic/gin"
12 | )
13 |
14 | // User represents the user http service
15 | type User struct {
16 | svc *user.Service
17 | }
18 |
19 | // UserRouter declares the orutes for users router group
20 | func UserRouter(svc *user.Service, r *gin.RouterGroup) {
21 | u := User{
22 | svc: svc,
23 | }
24 | ur := r.Group("/users")
25 | ur.GET("", u.list)
26 | ur.GET("/:id", u.view)
27 | ur.PATCH("/:id", u.update)
28 | ur.DELETE("/:id", u.delete)
29 | }
30 |
31 | type listResponse struct {
32 | Users []model.User `json:"users"`
33 | Page int `json:"page"`
34 | }
35 |
36 | func (u *User) list(c *gin.Context) {
37 | p, err := request.Paginate(c)
38 | if err != nil {
39 | return
40 | }
41 | result, err := u.svc.List(c, &model.Pagination{
42 | Limit: p.Limit, Offset: p.Offset,
43 | })
44 | if err != nil {
45 | apperr.Response(c, err)
46 | return
47 | }
48 | c.JSON(http.StatusOK, listResponse{
49 | Users: result,
50 | Page: p.Page,
51 | })
52 | }
53 |
54 | func (u *User) view(c *gin.Context) {
55 | id, err := request.ID(c)
56 | if err != nil {
57 | return
58 | }
59 | result, err := u.svc.View(c, id)
60 | if err != nil {
61 | apperr.Response(c, err)
62 | return
63 | }
64 | c.JSON(http.StatusOK, result)
65 | }
66 |
67 | func (u *User) update(c *gin.Context) {
68 | updateUser, err := request.UserUpdate(c)
69 | if err != nil {
70 | return
71 | }
72 | userUpdate, err := u.svc.Update(c, &user.Update{
73 | ID: updateUser.ID,
74 | FirstName: updateUser.FirstName,
75 | LastName: updateUser.LastName,
76 | Mobile: updateUser.Mobile,
77 | Phone: updateUser.Phone,
78 | Address: updateUser.Address,
79 | })
80 | if err != nil {
81 | apperr.Response(c, err)
82 | return
83 | }
84 | c.JSON(http.StatusOK, userUpdate)
85 | }
86 |
87 | func (u *User) delete(c *gin.Context) {
88 | id, err := request.ID(c)
89 | if err != nil {
90 | return
91 | }
92 | if err := u.svc.Delete(c, id); err != nil {
93 | apperr.Response(c, err)
94 | return
95 | }
96 | c.JSON(http.StatusOK, gin.H{})
97 | }
98 |
--------------------------------------------------------------------------------
/repository/user/user.go:
--------------------------------------------------------------------------------
1 | package user
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/alpacahq/ribbit-backend/apperr"
7 | "github.com/alpacahq/ribbit-backend/model"
8 | "github.com/alpacahq/ribbit-backend/repository/platform/query"
9 | "github.com/alpacahq/ribbit-backend/repository/platform/structs"
10 |
11 | "github.com/gin-gonic/gin"
12 | )
13 |
14 | // NewUserService create a new user application service
15 | func NewUserService(userRepo model.UserRepo, auth model.AuthService, rbac model.RBACService) *Service {
16 | return &Service{
17 | userRepo: userRepo,
18 | auth: auth,
19 | rbac: rbac,
20 | }
21 | }
22 |
23 | // Service represents the user application service
24 | type Service struct {
25 | userRepo model.UserRepo
26 | auth model.AuthService
27 | rbac model.RBACService
28 | }
29 |
30 | // List returns list of users
31 | func (s *Service) List(c *gin.Context, p *model.Pagination) ([]model.User, error) {
32 | u := s.auth.User(c)
33 | q, err := query.List(u)
34 | if err != nil {
35 | return nil, err
36 | }
37 | return s.userRepo.List(q, p)
38 | }
39 |
40 | // View returns single user
41 | func (s *Service) View(c *gin.Context, id int) (*model.User, error) {
42 | if !s.rbac.EnforceUser(c, id) {
43 | return nil, apperr.New(http.StatusForbidden, "Forbidden")
44 | }
45 | return s.userRepo.View(id)
46 | }
47 |
48 | // Update contains user's information used for updating
49 | type Update struct {
50 | ID int
51 | FirstName *string
52 | LastName *string
53 | Mobile *string
54 | Phone *string
55 | Address *string
56 | }
57 |
58 | // Update updates user's contact information
59 | func (s *Service) Update(c *gin.Context, update *Update) (*model.User, error) {
60 | if !s.rbac.EnforceUser(c, update.ID) {
61 | return nil, apperr.New(http.StatusForbidden, "Forbidden")
62 | }
63 | u, err := s.userRepo.View(update.ID)
64 | if err != nil {
65 | return nil, err
66 | }
67 | structs.Merge(u, update)
68 | return s.userRepo.Update(u)
69 | }
70 |
71 | // Delete deletes a user
72 | func (s *Service) Delete(c *gin.Context, id int) error {
73 | u, err := s.userRepo.View(id)
74 | if err != nil {
75 | return err
76 | }
77 | if !s.rbac.IsLowerRole(c, u.Role.AccessLevel) {
78 | return apperr.New(http.StatusForbidden, "Forbidden")
79 | }
80 | u.Delete()
81 | return s.userRepo.Delete(u)
82 | }
83 |
--------------------------------------------------------------------------------
/mobile/mobile.go:
--------------------------------------------------------------------------------
1 | package mobile
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "log"
7 | "net/http"
8 | "net/url"
9 | "strconv"
10 | "strings"
11 |
12 | "github.com/alpacahq/ribbit-backend/config"
13 | )
14 |
15 | // NewMobile creates a new mobile service implementation
16 | func NewMobile(config *config.TwilioConfig) *Mobile {
17 | return &Mobile{config}
18 | }
19 |
20 | // Mobile provides a mobile service implementation
21 | type Mobile struct {
22 | config *config.TwilioConfig
23 | }
24 |
25 | // GenerateSMSToken sends an sms token to the mobile numer
26 | func (m *Mobile) GenerateSMSToken(countryCode, mobile string) error {
27 | apiURL := m.getTwilioVerifyURL()
28 | data := url.Values{}
29 | data.Set("To", countryCode+mobile)
30 | data.Set("Channel", "sms")
31 | resp, err := m.send(apiURL, data)
32 | if err != nil {
33 | return err
34 | }
35 |
36 | bodyBytes, err := ioutil.ReadAll(resp.Body)
37 | if err != nil {
38 | log.Fatal(err)
39 | }
40 | bodyString := string(bodyBytes)
41 | fmt.Println(bodyString)
42 | return err
43 | }
44 |
45 | // CheckCode verifies if the user-provided code is approved
46 | func (m *Mobile) CheckCode(countryCode, mobile, code string) error {
47 | apiURL := m.getTwilioVerifyURL()
48 | data := url.Values{}
49 | data.Set("To", countryCode+mobile)
50 | data.Set("Code", code)
51 | resp, err := m.send(apiURL, data)
52 | if err != nil {
53 | return err
54 | }
55 |
56 | // take a look at our response
57 | fmt.Println(resp.StatusCode)
58 | fmt.Println(resp.Body)
59 | bodyBytes, err := ioutil.ReadAll(resp.Body)
60 | if err != nil {
61 | log.Fatal(err)
62 | }
63 | bodyString := string(bodyBytes)
64 | fmt.Println(bodyString)
65 | return nil
66 | }
67 |
68 | func (m *Mobile) getTwilioVerifyURL() string {
69 | return "https://verify.twilio.com/v2/Services/" + m.config.Verify + "/Verifications"
70 | }
71 |
72 | func (m *Mobile) send(apiURL string, data url.Values) (*http.Response, error) {
73 | u, _ := url.ParseRequestURI(apiURL)
74 | urlStr := u.String()
75 | // http client
76 | client := &http.Client{}
77 | r, _ := http.NewRequest("POST", urlStr, strings.NewReader(data.Encode())) // URL-encoded payload
78 | r.SetBasicAuth(m.config.Account, m.config.Token)
79 | r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
80 | r.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
81 |
82 | return client.Do(r)
83 | }
84 |
--------------------------------------------------------------------------------
/manager/manager.go:
--------------------------------------------------------------------------------
1 | package manager
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "reflect"
7 | "strings"
8 |
9 | "github.com/alpacahq/ribbit-backend/model"
10 | "github.com/alpacahq/ribbit-backend/repository"
11 | "github.com/alpacahq/ribbit-backend/secret"
12 |
13 | "github.com/gertd/go-pluralize"
14 | "github.com/go-pg/pg/v9"
15 | "github.com/go-pg/pg/v9/orm"
16 | )
17 |
18 | // NewManager returns a new manager
19 | func NewManager(accountRepo *repository.AccountRepo, roleRepo *repository.RoleRepo, db *pg.DB) *Manager {
20 | return &Manager{accountRepo, roleRepo, db}
21 | }
22 |
23 | // Manager holds a group of methods for writing tests
24 | type Manager struct {
25 | accountRepo *repository.AccountRepo
26 | roleRepo *repository.RoleRepo
27 | db *pg.DB
28 | }
29 |
30 | // CreateSchema creates tables declared as models (struct)
31 | func (m *Manager) CreateSchema(models ...interface{}) {
32 | for _, model := range models {
33 | opt := &orm.CreateTableOptions{
34 | IfNotExists: true,
35 | FKConstraints: true,
36 | }
37 | err := m.db.CreateTable(model, opt)
38 | if err != nil {
39 | log.Fatal(err)
40 | }
41 | p := pluralize.NewClient()
42 | modelName := GetType(model)
43 | tableName := p.Plural(strings.ToLower(modelName))
44 | fmt.Printf("Created model %s as table %s\n", modelName, tableName)
45 | }
46 | }
47 |
48 | // CreateRoles is a thin wrapper for roleRepo.CreateRoles(), which populates our roles table
49 | func (m *Manager) CreateRoles() {
50 | err := m.roleRepo.CreateRoles()
51 | if err != nil {
52 | log.Fatal(err)
53 | }
54 | }
55 |
56 | // CreateSuperAdmin is used to create a user object with superadmin role
57 | func (m *Manager) CreateSuperAdmin(email, password string) (*model.User, error) {
58 | u := &model.User{
59 | Email: email,
60 | Password: secret.New().HashPassword(password),
61 | Active: true,
62 | Verified: true,
63 | RoleID: int(model.SuperAdminRole),
64 | }
65 | return m.accountRepo.Create(u)
66 | }
67 |
68 | // GetType is a useful utility function to help us inspect the name of a model (struct) which is expressed as an interface{}
69 | func GetType(myvar interface{}) string {
70 | valueOf := reflect.ValueOf(myvar)
71 | if valueOf.Type().Kind() == reflect.Ptr {
72 | return reflect.Indirect(valueOf).Type().Name()
73 | }
74 | return valueOf.Type().Name()
75 | }
76 |
77 | // GetModels retrieve models
78 | func GetModels() []interface{} {
79 | return model.Models
80 | }
81 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ main ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ main ]
20 | schedule:
21 | - cron: '24 11 * * 4'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'go' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v2
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v1
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
52 |
53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
54 | # If this step fails, then you should remove it and run the build manually (see below)
55 | - name: Autobuild
56 | uses: github/codeql-action/autobuild@v1
57 |
58 | # ℹ️ Command-line programs to run using the OS shell.
59 | # 📚 https://git.io/JvXDl
60 |
61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
62 | # and modify them (or add more) to build your code if your project
63 | # uses a compiled language
64 |
65 | #- run: |
66 | # make bootstrap
67 | # make release
68 |
69 | - name: Perform CodeQL Analysis
70 | uses: github/codeql-action/analyze@v1
71 |
--------------------------------------------------------------------------------
/cmd/sync_assets.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "net/http"
8 | "os"
9 |
10 | "github.com/alpacahq/ribbit-backend/config"
11 | "github.com/alpacahq/ribbit-backend/model"
12 | "github.com/alpacahq/ribbit-backend/repository"
13 | "github.com/alpacahq/ribbit-backend/secret"
14 | "github.com/spf13/cobra"
15 | "go.uber.org/zap"
16 | )
17 |
18 | // syncAssetsCmd represents the syncAssets command
19 | var syncAssetsCmd = &cobra.Command{
20 | Use: "sync_assets",
21 | Short: "sync_assets sync all the assets from broker",
22 | Long: `sync_assets sync all the assets from broker`,
23 | Run: func(cmd *cobra.Command, args []string) {
24 | fmt.Println("syncAssets called")
25 | db := config.GetConnection()
26 | log, _ := zap.NewDevelopment()
27 | defer log.Sync()
28 | assetRepo := repository.NewAssetRepo(db, log, secret.New())
29 |
30 | client := &http.Client{}
31 |
32 | req, err := http.NewRequest("GET", os.Getenv("BROKER_API_BASE")+"/v1/assets", nil)
33 | if err != nil {
34 | fmt.Print(err.Error())
35 | }
36 |
37 | req.Header.Add("Authorization", os.Getenv("BROKER_TOKEN"))
38 | response, err := client.Do(req)
39 |
40 | if err != nil {
41 | fmt.Print(err.Error())
42 | }
43 |
44 | responseData, err := ioutil.ReadAll(response.Body)
45 | if err != nil {
46 | log.Fatal(err.Error())
47 | }
48 | // fmt.Printf("%v", string(responseData))
49 |
50 | var responseObject []interface{}
51 | json.Unmarshal(responseData, &responseObject)
52 |
53 | for _, asset := range responseObject {
54 | asset, _ := asset.(map[string]interface{})
55 |
56 | newAsset := new(model.Asset)
57 | newAsset.ID = asset["id"].(string)
58 | newAsset.Class = asset["class"].(string)
59 | newAsset.Exchange = asset["exchange"].(string)
60 | newAsset.Symbol = asset["symbol"].(string)
61 | newAsset.Name = asset["name"].(string)
62 | newAsset.Status = asset["status"].(string)
63 | newAsset.Tradable = asset["tradable"].(bool)
64 | newAsset.Marginable = asset["marginable"].(bool)
65 | newAsset.Shortable = asset["shortable"].(bool)
66 | newAsset.EasyToBorrow = asset["easy_to_borrow"].(bool)
67 | newAsset.Fractionable = asset["fractionable"].(bool)
68 |
69 | if _, err := assetRepo.CreateOrUpdate(newAsset); err != nil {
70 | log.Fatal(err.Error())
71 | } else {
72 | // fmt.Println(asset)
73 | }
74 | }
75 |
76 | // m := manager.NewManager(accountRepo, roleRepo, db)
77 | // models := manager.GetModels()
78 | // m.CreateSchema(models...)
79 | // m.CreateRoles()
80 | },
81 | }
82 |
83 | func init() {
84 | rootCmd.AddCommand(syncAssetsCmd)
85 | }
86 |
--------------------------------------------------------------------------------
/request/signup.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | import (
4 | "github.com/alpacahq/ribbit-backend/apperr"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | // EmailSignup contains the user signup request
10 | type EmailSignup struct {
11 | Email string `json:"email" binding:"required,min=3,email"`
12 | Password string `json:"password" binding:"required,min=8"`
13 | }
14 |
15 | // AccountSignup validates user signup request
16 | func AccountSignup(c *gin.Context) (*EmailSignup, error) {
17 | var r EmailSignup
18 | if err := c.ShouldBindJSON(&r); err != nil {
19 | apperr.Response(c, err)
20 | return nil, err
21 | }
22 | return &r, nil
23 | }
24 |
25 | // MobileSignup contains the user signup request with a mobile number
26 | type MobileSignup struct {
27 | CountryCode string `json:"country_code" binding:"required,min=2"`
28 | Mobile string `json:"mobile" binding:"required"`
29 | }
30 |
31 | // Mobile validates user signup request via mobile
32 | func Mobile(c *gin.Context) (*MobileSignup, error) {
33 | var r MobileSignup
34 | if err := c.ShouldBindJSON(&r); err != nil {
35 | apperr.Response(c, err)
36 | return nil, err
37 | }
38 | return &r, nil
39 | }
40 |
41 | // MagicSignup contains the user signup request with a mobile number
42 | type MagicSignup struct {
43 | Email string `json:"email" binding:"required,min=3,email"`
44 | }
45 |
46 | // Magic validates user signup request via mobile
47 | func Magic(c *gin.Context) (*MagicSignup, error) {
48 | var r MagicSignup
49 | if err := c.ShouldBindJSON(&r); err != nil {
50 | apperr.Response(c, err)
51 | return nil, err
52 | }
53 | return &r, nil
54 | }
55 |
56 | // MobileVerify contains the user's mobile verification country code, mobile number and verification code
57 | type MobileVerify struct {
58 | CountryCode string `json:"country_code" binding:"required,min=2"`
59 | Mobile string `json:"mobile" binding:"required"`
60 | Code string `json:"code" binding:"required"`
61 | Signup bool `json:"signup" binding:"required"`
62 | }
63 |
64 | // AccountVerifyMobile validates user mobile verification
65 | func AccountVerifyMobile(c *gin.Context) (*MobileVerify, error) {
66 | var r MobileVerify
67 | if err := c.ShouldBindJSON(&r); err != nil {
68 | return nil, err
69 | }
70 | return &r, nil
71 | }
72 |
73 | type ReferralVerify struct {
74 | ReferralCode string `json:"referral_code" binding:"required"`
75 | }
76 |
77 | // ReferralCodeVerify verifies referral code
78 | func ReferralCodeVerify(c *gin.Context) (*ReferralVerify, error) {
79 | var r ReferralVerify
80 | if err := c.ShouldBindJSON(&r); err != nil {
81 | return nil, err
82 | }
83 | return &r, nil
84 | }
85 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 |
8 | "github.com/alpacahq/ribbit-backend/route"
9 | "github.com/alpacahq/ribbit-backend/server"
10 |
11 | "github.com/spf13/cobra"
12 |
13 | homedir "github.com/mitchellh/go-homedir"
14 | "github.com/spf13/viper"
15 | )
16 |
17 | // routes will be attached to s
18 | var s server.Server
19 |
20 | var cfgFile string
21 |
22 | // rootCmd represents the base command when called without any subcommands
23 | var rootCmd = &cobra.Command{
24 | Use: "alpaca",
25 | Short: "Broker API middleware",
26 | Long: `Broker MVP that uses golang gin as webserver, and go-pg library for connecting with a PostgreSQL database.`,
27 | // Uncomment the following line if your bare application
28 | // has an action associated with it:
29 | Run: func(cmd *cobra.Command, args []string) {
30 | var env string
31 | var ok bool
32 | if env, ok = os.LookupEnv("ALPACA_ENV"); !ok {
33 | env = "dev"
34 | fmt.Printf("Run server in %s mode\n", env)
35 | }
36 | err := s.Run(env)
37 | if err != nil {
38 | log.Fatal(err)
39 | }
40 | },
41 | }
42 |
43 | // Execute adds all child commands to the root command and sets flags appropriately.
44 | // This is called by main.main(). It only needs to happen once to the rootCmd.
45 | func Execute(customRouteServices []route.ServicesI) {
46 | s.RouteServices = customRouteServices
47 | if err := rootCmd.Execute(); err != nil {
48 | fmt.Println(err, "sdjsbhfjbhsfb")
49 | os.Exit(1)
50 | }
51 | }
52 |
53 | func init() {
54 | cobra.OnInitialize(initConfig)
55 |
56 | // Here you will define your flags and configuration settings.
57 | // Cobra supports persistent flags, which, if defined here,
58 | // will be global for your application.
59 |
60 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.alpaca.yaml)")
61 |
62 | // Cobra also supports local flags, which will only run
63 | // when this action is called directly.
64 | // rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
65 | }
66 |
67 | // initConfig reads in config file and ENV variables if set.
68 | func initConfig() {
69 | if cfgFile != "" {
70 | // Use config file from the flag.
71 | viper.SetConfigFile(cfgFile)
72 | } else {
73 | // Find home directory.
74 | home, err := homedir.Dir()
75 | if err != nil {
76 | fmt.Println(err)
77 | os.Exit(1)
78 | }
79 |
80 | // Search config in home directory with name ".alpaca" (without extension).
81 | viper.AddConfigPath(home)
82 | viper.SetConfigName(".alpaca")
83 | }
84 |
85 | viper.AutomaticEnv() // read in environment variables that match
86 |
87 | // If a config file is found, read it in.
88 | if err := viper.ReadInConfig(); err == nil {
89 | fmt.Println("Using config file:", viper.ConfigFileUsed())
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/mock/mockdb/account.go:
--------------------------------------------------------------------------------
1 | package mockdb
2 |
3 | import (
4 | "github.com/alpacahq/ribbit-backend/model"
5 | )
6 |
7 | // Account database mock
8 | type Account struct {
9 | ActivateFn func(*model.User) error
10 | CreateFn func(*model.User) (*model.User, error)
11 | CreateAndVerifyFn func(*model.User) (*model.Verification, error)
12 | CreateWithMobileFn func(*model.User) error
13 | CreateForgotTokenFn func(*model.User) (*model.Verification, error)
14 | CreateNewOTPFn func(*model.User) (*model.Verification, error)
15 | CreateWithMagicFn func(*model.User) (int, error)
16 | ChangePasswordFn func(*model.User) error
17 | ResetPasswordFn func(*model.User) error
18 | UpdateAvatarFn func(*model.User) error
19 | FindVerificationTokenFn func(string) (*model.Verification, error)
20 | FindVerificationTokenByUserFn func(*model.User) (*model.Verification, error)
21 | DeleteVerificationTokenFn func(*model.Verification) error
22 | }
23 |
24 | func (a *Account) Activate(usr *model.User) error {
25 | return a.ActivateFn(usr)
26 | }
27 |
28 | // Create mock
29 | func (a *Account) Create(usr *model.User) (*model.User, error) {
30 | return a.CreateFn(usr)
31 | }
32 |
33 | // CreateAndVerify mock
34 | func (a *Account) CreateAndVerify(usr *model.User) (*model.Verification, error) {
35 | return a.CreateAndVerifyFn(usr)
36 | }
37 |
38 | // CreateWithMobile mock
39 | func (a *Account) CreateWithMobile(usr *model.User) error {
40 | return a.CreateWithMobileFn(usr)
41 | }
42 |
43 | func (a *Account) CreateForgotToken(usr *model.User) (*model.Verification, error) {
44 | return a.CreateForgotTokenFn(usr)
45 | }
46 |
47 | func (a *Account) CreateNewOTP(usr *model.User) (*model.Verification, error) {
48 | return a.CreateNewOTPFn(usr)
49 | }
50 |
51 | func (a *Account) CreateWithMagic(usr *model.User) (int, error) {
52 | return a.CreateWithMagicFn(usr)
53 | }
54 |
55 | // ChangePassword mock
56 | func (a *Account) ChangePassword(usr *model.User) error {
57 | return a.ChangePasswordFn(usr)
58 | }
59 |
60 | func (a *Account) UpdateAvatar(usr *model.User) error {
61 | return a.UpdateAvatarFn(usr)
62 | }
63 |
64 | func (a *Account) ResetPassword(usr *model.User) error {
65 | return a.ResetPasswordFn(usr)
66 | }
67 |
68 | // FindVerificationToken mock
69 | func (a *Account) FindVerificationToken(token string) (*model.Verification, error) {
70 | return a.FindVerificationTokenFn(token)
71 | }
72 |
73 | func (a *Account) FindVerificationTokenByUser(usr *model.User) (*model.Verification, error) {
74 | return a.FindVerificationTokenByUserFn(usr)
75 | }
76 |
77 | // DeleteVerificationToken mock
78 | func (a *Account) DeleteVerificationToken(v *model.Verification) error {
79 | return a.DeleteVerificationTokenFn(v)
80 | }
81 |
--------------------------------------------------------------------------------
/repository/account/account.go:
--------------------------------------------------------------------------------
1 | package account
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/alpacahq/ribbit-backend/apperr"
7 | "github.com/alpacahq/ribbit-backend/model"
8 | "github.com/alpacahq/ribbit-backend/repository/platform/structs"
9 | "github.com/alpacahq/ribbit-backend/request"
10 | "github.com/alpacahq/ribbit-backend/secret"
11 |
12 | "github.com/gin-gonic/gin"
13 | )
14 |
15 | // Service represents the account application service
16 | type Service struct {
17 | accountRepo model.AccountRepo
18 | userRepo model.UserRepo
19 | rbac model.RBACService
20 | secret secret.Service
21 | }
22 |
23 | // NewAccountService creates a new account application service
24 | func NewAccountService(userRepo model.UserRepo, accountRepo model.AccountRepo, rbac model.RBACService, secret secret.Service) *Service {
25 | return &Service{
26 | accountRepo: accountRepo,
27 | userRepo: userRepo,
28 | rbac: rbac,
29 | secret: secret,
30 | }
31 | }
32 |
33 | // Create creates a new user account
34 | func (s *Service) Create(c *gin.Context, u *model.User) error {
35 | if !s.rbac.AccountCreate(c, u.RoleID) {
36 | return apperr.New(http.StatusForbidden, "Forbidden")
37 | }
38 | u.Password = s.secret.HashPassword(u.Password)
39 | u, err := s.accountRepo.Create(u)
40 | return err
41 | }
42 |
43 | // ChangePassword changes user's password
44 | func (s *Service) ChangePassword(c *gin.Context, oldPass, newPass string, id int) error {
45 | if !s.rbac.EnforceUser(c, id) {
46 | return apperr.New(http.StatusForbidden, "Forbidden")
47 | }
48 | u, err := s.userRepo.View(id)
49 | if err != nil {
50 | return err
51 | }
52 | if !s.secret.HashMatchesPassword(u.Password, oldPass) {
53 | return apperr.New(http.StatusBadGateway, "old password is not correct")
54 | }
55 | u.Password = s.secret.HashPassword(newPass)
56 | return s.accountRepo.ChangePassword(u)
57 | }
58 |
59 | // UpdateAvatar changes user's avatar
60 | func (s *Service) UpdateAvatar(c *gin.Context, newAvatar string, id int) error {
61 | if !s.rbac.EnforceUser(c, id) {
62 | return apperr.New(http.StatusForbidden, "Forbidden")
63 | }
64 | u, err := s.userRepo.View(id)
65 | if err != nil {
66 | return err
67 | }
68 | u.Avatar = newAvatar
69 | return s.accountRepo.UpdateAvatar(u)
70 | }
71 |
72 | // GetProfile gets user's profile
73 | func (s *Service) GetProfile(c *gin.Context, id int) *model.User {
74 | if !s.rbac.EnforceUser(c, id) {
75 | return nil
76 | }
77 | u, err := s.userRepo.View(id)
78 | if err != nil {
79 | return nil
80 | }
81 |
82 | return u
83 | }
84 |
85 | // UpdateProfile updated user's profile
86 | func (s *Service) UpdateProfile(c *gin.Context, update *request.Update) (*model.User, error) {
87 | if !s.rbac.EnforceUser(c, update.ID) {
88 | return nil, apperr.New(http.StatusForbidden, "Forbidden")
89 | }
90 | u, err := s.userRepo.View(update.ID)
91 | if err != nil {
92 | return nil, err
93 | }
94 | structs.Merge(u, update)
95 | return s.userRepo.Update(u)
96 | }
97 |
--------------------------------------------------------------------------------
/middleware/jwt_test.go:
--------------------------------------------------------------------------------
1 | package middleware_test
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "strings"
7 | "testing"
8 |
9 | "github.com/alpacahq/ribbit-backend/config"
10 | mw "github.com/alpacahq/ribbit-backend/middleware"
11 | "github.com/alpacahq/ribbit-backend/mock"
12 | "github.com/alpacahq/ribbit-backend/model"
13 |
14 | "github.com/gin-gonic/gin"
15 | "github.com/stretchr/testify/assert"
16 | )
17 |
18 | func hwHandler(c *gin.Context) {
19 | c.JSON(200, gin.H{
20 | "text": "Hello World.",
21 | })
22 | }
23 |
24 | func ginHandler(mw ...gin.HandlerFunc) *gin.Engine {
25 | gin.SetMode(gin.TestMode)
26 | r := gin.New()
27 | for _, v := range mw {
28 | r.Use(v)
29 | }
30 | r.GET("/hello", hwHandler)
31 | return r
32 | }
33 |
34 | func TestMWFunc(t *testing.T) {
35 | cases := []struct {
36 | name string
37 | wantStatus int
38 | header string
39 | }{
40 | {
41 | name: "Empty header",
42 | wantStatus: http.StatusUnauthorized,
43 | },
44 | {
45 | name: "Header not containing Bearer",
46 | header: "notBearer",
47 | wantStatus: http.StatusUnauthorized,
48 | },
49 | {
50 | name: "Invalid header",
51 | header: mock.HeaderInvalid(),
52 | wantStatus: http.StatusUnauthorized,
53 | },
54 | {
55 | name: "Success",
56 | header: mock.HeaderValid(),
57 | wantStatus: http.StatusOK,
58 | },
59 | }
60 | jwtCfg := &config.JWT{Realm: "testRealm", Secret: "jwtsecret", Duration: 60, SigningAlgorithm: "HS256"}
61 | jwtMW := mw.NewJWT(jwtCfg)
62 | ts := httptest.NewServer(ginHandler(jwtMW.MWFunc()))
63 | defer ts.Close()
64 | path := ts.URL + "/hello"
65 | client := &http.Client{}
66 |
67 | for _, tt := range cases {
68 | t.Run(tt.name, func(t *testing.T) {
69 | req, _ := http.NewRequest("GET", path, nil)
70 | req.Header.Set("Authorization", tt.header)
71 | res, err := client.Do(req)
72 | if err != nil {
73 | t.Fatal("Cannot create http request")
74 | }
75 | assert.Equal(t, tt.wantStatus, res.StatusCode)
76 | })
77 | }
78 | }
79 |
80 | func TestGenerateToken(t *testing.T) {
81 | cases := []struct {
82 | name string
83 | wantToken string
84 | req *model.User
85 | }{
86 | {
87 | name: "Success",
88 | req: &model.User{
89 | Base: model.Base{},
90 | ID: 1,
91 | Username: "johndoe",
92 | Email: "johndoe@mail.com",
93 | Role: &model.Role{
94 | AccessLevel: model.SuperAdminRole,
95 | },
96 | },
97 | wantToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
98 | },
99 | }
100 | jwtCfg := &config.JWT{Realm: "testRealm", Secret: "jwtsecret", Duration: 60, SigningAlgorithm: "HS256"}
101 |
102 | for _, tt := range cases {
103 | t.Run(tt.name, func(t *testing.T) {
104 | jwt := mw.NewJWT(jwtCfg)
105 | str, _, err := jwt.GenerateToken(tt.req)
106 | assert.Nil(t, err)
107 | assert.Equal(t, tt.wantToken, strings.Split(str, ".")[0])
108 | })
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/mockgopg/mock.go:
--------------------------------------------------------------------------------
1 | package mockgopg
2 |
3 | import (
4 | "strings"
5 | "sync"
6 |
7 | "github.com/alpacahq/ribbit-backend/manager"
8 | )
9 |
10 | // SQLMock handles query mocks
11 | type SQLMock struct {
12 | lock *sync.RWMutex
13 | currentQuery string // tracking queries
14 | currentParams []interface{}
15 | queries map[string]buildQuery
16 | currentInsert string // tracking inserts
17 | inserts map[string]buildInsert
18 | }
19 |
20 | // ExpectInsert is a builder method that accepts a model as interface and returns an SQLMock pointer
21 | func (sqlMock *SQLMock) ExpectInsert(models ...interface{}) *SQLMock {
22 | sqlMock.lock.Lock()
23 | defer sqlMock.lock.Unlock()
24 |
25 | var inserts []string
26 | for _, v := range models {
27 | inserts = append(inserts, strings.ToLower(manager.GetType(v)))
28 | }
29 | currentInsert := strings.Join(inserts, ",")
30 |
31 | sqlMock.currentInsert = currentInsert
32 | return sqlMock
33 | }
34 |
35 | // ExpectExec is a builder method that accepts a query in string and returns an SQLMock pointer
36 | func (sqlMock *SQLMock) ExpectExec(query string) *SQLMock {
37 | sqlMock.lock.Lock()
38 | defer sqlMock.lock.Unlock()
39 |
40 | sqlMock.currentQuery = strings.TrimSpace(query)
41 | return sqlMock
42 | }
43 |
44 | // ExpectQuery accepts a query in string and returns an SQLMock pointer
45 | func (sqlMock *SQLMock) ExpectQuery(query string) *SQLMock {
46 | sqlMock.lock.Lock()
47 | defer sqlMock.lock.Unlock()
48 |
49 | sqlMock.currentQuery = strings.TrimSpace(query)
50 | return sqlMock
51 | }
52 |
53 | // ExpectQueryOne accepts a query in string and returns an SQLMock pointer
54 | func (sqlMock *SQLMock) ExpectQueryOne(query string) *SQLMock {
55 | sqlMock.lock.Lock()
56 | defer sqlMock.lock.Unlock()
57 |
58 | sqlMock.currentQuery = strings.TrimSpace(query)
59 | return sqlMock
60 | }
61 |
62 | // WithArgs is a builder method that accepts a query in string and returns an SQLMock pointer
63 | func (sqlMock *SQLMock) WithArgs(params ...interface{}) *SQLMock {
64 | sqlMock.lock.Lock()
65 | defer sqlMock.lock.Unlock()
66 |
67 | sqlMock.currentParams = make([]interface{}, 0)
68 | for _, p := range params {
69 | sqlMock.currentParams = append(sqlMock.currentParams, p)
70 | }
71 |
72 | return sqlMock
73 | }
74 |
75 | // Returns accepts expected result and error, and completes the build of our sqlMock object
76 | func (sqlMock *SQLMock) Returns(result *OrmResult, err error) {
77 | sqlMock.lock.Lock()
78 | defer sqlMock.lock.Unlock()
79 |
80 | q := buildQuery{
81 | query: sqlMock.currentQuery,
82 | params: sqlMock.currentParams,
83 | result: result,
84 | err: err,
85 | }
86 | sqlMock.queries[sqlMock.currentQuery] = q
87 | sqlMock.currentQuery = ""
88 | sqlMock.currentParams = nil
89 |
90 | i := buildInsert{
91 | insert: sqlMock.currentInsert,
92 | err: err,
93 | }
94 | sqlMock.inserts[sqlMock.currentInsert] = i
95 | sqlMock.currentInsert = ""
96 | }
97 |
98 | // FlushAll resets our sqlMock object
99 | func (sqlMock *SQLMock) FlushAll() {
100 | sqlMock.lock.Lock()
101 | defer sqlMock.lock.Unlock()
102 |
103 | sqlMock.currentQuery = ""
104 | sqlMock.currentParams = nil
105 | sqlMock.queries = make(map[string]buildQuery)
106 |
107 | sqlMock.currentInsert = ""
108 | sqlMock.inserts = make(map[string]buildInsert)
109 | }
110 |
--------------------------------------------------------------------------------
/repository/rbac_i_test.go:
--------------------------------------------------------------------------------
1 | package repository_test
2 |
3 | import (
4 | "net/http/httptest"
5 | "os"
6 | "path"
7 | "path/filepath"
8 | "runtime"
9 | "testing"
10 |
11 | "github.com/alpacahq/ribbit-backend/model"
12 | "github.com/alpacahq/ribbit-backend/repository"
13 | "github.com/alpacahq/ribbit-backend/repository/account"
14 | "github.com/alpacahq/ribbit-backend/secret"
15 |
16 | embeddedpostgres "github.com/fergusstrange/embedded-postgres"
17 | "github.com/gin-gonic/gin"
18 | "github.com/go-pg/pg/v9"
19 | "github.com/stretchr/testify/assert"
20 | "github.com/stretchr/testify/suite"
21 | "go.uber.org/zap"
22 | )
23 |
24 | type RBACTestSuite struct {
25 | suite.Suite
26 | db *pg.DB
27 | postgres *embeddedpostgres.EmbeddedPostgres
28 | }
29 |
30 | func (suite *RBACTestSuite) SetupTest() {
31 | _, b, _, _ := runtime.Caller(0)
32 | d := path.Join(path.Dir(b))
33 | projectRoot := filepath.Dir(d)
34 | tmpDir := path.Join(projectRoot, "tmp")
35 | os.RemoveAll(tmpDir)
36 | testConfig := embeddedpostgres.DefaultConfig().
37 | Username("db_test_user").
38 | Password("db_test_password").
39 | Database("db_test_database").
40 | Version(embeddedpostgres.V12).
41 | RuntimePath(tmpDir).
42 | Port(9876)
43 |
44 | suite.postgres = embeddedpostgres.NewDatabase(testConfig)
45 | err := suite.postgres.Start()
46 | assert.Equal(suite.T(), err, nil)
47 |
48 | suite.db = pg.Connect(&pg.Options{
49 | Addr: "localhost:9876",
50 | User: "db_test_user",
51 | Password: "db_test_password",
52 | Database: "db_test_database",
53 | })
54 | createSchema(suite.db, &model.Role{}, &model.User{}, &model.Verification{})
55 | }
56 |
57 | func (suite *RBACTestSuite) TearDownTest() {
58 | suite.postgres.Stop()
59 | }
60 |
61 | func TestRBACTestSuiteIntegration(t *testing.T) {
62 | if testing.Short() {
63 | t.Skip("skipping integration test")
64 | return
65 | }
66 | suite.Run(t, new(RBACTestSuite))
67 | }
68 |
69 | func (suite *RBACTestSuite) TestRBAC() {
70 | // create a context for tests
71 | resp := httptest.NewRecorder()
72 | gin.SetMode(gin.TestMode)
73 | c, _ := gin.CreateTestContext(resp)
74 | c.Set("role", int8(model.SuperAdminRole))
75 |
76 | // create a user in our test database, which is superadmin
77 | log, _ := zap.NewDevelopment()
78 | userRepo := repository.NewUserRepo(suite.db, log)
79 | accountRepo := repository.NewAccountRepo(suite.db, log, secret.New())
80 | rbac := repository.NewRBACService(userRepo)
81 |
82 | // ensure that our roles table is populated with default roles
83 | roleRepo := repository.NewRoleRepo(suite.db, log)
84 | err := roleRepo.CreateRoles()
85 | assert.Nil(suite.T(), err)
86 |
87 | accountService := account.NewAccountService(userRepo, accountRepo, rbac, secret.New())
88 | err = accountService.Create(c, &model.User{
89 | CountryCode: "+65",
90 | Mobile: "91919191",
91 | Active: true,
92 | RoleID: 3,
93 | })
94 |
95 | assert.Nil(suite.T(), err)
96 | assert.NotNil(suite.T(), rbac)
97 |
98 | // since the current user is a superadmin, we should be able to change user data
99 | userID := 1
100 | access := rbac.EnforceUser(c, userID)
101 | assert.True(suite.T(), access)
102 |
103 | // since the current user is a superadmin, we should be able to change location data
104 | // access = rbac.EnforceLocation(c, 1)
105 | // assert.True(suite.T(), access)
106 | }
107 |
--------------------------------------------------------------------------------
/config/postgres.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "net/url"
7 | "path"
8 | "path/filepath"
9 | "runtime"
10 | "strings"
11 |
12 | "github.com/caarlos0/env/v6"
13 | "github.com/go-pg/pg/v9"
14 | "github.com/joho/godotenv"
15 | )
16 |
17 | // PostgresConfig persists the config for our PostgreSQL database connection
18 | type PostgresConfig struct {
19 | URL string `env:"DATABASE_URL"` // DATABASE_URL will be used in preference if it exists
20 | Host string `env:"POSTGRES_HOST" envDefault:"localhost"`
21 | Port string `env:"POSTGRES_PORT" envDefault:"5432"`
22 | User string `env:"POSTGRES_USER"`
23 | Password string `env:"POSTGRES_PASSWORD"`
24 | Database string `env:"POSTGRES_DB"`
25 | }
26 |
27 | // PostgresSuperUser persists the config for our PostgreSQL superuser
28 | type PostgresSuperUser struct {
29 | Host string `env:"POSTGRES_HOST" envDefault:"localhost"`
30 | Port string `env:"POSTGRES_PORT" envDefault:"5432"`
31 | User string `env:"POSTGRES_SUPERUSER" envDefault:"postgres"`
32 | Password string `env:"POSTGRES_SUPERUSER_PASSWORD" envDefault:""`
33 | Database string `env:"POSTGRES_SUPERUSER_DB" envDefault:"postgres"`
34 | }
35 |
36 | // GetConnection returns our pg database connection
37 | // usage:
38 | // db := config.GetConnection()
39 | // defer db.Close()
40 | func GetConnection() *pg.DB {
41 | c := GetPostgresConfig()
42 | // if DATABASE_URL is valid, we will use its constituent values in preference
43 | validConfig, err := validPostgresURL(c.URL)
44 | if err == nil {
45 | c = validConfig
46 | }
47 | db := pg.Connect(&pg.Options{
48 | Addr: c.Host + ":" + c.Port,
49 | User: c.User,
50 | Password: c.Password,
51 | Database: c.Database,
52 | PoolSize: 150,
53 | })
54 | return db
55 | }
56 |
57 | // GetPostgresConfig returns a PostgresConfig pointer with the correct Postgres Config values
58 | func GetPostgresConfig() *PostgresConfig {
59 | c := PostgresConfig{}
60 |
61 | _, b, _, _ := runtime.Caller(0)
62 | d := path.Join(path.Dir(b))
63 | projectRoot := filepath.Dir(d)
64 | dotenvPath := path.Join(projectRoot, ".env")
65 | _ = godotenv.Load(dotenvPath)
66 |
67 | if err := env.Parse(&c); err != nil {
68 | fmt.Printf("%+v\n", err)
69 | }
70 | return &c
71 | }
72 |
73 | // GetPostgresSuperUserConnection gets the corresponding db connection for our superuser
74 | func GetPostgresSuperUserConnection() *pg.DB {
75 | c := getPostgresSuperUser()
76 | db := pg.Connect(&pg.Options{
77 | Addr: c.Host + ":" + c.Port,
78 | User: c.User,
79 | Password: c.Password,
80 | Database: c.Database,
81 | PoolSize: 150,
82 | })
83 | return db
84 | }
85 |
86 | func getPostgresSuperUser() *PostgresSuperUser {
87 | c := PostgresSuperUser{}
88 | if err := env.Parse(&c); err != nil {
89 | fmt.Printf("%+v\n", err)
90 | }
91 | return &c
92 | }
93 |
94 | func validPostgresURL(URL string) (*PostgresConfig, error) {
95 | if URL == "" || strings.TrimSpace(URL) == "" {
96 | return nil, errors.New("database url is blank")
97 | }
98 |
99 | validURL, err := url.Parse(URL)
100 | if err != nil {
101 | return nil, err
102 | }
103 | c := &PostgresConfig{}
104 | c.URL = URL
105 | c.Host = validURL.Host
106 | c.Database = validURL.Path
107 | c.Port = validURL.Port()
108 | c.User = validURL.User.Username()
109 | c.Password, _ = validURL.User.Password()
110 | return c, nil
111 | }
112 |
--------------------------------------------------------------------------------
/apperr/apperr.go:
--------------------------------------------------------------------------------
1 | package apperr
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/gin-gonic/gin"
8 | "gopkg.in/go-playground/validator.v8"
9 | )
10 |
11 | // APPError is the default error struct containing detailed information about the error
12 | type APPError struct {
13 | // HTTP Status code to be set in response
14 | Status int `json:"-"`
15 | // Message is the error message that may be displayed to end users
16 | Message string `json:"message,omitempty"`
17 | }
18 |
19 | var (
20 | // Generic error
21 | Generic = NewStatus(http.StatusInternalServerError)
22 | // DB represents database related errors
23 | DB = NewStatus(http.StatusInternalServerError)
24 | // Forbidden represents access to forbidden resource error
25 | Forbidden = NewStatus(http.StatusForbidden)
26 | // BadRequest represents error for bad requests
27 | BadRequest = NewStatus(http.StatusBadRequest)
28 | // NotFound represents errors for not found resources
29 | NotFound = NewStatus(http.StatusNotFound)
30 | // Unauthorized represents errors for unauthorized requests
31 | Unauthorized = NewStatus(http.StatusUnauthorized)
32 | )
33 |
34 | // NewStatus generates new error containing only http status code
35 | func NewStatus(status int) *APPError {
36 | return &APPError{Status: status}
37 | }
38 |
39 | // New generates an application error
40 | func New(status int, msg string) *APPError {
41 | return &APPError{Status: status, Message: msg}
42 | }
43 |
44 | // Error returns the error message.
45 | func (e APPError) Error() string {
46 | return e.Message
47 | }
48 |
49 | var validationErrors = map[string]string{
50 | "required": " is required, but was not received",
51 | "min": "'s value or length is less than allowed",
52 | "max": "'s value or length is bigger than allowed",
53 | }
54 |
55 | func getVldErrorMsg(s string) string {
56 | if v, ok := validationErrors[s]; ok {
57 | return v
58 | }
59 | return " failed on " + s + " validation"
60 | }
61 |
62 | // Response writes an error response to client
63 | func Response(c *gin.Context, err error) {
64 | switch err.(type) {
65 | case *APPError:
66 | e := err.(*APPError)
67 | if e.Message == "" {
68 | c.AbortWithStatus(e.Status)
69 | } else {
70 | c.AbortWithStatusJSON(e.Status, e)
71 | }
72 | return
73 | case validator.ValidationErrors:
74 | var errMsg []string
75 | e := err.(validator.ValidationErrors)
76 | for _, v := range e {
77 | errMsg = append(errMsg, fmt.Sprintf("%s%s", v.Name, getVldErrorMsg(v.ActualTag)))
78 | }
79 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": errMsg})
80 | default:
81 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
82 | "message": err.Error(),
83 | })
84 | }
85 | }
86 |
87 | // Response writes an error response to client
88 | func ResponseV2(c *gin.Context, code int, err error) {
89 | switch err.(type) {
90 | case *APPError:
91 | e := err.(*APPError)
92 | if e.Message == "" {
93 | c.AbortWithStatus(e.Status)
94 | } else {
95 | c.AbortWithStatusJSON(e.Status, e)
96 | }
97 | return
98 | case validator.ValidationErrors:
99 | var errMsg []string
100 | e := err.(validator.ValidationErrors)
101 | for _, v := range e {
102 | errMsg = append(errMsg, fmt.Sprintf("%s%s", v.Name, getVldErrorMsg(v.ActualTag)))
103 | }
104 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"message": errMsg})
105 | default:
106 | c.AbortWithStatusJSON(code, gin.H{
107 | "message": err.Error(),
108 | })
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/middleware/jwt.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "net/http"
5 | "strings"
6 | "time"
7 |
8 | "github.com/alpacahq/ribbit-backend/apperr"
9 | "github.com/alpacahq/ribbit-backend/config"
10 | "github.com/alpacahq/ribbit-backend/model"
11 |
12 | jwt "github.com/dgrijalva/jwt-go"
13 | "github.com/gin-gonic/gin"
14 | )
15 |
16 | // NewJWT generates new JWT variable necessery for auth middleware
17 | func NewJWT(c *config.JWT) *JWT {
18 | return &JWT{
19 | Realm: c.Realm,
20 | Key: []byte(c.Secret),
21 | Duration: time.Duration(c.Duration) * time.Minute,
22 | Algo: c.SigningAlgorithm,
23 | }
24 | }
25 |
26 | // JWT provides a Json-Web-Token authentication implementation
27 | type JWT struct {
28 | // Realm name to display to the user.
29 | Realm string
30 |
31 | // Secret key used for signing.
32 | Key []byte
33 |
34 | // Duration for which the jwt token is valid.
35 | Duration time.Duration
36 |
37 | // JWT signing algorithm
38 | Algo string
39 | }
40 |
41 | // MWFunc makes JWT implement the Middleware interface.
42 | func (j *JWT) MWFunc() gin.HandlerFunc {
43 |
44 | return func(c *gin.Context) {
45 | token, err := j.ParseToken(c)
46 | if err != nil || !token.Valid {
47 | c.Header("WWW-Authenticate", "JWT realm="+j.Realm)
48 | c.AbortWithStatus(http.StatusUnauthorized)
49 | return
50 | }
51 |
52 | claims := token.Claims.(jwt.MapClaims)
53 |
54 | id := int(claims["id"].(float64))
55 | username := claims["u"].(string)
56 | email := claims["e"].(string)
57 | role := int8(claims["r"].(float64))
58 |
59 | c.Set("id", id)
60 | c.Set("username", username)
61 | c.Set("email", email)
62 | c.Set("role", role)
63 |
64 | // Generate new token
65 | newToken := jwt.New(jwt.GetSigningMethod(j.Algo))
66 | newClaims := newToken.Claims.(jwt.MapClaims)
67 |
68 | expire := time.Now().Add(j.Duration)
69 | newClaims["id"] = id
70 | newClaims["u"] = username
71 | newClaims["e"] = email
72 | newClaims["r"] = role
73 | newClaims["exp"] = expire.Unix()
74 |
75 | newTokenString, err := newToken.SignedString(j.Key)
76 | if err == nil {
77 | c.Writer.Header().Set("New-Token", newTokenString)
78 | }
79 |
80 | c.Next()
81 | }
82 | }
83 |
84 | // ParseToken parses token from Authorization header
85 | func (j *JWT) ParseToken(c *gin.Context) (*jwt.Token, error) {
86 |
87 | token := c.Request.Header.Get("Authorization")
88 | if token == "" {
89 | return nil, apperr.New(http.StatusUnauthorized, "Unauthorized")
90 | }
91 | parts := strings.SplitN(token, " ", 2)
92 | if !(len(parts) == 2 && parts[0] == "Bearer") {
93 | return nil, apperr.New(http.StatusUnauthorized, "Unauthorized")
94 | }
95 |
96 | return jwt.Parse(parts[1], func(token *jwt.Token) (interface{}, error) {
97 | if jwt.GetSigningMethod(j.Algo) != token.Method {
98 | return nil, apperr.Generic
99 | }
100 | return j.Key, nil
101 | })
102 |
103 | }
104 |
105 | // GenerateToken generates new JWT token and populates it with user data
106 | func (j *JWT) GenerateToken(u *model.User) (string, string, error) {
107 | token := jwt.New(jwt.GetSigningMethod(j.Algo))
108 | claims := token.Claims.(jwt.MapClaims)
109 |
110 | expire := time.Now().Add(j.Duration)
111 | claims["id"] = u.ID
112 | claims["u"] = u.Username
113 | claims["e"] = u.Email
114 | claims["r"] = u.Role.AccessLevel
115 | claims["exp"] = expire.Unix()
116 |
117 | tokenString, err := token.SignedString(j.Key)
118 | return tokenString, expire.Format(time.RFC3339), err
119 | }
120 |
--------------------------------------------------------------------------------
/repository/asset.go:
--------------------------------------------------------------------------------
1 | package repository
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/alpacahq/ribbit-backend/apperr"
8 | "github.com/alpacahq/ribbit-backend/model"
9 | "github.com/alpacahq/ribbit-backend/secret"
10 |
11 | "github.com/go-pg/pg/v9/orm"
12 | "go.uber.org/zap"
13 | )
14 |
15 | // NewAssetRepo returns an AssetRepo instance
16 | func NewAssetRepo(db orm.DB, log *zap.Logger, secret secret.Service) *AssetRepo {
17 | return &AssetRepo{db, log, secret}
18 | }
19 |
20 | // AssetRepo represents the client for the user table
21 | type AssetRepo struct {
22 | db orm.DB
23 | log *zap.Logger
24 | Secret secret.Service
25 | }
26 |
27 | // Create creates a new asset in our database.
28 | func (a *AssetRepo) CreateOrUpdate(ass *model.Asset) (*model.Asset, error) {
29 | _asset := new(model.Asset)
30 | sql := `SELECT id FROM assets WHERE symbol = ?`
31 | res, err := a.db.Query(_asset, sql, ass.Symbol)
32 | if err == apperr.DB {
33 | a.log.Error("AssetRepo Error: ", zap.Error(err))
34 | return nil, apperr.DB
35 | }
36 | if res.RowsReturned() != 0 {
37 | // update..
38 | fmt.Println("updating...")
39 | _, err := a.db.Model(ass).Column(
40 | "class",
41 | "exchange",
42 | "name",
43 | "status",
44 | "tradable",
45 | "marginable",
46 | "shortable",
47 | "easy_to_borrow",
48 | "fractionable",
49 | "is_watchlisted",
50 | "updated_at",
51 | ).WherePK().Update()
52 | if err != nil {
53 | a.log.Warn("AssetRepo Error: ", zap.Error(err))
54 | return nil, err
55 | }
56 | return ass, nil
57 | } else {
58 | // create
59 | fmt.Println("creating...")
60 | if err := a.db.Insert(ass); err != nil {
61 | a.log.Warn("AssetRepo error: ", zap.Error(err))
62 | return nil, apperr.DB
63 | }
64 | }
65 | return ass, nil
66 | }
67 |
68 | // UpdateAsset changes user's avatar
69 | func (a *AssetRepo) UpdateAsset(u *model.Asset) error {
70 | _, err := a.db.Model(u).Column(
71 | "class",
72 | "exchange",
73 | "name",
74 | "status",
75 | "tradable",
76 | "marginable",
77 | "shortable",
78 | "easy_to_borrow",
79 | "fractionable",
80 | "is_watchlisted",
81 | "updated_at",
82 | ).WherePK().Update()
83 | if err != nil {
84 | a.log.Warn("AssetRepo Error: ", zap.Error(err))
85 | }
86 | return err
87 | }
88 |
89 | // SearchAssets changes user's avatar
90 | func (a *AssetRepo) Search(query string) ([]model.Asset, error) {
91 | var exactAsset model.Asset
92 | var assets []model.Asset
93 | sql := `SELECT * FROM assets WHERE LOWER(symbol) = LOWER(?) LIMIT 1`
94 | _, err := a.db.QueryOne(&exactAsset, sql, query, query, query)
95 | if err != nil {
96 | a.log.Warn("AssetRepo Error", zap.String("Error:", err.Error()))
97 | }
98 |
99 | sql2 := `SELECT * FROM assets WHERE symbol ILIKE ? || '%' OR name ILIKE ? || '%' ORDER BY symbol ASC LIMIT 50`
100 | _, err2 := a.db.Query(&assets, sql2, query, query, query)
101 | if err2 != nil {
102 | a.log.Warn("AssetRepo Error", zap.String("Error:", err2.Error()))
103 | return assets, apperr.New(http.StatusNotFound, "404 not found")
104 | }
105 |
106 | if err == nil {
107 | fmt.Println(exactAsset)
108 | assets = append([]model.Asset{exactAsset}, findAndDelete(assets, exactAsset)...)
109 | }
110 |
111 | return assets, nil
112 | }
113 |
114 | func findAndDelete(s []model.Asset, item model.Asset) []model.Asset {
115 | index := 0
116 | for _, i := range s {
117 | if i != item {
118 | s[index] = i
119 | index++
120 | }
121 | }
122 | return s[:index]
123 | }
124 |
--------------------------------------------------------------------------------
/migration/migration.go:
--------------------------------------------------------------------------------
1 | package migration
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "log"
7 | "os"
8 |
9 | "github.com/alpacahq/ribbit-backend/config"
10 | "github.com/alpacahq/ribbit-backend/model"
11 |
12 | migrations "github.com/go-pg/migrations/v7"
13 | "github.com/go-pg/pg/v9"
14 | "github.com/go-pg/pg/v9/orm"
15 | )
16 |
17 | const usageText = `This program runs command on the db. Supported commands are:
18 | - init - creates version info table in the database
19 | - up - runs all available migrations.
20 | - up [target] - runs available migrations up to the target one.
21 | - down - reverts last migration.
22 | - reset - reverts all migrations.
23 | - version - prints current db version.
24 | - set_version [version] - sets db version without running migrations.
25 | - create_schema [version] - creates initial set of tables from models (structs).
26 | - sync_assets - sync all the assets from broker.
27 | Usage:
28 | go run *.go [args]
29 | `
30 |
31 | // Run executes migration subcommands
32 | func Run(args ...string) error {
33 | fmt.Println("Running migration")
34 |
35 | p := config.GetPostgresConfig()
36 |
37 | // connection to db as postgres superuser
38 | dbSuper := config.GetPostgresSuperUserConnection()
39 | defer dbSuper.Close()
40 |
41 | // connection to db as POSTGRES_USER
42 | db := config.GetConnection()
43 | defer db.Close()
44 |
45 | createUserIfNotExist(dbSuper, p)
46 |
47 | createDatabaseIfNotExist(dbSuper, p)
48 |
49 | if flag.Arg(0) == "create_schema" {
50 | createSchema(db, &model.Role{}, &model.User{}, &model.Verification{})
51 | os.Exit(2)
52 | }
53 |
54 | oldVersion, newVersion, err := migrations.Run(db, args...)
55 | if err != nil {
56 | exitf(err.Error())
57 | }
58 | if newVersion != oldVersion {
59 | fmt.Printf("migrated from version %d to %d\n", oldVersion, newVersion)
60 | } else {
61 | fmt.Printf("version is %d\n", oldVersion)
62 | }
63 | return nil
64 | }
65 |
66 | func usage() {
67 | fmt.Print(usageText)
68 | flag.PrintDefaults()
69 | os.Exit(2)
70 | }
71 |
72 | func errorf(s string, args ...interface{}) {
73 | fmt.Fprintf(os.Stderr, s+"\n", args...)
74 | }
75 |
76 | func exitf(s string, args ...interface{}) {
77 | errorf(s, args...)
78 | os.Exit(1)
79 | }
80 |
81 | func createUserIfNotExist(db *pg.DB, p *config.PostgresConfig) {
82 | statement := fmt.Sprintf(`SELECT * FROM pg_roles WHERE rolname = '%s';`, p.User)
83 | res, _ := db.Exec(statement)
84 | if res.RowsReturned() == 0 {
85 | statement = fmt.Sprintf(`CREATE USER %s WITH PASSWORD '%s';`, p.User, p.Password)
86 | _, err := db.Exec(statement)
87 | if err != nil {
88 | fmt.Println(err)
89 | } else {
90 | fmt.Printf(`Created user %s`, p.User)
91 | }
92 | }
93 | }
94 |
95 | func createDatabaseIfNotExist(db *pg.DB, p *config.PostgresConfig) {
96 | statement := fmt.Sprintf(`SELECT 1 AS result FROM pg_database WHERE datname = '%s';`, p.Database)
97 | res, _ := db.Exec(statement)
98 | if res.RowsReturned() == 0 {
99 | fmt.Println("creating database")
100 | statement = fmt.Sprintf(`CREATE DATABASE %s WITH OWNER %s;`, p.Database, p.User)
101 | _, err := db.Exec(statement)
102 | if err != nil {
103 | fmt.Println(err)
104 | } else {
105 | fmt.Printf(`Created database %s`, p.Database)
106 | }
107 | }
108 | }
109 |
110 | func createSchema(db *pg.DB, models ...interface{}) {
111 | for _, model := range models {
112 | opt := &orm.CreateTableOptions{
113 | IfNotExists: true,
114 | FKConstraints: true,
115 | }
116 | err := db.CreateTable(model, opt)
117 | if err != nil {
118 | log.Fatal(err)
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/public/SNAP.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/83e52ac1-bb18-4e9f-b68d-dda5a8af3ec0.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/request/user.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | import (
4 | "github.com/alpacahq/ribbit-backend/apperr"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | // UpdateUser contains user update data from json request
10 | type UpdateUser struct {
11 | ID int `json:"-"`
12 | FirstName *string `json:"first_name,omitempty" binding:"omitempty,min=2"`
13 | LastName *string `json:"last_name,omitempty" binding:"omitempty,min=2"`
14 | Mobile *string `json:"mobile,omitempty"`
15 | Phone *string `json:"phone,omitempty"`
16 | Address *string `json:"address,omitempty"`
17 | AccountID *string `json:"account_id,omitempty"`
18 | AccountNumber *string `json:"account_number,omitempty"`
19 | AccountCurrency *string `json:"account_currency,omitempty"`
20 | AccountStatus *string `json:"account_status,omitempty"`
21 | DOB *string `json:"dob,omitempty"`
22 | City *string `json:"city,omitempty"`
23 | State *string `json:"state,omitempty"`
24 | Country *string `json:"country,omitempty"`
25 | TaxIDType *string `json:"tax_id_type,omitempty"`
26 | TaxID *string `json:"tax_id,omitempty"`
27 | FundingSource *string `json:"funding_source,omitempty"`
28 | EmploymentStatus *string `json:"employment_status"`
29 | InvestingExperience *string `json:"investing_experience,omitempty"`
30 | PublicShareholder *string `json:"public_shareholder,omitempty"`
31 | AnotherBrokerage *string `json:"another_brokerage,omitempty"`
32 | DeviceID *string `json:"device_id,omitempty"`
33 | ProfileCompletion *string `json:"profile_completion,omitempty"`
34 | BIO *string `json:"bio,omitempty"`
35 | FacebookURL *string `json:"facebook_url,omitempty"`
36 | TwitterURL *string `json:"twitter_url,omitempty"`
37 | InstagramURL *string `json:"instagram_url,omitempty"`
38 | PublicPortfolio *string `json:"public_portfolio,omitempty"`
39 | EmployerName *string `json:"employer_name,omitempty"`
40 | Occupation *string `json:"occupation,omitempty"`
41 | UnitApt *string `json:"unit_apt,omitempty"`
42 | ZipCode *string `json:"zip_code,omitempty"`
43 | StockSymbol *string `json:"stock_symbol,omitempty"`
44 | BrokerageFirmName *string `json:"brokerage_firm_name,omitempty"`
45 | BrokerageFirmEmployeeName *string `json:"brokerage_firm_employee_name,omitempty"`
46 | BrokerageFirmEmployeeRelationship *string `json:"brokerage_firm_employee_relationship,omitempty"`
47 | ShareholderCompanyName *string `json:"shareholder_company_name,omitempty"`
48 | Avatar *string `json:"avatar,omitempty"`
49 | ReferredBy *string `json:"referred_by,omitempty"`
50 | ReferralCode *string `json:"referral_code,omitempty"`
51 | }
52 |
53 | // UserUpdate validates user update request
54 | func UserUpdate(c *gin.Context) (*UpdateUser, error) {
55 | var u UpdateUser
56 | id, err := ID(c)
57 | if err != nil {
58 | return nil, err
59 | }
60 | if err := c.ShouldBindJSON(&u); err != nil {
61 | apperr.Response(c, err)
62 | return nil, err
63 | }
64 | u.ID = id
65 | return &u, nil
66 | }
67 |
--------------------------------------------------------------------------------
/service/plaid.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "io/ioutil"
7 | "net/http"
8 | "os"
9 |
10 | "github.com/alpacahq/ribbit-backend/apperr"
11 | "github.com/alpacahq/ribbit-backend/repository/account"
12 | "github.com/alpacahq/ribbit-backend/repository/plaid"
13 | "github.com/alpacahq/ribbit-backend/request"
14 |
15 | "github.com/gin-gonic/gin"
16 | )
17 |
18 | func PlaidRouter(svc *plaid.Service, acc *account.Service, r *gin.RouterGroup) {
19 | a := Plaid{svc, acc}
20 |
21 | ar := r.Group("/plaid")
22 | ar.GET("/create_link_token", a.createLinkToken)
23 | ar.POST("/set_access_token", a.setAccessToken)
24 | ar.GET("/recipient_banks", a.accountsList)
25 | ar.DELETE("/recipient_banks/:bank_id", a.detachAccount)
26 | }
27 |
28 | // Auth represents auth http service
29 | type Plaid struct {
30 | svc *plaid.Service
31 | acc *account.Service
32 | }
33 |
34 | func (a *Plaid) createLinkToken(c *gin.Context) {
35 | id, _ := c.Get("id")
36 | user := a.acc.GetProfile(c, id.(int))
37 |
38 | name := user.FirstName + " " + user.LastName
39 | linkToken, err := a.svc.CreateLinkToken(c, user.AccountID, name)
40 | if err != nil {
41 | apperr.Response(c, err)
42 | return
43 | }
44 |
45 | c.JSON(http.StatusOK, linkToken)
46 | }
47 |
48 | func (a *Plaid) setAccessToken(c *gin.Context) {
49 | data, err := request.SetAccessTokenbody(c)
50 | if err != nil {
51 | apperr.Response(c, err)
52 | return
53 | }
54 |
55 | id, _ := c.Get("id")
56 | user := a.acc.GetProfile(c, id.(int))
57 |
58 | if user.AccountID == "" {
59 | apperr.Response(c, apperr.New(http.StatusBadRequest, "Account not found."))
60 | return
61 | }
62 |
63 | response, err := a.svc.SetAccessToken(c, id.(int), user.AccountID, data)
64 | if err != nil {
65 | apperr.Response(c, apperr.New(http.StatusBadRequest, err.Error()))
66 | return
67 | }
68 | c.JSON(http.StatusOK, response)
69 | }
70 |
71 | func (a *Plaid) accountsList(c *gin.Context) {
72 | id, _ := c.Get("id")
73 | user := a.acc.GetProfile(c, id.(int))
74 | accountID := user.AccountID
75 |
76 | if accountID == "" {
77 | apperr.Response(c, apperr.New(http.StatusBadRequest, "Account not found."))
78 | return
79 | }
80 |
81 | client := &http.Client{}
82 | acountStatus, _ := json.Marshal(map[string]string{
83 | "statuses": "QUEUED,APPROVED,PENDING",
84 | })
85 | accountStatuses := bytes.NewBuffer(acountStatus)
86 |
87 | getAchAccountsList := os.Getenv("BROKER_API_BASE") + "/v1/accounts/" + accountID + "/ach_relationships"
88 |
89 | req, _ := http.NewRequest("GET", getAchAccountsList, accountStatuses)
90 | req.Header.Add("Authorization", os.Getenv("BROKER_TOKEN"))
91 |
92 | response, _ := client.Do(req)
93 | responseData, err := ioutil.ReadAll(response.Body)
94 | if err != nil {
95 | apperr.Response(c, apperr.New(http.StatusInternalServerError, "Something went wrong. Try again later."))
96 | return
97 | }
98 |
99 | var responseObject interface{}
100 | json.Unmarshal(responseData, &responseObject)
101 | c.JSON(response.StatusCode, responseObject)
102 | }
103 |
104 | func (a *Plaid) detachAccount(c *gin.Context) {
105 | id, _ := c.Get("id")
106 | user := a.acc.GetProfile(c, id.(int))
107 |
108 | accountID := user.AccountID
109 | bankID := c.Param("bank_id")
110 |
111 | if accountID == "" {
112 | apperr.Response(c, apperr.New(http.StatusBadRequest, "Account not found."))
113 | return
114 | }
115 |
116 | deleteAccountAPIURL := os.Getenv("BROKER_API_BASE") + "/v1/accounts/" + accountID + "/ach_relationships/" + bankID
117 |
118 | client := &http.Client{}
119 | req, _ := http.NewRequest("DELETE", deleteAccountAPIURL, nil)
120 | req.Header.Add("Authorization", os.Getenv("BROKER_TOKEN"))
121 | response, _ := client.Do(req)
122 |
123 | responseData, err := ioutil.ReadAll(response.Body)
124 | if err != nil {
125 | apperr.Response(c, apperr.New(http.StatusInternalServerError, "Something went wrong. Try again later."))
126 | return
127 | }
128 |
129 | var responseObject interface{}
130 | json.Unmarshal(responseData, &responseObject)
131 | c.JSON(response.StatusCode, responseObject)
132 | }
133 |
--------------------------------------------------------------------------------
/k8-cluster.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ConfigMap
3 | metadata:
4 | name: postgresql-configmap
5 | data:
6 | database_url: postgresql-service
7 | ---
8 | apiVersion: v1
9 | data:
10 | .dockerconfigjson: eyJhdXRocyI6eyJodHRwczovL3JlZ2lzdHJ5LmdpdGxhYi5jb20iOnsidXNlcm5hbWUiOiJyZW1vdGUtYWRtaW4iLCJwYXNzd29yZCI6IjhaYjU3X1duck5SRFF5eVlGV0s0IiwiZW1haWwiOiJub3QtbmVlZGVkQGV4YW1wbGUuY29tIiwiYXV0aCI6ImNtVnRiM1JsTFdGa2JXbHVPamhhWWpVM1gxZHVjazVTUkZGNWVWbEdWMHMwIn19fQ==
11 | kind: Secret
12 | metadata:
13 | creationTimestamp: "2021-08-13T14:23:14Z"
14 | name: regcerd
15 | namespace: default
16 | resourceVersion: "24010"
17 | uid: 6876b2ff-36cf-4b06-b8f3-49e0d7b51133
18 | type: kubernetes.io/dockerconfigjson
19 | ---
20 | apiVersion: v1
21 | kind: Secret
22 | metadata:
23 | name: postgresql-secret
24 | type: Opaque # Default key/value secret type
25 | data:
26 | postgres-root-username: dXNlcm5hbWU= # echo -n 'username' | base64
27 | postgres-root-password: cGFzc3dvcmQ= # echo -n 'password' | base64
28 | ---
29 | apiVersion: apps/v1
30 | kind: StatefulSet
31 | metadata:
32 | name: postgresql
33 | spec:
34 | serviceName: postgresql-service
35 | selector:
36 | matchLabels:
37 | app: postgresql
38 | replicas: 2
39 | template:
40 | metadata:
41 | labels:
42 | app: postgresql
43 | spec:
44 | containers:
45 | - name: postgresql
46 | image: postgres:latest
47 | volumeMounts:
48 | - name: postgresql-disk
49 | mountPath: /data
50 | env:
51 | - name: POSTGRES_USER
52 | valueFrom:
53 | secretKeyRef:
54 | name: postgresql-secret
55 | key: postgres-root-username
56 | - name: POSTGRES_PASSWORD
57 | valueFrom:
58 | secretKeyRef:
59 | name: postgresql-secret
60 | key: postgres-root-password
61 | - name: PGDATA
62 | value: /data/pgdata
63 | # Volume Claim
64 | volumeClaimTemplates:
65 | - metadata:
66 | name: postgresql-disk
67 | spec:
68 | accessModes: ["ReadWriteOnce"]
69 | resources:
70 | requests:
71 | storage: 25Gi
72 | ---
73 | apiVersion: v1
74 | kind: Service
75 | metadata:
76 | name: postgresql-lb
77 | spec:
78 | selector:
79 | app: postgresql
80 | type: LoadBalancer
81 | ports:
82 | - port: 5432
83 | targetPort: 5432
84 | ---
85 | apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
86 | kind: Deployment
87 | metadata:
88 | name: api-backend
89 | labels:
90 | app: api-backend
91 | spec:
92 | selector:
93 | matchLabels:
94 | app: api-backend
95 | replicas: 1 # tells deployment to run x pods matching the template
96 | template:
97 | metadata:
98 | labels:
99 | app: api-backend
100 | spec: # For pod
101 | containers:
102 | - name: api-backend
103 | image: registry.github.com/alpacahq/ribbit-backend/env-printer:latest
104 | ports:
105 | - containerPort: 8080
106 | env:
107 | - name: PGADMIN_DEFAULT_EMAIL
108 | valueFrom:
109 | secretKeyRef:
110 | name: postgresql-secret
111 | key: postgres-root-username
112 | - name: PGADMIN_DEFAULT_PASSWORD
113 | valueFrom:
114 | secretKeyRef:
115 | name: postgresql-secret
116 | key: postgres-root-password
117 | - name: PGADMIN_CONFIG_DEFAULT_SERVER
118 | valueFrom:
119 | configMapKeyRef:
120 | name: postgresql-configmap
121 | key: database_url
122 | - name: PGADMIN_LISTEN_PORT
123 | value: "8081"
124 | imagePullSecrets:
125 | - name: regcerd
126 | ---
127 | apiVersion: v1
128 | kind: Service
129 | metadata:
130 | name: api-backend-service
131 | spec:
132 | selector:
133 | app: api-backend
134 | type: LoadBalancer # for External service
135 | ports:
136 | - protocol: TCP
137 | port: 8080
138 | targetPort: 8080
139 | nodePort: 30001 # External port (can be in between 30000-32767)
140 |
--------------------------------------------------------------------------------
/route/route.go:
--------------------------------------------------------------------------------
1 | package route
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/alpacahq/ribbit-backend/docs"
7 | "github.com/alpacahq/ribbit-backend/magic"
8 | "github.com/alpacahq/ribbit-backend/mail"
9 | mw "github.com/alpacahq/ribbit-backend/middleware"
10 | "github.com/alpacahq/ribbit-backend/mobile"
11 | "github.com/alpacahq/ribbit-backend/repository"
12 | "github.com/alpacahq/ribbit-backend/repository/account"
13 | assets "github.com/alpacahq/ribbit-backend/repository/assets"
14 | "github.com/alpacahq/ribbit-backend/repository/auth"
15 | "github.com/alpacahq/ribbit-backend/repository/plaid"
16 | "github.com/alpacahq/ribbit-backend/repository/transfer"
17 | "github.com/alpacahq/ribbit-backend/repository/user"
18 | "github.com/alpacahq/ribbit-backend/secret"
19 | "github.com/alpacahq/ribbit-backend/service"
20 |
21 | "github.com/gin-gonic/gin"
22 | "github.com/go-pg/pg/v9"
23 | ginSwagger "github.com/swaggo/gin-swagger" // gin-swagger middleware
24 | "github.com/swaggo/gin-swagger/swaggerFiles" // swagger embed files
25 | "go.uber.org/zap"
26 | )
27 |
28 | // NewServices creates a new router services
29 | func NewServices(DB *pg.DB, Log *zap.Logger, JWT *mw.JWT, Mail mail.Service, Mobile mobile.Service, Magic magic.Service, R *gin.Engine) *Services {
30 | return &Services{DB, Log, JWT, Mail, Mobile, Magic, R}
31 | }
32 |
33 | // Services lets us bind specific services when setting up routes
34 | type Services struct {
35 | DB *pg.DB
36 | Log *zap.Logger
37 | JWT *mw.JWT
38 | Mail mail.Service
39 | Mobile mobile.Service
40 | Magic magic.Service
41 | R *gin.Engine
42 | }
43 |
44 | // SetupV1Routes instances various repos and services and sets up the routers
45 | func (s *Services) SetupV1Routes() {
46 | // database logic
47 | userRepo := repository.NewUserRepo(s.DB, s.Log)
48 | accountRepo := repository.NewAccountRepo(s.DB, s.Log, secret.New())
49 | assetRepo := repository.NewAssetRepo(s.DB, s.Log, secret.New())
50 | rbac := repository.NewRBACService(userRepo)
51 |
52 | // s.R.Use(cors.New(cors.Config{
53 | // AllowAllOrigins: true,
54 | // AllowMethods: []string{"GET", "PUT", "DELETE", "PATCH", "POST", "OPTIONS"},
55 | // AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "User-Agent", "Referrer", "Host", "Token", "Authorization"},
56 | // ExposeHeaders: []string{"Content-Length"},
57 | // AllowCredentials: false,
58 | // // AllowOriginFunc: func(origin string) bool {
59 | // // return origin == "https://github.com"
60 | // // },
61 | // MaxAge: 12 * time.Hour,
62 | // }))
63 |
64 | // service logic
65 | authService := auth.NewAuthService(userRepo, accountRepo, s.JWT, s.Mail, s.Mobile, s.Magic)
66 | accountService := account.NewAccountService(userRepo, accountRepo, rbac, secret.New())
67 | userService := user.NewUserService(userRepo, authService, rbac)
68 | plaidService := plaid.NewPlaidService(userRepo, accountRepo, s.JWT, s.DB, s.Log)
69 | transferService := transfer.NewTransferService(userRepo, accountRepo, s.JWT, s.DB, s.Log)
70 | assetsService := assets.NewAssetsService(userRepo, accountRepo, assetRepo, s.JWT, s.DB, s.Log)
71 |
72 | // no prefix, no jwt
73 | service.AuthRouter(authService, s.R)
74 |
75 | // prefixed with /v1 and protected by jwt
76 | v1Router := s.R.Group("/v1")
77 | v1Router.Use(s.JWT.MWFunc())
78 | service.AccountRouter(accountService, s.DB, v1Router)
79 | service.PlaidRouter(plaidService, accountService, v1Router)
80 | service.TransferRouter(transferService, accountService, v1Router)
81 | service.AssetsRouter(assetsService, accountService, v1Router)
82 | service.UserRouter(userService, v1Router)
83 |
84 | // Routes for static files
85 | s.R.StaticFS("/file", http.Dir("public"))
86 | s.R.StaticFS("/template", http.Dir("templates"))
87 |
88 | //Routes for swagger
89 | swagger := s.R.Group("swagger")
90 | {
91 | docs.SwaggerInfo.Title = "Alpaca MVP"
92 | docs.SwaggerInfo.Description = "Broker MVP that uses golang gin as webserver, and go-pg library for connecting with a PostgreSQL database"
93 | docs.SwaggerInfo.Version = "1.0"
94 |
95 | swagger.GET("/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
96 | }
97 |
98 | s.R.NoRoute(func(c *gin.Context) {
99 | c.Redirect(http.StatusMovedPermanently, "/swagger/index.html")
100 | })
101 | }
102 |
--------------------------------------------------------------------------------
/public/TME.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/662a919f-1455-497c-90e7-f76248e6d3a6.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/repository/user_test.go:
--------------------------------------------------------------------------------
1 | package repository_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/alpacahq/ribbit-backend/mockgopg"
7 | "github.com/alpacahq/ribbit-backend/model"
8 | "github.com/alpacahq/ribbit-backend/repository"
9 |
10 | "github.com/go-pg/pg/v9/orm"
11 | "github.com/stretchr/testify/assert"
12 | "github.com/stretchr/testify/suite"
13 | "go.uber.org/zap"
14 | )
15 |
16 | type UserUnitTestSuite struct {
17 | suite.Suite
18 | mock *mockgopg.SQLMock
19 | u *model.User
20 | userRepo *repository.UserRepo
21 | }
22 |
23 | func (suite *UserUnitTestSuite) SetupTest() {
24 | var err error
25 | var db orm.DB
26 | db, suite.mock, err = mockgopg.NewGoPGDBTest()
27 | if err != nil {
28 | suite.T().Fatalf("an error '%s' was not expected when opening a stub database connection", err)
29 | }
30 | suite.u = &model.User{
31 | Username: "hello",
32 | Email: "hello@world.org",
33 | CountryCode: "+65",
34 | Mobile: "91919191",
35 | Token: "someusertoken",
36 | }
37 |
38 | log, _ := zap.NewDevelopment()
39 | suite.userRepo = repository.NewUserRepo(db, log)
40 | }
41 |
42 | func (suite *UserUnitTestSuite) TearDownTest() {
43 | suite.mock.FlushAll()
44 | }
45 |
46 | func TestUserUnitTestSuite(t *testing.T) {
47 | suite.Run(t, new(UserUnitTestSuite))
48 | }
49 |
50 | func (suite *UserUnitTestSuite) TestFindByReferralCodeSuccess() {
51 | u := suite.u
52 | userRepo := suite.userRepo
53 | t := suite.T()
54 | mock := suite.mock
55 |
56 | sql := `SELECT "user".*, "role"."id" AS "role__id", "role"."access_level" AS "role__access_level", "role"."name" AS "role__name"
57 | FROM "users" AS "user" LEFT JOIN "roles" AS "role" ON "role"."id" = "user"."role_id"
58 | WHERE ("user"."username" = ? and deleted_at is null)`
59 | mock.ExpectQueryOne(sql).
60 | WithArgs(u.Username).
61 | Returns(mockgopg.NewResult(1, 1, u), nil)
62 |
63 | uReturned, err := userRepo.FindByReferralCode("hello")
64 | assert.Equal(t, u.Username, uReturned.Username)
65 | assert.Nil(t, err)
66 | }
67 |
68 | func (suite *UserUnitTestSuite) TestFindByUsernameSuccess() {
69 | u := suite.u
70 | userRepo := suite.userRepo
71 | t := suite.T()
72 | mock := suite.mock
73 |
74 | sql := `SELECT "user".*, "role"."id" AS "role__id", "role"."access_level" AS "role__access_level", "role"."name" AS "role__name"
75 | FROM "users" AS "user" LEFT JOIN "roles" AS "role" ON "role"."id" = "user"."role_id"
76 | WHERE ("user"."username" = ? and deleted_at is null)`
77 | mock.ExpectQueryOne(sql).
78 | WithArgs(u.Username).
79 | Returns(mockgopg.NewResult(1, 1, u), nil)
80 |
81 | uReturned, err := userRepo.FindByUsername("hello")
82 | assert.Equal(t, u.Username, uReturned.Username)
83 | assert.Nil(t, err)
84 | }
85 |
86 | func (suite *UserUnitTestSuite) TestFindByEmailSuccess() {
87 | u := suite.u
88 | userRepo := suite.userRepo
89 | t := suite.T()
90 | mock := suite.mock
91 |
92 | sql := `SELECT "user".*, "role"."id" AS "role__id", "role"."access_level" AS "role__access_level", "role"."name" AS "role__name"
93 | FROM "users" AS "user" LEFT JOIN "roles" AS "role" ON "role"."id" = "user"."role_id"
94 | WHERE ("user"."email" = ? and deleted_at is null)`
95 | mock.ExpectQueryOne(sql).
96 | WithArgs(u.Email).
97 | Returns(mockgopg.NewResult(1, 1, u), nil)
98 |
99 | uReturned, err := userRepo.FindByEmail("hello@world.org")
100 | assert.Equal(t, u.Email, uReturned.Email)
101 | assert.Nil(t, err)
102 | }
103 |
104 | func (suite *UserUnitTestSuite) TestFindByMobileSuccess() {
105 | u := suite.u
106 | userRepo := suite.userRepo
107 | t := suite.T()
108 | mock := suite.mock
109 |
110 | sql := `SELECT "user".*, "role"."id" AS "role__id", "role"."access_level" AS "role__access_level", "role"."name" AS "role__name"
111 | FROM "users" AS "user" LEFT JOIN "roles" AS "role" ON "role"."id" = "user"."role_id"
112 | WHERE ("user"."country_code" = ? and "user"."mobile" = ? and deleted_at is null)`
113 | mock.ExpectQueryOne(sql).
114 | WithArgs(u.CountryCode, u.Mobile).
115 | Returns(mockgopg.NewResult(1, 1, u), nil)
116 |
117 | uReturned, err := userRepo.FindByMobile(u.CountryCode, u.Mobile)
118 | assert.Equal(t, u.Mobile, uReturned.Mobile)
119 | assert.Nil(t, err)
120 | }
121 |
122 | func (suite *UserUnitTestSuite) TestFindByTokenSuccess() {
123 | u := suite.u
124 | userRepo := suite.userRepo
125 | t := suite.T()
126 | mock := suite.mock
127 |
128 | u.Token = "someusertoken"
129 |
130 | var user = new(model.User)
131 | user.Token = "someusertoken"
132 | user.ID = 1
133 | sql := `SELECT "user".*, "role"."id" AS "role__id", "role"."access_level" AS "role__access_level", "role"."name" AS "role__name"
134 | FROM "users" AS "user" LEFT JOIN "roles" AS "role" ON "role"."id" = "user"."role_id"
135 | WHERE ("user"."token" = ? and deleted_at is null)`
136 | mock.ExpectQueryOne(sql).
137 | WithArgs("someusertoken").
138 | Returns(mockgopg.NewResult(1, 1, user), nil)
139 |
140 | _, err := userRepo.FindByToken(u.Token)
141 | assert.Equal(t, u.Token, user.Token)
142 | assert.Nil(t, err)
143 | }
144 |
--------------------------------------------------------------------------------
/e2e/e2e_i_test.go:
--------------------------------------------------------------------------------
1 | package e2e_test
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path"
7 | "path/filepath"
8 | "runtime"
9 | "testing"
10 |
11 | "github.com/alpacahq/ribbit-backend/config"
12 | "github.com/alpacahq/ribbit-backend/e2e"
13 | "github.com/alpacahq/ribbit-backend/manager"
14 | mw "github.com/alpacahq/ribbit-backend/middleware"
15 | "github.com/alpacahq/ribbit-backend/mock"
16 | "github.com/alpacahq/ribbit-backend/model"
17 | "github.com/alpacahq/ribbit-backend/repository"
18 | "github.com/alpacahq/ribbit-backend/route"
19 | "github.com/alpacahq/ribbit-backend/secret"
20 | "github.com/alpacahq/ribbit-backend/testhelper"
21 |
22 | embeddedpostgres "github.com/fergusstrange/embedded-postgres"
23 | "github.com/gin-contrib/cors"
24 | "github.com/gin-gonic/gin"
25 | "github.com/go-pg/pg/v9"
26 | "github.com/stretchr/testify/assert"
27 | "github.com/stretchr/testify/suite"
28 | "go.uber.org/zap"
29 | )
30 |
31 | var (
32 | superUser *model.User
33 | isCI bool
34 | port uint32 = 5432 // uses 5432 in CI, and 9877 when running integration tests locally, against embedded postgresql
35 | )
36 |
37 | // end-to-end test constants
38 | const (
39 | username string = "db_test_user"
40 | password string = "db_test_password"
41 | database string = "db_test_database"
42 | host string = "localhost"
43 | tmpDirname string = "tmp2"
44 | )
45 |
46 | type E2ETestSuite struct {
47 | suite.Suite
48 | db *pg.DB
49 | postgres *embeddedpostgres.EmbeddedPostgres
50 | m *manager.Manager
51 | r *gin.Engine
52 | v *model.Verification
53 | authToken model.AuthToken
54 | }
55 |
56 | // SetupSuite runs before all tests in this test suite
57 | func (suite *E2ETestSuite) SetupSuite() {
58 | _, b, _, _ := runtime.Caller(0)
59 | d := path.Join(path.Dir(b))
60 | projectRoot := filepath.Dir(d)
61 | tmpDir := path.Join(projectRoot, tmpDirname)
62 | os.RemoveAll(tmpDir) // ensure that we start afresh
63 |
64 | _, isCI = os.LookupEnv("CIRCLECI")
65 | if !isCI { // not in CI environment, so setup our embedded postgresql for integration test
66 | port = testhelper.AllocatePort(host, 9877)
67 | testConfig := embeddedpostgres.DefaultConfig().
68 | Username(username).
69 | Password(password).
70 | Database(database).
71 | Version(embeddedpostgres.V12).
72 | RuntimePath(tmpDir).
73 | Port(port)
74 | suite.postgres = embeddedpostgres.NewDatabase(testConfig)
75 | err := suite.postgres.Start()
76 | if err != nil {
77 | fmt.Println(err)
78 | }
79 | }
80 |
81 | suite.db = pg.Connect(&pg.Options{
82 | Addr: host + ":" + fmt.Sprint(port),
83 | User: username,
84 | Password: password,
85 | Database: database,
86 | })
87 |
88 | log, _ := zap.NewDevelopment()
89 | defer log.Sync()
90 | accountRepo := repository.NewAccountRepo(suite.db, log, secret.New())
91 | roleRepo := repository.NewRoleRepo(suite.db, log)
92 | suite.m = manager.NewManager(accountRepo, roleRepo, suite.db)
93 |
94 | superUser, _ = e2e.SetupDatabase(suite.m)
95 |
96 | gin.SetMode(gin.TestMode)
97 | r := gin.Default()
98 |
99 | // middleware
100 | mw.Add(r, cors.Default())
101 |
102 | // load configuration
103 | _ = config.Load("test")
104 | j := config.LoadJWT("test")
105 | jwt := mw.NewJWT(j)
106 |
107 | // mock mail
108 | m := &mock.Mail{
109 | SendVerificationEmailFn: suite.sendVerification,
110 | }
111 | // mock mobile
112 | mobile := &mock.Mobile{
113 | GenerateSMSTokenFn: func(string, string) error {
114 | return nil
115 | },
116 | CheckCodeFn: func(string, string, string) error {
117 | return nil
118 | },
119 | }
120 |
121 | // setup routes
122 | rs := route.NewServices(suite.db, log, jwt, m, mobile, r)
123 | rs.SetupV1Routes()
124 |
125 | // we can now test our routes in an end-to-end fashion by making http calls
126 | suite.r = r
127 | }
128 |
129 | // TearDownSuite runs after all tests in this test suite
130 | func (suite *E2ETestSuite) TearDownSuite() {
131 | if !isCI { // not in CI environment, so stop our embedded postgresql db
132 | suite.postgres.Stop()
133 | }
134 | }
135 |
136 | func (suite *E2ETestSuite) TestGetModels() {
137 | models := manager.GetModels()
138 | sql := `SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public';`
139 | var count int
140 | res, err := suite.db.Query(pg.Scan(&count), sql, nil)
141 |
142 | assert.NotNil(suite.T(), res)
143 | assert.Nil(suite.T(), err)
144 | assert.Equal(suite.T(), len(models), count)
145 |
146 | sql = `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';`
147 | var names pg.Strings
148 | res, err = suite.db.Query(&names, sql, nil)
149 |
150 | assert.NotNil(suite.T(), res)
151 | assert.Nil(suite.T(), err)
152 | assert.Equal(suite.T(), len(models), len(names))
153 | }
154 |
155 | func (suite *E2ETestSuite) TestSuperUser() {
156 | assert.NotNil(suite.T(), superUser)
157 | }
158 |
159 | func TestE2ETestSuiteIntegration(t *testing.T) {
160 | if testing.Short() {
161 | t.Skip("skipping integration test")
162 | return
163 | }
164 | suite.Run(t, new(E2ETestSuite))
165 | }
166 |
167 | // our mock verification token is saved into suite.token for subsequent use
168 | func (suite *E2ETestSuite) sendVerification(email string, v *model.Verification) error {
169 | suite.v = v
170 | return nil
171 | }
172 |
--------------------------------------------------------------------------------
/mockgopg/orm.go:
--------------------------------------------------------------------------------
1 | package mockgopg
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "regexp"
9 | "strings"
10 | "sync"
11 |
12 | "github.com/alpacahq/ribbit-backend/manager"
13 |
14 | "github.com/go-pg/pg/v9/orm"
15 | )
16 |
17 | type goPgDB struct {
18 | sqlMock *SQLMock
19 | }
20 |
21 | // NewGoPGDBTest returns method that already implements orm.DB and mock instance to mocking arguments and results.
22 | func NewGoPGDBTest() (conn orm.DB, mock *SQLMock, err error) {
23 | sqlMock := &SQLMock{
24 | lock: new(sync.RWMutex),
25 | currentQuery: "",
26 | currentParams: nil,
27 | queries: make(map[string]buildQuery),
28 | currentInsert: "",
29 | inserts: make(map[string]buildInsert),
30 | }
31 |
32 | goPG := &goPgDB{
33 | sqlMock: sqlMock,
34 | }
35 |
36 | return goPG, sqlMock, nil
37 | }
38 |
39 | // not yet implemented
40 | func (p *goPgDB) Model(model ...interface{}) *orm.Query {
41 | return nil
42 | }
43 |
44 | func (p *goPgDB) ModelContext(c context.Context, model ...interface{}) *orm.Query {
45 | return nil
46 | }
47 |
48 | func (p *goPgDB) Select(model interface{}) error {
49 | return nil
50 | }
51 |
52 | func (p *goPgDB) Insert(model ...interface{}) error {
53 | // return nil
54 | return p.doInsert(context.Background(), model...)
55 | }
56 |
57 | func (p *goPgDB) Update(model interface{}) error {
58 | return nil
59 | }
60 |
61 | func (p *goPgDB) Delete(model interface{}) error {
62 | return nil
63 | }
64 |
65 | func (p *goPgDB) ForceDelete(model interface{}) error {
66 | return nil
67 | }
68 |
69 | func (p *goPgDB) Exec(query interface{}, params ...interface{}) (orm.Result, error) {
70 | sqlQuery := fmt.Sprintf("%v", query)
71 | return p.doQuery(context.Background(), nil, sqlQuery, params...)
72 | }
73 |
74 | func (p *goPgDB) ExecContext(c context.Context, query interface{}, params ...interface{}) (orm.Result, error) {
75 | sqlQuery := fmt.Sprintf("%v", query)
76 | return p.doQuery(c, nil, sqlQuery, params...)
77 | }
78 |
79 | func (p *goPgDB) ExecOne(query interface{}, params ...interface{}) (orm.Result, error) {
80 | return nil, nil
81 | }
82 |
83 | func (p *goPgDB) ExecOneContext(c context.Context, query interface{}, params ...interface{}) (orm.Result, error) {
84 | return nil, nil
85 | }
86 |
87 | func (p *goPgDB) Query(model, query interface{}, params ...interface{}) (orm.Result, error) {
88 | sqlQuery := fmt.Sprintf("%v", query)
89 | return p.doQuery(context.Background(), model, sqlQuery, params...)
90 | }
91 |
92 | func (p *goPgDB) QueryContext(c context.Context, model, query interface{}, params ...interface{}) (orm.Result, error) {
93 | sqlQuery := fmt.Sprintf("%v", query)
94 | return p.doQuery(c, model, sqlQuery, params...)
95 | }
96 |
97 | func (p *goPgDB) QueryOne(model, query interface{}, params ...interface{}) (orm.Result, error) {
98 | sqlQuery := fmt.Sprintf("%v", query)
99 | return p.doQuery(context.Background(), model, sqlQuery, params...)
100 | }
101 |
102 | func (p *goPgDB) QueryOneContext(c context.Context, model, query interface{}, params ...interface{}) (orm.Result, error) {
103 | return nil, nil
104 | }
105 |
106 | func (p *goPgDB) CopyFrom(r io.Reader, query interface{}, params ...interface{}) (orm.Result, error) {
107 | return nil, nil
108 | }
109 |
110 | func (p *goPgDB) CopyTo(w io.Writer, query interface{}, params ...interface{}) (orm.Result, error) {
111 | return nil, nil
112 | }
113 |
114 | func (p *goPgDB) Context() context.Context {
115 | return context.Background()
116 | }
117 |
118 | func (p *goPgDB) Formatter() orm.QueryFormatter {
119 | f := new(Formatter)
120 | return f
121 | }
122 |
123 | func (p *goPgDB) doInsert(ctx context.Context, models ...interface{}) error {
124 | // update p.insertMock
125 | for k, v := range p.sqlMock.inserts {
126 |
127 | // not handling value at the moment
128 |
129 | onTheListInsertStr := k
130 |
131 | var inserts []string
132 | for _, v := range models {
133 | inserts = append(inserts, strings.ToLower(manager.GetType(v)))
134 | }
135 | wantedInsertStr := strings.Join(inserts, ",")
136 |
137 | if onTheListInsertStr == wantedInsertStr {
138 | return v.err
139 | }
140 | }
141 |
142 | return nil
143 | }
144 |
145 | func (p *goPgDB) doQuery(ctx context.Context, dst interface{}, query string, params ...interface{}) (orm.Result, error) {
146 | // replace duplicate space
147 | space := regexp.MustCompile(`\s+`)
148 |
149 | for k, v := range p.sqlMock.queries {
150 | onTheList := p.Formatter().FormatQuery(nil, k, v.params...)
151 | onTheListQueryStr := strings.TrimSpace(space.ReplaceAllString(string(onTheList), " "))
152 |
153 | wantedQuery := p.Formatter().FormatQuery(nil, query, params...)
154 | wantedQueryStr := strings.TrimSpace(space.ReplaceAllString(string(wantedQuery), " "))
155 |
156 | if onTheListQueryStr == wantedQueryStr {
157 | var (
158 | data []byte
159 | err error
160 | )
161 |
162 | if dst == nil {
163 | return v.result, v.err
164 | }
165 |
166 | data, err = json.Marshal(v.result.model)
167 | if err != nil {
168 | return v.result, err
169 | }
170 |
171 | err = json.Unmarshal(data, dst)
172 | if err != nil {
173 | return v.result, err
174 | }
175 |
176 | return v.result, v.err
177 | }
178 | }
179 |
180 | return nil, fmt.Errorf("no mock expectation result")
181 | }
182 |
--------------------------------------------------------------------------------
/countries.csv:
--------------------------------------------------------------------------------
1 | "short_code","name"
2 | "USA","United States"
3 | "AFG","Afghanistan"
4 | "ALB","Albania"
5 | "DZA","Algeria"
6 | "ASM","American Samoa"
7 | "AND","Andorra"
8 | "AGO","Angola"
9 | "AIA","Anguilla"
10 | "ATA","Antarctica"
11 | "ATG","Antigua And Barbuda"
12 | "ARG","Argentina"
13 | "ARM","Armenia"
14 | "ABW","Aruba"
15 | "AUS","Australia"
16 | "AUT","Austria"
17 | "AZE","Azerbaijan"
18 | "BHS","Bahamas"
19 | "BHR","Bahrain"
20 | "BGD","Bangladesh"
21 | "BRB","Barbados"
22 | "BEL","Belgium"
23 | "BLZ","Belize"
24 | "BEN","Benin"
25 | "BMU","Bermuda"
26 | "BTN","Bhutan"
27 | "BOL","Bolivia"
28 | "BIH","Bosnia And Herzegovina"
29 | "BWA","Botswana"
30 | "BVT","Bouvet Island"
31 | "BRA","Brazil"
32 | "IOT","British Indian Ocean Territory"
33 | "BRN","Brunei Darussalam"
34 | "BGR","Bulgaria"
35 | "BFA","Burkina Faso"
36 | "BDI","Burundi"
37 | "KHM","Cambodia"
38 | "CMR","Cameroon"
39 | "CAN","Canada"
40 | "CPV","Cape Verde"
41 | "CYM","Cayman Islands"
42 | "CAF","Central African Republic"
43 | "TCD","Chad"
44 | "CHL","Chile"
45 | "CHN","China"
46 | "CXR","Christmas Island"
47 | "CCK","Cocos (Keeling) Islands"
48 | "COL","Colombia"
49 | "COM","Comoros"
50 | "COK","Cook Islands"
51 | "CRI","Costa Rica"
52 | "HRV","Croatia"
53 | "CYP","Cyprus"
54 | "CZE","Czech Republic"
55 | "DNK","Denmark"
56 | "DJI","Djibouti"
57 | "DMA","Dominica"
58 | "DOM","Dominican Republic"
59 | "ECU","Ecuador"
60 | "EGY","Egypt"
61 | "SLV","El Salvador"
62 | "GNQ","Equatorial Guinea"
63 | "ERI","Eritrea"
64 | "EST","Estonia"
65 | "ETH","Ethiopia"
66 | "FLK","Falkland Islands (Malvinas)"
67 | "FRO","Faroe Islands"
68 | "FJI","Fiji"
69 | "FIN","Finland"
70 | "FRA","France"
71 | "GUF","French Guiana"
72 | "PYF","French Polynesia"
73 | "ATF","French Southern Territories"
74 | "GAB","Gabon"
75 | "GMB","Gambia"
76 | "GEO","Georgia"
77 | "DEU","Germany"
78 | "GHA","Ghana"
79 | "GIB","Gibraltar"
80 | "GRC","Greece"
81 | "GRL","Greenland"
82 | "GRD","Grenada"
83 | "GLP","Guadeloupe"
84 | "GUM","Guam"
85 | "GTM","Guatemala"
86 | "GIN","Guinea"
87 | "GNB","Guinea-Bissau"
88 | "GUY","Guyana"
89 | "HTI","Haiti"
90 | "HMD","Heard Island & Mcdonald Islands"
91 | "VAT","Holy See (Vatican City State)"
92 | "HND","Honduras"
93 | "HKG","Hong Kong"
94 | "HUN","Hungary"
95 | "ISL","Iceland"
96 | "IND","India"
97 | "IDN","Indonesia"
98 | "IRL","Ireland"
99 | "ISR","Israel"
100 | "ITA","Italy"
101 | "JAM","Jamaica"
102 | "JPN","Japan"
103 | "JOR","Jordan"
104 | "KAZ","Kazakhstan"
105 | "KEN","Kenya"
106 | "KIR","Kiribati"
107 | "KOR","Korea"
108 | "KWT","Kuwait"
109 | "KGZ","Kyrgyzstan"
110 | "LAO","Lao People's Democratic Republic"
111 | "LVA","Latvia"
112 | "LBN","Lebanon"
113 | "LSO","Lesotho"
114 | "LBY","Libyan Arab Jamahiriya"
115 | "LIE","Liechtenstein"
116 | "LTU","Lithuania"
117 | "LUX","Luxembourg"
118 | "MAC","Macao"
119 | "MKD","Macedonia"
120 | "MDG","Madagascar"
121 | "MWI","Malawi"
122 | "MYS","Malaysia"
123 | "MDV","Maldives"
124 | "MLI","Mali"
125 | "MLT","Malta"
126 | "MHL","Marshall Islands"
127 | "MTQ","Martinique"
128 | "MRT","Mauritania"
129 | "MUS","Mauritius"
130 | "MYT","Mayotte"
131 | "MEX","Mexico"
132 | "FSM","Micronesia, Federated States Of"
133 | "MDA","Moldova"
134 | "MCO","Monaco"
135 | "MNG","Mongolia"
136 | "MSR","Montserrat"
137 | "MAR","Morocco"
138 | "MOZ","Mozambique"
139 | "MMR","Myanmar"
140 | "NAM","Namibia"
141 | "NRU","Nauru"
142 | "NPL","Nepal"
143 | "NLD","Netherlands"
144 | "ANT","Netherlands Antilles"
145 | "NCL","New Caledonia"
146 | "NZL","New Zealand"
147 | "NIC","Nicaragua"
148 | "NER","Niger"
149 | "NGA","Nigeria"
150 | "NIU","Niue"
151 | "NFK","Norfolk Island"
152 | "MNP","Northern Mariana Islands"
153 | "NOR","Norway"
154 | "OMN","Oman"
155 | "PAK","Pakistan"
156 | "PLW","Palau"
157 | "PAN","Panama"
158 | "PNG","Papua New Guinea"
159 | "PRY","Paraguay"
160 | "PER","Peru"
161 | "PHL","Philippines"
162 | "PCN","Pitcairn"
163 | "POL","Poland"
164 | "PRT","Portugal"
165 | "PRI","Puerto Rico"
166 | "QAT","Qatar"
167 | "REU","Reunion"
168 | "ROU","Romania"
169 | "RUS","Russian Federation"
170 | "RWA","Rwanda"
171 | "SHN","Saint Helena"
172 | "KNA","Saint Kitts And Nevis"
173 | "LCA","Saint Lucia"
174 | "SPM","Saint Pierre And Miquelon"
175 | "VCT","Saint Vincent And Grenadines"
176 | "WSM","Samoa"
177 | "SMR","San Marino"
178 | "STP","Sao Tome And Principe"
179 | "SAU","Saudi Arabia"
180 | "SEN","Senegal"
181 | "SRB","Serbia"
182 | "SYC","Seychelles"
183 | "SLE","Sierra Leone"
184 | "SGP","Singapore"
185 | "SVK","Slovakia"
186 | "SVN","Slovenia"
187 | "SLB","Solomon Islands"
188 | "SOM","Somalia"
189 | "ZAF","South Africa"
190 | "ESP","Spain"
191 | "LKA","Sri Lanka"
192 | "SUR","Suriname"
193 | "SJM","Svalbard And Jan Mayen"
194 | "SWZ","Swaziland"
195 | "SWE","Sweden"
196 | "CHE","Switzerland"
197 | "TWN","Taiwan"
198 | "TJK","Tajikistan"
199 | "TZA","Tanzania"
200 | "THA","Thailand"
201 | "TLS","Timor-Leste"
202 | "TGO","Togo"
203 | "TKL","Tokelau"
204 | "TON","Tonga"
205 | "TTO","Trinidad And Tobago"
206 | "TUN","Tunisia"
207 | "TUR","Turkey"
208 | "TKM","Turkmenistan"
209 | "TCA","Turks And Caicos Islands"
210 | "TUV","Tuvalu"
211 | "UGA","Uganda"
212 | "UKR","Ukraine"
213 | "ARE","United Arab Emirates"
214 | "GBR","United Kingdom"
215 | "UMI","United States Outlying Islands"
216 | "URY","Uruguay"
217 | "UZB","Uzbekistan"
218 | "VUT","Vanuatu"
219 | "VEN","Venezuela"
220 | "VNM","Viet Nam"
221 | "VGB","Virgin Islands, British"
222 | "VIR","Virgin Islands, U.S."
223 | "WLF","Wallis And Futuna"
224 | "ESH","Western Sahara"
225 | "YEM","Yemen"
226 | "ZMB","Zambia"
--------------------------------------------------------------------------------