├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── es-restore │ └── main.go ├── mccs-alpha │ └── main.go ├── pg-setup │ └── main.go └── seed │ └── main.go ├── configs ├── revive.toml ├── seed.yaml └── test.yaml ├── docker-compose.dev.yml ├── docker-compose.production.yml ├── dockerfile.dev ├── dockerfile.production ├── global ├── app.go ├── constant │ ├── business.go │ ├── constant.go │ ├── date.go │ ├── flash.go │ ├── journal.go │ ├── trading.go │ └── transaction.go └── init.go ├── go.mod ├── go.sum ├── internal ├── app │ ├── http │ │ ├── controller │ │ │ ├── account_handler.go │ │ │ ├── admin_business_handler.go │ │ │ ├── admin_history_handler.go │ │ │ ├── admin_tag_handler.go │ │ │ ├── admin_transaction_handler.go │ │ │ ├── admin_user_handler.go │ │ │ ├── business_handler.go │ │ │ ├── dashboard_handler.go │ │ │ ├── history.handler.go │ │ │ ├── log_handler.go │ │ │ ├── service_discovery.go │ │ │ ├── tag_handler.go │ │ │ ├── trading_handler.go │ │ │ ├── transaction_handler.go │ │ │ └── user_handler.go │ │ ├── middleware │ │ │ ├── auth.go │ │ │ ├── cache.go │ │ │ ├── logging.go │ │ │ └── recover.go │ │ ├── routes.go │ │ └── server.go │ ├── repositories │ │ ├── es │ │ │ ├── business_es.go │ │ │ ├── es.go │ │ │ ├── helper.go │ │ │ ├── index.go │ │ │ ├── tag_es.go │ │ │ └── user_es.go │ │ ├── mongo │ │ │ ├── admin_tag_mongo.go │ │ │ ├── admin_user.go │ │ │ ├── business_mongo.go │ │ │ ├── config.go │ │ │ ├── helper.go │ │ │ ├── lost_password_mongo.go │ │ │ ├── mongo.go │ │ │ ├── tag_mongo.go │ │ │ ├── user_action.go │ │ │ └── user_mongo.go │ │ └── pg │ │ │ ├── account.go │ │ │ ├── balance_limit.go │ │ │ ├── pg.go │ │ │ ├── posting.go │ │ │ └── transaction.go │ ├── service │ │ ├── account.go │ │ ├── admin_tag.go │ │ ├── admin_transaction.go │ │ ├── admin_user.go │ │ ├── balance_limit.go │ │ ├── balancecheck │ │ │ └── balancecheck.go │ │ ├── business.go │ │ ├── dailyemail │ │ │ ├── dailyemail.go │ │ │ └── work.go │ │ ├── lostpassword.go │ │ ├── tag.go │ │ ├── trading.go │ │ ├── transaction.go │ │ ├── user.go │ │ └── user_action.go │ └── types │ │ ├── account.go │ │ ├── admin_tag.go │ │ ├── admin_user.go │ │ ├── balance_limit.go │ │ ├── business.go │ │ ├── form.go │ │ ├── journal.go │ │ ├── lost_password.go │ │ ├── posting.go │ │ ├── tag.go │ │ ├── trading.go │ │ ├── transaction.go │ │ ├── user.go │ │ └── user_action.go ├── migration │ ├── migration.go │ └── user_action_category.go ├── pkg │ ├── bcrypt │ │ └── bcrypt.go │ ├── cookie │ │ └── cookie.go │ ├── e │ │ ├── code.go │ │ └── errors.go │ ├── email │ │ ├── balance.go │ │ ├── email.go │ │ └── transaction.go │ ├── flash │ │ └── flash.go │ ├── helper │ │ ├── tag.go │ │ ├── tag_test.go │ │ ├── trading.go │ │ └── user.go │ ├── ip │ │ └── ip.go │ ├── jsonerror │ │ ├── errorcode.go │ │ └── jsonerror.go │ ├── jwt │ │ ├── jwt.go │ │ └── jwt_test.go │ ├── l │ │ └── logger.go │ ├── log │ │ ├── admin.go │ │ └── user.go │ ├── pagination │ │ └── pagination.go │ ├── passlib │ │ └── passlib.go │ ├── recaptcha │ │ └── recaptcha.go │ ├── template │ │ ├── data.go │ │ ├── functions.go │ │ └── template.go │ ├── util │ │ ├── amount.go │ │ ├── amount_test.go │ │ ├── check_field_diff.go │ │ ├── email.go │ │ ├── email_test.go │ │ ├── id.go │ │ ├── id_test.go │ │ ├── status_checking.go │ │ ├── time.go │ │ └── time_test.go │ ├── validator │ │ ├── password.go │ │ ├── register.go │ │ ├── tag.go │ │ └── validator.go │ └── version │ │ └── version.go └── seed │ ├── data │ ├── admin_tag.json │ ├── admin_user.json │ ├── business.json │ ├── tag.json │ └── user.json │ └── seed.go ├── reflex.dev.conf └── web ├── static ├── css │ ├── main.css │ ├── semantic.min.css │ └── themes │ │ └── default │ │ └── assets │ │ └── fonts │ │ ├── brand-icons.eot │ │ ├── brand-icons.svg │ │ ├── brand-icons.ttf │ │ ├── brand-icons.woff │ │ ├── brand-icons.woff2 │ │ ├── icons.eot │ │ ├── icons.svg │ │ ├── icons.ttf │ │ ├── icons.woff │ │ ├── icons.woff2 │ │ ├── outline-icons.eot │ │ ├── outline-icons.svg │ │ ├── outline-icons.ttf │ │ ├── outline-icons.woff │ │ └── outline-icons.woff2 ├── img │ ├── favicon │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ └── mstile-150x150.png │ ├── ocn-logo.svg │ ├── trading-member-icon.svg │ └── trading-member.svg ├── js │ ├── main.js │ └── semantic.min.js └── site.webmanifest └── template ├── account.html ├── admin ├── accounts.html ├── admin-tags.html ├── business.html ├── dashboard.html ├── history.html ├── log.html ├── login.html ├── transaction.html ├── user-tags.html └── user.html ├── business.html ├── businesses.html ├── dashboard.html ├── email ├── dailyEmail.html ├── thankYou.html └── welcome.html ├── history.html ├── layout ├── base.html ├── errors.html ├── footer.html ├── header.html └── messages.html ├── login.html ├── lost-password.html ├── member-signup.html ├── password-resets.html ├── signup.html └── transaction.html /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: [push, pull_request] 3 | jobs: 4 | 5 | test: 6 | name: Test 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | 11 | - name: Set up Go 12 | uses: actions/setup-go@v5 13 | with: 14 | go-version: 1.19.5 15 | 16 | - name: Test 17 | run: make test 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | .volumes 4 | mccs 5 | 6 | # config files 7 | production.yaml 8 | development.yaml 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019-2020 IC3 Network 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP = mccs 2 | # Get the latest Git tag, or if not available, get the latest commit hash. 3 | GIT_TAG = $(shell if [ "`git describe --tags --abbrev=0 2>/dev/null`" != "" ]; then git describe --tags --abbrev=0; else git log --pretty=format:'%h' -n 1; fi) 4 | # Get the current date and time in UTC in ISO 8601 format. 5 | BUILD_DATE = $(shell TZ=UTC date +%FT%T%z) 6 | # Get the hash of the latest commit. 7 | GIT_COMMIT = $(shell git log --pretty=format:'%H' -n 1) 8 | # Check the status of the Git tree, determining whether it's clean or dirty. 9 | GIT_TREE_STATUS = $(shell if git status | grep -q 'clean'; then echo clean; else echo dirty; fi) 10 | 11 | # Production target for starting the production server. 12 | production: 13 | @echo "============= Starting production server =============" 14 | GIT_TAG=${GIT_TAG} BUILD_DATE=${BUILD_DATE} GIT_COMMIT=${GIT_COMMIT} GIT_TREE_STATUS=${GIT_TREE_STATUS} \ 15 | docker-compose -f docker-compose.production.yml up --build 16 | 17 | # Clean target for removing the application. 18 | clean: 19 | @echo "============= Removing app =============" 20 | rm -f ${APP} 21 | 22 | # Run target for starting the server using the development Docker Compose configuration. 23 | run: 24 | @echo "============= Starting server =============" 25 | docker-compose -f docker-compose.dev.yml up --build 26 | 27 | # Test target for running unit tests on the application. 28 | test: 29 | @echo "============= Running tests =============" 30 | go test ./... 31 | 32 | # Seed target for generating seed data. 33 | seed: 34 | @echo "============= Generating seed data =============" 35 | go run cmd/seed/main.go -config="seed" 36 | 37 | # es-restore target for restoring Elasticsearch data. 38 | es-restore: 39 | @echo "============= Restoring Elasticsearch data =============" 40 | go run cmd/es-restore/main.go -config="seed" 41 | 42 | # pg-setup target for setting up PostgreSQL accounts. 43 | pg-setup: 44 | @echo "============= Setting up PostgreSQL accounts =============" 45 | go run cmd/pg-setup/main.go -config="seed" 46 | -------------------------------------------------------------------------------- /cmd/es-restore/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "github.com/ic3network/mccs-alpha/global" 9 | "github.com/ic3network/mccs-alpha/internal/app/repositories/es" 10 | "github.com/ic3network/mccs-alpha/internal/app/repositories/mongo" 11 | "github.com/ic3network/mccs-alpha/internal/app/types" 12 | "go.mongodb.org/mongo-driver/bson" 13 | ) 14 | 15 | func main() { 16 | global.Init() 17 | restoreUser() 18 | restoreBusiness() 19 | restoreTag() 20 | } 21 | 22 | func restoreUser() { 23 | log.Println("start restoring users") 24 | startTime := time.Now() 25 | 26 | // Don't incluse deleted item. 27 | filter := bson.M{ 28 | "deletedAt": bson.M{"$exists": false}, 29 | } 30 | 31 | cur, err := mongo.DB().Collection("users").Find(context.TODO(), filter) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | counter := 0 37 | for cur.Next(context.TODO()) { 38 | var u types.User 39 | err := cur.Decode(&u) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | // Add the user to elastic search. 44 | { 45 | userID := u.ID.Hex() 46 | uRecord := types.UserESRecord{ 47 | UserID: userID, 48 | FirstName: u.FirstName, 49 | LastName: u.LastName, 50 | Email: u.Email, 51 | } 52 | _, err = es.Client().Index(). 53 | Index("users"). 54 | Id(userID). 55 | BodyJson(uRecord). 56 | Do(context.Background()) 57 | } 58 | counter++ 59 | } 60 | if err := cur.Err(); err != nil { 61 | log.Fatal(err) 62 | } 63 | cur.Close(context.TODO()) 64 | 65 | log.Printf("count %v\n", counter) 66 | log.Printf("took %v\n\n", time.Now().Sub(startTime)) 67 | } 68 | 69 | func restoreBusiness() { 70 | log.Println("start restoring businesses") 71 | startTime := time.Now() 72 | 73 | // Don't incluse deleted item. 74 | filter := bson.M{ 75 | "deletedAt": bson.M{"$exists": false}, 76 | } 77 | 78 | cur, err := mongo.DB().Collection("businesses").Find(context.TODO(), filter) 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | counter := 0 83 | for cur.Next(context.TODO()) { 84 | var b types.Business 85 | err := cur.Decode(&b) 86 | if err != nil { 87 | log.Fatal(err) 88 | } 89 | // Add the business to elastic search. 90 | { 91 | businessID := b.ID.Hex() 92 | uRecord := types.BusinessESRecord{ 93 | BusinessID: businessID, 94 | BusinessName: b.BusinessName, 95 | Offers: b.Offers, 96 | Wants: b.Wants, 97 | LocationCity: b.LocationCity, 98 | LocationCountry: b.LocationCountry, 99 | Status: b.Status, 100 | AdminTags: b.AdminTags, 101 | } 102 | _, err = es.Client().Index(). 103 | Index("businesses"). 104 | Id(businessID). 105 | BodyJson(uRecord). 106 | Do(context.Background()) 107 | } 108 | counter++ 109 | } 110 | if err := cur.Err(); err != nil { 111 | log.Fatal(err) 112 | } 113 | cur.Close(context.TODO()) 114 | 115 | log.Printf("count %v\n", counter) 116 | log.Printf("took %v\n\n", time.Now().Sub(startTime)) 117 | } 118 | 119 | func restoreTag() { 120 | log.Println("start restoring tag") 121 | startTime := time.Now() 122 | 123 | // Don't incluse deleted item. 124 | filter := bson.M{ 125 | "deletedAt": bson.M{"$exists": false}, 126 | } 127 | 128 | cur, err := mongo.DB().Collection("tags").Find(context.TODO(), filter) 129 | if err != nil { 130 | log.Fatal(err) 131 | } 132 | counter := 0 133 | for cur.Next(context.TODO()) { 134 | var t types.Tag 135 | err := cur.Decode(&t) 136 | if err != nil { 137 | log.Fatal(err) 138 | } 139 | // Add the tag to elastic search. 140 | { 141 | tagID := t.ID.Hex() 142 | uRecord := types.TagESRecord{ 143 | TagID: tagID, 144 | Name: t.Name, 145 | OfferAddedAt: t.OfferAddedAt, 146 | WantAddedAt: t.WantAddedAt, 147 | } 148 | _, err = es.Client().Index(). 149 | Index("tags"). 150 | Id(tagID). 151 | BodyJson(uRecord). 152 | Do(context.Background()) 153 | } 154 | counter++ 155 | } 156 | if err := cur.Err(); err != nil { 157 | log.Fatal(err) 158 | } 159 | cur.Close(context.TODO()) 160 | 161 | log.Printf("count %v\n", counter) 162 | log.Printf("took %v\n\n", time.Now().Sub(startTime)) 163 | } 164 | -------------------------------------------------------------------------------- /cmd/mccs-alpha/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/ic3network/mccs-alpha/global" 9 | "github.com/ic3network/mccs-alpha/internal/app/http" 10 | "github.com/ic3network/mccs-alpha/internal/app/service/balancecheck" 11 | "github.com/ic3network/mccs-alpha/internal/app/service/dailyemail" 12 | "github.com/ic3network/mccs-alpha/internal/migration" 13 | "github.com/ic3network/mccs-alpha/internal/pkg/l" 14 | "github.com/ic3network/mccs-alpha/internal/pkg/version" 15 | "github.com/robfig/cron" 16 | "github.com/spf13/viper" 17 | ) 18 | 19 | func init() { 20 | global.Init() 21 | } 22 | 23 | func main() { 24 | // Flushes log buffer, if any. 25 | defer l.Logger.Sync() 26 | 27 | if *global.ShowVersionInfo { 28 | versionInfo := version.Get() 29 | marshalled, err := json.MarshalIndent(&versionInfo, "", " ") 30 | if err != nil { 31 | fmt.Printf("%v\n", err) 32 | os.Exit(1) 33 | } 34 | fmt.Println(string(marshalled)) 35 | return 36 | } 37 | 38 | go ServeBackGround() 39 | go RunMigration() 40 | 41 | http.AppServer.Run(viper.GetString("port")) 42 | } 43 | 44 | // ServeBackGround performs the background activities. 45 | func ServeBackGround() { 46 | c := cron.New() 47 | viper.SetDefault("daily_email_schedule", "0 0 7 * * *") 48 | c.AddFunc(viper.GetString("daily_email_schedule"), func() { 49 | l.Logger.Info("[ServeBackGround] Running daily email schedule. \n") 50 | dailyemail.Run() 51 | }) 52 | viper.SetDefault("balance_check_schedule", "0 0 * * * *") 53 | c.AddFunc(viper.GetString("balance_check_schedule"), func() { 54 | l.Logger.Info("[ServeBackGround] Running balance check schedule. \n") 55 | balancecheck.Run() 56 | }) 57 | c.Start() 58 | } 59 | 60 | func RunMigration() { 61 | // Runs at 2019-08-20 62 | migration.SetUserActionCategory() 63 | } 64 | -------------------------------------------------------------------------------- /cmd/pg-setup/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/ic3network/mccs-alpha/global" 5 | 6 | "context" 7 | "log" 8 | "time" 9 | 10 | "github.com/ic3network/mccs-alpha/internal/app/repositories/mongo" 11 | "github.com/ic3network/mccs-alpha/internal/app/service" 12 | "github.com/ic3network/mccs-alpha/internal/app/types" 13 | "go.mongodb.org/mongo-driver/bson" 14 | ) 15 | 16 | func main() { 17 | global.Init() 18 | setUpAccount() 19 | } 20 | 21 | // setUpAccount reads the businesses from MongoDB and build up the accounts in PostgreSQL. 22 | func setUpAccount() { 23 | log.Println("start setting up accounts in PostgreSQL") 24 | startTime := time.Now() 25 | ctx := context.Background() 26 | 27 | filter := bson.M{ 28 | "deletedAt": bson.M{"$exists": false}, 29 | } 30 | cur, err := mongo.DB().Collection("businesses").Find(ctx, filter) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | counter := 0 36 | for cur.Next(ctx) { 37 | var b types.Business 38 | err := cur.Decode(&b) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | // Create account from business. 43 | err = service.Account.Create(b.ID.Hex()) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | counter++ 48 | } 49 | if err := cur.Err(); err != nil { 50 | log.Fatal(err) 51 | } 52 | cur.Close(ctx) 53 | 54 | log.Printf("count %v\n", counter) 55 | log.Printf("took %v\n\n", time.Now().Sub(startTime)) 56 | } 57 | -------------------------------------------------------------------------------- /cmd/seed/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/ic3network/mccs-alpha/global" 5 | "github.com/ic3network/mccs-alpha/internal/seed" 6 | ) 7 | 8 | func main() { 9 | global.Init() 10 | seed.LoadData() 11 | seed.Run() 12 | } 13 | -------------------------------------------------------------------------------- /configs/revive.toml: -------------------------------------------------------------------------------- 1 | ignoreGeneratedHeader = false 2 | severity = "warning" 3 | confidence = 0.8 4 | errorCode = 0 5 | warningCode = 0 6 | 7 | [rule.blank-imports] 8 | [rule.context-as-argument] 9 | [rule.context-keys-type] 10 | [rule.dot-imports] 11 | [rule.error-return] 12 | [rule.error-strings] 13 | [rule.error-naming] 14 | [rule.exported] 15 | [rule.if-return] 16 | [rule.increment-decrement] 17 | [rule.var-naming] 18 | [rule.var-declaration] 19 | [rule.package-comments] 20 | [rule.range] 21 | [rule.receiver-naming] 22 | [rule.time-naming] 23 | [rule.unexported-return] 24 | [rule.indent-error-flow] 25 | [rule.errorf] 26 | [rule.empty-block] 27 | [rule.superfluous-else] 28 | [rule.unused-parameter] 29 | [rule.unreachable-code] 30 | [rule.redefines-builtin-id] 31 | -------------------------------------------------------------------------------- /configs/seed.yaml: -------------------------------------------------------------------------------- 1 | # To create a development.yaml file, please replace xxx with actual values. 2 | # Also remember to check the docker-compose file to update the relative url links. 3 | 4 | # change "seed" to "development" or "production" 5 | env: seed 6 | url: http://localhost:8080 7 | port: 8080 8 | reset_password_timeout: 60 9 | page_size: 10 10 | tags_limit: 10 11 | login_attempts_limit: 3 12 | login_attempts_timeout: 900 13 | email_from: MCCS 14 | daily_email_schedule: "0 0 7 * * *" 15 | balance_check_schedule: "0 0 * * * *" 16 | concurrency_num: 3 17 | receive_trade_contact_emails: false 18 | receive_signup_notifications: false 19 | 20 | transaction: 21 | maxNegBal: 0 22 | maxPosBal: 500 23 | 24 | psql: 25 | # change "localhost" to "postgres" when you are creating a development.yaml / production.yaml. 26 | host: localhost 27 | port: 5432 28 | user: postgres 29 | password: 30 | db: mccs 31 | 32 | mongo: 33 | # change "localhost" to "mongo" when you are creating a development.yaml / production.yaml. 34 | url: mongodb://localhost:27017 35 | database: mccs 36 | 37 | es: 38 | # change "localhost" to "es01" when you are creating a development.yaml / production.yaml. 39 | url: http://localhost:9200 40 | 41 | jwt: 42 | private_key: | 43 | -----BEGIN RSA PRIVATE KEY----- 44 | xxx 45 | -----END RSA PRIVATE KEY----- 46 | public_key: | 47 | -----BEGIN PUBLIC KEY----- 48 | xxx 49 | -----END PUBLIC KEY----- 50 | 51 | sendgrid: 52 | key: xxx 53 | sender_email: xxx 54 | 55 | # Use reCAPTCHA v2, not v3 56 | recaptcha: 57 | site_key: xxx 58 | secret_key: xxx 59 | -------------------------------------------------------------------------------- /configs/test.yaml: -------------------------------------------------------------------------------- 1 | env: test 2 | url: http://localhost:8080 3 | port: 8080 4 | reset_password_timeout: 60 5 | page_size: 10 6 | tags_limit: 10 7 | login_attempts_limit: 3 8 | login_attempts_timeout: 900 9 | email_from: MCCS 10 | daily_email_schedule: "0 0 7 * * *" 11 | balance_check_schedule: "0 0 * * * *" 12 | concurrency_num: 3 13 | receive_trade_contact_emails: true 14 | receive_signup_notifications: true 15 | 16 | transaction: 17 | maxNegBal: 0 18 | maxPosBal: 500 19 | 20 | psql: 21 | host: postgres 22 | port: 5432 23 | user: postgres 24 | password: 25 | db: mccs 26 | 27 | mongo: 28 | url: mongodb://mongo:27017 29 | database: mccs 30 | 31 | es: 32 | url: http://es01:9200 33 | 34 | jwt: 35 | private_key: | 36 | -----BEGIN RSA PRIVATE KEY----- 37 | xxx 38 | -----END RSA PRIVATE KEY----- 39 | public_key: | 40 | -----BEGIN PUBLIC KEY----- 41 | xxx 42 | -----END PUBLIC KEY----- 43 | 44 | sendgrid: 45 | key: xxx 46 | sender_email: xxx 47 | 48 | recaptcha: 49 | # For reCAPTCHA v2, use the following test keys. 50 | # You will always get No CAPTCHA and all verification requests will pass. 51 | # The reCAPTCHA widget will show a warning message to ensure it's not used 52 | # for production traffic. 53 | site_key: 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI 54 | secret_key: 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe 55 | 56 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | web: 4 | container_name: mccs 5 | build: 6 | context: . 7 | dockerfile: dockerfile.dev 8 | volumes: 9 | - ./:/usr/src/app 10 | ports: 11 | - 8080:8080 12 | depends_on: 13 | - mongo 14 | - es01 15 | 16 | postgres: 17 | container_name: postgres 18 | image: postgres:11.4 19 | ports: 20 | - 5432:5432 21 | environment: 22 | - POSTGRES_USER=postgres 23 | - POSTGRES_DB=mccs 24 | volumes: 25 | - postgresql:/var/lib/postgresql/data 26 | 27 | mongo: 28 | container_name: mongo 29 | image: mongo:4.0.10 30 | ports: 31 | - 27017:27017 32 | volumes: 33 | - mongodb:/data/db 34 | 35 | es01: 36 | container_name: es01 37 | image: docker.elastic.co/elasticsearch/elasticsearch:7.17.5 38 | environment: 39 | - node.name=es01 40 | - discovery.type=single-node 41 | # JVM memory: initial and max set to 512MB. 42 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 43 | ports: 44 | - 9200:9200 45 | volumes: 46 | - esdata01:/usr/share/elasticsearch/data 47 | healthcheck: 48 | test: ["CMD-SHELL", "curl --silent --fail localhost:9200/_cluster/health || exit 1"] 49 | interval: 30s 50 | timeout: 30s 51 | retries: 3 52 | 53 | kibana: 54 | container_name: kibana 55 | image: docker.elastic.co/kibana/kibana:7.1.1 56 | environment: 57 | - ELASTICSEARCH_HOSTS=http://es01:9200 58 | ports: 59 | - 5601:5601 60 | depends_on: 61 | - es01 62 | 63 | # Named Volumes Configuration. 64 | volumes: 65 | postgresql: 66 | mongodb: 67 | esdata01: 68 | -------------------------------------------------------------------------------- /docker-compose.production.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | web: 4 | container_name: mccs 5 | build: 6 | context: . 7 | dockerfile: dockerfile.production 8 | args: 9 | - GIT_TAG=$GIT_TAG 10 | - BUILD_DATE=$BUILD_DATE 11 | - GIT_COMMIT=$GIT_COMMIT 12 | - GIT_TREE_STATUS=$GIT_TREE_STATUS 13 | restart: always 14 | volumes: 15 | - ./:/usr/src/app 16 | ports: 17 | - 8080:8080 18 | depends_on: 19 | - mongo 20 | - es01 21 | 22 | postgres: 23 | container_name: postgres 24 | image: postgres:11.4 25 | restart: always 26 | ports: 27 | - 5432:5432 28 | environment: 29 | - POSTGRES_USER=postgres 30 | - POSTGRES_DB=mccs 31 | volumes: 32 | - postgresql:/var/lib/postgresql/data 33 | 34 | mongo: 35 | container_name: mongo 36 | image: mongo:4.0.10 37 | restart: always 38 | ports: 39 | - 27017:27017 40 | volumes: 41 | - mongodb:/data/db 42 | - restore:/data/restore 43 | 44 | es01: 45 | container_name: es01 46 | image: docker.elastic.co/elasticsearch/elasticsearch:7.17.5 47 | restart: always 48 | environment: 49 | - node.name=es01 50 | - discovery.type=single-node 51 | # JVM memory: initial and max set to 512MB. 52 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 53 | ports: 54 | - 9200:9200 55 | volumes: 56 | - esdata01:/usr/share/elasticsearch/data 57 | healthcheck: 58 | test: ["CMD-SHELL", "curl --silent --fail localhost:9200/_cluster/health || exit 1"] 59 | interval: 30s 60 | timeout: 30s 61 | retries: 3 62 | 63 | kibana: 64 | container_name: kibana 65 | image: docker.elastic.co/kibana/kibana:7.1.1 66 | restart: always 67 | environment: 68 | - ELASTICSEARCH_HOSTS=http://es01:9200 69 | ports: 70 | - 5601:5601 71 | depends_on: 72 | - es01 73 | 74 | volumes: 75 | mongodb: 76 | driver: local 77 | driver_opts: 78 | type: 'none' 79 | o: 'bind' 80 | device: 'mnt/mccs_data/mongo' 81 | esdata01: 82 | driver: local 83 | driver_opts: 84 | type: 'none' 85 | o: 'bind' 86 | device: 'mnt/mccs_data/es' 87 | restore: 88 | driver: local 89 | driver_opts: 90 | type: 'none' 91 | o: 'bind' 92 | device: 'mnt/mccs_data/restore' 93 | postgresql: 94 | driver: local 95 | driver_opts: 96 | type: 'none' 97 | o: 'bind' 98 | device: 'mnt/mccs_data/postgres' 99 | -------------------------------------------------------------------------------- /dockerfile.dev: -------------------------------------------------------------------------------- 1 | # Base image from which we are building. 2 | FROM golang:1.19.5-alpine 3 | 4 | WORKDIR /usr/src/app 5 | 6 | COPY go.mod go.sum ./ 7 | 8 | # Download the dependencies listed in the go.mod file. 9 | RUN go mod download 10 | 11 | # Install reflex, a tool for hot reloading of Go applications. 12 | RUN go install github.com/cespare/reflex@latest 13 | 14 | # The CMD instruction provides defaults for executing the container. 15 | CMD ["reflex", "-c", "./reflex.dev.conf"] 16 | -------------------------------------------------------------------------------- /dockerfile.production: -------------------------------------------------------------------------------- 1 | FROM golang:1.19.5-alpine AS builder 2 | 3 | WORKDIR /temp 4 | 5 | COPY go.mod go.sum ./ 6 | 7 | # Download the dependencies listed in the go.mod file. 8 | RUN go mod download 9 | 10 | COPY . . 11 | 12 | # Build with version information 13 | ARG APP=mccs 14 | ARG VERSION_DIR="github.com/ic3network/mccs-alpha/internal/pkg/version" 15 | ARG GIT_TAG 16 | ARG BUILD_DATE 17 | ARG GIT_COMMIT 18 | ARG GIT_TREE_STATUS 19 | ARG ldflags="-w -X $VERSION_DIR.gitTag=$GIT_TAG -X $VERSION_DIR.buildDate=$BUILD_DATE -X $VERSION_DIR.gitCommit=$GIT_COMMIT -X $VERSION_DIR.gitTreeState=$GIT_TREE_STATUS" 20 | 21 | # * CGO_ENABLED=0 to build a statically-linked executable 22 | RUN CGO_ENABLED=0 GOOS=linux go build -a -v -ldflags "$ldflags" -o "$APP" ./cmd/mccs-alpha 23 | 24 | ######## Start a new stage from scratch ####### 25 | FROM alpine:latest 26 | RUN apk --no-cache --update upgrade && apk --no-cache add ca-certificates 27 | 28 | WORKDIR /app 29 | 30 | COPY --from=builder /temp . 31 | COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 32 | 33 | EXPOSE 8080 34 | 35 | # Run the executable 36 | ENTRYPOINT ["./mccs", "-config=production"] 37 | CMD [] 38 | -------------------------------------------------------------------------------- /global/app.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | func init() { 11 | App.RootDir = "." 12 | if !viper.InConfig("url") { 13 | App.RootDir = inferRootDir() 14 | } 15 | } 16 | 17 | func inferRootDir() string { 18 | cwd, err := os.Getwd() 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | var infer func(d string) string 24 | infer = func(d string) string { 25 | if Exist(d + "/configs") { 26 | return d 27 | } 28 | 29 | return infer(filepath.Dir(d)) 30 | } 31 | 32 | return infer(cwd) 33 | } 34 | 35 | var App = &app{} 36 | 37 | type app struct { 38 | RootDir string 39 | } 40 | 41 | func Exist(filename string) bool { 42 | _, err := os.Stat(filename) 43 | return err == nil || os.IsExist(err) 44 | } 45 | -------------------------------------------------------------------------------- /global/constant/business.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | // Business status 4 | var Business = struct { 5 | Pending string 6 | Accepted string 7 | Rejected string 8 | }{ 9 | Pending: "pending", 10 | Accepted: "accepted", 11 | Rejected: "rejected", 12 | } 13 | -------------------------------------------------------------------------------- /global/constant/constant.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | var ( 4 | ALL = "all" 5 | 6 | OFFERS = "offers" 7 | WANTS = "wants" 8 | ) 9 | -------------------------------------------------------------------------------- /global/constant/date.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | import "time" 4 | 5 | var Date = struct { 6 | DefaultFrom time.Time 7 | DefaultTo time.Time 8 | }{ 9 | DefaultFrom: time.Date(2000, 1, 1, 00, 00, 0, 0, time.UTC), 10 | DefaultTo: time.Date(2100, 1, 1, 00, 00, 0, 0, time.UTC), 11 | } 12 | -------------------------------------------------------------------------------- /global/constant/flash.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | var Flash = struct { 4 | Success string 5 | Info string 6 | }{ 7 | Success: "success", 8 | Info: "info", 9 | } 10 | -------------------------------------------------------------------------------- /global/constant/journal.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | var Journal = struct { 4 | Transfer string 5 | }{ 6 | Transfer: "Transfer", 7 | } 8 | -------------------------------------------------------------------------------- /global/constant/trading.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | // Trading Status decides whether a business can perform transactions (already in accepted status). 4 | var Trading = struct { 5 | Pending string 6 | Accepted string 7 | Rejected string 8 | }{ 9 | Pending: "tradingPending", 10 | Accepted: "tradingAccepted", 11 | Rejected: "tradingRejected", 12 | } 13 | -------------------------------------------------------------------------------- /global/constant/transaction.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | // Transaction Status 4 | var Transaction = struct { 5 | Initiated string 6 | Completed string 7 | Cancelled string 8 | }{ 9 | Initiated: "transactionInitiated", 10 | Completed: "transactionCompleted", 11 | Cancelled: "transactionCancelled", 12 | } 13 | -------------------------------------------------------------------------------- /global/init.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/fsnotify/fsnotify" 12 | "github.com/ic3network/mccs-alpha/internal/pkg/l" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | var ( 17 | once = new(sync.Once) 18 | configName = flag.String( 19 | "config", 20 | "development", 21 | "config file name, default is development", 22 | ) 23 | ShowVersionInfo = flag.Bool("v", false, "show version info or not") 24 | ) 25 | 26 | func Init() { 27 | once.Do(func() { 28 | isTest := false 29 | for _, arg := range os.Args { 30 | if strings.Contains(arg, "test") { 31 | isTest = true 32 | break 33 | } 34 | } 35 | 36 | if !isTest && !flag.Parsed() { 37 | flag.Parse() 38 | } 39 | 40 | if err := initConfig(); err != nil { 41 | panic(fmt.Errorf("initconfig failed: %s", err)) 42 | } 43 | watchConfig() 44 | 45 | l.Init(viper.GetString("env")) 46 | }) 47 | } 48 | 49 | func initConfig() error { 50 | if err := setConfigNameAndType(); err != nil { 51 | return fmt.Errorf("setting config name and type failed: %w", err) 52 | } 53 | 54 | addConfigPaths() 55 | 56 | if err := setupEnvironmentVariables(); err != nil { 57 | return fmt.Errorf("setting up environment variables failed: %w", err) 58 | } 59 | 60 | if err := viper.ReadInConfig(); err != nil { 61 | return fmt.Errorf("reading config failed: %w", err) 62 | } 63 | 64 | return nil 65 | } 66 | 67 | func setConfigNameAndType() error { 68 | viper.SetConfigName(*configName) 69 | viper.SetConfigType("yaml") 70 | return nil 71 | } 72 | 73 | func addConfigPaths() { 74 | viper.AddConfigPath("configs") 75 | viper.AddConfigPath(App.RootDir + "/configs") 76 | } 77 | 78 | func setupEnvironmentVariables() error { 79 | viper.AutomaticEnv() 80 | replacer := strings.NewReplacer(".", "_") 81 | viper.SetEnvKeyReplacer(replacer) 82 | return nil 83 | } 84 | 85 | func watchConfig() { 86 | viper.WatchConfig() 87 | viper.OnConfigChange(func(e fsnotify.Event) { 88 | log.Printf("Config file changed: %s", e.Name) 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ic3network/mccs-alpha 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.7.0 7 | github.com/gofrs/uuid/v5 v5.0.0 8 | github.com/golang-jwt/jwt/v5 v5.2.0 9 | github.com/gorilla/mux v1.8.1 10 | github.com/jinzhu/gorm v1.9.16 11 | github.com/jinzhu/now v1.1.5 12 | github.com/olivere/elastic/v7 v7.0.32 13 | github.com/pkg/errors v0.9.1 14 | github.com/robfig/cron v1.2.0 15 | github.com/segmentio/ksuid v1.0.4 16 | github.com/sendgrid/sendgrid-go v3.5.0+incompatible 17 | github.com/shirou/gopsutil v3.21.11+incompatible 18 | github.com/spf13/viper v1.18.2 19 | github.com/stretchr/testify v1.8.4 20 | github.com/unrolled/render v1.6.1 21 | go.mongodb.org/mongo-driver v1.13.1 22 | go.uber.org/zap v1.26.0 23 | golang.org/x/crypto v0.19.0 24 | gopkg.in/oleiade/reflections.v1 v1.0.0 25 | ) 26 | 27 | require ( 28 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 29 | github.com/go-ole/go-ole v1.2.6 // indirect 30 | github.com/golang/snappy v0.0.1 // indirect 31 | github.com/hashicorp/hcl v1.0.0 // indirect 32 | github.com/jinzhu/inflection v1.0.0 // indirect 33 | github.com/josharian/intern v1.0.0 // indirect 34 | github.com/klauspost/compress v1.17.0 // indirect 35 | github.com/lib/pq v1.1.1 // indirect 36 | github.com/magiconair/properties v1.8.7 // indirect 37 | github.com/mailru/easyjson v0.7.7 // indirect 38 | github.com/mitchellh/mapstructure v1.5.0 // indirect 39 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect 40 | github.com/oleiade/reflections v1.0.0 // indirect 41 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 42 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 43 | github.com/sagikazarmark/locafero v0.4.0 // indirect 44 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 45 | github.com/sendgrid/rest v2.4.1+incompatible // indirect 46 | github.com/sourcegraph/conc v0.3.0 // indirect 47 | github.com/spf13/afero v1.11.0 // indirect 48 | github.com/spf13/cast v1.6.0 // indirect 49 | github.com/spf13/pflag v1.0.5 // indirect 50 | github.com/subosito/gotenv v1.6.0 // indirect 51 | github.com/tklauser/go-sysconf v0.3.12 // indirect 52 | github.com/tklauser/numcpus v0.6.1 // indirect 53 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect 54 | github.com/xdg-go/scram v1.1.2 // indirect 55 | github.com/xdg-go/stringprep v1.0.4 // indirect 56 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect 57 | github.com/yusufpapurcu/wmi v1.2.3 // indirect 58 | go.uber.org/multierr v1.10.0 // indirect 59 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 60 | golang.org/x/sync v0.5.0 // indirect 61 | golang.org/x/sys v0.17.0 // indirect 62 | golang.org/x/text v0.14.0 // indirect 63 | gopkg.in/ini.v1 v1.67.0 // indirect 64 | gopkg.in/yaml.v3 v3.0.1 // indirect 65 | ) 66 | -------------------------------------------------------------------------------- /internal/app/http/controller/admin_history_handler.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "sync" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/ic3network/mccs-alpha/internal/app/service" 10 | "github.com/ic3network/mccs-alpha/internal/app/types" 11 | "github.com/ic3network/mccs-alpha/internal/pkg/l" 12 | "github.com/ic3network/mccs-alpha/internal/pkg/template" 13 | "github.com/ic3network/mccs-alpha/internal/pkg/util" 14 | "go.uber.org/zap" 15 | ) 16 | 17 | type adminHistoryHandler struct { 18 | once *sync.Once 19 | } 20 | 21 | var AdminHistoryHandler = newAdminHistoryHandler() 22 | 23 | func newAdminHistoryHandler() *adminHistoryHandler { 24 | return &adminHistoryHandler{ 25 | once: new(sync.Once), 26 | } 27 | } 28 | 29 | func (h *adminHistoryHandler) RegisterRoutes( 30 | public *mux.Router, 31 | private *mux.Router, 32 | adminPublic *mux.Router, 33 | adminPrivate *mux.Router, 34 | ) { 35 | h.once.Do(func() { 36 | adminPrivate.Path("/history/{id}"). 37 | HandlerFunc(h.historyPage()). 38 | Methods("GET") 39 | }) 40 | } 41 | 42 | func (h *adminHistoryHandler) historyPage() func(http.ResponseWriter, *http.Request) { 43 | t := template.NewView("admin/history") 44 | type formData struct { 45 | DateFrom string 46 | DateTo string 47 | Page int 48 | } 49 | type response struct { 50 | FormData formData 51 | TotalPages int 52 | Balance float64 53 | Transactions []*types.Transaction 54 | Email string 55 | BusinessID string 56 | } 57 | return func(w http.ResponseWriter, r *http.Request) { 58 | vars := mux.Vars(r) 59 | bID := vars["id"] 60 | q := r.URL.Query() 61 | 62 | page, err := strconv.Atoi(q.Get("page")) 63 | if err != nil { 64 | l.Logger.Error( 65 | "controller.AdminHistory.HistoryPage failed", 66 | zap.Error(err), 67 | ) 68 | t.Error(w, r, nil, err) 69 | return 70 | } 71 | 72 | f := formData{ 73 | DateFrom: q.Get("date-from"), 74 | DateTo: q.Get("date-to"), 75 | Page: page, 76 | } 77 | user, err := UserHandler.FindByBusinessID(bID) 78 | if err != nil { 79 | l.Logger.Error( 80 | "controller.AdminHistory.HistoryPage failed", 81 | zap.Error(err), 82 | ) 83 | t.Error(w, r, nil, err) 84 | return 85 | } 86 | res := response{FormData: f, BusinessID: bID, Email: user.Email} 87 | 88 | // Get the account balance. 89 | account, err := service.Account.FindByBusinessID(bID) 90 | if err != nil { 91 | l.Logger.Error( 92 | "controller.AdminHistory.HistoryPage failed", 93 | zap.Error(err), 94 | ) 95 | t.Error(w, r, nil, err) 96 | return 97 | } 98 | res.Balance = account.Balance 99 | 100 | // Get the recent transactions. 101 | transactions, totalPages, err := service.Transaction.FindInRange( 102 | account.ID, 103 | util.ParseTime(f.DateFrom), 104 | util.ParseTime(f.DateTo), 105 | page, 106 | ) 107 | if err != nil { 108 | l.Logger.Error( 109 | "controller.AdminHistory.HistoryPage failed", 110 | zap.Error(err), 111 | ) 112 | t.Error(w, r, nil, err) 113 | return 114 | } 115 | res.Transactions = transactions 116 | res.TotalPages = totalPages 117 | 118 | t.Render(w, r, res, nil) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /internal/app/http/controller/dashboard_handler.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | "time" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/ic3network/mccs-alpha/internal/app/service" 10 | "github.com/ic3network/mccs-alpha/internal/app/types" 11 | "github.com/ic3network/mccs-alpha/internal/pkg/helper" 12 | "github.com/ic3network/mccs-alpha/internal/pkg/l" 13 | "github.com/ic3network/mccs-alpha/internal/pkg/template" 14 | "go.uber.org/zap" 15 | ) 16 | 17 | type dashBoardHandler struct { 18 | once *sync.Once 19 | } 20 | 21 | var DashBoardHandler = newDashBoardHandler() 22 | 23 | func newDashBoardHandler() *dashBoardHandler { 24 | return &dashBoardHandler{ 25 | once: new(sync.Once), 26 | } 27 | } 28 | 29 | func (d *dashBoardHandler) RegisterRoutes( 30 | public *mux.Router, 31 | private *mux.Router, 32 | adminPublic *mux.Router, 33 | adminPrivate *mux.Router, 34 | ) { 35 | d.once.Do(func() { 36 | private.Path("/").HandlerFunc(d.dashboardPage()).Methods("GET") 37 | }) 38 | } 39 | 40 | func (d *dashBoardHandler) dashboardPage() func(http.ResponseWriter, *http.Request) { 41 | t := template.NewView("dashboard") 42 | type response struct { 43 | User *types.User 44 | Business *types.Business 45 | MatchedOffers map[string][]string 46 | MatchedWants map[string][]string 47 | Balance float64 48 | } 49 | return func(w http.ResponseWriter, r *http.Request) { 50 | user, err := UserHandler.FindByID(r.Header.Get("userID")) 51 | 52 | if err != nil { 53 | l.Logger.Error("DashboardPage failed", zap.Error(err)) 54 | t.Error(w, r, nil, err) 55 | return 56 | } 57 | 58 | business, err := service.Business.FindByID(user.CompanyID) 59 | if err != nil { 60 | l.Logger.Error("DashboardPage failed", zap.Error(err)) 61 | t.Error(w, r, nil, err) 62 | return 63 | } 64 | 65 | lastLoginDate := time.Time{} 66 | if user.ShowRecentMatchedTags { 67 | lastLoginDate = user.LastLoginDate 68 | } 69 | 70 | matchedOffers, err := service.Tag.MatchOffers( 71 | helper.GetTagNames(business.Offers), 72 | lastLoginDate, 73 | ) 74 | if err != nil { 75 | l.Logger.Error("DashboardPage failed", zap.Error(err)) 76 | t.Error(w, r, nil, err) 77 | return 78 | } 79 | matchedWants, err := service.Tag.MatchWants( 80 | helper.GetTagNames(business.Wants), 81 | lastLoginDate, 82 | ) 83 | if err != nil { 84 | l.Logger.Error("DashboardPage failed", zap.Error(err)) 85 | t.Error(w, r, nil, err) 86 | return 87 | } 88 | 89 | res := response{ 90 | User: user, 91 | Business: business, 92 | MatchedOffers: matchedOffers, 93 | MatchedWants: matchedWants, 94 | } 95 | 96 | // Get the account balance. 97 | account, err := service.Account.FindByBusinessID(user.CompanyID.Hex()) 98 | if err != nil { 99 | l.Logger.Error("DashboardPage failed", zap.Error(err)) 100 | t.Error(w, r, nil, err) 101 | return 102 | } 103 | res.Balance = account.Balance 104 | 105 | t.Render(w, r, res, nil) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /internal/app/http/controller/history.handler.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "sync" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/ic3network/mccs-alpha/global/constant" 10 | "github.com/ic3network/mccs-alpha/internal/app/service" 11 | "github.com/ic3network/mccs-alpha/internal/app/types" 12 | "github.com/ic3network/mccs-alpha/internal/pkg/l" 13 | "github.com/ic3network/mccs-alpha/internal/pkg/template" 14 | "github.com/ic3network/mccs-alpha/internal/pkg/util" 15 | "go.uber.org/zap" 16 | ) 17 | 18 | type historyHandler struct { 19 | once *sync.Once 20 | } 21 | 22 | var HistoryHandler = newHistoryHandler() 23 | 24 | func newHistoryHandler() *historyHandler { 25 | return &historyHandler{ 26 | once: new(sync.Once), 27 | } 28 | } 29 | 30 | func (h *historyHandler) RegisterRoutes( 31 | public *mux.Router, 32 | private *mux.Router, 33 | adminPublic *mux.Router, 34 | adminPrivate *mux.Router, 35 | ) { 36 | h.once.Do(func() { 37 | private.Path("/history").HandlerFunc(h.historyPage()).Methods("GET") 38 | private.Path("/history/search"). 39 | HandlerFunc(h.searchHistory()). 40 | Methods("GET") 41 | }) 42 | } 43 | 44 | func (h *historyHandler) historyPage() func(http.ResponseWriter, *http.Request) { 45 | t := template.NewView("history") 46 | return func(w http.ResponseWriter, r *http.Request) { 47 | // Only allow access to History screens for users with trading-accepted status 48 | business, _ := BusinessHandler.FindByUserID(r.Header.Get("userID")) 49 | if business.Status != constant.Trading.Accepted { 50 | http.Redirect(w, r, "/", http.StatusFound) 51 | } 52 | t.Render(w, r, nil, nil) 53 | } 54 | } 55 | 56 | func (h *historyHandler) searchHistory() func(http.ResponseWriter, *http.Request) { 57 | t := template.NewView("history") 58 | type formData struct { 59 | DateFrom string 60 | DateTo string 61 | Page int 62 | } 63 | type response struct { 64 | FormData formData 65 | TotalPages int 66 | Balance float64 67 | Transactions []*types.Transaction 68 | } 69 | return func(w http.ResponseWriter, r *http.Request) { 70 | q := r.URL.Query() 71 | 72 | page, err := strconv.Atoi(q.Get("page")) 73 | if err != nil { 74 | l.Logger.Error( 75 | "controller.History.HistoryPage failed", 76 | zap.Error(err), 77 | ) 78 | t.Error(w, r, nil, err) 79 | return 80 | } 81 | 82 | f := formData{ 83 | DateFrom: q.Get("date-from"), 84 | DateTo: q.Get("date-to"), 85 | Page: page, 86 | } 87 | res := response{FormData: f} 88 | 89 | user, err := UserHandler.FindByID(r.Header.Get("userID")) 90 | if err != nil { 91 | l.Logger.Error( 92 | "controller.History.HistoryPage failed", 93 | zap.Error(err), 94 | ) 95 | t.Error(w, r, nil, err) 96 | return 97 | } 98 | 99 | // Get the account balance. 100 | account, err := service.Account.FindByBusinessID(user.CompanyID.Hex()) 101 | if err != nil { 102 | l.Logger.Error( 103 | "controller.History.HistoryPage failed", 104 | zap.Error(err), 105 | ) 106 | t.Error(w, r, nil, err) 107 | return 108 | } 109 | res.Balance = account.Balance 110 | 111 | // Get the recent transactions. 112 | transactions, totalPages, err := service.Transaction.FindInRange( 113 | account.ID, 114 | util.ParseTime(f.DateFrom), 115 | util.ParseTime(f.DateTo), 116 | page, 117 | ) 118 | if err != nil { 119 | l.Logger.Error( 120 | "controller.History.HistoryPage failed", 121 | zap.Error(err), 122 | ) 123 | t.Error(w, r, nil, err) 124 | return 125 | } 126 | res.Transactions = transactions 127 | res.TotalPages = totalPages 128 | 129 | t.Render(w, r, res, nil) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /internal/app/http/controller/log_handler.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "sync" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/ic3network/mccs-alpha/internal/app/service" 10 | "github.com/ic3network/mccs-alpha/internal/app/types" 11 | "github.com/ic3network/mccs-alpha/internal/pkg/l" 12 | "github.com/ic3network/mccs-alpha/internal/pkg/template" 13 | "github.com/ic3network/mccs-alpha/internal/pkg/util" 14 | "go.uber.org/zap" 15 | ) 16 | 17 | type logHandler struct { 18 | once *sync.Once 19 | } 20 | 21 | var LogHandler = newLogHandler() 22 | 23 | func newLogHandler() *logHandler { 24 | return &logHandler{ 25 | once: new(sync.Once), 26 | } 27 | } 28 | 29 | func (lh *logHandler) RegisterRoutes( 30 | public *mux.Router, 31 | private *mux.Router, 32 | adminPublic *mux.Router, 33 | adminPrivate *mux.Router, 34 | ) { 35 | lh.once.Do(func() { 36 | adminPrivate.Path("/log").HandlerFunc(lh.logPage()).Methods("GET") 37 | adminPrivate.Path("/log/search"). 38 | HandlerFunc(lh.searchLog()). 39 | Methods("GET") 40 | }) 41 | } 42 | 43 | func (lh *logHandler) logPage() func(http.ResponseWriter, *http.Request) { 44 | t := template.NewView("/admin/log") 45 | return func(w http.ResponseWriter, r *http.Request) { 46 | t.Render(w, r, nil, nil) 47 | } 48 | } 49 | 50 | func (lh *logHandler) searchLog() func(http.ResponseWriter, *http.Request) { 51 | t := template.NewView("/admin/log") 52 | type formData struct { 53 | Email string 54 | DateFrom string 55 | DateTo string 56 | Category string 57 | Page int 58 | } 59 | type response struct { 60 | FormData formData 61 | UserActions []*types.UserAction 62 | TotalPages int 63 | } 64 | return func(w http.ResponseWriter, r *http.Request) { 65 | q := r.URL.Query() 66 | 67 | page, err := strconv.Atoi(q.Get("page")) 68 | if err != nil { 69 | l.Logger.Error("SearchUserLogs failed", zap.Error(err)) 70 | t.Error(w, r, nil, err) 71 | return 72 | } 73 | 74 | f := formData{ 75 | Email: q.Get("email"), 76 | Category: q.Get("category"), 77 | DateFrom: q.Get("date-from"), 78 | DateTo: q.Get("date-to"), 79 | Page: page, 80 | } 81 | res := response{FormData: f} 82 | 83 | c := types.UserActionSearchCriteria{ 84 | Email: f.Email, 85 | Category: f.Category, 86 | DateFrom: util.ParseTime(f.DateFrom), 87 | DateTo: util.ParseTime(f.DateTo), 88 | } 89 | 90 | userAction, totalPages, err := service.UserAction.Find( 91 | &c, 92 | int64(f.Page), 93 | ) 94 | res.TotalPages = totalPages 95 | res.UserActions = userAction 96 | if err != nil { 97 | l.Logger.Error("SearchUserLogs failed", zap.Error(err)) 98 | t.Error(w, r, res, err) 99 | return 100 | } 101 | 102 | t.Render(w, r, res, nil) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /internal/app/http/controller/service_discovery.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "sync" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/shirou/gopsutil/cpu" 10 | "github.com/shirou/gopsutil/disk" 11 | "github.com/shirou/gopsutil/load" 12 | "github.com/shirou/gopsutil/mem" 13 | ) 14 | 15 | type serviceDiscovery struct { 16 | once *sync.Once 17 | } 18 | 19 | var ServiceDiscovery = newServiceDiscovery() 20 | 21 | func newServiceDiscovery() *serviceDiscovery { 22 | return &serviceDiscovery{ 23 | once: new(sync.Once), 24 | } 25 | } 26 | 27 | func (s *serviceDiscovery) RegisterRoutes( 28 | public *mux.Router, 29 | private *mux.Router, 30 | adminPublic *mux.Router, 31 | adminPrivate *mux.Router, 32 | ) { 33 | s.once.Do(func() { 34 | public.Path("/health").HandlerFunc(s.healthCheck).Methods("GET") 35 | public.Path("/disk").HandlerFunc(s.diskCheck).Methods("GET") 36 | public.Path("/cpu").HandlerFunc(s.cpuCheck).Methods("GET") 37 | public.Path("/ram").HandlerFunc(s.ramCheck).Methods("GET") 38 | }) 39 | } 40 | 41 | const ( 42 | B = 1 43 | KB = 1024 * B 44 | MB = 1024 * KB 45 | GB = 1024 * MB 46 | ) 47 | 48 | // HealthCheck shows `OK` as the ping-pong result. 49 | func (s *serviceDiscovery) healthCheck(w http.ResponseWriter, r *http.Request) { 50 | message := "OK" 51 | w.WriteHeader(http.StatusOK) 52 | w.Write([]byte("\n" + message)) 53 | } 54 | 55 | // DiskCheck checks the disk usage. 56 | func (s *serviceDiscovery) diskCheck(w http.ResponseWriter, r *http.Request) { 57 | u, _ := disk.Usage("/") 58 | 59 | usedMB := int(u.Used) / MB 60 | usedGB := int(u.Used) / GB 61 | totalMB := int(u.Total) / MB 62 | totalGB := int(u.Total) / GB 63 | usedPercent := int(u.UsedPercent) 64 | 65 | status := http.StatusOK 66 | text := "OK" 67 | 68 | if usedPercent >= 95 { 69 | status = http.StatusOK 70 | text = "CRITICAL" 71 | } else if usedPercent >= 90 { 72 | status = http.StatusTooManyRequests 73 | text = "WARNING" 74 | } 75 | 76 | message := fmt.Sprintf( 77 | "%s - Free space: %dMB (%dGB) / %dMB (%dGB) | Used: %d%%", 78 | text, 79 | usedMB, 80 | usedGB, 81 | totalMB, 82 | totalGB, 83 | usedPercent, 84 | ) 85 | w.WriteHeader(status) 86 | w.Write([]byte("\n" + message)) 87 | } 88 | 89 | // CPUCheck checks the cpu usage. 90 | func (s *serviceDiscovery) cpuCheck(w http.ResponseWriter, r *http.Request) { 91 | cores, _ := cpu.Counts(false) 92 | 93 | a, _ := load.Avg() 94 | l1 := a.Load1 95 | l5 := a.Load5 96 | l15 := a.Load15 97 | 98 | status := http.StatusOK 99 | text := "OK" 100 | 101 | if l5 >= float64(cores-1) { 102 | status = http.StatusInternalServerError 103 | text = "CRITICAL" 104 | } else if l5 >= float64(cores-2) { 105 | status = http.StatusTooManyRequests 106 | text = "WARNING" 107 | } 108 | 109 | message := fmt.Sprintf( 110 | "%s - Load average: %.2f, %.2f, %.2f | Cores: %d", 111 | text, 112 | l1, 113 | l5, 114 | l15, 115 | cores, 116 | ) 117 | w.WriteHeader(status) 118 | w.Write([]byte("\n" + message)) 119 | } 120 | 121 | // RAMCheck checks the disk usage. 122 | func (s *serviceDiscovery) ramCheck(w http.ResponseWriter, r *http.Request) { 123 | u, _ := mem.VirtualMemory() 124 | 125 | usedMB := int(u.Used) / MB 126 | usedGB := int(u.Used) / GB 127 | totalMB := int(u.Total) / MB 128 | totalGB := int(u.Total) / GB 129 | usedPercent := int(u.UsedPercent) 130 | 131 | status := http.StatusOK 132 | text := "OK" 133 | 134 | if usedPercent >= 95 { 135 | status = http.StatusInternalServerError 136 | text = "CRITICAL" 137 | } else if usedPercent >= 90 { 138 | status = http.StatusTooManyRequests 139 | text = "WARNING" 140 | } 141 | 142 | message := fmt.Sprintf( 143 | "%s - Free space: %dMB (%dGB) / %dMB (%dGB) | Used: %d%%", 144 | text, 145 | usedMB, 146 | usedGB, 147 | totalMB, 148 | totalGB, 149 | usedPercent, 150 | ) 151 | w.WriteHeader(status) 152 | w.Write([]byte("\n" + message)) 153 | } 154 | -------------------------------------------------------------------------------- /internal/app/http/middleware/auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "strconv" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/ic3network/mccs-alpha/internal/pkg/jwt" 10 | ) 11 | 12 | func GetLoggedInUser() mux.MiddlewareFunc { 13 | return func(next http.Handler) http.Handler { 14 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | cookie, err := r.Cookie("mccsToken") 16 | if err != nil { 17 | next.ServeHTTP(w, r) 18 | return 19 | } 20 | mccsToken := cookie.Value 21 | claims, err := jwt.NewJWTManager().Validate(mccsToken) 22 | if err != nil { 23 | next.ServeHTTP(w, r) 24 | return 25 | } 26 | r.Header.Set("userID", claims.UserID) 27 | r.Header.Set("admin", strconv.FormatBool(claims.Admin)) 28 | next.ServeHTTP(w, r) 29 | }) 30 | } 31 | } 32 | 33 | func RequireUser() mux.MiddlewareFunc { 34 | return func(next http.Handler) http.Handler { 35 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 36 | userID := r.Header.Get("userID") 37 | // When user is not logged in. 38 | // 1. If it's on the root page, redirect to find businesses page. 39 | // 2. If it's on the other page, redirect to the targeting page after logging in. 40 | if userID == "" { 41 | if url.QueryEscape(r.URL.String()) == url.QueryEscape("/") { 42 | http.Redirect( 43 | w, 44 | r, 45 | "/businesses/search?page=1", 46 | http.StatusFound, 47 | ) 48 | } else { 49 | http.Redirect(w, r, "/login?redirect_login="+url.QueryEscape(r.URL.String()), http.StatusFound) 50 | } 51 | return 52 | } 53 | admin, _ := strconv.ParseBool(r.Header.Get("admin")) 54 | if admin == true { 55 | // Redirect to admin page if user is an admin. 56 | http.Redirect(w, r, "/admin", http.StatusFound) 57 | return 58 | } 59 | next.ServeHTTP(w, r) 60 | }) 61 | } 62 | } 63 | 64 | func RequireAdmin() mux.MiddlewareFunc { 65 | return func(next http.Handler) http.Handler { 66 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 67 | admin, err := strconv.ParseBool(r.Header.Get("admin")) 68 | if err != nil { 69 | w.WriteHeader(http.StatusInternalServerError) 70 | return 71 | } 72 | if admin != true { 73 | w.WriteHeader(http.StatusForbidden) 74 | return 75 | } 76 | next.ServeHTTP(w, r) 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /internal/app/http/middleware/cache.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/mux" 7 | ) 8 | 9 | func NoCache() mux.MiddlewareFunc { 10 | return func(next http.Handler) http.Handler { 11 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 12 | w.Header(). 13 | Set("Cache-Control", "no-cache, must-revalidate, no-store") 14 | // HTTP 1.1. 15 | w.Header(). 16 | Set("Pragma", "no-cache") 17 | // HTTP 1.0. 18 | w.Header(). 19 | Set("Expires", "0") 20 | // Proxies. 21 | next.ServeHTTP(w, r) 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /internal/app/http/middleware/logging.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "time" 7 | 8 | "go.uber.org/zap" 9 | 10 | "github.com/gorilla/mux" 11 | "github.com/ic3network/mccs-alpha/internal/pkg/ip" 12 | "github.com/ic3network/mccs-alpha/internal/pkg/l" 13 | ) 14 | 15 | // Logging middleware logs messages. 16 | func Logging() mux.MiddlewareFunc { 17 | return func(next http.Handler) http.Handler { 18 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 19 | startTime := time.Now() 20 | 21 | defer func() { 22 | uri := r.RequestURI 23 | // Skip for the health check and static requests. 24 | if uri == "/health" || uri == "/ram" || uri == "/cpu" || 25 | uri == "/disk" || 26 | strings.HasPrefix(uri, "/static") { 27 | return 28 | } 29 | elapse := time.Now().Sub(startTime) 30 | l.Logger.Info("request", 31 | zap.String("ip", ip.FromRequest(r)), 32 | zap.String("method", r.Method), 33 | zap.String("uri", uri), 34 | zap.String("userAgent", r.UserAgent()), 35 | zap.Duration("responseTime", elapse)) 36 | }() 37 | 38 | next.ServeHTTP(w, r) 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/app/http/middleware/recover.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "runtime" 6 | 7 | "github.com/gorilla/mux" 8 | "github.com/ic3network/mccs-alpha/internal/pkg/l" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | func Recover() mux.MiddlewareFunc { 13 | return func(next http.Handler) http.Handler { 14 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | defer func() { 16 | if err := recover(); err != nil { 17 | buf := make([]byte, 1024) 18 | runtime.Stack(buf, false) 19 | l.Logger.Error( 20 | "recover, error", 21 | zap.Any("err", err), 22 | zap.ByteString("method", buf), 23 | ) 24 | } 25 | }() 26 | next.ServeHTTP(w, r) 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/app/http/routes.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/mux" 7 | "github.com/ic3network/mccs-alpha/internal/app/http/controller" 8 | "github.com/ic3network/mccs-alpha/internal/app/http/middleware" 9 | ) 10 | 11 | func RegisterRoutes(r *mux.Router) { 12 | public := r.PathPrefix("/").Subrouter() 13 | public.Use( 14 | middleware.Recover(), 15 | middleware.NoCache(), 16 | middleware.Logging(), 17 | middleware.GetLoggedInUser(), 18 | ) 19 | private := r.PathPrefix("/").Subrouter() 20 | private.Use( 21 | middleware.Recover(), 22 | middleware.NoCache(), 23 | middleware.Logging(), 24 | middleware.GetLoggedInUser(), 25 | middleware.RequireUser(), 26 | ) 27 | adminPublic := r.PathPrefix("/admin").Subrouter() 28 | adminPublic.Use( 29 | middleware.Recover(), 30 | middleware.NoCache(), 31 | middleware.Logging(), 32 | middleware.GetLoggedInUser(), 33 | ) 34 | adminPrivate := r.PathPrefix("/admin").Subrouter() 35 | adminPrivate.Use( 36 | middleware.Recover(), 37 | middleware.NoCache(), 38 | middleware.Logging(), 39 | middleware.GetLoggedInUser(), 40 | middleware.RequireAdmin(), 41 | ) 42 | 43 | // Serving static files. 44 | fs := http.FileServer(http.Dir("web/static")) 45 | public.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fs)) 46 | 47 | controller.ServiceDiscovery.RegisterRoutes( 48 | public, 49 | private, 50 | adminPublic, 51 | adminPrivate, 52 | ) 53 | controller.DashBoardHandler.RegisterRoutes( 54 | public, 55 | private, 56 | adminPublic, 57 | adminPrivate, 58 | ) 59 | controller.BusinessHandler.RegisterRoutes( 60 | public, 61 | private, 62 | adminPublic, 63 | adminPrivate, 64 | ) 65 | controller.UserHandler.RegisterRoutes( 66 | public, 67 | private, 68 | adminPublic, 69 | adminPrivate, 70 | ) 71 | controller.TransactionHandler.RegisterRoutes( 72 | public, 73 | private, 74 | adminPublic, 75 | adminPrivate, 76 | ) 77 | controller.HistoryHandler.RegisterRoutes( 78 | public, 79 | private, 80 | adminPublic, 81 | adminPrivate, 82 | ) 83 | controller.TradingHandler.RegisterRoutes( 84 | public, 85 | private, 86 | adminPublic, 87 | adminPrivate, 88 | ) 89 | 90 | controller.AdminBusinessHandler.RegisterRoutes( 91 | public, 92 | private, 93 | adminPublic, 94 | adminPrivate, 95 | ) 96 | controller.AdminUserHandler.RegisterRoutes( 97 | public, 98 | private, 99 | adminPublic, 100 | adminPrivate, 101 | ) 102 | controller.AdminHistoryHandler.RegisterRoutes( 103 | public, 104 | private, 105 | adminPublic, 106 | adminPrivate, 107 | ) 108 | controller.AdminTransactionHandler.RegisterRoutes( 109 | public, 110 | private, 111 | adminPublic, 112 | adminPrivate, 113 | ) 114 | controller.AdminTagHandler.RegisterRoutes( 115 | public, 116 | private, 117 | adminPublic, 118 | adminPrivate, 119 | ) 120 | controller.LogHandler.RegisterRoutes( 121 | public, 122 | private, 123 | adminPublic, 124 | adminPrivate, 125 | ) 126 | 127 | controller.AccountHandler.RegisterRoutes( 128 | public, 129 | private, 130 | adminPublic, 131 | adminPrivate, 132 | ) 133 | controller.TagHandler.RegisterRoutes( 134 | public, 135 | private, 136 | adminPublic, 137 | adminPrivate, 138 | ) 139 | } 140 | -------------------------------------------------------------------------------- /internal/app/http/server.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/ic3network/mccs-alpha/internal/pkg/l" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | // AppServer contains the information to run a server. 14 | type appServer struct{} 15 | 16 | var AppServer = &appServer{} 17 | 18 | // Run will start the http server. 19 | func (a *appServer) Run(port string) { 20 | r := mux.NewRouter().StrictSlash(true) 21 | // New Implementation 22 | RegisterRoutes(r) 23 | 24 | srv := &http.Server{ 25 | Addr: fmt.Sprintf("0.0.0.0:%s", port), 26 | WriteTimeout: time.Second * 15, 27 | ReadTimeout: time.Second * 15, 28 | IdleTimeout: time.Second * 60, 29 | Handler: r, 30 | } 31 | 32 | l.Logger.Info("app is running at localhost:" + port) 33 | 34 | if err := srv.ListenAndServe(); err != nil { 35 | l.Logger.Fatal("ListenAndServe failed", zap.Error(err)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/app/repositories/es/es.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/ic3network/mccs-alpha/global" 8 | "github.com/olivere/elastic/v7" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | var client *elastic.Client 13 | 14 | func init() { 15 | global.Init() 16 | client = New() 17 | registerCollections(client) 18 | } 19 | 20 | func registerCollections(client *elastic.Client) { 21 | Business.Register(client) 22 | User.Register(client) 23 | Tag.Register(client) 24 | } 25 | 26 | // New returns an initialized ES instance. 27 | func New() *elastic.Client { 28 | var client *elastic.Client 29 | var err error 30 | 31 | for { 32 | client, err = elastic.NewClient( 33 | elastic.SetURL(viper.GetString("es.url")), 34 | elastic.SetSniff(false), 35 | ) 36 | if err != nil { 37 | log.Printf("ElasticSearch connection error: %+v \n", err) 38 | time.Sleep(5 * time.Second) 39 | } else { 40 | break 41 | } 42 | } 43 | 44 | checkIndexes(client) 45 | return client 46 | } 47 | 48 | // Client is for seed/restore data 49 | func Client() *elastic.Client { 50 | return client 51 | } 52 | -------------------------------------------------------------------------------- /internal/app/repositories/es/helper.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/olivere/elastic/v7" 8 | ) 9 | 10 | func newFuzzyWildcardQuery(name, text string) *elastic.BoolQuery { 11 | q := elastic.NewBoolQuery() 12 | q.Should(elastic.NewMatchQuery(name, text).Fuzziness("auto").Boost(3)) 13 | q.Should(elastic.NewWildcardQuery(name, strings.ToLower(text)+"*").Boost(2)) 14 | q.Should(elastic.NewRegexpQuery(name, ".*"+strings.ToLower(text)+".*")) 15 | return q 16 | } 17 | 18 | // Should match one of the three queries. (MatchQuery, WildcardQuery, RegexpQuery) 19 | func newFuzzyWildcardTimeQueryForTag( 20 | tagField, tagName string, 21 | createdOnOrAfter time.Time, 22 | ) *elastic.NestedQuery { 23 | q := elastic.NewBoolQuery() 24 | 25 | qq := elastic.NewBoolQuery() 26 | qq.Must( 27 | (elastic.NewMatchQuery(tagField+".name", tagName).Fuzziness("auto").Boost(2)), 28 | ) 29 | qq.Must( 30 | elastic.NewRangeQuery(tagField + ".createdAt").Gte(createdOnOrAfter), 31 | ) 32 | q.Should(qq) 33 | 34 | qq = elastic.NewBoolQuery() 35 | qq.Must( 36 | elastic.NewWildcardQuery(tagField+".name", strings.ToLower(tagName)+"*"). 37 | Boost(1.5), 38 | ) 39 | qq.Must( 40 | elastic.NewRangeQuery(tagField + ".createdAt").Gte(createdOnOrAfter), 41 | ) 42 | q.Should(qq) 43 | 44 | qq = elastic.NewBoolQuery() 45 | qq.Must( 46 | elastic.NewRegexpQuery( 47 | tagField+".name", 48 | ".*"+strings.ToLower(tagName)+".*", 49 | ), 50 | ) 51 | qq.Must( 52 | elastic.NewRangeQuery(tagField + ".createdAt").Gte(createdOnOrAfter), 53 | ) 54 | q.Should(qq) 55 | 56 | nestedQ := elastic.NewNestedQuery(tagField, q) 57 | 58 | return nestedQ 59 | } 60 | 61 | // Should match one of the three queries. (MatchQuery, WildcardQuery, RegexpQuery) 62 | func newTagQuery( 63 | tag string, 64 | lastLoginDate time.Time, 65 | timeField string, 66 | ) *elastic.BoolQuery { 67 | q := elastic.NewBoolQuery() 68 | 69 | // The default value for both offerAddedAt and wantAddedAt is 0001-01-01T00:00:00.000+0000. 70 | // If the user never login before and his lastLoginDate will be 0001-01-01T00:00:00.000+0000. 71 | // And we will match the user's own tags. 72 | // Added this filter to solve the problem. 73 | q.MustNot(elastic.NewRangeQuery(timeField).Lte(time.Time{})) 74 | 75 | qq := elastic.NewBoolQuery() 76 | qq.Must(elastic.NewMatchQuery("name", tag).Fuzziness("auto")) 77 | qq.Must(elastic.NewRangeQuery(timeField).Gte(lastLoginDate)) 78 | q.Should(qq) 79 | 80 | qq = elastic.NewBoolQuery() 81 | qq.Must(elastic.NewWildcardQuery("name", strings.ToLower(tag)+"*")) 82 | qq.Must(elastic.NewRangeQuery(timeField).Gte(lastLoginDate)) 83 | q.Should(qq) 84 | 85 | qq = elastic.NewBoolQuery() 86 | qq.Must(elastic.NewRegexpQuery("name", ".*"+strings.ToLower(tag)+".*")) 87 | qq.Must(elastic.NewRangeQuery(timeField).Gte(lastLoginDate)) 88 | q.Should(qq) 89 | 90 | return q 91 | } 92 | -------------------------------------------------------------------------------- /internal/app/repositories/es/user_es.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/ic3network/mccs-alpha/internal/app/types" 8 | "github.com/ic3network/mccs-alpha/internal/pkg/e" 9 | "github.com/ic3network/mccs-alpha/internal/pkg/pagination" 10 | "github.com/olivere/elastic/v7" 11 | "github.com/spf13/viper" 12 | "go.mongodb.org/mongo-driver/bson/primitive" 13 | ) 14 | 15 | type user struct { 16 | c *elastic.Client 17 | index string 18 | } 19 | 20 | var User = &user{} 21 | 22 | func (es *user) Register(client *elastic.Client) { 23 | es.c = client 24 | es.index = "users" 25 | } 26 | 27 | // Create creates an UserESRecord in Elasticsearch. 28 | func (es *user) Create(u *types.User) error { 29 | body := types.UserESRecord{ 30 | UserID: u.ID.Hex(), 31 | FirstName: u.FirstName, 32 | LastName: u.LastName, 33 | Email: u.Email, 34 | } 35 | _, err := es.c.Index(). 36 | Index(es.index). 37 | Id(u.ID.Hex()). 38 | BodyJson(body). 39 | Do(context.Background()) 40 | if err != nil { 41 | return err 42 | } 43 | return nil 44 | } 45 | 46 | // Find finds users from Elasticsearch. 47 | func (es *user) Find(u *types.User, page int64) ([]string, int, int, error) { 48 | if page < 0 || page == 0 { 49 | return nil, 0, 0, e.New(e.InvalidPageNumber, "find user failed") 50 | } 51 | 52 | var ids []string 53 | size := viper.GetInt("page_size") 54 | from := viper.GetInt("page_size") * (int(page) - 1) 55 | 56 | q := elastic.NewBoolQuery() 57 | 58 | if u.LastName != "" { 59 | q.Must(newFuzzyWildcardQuery("lastName", u.LastName)) 60 | } 61 | if u.Email != "" { 62 | q.Must(newFuzzyWildcardQuery("email", u.Email)) 63 | } 64 | 65 | res, err := es.c.Search(). 66 | Index(es.index). 67 | From(from). 68 | Size(size). 69 | Query(q). 70 | Do(context.Background()) 71 | 72 | if err != nil { 73 | return nil, 0, 0, e.Wrap(err, "find user failed") 74 | } 75 | 76 | for _, hit := range res.Hits.Hits { 77 | var record types.UserESRecord 78 | err := json.Unmarshal(hit.Source, &record) 79 | if err != nil { 80 | return nil, 0, 0, e.Wrap(err, "find user failed") 81 | } 82 | ids = append(ids, record.UserID) 83 | } 84 | 85 | numberOfResults := res.Hits.TotalHits.Value 86 | totalPages := pagination.Pages(numberOfResults, viper.GetInt64("page_size")) 87 | 88 | return ids, int(numberOfResults), totalPages, nil 89 | } 90 | 91 | func (es *user) Update(u *types.User) error { 92 | doc := map[string]interface{}{ 93 | "email": u.Email, 94 | "firstName": u.FirstName, 95 | "lastName": u.LastName, 96 | } 97 | 98 | _, err := es.c.Update(). 99 | Index(es.index). 100 | Id(u.ID.Hex()). 101 | Doc(doc). 102 | Do(context.Background()) 103 | if err != nil { 104 | return err 105 | } 106 | return nil 107 | } 108 | 109 | func (es *user) UpdateTradingInfo( 110 | id primitive.ObjectID, 111 | data *types.TradingRegisterData, 112 | ) error { 113 | doc := map[string]interface{}{ 114 | "firstName": data.FirstName, 115 | "lastName": data.LastName, 116 | } 117 | _, err := es.c.Update(). 118 | Index(es.index). 119 | Id(id.Hex()). 120 | Doc(doc). 121 | Do(context.Background()) 122 | if err != nil { 123 | return err 124 | } 125 | return nil 126 | } 127 | 128 | func (es *user) Delete(id string) error { 129 | _, err := es.c.Delete(). 130 | Index(es.index). 131 | Id(id). 132 | Do(context.Background()) 133 | if err != nil { 134 | return err 135 | } 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /internal/app/repositories/mongo/admin_user.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | 8 | "github.com/ic3network/mccs-alpha/internal/app/types" 9 | "github.com/ic3network/mccs-alpha/internal/pkg/e" 10 | "go.mongodb.org/mongo-driver/bson" 11 | "go.mongodb.org/mongo-driver/bson/primitive" 12 | "go.mongodb.org/mongo-driver/mongo" 13 | "go.mongodb.org/mongo-driver/mongo/options" 14 | ) 15 | 16 | type adminUser struct { 17 | c *mongo.Collection 18 | } 19 | 20 | var AdminUser = &adminUser{} 21 | 22 | func (a *adminUser) Register(db *mongo.Database) { 23 | a.c = db.Collection("adminUsers") 24 | } 25 | 26 | func (a *adminUser) FindByEmail(email string) (*types.AdminUser, error) { 27 | email = strings.ToLower(email) 28 | 29 | if email == "" { 30 | return &types.AdminUser{}, e.New(e.UserNotFound, "admin user not found") 31 | } 32 | user := types.AdminUser{} 33 | filter := bson.M{ 34 | "email": email, 35 | "deletedAt": bson.M{"$exists": false}, 36 | } 37 | err := a.c.FindOne(context.Background(), filter).Decode(&user) 38 | if err != nil { 39 | return nil, e.New(e.UserNotFound, "admin user not found") 40 | } 41 | return &user, nil 42 | } 43 | 44 | func (a *adminUser) FindByID(id primitive.ObjectID) (*types.AdminUser, error) { 45 | adminUser := types.AdminUser{} 46 | filter := bson.M{ 47 | "_id": id, 48 | "deletedAt": bson.M{"$exists": false}, 49 | } 50 | err := a.c.FindOne(context.Background(), filter).Decode(&adminUser) 51 | if err != nil { 52 | return nil, e.New(e.UserNotFound, "admin user not found") 53 | } 54 | return &adminUser, nil 55 | } 56 | 57 | func (a *adminUser) GetLoginInfo( 58 | id primitive.ObjectID, 59 | ) (*types.LoginInfo, error) { 60 | loginInfo := &types.LoginInfo{} 61 | filter := bson.M{"_id": id} 62 | projection := bson.M{ 63 | "currentLoginIP": 1, 64 | "currentLoginDate": 1, 65 | "lastLoginIP": 1, 66 | "lastLoginDate": 1, 67 | } 68 | findOneOptions := options.FindOne() 69 | findOneOptions.SetProjection(projection) 70 | err := a.c.FindOne(context.Background(), filter, findOneOptions). 71 | Decode(&loginInfo) 72 | if err != nil { 73 | return nil, e.Wrap(err, "AdminUserMongo GetLoginInfo failed") 74 | } 75 | return loginInfo, nil 76 | } 77 | 78 | func (a *adminUser) UpdateLoginInfo( 79 | id primitive.ObjectID, 80 | i *types.LoginInfo, 81 | ) error { 82 | filter := bson.M{"_id": id} 83 | update := bson.M{"$set": bson.M{ 84 | "currentLoginIP": i.CurrentLoginIP, 85 | "currentLoginDate": time.Now(), 86 | "lastLoginIP": i.LastLoginIP, 87 | "lastLoginDate": i.LastLoginDate, 88 | "updatedAt": time.Now(), 89 | }} 90 | _, err := a.c.UpdateOne( 91 | context.Background(), 92 | filter, 93 | update, 94 | ) 95 | if err != nil { 96 | return e.Wrap(err, "AdminUserMongo UpdateLoginInfo failed") 97 | } 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /internal/app/repositories/mongo/config.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | // Config contains the environment variables requirements to initialize mongodb. 4 | type Config struct { 5 | URL string 6 | Database string 7 | } 8 | -------------------------------------------------------------------------------- /internal/app/repositories/mongo/helper.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson" 5 | "go.mongodb.org/mongo-driver/bson/primitive" 6 | ) 7 | 8 | func toObjectIDs(ids []string) ([]primitive.ObjectID, error) { 9 | objectIDs := make([]primitive.ObjectID, 0, len(ids)) 10 | for _, id := range ids { 11 | objectID, err := primitive.ObjectIDFromHex(id) 12 | if err != nil { 13 | return objectIDs, err 14 | } 15 | objectIDs = append(objectIDs, objectID) 16 | } 17 | return objectIDs, nil 18 | } 19 | 20 | func newFindByIDsPipeline(objectIDs []primitive.ObjectID) []primitive.M { 21 | return []bson.M{ 22 | { 23 | "$match": bson.M{ 24 | "_id": bson.M{"$in": objectIDs}, 25 | "deletedAt": bson.M{"$exists": false}, 26 | }, 27 | }, 28 | { 29 | "$addFields": bson.M{ 30 | "idOrder": bson.M{"$indexOfArray": bson.A{objectIDs, "$_id"}}, 31 | }, 32 | }, 33 | { 34 | "$sort": bson.M{"idOrder": 1}, 35 | }, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/app/repositories/mongo/lost_password_mongo.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/ic3network/mccs-alpha/internal/pkg/e" 8 | 9 | "github.com/ic3network/mccs-alpha/internal/app/types" 10 | "go.mongodb.org/mongo-driver/bson" 11 | "go.mongodb.org/mongo-driver/mongo" 12 | "go.mongodb.org/mongo-driver/mongo/options" 13 | ) 14 | 15 | type lostPassword struct { 16 | c *mongo.Collection 17 | } 18 | 19 | var LostPassword = &lostPassword{} 20 | 21 | func (l *lostPassword) Register(db *mongo.Database) { 22 | l.c = db.Collection("lostPassword") 23 | } 24 | 25 | // Create creates a lost password record in the table 26 | func (l *lostPassword) Create(lostPassword *types.LostPassword) error { 27 | filter := bson.M{"email": lostPassword.Email} 28 | update := bson.M{"$set": bson.M{ 29 | "email": lostPassword.Email, 30 | "token": lostPassword.Token, 31 | "tokenUsed": false, 32 | "createdAt": time.Now(), 33 | }} 34 | _, err := l.c.UpdateOne( 35 | context.Background(), 36 | filter, 37 | update, 38 | options.Update().SetUpsert(true), 39 | ) 40 | return err 41 | } 42 | 43 | func (l *lostPassword) FindByToken(token string) (*types.LostPassword, error) { 44 | if token == "" { 45 | return nil, e.New(e.TokenInvalid, "token not found") 46 | } 47 | lostPassword := types.LostPassword{} 48 | err := l.c.FindOne(context.Background(), types.LostPassword{Token: token}). 49 | Decode(&lostPassword) 50 | if err != nil { 51 | return nil, e.New(e.TokenInvalid, "token not found") 52 | } 53 | return &lostPassword, nil 54 | } 55 | 56 | func (l *lostPassword) FindByEmail(email string) (*types.LostPassword, error) { 57 | if email == "" { 58 | return nil, e.New(e.TokenInvalid, "token not found") 59 | } 60 | lostPassword := types.LostPassword{} 61 | err := l.c.FindOne(context.Background(), types.LostPassword{Email: email}). 62 | Decode(&lostPassword) 63 | if err != nil { 64 | return nil, e.New(e.TokenInvalid, "token not found") 65 | } 66 | return &lostPassword, nil 67 | } 68 | 69 | func (l *lostPassword) SetTokenUsed(token string) error { 70 | filter := bson.M{"token": token} 71 | update := bson.M{"$set": bson.M{"tokenUsed": true}} 72 | _, err := l.c.UpdateOne(context.Background(), filter, update) 73 | return err 74 | } 75 | -------------------------------------------------------------------------------- /internal/app/repositories/mongo/mongo.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "github.com/ic3network/mccs-alpha/global" 9 | "github.com/spf13/viper" 10 | "go.mongodb.org/mongo-driver/mongo" 11 | "go.mongodb.org/mongo-driver/mongo/options" 12 | ) 13 | 14 | var db *mongo.Database 15 | 16 | func init() { 17 | global.Init() 18 | // TODO: set up test docker environment. 19 | if viper.GetString("env") == "test" { 20 | return 21 | } 22 | db = New() 23 | registerCollections(db) 24 | } 25 | 26 | func registerCollections(db *mongo.Database) { 27 | Business.Register(db) 28 | User.Register(db) 29 | UserAction.Register(db) 30 | AdminUser.Register(db) 31 | Tag.Register(db) 32 | AdminTag.Register(db) 33 | LostPassword.Register(db) 34 | } 35 | 36 | // New returns an initialized JWT instance. 37 | func New() *mongo.Database { 38 | ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) 39 | 40 | client, err := mongo.NewClient( 41 | options.Client().ApplyURI(viper.GetString("mongo.url")), 42 | ) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | // connect to mongo 48 | if err := client.Connect(ctx); err != nil { 49 | log.Fatal(err) 50 | } 51 | 52 | // check the connection 53 | err = client.Ping(ctx, nil) 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | 58 | db := client.Database(viper.GetString("mongo.database")) 59 | return db 60 | } 61 | 62 | // For seed/migration/restore data 63 | func DB() *mongo.Database { 64 | return db 65 | } 66 | -------------------------------------------------------------------------------- /internal/app/repositories/mongo/user_action.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/ic3network/mccs-alpha/internal/app/types" 8 | "github.com/ic3network/mccs-alpha/internal/pkg/e" 9 | "github.com/ic3network/mccs-alpha/internal/pkg/pagination" 10 | "github.com/spf13/viper" 11 | "go.mongodb.org/mongo-driver/bson" 12 | "go.mongodb.org/mongo-driver/bson/primitive" 13 | "go.mongodb.org/mongo-driver/mongo" 14 | "go.mongodb.org/mongo-driver/mongo/options" 15 | ) 16 | 17 | type userAction struct { 18 | c *mongo.Collection 19 | } 20 | 21 | var UserAction = &userAction{} 22 | 23 | func (u *userAction) Register(db *mongo.Database) { 24 | u.c = db.Collection("userActions") 25 | } 26 | 27 | func (u *userAction) Log(a *types.UserAction) error { 28 | ctx := context.Background() 29 | doc := bson.M{ 30 | "userID": a.UserID, 31 | "email": a.Email, 32 | "action": a.Action, 33 | "actionDetails": a.ActionDetails, 34 | "category": a.Category, 35 | "createdAt": time.Now(), 36 | } 37 | _, err := u.c.InsertOne(ctx, doc) 38 | if err != nil { 39 | return err 40 | } 41 | return nil 42 | } 43 | 44 | func (u *userAction) Find( 45 | c *types.UserActionSearchCriteria, 46 | page int64, 47 | ) ([]*types.UserAction, int, error) { 48 | ctx := context.Background() 49 | if page < 0 || page == 0 { 50 | return nil, 0, e.New( 51 | e.InvalidPageNumber, 52 | "mongo.userAction.Find failed", 53 | ) 54 | } 55 | 56 | var results []*types.UserAction 57 | 58 | findOptions := options.Find() 59 | findOptions.SetSkip(viper.GetInt64("page_size") * (page - 1)) 60 | findOptions.SetLimit(viper.GetInt64("page_size")) 61 | findOptions.SetSort(bson.M{"createdAt": -1}) 62 | 63 | filter := bson.M{ 64 | "deletedAt": bson.M{"$exists": false}, 65 | } 66 | if c.Email != "" { 67 | pattern := c.Email 68 | filter["email"] = primitive.Regex{Pattern: pattern, Options: "i"} 69 | } 70 | if c.Category != "" { 71 | filter["category"] = c.Category 72 | } 73 | 74 | // Should not overwrite each others. 75 | if !c.DateFrom.IsZero() || !c.DateTo.IsZero() { 76 | if !c.DateFrom.IsZero() && !c.DateTo.IsZero() { 77 | filter["createdAt"] = bson.M{"$gte": c.DateFrom, "$lte": c.DateTo} 78 | } else if !c.DateFrom.IsZero() { 79 | filter["createdAt"] = bson.M{"$gte": c.DateFrom} 80 | } else { 81 | filter["createdAt"] = bson.M{"$lte": c.DateTo} 82 | } 83 | } 84 | 85 | cur, err := u.c.Find(ctx, filter, findOptions) 86 | if err != nil { 87 | return nil, 0, e.Wrap(err, "mongo.userAction.Find failed") 88 | } 89 | 90 | for cur.Next(ctx) { 91 | var elem types.UserAction 92 | err := cur.Decode(&elem) 93 | if err != nil { 94 | return nil, 0, e.Wrap(err, "mongo.userAction.Find failed") 95 | } 96 | results = append(results, &elem) 97 | } 98 | if err := cur.Err(); err != nil { 99 | return nil, 0, e.Wrap(err, "mongo.userAction.Find failed") 100 | } 101 | cur.Close(ctx) 102 | 103 | // Calculate the total page. 104 | totalCount, err := u.c.CountDocuments(ctx, filter) 105 | if err != nil { 106 | return nil, 0, e.Wrap(err, "mongo.userAction.Find failed") 107 | } 108 | totalPages := pagination.Pages(totalCount, viper.GetInt64("page_size")) 109 | 110 | return results, totalPages, nil 111 | } 112 | -------------------------------------------------------------------------------- /internal/app/repositories/pg/account.go: -------------------------------------------------------------------------------- 1 | package pg 2 | 3 | import ( 4 | "github.com/ic3network/mccs-alpha/internal/app/types" 5 | "github.com/ic3network/mccs-alpha/internal/pkg/e" 6 | ) 7 | 8 | type account struct{} 9 | 10 | var Account = &account{} 11 | 12 | func (a *account) Create(bID string) error { 13 | tx := db.Begin() 14 | 15 | account := &types.Account{BusinessID: bID, Balance: 0} 16 | err := tx.Create(account).Error 17 | if err != nil { 18 | tx.Rollback() 19 | return e.Wrap(err, "pg.Account.Create failed") 20 | } 21 | err = BalanceLimit.Create(tx, account.ID) 22 | if err != nil { 23 | tx.Rollback() 24 | return e.Wrap(err, "pg.Account.Create failed") 25 | } 26 | 27 | return tx.Commit().Error 28 | } 29 | 30 | func (a *account) FindByID(accountID uint) (*types.Account, error) { 31 | var result types.Account 32 | err := db.Raw(` 33 | SELECT A.id, A.business_id, A.balance 34 | FROM accounts AS A 35 | WHERE A.id = ? 36 | LIMIT 1 37 | `, accountID).Scan(&result).Error 38 | if err != nil { 39 | return nil, e.Wrap(err, "pg.Account.FindByID") 40 | } 41 | return &result, nil 42 | } 43 | 44 | func (a *account) FindByBusinessID(businessID string) (*types.Account, error) { 45 | account := new(types.Account) 46 | err := db.Where("business_id = ?", businessID).First(account).Error 47 | if err != nil { 48 | return nil, e.New(e.UserNotFound, "user not found") 49 | } 50 | return account, nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/app/repositories/pg/balance_limit.go: -------------------------------------------------------------------------------- 1 | package pg 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/ic3network/mccs-alpha/internal/app/types" 7 | "github.com/ic3network/mccs-alpha/internal/pkg/e" 8 | "github.com/jinzhu/gorm" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | type balanceLimit struct{} 13 | 14 | var BalanceLimit = balanceLimit{} 15 | 16 | func (b balanceLimit) Create(tx *gorm.DB, accountID uint) error { 17 | balance := &types.BalanceLimit{ 18 | AccountID: accountID, 19 | MaxNegBal: viper.GetFloat64("transaction.maxNegBal"), 20 | MaxPosBal: viper.GetFloat64("transaction.maxPosBal"), 21 | } 22 | err := tx.Create(balance).Error 23 | if err != nil { 24 | return e.Wrap(err, "pg.BalanceLimit.Create failed") 25 | } 26 | return nil 27 | } 28 | 29 | func (b balanceLimit) FindByAccountID( 30 | accountID uint, 31 | ) (*types.BalanceLimit, error) { 32 | balance := new(types.BalanceLimit) 33 | err := db.Where("account_id = ?", accountID).First(balance).Error 34 | if err != nil { 35 | return nil, e.Wrap(err, "pg.BalanceLimit.FindByAccountID failed") 36 | } 37 | return balance, nil 38 | } 39 | 40 | func (b balanceLimit) Update( 41 | id uint, 42 | maxPosBal float64, 43 | maxNegBal float64, 44 | ) error { 45 | if math.Abs(maxNegBal) == 0 { 46 | maxNegBal = 0 47 | } else { 48 | maxNegBal = math.Abs(maxNegBal) 49 | } 50 | 51 | err := db. 52 | Model(&types.BalanceLimit{}). 53 | Where("account_id = ?", id). 54 | Updates(map[string]interface{}{ 55 | "max_pos_bal": math.Abs(maxPosBal), 56 | "max_neg_bal": maxNegBal, 57 | }).Error 58 | if err != nil { 59 | return e.Wrap(err, "pg.BalanceLimit.Update failed") 60 | } 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /internal/app/repositories/pg/pg.go: -------------------------------------------------------------------------------- 1 | package pg 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/ic3network/mccs-alpha/global" 9 | "github.com/ic3network/mccs-alpha/internal/app/types" 10 | "github.com/jinzhu/gorm" 11 | _ "github.com/jinzhu/gorm/dialects/postgres" 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | var db *gorm.DB 16 | 17 | func init() { 18 | global.Init() 19 | // TODO: set up test docker environment. 20 | if viper.GetString("env") == "test" { 21 | return 22 | } 23 | db = New() 24 | } 25 | 26 | // New returns an initialized DB instance. 27 | func New() *gorm.DB { 28 | db, err := gorm.Open("postgres", connectionInfo()) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | for { 34 | err := db.DB().Ping() 35 | if err != nil { 36 | log.Printf("PostgreSQL connection error: %+v \n", err) 37 | time.Sleep(5 * time.Second) 38 | } else { 39 | break 40 | } 41 | } 42 | 43 | autoMigrate(db) 44 | 45 | return db 46 | } 47 | 48 | func connectionInfo() string { 49 | password := viper.GetString("psql.password") 50 | host := viper.GetString("psql.host") 51 | port := viper.GetString("psql.port") 52 | user := viper.GetString("psql.user") 53 | dbName := viper.GetString("psql.db") 54 | 55 | if password == "" { 56 | return fmt.Sprintf("host=%s port=%s user=%s dbname=%s "+ 57 | "sslmode=disable", host, port, user, dbName) 58 | } 59 | return fmt.Sprintf("host=%s port=%s user=%s password=%s "+ 60 | "dbname=%s sslmode=disable", host, port, user, 61 | password, dbName) 62 | } 63 | 64 | // AutoMigrate will attempt to automatically migrate all tables 65 | func autoMigrate(db *gorm.DB) { 66 | err := db.AutoMigrate( 67 | &types.Account{}, 68 | &types.BalanceLimit{}, 69 | &types.Journal{}, 70 | &types.Posting{}, 71 | ).Error 72 | if err != nil { 73 | panic(err) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /internal/app/repositories/pg/posting.go: -------------------------------------------------------------------------------- 1 | package pg 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ic3network/mccs-alpha/internal/app/types" 7 | "github.com/ic3network/mccs-alpha/internal/pkg/e" 8 | ) 9 | 10 | type posting struct{} 11 | 12 | var Posting = &posting{} 13 | 14 | func (t *posting) FindInRange( 15 | from time.Time, 16 | to time.Time, 17 | ) ([]*types.Posting, error) { 18 | var result []*types.Posting 19 | err := db.Raw(` 20 | SELECT P.amount, P.created_at 21 | FROM postings AS P 22 | WHERE P.created_at BETWEEN ? AND ? 23 | ORDER BY P.created_at DESC 24 | `, from, to).Scan(&result).Error 25 | if err != nil { 26 | return nil, e.Wrap(err, "pg.Posting.FindInRange failed") 27 | } 28 | return result, nil 29 | } 30 | -------------------------------------------------------------------------------- /internal/app/service/account.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/ic3network/mccs-alpha/internal/app/repositories/pg" 5 | "github.com/ic3network/mccs-alpha/internal/app/types" 6 | ) 7 | 8 | type account struct{} 9 | 10 | var Account = &account{} 11 | 12 | func (a *account) Create(bID string) error { 13 | err := pg.Account.Create(bID) 14 | if err != nil { 15 | return err 16 | } 17 | return nil 18 | } 19 | 20 | func (a *account) FindByID(accountID uint) (*types.Account, error) { 21 | account, err := pg.Account.FindByID(accountID) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return account, nil 26 | } 27 | 28 | func (a *account) FindByBusinessID(businessID string) (*types.Account, error) { 29 | account, err := pg.Account.FindByBusinessID(businessID) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return account, nil 34 | } 35 | -------------------------------------------------------------------------------- /internal/app/service/admin_tag.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/ic3network/mccs-alpha/internal/app/repositories/mongo" 5 | "github.com/ic3network/mccs-alpha/internal/app/types" 6 | "github.com/ic3network/mccs-alpha/internal/pkg/e" 7 | "go.mongodb.org/mongo-driver/bson/primitive" 8 | ) 9 | 10 | type adminTag struct{} 11 | 12 | var AdminTag = &adminTag{} 13 | 14 | func (a *adminTag) Create(name string) error { 15 | err := mongo.AdminTag.Create(name) 16 | if err != nil { 17 | return e.Wrap(err, "create admin tag failed") 18 | } 19 | return nil 20 | } 21 | 22 | func (a *adminTag) FindByName(name string) (*types.AdminTag, error) { 23 | adminTag, err := mongo.AdminTag.FindByName(name) 24 | if err != nil { 25 | return nil, e.Wrap(err, "AdminTagService FindByName failed") 26 | } 27 | return adminTag, nil 28 | } 29 | 30 | func (a *adminTag) FindByID(id primitive.ObjectID) (*types.AdminTag, error) { 31 | adminTag, err := mongo.AdminTag.FindByID(id) 32 | if err != nil { 33 | return nil, e.Wrap(err, "AdminTagService FindByID failed") 34 | } 35 | return adminTag, nil 36 | } 37 | 38 | func (a *adminTag) FindTags( 39 | name string, 40 | page int64, 41 | ) (*types.FindAdminTagResult, error) { 42 | result, err := mongo.AdminTag.FindTags(name, page) 43 | if err != nil { 44 | return nil, e.Wrap(err, "AdminTagService FindTags failed") 45 | } 46 | return result, nil 47 | } 48 | 49 | func (a *adminTag) TagStartWith(prefix string) ([]string, error) { 50 | tags, err := mongo.AdminTag.TagStartWith(prefix) 51 | if err != nil { 52 | return nil, err 53 | } 54 | return tags, nil 55 | } 56 | 57 | func (a *adminTag) GetAll() ([]*types.AdminTag, error) { 58 | adminTags, err := mongo.AdminTag.GetAll() 59 | if err != nil { 60 | return nil, e.Wrap(err, "AdminTagService GetAll failed") 61 | } 62 | return adminTags, nil 63 | } 64 | 65 | func (a *adminTag) Update(tag *types.AdminTag) error { 66 | err := mongo.AdminTag.Update(tag) 67 | if err != nil { 68 | return e.Wrap(err, "AdminTagService Update failed") 69 | } 70 | return nil 71 | } 72 | 73 | func (a *adminTag) DeleteByID(id primitive.ObjectID) error { 74 | err := mongo.AdminTag.DeleteByID(id) 75 | if err != nil { 76 | return e.Wrap(err, "AdminTagService DeleteByID failed") 77 | } 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /internal/app/service/admin_transaction.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/ic3network/mccs-alpha/internal/app/repositories/pg" 5 | "github.com/ic3network/mccs-alpha/internal/pkg/e" 6 | ) 7 | 8 | type adminTransaction struct{} 9 | 10 | var AdminTransaction = &adminTransaction{} 11 | 12 | func (a *adminTransaction) Create( 13 | fromID, 14 | fromEmail, 15 | fromBusinessName, 16 | 17 | toID, 18 | toEmail, 19 | toBusinessName string, 20 | 21 | amount float64, 22 | description string, 23 | ) error { 24 | // Get the Account IDs using MongoIDs. 25 | from, err := pg.Account.FindByBusinessID(fromID) 26 | if err != nil { 27 | return e.Wrap(err, "service.Account.MakeTransfer failed") 28 | } 29 | to, err := pg.Account.FindByBusinessID(toID) 30 | if err != nil { 31 | return e.Wrap(err, "service.Account.MakeTransfer failed") 32 | } 33 | 34 | // Check the account balance. 35 | exceed, err := BalanceLimit.IsExceedLimit(from.ID, from.Balance-amount) 36 | if err != nil { 37 | return e.Wrap(err, "service.Account.MakeTransfer failed") 38 | } 39 | if exceed { 40 | return e.New(e.ExceedMaxNegBalance, "max negative exceed") 41 | } 42 | exceed, err = BalanceLimit.IsExceedLimit(to.ID, to.Balance+amount) 43 | if err != nil { 44 | return e.Wrap(err, "service.Account.MakeTransfer failed") 45 | } 46 | if exceed { 47 | return e.New(e.ExceedMaxPosBalance, "max positive exceed") 48 | } 49 | 50 | err = pg.Transaction.Create( 51 | from.ID, 52 | fromEmail, 53 | fromBusinessName, 54 | to.ID, 55 | toEmail, 56 | toBusinessName, 57 | amount, 58 | description, 59 | ) 60 | if err != nil { 61 | return e.Wrap(err, "service.Account.MakeTransfer failed") 62 | } 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /internal/app/service/admin_user.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/ic3network/mccs-alpha/internal/app/repositories/mongo" 5 | "github.com/ic3network/mccs-alpha/internal/app/types" 6 | "github.com/ic3network/mccs-alpha/internal/pkg/bcrypt" 7 | "github.com/ic3network/mccs-alpha/internal/pkg/e" 8 | "go.mongodb.org/mongo-driver/bson/primitive" 9 | ) 10 | 11 | type adminUser struct{} 12 | 13 | var AdminUser = &adminUser{} 14 | 15 | func (a *adminUser) Login( 16 | email string, 17 | password string, 18 | ) (*types.AdminUser, error) { 19 | user, err := mongo.AdminUser.FindByEmail(email) 20 | if err != nil { 21 | return &types.AdminUser{}, e.Wrap(err, "login admin user failed") 22 | } 23 | 24 | err = bcrypt.CompareHash(user.Password, password) 25 | if err != nil { 26 | return &types.AdminUser{}, e.New(e.PasswordIncorrect, err) 27 | } 28 | 29 | return user, nil 30 | } 31 | 32 | func (a *adminUser) FindByID(id primitive.ObjectID) (*types.AdminUser, error) { 33 | adminUser, err := mongo.AdminUser.FindByID(id) 34 | if err != nil { 35 | return nil, e.Wrap(err, "service.AdminUser.FindByID failed") 36 | } 37 | return adminUser, nil 38 | } 39 | 40 | func (a *adminUser) FindByEmail(email string) (*types.AdminUser, error) { 41 | adminUser, err := mongo.AdminUser.FindByEmail(email) 42 | if err != nil { 43 | return nil, e.Wrap(err, "service.AdminUser.FindByEmail failed") 44 | } 45 | return adminUser, nil 46 | } 47 | 48 | func (a *adminUser) UpdateLoginInfo(id primitive.ObjectID, ip string) error { 49 | loginInfo, err := mongo.AdminUser.GetLoginInfo(id) 50 | if err != nil { 51 | return e.Wrap(err, "service.AdminUser.UpdateLoginInfo failed") 52 | } 53 | 54 | newLoginInfo := &types.LoginInfo{ 55 | CurrentLoginIP: ip, 56 | LastLoginIP: loginInfo.CurrentLoginIP, 57 | LastLoginDate: loginInfo.CurrentLoginDate, 58 | } 59 | 60 | err = mongo.AdminUser.UpdateLoginInfo(id, newLoginInfo) 61 | if err != nil { 62 | return e.Wrap(err, "AdminUserService UpdateLoginInfo failed") 63 | } 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /internal/app/service/balance_limit.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/ic3network/mccs-alpha/internal/app/repositories/pg" 7 | "github.com/ic3network/mccs-alpha/internal/app/types" 8 | "github.com/ic3network/mccs-alpha/internal/pkg/e" 9 | ) 10 | 11 | type balanceLimit struct{} 12 | 13 | var BalanceLimit = balanceLimit{} 14 | 15 | func (b balanceLimit) FindByAccountID(id uint) (*types.BalanceLimit, error) { 16 | record, err := pg.BalanceLimit.FindByAccountID(id) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return record, nil 21 | } 22 | 23 | func (b balanceLimit) FindByBusinessID(id string) (*types.BalanceLimit, error) { 24 | account, err := Account.FindByBusinessID(id) 25 | if err != nil { 26 | return nil, err 27 | } 28 | record, err := pg.BalanceLimit.FindByAccountID(account.ID) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return record, nil 33 | } 34 | 35 | func (b balanceLimit) GetMaxPosBalance(id uint) (float64, error) { 36 | balanceLimitRecord, err := pg.BalanceLimit.FindByAccountID(id) 37 | if err != nil { 38 | return 0, e.Wrap(err, "service.BalanceLimit.GetMaxPosBalance failed") 39 | } 40 | return balanceLimitRecord.MaxPosBal, nil 41 | } 42 | 43 | func (b balanceLimit) GetMaxNegBalance(id uint) (float64, error) { 44 | balanceLimitRecord, err := pg.BalanceLimit.FindByAccountID(id) 45 | if err != nil { 46 | return 0, e.Wrap(err, "service.BalanceLimit.GetMaxNegBalance failed") 47 | } 48 | return math.Abs(balanceLimitRecord.MaxNegBal), nil 49 | } 50 | 51 | // IsExceedLimit checks whether or not the account exceeds the max positive or max negative limit. 52 | func (b balanceLimit) IsExceedLimit(id uint, balance float64) (bool, error) { 53 | balanceLimitRecord, err := pg.BalanceLimit.FindByAccountID(id) 54 | if err != nil { 55 | return false, e.Wrap(err, "service.BalanceLimit.FindByAccountID failed") 56 | } 57 | // MaxNegBal should be positive in the DB. 58 | if balance < -(math.Abs(balanceLimitRecord.MaxNegBal)) || 59 | balance > balanceLimitRecord.MaxPosBal { 60 | return true, nil 61 | } 62 | return false, nil 63 | } 64 | 65 | func (b balanceLimit) Update( 66 | id uint, 67 | maxPosBal float64, 68 | maxNegBal float64, 69 | ) error { 70 | err := pg.BalanceLimit.Update(id, maxPosBal, maxNegBal) 71 | if err != nil { 72 | return err 73 | } 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /internal/app/service/balancecheck/balancecheck.go: -------------------------------------------------------------------------------- 1 | package balancecheck 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ic3network/mccs-alpha/internal/app/repositories/pg" 7 | "github.com/ic3network/mccs-alpha/internal/pkg/email" 8 | "github.com/ic3network/mccs-alpha/internal/pkg/l" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | // Run will check whether the last past 5 hours the sum of the balance in the posting table is zero. 13 | func Run() { 14 | to := time.Now() 15 | from := to.Add(-5 * time.Hour) 16 | 17 | postings, err := pg.Posting.FindInRange(from, to) 18 | if err != nil { 19 | l.Logger.Error("checking balance failed", zap.Error(err)) 20 | return 21 | } 22 | 23 | var sum float64 24 | for _, p := range postings { 25 | sum += p.Amount 26 | } 27 | 28 | if sum != 0.0 { 29 | err := email.Balance.NonZeroBalance(from, to) 30 | if err != nil { 31 | l.Logger.Error( 32 | "sending NonZeroBalance email failed", 33 | zap.Error(err), 34 | ) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/app/service/business.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ic3network/mccs-alpha/internal/app/repositories/es" 7 | "github.com/ic3network/mccs-alpha/internal/app/repositories/mongo" 8 | "github.com/ic3network/mccs-alpha/internal/app/types" 9 | "github.com/ic3network/mccs-alpha/internal/pkg/e" 10 | "go.mongodb.org/mongo-driver/bson/primitive" 11 | ) 12 | 13 | type business struct{} 14 | 15 | var Business = &business{} 16 | 17 | func (b *business) FindByID(id primitive.ObjectID) (*types.Business, error) { 18 | bs, err := mongo.Business.FindByID(id) 19 | if err != nil { 20 | return nil, err 21 | } 22 | return bs, nil 23 | } 24 | 25 | func (b *business) Create( 26 | business *types.BusinessData, 27 | ) (primitive.ObjectID, error) { 28 | id, err := mongo.Business.Create(business) 29 | if err != nil { 30 | return primitive.ObjectID{}, e.Wrap(err, "create business failed") 31 | } 32 | err = es.Business.Create(id, business) 33 | if err != nil { 34 | return primitive.ObjectID{}, e.Wrap(err, "create business failed") 35 | } 36 | return id, nil 37 | } 38 | 39 | func (b *business) UpdateBusiness( 40 | id primitive.ObjectID, 41 | business *types.BusinessData, 42 | isAdmin bool, 43 | ) error { 44 | err := es.Business.UpdateBusiness(id, business) 45 | if err != nil { 46 | return e.Wrap(err, "update business failed") 47 | } 48 | err = mongo.Business.UpdateBusiness(id, business, isAdmin) 49 | if err != nil { 50 | return e.Wrap(err, "update business failed") 51 | } 52 | return nil 53 | } 54 | 55 | func (b *business) SetMemberStartedAt(id primitive.ObjectID) error { 56 | err := mongo.Business.SetMemberStartedAt(id) 57 | if err != nil { 58 | return err 59 | } 60 | return nil 61 | } 62 | 63 | func (b *business) UpdateAllTagsCreatedAt( 64 | id primitive.ObjectID, 65 | t time.Time, 66 | ) error { 67 | err := es.Business.UpdateAllTagsCreatedAt(id, t) 68 | if err != nil { 69 | return e.Wrap(err, "BusinessService UpdateAllTagsCreatedAt failed") 70 | } 71 | err = mongo.Business.UpdateAllTagsCreatedAt(id, t) 72 | if err != nil { 73 | return e.Wrap(err, "BusinessService UpdateAllTagsCreatedAt failed") 74 | } 75 | return nil 76 | } 77 | 78 | func (b *business) FindBusiness( 79 | c *types.SearchCriteria, 80 | page int64, 81 | ) (*types.FindBusinessResult, error) { 82 | ids, numberOfResults, totalPages, err := es.Business.Find(c, page) 83 | if err != nil { 84 | return nil, e.Wrap(err, "BusinessService FindBusiness failed") 85 | } 86 | businesses, err := mongo.Business.FindByIDs(ids) 87 | if err != nil { 88 | return nil, e.Wrap(err, "BusinessService FindBusiness failed") 89 | } 90 | return &types.FindBusinessResult{ 91 | Businesses: businesses, 92 | NumberOfResults: numberOfResults, 93 | TotalPages: totalPages, 94 | }, nil 95 | } 96 | 97 | func (b *business) DeleteByID(id primitive.ObjectID) error { 98 | err := es.Business.Delete(id.Hex()) 99 | if err != nil { 100 | return e.Wrap(err, "delete business by id failed") 101 | } 102 | err = mongo.Business.DeleteByID(id) 103 | if err != nil { 104 | return e.Wrap(err, "delete business by id failed") 105 | } 106 | return nil 107 | } 108 | 109 | func (b *business) RenameTag(old string, new string) error { 110 | err := es.Business.RenameTag(old, new) 111 | if err != nil { 112 | return e.Wrap(err, "BusinessMongo RenameTag failed") 113 | } 114 | err = mongo.Business.RenameTag(old, new) 115 | if err != nil { 116 | return e.Wrap(err, "BusinessMongo RenameTag failed") 117 | } 118 | return nil 119 | } 120 | 121 | func (b *business) RenameAdminTag(old string, new string) error { 122 | err := es.Business.RenameAdminTag(old, new) 123 | if err != nil { 124 | return e.Wrap(err, "BusinessMongo RenameAdminTag failed") 125 | } 126 | err = mongo.Business.RenameAdminTag(old, new) 127 | if err != nil { 128 | return e.Wrap(err, "BusinessMongo RenameAdminTag failed") 129 | } 130 | return nil 131 | } 132 | 133 | func (b *business) DeleteTag(name string) error { 134 | err := es.Business.DeleteTag(name) 135 | if err != nil { 136 | return e.Wrap(err, "BusinessMongo DeleteTag failed") 137 | } 138 | err = mongo.Business.DeleteTag(name) 139 | if err != nil { 140 | return e.Wrap(err, "BusinessMongo DeleteTag failed") 141 | } 142 | return nil 143 | } 144 | 145 | func (b *business) DeleteAdminTags(name string) error { 146 | err := es.Business.DeleteAdminTags(name) 147 | if err != nil { 148 | return e.Wrap(err, "BusinessMongo DeleteAdminTags failed") 149 | } 150 | err = mongo.Business.DeleteAdminTags(name) 151 | if err != nil { 152 | return e.Wrap(err, "BusinessMongo DeleteAdminTags failed") 153 | } 154 | return nil 155 | } 156 | -------------------------------------------------------------------------------- /internal/app/service/dailyemail/dailyemail.go: -------------------------------------------------------------------------------- 1 | package dailyemail 2 | 3 | import ( 4 | "github.com/ic3network/mccs-alpha/internal/app/service" 5 | "github.com/ic3network/mccs-alpha/internal/app/types" 6 | "github.com/ic3network/mccs-alpha/internal/pkg/e" 7 | "github.com/ic3network/mccs-alpha/internal/pkg/email" 8 | "github.com/ic3network/mccs-alpha/internal/pkg/helper" 9 | "github.com/ic3network/mccs-alpha/internal/pkg/l" 10 | "github.com/spf13/viper" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | // Run performs the daily email notification. 15 | func Run() { 16 | users, err := service.User.FindByDailyNotification() 17 | if err != nil { 18 | l.Logger.Error("dailyemail failed", zap.Error(err)) 19 | return 20 | } 21 | 22 | viper.SetDefault("concurrency_num", 1) 23 | pool := NewPool(viper.GetInt("concurrency_num")) 24 | 25 | for _, user := range users { 26 | worker := createEmailWorker(user) 27 | pool.Run(worker) 28 | } 29 | 30 | pool.Shutdown() 31 | } 32 | 33 | func createEmailWorker(u *types.User) func() { 34 | return func() { 35 | matchedTags, err := getMatchTags(u) 36 | if err != nil { 37 | l.Logger.Error("dailyemail failed", zap.Error(err)) 38 | return 39 | } 40 | if len(matchedTags.MatchedOffers) == 0 && 41 | len(matchedTags.MatchedWants) == 0 { 42 | return 43 | } 44 | err = email.SendDailyEmailList(u, matchedTags) 45 | if err != nil { 46 | l.Logger.Error("dailyemail failed", zap.Error(err)) 47 | } 48 | err = service.User.UpdateLastNotificationSentDate(u.ID) 49 | if err != nil { 50 | l.Logger.Error("dailyemail failed", zap.Error(err)) 51 | } 52 | } 53 | } 54 | 55 | func getMatchTags(user *types.User) (*types.MatchedTags, error) { 56 | business, err := service.Business.FindByID(user.CompanyID) 57 | if err != nil { 58 | return nil, e.Wrap(err, "getMatchTags failed") 59 | } 60 | 61 | matchedOffers, err := service.Tag.MatchOffers( 62 | helper.GetTagNames(business.Offers), 63 | user.LastNotificationSentDate, 64 | ) 65 | if err != nil { 66 | return nil, e.Wrap(err, "getMatchTags failed") 67 | } 68 | matchedWants, err := service.Tag.MatchWants( 69 | helper.GetTagNames(business.Wants), 70 | user.LastNotificationSentDate, 71 | ) 72 | if err != nil { 73 | return nil, e.Wrap(err, "getMatchTags failed") 74 | } 75 | 76 | return &types.MatchedTags{ 77 | MatchedOffers: matchedOffers, 78 | MatchedWants: matchedWants, 79 | }, nil 80 | } 81 | -------------------------------------------------------------------------------- /internal/app/service/dailyemail/work.go: -------------------------------------------------------------------------------- 1 | package dailyemail 2 | 3 | import "sync" 4 | 5 | type Pool struct { 6 | worker chan func() 7 | wg sync.WaitGroup 8 | } 9 | 10 | func NewPool(maxGoroutines int) *Pool { 11 | p := Pool{ 12 | worker: make(chan func()), 13 | } 14 | 15 | p.wg.Add(maxGoroutines) 16 | for i := 0; i < maxGoroutines; i++ { 17 | go func() { 18 | for w := range p.worker { 19 | w() 20 | } 21 | p.wg.Done() 22 | }() 23 | } 24 | 25 | return &p 26 | } 27 | 28 | func (p *Pool) Run(w func()) { 29 | p.worker <- w 30 | } 31 | 32 | func (p *Pool) Shutdown() { 33 | close(p.worker) 34 | p.wg.Wait() 35 | } 36 | -------------------------------------------------------------------------------- /internal/app/service/lostpassword.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ic3network/mccs-alpha/internal/app/repositories/mongo" 7 | "github.com/ic3network/mccs-alpha/internal/app/types" 8 | "github.com/ic3network/mccs-alpha/internal/pkg/e" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | type lostpassword struct{} 13 | 14 | var Lostpassword = &lostpassword{} 15 | 16 | func (s *lostpassword) Create(l *types.LostPassword) error { 17 | err := mongo.LostPassword.Create(l) 18 | if err != nil { 19 | return e.Wrap(err, "Create failed") 20 | } 21 | return nil 22 | } 23 | 24 | func (s *lostpassword) FindByToken(token string) (*types.LostPassword, error) { 25 | lostPassword, err := mongo.LostPassword.FindByToken(token) 26 | if err != nil { 27 | return nil, e.Wrap(err, "FindByToken failed") 28 | } 29 | return lostPassword, nil 30 | } 31 | 32 | func (s *lostpassword) FindByEmail(email string) (*types.LostPassword, error) { 33 | lostPassword, err := mongo.LostPassword.FindByEmail(email) 34 | if err != nil { 35 | return nil, e.Wrap(err, "FindByEmail failed") 36 | } 37 | return lostPassword, nil 38 | } 39 | 40 | func (s *lostpassword) SetTokenUsed(token string) error { 41 | err := mongo.LostPassword.SetTokenUsed(token) 42 | if err != nil { 43 | return e.Wrap(err, "SetTokenUsed failed") 44 | } 45 | return nil 46 | } 47 | 48 | func (s *lostpassword) TokenInvalid(l *types.LostPassword) bool { 49 | if time.Now(). 50 | Sub(l.CreatedAt). 51 | Seconds() >= 52 | viper.GetFloat64( 53 | "reset_password_timeout", 54 | ) || 55 | l.TokenUsed == true { 56 | return true 57 | } 58 | return false 59 | } 60 | -------------------------------------------------------------------------------- /internal/app/service/tag.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ic3network/mccs-alpha/internal/app/repositories/es" 7 | "github.com/ic3network/mccs-alpha/internal/app/repositories/mongo" 8 | "github.com/ic3network/mccs-alpha/internal/app/types" 9 | "github.com/ic3network/mccs-alpha/internal/pkg/e" 10 | "go.mongodb.org/mongo-driver/bson/primitive" 11 | ) 12 | 13 | type tag struct{} 14 | 15 | var Tag = &tag{} 16 | 17 | func (t *tag) Create(name string) error { 18 | id, err := mongo.Tag.Create(name) 19 | if err != nil { 20 | return e.Wrap(err, "TagService Create failed") 21 | } 22 | err = es.Tag.Create(id, name) 23 | if err != nil { 24 | return e.Wrap(err, "TagService Create failed") 25 | } 26 | return nil 27 | } 28 | 29 | // UpdateOffer will add/modify the offer tag. 30 | func (t *tag) UpdateOffer(name string) error { 31 | id, err := mongo.Tag.UpdateOffer(name) 32 | if err != nil { 33 | return e.Wrap(err, "TagService UpdateOffer failed") 34 | } 35 | err = es.Tag.UpdateOffer(id.Hex(), name) 36 | if err != nil { 37 | return e.Wrap(err, "TagService UpdateOffer failed") 38 | } 39 | return nil 40 | } 41 | 42 | // UpdateWant will add/modify the want tag. 43 | func (t *tag) UpdateWant(name string) error { 44 | id, err := mongo.Tag.UpdateWant(name) 45 | if err != nil { 46 | return e.Wrap(err, "TagService UpdateWant failed") 47 | } 48 | err = es.Tag.UpdateWant(id.Hex(), name) 49 | if err != nil { 50 | return e.Wrap(err, "TagService UpdateWant failed") 51 | } 52 | return nil 53 | } 54 | 55 | func (t *tag) FindByName(name string) (*types.Tag, error) { 56 | tag, err := mongo.Tag.FindByName(name) 57 | if err != nil { 58 | return nil, e.Wrap(err, "TagService FindTag failed") 59 | } 60 | return tag, nil 61 | } 62 | 63 | func (t *tag) FindByID(id primitive.ObjectID) (*types.Tag, error) { 64 | tag, err := mongo.Tag.FindByID(id) 65 | if err != nil { 66 | return nil, e.Wrap(err, "TagService FindByID failed") 67 | } 68 | return tag, nil 69 | } 70 | 71 | func (t *tag) FindTags(name string, page int64) (*types.FindTagResult, error) { 72 | result, err := mongo.Tag.FindTags(name, page) 73 | if err != nil { 74 | return nil, e.Wrap(err, "TagService FindTags failed") 75 | } 76 | return result, nil 77 | } 78 | 79 | func (t *tag) Rename(tag *types.Tag) error { 80 | err := es.Tag.Rename(tag) 81 | if err != nil { 82 | return e.Wrap(err, "TagService Rename failed") 83 | } 84 | err = mongo.Tag.Rename(tag) 85 | if err != nil { 86 | return e.Wrap(err, "TagService Rename failed") 87 | } 88 | return nil 89 | } 90 | 91 | func (t *tag) DeleteByID(id primitive.ObjectID) error { 92 | err := es.Tag.DeleteByID(id.Hex()) 93 | if err != nil { 94 | return e.Wrap(err, "TagService DeleteByID failed") 95 | } 96 | err = mongo.Tag.DeleteByID(id) 97 | if err != nil { 98 | return e.Wrap(err, "TagService DeleteByID failed") 99 | } 100 | return nil 101 | } 102 | 103 | // MatchOffers loops through user's offers and finds out the matched wants. 104 | // Only add to the result when matches more than one tag. 105 | func (t *tag) MatchOffers( 106 | offers []string, 107 | lastLoginDate time.Time, 108 | ) (map[string][]string, error) { 109 | resultMap := make(map[string][]string, len(offers)) 110 | 111 | for _, offer := range offers { 112 | matches, err := es.Tag.MatchOffer(offer, lastLoginDate) 113 | if err != nil { 114 | return nil, e.Wrap(err, "TagService MatchOffers failed") 115 | } 116 | if len(matches) > 0 { 117 | resultMap[offer] = matches 118 | } 119 | } 120 | 121 | return resultMap, nil 122 | } 123 | 124 | // MatchWants loops through user's wants and finds out the matched offers. 125 | // Only add to the result when matches more than one tag. 126 | func (t *tag) MatchWants( 127 | wants []string, 128 | lastLoginDate time.Time, 129 | ) (map[string][]string, error) { 130 | resultMap := make(map[string][]string, len(wants)) 131 | 132 | for _, want := range wants { 133 | matches, err := es.Tag.MatchWant(want, lastLoginDate) 134 | if err != nil { 135 | return nil, e.Wrap(err, "TagService MatchWants failed") 136 | } 137 | if len(matches) > 0 { 138 | resultMap[want] = matches 139 | } 140 | } 141 | 142 | return resultMap, nil 143 | } 144 | -------------------------------------------------------------------------------- /internal/app/service/trading.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/ic3network/mccs-alpha/internal/app/repositories/es" 5 | "github.com/ic3network/mccs-alpha/internal/app/repositories/mongo" 6 | "github.com/ic3network/mccs-alpha/internal/app/types" 7 | "go.mongodb.org/mongo-driver/bson/primitive" 8 | ) 9 | 10 | type trading struct{} 11 | 12 | var Trading = &trading{} 13 | 14 | func (t *trading) UpdateBusiness( 15 | id primitive.ObjectID, 16 | data *types.TradingRegisterData, 17 | ) error { 18 | err := es.Business.UpdateTradingInfo(id, data) 19 | if err != nil { 20 | return err 21 | } 22 | err = mongo.Business.UpdateTradingInfo(id, data) 23 | if err != nil { 24 | return err 25 | } 26 | return nil 27 | } 28 | 29 | func (t *trading) UpdateUser( 30 | id primitive.ObjectID, 31 | data *types.TradingRegisterData, 32 | ) error { 33 | err := es.User.UpdateTradingInfo(id, data) 34 | if err != nil { 35 | return err 36 | } 37 | err = mongo.User.UpdateTradingInfo(id, data) 38 | if err != nil { 39 | return err 40 | } 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /internal/app/service/user_action.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/ic3network/mccs-alpha/internal/app/repositories/mongo" 5 | "github.com/ic3network/mccs-alpha/internal/app/types" 6 | "github.com/ic3network/mccs-alpha/internal/pkg/e" 7 | ) 8 | 9 | type userAction struct{} 10 | 11 | var UserAction = &userAction{} 12 | 13 | func (u *userAction) Log(log *types.UserAction) error { 14 | if log == nil { 15 | return nil 16 | } 17 | err := mongo.UserAction.Log(log) 18 | if err != nil { 19 | return e.Wrap(err, "UserActionService Log failed") 20 | } 21 | return nil 22 | } 23 | 24 | func (u *userAction) Find( 25 | c *types.UserActionSearchCriteria, 26 | page int64, 27 | ) ([]*types.UserAction, int, error) { 28 | userActions, totalPages, err := mongo.UserAction.Find(c, page) 29 | if err != nil { 30 | return nil, 0, e.Wrap(err, "UserActionService Find failed") 31 | } 32 | return userActions, totalPages, nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/app/types/account.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | type Account struct { 8 | gorm.Model 9 | // Account has many postings, AccountID is the foreign key 10 | Postings []Posting 11 | BusinessID string `gorm:"type:varchar(24);not null;unique_index"` 12 | Balance float64 `gorm:"not null;default:0"` 13 | } 14 | -------------------------------------------------------------------------------- /internal/app/types/admin_tag.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // AdminTag is the model representation of an admin tag in the data model. 10 | type AdminTag struct { 11 | ID primitive.ObjectID `json:"_id,omitempty" bson:"_id,omitempty"` 12 | CreatedAt time.Time `json:"createdAt,omitempty" bson:"createdAt,omitempty"` 13 | UpdatedAt time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` 14 | DeletedAt time.Time `json:"deletedAt,omitempty" bson:"deletedAt,omitempty"` 15 | 16 | Name string `json:"name,omitempty" bson:"name,omitempty"` 17 | } 18 | 19 | // Helper types 20 | 21 | type FindAdminTagResult struct { 22 | AdminTags []*AdminTag 23 | NumberOfResults int 24 | TotalPages int 25 | } 26 | -------------------------------------------------------------------------------- /internal/app/types/admin_user.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // AdminUser is the model representation of an admin user in the data model. 10 | type AdminUser struct { 11 | ID primitive.ObjectID `json:"_id,omitempty" bson:"_id,omitempty"` 12 | CreatedAt time.Time `json:"createdAt,omitempty" bson:"createdAt,omitempty"` 13 | UpdatedAt time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` 14 | DeletedAt time.Time `json:"deletedAt,omitempty" bson:"deletedAt,omitempty"` 15 | 16 | Email string `json:"email,omitempty" bson:"email,omitempty"` 17 | Name string `json:"name,omitempty" bson:"name,omitempty"` 18 | Password string `json:"password,omitempty" bson:"password,omitempty"` 19 | Roles []string `json:"roles,omitempty" bson:"roles,omitempty"` 20 | 21 | CurrentLoginIP string `json:"currentLoginIP,omitempty" bson:"currentLoginIP,omitempty"` 22 | CurrentLoginDate time.Time `json:"currentLoginDate,omitempty" bson:"currentLoginDate,omitempty"` 23 | LastLoginIP string `json:"lastLoginIP,omitempty" bson:"lastLoginIP,omitempty"` 24 | LastLoginDate time.Time `json:"lastLoginDate,omitempty" bson:"lastLoginDate,omitempty"` 25 | } 26 | -------------------------------------------------------------------------------- /internal/app/types/balance_limit.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | type BalanceLimit struct { 8 | gorm.Model 9 | // `BalanceLimit` belongs to `Account`, `AccountID` is the foreign key 10 | Account Account 11 | AccountID uint `gorm:"not null;unique_index"` 12 | MaxNegBal float64 `gorm:"type:int;not null"` 13 | MaxPosBal float64 `gorm:"type:int;not null"` 14 | } 15 | -------------------------------------------------------------------------------- /internal/app/types/business.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // Business is the model representation of a business in the data model. 10 | type Business struct { 11 | ID primitive.ObjectID `json:"_id,omitempty" bson:"_id,omitempty"` 12 | CreatedAt time.Time `json:"createdAt,omitempty" bson:"createdAt,omitempty"` 13 | UpdatedAt time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` 14 | DeletedAt time.Time `json:"deletedAt,omitempty" bson:"deletedAt,omitempty"` 15 | 16 | BusinessName string `json:"businessName,omitempty" bson:"businessName,omitempty"` 17 | BusinessPhone string `json:"businessPhone,omitempty" bson:"businessPhone,omitempty"` 18 | IncType string `json:"incType,omitempty" bson:"incType,omitempty"` 19 | CompanyNumber string `json:"companyNumber,omitempty" bson:"companyNumber,omitempty"` 20 | Website string `json:"website,omitempty" bson:"website,omitempty"` 21 | Turnover int `json:"turnover,omitempty" bson:"turnover,omitempty"` 22 | Offers []*TagField `json:"offers,omitempty" bson:"offers,omitempty"` 23 | Wants []*TagField `json:"wants,omitempty" bson:"wants,omitempty"` 24 | Description string `json:"description,omitempty" bson:"description,omitempty"` 25 | LocationAddress string `json:"locationAddress,omitempty" bson:"locationAddress,omitempty"` 26 | LocationCity string `json:"locationCity,omitempty" bson:"locationCity,omitempty"` 27 | LocationRegion string `json:"locationRegion,omitempty" bson:"locationRegion,omitempty"` 28 | LocationPostalCode string `json:"locationPostalCode,omitempty" bson:"locationPostalCode,omitempty"` 29 | LocationCountry string `json:"locationCountry,omitempty" bson:"locationCountry,omitempty"` 30 | Status string `json:"status,omitempty" bson:"status,omitempty"` 31 | AdminTags []string `json:"adminTags,omitempty" bson:"adminTags,omitempty"` 32 | // Timestamp when trading status applied 33 | MemberStartedAt time.Time `json:"memberStartedAt,omitempty" bson:"memberStartedAt,omitempty"` 34 | } 35 | 36 | type TagField struct { 37 | Name string `json:"name,omitempty" bson:"name,omitempty"` 38 | CreatedAt time.Time `json:"createdAt,omitempty" bson:"createdAt,omitempty"` 39 | } 40 | 41 | // BusinessESRecord is the data that will store into the elastic search. 42 | type BusinessESRecord struct { 43 | BusinessID string `json:"businessID,omitempty"` 44 | BusinessName string `json:"businessName,omitempty"` 45 | Offers []*TagField `json:"offers,omitempty"` 46 | Wants []*TagField `json:"wants,omitempty"` 47 | LocationCity string `json:"locationCity,omitempty"` 48 | LocationCountry string `json:"locationCountry,omitempty"` 49 | Status string `json:"status,omitempty"` 50 | AdminTags []string `json:"adminTags,omitempty"` 51 | } 52 | 53 | // Helper types 54 | 55 | type BusinessData struct { 56 | ID primitive.ObjectID 57 | BusinessName string 58 | IncType string 59 | CompanyNumber string 60 | BusinessPhone string 61 | Website string 62 | Turnover int 63 | Offers []*TagField 64 | Wants []*TagField 65 | OffersAdded []string 66 | OffersRemoved []string 67 | WantsAdded []string 68 | WantsRemoved []string 69 | Description string 70 | LocationAddress string 71 | LocationCity string 72 | LocationRegion string 73 | LocationPostalCode string 74 | LocationCountry string 75 | Status string 76 | AdminTags []string 77 | } 78 | 79 | type SearchCriteria struct { 80 | TagType string 81 | Tags []*TagField 82 | CreatedOnOrAfter time.Time 83 | 84 | Statuses []string // accepted", "pending", rejected", "tradingPending", "tradingAccepted", "tradingRejected" 85 | BusinessName string 86 | LocationCountry string 87 | LocationCity string 88 | ShowUserFavoritesOnly bool 89 | FavoriteBusinesses []primitive.ObjectID 90 | AdminTag string 91 | } 92 | 93 | type FindBusinessResult struct { 94 | Businesses []*Business 95 | NumberOfResults int 96 | TotalPages int 97 | } 98 | -------------------------------------------------------------------------------- /internal/app/types/form.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type RegisterData struct { 4 | User *User 5 | Business *BusinessData 6 | ConfirmPassword string 7 | ConfirmEmail string 8 | Terms string 9 | RecaptchaSitekey string 10 | } 11 | 12 | type UpdateAccountData struct { 13 | User *User 14 | Business *BusinessData 15 | Balance *BalanceLimit 16 | CurrentPassword string 17 | ConfirmPassword string 18 | } 19 | -------------------------------------------------------------------------------- /internal/app/types/journal.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | type Journal struct { 8 | gorm.Model 9 | // Journal has many postings, JournalID is the foreign key 10 | Postings []Posting 11 | 12 | TransactionID string `gorm:"type:varchar(27);not null;default:''"` 13 | 14 | InitiatedBy uint `gorm:"type:int;not null;default:0"` 15 | 16 | FromID uint `gorm:"type:int;not null;default:0"` 17 | FromEmail string `gorm:"type:varchar(120);not null;default:''"` 18 | FromBusinessName string `gorm:"type:varchar(120);not null;default:''"` 19 | 20 | ToID uint `gorm:"type:int;not null;default:0"` 21 | ToEmail string `gorm:"type:varchar(120);not null;default:''"` 22 | ToBusinessName string `gorm:"type:varchar(120);not null;default:''"` 23 | 24 | Amount float64 `gorm:"not null;default:0"` 25 | Description string `gorm:"type:varchar(510);not null;default:''"` 26 | Type string `gorm:"type:varchar(31);not null;default:'transfer'"` 27 | Status string `gorm:"type:varchar(31);not null;default:''"` 28 | 29 | CancellationReason string `gorm:"type:varchar(510);not null;default:''"` 30 | } 31 | -------------------------------------------------------------------------------- /internal/app/types/lost_password.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // LostPassword is the model representation of a lost password in the data model. 10 | type LostPassword struct { 11 | ID primitive.ObjectID `json:"_id,omitempty" bson:"_id,omitempty"` 12 | CreatedAt time.Time `json:"createdAt,omitempty" bson:"createdAt,omitempty"` 13 | Email string `json:"email,omitempty" bson:"email,omitempty"` 14 | Token string `json:"token,omitempty" bson:"token,omitempty"` 15 | TokenUsed bool `json:"tokenUsed,omitempty" bson:"tokenUsed,omitempty"` 16 | } 17 | -------------------------------------------------------------------------------- /internal/app/types/posting.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | type Posting struct { 8 | gorm.Model 9 | AccountID uint `gorm:"not null"` 10 | JournalID uint `gorm:"not null"` 11 | Amount float64 `gorm:"not null"` 12 | } 13 | -------------------------------------------------------------------------------- /internal/app/types/tag.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // Tag is the model representation of a tag in the data model. 10 | type Tag struct { 11 | ID primitive.ObjectID `json:"_id,omitempty" bson:"_id,omitempty"` 12 | CreatedAt time.Time `json:"createdAt,omitempty" bson:"createdAt,omitempty"` 13 | UpdatedAt time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` 14 | DeletedAt time.Time `json:"deletedAt,omitempty" bson:"deletedAt,omitempty"` 15 | 16 | Name string `json:"name,omitempty" bson:"name,omitempty"` 17 | OfferAddedAt time.Time `json:"offerAddedAt,omitempty" bson:"offerAddedAt,omitempty"` 18 | WantAddedAt time.Time `json:"wantAddedAt,omitempty" bson:"wantAddedAt,omitempty"` 19 | } 20 | 21 | type TagESRecord struct { 22 | TagID string `json:"tagID,omitempty"` 23 | Name string `json:"name,omitempty"` 24 | OfferAddedAt time.Time `json:"offerAddedAt,omitempty"` 25 | WantAddedAt time.Time `json:"wantAddedAt,omitempty"` 26 | } 27 | 28 | // Helper types 29 | 30 | type FindTagResult struct { 31 | Tags []*Tag 32 | NumberOfResults int 33 | TotalPages int 34 | } 35 | 36 | type MatchedTags struct { 37 | MatchedOffers map[string][]string 38 | MatchedWants map[string][]string 39 | } 40 | -------------------------------------------------------------------------------- /internal/app/types/transaction.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Transaction struct { 8 | ID uint // Journal ID 9 | TransactionID string 10 | IsInitiator bool 11 | InitiatedBy uint 12 | FromID uint 13 | FromEmail string 14 | FromBusinessName string 15 | ToID uint 16 | ToEmail string 17 | ToBusinessName string 18 | Amount float64 19 | Description string 20 | Status string 21 | CreatedAt time.Time 22 | } 23 | -------------------------------------------------------------------------------- /internal/app/types/user.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // LoginInfo is shared by user and admin user model. 10 | type LoginInfo struct { 11 | CurrentLoginIP string `json:"currentLoginIP,omitempty" bson:"currentLoginIP,omitempty"` 12 | CurrentLoginDate time.Time `json:"currentLoginDate,omitempty" bson:"currentLoginDate,omitempty"` 13 | LastLoginIP string `json:"lastLoginIP,omitempty" bson:"lastLoginIP,omitempty"` 14 | LastLoginDate time.Time `json:"lastLoginDate,omitempty" bson:"lastLoginDate,omitempty"` 15 | } 16 | 17 | // User is the model representation of an user in the data model. 18 | type User struct { 19 | ID primitive.ObjectID `json:"_id,omitempty" bson:"_id,omitempty"` 20 | CreatedAt time.Time `json:"createdAt,omitempty" bson:"createdAt,omitempty"` 21 | UpdatedAt time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` 22 | DeletedAt time.Time `json:"deletedAt,omitempty" bson:"deletedAt,omitempty"` 23 | 24 | FirstName string `json:"firstName,omitempty" bson:"firstName,omitempty"` 25 | LastName string `json:"lastName,omitempty" bson:"lastName,omitempty"` 26 | Email string `json:"email,omitempty" bson:"email,omitempty"` 27 | Password string `json:"password,omitempty" bson:"password,omitempty"` 28 | Telephone string `json:"telephone,omitempty" bson:"telephone,omitempty"` 29 | CompanyID primitive.ObjectID `json:"companyID,omitempty" bson:"companyID,omitempty"` 30 | 31 | CurrentLoginIP string `json:"currentLoginIP,omitempty" bson:"currentLoginIP,omitempty"` 32 | CurrentLoginDate time.Time `json:"currentLoginDate,omitempty" bson:"currentLoginDate,omitempty"` 33 | LastLoginIP string `json:"lastLoginIP,omitempty" bson:"lastLoginIP,omitempty"` 34 | LastLoginDate time.Time `json:"lastLoginDate,omitempty" bson:"lastLoginDate,omitempty"` 35 | 36 | LoginAttempts int `json:"loginAttempts,omitempty" bson:"loginAttempts,omitempty"` 37 | LastLoginFailDate time.Time `json:"lastLoginFailDate,omitempty" bson:"lastLoginFailDate,omitempty"` 38 | 39 | ShowRecentMatchedTags bool `json:"showRecentMatchedTags,omitempty" bson:"showRecentMatchedTags,omitempty"` 40 | FavoriteBusinesses []primitive.ObjectID `json:"favoriteBusinesses,omitempty" bson:"favoriteBusinesses,omitempty"` 41 | DailyNotification bool `json:"dailyNotification,omitempty" bson:"dailyNotification,omitempty"` 42 | LastNotificationSentDate time.Time `json:"lastNotificationSentDate,omitempty" bson:"lastNotificationSentDate,omitempty"` 43 | } 44 | 45 | // UserESRecord is the data that will store into the elastic search. 46 | type UserESRecord struct { 47 | UserID string `json:"userID"` 48 | FirstName string `json:"firstName,omitempty"` 49 | LastName string `json:"lastName,omitempty"` 50 | Email string `json:"email,omitempty"` 51 | } 52 | 53 | // Helper types 54 | 55 | type FindUserResult struct { 56 | Users []*User 57 | NumberOfResults int 58 | TotalPages int 59 | } 60 | -------------------------------------------------------------------------------- /internal/app/types/user_action.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // UserAction is the model representation of an user action in the data model. 10 | type UserAction struct { 11 | ID primitive.ObjectID `json:"_id,omitempty" bson:"_id,omitempty"` 12 | CreatedAt time.Time `json:"createdAt,omitempty" bson:"createdAt,omitempty"` 13 | UpdatedAt time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` 14 | DeletedAt time.Time `json:"deletedAt,omitempty" bson:"deletedAt,omitempty"` 15 | 16 | UserID primitive.ObjectID `json:"userID,omitempty" bson:"userID,omitempty"` 17 | Email string `json:"email,omitempty" bson:"email,omitempty"` 18 | Action string `json:"action,omitempty" bson:"action,omitempty"` 19 | ActionDetails string `json:"actionDetails,omitempty" bson:"actionDetails,omitempty"` 20 | Category string `json:"category,omitempty" bson:"category,omitempty"` 21 | } 22 | 23 | type UserActionSearchCriteria struct { 24 | Email string 25 | Category string 26 | DateFrom time.Time 27 | DateTo time.Time 28 | } 29 | -------------------------------------------------------------------------------- /internal/migration/migration.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "github.com/ic3network/mccs-alpha/global" 5 | ) 6 | 7 | func init() { 8 | global.Init() 9 | } 10 | -------------------------------------------------------------------------------- /internal/migration/user_action_category.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "github.com/ic3network/mccs-alpha/internal/app/repositories/mongo" 9 | "go.mongodb.org/mongo-driver/bson" 10 | ) 11 | 12 | // SetUserActionCategory sets all the previous existed user actions' category as "user". 13 | // Since we added a new field to the userAction table (category), 14 | // we need to fill the existing userAction records. 15 | // This migration script starts running at 2019-08-20, we can delete this in the near future. 16 | func SetUserActionCategory() { 17 | log.Println("start setting user action category") 18 | startTime := time.Now() 19 | ctx := context.Background() 20 | 21 | filter := bson.M{ 22 | "category": bson.M{"$exists": false}, 23 | } 24 | update := bson.M{ 25 | "$set": bson.M{ 26 | "category": "user", 27 | }, 28 | } 29 | 30 | _, err := mongo.DB(). 31 | Collection("userActions"). 32 | UpdateMany(ctx, filter, update) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | log.Printf("took %v\n\n", time.Now().Sub(startTime)) 38 | } 39 | -------------------------------------------------------------------------------- /internal/pkg/bcrypt/bcrypt.go: -------------------------------------------------------------------------------- 1 | package bcrypt 2 | 3 | import "golang.org/x/crypto/bcrypt" 4 | 5 | // Hash hashes the password. 6 | func Hash(password string) (string, error) { 7 | bytePwd := []byte(password) 8 | hash, err := bcrypt.GenerateFromPassword(bytePwd, bcrypt.DefaultCost) 9 | if err != nil { 10 | return "", err 11 | } 12 | return string(hash), nil 13 | } 14 | 15 | // CompareHash compares the hashedPassword with plainPassword. 16 | func CompareHash(hashedPassword string, plainPassword string) error { 17 | err := bcrypt.CompareHashAndPassword( 18 | []byte(hashedPassword), 19 | []byte(plainPassword), 20 | ) 21 | if err != nil { 22 | return err 23 | } 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /internal/pkg/cookie/cookie.go: -------------------------------------------------------------------------------- 1 | package cookie 2 | 3 | import "net/http" 4 | 5 | // CreateCookie creates the default cookie. 6 | func CreateCookie(value string) *http.Cookie { 7 | return &http.Cookie{ 8 | Name: "mccsToken", 9 | Value: value, 10 | Path: "/", 11 | MaxAge: 86400, 12 | HttpOnly: true, 13 | } 14 | } 15 | 16 | // ResetCookie resets the default cookie. 17 | func ResetCookie() *http.Cookie { 18 | return &http.Cookie{ 19 | Name: "mccsToken", 20 | Value: "", 21 | Path: "/", 22 | MaxAge: -1, 23 | HttpOnly: true, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/pkg/e/code.go: -------------------------------------------------------------------------------- 1 | package e 2 | 3 | const ( 4 | UserNotFound = iota 5 | BusinessNotFound 6 | InternalServerError 7 | EmailExisted 8 | PasswordIncorrect 9 | AccountLocked 10 | TokenInvalid 11 | InvalidPageNumber 12 | ExceedMaxPosBalance 13 | ExceedMaxNegBalance 14 | ) 15 | 16 | var Msg = map[int]string{ 17 | UserNotFound: "Email address not found.", 18 | BusinessNotFound: "Business not found.", 19 | EmailExisted: "Email address is already registered.", 20 | TokenInvalid: "Invalid token.", 21 | PasswordIncorrect: "Invalid password.", 22 | AccountLocked: "Your account has been temporarily locked for 15 minutes. Please try again later.", 23 | InternalServerError: "Sorry, something went wrong. Please try again later.", 24 | InvalidPageNumber: "Invalid page number: should start with 1.", 25 | ExceedMaxPosBalance: "Transfer rejected: receiver will exceed maximum balance limit.", 26 | ExceedMaxNegBalance: "Transfer rejected: you will exceed your maximum negative balance limit.", 27 | } 28 | -------------------------------------------------------------------------------- /internal/pkg/e/errors.go: -------------------------------------------------------------------------------- 1 | package e 2 | 3 | import "github.com/pkg/errors" 4 | 5 | type Error struct { 6 | Code int 7 | CustomMessage string 8 | SystemErr error 9 | } 10 | 11 | func New(code int, systemErr interface{}) error { 12 | err := Error{ 13 | Code: code, 14 | } 15 | if v, ok := systemErr.(error); ok { 16 | err.SystemErr = v 17 | return err 18 | } 19 | if v, ok := systemErr.(string); ok { 20 | err.SystemErr = errors.New(v) 21 | return err 22 | } 23 | err.SystemErr = errors.New("Undefined Error") 24 | return err 25 | } 26 | 27 | func CustomMessage(message string) error { 28 | err := Error{CustomMessage: message} 29 | err.SystemErr = errors.New("custom error") 30 | return err 31 | } 32 | 33 | func (d Error) Error() string { 34 | return d.SystemErr.Error() 35 | } 36 | 37 | func (d Error) Message() string { 38 | if d.CustomMessage != "" { 39 | return d.CustomMessage 40 | } else if msg, ok := Msg[d.Code]; ok { 41 | return msg 42 | } 43 | return Msg[InternalServerError] 44 | } 45 | 46 | func Wrap(err error, message string) error { 47 | if v, ok := err.(Error); ok { 48 | return Error{ 49 | Code: v.Code, 50 | SystemErr: errors.Wrap(v.SystemErr, message), 51 | } 52 | } 53 | return errors.Wrap(err, message) 54 | } 55 | 56 | func IsPasswordInvalid(err error) bool { 57 | if v, ok := err.(Error); ok { 58 | return v.Code == PasswordIncorrect 59 | } 60 | return false 61 | } 62 | 63 | func IsUserNotFound(err error) bool { 64 | if v, ok := err.(Error); ok { 65 | return v.Code == UserNotFound 66 | } 67 | return false 68 | } 69 | -------------------------------------------------------------------------------- /internal/pkg/email/balance.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | type balance struct{} 10 | 11 | var Balance = &balance{} 12 | 13 | func (b *balance) NonZeroBalance(from time.Time, to time.Time) error { 14 | body := "Non-zero balance encountered! Please check the timespan from " + from.Format( 15 | "2006-01-02 15:04:05", 16 | ) + " to " + to.Format( 17 | "2006-01-02 15:04:05", 18 | ) + " in the posting table." 19 | 20 | d := emailData{ 21 | receiver: viper.GetString("email_from"), 22 | receiverEmail: viper.GetString("sendgrid.sender_email"), 23 | subject: "[System Check] Non-zero balance encountered", 24 | text: body, 25 | html: body, 26 | } 27 | err := e.send(d) 28 | if err != nil { 29 | return err 30 | } 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/pkg/flash/flash.go: -------------------------------------------------------------------------------- 1 | package flash 2 | 3 | import ( 4 | "encoding/base64" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/ic3network/mccs-alpha/global/constant" 9 | ) 10 | 11 | func Info(w http.ResponseWriter, value string) { 12 | c := &http.Cookie{ 13 | Name: constant.Flash.Info, 14 | Value: encode([]byte(value)), 15 | Path: "/", 16 | } 17 | http.SetCookie(w, c) 18 | } 19 | 20 | func Success(w http.ResponseWriter, value string) { 21 | c := &http.Cookie{ 22 | Name: constant.Flash.Success, 23 | Value: encode([]byte(value)), 24 | Path: "/", 25 | } 26 | http.SetCookie(w, c) 27 | } 28 | 29 | func GetFlash(w http.ResponseWriter, r *http.Request, name string) string { 30 | c, err := r.Cookie(name) 31 | if err != nil { 32 | switch err { 33 | case http.ErrNoCookie: 34 | return "" 35 | default: 36 | return "" 37 | } 38 | } 39 | value, err := decode(c.Value) 40 | if err != nil { 41 | return "" 42 | } 43 | dc := &http.Cookie{ 44 | Name: name, 45 | MaxAge: -1, 46 | Expires: time.Unix(1, 0), 47 | Path: "/", 48 | } 49 | http.SetCookie(w, dc) 50 | return string(value) 51 | } 52 | 53 | func encode(src []byte) string { 54 | return base64.URLEncoding.EncodeToString(src) 55 | } 56 | 57 | func decode(src string) ([]byte, error) { 58 | return base64.URLEncoding.DecodeString(src) 59 | } 60 | -------------------------------------------------------------------------------- /internal/pkg/helper/tag.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "time" 7 | 8 | "github.com/ic3network/mccs-alpha/internal/app/types" 9 | ) 10 | 11 | var ( 12 | specialCharRe *regexp.Regexp 13 | multiDashRe *regexp.Regexp 14 | ltDashRe *regexp.Regexp 15 | adminTagRe *regexp.Regexp 16 | ) 17 | 18 | func init() { 19 | specialCharRe = regexp.MustCompile("(")|([^a-zA-Z-]+)") 20 | multiDashRe = regexp.MustCompile("-+") 21 | ltDashRe = regexp.MustCompile("(^-+)|(-+$)") 22 | adminTagRe = regexp.MustCompile("[0-9]|(")|([^a-zA-Z ]+)") 23 | } 24 | 25 | // GetTags transforms tags from the user inputs into a standard format. 26 | // dog walking -> dog-walking (one word) 27 | func GetTags(words string) []*types.TagField { 28 | splitFn := func(c rune) bool { 29 | return c == ',' 30 | } 31 | tagArray := strings.FieldsFunc(strings.ToLower(words), splitFn) 32 | 33 | encountered := map[string]bool{} 34 | tags := make([]*types.TagField, 0, len(tagArray)) 35 | for _, tag := range tagArray { 36 | tag = strings.Replace(tag, " ", "-", -1) 37 | tag = specialCharRe.ReplaceAllString(tag, "") 38 | tag = multiDashRe.ReplaceAllString(tag, "-") 39 | tag = ltDashRe.ReplaceAllString(tag, "") 40 | if len(tag) == 0 { 41 | continue 42 | } 43 | // remove duplicates 44 | if !encountered[tag] { 45 | tags = append(tags, &types.TagField{ 46 | Name: tag, 47 | CreatedAt: time.Now(), 48 | }) 49 | encountered[tag] = true 50 | } 51 | } 52 | return tags 53 | } 54 | 55 | // ToSearchTags transforms tags from user inputs into searching tags. 56 | // dog walking -> dog, walking (two words) 57 | func ToSearchTags(words string) []*types.TagField { 58 | splitFn := func(c rune) bool { 59 | return c == ',' || c == ' ' 60 | } 61 | tagArray := strings.FieldsFunc(strings.ToLower(words), splitFn) 62 | 63 | encountered := map[string]bool{} 64 | tags := make([]*types.TagField, 0, len(tagArray)) 65 | for _, tag := range tagArray { 66 | tag = strings.Replace(tag, " ", "-", -1) 67 | tag = specialCharRe.ReplaceAllString(tag, "") 68 | tag = multiDashRe.ReplaceAllString(tag, "-") 69 | tag = ltDashRe.ReplaceAllString(tag, "") 70 | if len(tag) == 0 { 71 | continue 72 | } 73 | // remove duplicates 74 | if !encountered[tag] { 75 | tags = append(tags, &types.TagField{ 76 | Name: tag, 77 | CreatedAt: time.Now(), 78 | }) 79 | encountered[tag] = true 80 | } 81 | } 82 | return tags 83 | } 84 | 85 | func getAdminTags(words string) []string { 86 | splitFn := func(c rune) bool { 87 | return c == ',' 88 | } 89 | tags := make([]string, 0, 8) 90 | encountered := map[string]bool{} 91 | for _, tag := range strings.FieldsFunc(words, splitFn) { 92 | tag = adminTagRe.ReplaceAllString(tag, "") 93 | // remove duplicates 94 | if !encountered[tag] { 95 | tags = append(tags, tag) 96 | encountered[tag] = true 97 | } 98 | } 99 | return tags 100 | } 101 | 102 | func FormatAdminTag(tag string) string { 103 | return adminTagRe.ReplaceAllString(tag, "") 104 | } 105 | 106 | // TagDifference finds out the new added tags. 107 | func TagDifference(tags, oldTags []*types.TagField) ([]string, []string) { 108 | encountered := map[string]int{} 109 | added := []string{} 110 | removed := []string{} 111 | for _, tag := range oldTags { 112 | if _, ok := encountered[tag.Name]; !ok { 113 | encountered[tag.Name]++ 114 | } 115 | } 116 | for _, tag := range tags { 117 | encountered[tag.Name]-- 118 | } 119 | for name, flag := range encountered { 120 | if flag == -1 { 121 | added = append(added, name) 122 | } 123 | if flag == 1 { 124 | removed = append(removed, name) 125 | } 126 | } 127 | return added, removed 128 | } 129 | 130 | func SameTags(new, old []*types.TagField) bool { 131 | added, removed := TagDifference(new, old) 132 | if len(added)+len(removed) > 0 { 133 | return false 134 | } 135 | return true 136 | } 137 | 138 | // ToTagFields converts tags into TagFields. 139 | func ToTagFields(tags []string) []*types.TagField { 140 | tagFields := make([]*types.TagField, 0, len(tags)) 141 | for _, tagName := range tags { 142 | tagField := &types.TagField{ 143 | Name: tagName, 144 | CreatedAt: time.Now(), 145 | } 146 | tagFields = append(tagFields, tagField) 147 | } 148 | return tagFields 149 | } 150 | 151 | // GetTagNames gets tag name from TagField. 152 | func GetTagNames(tags []*types.TagField) []string { 153 | names := make([]string, 0, len(tags)) 154 | for _, t := range tags { 155 | names = append(names, t.Name) 156 | } 157 | return names 158 | } 159 | 160 | // GetAdminTagNames gets admin tag name from AdminTagField. 161 | func GetAdminTagNames(tags []*types.AdminTag) []string { 162 | names := make([]string, 0, len(tags)) 163 | for _, t := range tags { 164 | names = append(names, t.Name) 165 | } 166 | return names 167 | } 168 | -------------------------------------------------------------------------------- /internal/pkg/helper/tag_test.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ic3network/mccs-alpha/internal/app/types" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGetTags(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | input string 14 | expected []string 15 | }{ 16 | { 17 | "should format user input tags", 18 | "egg,apple2,apple2,apple3,james@,TRE~E", 19 | []string{"egg", "apple", "james", "tree"}, 20 | }, 21 | } 22 | 23 | for _, tt := range tests { 24 | t.Run(tt.name, func(t *testing.T) { 25 | actual := []string{} 26 | for _, v := range GetTags(tt.input) { 27 | actual = append(actual, v.Name) 28 | } 29 | assert.ElementsMatch(t, actual, tt.expected) 30 | }) 31 | } 32 | } 33 | 34 | func TestGetAdminTags(t *testing.T) { 35 | tests := []struct { 36 | name string 37 | input string 38 | expected []string 39 | }{ 40 | { 41 | "should split user input tags", 42 | "egg,apple,apple,pineapple,car,WALL", 43 | []string{"egg", "apple", "pineapple", "car", "WALL"}, 44 | }, 45 | } 46 | 47 | for _, tt := range tests { 48 | t.Run(tt.name, func(t *testing.T) { 49 | actual := getAdminTags(tt.input) 50 | assert.ElementsMatch(t, actual, tt.expected) 51 | }) 52 | } 53 | } 54 | 55 | func TestTagDifference(t *testing.T) { 56 | tests := []struct { 57 | name string 58 | tags []*types.TagField 59 | oldTags []*types.TagField 60 | added []string 61 | removed []string 62 | }{ 63 | { 64 | "should create tags for a new business", 65 | []*types.TagField{ 66 | &types.TagField{Name: "newTag1"}, 67 | &types.TagField{Name: "newTag2"}, 68 | }, 69 | nil, 70 | []string{"newTag1", "newTag2"}, 71 | []string{}, 72 | }, 73 | { 74 | "should update tags of a existed business", 75 | []*types.TagField{ 76 | &types.TagField{Name: "oldTag1"}, 77 | &types.TagField{Name: "oldTag2"}, 78 | &types.TagField{Name: "newTag1"}, 79 | &types.TagField{Name: "newTag2"}, 80 | }, 81 | []*types.TagField{ 82 | &types.TagField{Name: "oldTag1"}, 83 | &types.TagField{Name: "oldTag2"}, 84 | &types.TagField{Name: "oldTag3"}, 85 | }, 86 | []string{"newTag1", "newTag2"}, 87 | []string{"oldTag3"}, 88 | }, 89 | } 90 | 91 | for _, tt := range tests { 92 | t.Run(tt.name, func(t *testing.T) { 93 | added, removed := TagDifference(tt.tags, tt.oldTags) 94 | assert.ElementsMatch(t, added, tt.added) 95 | assert.ElementsMatch(t, removed, tt.removed) 96 | }) 97 | } 98 | } 99 | func TestToTagFields(t *testing.T) { 100 | tests := []struct { 101 | name string 102 | input []string 103 | expected []string 104 | }{ 105 | { 106 | "should convert tags into tag fields", 107 | []string{"egg", "apple", "james", "tree"}, 108 | []string{"egg", "apple", "james", "tree"}, 109 | }, 110 | } 111 | 112 | for _, tt := range tests { 113 | t.Run(tt.name, func(t *testing.T) { 114 | actual := []string{} 115 | for _, v := range ToTagFields(tt.input) { 116 | actual = append(actual, v.Name) 117 | } 118 | assert.ElementsMatch(t, actual, tt.expected) 119 | }) 120 | } 121 | } 122 | 123 | func TestGetTagNames(t *testing.T) { 124 | tests := []struct { 125 | name string 126 | input []*types.TagField 127 | expected []string 128 | }{ 129 | { 130 | "should convert tags into tag fields", 131 | []*types.TagField{ 132 | &types.TagField{Name: "egg"}, 133 | &types.TagField{Name: "apple"}, 134 | &types.TagField{Name: "water"}, 135 | &types.TagField{Name: "oil"}, 136 | }, 137 | []string{"egg", "apple", "water", "oil"}, 138 | }, 139 | } 140 | 141 | for _, tt := range tests { 142 | actual := GetTagNames(tt.input) 143 | assert.ElementsMatch(t, actual, tt.expected) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /internal/pkg/helper/trading.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/ic3network/mccs-alpha/internal/app/types" 8 | ) 9 | 10 | type trading struct{} 11 | 12 | var Trading = &trading{} 13 | 14 | func (t *trading) GetRegisterData(r *http.Request) *types.TradingRegisterData { 15 | turnover, _ := strconv.Atoi(r.FormValue("turnover")) 16 | return &types.TradingRegisterData{ 17 | BusinessName: r.FormValue("business_name"), // 100 chars 18 | IncType: r.FormValue("inc_type"), // 25 chars 19 | CompanyNumber: r.FormValue("company_number"), // 20 chars 20 | BusinessPhone: r.FormValue("business_phone"), // 25 chars 21 | Website: r.FormValue("website"), // 100 chars 22 | Turnover: turnover, // 20 chars 23 | Description: r.FormValue("description"), // 500 chars 24 | LocationAddress: r.FormValue("location_address"), // 255 chars 25 | LocationCity: r.FormValue("location_city"), // 50 chars 26 | LocationRegion: r.FormValue("location_region"), // 50 chars 27 | LocationPostalCode: r.FormValue("location_postal_code"), // 10 chars 28 | LocationCountry: r.FormValue("location_country"), // 50 chars 29 | FirstName: r.FormValue("first_name"), // 100 chars 30 | LastName: r.FormValue("last_name"), // 100 chars 31 | Telephone: r.FormValue("telephone"), // 25 chars 32 | Authorised: r.FormValue("authorised"), 33 | } 34 | } 35 | 36 | func (t *trading) GetUpdateData(r *http.Request) *types.TradingUpdateData { 37 | turnover, _ := strconv.Atoi(r.FormValue("turnover")) 38 | return &types.TradingUpdateData{ 39 | BusinessName: r.FormValue("business_name"), // 100 chars 40 | IncType: r.FormValue("inc_type"), // 25 chars 41 | CompanyNumber: r.FormValue("company_number"), // 20 chars 42 | BusinessPhone: r.FormValue("business_phone"), // 25 chars 43 | Website: r.FormValue("website"), // 100 chars 44 | Turnover: turnover, // 20 chars 45 | Description: r.FormValue("description"), // 500 chars 46 | LocationAddress: r.FormValue("location_address"), // 255 chars 47 | LocationCity: r.FormValue("location_city"), // 50 chars 48 | LocationRegion: r.FormValue("location_region"), // 50 chars 49 | LocationPostalCode: r.FormValue("location_postal_code"), // 10 chars 50 | LocationCountry: r.FormValue("location_country"), // 50 chars 51 | FirstName: r.FormValue("first_name"), // 100 chars 52 | LastName: r.FormValue("last_name"), // 100 chars 53 | Telephone: r.FormValue("telephone"), // 25 chars 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/pkg/helper/user.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/ic3network/mccs-alpha/internal/app/types" 8 | ) 9 | 10 | func GetRegisterData(r *http.Request) *types.RegisterData { 11 | return &types.RegisterData{ 12 | Business: GetBusiness(r), 13 | User: GetUser(r), 14 | ConfirmPassword: r.FormValue("confirm_password"), 15 | ConfirmEmail: r.FormValue("confirm_email"), 16 | Terms: r.FormValue("terms"), 17 | } 18 | } 19 | 20 | func GetUpdateData(r *http.Request) *types.UpdateAccountData { 21 | return &types.UpdateAccountData{ 22 | Business: GetBusiness(r), 23 | User: GetUser(r), 24 | Balance: &types.BalanceLimit{}, 25 | CurrentPassword: r.FormValue("current_password"), 26 | ConfirmPassword: r.FormValue("confirm_password"), 27 | } 28 | } 29 | 30 | func GetBusiness(r *http.Request) *types.BusinessData { 31 | turnover, _ := strconv.Atoi(r.FormValue("turnover")) 32 | b := &types.BusinessData{ 33 | BusinessName: r.FormValue("business_name"), // 100 chars 34 | IncType: r.FormValue("inc_type"), // 25 chars 35 | CompanyNumber: r.FormValue("company_number"), // 20 chars 36 | BusinessPhone: r.FormValue("business_phone"), // 25 chars 37 | Website: r.FormValue("website"), // 100 chars 38 | Turnover: turnover, // 20 chars 39 | Offers: GetTags( 40 | r.FormValue("offers"), 41 | ), // 500 chars (max 50 chars per tag) 42 | Wants: GetTags( 43 | r.FormValue("wants"), 44 | ), // 500 chars (max 50 chars per tag) 45 | Description: r.FormValue("description"), // 500 chars 46 | LocationAddress: r.FormValue("location_address"), // 255 chars 47 | LocationCity: r.FormValue("location_city"), // 50 chars 48 | LocationRegion: r.FormValue("location_region"), // 50 chars 49 | LocationPostalCode: r.FormValue("location_postal_code"), // 10 chars 50 | LocationCountry: r.FormValue("location_country"), // 50 chars 51 | AdminTags: getAdminTags(r.FormValue("adminTags")), 52 | Status: r.FormValue("status"), 53 | } 54 | return b 55 | } 56 | 57 | func GetUser(r *http.Request) *types.User { 58 | return &types.User{ 59 | FirstName: r.FormValue("first_name"), // 100 chars 60 | LastName: r.FormValue("last_name"), // 100 chars 61 | Email: r.FormValue("email"), // 100 chars 62 | Telephone: r.FormValue("telephone"), // 25 chars 63 | Password: r.FormValue("new_password"), // 100 chars 64 | DailyNotification: r.FormValue("daily_notification") == "true", 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /internal/pkg/ip/ip.go: -------------------------------------------------------------------------------- 1 | package ip 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | ) 7 | 8 | const ( 9 | XForwardedFor = "X-Forwarded-For" 10 | XRealIP = "X-Real-IP" 11 | ) 12 | 13 | func FromRequest(r *http.Request) string { 14 | remoteAddr := r.RemoteAddr 15 | 16 | if ip := r.Header.Get(XRealIP); ip != "" { 17 | remoteAddr = ip 18 | } else if ip = r.Header.Get(XForwardedFor); ip != "" { 19 | remoteAddr = ip 20 | } else { 21 | remoteAddr, _, _ = net.SplitHostPort(remoteAddr) 22 | } 23 | 24 | if remoteAddr == "::1" { 25 | remoteAddr = "127.0.0.1" 26 | } 27 | 28 | return remoteAddr 29 | } 30 | -------------------------------------------------------------------------------- /internal/pkg/jsonerror/errorcode.go: -------------------------------------------------------------------------------- 1 | package jsonerror 2 | -------------------------------------------------------------------------------- /internal/pkg/jsonerror/jsonerror.go: -------------------------------------------------------------------------------- 1 | package jsonerror 2 | 3 | type JE struct { 4 | Code string 5 | message string 6 | } 7 | 8 | // New creates a new JE struct. 9 | func New(code, message string) JE { 10 | j := JE{Code: code, message: message} 11 | return j 12 | } 13 | 14 | func (j JE) Render() map[string]string { 15 | return map[string]string{"code": j.Code, "message": j.message} 16 | } 17 | -------------------------------------------------------------------------------- /internal/pkg/jwt/jwt.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "crypto/rsa" 5 | "errors" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | jwtlib "github.com/golang-jwt/jwt/v5" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | // JWTManager manages JWT operations. 15 | type JWTManager struct { 16 | signKey *rsa.PrivateKey 17 | verifyKey *rsa.PublicKey 18 | } 19 | 20 | // NewJWTManager initializes and returns a new JWTManager instance. 21 | func NewJWTManager() *JWTManager { 22 | privateKeyPEM := getEnvOrFallback("jwt.private_key", "JWT_PRIVATE_KEY") 23 | publicKeyPEM := getEnvOrFallback("jwt.public_key", "JWT_PUBLIC_KEY") 24 | 25 | signKey, err := jwtlib.ParseRSAPrivateKeyFromPEM([]byte(privateKeyPEM)) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | verifyKey, err := jwtlib.ParseRSAPublicKeyFromPEM([]byte(publicKeyPEM)) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | return &JWTManager{ 36 | signKey: signKey, 37 | verifyKey: verifyKey, 38 | } 39 | } 40 | 41 | type userClaims struct { 42 | jwtlib.RegisteredClaims 43 | UserID string `json:"userID"` 44 | Admin bool `json:"admin"` 45 | } 46 | 47 | // GenerateToken generates a JWT token for a user. 48 | func (jm *JWTManager) Generate( 49 | userID string, 50 | isAdmin bool, 51 | ) (string, error) { 52 | claims := userClaims{ 53 | UserID: userID, 54 | Admin: isAdmin, 55 | RegisteredClaims: jwtlib.RegisteredClaims{ 56 | ExpiresAt: jwtlib.NewNumericDate(time.Now().Add(24 * time.Hour)), 57 | }, 58 | } 59 | 60 | token := jwtlib.NewWithClaims(jwtlib.SigningMethodRS256, claims) 61 | return token.SignedString(jm.signKey) 62 | } 63 | 64 | // Validate validates a JWT token and returns the associated claims. 65 | func (jm *JWTManager) Validate(tokenString string) (*userClaims, error) { 66 | claims := &userClaims{} 67 | token, err := jwtlib.ParseWithClaims( 68 | tokenString, 69 | claims, 70 | func(token *jwtlib.Token) (interface{}, error) { 71 | return jm.verifyKey, nil 72 | }, 73 | ) 74 | 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | if !token.Valid { 80 | return nil, errors.New("invalid token") 81 | } 82 | 83 | return claims, nil 84 | } 85 | 86 | func getEnvOrFallback(viperKey, envKey string) string { 87 | value := viper.GetString(viperKey) 88 | if value == "" { 89 | value = os.Getenv(viperKey) 90 | } 91 | if value == "" { 92 | value = os.Getenv(envKey) 93 | } 94 | return value 95 | } 96 | -------------------------------------------------------------------------------- /internal/pkg/jwt/jwt_test.go: -------------------------------------------------------------------------------- 1 | package jwt_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ic3network/mccs-alpha/internal/pkg/jwt" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | const ( 11 | TEST_PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY----- 12 | MIIEpQIBAAKCAQEA1vl/vENflHIgomF0qxfPg9l9FqwguCYtGNIbAfvVXSsLueAV 13 | D23ZDtazisA67y+dO8TQk+KTGeCbM2Otcvvs7mdsRsbFe+demdSfydQHAp2tGb7O 14 | DBQPGJeyCnyfePB2GmqjVrBB1BjjWgnwwNaXJQkmtLruG8Sgrwl3nKjTeC3x8LjB 15 | l0gGU6UFON6SBF+/CovbOHn+P8eUC/LJrvX8dGpXfGoTzk18WKU5GzThrSgoGkL7 16 | CToAN8/JoW8G/1gtiuhFRAi53oxpt058An8ORBP8PkxGj+enjq59C/5YMYLwfKHj 17 | lylu0uRpfbmih4/46pgbYjjUyOhSXxHd1k5VmwIDAQABAoIBAQCMb29X0HeXJTtW 18 | eO3be3GQA7to3Ud+pUnephsIn7iR5bYCVnXLn4ol3HJr2QpnCKbhzcAoa+KHDCi3 19 | WI2NyS/Nynh8gAuw1sQBIFrGYaG2vsS/RdubHluCSE8B9MnFGuk8dp9/2SMX6K5V 20 | Opsxjr4sbp7/gAJe14PU9Q1TpSKIpeBvH3li+mF405YIo6JTcxZToJtJcRMB7V7P 21 | E8KI6F+cwczAmc282pMtpT8/TL1PHq7JYH8LJ18+EwQOvEvo6A8KJIDlFqEaK+yu 22 | oQhHdUxxO9ZxAlTcb48g6o2laLd+v6k8N9yQagEyqHof/cSNXA24ZnqGHS3sECri 23 | TpV1gDypAoGBAPLzXtxV2zseEDH5dYQ3vh6bdEudBFy/3YGFIMrtbMZCP8dx2Lie 24 | P9ABWJqYEUrQpSNnKk6XdNQQGiuwFmygOuZMvyw4svuGKHIIQsSFQRtn+oO6lqSe 25 | cOW+59UgBipBBjRuNvdSh3g4i+JI33bDwVedO5Qp+OinenVHMx27NHNNAoGBAOKF 26 | c2/W7PYllHMGPIfjW1/+otkHdwPyLiBleamUgs33do8YGJekddX0+2BgR1ZIoIWQ 27 | MsiWA/FcsTKERaZv220s7iz58w0GTcpbHQQW7e6D9cl+5DIXnEyG6vQ+hSiUOQXe 28 | LjblgGQJHitrH2wUW/eEjQvXYLIduKlTcOWGZ6iHAoGBAJUtLJUcPsX4+rbE1wy9 29 | cYa3q1v2aMROp0MtLGqOCJlf+muLkyghO0uMWAxszUlj/dJUOV0SkJDZ5kfnEo3W 30 | gPQCMeyEUBozUUhbnCuxKr4aRW93NaKVCvt3EkECLebqEFZHSobobPg7uGDUoCn7 31 | nw8eI4QhlY29sGqssk1SMq2NAoGBAIW12n8w8e0WH7uJ+d8IoJ5Yc44CbwlgQkQT 32 | Qi6MoG2t3kj3I0UX6gqismOgUVuoQUC17pQioS8u1NYJ6AcnzfFy7SCVZhfRGcgR 33 | 4l3QnyAEuuf2xAKhlzxBA52q7fUXEVXaYZM8A36JN0rPz9t/ZQ4FKzDLMKPTEXa5 34 | 71E89iEvAoGASgN2hEjcF4lwe5ahgrLAheibPzC+6DFdSCBw9CbL183C5s3r4JDh 35 | VDH3H2SHpB0qmBa+YLRwRvHxpWU9uq/unaJvc+AQ3JwZX3bQ8ixvyVfpeBXZF6Dh 36 | KoQ0MewWRNtrpGFa5qdWBfcenKdhWgWrdMnroNhqCHfXEIiYsj3qqWs= 37 | -----END RSA PRIVATE KEY-----` 38 | 39 | TEST_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- 40 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1vl/vENflHIgomF0qxfP 41 | g9l9FqwguCYtGNIbAfvVXSsLueAVD23ZDtazisA67y+dO8TQk+KTGeCbM2Otcvvs 42 | 7mdsRsbFe+demdSfydQHAp2tGb7ODBQPGJeyCnyfePB2GmqjVrBB1BjjWgnwwNaX 43 | JQkmtLruG8Sgrwl3nKjTeC3x8LjBl0gGU6UFON6SBF+/CovbOHn+P8eUC/LJrvX8 44 | dGpXfGoTzk18WKU5GzThrSgoGkL7CToAN8/JoW8G/1gtiuhFRAi53oxpt058An8O 45 | RBP8PkxGj+enjq59C/5YMYLwfKHjlylu0uRpfbmih4/46pgbYjjUyOhSXxHd1k5V 46 | mwIDAQAB 47 | -----END PUBLIC KEY-----` 48 | ) 49 | 50 | func TestJWT(t *testing.T) { 51 | tests := []struct { 52 | name string 53 | userID string 54 | isAdmin bool 55 | }{ 56 | { 57 | name: "Valid User Admin", 58 | userID: "123", 59 | isAdmin: true, 60 | }, 61 | { 62 | name: "Valid User Not Admin", 63 | userID: "456", 64 | isAdmin: false, 65 | }, 66 | } 67 | 68 | for _, tt := range tests { 69 | t.Run(tt.name, func(t *testing.T) { 70 | t.Setenv("jwt.private_key", TEST_PRIVATE_KEY) 71 | t.Setenv("jwt.public_key", TEST_PUBLIC_KEY) 72 | j := jwt.NewJWTManager() 73 | 74 | token, err := j.Generate(tt.userID, tt.isAdmin) 75 | require.NoError(t, err) 76 | 77 | claims, err := j.Validate(token) 78 | require.NoError(t, err) 79 | 80 | require.Equal(t, tt.userID, claims.UserID) 81 | require.Equal(t, tt.isAdmin, claims.Admin) 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /internal/pkg/l/logger.go: -------------------------------------------------------------------------------- 1 | package l 2 | 3 | import "go.uber.org/zap" 4 | 5 | var Logger *zap.Logger 6 | 7 | // Init initialized the logging tool. 8 | func Init(env string) { 9 | if env == "production" { 10 | Logger, _ = zap.NewProduction() 11 | } else { 12 | Logger, _ = zap.NewDevelopment() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /internal/pkg/pagination/pagination.go: -------------------------------------------------------------------------------- 1 | package pagination 2 | 3 | import "math" 4 | 5 | func Pages(count int64, size int64) int { 6 | pages := int(math.Ceil(float64(count) / float64(size))) 7 | if count == 0 { 8 | return 1 9 | } 10 | return pages 11 | } 12 | -------------------------------------------------------------------------------- /internal/pkg/passlib/passlib.go: -------------------------------------------------------------------------------- 1 | package passlib 2 | 3 | import ( 4 | "strconv" 5 | "unicode" 6 | ) 7 | 8 | var ( 9 | minLen = 8 10 | hasLetter = false 11 | hasNumber = false 12 | hasSpecial = false 13 | ) 14 | 15 | // Validate validates the given password. 16 | func Validate(password string) []string { 17 | messages := make([]string, 0, 4) 18 | 19 | for _, ch := range password { 20 | switch { 21 | case unicode.IsLetter(ch): 22 | hasLetter = true 23 | case unicode.IsNumber(ch): 24 | hasNumber = true 25 | case unicode.IsPunct(ch) || unicode.IsSymbol(ch): 26 | hasSpecial = true 27 | } 28 | } 29 | 30 | if len(password) < minLen { 31 | messages = append( 32 | messages, 33 | "Password must be at least "+strconv.Itoa( 34 | minLen, 35 | )+" characters long.", 36 | ) 37 | } else if len(password) > 100 { 38 | messages = append(messages, "Password cannot exceed 100 characters.") 39 | } 40 | if !hasLetter { 41 | messages = append(messages, "Password must have at least one letter.") 42 | } 43 | if !hasNumber { 44 | messages = append( 45 | messages, 46 | "Password must have at least one numeric value.", 47 | ) 48 | } 49 | if !hasSpecial { 50 | messages = append( 51 | messages, 52 | "Password must have at least one special character.", 53 | ) 54 | } 55 | 56 | return messages 57 | } 58 | -------------------------------------------------------------------------------- /internal/pkg/recaptcha/recaptcha.go: -------------------------------------------------------------------------------- 1 | package recaptcha 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "net/url" 8 | "time" 9 | 10 | "github.com/ic3network/mccs-alpha/global" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | var r *Recaptcha 15 | 16 | func init() { 17 | global.Init() 18 | r = New() 19 | } 20 | 21 | // Recaptcha is a prioritized configuration registry. 22 | type Recaptcha struct { 23 | Secret string 24 | errMsg string 25 | } 26 | 27 | // New returns an initialized recaptcha instance. 28 | func New() *Recaptcha { 29 | j := new(Recaptcha) 30 | j.Secret = viper.GetString("recaptcha.secret_key") 31 | return j 32 | } 33 | 34 | // Struct for parsing json in google's response 35 | type googleResponse struct { 36 | Success bool 37 | ErrorCodes []string `json:"error-codes"` 38 | } 39 | 40 | // url to post submitted re-captcha response to 41 | var postURL = "https://www.google.com/recaptcha/api/siteverify" 42 | 43 | // Verify method, verifies if current request have valid re-captcha response and returns true or false 44 | // This method also records any errors in validation. 45 | // These errors can be received by calling LastError() method. 46 | func Verify(req http.Request) bool { return r.verify(req) } 47 | func (r *Recaptcha) verify(req http.Request) bool { 48 | response := req.FormValue("g-recaptcha-response") 49 | return r.verifyResponse(response) 50 | } 51 | 52 | // VerifyResponse is a method similar to `Verify`; but doesn't parse the form for you. Useful if 53 | // you're receiving the data as a JSON object from a javascript app or similar. 54 | func (r *Recaptcha) verifyResponse(response string) bool { 55 | if response == "" { 56 | r.errMsg = "Please select captcha first." 57 | return false 58 | } 59 | client := &http.Client{Timeout: 5 * time.Second} 60 | resp, err := client.PostForm( 61 | postURL, 62 | url.Values{"secret": {r.Secret}, "response": {response}}, 63 | ) 64 | if err != nil { 65 | r.errMsg = err.Error() 66 | return false 67 | } 68 | defer resp.Body.Close() 69 | body, err := ioutil.ReadAll(resp.Body) 70 | if err != nil { 71 | r.errMsg = err.Error() 72 | return false 73 | } 74 | gr := new(googleResponse) 75 | err = json.Unmarshal(body, gr) 76 | if err != nil { 77 | r.errMsg = err.Error() 78 | return false 79 | } 80 | if !gr.Success { 81 | r.errMsg = gr.ErrorCodes[len(gr.ErrorCodes)-1] 82 | } 83 | return gr.Success 84 | } 85 | 86 | // Error returns errors occurred in last re-captcha validation attempt 87 | func Error() []string { return r.error() } 88 | func (r *Recaptcha) error() []string { 89 | return []string{r.errMsg} 90 | } 91 | -------------------------------------------------------------------------------- /internal/pkg/template/data.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | type Data struct { 4 | User struct { 5 | ID string 6 | Admin bool 7 | } 8 | ErrorMessages []string 9 | Messages struct { 10 | Success string 11 | Info string 12 | } 13 | Yield interface{} 14 | } 15 | -------------------------------------------------------------------------------- /internal/pkg/template/functions.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | "time" 8 | 9 | "github.com/ic3network/mccs-alpha/internal/app/types" 10 | "go.mongodb.org/mongo-driver/bson/primitive" 11 | ) 12 | 13 | func arrToSting(stringArray []string) string { 14 | return strings.Join(stringArray, ",") 15 | } 16 | 17 | func tagsToString(tags []*types.TagField) string { 18 | var sb strings.Builder 19 | l := len(tags) - 1 20 | for i, t := range tags { 21 | sb.WriteString(t.Name) 22 | if i != l { 23 | sb.WriteString(",") 24 | } 25 | } 26 | return sb.String() 27 | } 28 | 29 | func tagsToSearchString(tags []*types.TagField) string { 30 | var sb strings.Builder 31 | l := len(tags) - 1 32 | for i, t := range tags { 33 | sb.WriteString(t.Name) 34 | if i != l { 35 | sb.WriteString(" ") 36 | } 37 | } 38 | return sb.String() 39 | } 40 | 41 | func add(number int, inc int) int { 42 | return number + inc 43 | } 44 | 45 | func minus(number int, inc int) int { 46 | return number - inc 47 | } 48 | 49 | func n(start int, end int) []int { 50 | numbers := make([]int, 0, end-start+1) 51 | for i := start; i <= end; i++ { 52 | numbers = append(numbers, i) 53 | } 54 | return numbers 55 | } 56 | 57 | func idToString(id primitive.ObjectID) string { 58 | return id.Hex() 59 | } 60 | 61 | func formatTime(t time.Time) string { 62 | tt := t.UTC() 63 | return fmt.Sprintf("%d-%02d-%02d %02d:%02d:%02d UTC", 64 | tt.Year(), tt.Month(), tt.Day(), 65 | tt.Hour(), tt.Minute(), tt.Second()) 66 | } 67 | 68 | func formatTimeRFC3339(t time.Time) string { 69 | tt := t.UTC() 70 | return tt.Format(time.RFC3339) 71 | } 72 | 73 | func formatAccountBalance(balance float64) string { 74 | return fmt.Sprintf("%.2f", balance) 75 | } 76 | 77 | func formatTransactionID(id string) string { 78 | return id[0:8] 79 | } 80 | 81 | func shouldDisplayTime(t time.Time) bool { 82 | return !t.IsZero() 83 | } 84 | 85 | func includesID(list []primitive.ObjectID, target primitive.ObjectID) bool { 86 | for _, id := range list { 87 | if id == target { 88 | return true 89 | } 90 | } 91 | return false 92 | } 93 | 94 | func timeNow() string { 95 | return time.Now().Format("2006-01-02") 96 | } 97 | 98 | func daysBefore(days int) string { 99 | return time.Now().AddDate(0, 0, -days).Format("2006-01-02") 100 | } 101 | 102 | func sortAdminTags(tags []*types.AdminTag) []*types.AdminTag { 103 | sort.Slice(tags, func(i, j int) bool { 104 | return tags[i].Name < tags[j].Name 105 | }) 106 | return tags 107 | } 108 | 109 | func containPrefix(arr []string, prefix string) bool { 110 | for _, s := range arr { 111 | if strings.HasPrefix(strings.ToUpper(s), strings.ToUpper(prefix)) { 112 | return true 113 | } 114 | } 115 | return false 116 | } 117 | -------------------------------------------------------------------------------- /internal/pkg/template/template.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "html/template" 5 | "log" 6 | "net/http" 7 | "path/filepath" 8 | "strconv" 9 | 10 | "github.com/ic3network/mccs-alpha/global/constant" 11 | "github.com/ic3network/mccs-alpha/internal/pkg/e" 12 | "github.com/ic3network/mccs-alpha/internal/pkg/flash" 13 | ) 14 | 15 | var ( 16 | layoutDir = "web/template/layout/" 17 | templateExt = ".html" 18 | ) 19 | 20 | type View struct { 21 | Template *template.Template 22 | Layout string 23 | } 24 | 25 | func NewView(templateName string) *View { 26 | templates := append(layoutFiles(), "web/template/"+templateName+".html") 27 | 28 | t, err := template.New(""). 29 | Funcs(template.FuncMap{ 30 | "ArrToSting": arrToSting, 31 | "TagsToString": tagsToString, 32 | "TagsToSearchString": tagsToSearchString, 33 | "Add": add, 34 | "Minus": minus, 35 | "N": n, 36 | "IDToString": idToString, 37 | "FormatTime": formatTime, 38 | "FormatAccountBalance": formatAccountBalance, 39 | "FormatTransactionID": formatTransactionID, 40 | "ShouldDisplayTime": shouldDisplayTime, 41 | "IncludesID": includesID, 42 | "TimeNow": timeNow, 43 | "DaysBefore": daysBefore, 44 | "SortAdminTags": sortAdminTags, 45 | "ContainPrefix": containPrefix, 46 | }). 47 | ParseFiles(templates...) 48 | if err != nil { 49 | log.Fatal("parse template file error:", err.Error()) 50 | } 51 | 52 | return &View{ 53 | Template: t, 54 | Layout: "base", 55 | } 56 | } 57 | 58 | func NewEmailView(templateName string) (*template.Template, error) { 59 | templates := "web/template/email/" + templateName + ".html" 60 | 61 | t, err := template.New(""). 62 | Funcs(template.FuncMap{ 63 | "ArrToSting": arrToSting, 64 | "TagsToString": tagsToString, 65 | "TagsToSearchString": tagsToSearchString, 66 | "Add": add, 67 | "Minus": minus, 68 | "N": n, 69 | "IDToString": idToString, 70 | "FormatTimeRFC3339": formatTimeRFC3339, 71 | "ShouldDisplayTime": shouldDisplayTime, 72 | "IncludesID": includesID, 73 | }). 74 | ParseFiles(templates) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | return t, nil 80 | } 81 | 82 | // Render is used to render the view with the predefined layout. 83 | func (v *View) Render( 84 | w http.ResponseWriter, 85 | r *http.Request, 86 | yield interface{}, 87 | ErrorMessages []string, 88 | ) { 89 | w.Header().Set("Content-Type", "text/html") 90 | 91 | var vd Data 92 | vd.User.ID = r.Header.Get("userID") 93 | admin, err := strconv.ParseBool(r.Header.Get("admin")) 94 | if err != nil { 95 | vd.User.Admin = false 96 | } else { 97 | vd.User.Admin = admin 98 | } 99 | vd.ErrorMessages = ErrorMessages 100 | vd.Yield = yield 101 | vd.Messages.Success = flash.GetFlash(w, r, constant.Flash.Success) 102 | vd.Messages.Info = flash.GetFlash(w, r, constant.Flash.Info) 103 | 104 | v.Template.ExecuteTemplate(w, v.Layout, vd) 105 | } 106 | 107 | // Success renders the self defined success message. 108 | func (v *View) Success( 109 | w http.ResponseWriter, 110 | r *http.Request, 111 | yield interface{}, 112 | message string, 113 | ) { 114 | w.Header().Set("Content-Type", "text/html") 115 | 116 | var vd Data 117 | vd.User.ID = r.Header.Get("userID") 118 | admin, err := strconv.ParseBool(r.Header.Get("admin")) 119 | if err != nil { 120 | vd.User.Admin = false 121 | } else { 122 | vd.User.Admin = admin 123 | } 124 | vd.Yield = yield 125 | vd.Messages.Success = message 126 | 127 | v.Template.ExecuteTemplate(w, v.Layout, vd) 128 | } 129 | 130 | // Error renders the self defined error message. 131 | func (v *View) Error( 132 | w http.ResponseWriter, 133 | r *http.Request, 134 | yield interface{}, 135 | err error, 136 | ) { 137 | w.Header().Set("Content-Type", "text/html") 138 | 139 | var vd Data 140 | error, ok := err.(e.Error) 141 | if ok { 142 | vd.ErrorMessages = []string{error.Message()} 143 | } else { 144 | vd.ErrorMessages = []string{"Sorry, something went wrong. Please try again later."} 145 | } 146 | vd.User.ID = r.Header.Get("userID") 147 | admin, err := strconv.ParseBool(r.Header.Get("admin")) 148 | if err != nil { 149 | vd.User.Admin = false 150 | } else { 151 | vd.User.Admin = admin 152 | } 153 | vd.Yield = yield 154 | 155 | v.Template.ExecuteTemplate(w, v.Layout, vd) 156 | } 157 | 158 | // layoutFiles returns a slice of strings representing 159 | // the layout files used in our application. 160 | func layoutFiles() []string { 161 | files, err := filepath.Glob(layoutDir + "*" + templateExt) 162 | if err != nil { 163 | panic(err) 164 | } 165 | return files 166 | } 167 | -------------------------------------------------------------------------------- /internal/pkg/util/amount.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "strings" 4 | 5 | // IsDecimalValid checks the num is positive value and with up to two decimal places. 6 | func IsDecimalValid(num string) bool { 7 | numArr := strings.Split(num, ".") 8 | if len(numArr) == 1 { 9 | return true 10 | } 11 | if len(numArr) == 2 && len(numArr[1]) <= 2 { 12 | return true 13 | } 14 | return false 15 | } 16 | -------------------------------------------------------------------------------- /internal/pkg/util/amount_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestIsDecimalValid(t *testing.T) { 10 | tests := []struct { 11 | input string 12 | expected bool 13 | }{ 14 | { 15 | "10", 16 | true, 17 | }, 18 | { 19 | "10.1", 20 | true, 21 | }, 22 | { 23 | "10.12", 24 | true, 25 | }, 26 | { 27 | "10.123", 28 | false, 29 | }, 30 | { 31 | "10.123.13", 32 | false, 33 | }, 34 | } 35 | 36 | for _, tt := range tests { 37 | t.Run("", func(t *testing.T) { 38 | actual := IsDecimalValid(tt.input) 39 | assert.Equal(t, tt.expected, actual) 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/pkg/util/check_field_diff.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | 8 | "gopkg.in/oleiade/reflections.v1" 9 | ) 10 | 11 | // CheckDiff checks what fields have been changed. 12 | // Only checks "String", "Int" and "Float64" types. 13 | func CheckDiff( 14 | old interface{}, 15 | new interface{}, 16 | fieldsToSkip map[string]bool, 17 | ) []string { 18 | modifiedFields := make([]string, 0) 19 | structItems, _ := reflections.Items(old) 20 | 21 | for field, oldValue := range structItems { 22 | if _, ok := fieldsToSkip[field]; ok { 23 | continue 24 | } 25 | fieldKind, _ := reflections.GetFieldKind(old, field) 26 | if fieldKind != reflect.String && fieldKind != reflect.Int && 27 | fieldKind != reflect.Float64 { 28 | continue 29 | } 30 | newValue, _ := reflections.GetField(new, field) 31 | if newValue != oldValue { 32 | if fieldKind == reflect.Int { 33 | modifiedFields = append( 34 | modifiedFields, 35 | field+": "+strconv.Itoa( 36 | oldValue.(int), 37 | )+" -> "+strconv.Itoa( 38 | newValue.(int), 39 | ), 40 | ) 41 | } else if fieldKind == reflect.Float64 { 42 | modifiedFields = append(modifiedFields, field+": "+fmt.Sprintf("%.2f", oldValue.(float64))+" -> "+fmt.Sprintf("%.2f", newValue.(float64))) 43 | } else { 44 | modifiedFields = append(modifiedFields, field+": "+oldValue.(string)+" -> "+newValue.(string)) 45 | } 46 | } 47 | } 48 | 49 | return modifiedFields 50 | } 51 | -------------------------------------------------------------------------------- /internal/pkg/util/email.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "regexp" 4 | 5 | var emailRe = regexp.MustCompile( 6 | "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", 7 | ) 8 | 9 | // IsValidEmail checks if email is valid. 10 | func IsValidEmail(email string) bool { 11 | if email == "" || !emailRe.MatchString(email) || len(email) > 100 { 12 | return false 13 | } 14 | return true 15 | } 16 | -------------------------------------------------------------------------------- /internal/pkg/util/email_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestIsValidEmail(t *testing.T) { 10 | tests := []struct { 11 | input string 12 | expected bool 13 | }{ 14 | { 15 | "email@domain.com", 16 | true, 17 | }, 18 | { 19 | "firstname.lastname@domain.com", 20 | true, 21 | }, 22 | { 23 | "email@subdomain.domain.com", 24 | true, 25 | }, 26 | { 27 | "firstname+lastname@domain.com", 28 | true, 29 | }, 30 | { 31 | "plainaddress", 32 | false, 33 | }, 34 | { 35 | "#@%^%#$@#$@#.com", 36 | false, 37 | }, 38 | { 39 | "@domain.com", 40 | false, 41 | }, 42 | { 43 | "あいうえお@domain.com", 44 | false, 45 | }, 46 | { 47 | "jdoe1test@opencredit.network", 48 | true, 49 | }, 50 | } 51 | 52 | for _, tt := range tests { 53 | t.Run("", func(t *testing.T) { 54 | actual := IsValidEmail(tt.input) 55 | assert.Equal(t, tt.expected, actual) 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /internal/pkg/util/id.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson/primitive" 5 | ) 6 | 7 | // ToIDStrings converts Object IDs into strings. 8 | func ToIDStrings(objIDs []primitive.ObjectID) []string { 9 | ids := make([]string, 0, len(objIDs)) 10 | for _, objID := range objIDs { 11 | ids = append(ids, objID.Hex()) 12 | } 13 | return ids 14 | } 15 | -------------------------------------------------------------------------------- /internal/pkg/util/id_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "go.mongodb.org/mongo-driver/bson/primitive" 8 | ) 9 | 10 | func TestToIDStrings(t *testing.T) { 11 | idString1 := "5d5516a0f613a4f874b1bf1d" 12 | idString2 := "5d5516a0f613a4f874b1bf1e" 13 | idString3 := "5d5516a0f613a4f874b1bf1f" 14 | idString4 := "5d5516a0f613a4f874b1bf20" 15 | id1, _ := primitive.ObjectIDFromHex(idString1) 16 | id2, _ := primitive.ObjectIDFromHex(idString2) 17 | id3, _ := primitive.ObjectIDFromHex(idString3) 18 | id4, _ := primitive.ObjectIDFromHex(idString4) 19 | 20 | tests := []struct { 21 | input []primitive.ObjectID 22 | expected []string 23 | }{ 24 | { 25 | []primitive.ObjectID{id1, id2, id3, id4}, 26 | []string{idString1, idString2, idString3, idString4}, 27 | }, 28 | } 29 | 30 | for _, tt := range tests { 31 | t.Run("", func(t *testing.T) { 32 | actual := ToIDStrings(tt.input) 33 | assert.Equal(t, tt.expected, actual) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/pkg/util/status_checking.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/ic3network/mccs-alpha/global/constant" 5 | ) 6 | 7 | // IsAcceptedStatus checks if the business status is accpeted. 8 | func IsAcceptedStatus(status string) bool { 9 | if status == constant.Business.Accepted || 10 | status == constant.Trading.Pending || 11 | status == constant.Trading.Accepted || 12 | status == constant.Trading.Rejected { 13 | return true 14 | } 15 | return false 16 | } 17 | -------------------------------------------------------------------------------- /internal/pkg/util/time.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/jinzhu/now" 9 | ) 10 | 11 | // ParseTime parses string into time. 12 | func ParseTime(s string) time.Time { 13 | if s == "" || s == "1-01-01 00:00:00 UTC" { 14 | return time.Time{} 15 | } 16 | 17 | now.TimeFormats = append(now.TimeFormats, 18 | "2 January 2006", 19 | "2 January 2006 3:04 PM", 20 | "2006-01-02 03:04:05 MST", 21 | ) 22 | 23 | t, err := now.ParseInLocation(time.UTC, s) 24 | if err != nil { 25 | log.Printf("[ERROR] ParseTime failed: %+v", err) 26 | return time.Time{} 27 | } 28 | return t 29 | } 30 | 31 | // FormatTime formats time in UK format. 32 | func FormatTime(t time.Time) string { 33 | tt := t.UTC() 34 | return fmt.Sprintf("%d-%02d-%02d %02d:%02d:%02d UTC", 35 | tt.Year(), tt.Month(), tt.Day(), 36 | tt.Hour(), tt.Minute(), tt.Second()) 37 | } 38 | -------------------------------------------------------------------------------- /internal/pkg/util/time_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestParseTime(t *testing.T) { 11 | tests := []struct { 12 | input string 13 | expected time.Time 14 | }{ 15 | { 16 | "", 17 | time.Time{}, 18 | }, 19 | { 20 | "error time", 21 | time.Time{}, 22 | }, 23 | { 24 | "18 January 2019", 25 | time.Date(2019, time.January, 18, 00, 0, 0, 0, time.UTC), 26 | }, 27 | } 28 | 29 | for _, tt := range tests { 30 | t.Run("", func(t *testing.T) { 31 | actual := ParseTime(tt.input) 32 | assert.Equal(t, tt.expected, actual) 33 | }) 34 | } 35 | } 36 | 37 | func TestFormatTime(t *testing.T) { 38 | tests := []struct { 39 | input time.Time 40 | expected string 41 | }{ 42 | { 43 | time.Date(2019, time.January, 1, 00, 00, 0, 0, time.UTC), 44 | "2019-01-01 00:00:00 UTC", 45 | }, 46 | } 47 | 48 | for _, tt := range tests { 49 | t.Run("", func(t *testing.T) { 50 | actual := FormatTime(tt.input) 51 | assert.Equal(t, tt.expected, actual) 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /internal/pkg/validator/password.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "github.com/ic3network/mccs-alpha/internal/pkg/passlib" 5 | ) 6 | 7 | func ValidatePassword(password string, confirmPassword string) []string { 8 | errorMessages := []string{} 9 | 10 | if password == "" { 11 | errorMessages = append(errorMessages, "Please enter a password.") 12 | } else if password != confirmPassword { 13 | errorMessages = append(errorMessages, "Password and confirmation password do not match.") 14 | } else { 15 | errorMessages = append(errorMessages, passlib.Validate(password)...) 16 | } 17 | 18 | return errorMessages 19 | } 20 | 21 | func validateUpdatePassword( 22 | currentPass string, 23 | newPass string, 24 | confirmPass string, 25 | ) []string { 26 | errorMessages := []string{} 27 | 28 | if currentPass == "" && newPass == "" && confirmPass == "" { 29 | return errorMessages 30 | } 31 | 32 | if currentPass == "" { 33 | errorMessages = append( 34 | errorMessages, 35 | "Please enter your current password.", 36 | ) 37 | } else if newPass != confirmPass { 38 | errorMessages = append(errorMessages, "New password and confirmation password do not match.") 39 | } else { 40 | errorMessages = append(errorMessages, passlib.Validate(newPass)...) 41 | } 42 | 43 | return errorMessages 44 | } 45 | -------------------------------------------------------------------------------- /internal/pkg/validator/register.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/ic3network/mccs-alpha/internal/app/types" 7 | "github.com/ic3network/mccs-alpha/internal/pkg/util" 8 | ) 9 | 10 | // ValidateBusiness validates 11 | // BusinessName, Offers and Wants 12 | func ValidateBusiness(b *types.BusinessData) []string { 13 | errs := []string{} 14 | if b.BusinessName == "" { 15 | errs = append(errs, "Business name is missing.") 16 | } else if len(b.BusinessName) > 100 { 17 | errs = append(errs, "Business Name cannot exceed 100 characters.") 18 | } 19 | if b.Website != "" && !strings.HasPrefix(b.Website, "http://") && 20 | !strings.HasPrefix(b.Website, "https://") { 21 | errs = append( 22 | errs, 23 | "Website URL should start with http:// or https://.", 24 | ) 25 | } else if len(b.Website) > 100 { 26 | errs = append(errs, "Website URL cannot exceed 100 characters.") 27 | } 28 | errs = append(errs, validateTagsLimit(b)...) 29 | return errs 30 | } 31 | 32 | // ValidateUser validates 33 | // FirstName, LastName, Email and Email 34 | func ValidateUser(u *types.User) []string { 35 | errorMessages := []string{} 36 | u.Email = strings.ToLower(u.Email) 37 | if u.Email == "" { 38 | errorMessages = append(errorMessages, "Email is missing.") 39 | } else if !util.IsValidEmail(u.Email) { 40 | errorMessages = append(errorMessages, "Email is invalid.") 41 | } else if len(u.Email) > 100 { 42 | errorMessages = append(errorMessages, "Email cannot exceed 100 characters.") 43 | } 44 | return errorMessages 45 | } 46 | -------------------------------------------------------------------------------- /internal/pkg/validator/tag.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "github.com/ic3network/mccs-alpha/internal/app/types" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | func validateTagsLimit(b *types.BusinessData) []string { 9 | errorMessages := []string{} 10 | 11 | if len(b.Offers) == 0 { 12 | errorMessages = append( 13 | errorMessages, 14 | "Missing at least one valid tag for Products/Services Offered.", 15 | ) 16 | } else if len(b.Offers) > viper.GetInt("tags_limit") { 17 | errorMessages = append(errorMessages, "No more than "+viper.GetString("tags_limit")+" tags can be specified for Products/Services Offered.") 18 | } 19 | 20 | if len(b.Wants) == 0 { 21 | errorMessages = append( 22 | errorMessages, 23 | "Missing at least one valid tag for Products/Services Wanted.", 24 | ) 25 | } else if len(b.Wants) > viper.GetInt("tags_limit") { 26 | errorMessages = append(errorMessages, "No more than "+viper.GetString("tags_limit")+" tags can be specified for Products/Services Wanted.") 27 | } 28 | 29 | for _, offer := range b.Offers { 30 | if len(offer.Name) > 50 { 31 | errorMessages = append( 32 | errorMessages, 33 | "An Offer tag cannot exceed 50 characters.", 34 | ) 35 | break 36 | } 37 | } 38 | 39 | for _, want := range b.Wants { 40 | if len(want.Name) > 50 { 41 | errorMessages = append( 42 | errorMessages, 43 | "A Want tag cannot exceed 50 characters.", 44 | ) 45 | break 46 | } 47 | } 48 | 49 | return errorMessages 50 | } 51 | -------------------------------------------------------------------------------- /internal/pkg/validator/validator.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "github.com/ic3network/mccs-alpha/internal/app/types" 5 | ) 6 | 7 | func Register(d *types.RegisterData) []string { 8 | errorMessages := []string{} 9 | errorMessages = append(errorMessages, ValidateBusiness(d.Business)...) 10 | errorMessages = append(errorMessages, ValidateUser(d.User)...) 11 | errorMessages = append( 12 | errorMessages, 13 | ValidatePassword(d.User.Password, d.ConfirmPassword)...) 14 | 15 | if d.User.Email != d.ConfirmEmail { 16 | errorMessages = append( 17 | errorMessages, 18 | "The email addresses you entered do not match.", 19 | ) 20 | } 21 | if d.Terms != "on" { 22 | errorMessages = append( 23 | errorMessages, 24 | "Please confirm you accept to have your business listed in OCN's directory.", 25 | ) 26 | } 27 | return errorMessages 28 | } 29 | 30 | func Account(d *types.UpdateAccountData) []string { 31 | errorMessages := []string{} 32 | errorMessages = append(errorMessages, ValidateBusiness(d.Business)...) 33 | errorMessages = append(errorMessages, ValidateUser(d.User)...) 34 | errorMessages = append( 35 | errorMessages, 36 | validateUpdatePassword( 37 | d.CurrentPassword, 38 | d.User.Password, 39 | d.ConfirmPassword, 40 | )...) 41 | return errorMessages 42 | } 43 | 44 | func UpdateBusiness(b *types.BusinessData) []string { 45 | errorMessages := []string{} 46 | errorMessages = append(errorMessages, ValidateBusiness(b)...) 47 | return errorMessages 48 | } 49 | -------------------------------------------------------------------------------- /internal/pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | var ( 9 | gitTag string 10 | gitCommit = "$Format:%H$" 11 | gitTreeState = "not a git tree" 12 | buildDate = "1970-01-01T00:00:00Z" 13 | ) 14 | 15 | // Info contains versioning information. 16 | type Info struct { 17 | GitTag string `json:"gitTag"` 18 | GitCommit string `json:"gitCommit"` 19 | GitTreeState string `json:"gitTreeState"` 20 | BuildDate string `json:"buildDate"` 21 | GoVersion string `json:"goVersion"` 22 | Compiler string `json:"compiler"` 23 | Platform string `json:"platform"` 24 | } 25 | 26 | func Get() Info { 27 | return Info{ 28 | GitTag: gitTag, 29 | GitCommit: gitCommit, 30 | GitTreeState: gitTreeState, 31 | BuildDate: buildDate, 32 | GoVersion: runtime.Version(), 33 | Compiler: runtime.Compiler, 34 | Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/seed/data/admin_tag.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Cafe/Bar", 4 | "createdAt": "2019-04-06T10:52:52Z", 5 | "updatedAt": "2019-04-06T10:52:52Z" 6 | }, 7 | { 8 | "name": "Transport", 9 | "createdAt": "2019-04-06T10:52:52Z", 10 | "updatedAt": "2019-04-06T10:52:52Z" 11 | }, 12 | { 13 | "name": "Restaurant", 14 | "createdAt": "2019-04-06T10:52:52Z", 15 | "updatedAt": "2019-04-06T10:52:52Z" 16 | }, 17 | { 18 | "name": "Manufacturing", 19 | "createdAt": "2019-04-06T10:52:52Z", 20 | "updatedAt": "2019-04-06T10:52:52Z" 21 | }, 22 | { 23 | "name": "Professional Services", 24 | "createdAt": "2019-04-06T10:52:52Z", 25 | "updatedAt": "2019-04-06T10:52:52Z" 26 | }, 27 | { 28 | "name": "Agriculture", 29 | "createdAt": "2019-04-06T10:52:52Z", 30 | "updatedAt": "2019-04-06T10:52:52Z" 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /internal/seed/data/admin_user.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "email": "admin1@dev.null", 3 | "name": "admin1", 4 | "password": "password", 5 | "roles": [], 6 | "createdAt": "2019-02-12T04:46:10Z", 7 | "updatedAt": "2019-05-15T01:41:33Z" 8 | }, { 9 | "email": "admin2@dev.null", 10 | "name": "admin2", 11 | "password": "password", 12 | "roles": [], 13 | "createdAt": "2019-01-05T06:33:03Z", 14 | "updatedAt": "2019-04-20T20:40:01Z" 15 | }] 16 | -------------------------------------------------------------------------------- /reflex.dev.conf: -------------------------------------------------------------------------------- 1 | -r '(\.go$|\.html$)' -R '_test\.go$' -s go run cmd/mccs-alpha/main.go 2 | -------------------------------------------------------------------------------- /web/static/css/main.css: -------------------------------------------------------------------------------- 1 | .all-content { 2 | display : flex; 3 | min-height : 100vh; 4 | flex-direction: column; 5 | } 6 | 7 | .all-content-main { 8 | margin-top : 6em; 9 | margin-bottom: 2em; 10 | flex : 1; 11 | } 12 | 13 | .login-box { 14 | max-width: 450px; 15 | } 16 | 17 | .ui.container.topnav { 18 | overflow: hidden; 19 | } 20 | 21 | .ui.container.topnav a { 22 | float : left; 23 | display: block; 24 | } 25 | 26 | .ui.container.topnav .menu-icon { 27 | display: none; 28 | } 29 | 30 | .menu-icon { 31 | display: none; 32 | } 33 | 34 | /* for menu */ 35 | @media screen and (max-width: 600px) { 36 | .ui.container.topnav a:not(:first-child) { 37 | display: none; 38 | } 39 | 40 | .ui.container.topnav .menu-icon { 41 | float : right; 42 | display: block; 43 | } 44 | 45 | .ui.form .field>label { 46 | margin-top: 1.5em; 47 | } 48 | } 49 | 50 | /* for pagination */ 51 | @media screen and (max-width: 425px) { 52 | tfoot { 53 | display: flex !important; 54 | justify-content: center !important; 55 | } 56 | .pagination.menu { 57 | font-size: 10px; 58 | } 59 | .right-chevron, .left-chevron { 60 | display: none !important; 61 | } 62 | } 63 | 64 | .server-success, .ajax-success, .server-error, .ajax-error { 65 | margin-bottom: 15px !important; 66 | } 67 | 68 | .anchored { padding-top: 4em !important; } 69 | 70 | /* for search box */ 71 | 72 | .spaced { 73 | margin-top: 1em !important; 74 | margin-bottom: 1em !important; 75 | } 76 | 77 | .active-link { 78 | color: #4183c4; 79 | cursor: pointer; 80 | } 81 | 82 | .padded { 83 | margin-top: 0.25em !important; 84 | margin-bottom: 0.25em !important; 85 | } 86 | 87 | .notification-badge { 88 | position: absolute; 89 | top: -10px; 90 | right: -10px; 91 | padding: 5px 10px; 92 | border-radius: 50%; 93 | background: red; 94 | color: white; 95 | } 96 | 97 | /* for browse directory tab */ 98 | .alphabet-list { 99 | font-size: 1.8rem; 100 | } 101 | -------------------------------------------------------------------------------- /web/static/css/themes/default/assets/fonts/brand-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ic3software/mccs-alpha/fd659943feceddb6df036ff4217cdca9ab12714a/web/static/css/themes/default/assets/fonts/brand-icons.eot -------------------------------------------------------------------------------- /web/static/css/themes/default/assets/fonts/brand-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ic3software/mccs-alpha/fd659943feceddb6df036ff4217cdca9ab12714a/web/static/css/themes/default/assets/fonts/brand-icons.ttf -------------------------------------------------------------------------------- /web/static/css/themes/default/assets/fonts/brand-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ic3software/mccs-alpha/fd659943feceddb6df036ff4217cdca9ab12714a/web/static/css/themes/default/assets/fonts/brand-icons.woff -------------------------------------------------------------------------------- /web/static/css/themes/default/assets/fonts/brand-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ic3software/mccs-alpha/fd659943feceddb6df036ff4217cdca9ab12714a/web/static/css/themes/default/assets/fonts/brand-icons.woff2 -------------------------------------------------------------------------------- /web/static/css/themes/default/assets/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ic3software/mccs-alpha/fd659943feceddb6df036ff4217cdca9ab12714a/web/static/css/themes/default/assets/fonts/icons.eot -------------------------------------------------------------------------------- /web/static/css/themes/default/assets/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ic3software/mccs-alpha/fd659943feceddb6df036ff4217cdca9ab12714a/web/static/css/themes/default/assets/fonts/icons.ttf -------------------------------------------------------------------------------- /web/static/css/themes/default/assets/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ic3software/mccs-alpha/fd659943feceddb6df036ff4217cdca9ab12714a/web/static/css/themes/default/assets/fonts/icons.woff -------------------------------------------------------------------------------- /web/static/css/themes/default/assets/fonts/icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ic3software/mccs-alpha/fd659943feceddb6df036ff4217cdca9ab12714a/web/static/css/themes/default/assets/fonts/icons.woff2 -------------------------------------------------------------------------------- /web/static/css/themes/default/assets/fonts/outline-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ic3software/mccs-alpha/fd659943feceddb6df036ff4217cdca9ab12714a/web/static/css/themes/default/assets/fonts/outline-icons.eot -------------------------------------------------------------------------------- /web/static/css/themes/default/assets/fonts/outline-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ic3software/mccs-alpha/fd659943feceddb6df036ff4217cdca9ab12714a/web/static/css/themes/default/assets/fonts/outline-icons.ttf -------------------------------------------------------------------------------- /web/static/css/themes/default/assets/fonts/outline-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ic3software/mccs-alpha/fd659943feceddb6df036ff4217cdca9ab12714a/web/static/css/themes/default/assets/fonts/outline-icons.woff -------------------------------------------------------------------------------- /web/static/css/themes/default/assets/fonts/outline-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ic3software/mccs-alpha/fd659943feceddb6df036ff4217cdca9ab12714a/web/static/css/themes/default/assets/fonts/outline-icons.woff2 -------------------------------------------------------------------------------- /web/static/img/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ic3software/mccs-alpha/fd659943feceddb6df036ff4217cdca9ab12714a/web/static/img/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /web/static/img/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ic3software/mccs-alpha/fd659943feceddb6df036ff4217cdca9ab12714a/web/static/img/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /web/static/img/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ic3software/mccs-alpha/fd659943feceddb6df036ff4217cdca9ab12714a/web/static/img/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /web/static/img/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ic3software/mccs-alpha/fd659943feceddb6df036ff4217cdca9ab12714a/web/static/img/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /web/static/img/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ic3software/mccs-alpha/fd659943feceddb6df036ff4217cdca9ab12714a/web/static/img/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /web/static/img/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ic3software/mccs-alpha/fd659943feceddb6df036ff4217cdca9ab12714a/web/static/img/favicon/favicon.ico -------------------------------------------------------------------------------- /web/static/img/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ic3software/mccs-alpha/fd659943feceddb6df036ff4217cdca9ab12714a/web/static/img/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /web/static/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Open Credit Network", 3 | "short_name": "OCN", 4 | "icons": [ 5 | { 6 | "src": "/static/image/favicon/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/static/image/favicon/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#003fa7", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /web/template/admin/dashboard.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 |

