diff --git a/go.mod b/go.mod index 520f6a3..e9d1c96 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/gin-gonic/gin v1.6.3 github.com/go-playground/validator/v10 v10.3.0 // indirect github.com/jinzhu/gorm v1.9.16 + github.com/oklog/ulid v1.3.1 github.com/oklog/ulid/v2 v2.0.2 github.com/prometheus/client_golang v1.7.1 github.com/prometheus/common v0.10.0 diff --git a/go.sum b/go.sum index 265ec82..96b4945 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,10 @@ github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca h1:kWzLcty5V2rzOqJM7Tp/MfSX0RMSI1x4IOLApEefYxA= github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -183,6 +185,7 @@ github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc= github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -337,6 +340,7 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= diff --git a/handlers/badges.go b/handlers/badges.go index a8f175b..4691424 100644 --- a/handlers/badges.go +++ b/handlers/badges.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "math/rand" "net/http" "strconv" "time" @@ -15,7 +14,6 @@ import ( "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" "github.com/jinzhu/gorm/dialects/postgres" - "github.com/oklog/ulid/v2" "github.com/prometheus/client_golang/api" v1 "github.com/prometheus/client_golang/api/prometheus/v1" "github.com/prometheus/client_golang/prometheus" @@ -193,7 +191,7 @@ func (handler *BadgesHandler) queryBadgeFromDB(queryName string) (*models.Badge, } func (handler *BadgesHandler) CreateBadge(c *gin.Context) { - newID := newULID() + newID := NewULID() jsonB := new(postgres.Jsonb) options := models.PrometheusOptions{ ValueField: "", @@ -229,12 +227,6 @@ func (handler *BadgesHandler) CreateBadge(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"id": newID}) } -func newULID() string { - now := time.Now() - entropy := ulid.Monotonic(rand.New(rand.NewSource(now.UnixNano())), 0) - return ulid.MustNew(ulid.Timestamp(now), entropy).String() -} - func queryMetric(client api.Client, options *models.PrometheusOptions) (*Metric, error) { v1api := v1.NewAPI(client) ctx, cancel := context.WithTimeout(context.Background(), metricRequestTimeout) diff --git a/handlers/base.go b/handlers/base.go new file mode 100644 index 0000000..1bce0ec --- /dev/null +++ b/handlers/base.go @@ -0,0 +1,18 @@ +package handlers + +import ( + "math/rand" + "time" + + "github.com/oklog/ulid" +) + +func NewULID() string { + now := time.Now() + entropy := ulid.Monotonic(rand.New(rand.NewSource(now.UnixNano())), 0) + return ulid.MustNew(ulid.Timestamp(now), entropy).String() +} + +type GeneralError struct { + Message string `json:"msg"` +} diff --git a/handlers/users.go b/handlers/users.go new file mode 100644 index 0000000..9686782 --- /dev/null +++ b/handlers/users.go @@ -0,0 +1,127 @@ +package handlers + +import ( + "fmt" + "io/ioutil" + "net/http" + "time" + + "github.com/getsentry/sentry-go" + "github.com/gin-gonic/gin" + "github.com/jinzhu/gorm" + "github.com/prometheus/common/log" + "github.com/sirupsen/logrus" + "github.com/wahyd4/badger/models" +) + +const ( + githubHTTPTimeout = 10 +) + +type UsersHandler struct { + DB *gorm.DB +} + +type UserAuthenticationRequest struct { + Name string `json:"name" binding:"required"` + GithubID string `json:"githubId" binding:"required"` + GithubUsername string `json:"githubUsername" binding:"required"` + GithubAvatarURL string `json:"githubAvatar" binding:"required"` + Email string `json:"email"` + Token string `json:"token" binding:"required"` + ExpiresAt string `json:"expiresAt"` +} + +type UserInfo struct { + ID uint `json:"id"` + GithubID string `json:"githubId"` +} + +func (usersHandler *UsersHandler) Authenticate(c *gin.Context) { + var requestBody UserAuthenticationRequest + + if err := c.ShouldBindJSON(&requestBody); err != nil { + logrus.Error(err) + sentry.CaptureException(err) + c.JSON(http.StatusBadRequest, GeneralError{"invalid request body"}) + return + } + + if !validateUser(requestBody.Token) { + c.JSON(http.StatusBadRequest, GeneralError{fmt.Sprintf("invalid token for user %s", requestBody.Name)}) + return + } + var user models.User + + if err := usersHandler.DB.Preload("Token").First(&user, "name = ?", requestBody.Name).Error; err != nil { + if err == gorm.ErrRecordNotFound { + user = models.User{ + Name: requestBody.Name, + ULID: NewULID(), + GithubID: requestBody.GithubID, + GithubUsername: requestBody.GithubUsername, + GithubAvatarURL: requestBody.GithubAvatarURL, + Email: requestBody.Email, + Token: models.Token{ + Token: requestBody.Token, + ExpiresAt: requestBody.ExpiresAt, + }, + } + } else { + logrus.Error(err) + sentry.CaptureException(err) + c.JSON(http.StatusInternalServerError, GeneralError{"internal error, please try it later"}) + return + } + } else { + user.Token.Token = requestBody.Token + user.Token.ExpiresAt = requestBody.ExpiresAt + } + + if err := usersHandler.DB.Save(&user).Error; err != nil { + logrus.Error(err) + sentry.CaptureException(err) + c.JSON(http.StatusInternalServerError, GeneralError{"internal error, please try it later"}) + return + } + + c.JSON(http.StatusOK, UserInfo{ID: user.ID, GithubID: user.GithubID}) + +} + +func validateUser(token string) bool { + client := &http.Client{ + Timeout: time.Second * githubHTTPTimeout, + } + + req, err := http.NewRequest("GET", "https://api.github.com/user", nil) + if err != nil { + logrus.Error(err) + sentry.CaptureException(err) + return false + } + req.Header.Add("Accept", "application/vnd.github.v3+json") + req.Header.Add("Authorization", fmt.Sprintf("token %s", token)) + resp, err := client.Do(req) + + if err != nil { + logrus.Error(err) + sentry.CaptureException(err) + return false + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + + if err != nil { + logrus.Error(err) + sentry.CaptureException(err) + return false + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + log.Warnf("request with error %s", body) + return false + } + + logrus.Info() + return true +} diff --git a/main.go b/main.go index 19f6212..2d2544e 100644 --- a/main.go +++ b/main.go @@ -55,7 +55,7 @@ func main() { defer db.Close() // Migrate the schema - db.AutoMigrate(&models.Badge{}) + db.AutoMigrate(&models.Badge{}, &models.Token{}, &models.User{}) client, err := api.NewClient(api.Config{ Address: "https://prometheus-api.home.toozhao.com", @@ -68,6 +68,8 @@ func main() { handler := &handlers.BadgesHandler{DB: db, PrometheusAPIClient: client} + userHandler := &handlers.UsersHandler{DB: db} + r := gin.Default() r.Use(sentrygin.New(sentrygin.Options{ @@ -84,6 +86,7 @@ func main() { api := r.Group("/api") + api.POST("/auth", userHandler.Authenticate) api.POST("/badges", handler.CreateBadge) go func() { diff --git a/models/badge.go b/models/badge.go index 6747ac6..2c80ccd 100644 --- a/models/badge.go +++ b/models/badge.go @@ -10,6 +10,8 @@ type Badge struct { Label string ULID string `gorm:"column:ulid;type:varchar;not null;unique_index"` Type string + User User + UserID int Options *postgres.Jsonb OptionStruct interface{} `gorm:"-"` } diff --git a/models/user.go b/models/user.go new file mode 100644 index 0000000..dce3e29 --- /dev/null +++ b/models/user.go @@ -0,0 +1,22 @@ +package models + +import "github.com/jinzhu/gorm" + +type User struct { + gorm.Model + Name string + ULID string `gorm:"column:ulid"` + GithubID string + GithubUsername string + GithubAvatarURL string + Email string + Token Token + TokenID string +} + +type Token struct { + gorm.Model + Token string + ExpiresAt string + UserID int +}