diff --git a/Contribution.md b/Contribution.md index 2e85c0cc6..d9fdba345 100644 --- a/Contribution.md +++ b/Contribution.md @@ -15,3 +15,7 @@ - Folders should be named in camel case i.e (peopleData) - Typescript files should be named in camel case also - Only the index.tsx files should be named in small letters + +### Prettier Fixing + +- Make sure you run ```yarn run prettier``` to fix prettier error before submitting diff --git a/README.md b/README.md index a917fffdc..584437d3a 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,34 @@ Meme image upload works with Relay enabled, so a running Relay is required for M MEME_URL= ``` +### Add REDIS for cache + +- Create a Redis instance +- Create a .env file and populate the .env files with these variables + +If you have a Redis url add the REDIS_URL variable to .env + +```REDIS_URL = ``` + +else add these variables to the env to enable Redis + +``` + REDIS_HOST = + REDIS_DB = + REDIS_USER = + REDIS_PASS = +``` + +### Add SuperAdmins to access admin dashboard + +Add comma separated public keys to the SUPER_ADMINS env var in the .env file, +any user public key added to this comaa separated strings will have access to the admin dashboard +e.g '{pubkey}, {pubkey}, {pubkey}' + +``` +ADMINS +``` + ### For Contributions Read the contribution doc [here](./Contribution.md) diff --git a/auth/auth.go b/auth/auth.go index 6a979088f..dd7adc5d9 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -83,6 +83,79 @@ func PubKeyContext(next http.Handler) http.Handler { }) } +// PubKeyContext parses pukey from signed timestamp +func PubKeyContextSuperAdmin(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.URL.Query().Get("token") + if token == "" { + token = r.Header.Get("x-jwt") + } + + if token == "" { + fmt.Println("[auth] no token") + http.Error(w, http.StatusText(401), 401) + return + } + + isJwt := strings.Contains(token, ".") && !strings.HasPrefix(token, ".") + + if isJwt { + claims, err := DecodeJwt(token) + + if err != nil { + fmt.Println("Failed to parse JWT") + http.Error(w, http.StatusText(401), 401) + return + } + + if claims.VerifyExpiresAt(time.Now().UnixNano(), true) { + fmt.Println("Token has expired") + http.Error(w, http.StatusText(401), 401) + return + } + + pubkey := fmt.Sprintf("%v", claims["pubkey"]) + if !AdminCheck(pubkey) { + fmt.Println("Not a super admin") + http.Error(w, http.StatusText(401), 401) + return + } + + ctx := context.WithValue(r.Context(), ContextKey, claims["pubkey"]) + next.ServeHTTP(w, r.WithContext(ctx)) + } else { + pubkey, err := VerifyTribeUUID(token, true) + + if pubkey == "" || err != nil { + fmt.Println("[auth] no pubkey || err != nil") + if err != nil { + fmt.Println(err) + } + http.Error(w, http.StatusText(401), 401) + return + } + + if !AdminCheck(pubkey) { + fmt.Println("Not a super admin") + http.Error(w, http.StatusText(401), 401) + return + } + + ctx := context.WithValue(r.Context(), ContextKey, pubkey) + next.ServeHTTP(w, r.WithContext(ctx)) + } + }) +} + +func AdminCheck(pubkey string) bool { + for _, val := range config.SuperAdmins { + if val == pubkey { + return true + } + } + return false +} + // VerifyTribeUUID takes base64 uuid and returns hex pubkey func VerifyTribeUUID(uuid string, checkTimestamp bool) (string, error) { diff --git a/config/config.go b/config/config.go index f98dff1b7..2ace1fa25 100644 --- a/config/config.go +++ b/config/config.go @@ -18,6 +18,7 @@ var RelayUrl string var MemeUrl string var RelayAuthKey string var RelayNodeKey string +var SuperAdmins []string = []string{""} // these are constants for the store var InvoiceList = "INVOICELIST" @@ -29,6 +30,9 @@ func InitConfig() { RelayUrl = os.Getenv("RELAY_URL") MemeUrl = os.Getenv("MEME_URL") RelayAuthKey = os.Getenv("RELAY_AUTH_KEY") + AdminStrings := os.Getenv("ADMINS") + // Add to super admins + SuperAdmins = StripSuperAdmins(AdminStrings) // only make this call if there is a Relay auth key if RelayAuthKey != "" { @@ -46,7 +50,28 @@ func InitConfig() { if JwtKey == "" { JwtKey = GenerateRandomString() } +} + +func StripSuperAdmins(adminStrings string) []string { + superAdmins := []string{} + if adminStrings != "" { + if strings.Contains(adminStrings, ",") { + splitArray := strings.Split(adminStrings, ",") + splitLength := len(splitArray) + for i := 0; i < splitLength; i++ { + // append indexes, and skip all the commas + if splitArray[i] == "," { + continue + } else { + superAdmins = append(superAdmins, strings.TrimSpace(splitArray[i])) + } + } + } else { + superAdmins = append(superAdmins, strings.TrimSpace(adminStrings)) + } + } + return superAdmins } func GenerateRandomString() string { diff --git a/config/config_test.go b/config/config_test.go index 3e1e6e4ff..f6e943dd2 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2,9 +2,11 @@ package config import ( "fmt" - "github.com/h2non/gock" "os" "testing" + + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" ) func TestInitConfig(t *testing.T) { @@ -46,8 +48,6 @@ func TestGenerateRandomString(t *testing.T) { func TestGetNodePubKey(t *testing.T) { defer gock.Off() - //response := map[string]string{"identity_pubkey": "1234"} - //success := map[string]bool{"success": true} response := NodeGetInfoResponse{IdentityPubkey: "1234"} nodeGetInfo := NodeGetInfo{Success: true, Response: response} @@ -66,3 +66,21 @@ func TestGetNodePubKey(t *testing.T) { } } + +func TestStripSuperAdmins(t *testing.T) { + testAdminList := "hello, hi, yes, now" + admins := StripSuperAdmins(testAdminList) + assert.Equal(t, len(admins), 4) + + testAdminNocomma := "hello" + adminsNoComma := StripSuperAdmins(testAdminNocomma) + assert.Equal(t, len(adminsNoComma), 1) + + testNoAdmins := "" + noAdmins := StripSuperAdmins(testNoAdmins) + assert.Equal(t, len(noAdmins), 0) + + test2Admins := "hello, hi" + admins2 := StripSuperAdmins(test2Admins) + assert.Equal(t, len(admins2), 2) +} diff --git a/db/metrics.go b/db/metrics.go new file mode 100644 index 000000000..7ebaf8556 --- /dev/null +++ b/db/metrics.go @@ -0,0 +1,156 @@ +package db + +import ( + "fmt" + "math" + "net/http" + + "github.com/stakwork/sphinx-tribes/utils" +) + +var SecondsToDateConversion = 60 * 60 * 24 + +func (db database) TotalPeopleByDateRange(r PaymentDateRange) int64 { + var count int64 + db.db.Model(&Person{}).Where("created >= ?", r.StartDate).Where("created <= ?", r.EndDate).Count(&count) + return count +} + +func (db database) TotalOrganizationsByDateRange(r PaymentDateRange) int64 { + var count int64 + db.db.Model(&Organization{}).Where("created >= ?", r.StartDate).Where("created <= ?", r.EndDate).Count(&count) + return count +} + +func (db database) TotalPaymentsByDateRange(r PaymentDateRange) uint { + var sum uint + db.db.Model(&PaymentHistory{}).Where("payment_type = ?", r.PaymentType).Where("created >= ?", r.StartDate).Where("created <= ?", r.EndDate).Select("SUM(amount)").Row().Scan(&sum) + return sum +} + +func (db database) TotalSatsPosted(r PaymentDateRange) uint { + var sum uint + db.db.Model(&Bounty{}).Where("created >= ?", r.StartDate).Where("created <= ?", r.EndDate).Select("SUM(price)").Row().Scan(&sum) + return sum +} + +func (db database) TotalSatsPaid(r PaymentDateRange) uint { + var sum uint + db.db.Model(&Bounty{}).Where("paid = ?", true).Where("created >= ?", r.StartDate).Where("created <= ?", r.EndDate).Select("SUM(price)").Row().Scan(&sum) + return sum +} + +func (db database) SatsPaidPercentage(r PaymentDateRange) uint { + satsPosted := DB.TotalSatsPosted(r) + satsPaid := DB.TotalSatsPaid(r) + if satsPaid != 0 && satsPosted != 0 { + value := (satsPaid * 100) / satsPosted + paidPercentage := math.Round(float64(value)) + return uint(paidPercentage) + } + return 0 +} + +func (db database) TotalPaidBounties(r PaymentDateRange) int64 { + var count int64 + db.db.Model(&Bounty{}).Where("paid = ?", true).Where("created >= ?", r.StartDate).Where("created <= ?", r.EndDate).Count(&count) + return count +} + +func (db database) TotalBountiesPosted(r PaymentDateRange) int64 { + var count int64 + db.db.Model(&Bounty{}).Where("created >= ?", r.StartDate).Where("created <= ?", r.EndDate).Count(&count) + return count +} + +func (db database) BountiesPaidPercentage(r PaymentDateRange) uint { + bountiesPosted := DB.TotalBountiesPosted(r) + bountiesPaid := DB.TotalPaidBounties(r) + if bountiesPaid != 0 && bountiesPosted != 0 { + value := bountiesPaid * 100 / bountiesPosted + paidPercentage := math.Round(float64(value)) + return uint(paidPercentage) + } + return 0 +} + +func (db database) PaidDifference(r PaymentDateRange) []DateDifference { + ms := []DateDifference{} + + db.db.Raw(`SELECT EXTRACT(EPOCH FROM (paid_date - TO_TIMESTAMP(created))) as diff FROM public.bounty WHERE paid_date IS NOT NULL AND created >= '` + r.StartDate + `' AND created <= '` + r.EndDate + `' `).Find(&ms) + return ms +} + +func (db database) PaidDifferenceCount(r PaymentDateRange) int64 { + var count int64 + list := db.PaidDifference(r) + count = int64(len(list)) + return count +} + +func (db database) AveragePaidTime(r PaymentDateRange) uint { + paidList := DB.PaidDifference(r) + paidCount := DB.PaidDifferenceCount(r) + var paidSum uint + for _, diff := range paidList { + paidSum = uint(math.Round(diff.Diff)) + } + return CalculateAverageDays(paidCount, paidSum) +} + +func (db database) CompletedDifference(r PaymentDateRange) []DateDifference { + ms := []DateDifference{} + + db.db.Raw(`SELECT EXTRACT(EPOCH FROM (completion_date - TO_TIMESTAMP(created))) as diff FROM public.bounty WHERE completion_date IS NOT NULL AND created >= '` + r.StartDate + `' AND created <= '` + r.EndDate + `' `).Find(&ms) + return ms +} + +func (db database) CompletedDifferenceCount(r PaymentDateRange) int64 { + var count int64 + list := db.CompletedDifference(r) + count = int64(len(list)) + return count +} + +func (db database) AverageCompletedTime(r PaymentDateRange) uint { + paidList := DB.CompletedDifference(r) + paidCount := DB.CompletedDifferenceCount(r) + var paidSum uint + for _, diff := range paidList { + paidSum = uint(math.Round(diff.Diff)) + } + return CalculateAverageDays(paidCount, paidSum) +} + +func CalculateAverageDays(paidCount int64, paidSum uint) uint { + if paidCount != 0 && paidSum != 0 { + avg := paidSum / uint(paidCount) + avgSeconds := math.Round(float64(avg)) + avgDays := math.Round(avgSeconds / float64(SecondsToDateConversion)) + return uint(avgDays) + } + return 0 +} + +func (db database) GetBountiesByDateRange(r PaymentDateRange, re *http.Request) []Bounty { + offset, limit, sortBy, direction, _ := utils.GetPaginationParams(re) + + orderQuery := "" + limitQuery := "" + + if sortBy != "" && direction != "" { + orderQuery = "ORDER BY " + "body." + sortBy + " " + direction + } else { + orderQuery = " ORDER BY " + "body." + sortBy + "" + "DESC" + } + if limit != 0 { + limitQuery = fmt.Sprintf("LIMIT %d OFFSET %d", limit, offset) + } + + query := `SELECT * public.bounty WHERE created >= '` + r.StartDate + `' AND created <= '` + r.EndDate + `'` + allQuery := query + " " + " " + orderQuery + " " + limitQuery + + b := []Bounty{} + db.db.Raw(allQuery).Scan(&b) + return b +} diff --git a/db/redis.go b/db/redis.go new file mode 100644 index 000000000..b5e1871cc --- /dev/null +++ b/db/redis.go @@ -0,0 +1,72 @@ +package db + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/redis/go-redis/v9" + "github.com/stakwork/sphinx-tribes/utils" +) + +var ctx = context.Background() +var RedisClient *redis.Client +var RedisError interface{} +var expireTime = 6 * time.Hour + +func InitRedis() { + redisURL := os.Getenv("REDIS_URL") + + if redisURL == "" { + dbInt, _ := utils.ConvertStringToInt(os.Getenv("REDIS_DB")) + RedisClient = redis.NewClient(&redis.Options{ + Addr: os.Getenv("REDIS_HOST"), + Username: os.Getenv("REDIS_USER"), + Password: os.Getenv("REDIS_PASS"), + DB: dbInt, + }) + } else { + opt, err := redis.ParseURL(redisURL) + if err != nil { + RedisError = err + fmt.Println("REDIS URL CONNECTION ERROR ===", err) + } + RedisClient = redis.NewClient(opt) + } + if err := RedisClient.Ping(ctx); err != nil { + RedisError = err + fmt.Println("Could Not Connect To Redis", err) + } +} + +func SetValue(key string, value interface{}) { + err := RedisClient.Set(ctx, key, value, expireTime).Err() + if err != nil { + fmt.Println("REDIS SET ERROR :", err) + } +} + +func GetValue(key string) string { + val, err := RedisClient.Get(ctx, key).Result() + if err != nil { + fmt.Println("REDIS GET ERROR :", err) + } + + return val +} + +func SetMap(key string, values map[string]interface{}) { + for k, v := range values { + err := RedisClient.HSet(ctx, key, k, v).Err() + if err != nil { + fmt.Println("REDIS SET MAP ERROR :", err) + } + } + RedisClient.Expire(ctx, key, expireTime) +} + +func GetMap(key string) map[string]string { + values := RedisClient.HGetAll(ctx, key).Val() + return values +} diff --git a/db/structs.go b/db/structs.go index d50e794aa..2d92b3ef7 100644 --- a/db/structs.go +++ b/db/structs.go @@ -364,10 +364,9 @@ type Bounty struct { AssignedHours uint8 `json:"assigned_hours"` BountyExpires string `json:"bounty_expires"` CommitmentFee uint64 `json:"commitment_fee"` - Price string `json:"price"` + Price uint `json:"price"` Title string `json:"title"` Tribe string `json:"tribe"` - Created int64 `json:"created"` Assignee string `json:"assignee"` TicketUrl string `json:"ticket_url"` OrgUuid string `json:"org_uuid"` @@ -378,8 +377,12 @@ type Bounty struct { OneSentenceSummary string `json:"one_sentence_summary"` EstimatedSessionLength string `json:"estimated_session_length"` EstimatedCompletionDate string `json:"estimated_completion_date"` + Created int64 `json:"created"` Updated *time.Time `json:"updated"` - PaidDate *time.Time `json:"paid_date"` + AssignedDate *time.Time `json:"assigned_date,omitempty"` + CompletionDate *time.Time `json:"completion_date,omitempty"` + MarkAsPaidDate *time.Time `json:"mark_as_paid_date,omitempty"` + PaidDate *time.Time `json:"paid_date,omitempty"` CodingLanguages pq.StringArray `gorm:"type:text[];not null default:'[]'" json:"coding_languages"` } @@ -594,6 +597,12 @@ type WithdrawBudgetRequest struct { OrgUuid string `json:"org_uuid"` } +type PaymentDateRange struct { + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + PaymentType PaymentType `json:"payment_type,omitempty"` +} + type MemeChallenge struct { Id string `json:"id"` Challenge string `json:"challenge"` @@ -631,6 +640,21 @@ type Meme struct { Expiry *time.Time `json:"expiry"` } +type DateDifference struct { + Diff float64 `json:"diff"` +} + +type BountyMetrics struct { + BountiesPosted int64 `json:"bounties_posted"` + BountiesPaid int64 `json:"bounties_paid"` + BountiesPaidPercentage uint `json:"bounties_paid_average"` + SatsPosted uint `json:"sats_posted"` + SatsPaid uint `json:"sats_paid"` + SatsPaidPercentage uint `json:"sats_paid_percentage"` + AveragePaid uint `json:"average_paid"` + AverageCompleted uint `json:"average_completed"` +} + func (Person) TableName() string { return "people" } diff --git a/frontend/app/src/people/main/FocusView.tsx b/frontend/app/src/people/main/FocusView.tsx index 4ce16e3f3..72849fa97 100644 --- a/frontend/app/src/people/main/FocusView.tsx +++ b/frontend/app/src/people/main/FocusView.tsx @@ -186,6 +186,10 @@ function FocusedView(props: FocusViewProps) { newBody.description = description; } + if (newBody.price) { + newBody.price = Number(newBody.price); + } + // body.description = description; newBody.title = newBody.one_sentence_summary; diff --git a/go.mod b/go.mod index c3c1403a9..9aaf58172 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.2 require ( github.com/DATA-DOG/go-sqlmock v1.5.1 + github.com/ambelovsky/go-structs v1.1.0 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de github.com/btcsuite/btcd/btcec/v2 v2.3.2 github.com/btcsuite/btcd/chaincfg/chainhash v1.0.3 @@ -11,6 +12,7 @@ require ( github.com/form3tech-oss/jwt-go v3.2.5+incompatible github.com/go-chi/chi v1.5.5 github.com/go-chi/jwtauth v1.2.0 + github.com/go-co-op/gocron v1.37.0 github.com/gobuffalo/packr/v2 v2.8.3 github.com/google/go-github/v39 v39.2.0 github.com/gorilla/websocket v1.5.1 @@ -21,6 +23,7 @@ require ( github.com/lib/pq v1.10.9 github.com/nbd-wtf/ln-decodepay v1.11.1 github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/redis/go-redis/v9 v9.3.0 github.com/rs/cors v1.10.1 github.com/rs/xid v1.5.0 github.com/stretchr/testify v1.8.4 diff --git a/go.sum b/go.sum index 99331da7f..07d342104 100644 --- a/go.sum +++ b/go.sum @@ -1188,6 +1188,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/ambelovsky/go-structs v1.1.0 h1:LXj4/mHnYw0qhXQhOo96+ULGQ88H8qMcZd5SHef8boY= +github.com/ambelovsky/go-structs v1.1.0/go.mod h1:zN3RBXQvxgjjq/Q/WZS7p5AEK+qC9mNg7ycnvoQ63Ak= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= @@ -1245,6 +1247,8 @@ github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqO github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= @@ -1396,6 +1400,8 @@ github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0 github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dhui/dktest v0.3.16/go.mod h1:gYaA3LRmM8Z4vJl2MA0THIigJoZrwOansEOsp+kqxp0= github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= @@ -1468,6 +1474,8 @@ github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= github.com/go-chi/jwtauth v1.2.0 h1:Z116SPpevIABBYsv8ih/AHYBHmd4EufKSKsLUnWdrTM= github.com/go-chi/jwtauth v1.2.0/go.mod h1:NTUpKoTQV6o25UwYE6w/VaLUu83hzrVKYTVo+lE6qDA= +github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= +github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= @@ -2185,17 +2193,24 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg= +github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0= +github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= @@ -2398,6 +2413,8 @@ go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= diff --git a/handlers/bounties.go b/handlers/bounties.go index c452c9b9f..f2efb4e70 100644 --- a/handlers/bounties.go +++ b/handlers/bounties.go @@ -137,9 +137,9 @@ func MigrateBounties(w http.ResponseWriter, r *http.Request) { migrateBountyFinal.Award = Award } - Price, ok5 := migrateBounty["price"].(string) + Price, ok5 := migrateBounty["price"].(uint) if !ok5 { - migrateBountyFinal.Price = "0" + migrateBountyFinal.Price = 0 } else { migrateBountyFinal.Price = Price } diff --git a/handlers/bounty.go b/handlers/bounty.go index ada6fc4b7..2f2d353b6 100644 --- a/handlers/bounty.go +++ b/handlers/bounty.go @@ -159,6 +159,11 @@ func CreateOrEditBounty(w http.ResponseWriter, r *http.Request) { return } + if bounty.Assignee != "" { + now := time.Now() + bounty.AssignedDate = &now + } + if bounty.Tribe == "" { bounty.Tribe = "None" } @@ -209,8 +214,15 @@ func CreateOrEditBounty(w http.ResponseWriter, r *http.Request) { } func DeleteBounty(w http.ResponseWriter, r *http.Request) { - //ctx := r.Context() - //pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) + ctx := r.Context() + pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) + + if pubKeyFromAuth == "" { + fmt.Println("no pubkey from auth") + w.WriteHeader(http.StatusUnauthorized) + return + } + created := chi.URLParam(r, "created") pubkey := chi.URLParam(r, "pubkey") @@ -238,6 +250,13 @@ func UpdatePaymentStatus(w http.ResponseWriter, r *http.Request) { bounty, _ := db.DB.GetBountyByCreated(uint(created)) if bounty.ID != 0 && bounty.Created == int64(created) { bounty.Paid = !bounty.Paid + now := time.Now() + // if setting paid as true by mark as paid + // set completion date and mark as paid + if bounty.Paid { + bounty.CompletionDate = &now + bounty.MarkAsPaidDate = &now + } db.DB.UpdateBountyPayment(bounty) } w.WriteHeader(http.StatusOK) @@ -347,7 +366,7 @@ func MakeBountyPayment(w http.ResponseWriter, r *http.Request) { } bounty := db.DB.GetBounty(id) - amount, _ := utils.ConvertStringToUint(bounty.Price) + amount := bounty.Price if bounty.ID != id { w.WriteHeader(http.StatusNotFound) @@ -430,10 +449,11 @@ func MakeBountyPayment(w http.ResponseWriter, r *http.Request) { Status: true, PaymentType: "payment", } - db.DB.AddPaymentHistory(paymentHistory) + bounty.Paid = true bounty.PaidDate = &now + bounty.CompletionDate = &now db.DB.UpdateBounty(bounty) msg["msg"] = "keysend_success" diff --git a/handlers/invoiceCron.go b/handlers/invoiceCron.go new file mode 100644 index 000000000..f1f578d8b --- /dev/null +++ b/handlers/invoiceCron.go @@ -0,0 +1,244 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "strconv" + "time" + + "github.com/go-co-op/gocron" + "github.com/stakwork/sphinx-tribes/config" + "github.com/stakwork/sphinx-tribes/db" + "github.com/stakwork/sphinx-tribes/utils" +) + +// Keep for future reference +func InitInvoiceCron() { + s := gocron.NewScheduler(time.UTC) + msg := make(map[string]interface{}) + + s.Every(5).Seconds().Do(func() { + invoiceList, _ := db.Store.GetInvoiceCache() + invoiceCount := len(invoiceList) + + if invoiceCount > 0 { + for index, inv := range invoiceList { + url := fmt.Sprintf("%s/invoice?payment_request=%s", config.RelayUrl, inv.Invoice) + + client := &http.Client{} + req, err := http.NewRequest(http.MethodGet, url, nil) + + req.Header.Set("x-user-token", config.RelayAuthKey) + req.Header.Set("Content-Type", "application/json") + res, _ := client.Do(req) + + if err != nil { + log.Printf("Request Failed: %s", err) + return + } + + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + + // Unmarshal result + invoiceRes := db.InvoiceResult{} + + err = json.Unmarshal(body, &invoiceRes) + + if err != nil { + log.Printf("Reading Invoice body failed: %s", err) + return + } + + if invoiceRes.Response.Settled { + if inv.Invoice == invoiceRes.Response.Payment_request { + /** + If the invoice is settled and still in store + make keysend payment + */ + msg["msg"] = "invoice_success" + msg["invoice"] = inv.Invoice + + socket, err := db.Store.GetSocketConnections(inv.Host) + + if err == nil { + socket.Conn.WriteJSON(msg) + } + + if inv.Type == "KEYSEND" { + url := fmt.Sprintf("%s/payment", config.RelayUrl) + + amount, _ := utils.ConvertStringToUint(inv.Amount) + + bodyData := utils.BuildKeysendBodyData(amount, inv.User_pubkey, inv.Route_hint) + + jsonBody := []byte(bodyData) + + client := &http.Client{} + req, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonBody)) + + req.Header.Set("x-user-token", config.RelayAuthKey) + req.Header.Set("Content-Type", "application/json") + res, _ := client.Do(req) + + if err != nil { + log.Printf("Request Failed: %s", err) + return + } + + defer res.Body.Close() + + body, err = io.ReadAll(res.Body) + + if res.StatusCode == 200 { + // Unmarshal result + keysendRes := db.KeysendSuccess{} + err = json.Unmarshal(body, &keysendRes) + + dateInt, _ := strconv.ParseInt(inv.Created, 10, 32) + bounty, err := db.DB.GetBountyByCreated(uint(dateInt)) + + if err == nil { + bounty.Paid = true + } + + db.DB.UpdateBounty(bounty) + + // Delete the index from the store array list and reset the store + updateInvoiceCache(invoiceList, index) + + msg["msg"] = "keysend_success" + msg["invoice"] = inv.Invoice + + socket, err := db.Store.GetSocketConnections(inv.Host) + if err == nil { + socket.Conn.WriteJSON(msg) + } + } else { + // Unmarshal result + keysendError := db.KeysendError{} + err = json.Unmarshal(body, &keysendError) + log.Printf("Keysend Payment to %s Failed, with Error: %s", inv.User_pubkey, keysendError.Error) + + msg["msg"] = "keysend_error" + msg["invoice"] = inv.Invoice + + socket, err := db.Store.GetSocketConnections(inv.Host) + + if err == nil { + socket.Conn.WriteJSON(msg) + } + + updateInvoiceCache(invoiceList, index) + } + + if err != nil { + log.Printf("Reading body failed: %s", err) + return + } + } else { + dateInt, _ := strconv.ParseInt(inv.Created, 10, 32) + bounty, err := db.DB.GetBountyByCreated(uint(dateInt)) + + if err == nil { + bounty.Assignee = inv.User_pubkey + bounty.CommitmentFee = uint64(inv.Commitment_fee) + bounty.AssignedHours = uint8(inv.Assigned_hours) + bounty.BountyExpires = inv.Bounty_expires + } + + db.DB.UpdateBounty(bounty) + + // Delete the index from the store array list and reset the store + updateInvoiceCache(invoiceList, index) + + msg := make(map[string]interface{}) + msg["msg"] = "assign_success" + msg["invoice"] = inv.Invoice + + socket, err := db.Store.GetSocketConnections(inv.Host) + if err == nil { + socket.Conn.WriteJSON(msg) + } + } + } + } + } + } + }) + + s.Every(5).Seconds().Do(func() { + invoiceList, _ := db.Store.GetBudgetInvoiceCache() + invoiceCount := len(invoiceList) + + if invoiceCount > 0 { + for index, inv := range invoiceList { + url := fmt.Sprintf("%s/invoice?payment_request=%s", config.RelayUrl, inv.Invoice) + + client := &http.Client{} + req, err := http.NewRequest(http.MethodGet, url, nil) + + req.Header.Set("x-user-token", config.RelayAuthKey) + req.Header.Set("Content-Type", "application/json") + res, _ := client.Do(req) + + if err != nil { + log.Printf("Request Failed: %s", err) + return + } + + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + + // Unmarshal result + invoiceRes := db.InvoiceResult{} + + err = json.Unmarshal(body, &invoiceRes) + + if err != nil { + log.Printf("Reading Organization Invoice body failed: %s", err) + return + } + + if invoiceRes.Response.Settled { + if inv.Invoice == invoiceRes.Response.Payment_request { + /** + If the invoice is settled and still in store + make keysend payment + */ + msg["msg"] = "budget_success" + msg["invoice"] = inv.Invoice + + socket, err := db.Store.GetSocketConnections(inv.Host) + + if err == nil { + socket.Conn.WriteJSON(msg) + } + + // db.DB.AddAndUpdateBudget(inv) + updateBudgetInvoiceCache(invoiceList, index) + } + } + } + + } + }) + + s.StartAsync() +} + +func updateInvoiceCache(invoiceList []db.InvoiceStoreData, index int) { + newInvoiceList := append(invoiceList[:index], invoiceList[index+1:]...) + db.Store.SetInvoiceCache(newInvoiceList) +} + +func updateBudgetInvoiceCache(invoiceList []db.BudgetStoreData, index int) { + newInvoiceList := append(invoiceList[:index], invoiceList[index+1:]...) + db.Store.SetBudgetInvoiceCache(newInvoiceList) +} diff --git a/handlers/metrics.go b/handlers/metrics.go new file mode 100644 index 000000000..a78c5e1e1 --- /dev/null +++ b/handlers/metrics.go @@ -0,0 +1,213 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/ambelovsky/go-structs" + "github.com/stakwork/sphinx-tribes/auth" + "github.com/stakwork/sphinx-tribes/db" +) + +func PaymentMetrics(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) + + if pubKeyFromAuth == "" { + fmt.Println("no pubkey from auth") + w.WriteHeader(http.StatusUnauthorized) + return + } + + request := db.PaymentDateRange{} + body, err := io.ReadAll(r.Body) + r.Body.Close() + + err = json.Unmarshal(body, &request) + if err != nil { + w.WriteHeader(http.StatusNotAcceptable) + json.NewEncoder(w).Encode("Request body not accepted") + return + } + + sumAmount := db.DB.TotalPaymentsByDateRange(request) + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(sumAmount) +} + +func OrganizationtMetrics(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) + + if pubKeyFromAuth == "" { + fmt.Println("no pubkey from auth") + w.WriteHeader(http.StatusUnauthorized) + return + } + + request := db.PaymentDateRange{} + body, err := io.ReadAll(r.Body) + r.Body.Close() + + err = json.Unmarshal(body, &request) + if err != nil { + w.WriteHeader(http.StatusNotAcceptable) + json.NewEncoder(w).Encode("Request body not accepted") + return + } + + sumAmount := db.DB.TotalOrganizationsByDateRange(request) + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(sumAmount) +} + +func PeopleMetrics(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) + + if pubKeyFromAuth == "" { + fmt.Println("no pubkey from auth") + w.WriteHeader(http.StatusUnauthorized) + return + } + + request := db.PaymentDateRange{} + body, err := io.ReadAll(r.Body) + r.Body.Close() + + err = json.Unmarshal(body, &request) + if err != nil { + w.WriteHeader(http.StatusNotAcceptable) + json.NewEncoder(w).Encode("Request body not accepted") + return + } + + sumAmount := db.DB.TotalOrganizationsByDateRange(request) + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(sumAmount) +} + +func BountyMetrics(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) + + if pubKeyFromAuth == "" { + fmt.Println("no pubkey from auth") + w.WriteHeader(http.StatusUnauthorized) + return + } + + request := db.PaymentDateRange{} + body, err := io.ReadAll(r.Body) + r.Body.Close() + + err = json.Unmarshal(body, &request) + if err != nil { + w.WriteHeader(http.StatusNotAcceptable) + json.NewEncoder(w).Encode("Request body not accepted") + return + } + + metricsKey := fmt.Sprintf("metrics - %s - %s", request.StartDate, request.EndDate) + /** + check redis if cache id available for the date range + or add to redis + */ + if db.RedisError == nil { + redisMetrics := db.GetMap(metricsKey) + if len(redisMetrics) != 0 { + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(redisMetrics) + return + } + } + + totalBountiesPosted := db.DB.TotalBountiesPosted(request) + totalBountiesPaid := db.DB.TotalPaidBounties(request) + bountiesPaidPercentage := db.DB.BountiesPaidPercentage(request) + totalSatsPosted := db.DB.TotalSatsPosted(request) + totalSatsPaid := db.DB.TotalSatsPaid(request) + satsPaidPercentage := db.DB.SatsPaidPercentage(request) + avgPaidDays := db.DB.AveragePaidTime(request) + avgCompletedDays := db.DB.AverageCompletedTime(request) + + bountyMetrics := db.BountyMetrics{ + BountiesPosted: totalBountiesPosted, + BountiesPaid: totalBountiesPaid, + BountiesPaidPercentage: bountiesPaidPercentage, + SatsPosted: totalSatsPosted, + SatsPaid: totalSatsPaid, + SatsPaidPercentage: satsPaidPercentage, + AveragePaid: avgPaidDays, + AverageCompleted: avgCompletedDays, + } + + if db.RedisError == nil { + metricsMap := structs.Map(bountyMetrics) + db.SetMap(metricsKey, metricsMap) + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(bountyMetrics) +} + +func MetricsBounties(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) + + if pubKeyFromAuth == "" { + fmt.Println("no pubkey from auth") + w.WriteHeader(http.StatusUnauthorized) + return + } + + request := db.PaymentDateRange{} + body, err := io.ReadAll(r.Body) + r.Body.Close() + + err = json.Unmarshal(body, &request) + if err != nil { + w.WriteHeader(http.StatusNotAcceptable) + json.NewEncoder(w).Encode("Request body not accepted") + return + } + + metricBounties := db.DB.GetBountiesByDateRange(request, r) + var metricBountiesData []db.BountyData + + for _, bounty := range metricBounties { + bountyOwner := db.DB.GetPersonByPubkey(bounty.OwnerID) + bountyAssignee := db.DB.GetPersonByPubkey(bounty.Assignee) + organization := db.DB.GetOrganizationByUuid(bounty.OrgUuid) + + bountyData := db.BountyData{ + Bounty: bounty, + BountyId: bounty.ID, + Person: bountyOwner, + BountyCreated: bounty.Created, + BountyDescription: bounty.Description, + BountyUpdated: bounty.Updated, + AssigneeId: bountyAssignee.ID, + AssigneeAlias: bountyAssignee.OwnerAlias, + AssigneeDescription: bountyAssignee.Description, + AssigneeRouteHint: bountyAssignee.OwnerRouteHint, + BountyOwnerId: bountyOwner.ID, + OwnerUuid: bountyOwner.Uuid, + OwnerDescription: bountyOwner.Description, + OwnerUniqueName: bountyOwner.UniqueName, + OwnerImg: bountyOwner.Img, + OrganizationName: organization.Name, + OrganizationImg: organization.Img, + OrganizationUuid: organization.Uuid, + } + + metricBountiesData = append(metricBountiesData, bountyData) + } + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(metricBountiesData) +} diff --git a/main.go b/main.go index 5c49444b3..bb75d7cb9 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,7 @@ func main() { } db.InitDB() + db.InitRedis() db.InitCache() db.InitRoles() // Config has to be inited before JWT, if not it will lead to NO JWT error diff --git a/routes/index.go b/routes/index.go index 126389c0f..afdeef70e 100644 --- a/routes/index.go +++ b/routes/index.go @@ -31,6 +31,7 @@ func NewRouter() *http.Server { r.Mount("/github_issue", GithubIssuesRoutes()) r.Mount("/gobounties", BountyRoutes()) r.Mount("/organizations", OrganizationRoutes()) + r.Mount("/metrics", MetricsRoutes()) r.Group(func(r chi.Router) { r.Get("/tribe_by_feed", handlers.GetFirstTribeByFeed) diff --git a/routes/metrics.go b/routes/metrics.go new file mode 100644 index 000000000..22082f243 --- /dev/null +++ b/routes/metrics.go @@ -0,0 +1,22 @@ +package routes + +import ( + "github.com/go-chi/chi" + "github.com/stakwork/sphinx-tribes/auth" + "github.com/stakwork/sphinx-tribes/handlers" +) + +func MetricsRoutes() chi.Router { + r := chi.NewRouter() + + r.Group(func(r chi.Router) { + r.Use(auth.PubKeyContextSuperAdmin) + + r.Post("/payment", handlers.PaymentMetrics) + r.Post("/people", handlers.PeopleMetrics) + r.Post("/organization", handlers.OrganizationtMetrics) + r.Post("/bounty", handlers.BountyMetrics) + r.Post("/bounties", handlers.MetricsBounties) + }) + return r +} diff --git a/utils/helpers.go b/utils/helpers.go index 7393b31c5..189b10ea4 100644 --- a/utils/helpers.go +++ b/utils/helpers.go @@ -5,6 +5,7 @@ import ( "encoding/base32" "fmt" "strconv" + "time" decodepay "github.com/nbd-wtf/ln-decodepay" ) @@ -52,3 +53,10 @@ func GetInvoiceAmount(paymentRequest string) uint { return amount } + +func GetDateDaysDifference(createdDate int64, paidDate *time.Time) int64 { + firstDate := time.Unix(createdDate, 0) + difference := paidDate.Sub(*&firstDate) + days := int64(difference.Hours() / 24) + return days +} diff --git a/utils/helpers_test.go b/utils/helpers_test.go new file mode 100644 index 000000000..046dc7d9d --- /dev/null +++ b/utils/helpers_test.go @@ -0,0 +1,74 @@ +package utils + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestGetDateDaysDifference(t *testing.T) { + created := 1700238956 + exactTime := time.Now().Unix() + + testDate := time.Now() + nextDate := time.Now().AddDate(0, 0, 2) + days := GetDateDaysDifference(int64(created), &testDate) + + assert.NotEqual(t, 0, days) + + daysEqual := GetDateDaysDifference(exactTime, &testDate) + assert.Equal(t, int64(0), daysEqual) + + daysNext := GetDateDaysDifference(exactTime, &nextDate) + assert.Equal(t, int64(2), daysNext) + + wrongDate := GetDateDaysDifference(0, &nextDate) + assert.Greater(t, wrongDate, int64(365)) +} + +func TestGetRandomToken(t *testing.T) { + randomToken32 := GetRandomToken(32) + assert.GreaterOrEqual(t, len(randomToken32), 32) + + randomToken64 := GetRandomToken(56) + assert.GreaterOrEqual(t, len(randomToken64), 56) +} + +func TestConvertStringToUint(t *testing.T) { + number := "20" + result, _ := ConvertStringToUint(number) + + assert.Equal(t, uint(20), result) + + wrongNum := "wrong" + result2, err := ConvertStringToUint(wrongNum) + assert.Equal(t, uint(0), result2) + + assert.NotEqual(t, err, nil) +} + +func TestConvertStringToInt(t *testing.T) { + number := "10" + result, _ := ConvertStringToInt(number) + + assert.Equal(t, int(10), result) + + wrongNum := "wrong" + result2, err := ConvertStringToInt(wrongNum) + assert.Equal(t, int(0), result2) + + assert.NotEqual(t, err, nil) +} + +func TestGetInvoiceAmount(t *testing.T) { + invoice := "lnbc15u1p3xnhl2pp5jptserfk3zk4qy42tlucycrfwxhydvlemu9pqr93tuzlv9cc7g3sdqsvfhkcap3xyhx7un8cqzpgxqzjcsp5f8c52y2stc300gl6s4xswtjpc37hrnnr3c9wvtgjfuvqmpm35evq9qyyssqy4lgd8tj637qcjp05rdpxxykjenthxftej7a2zzmwrmrl70fyj9hvj0rewhzj7jfyuwkwcg9g2jpwtk3wkjtwnkdks84hsnu8xps5vsq4gj5hs" + + amount := GetInvoiceAmount(invoice) + assert.Equal(t, uint(1500), amount) + + invalidInvoice := "lnbc15u1p3xnhl2pp5jptserfk3zk4qy42tlucycrfwxhydvlemu9pqr93tuzlv9cc7g3sdqsvfhkcap3xyhx7un8cqzpgxqzjcsp5f8c52y2stc300gl6s4xswtjpc37hrnnr3c9wvtgjfuvqmpm35evq9qyyssqy4lgd8tj637qcjp05rdpxxykjenthxftej7a2zzmwrmrl70fyj9hvj0rewhzj7jfyuwkwcg9g2jpwtk3wkjtwnkdks84hsnu8xps5vsq" + + amount2 := GetInvoiceAmount(invalidInvoice) + assert.Equal(t, uint(0), amount2) +}