Dashboard

3 |
4 | {{if ShouldDisplayTime .LastLoginDate}} 5 |

Welcome back, {{.Name}}

6 |
7 |

You last logged in on {{FormatTime .LastLoginDate}} from {{.LastLoginIP}}

8 |
9 | {{ else }} 10 |

Welcome to the Open Credit Network, {{.Name}}

11 | {{end}} 12 |
13 | {{ end }} 14 | -------------------------------------------------------------------------------- /web/template/admin/login.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 |
3 |
4 |

5 | Admin Login 6 |

7 |
8 |
9 |
10 |
11 | 12 | 13 |
14 |
15 |
16 |
17 | 18 | 19 |
20 |
21 |
22 | 23 |
24 |
25 |
26 |
27 |
28 | {{ end }} 29 | -------------------------------------------------------------------------------- /web/template/admin/transaction.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 |

Transfer Units

3 |
4 |
5 |
6 |
7 | 8 | {{if .FormData.FromEmail}} 9 | 11 | {{else}} 12 | 14 | {{end}} 15 |
16 |
17 | 18 | {{if .FormData.ToEmail}} 19 | 21 | {{else}} 22 | 24 | {{end}} 25 |
26 |
27 | 28 | {{if .FormData.Amount}} 29 | 30 | {{else}} 31 | 32 | {{end}} 33 |
34 |
35 |
36 | 37 | {{if .FormData.Description}} 38 | 39 | {{else}} 40 | 41 | {{end}} 42 |
43 | 46 |
47 |
48 | {{ end }} 49 | -------------------------------------------------------------------------------- /web/template/admin/user.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 |

