├── .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 |
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 |
28 | {{ end }}
29 |
--------------------------------------------------------------------------------
/web/template/admin/transaction.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
48 | {{ end }}
49 |
--------------------------------------------------------------------------------
/web/template/admin/user.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
4 |
53 |
54 |
55 |
Create a new password for this user
56 |
66 |
67 | Update User
68 |
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 |
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 |
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 |
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 |
7 |
8 | {{range .}}
9 | {{ . }}
10 | {{end}}
11 |
12 |
13 | {{end}}
14 | {{/* This is for AJAX */}}
15 |
16 |
17 |
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 |
7 |
8 | {{end}}
9 | {{if .Info}}
10 |
11 |
14 |
15 | {{end}}
16 | {{/* This is for AJAX */}}
17 |
18 |
19 |
20 | {{end}}
21 |
--------------------------------------------------------------------------------
/web/template/login.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
4 |
7 |
8 |
9 |
15 |
21 |
22 |
Login
23 |
24 |
25 |
26 |
30 |
31 |
32 | {{ end }}
33 |
--------------------------------------------------------------------------------
/web/template/lost-password.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
4 |
7 |
8 |
9 | {{ if .Success }}
10 |
11 |
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 |
22 |
23 |
Reset Password
24 |
25 |
26 |
27 |
28 | {{ end }}
29 |
--------------------------------------------------------------------------------
/web/template/password-resets.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
4 |
7 |
8 |
9 |
10 |
16 |
22 |
Reset Password
23 |
24 |
25 |
26 |
27 | {{ end }}
28 |
--------------------------------------------------------------------------------