View/Modify User

3 |
4 |
5 |

User Details

6 |
7 |
8 | 9 | {{if .User.FirstName}} 10 | 11 | {{else}} 12 | 13 | {{end}} 14 |
15 |
16 | 17 | {{if .User.LastName}} 18 | 19 | {{else}} 20 | 21 | {{end}} 22 |
23 |
24 |
25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 | {{if .User.Telephone}} 33 | 34 | {{else}} 35 | 36 | {{end}} 37 |
38 |
39 |
40 |
41 | 42 |
43 | {{if .User.DailyNotification}} 44 | 45 | {{else}} 46 | 47 | {{end}} 48 | 49 |
50 |
51 |
52 |
53 |
54 |

Reset Password

55 |

Create a new password for this user

56 |
57 |
58 | 59 | 60 |
61 |
62 | 63 | 64 |
65 |
66 | 69 |
70 |
71 | {{ end }} 72 | -------------------------------------------------------------------------------- /web/template/email/dailyEmail.html: -------------------------------------------------------------------------------- 1 | {{define "dailyEmail"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | Potential trades via the Open Credit Network 9 | 10 | 11 | 12 |
13 |
14 | {{if or .MatchedOffers .MatchedWants}} 15 |

Good news!

16 | {{if .MatchedOffers}} 17 |
18 |

Matched Offers

19 |

There are new customers on the Open Credit Network who want what you're offering:

20 |
21 | 32 |
33 |
34 | {{ end }} 35 | 36 |
37 | 38 | {{if .MatchedWants}} 39 |
40 |

Matched Wants

41 |

There are new suppliers on the Open Credit Network who are offering what you want:

42 |
43 | 54 |
55 |
56 | {{ end }} 57 |

If your wants, offers or business details have changed, please update your listing.

58 | 59 |

You can also unsubscribe from these emails by following this link.

60 | 61 |

Happy trading!
62 | The Open Credit Network

63 | {{ end }} 64 |
65 |
66 | 67 | 68 | {{ end }} 69 | -------------------------------------------------------------------------------- /web/template/email/thankYou.html: -------------------------------------------------------------------------------- 1 | {{define "thankYou"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | Open Credit Network 9 | 10 | 11 | 12 |

Hi {{.FirstName}},

13 |

Thanks for applying to join the Open Credit Network as a Trading Member. Your details will be reviewed by the OCN team and if everything is OK, your application for Membership will be approved and we will be in touch again very soon.

14 |

We can only offer credit to UK registered businesses, but Sole Traders can still become members; they just have to sell before they can buy.

15 |

If you know people from other businesses who may be interested in being part of the Open Credit Network, please spread the word.

16 |

If you have any questions, just let us know by replying to this email.

17 |

18 | In Mutuality! 19 |
20 | The OCN Team 21 |

22 | 23 | 24 | {{ end }} 25 | -------------------------------------------------------------------------------- /web/template/email/welcome.html: -------------------------------------------------------------------------------- 1 | {{define "welcome"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | Open Credit Network 9 | 10 | 11 | 12 |

Hi {{.BusinessName}},

13 |

Thanks for signing up to The Open Credit Network directory! Your details will be reviewed by the OCN team and if everything is OK, your directory entry will go live. We will be in touch again very soon.

14 |

The more detail you add about the goods and services you provide and are looking for, the more potential business it will generate, so please sign in to complete your profile.

15 |

If you would like to trade with another business using Mutual Credit you will need to apply to become a Trading Member.

16 |

If you know people from other businesses who may be interested in being part of the Open Credit Network please spread the word.

17 |

If you have any questions just let us know by replying to this email.

18 |

19 | In Mutuality! 20 |
21 | The OCN Team 22 |

23 | 24 | 25 | {{ end }} 26 | -------------------------------------------------------------------------------- /web/template/layout/base.html: -------------------------------------------------------------------------------- 1 | {{define "base"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Open Credit Network 22 | 23 | 24 | 25 |
26 |
27 |
{{template "header" . }}
28 |
29 |
30 | {{template "errors" .ErrorMessages}} 31 | {{template "messages" .Messages}} 32 |
{{template "content" .Yield}}
33 |
34 |
35 | 36 |
37 |
38 | 39 | 40 | 41 | {{ end }} 42 | -------------------------------------------------------------------------------- /web/template/layout/errors.html: -------------------------------------------------------------------------------- 1 | {{ define "errors" }} 2 | {{if .}} 3 |
4 |
5 | There were some errors with your submission 6 |
7 | 12 |
13 | {{end}} 14 | {{/* This is for AJAX */}} 15 | 18 | {{end}} 19 | -------------------------------------------------------------------------------- /web/template/layout/footer.html: -------------------------------------------------------------------------------- 1 | {{ define "footer" }} 2 | 19 | {{ end }} 20 | -------------------------------------------------------------------------------- /web/template/layout/header.html: -------------------------------------------------------------------------------- 1 | {{ define "header" }} 2 | 76 | {{ end }} 77 | -------------------------------------------------------------------------------- /web/template/layout/messages.html: -------------------------------------------------------------------------------- 1 | {{ define "messages" }} 2 | {{if .Success}} 3 |
4 |
5 | {{.Success}} 6 |
7 |
8 | {{end}} 9 | {{if .Info}} 10 |
11 |
12 | {{.Info}} 13 |
14 |
15 | {{end}} 16 | {{/* This is for AJAX */}} 17 | 20 | {{end}} 21 | -------------------------------------------------------------------------------- /web/template/login.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 |
3 |
4 |

5 | Login to your account 6 |

7 |
8 |
9 |
10 |
11 | 12 | 13 |
14 |
15 |
16 |
17 | 18 | 19 |
20 |
21 |
22 | 23 |
24 |
25 |
26 |
27 |

Click here if you forgot your password

28 |

Need an account? Sign Up

29 |
30 |
31 |
32 | {{ end }} 33 | -------------------------------------------------------------------------------- /web/template/lost-password.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 |
3 |
4 |

5 | Reset your password 6 |

7 |
8 |
9 | {{ if .Success }} 10 |
11 |
Email Sent
12 | A password reset link has been sent to your email address. Please use it to change your password 13 | immediately. 14 |
15 | {{ end }} 16 |
17 |
18 | 19 | 20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 |
28 | {{ end }} 29 | -------------------------------------------------------------------------------- /web/template/password-resets.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 |
3 |
4 |

5 | Enter a new password 6 |

7 |
8 |
9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 |
24 |
25 |
26 |
27 | {{ end }} 28 | --------------------------------------------------------------------------------