From b0b77d667c31ff02e015148faf65b2c36ad36ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 22 Sep 2014 10:46:13 +0200 Subject: [PATCH] added gravatar --- grafana | 2 +- grafana.go | 4 +- pkg/api/api.go | 12 ++-- pkg/api/api_account.go | 2 +- pkg/api/api_dtos.go | 2 +- pkg/api/api_google_oauth.go | 111 +++++++++++++++++++++++++++++ pkg/api/api_login.go | 25 +++++-- pkg/api/api_models.go | 26 ++++++- pkg/configuration/configuration.go | 27 ++++++- pkg/models/account.go | 1 + pkg/server/server.go | 6 +- pkg/stores/rethinkdb_accounts.go | 2 +- pkg/stores/store.go | 7 ++ todo.txt | 4 ++ views/index.html | 4 +- 15 files changed, 212 insertions(+), 23 deletions(-) create mode 100644 pkg/api/api_google_oauth.go create mode 100644 todo.txt diff --git a/grafana b/grafana index 5dfeddf5831..071ac0dc85e 160000 --- a/grafana +++ b/grafana @@ -1 +1 @@ -Subproject commit 5dfeddf583176b52ef36945ec5b6e73a7cdf8646 +Subproject commit 071ac0dc85e48be546315dde196f90f01ad7b274 diff --git a/grafana.go b/grafana.go index 704d2b0d1dd..31c46469a2f 100644 --- a/grafana.go +++ b/grafana.go @@ -5,6 +5,7 @@ import ( "time" log "github.com/alecthomas/log4go" + "github.com/torkelo/grafana-pro/pkg/configuration" "github.com/torkelo/grafana-pro/pkg/server" ) @@ -16,7 +17,8 @@ func main() { log.Info("Starting Grafana-Pro v.1-alpha") - server, err := server.NewServer(port) + cfg := configuration.NewCfg(port) + server, err := server.NewServer(cfg) if err != nil { time.Sleep(time.Second) panic(err) diff --git a/pkg/api/api.go b/pkg/api/api.go index 6f7ff5b60ec..9dd1b0bfbba 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -7,6 +7,7 @@ import ( "github.com/gin-gonic/gin" "github.com/gorilla/sessions" "github.com/torkelo/grafana-pro/pkg/components" + "github.com/torkelo/grafana-pro/pkg/configuration" "github.com/torkelo/grafana-pro/pkg/models" "github.com/torkelo/grafana-pro/pkg/stores" ) @@ -17,13 +18,15 @@ type HttpServer struct { store stores.Store renderer *components.PhantomRenderer router *gin.Engine + cfg *configuration.Cfg } var sessionStore = sessions.NewCookieStore([]byte("something-very-secret")) -func NewHttpServer(port string, store stores.Store) *HttpServer { +func NewHttpServer(cfg *configuration.Cfg, store stores.Store) *HttpServer { self := &HttpServer{} - self.port = port + self.cfg = cfg + self.port = cfg.Http.Port self.store = store self.renderer = &components.PhantomRenderer{ImagesDir: "data/png", PhantomDir: "_vendor/phantomjs"} @@ -63,9 +66,8 @@ func (self *HttpServer) ListenAndServe() { func (self *HttpServer) index(c *gin.Context) { viewModel := &IndexDto{} userAccount, _ := c.Get("userAccount") - if userAccount != nil { - viewModel.User.Login = userAccount.(*models.Account).Login - } + account, _ := userAccount.(*models.Account) + initCurrentUserDto(&viewModel.User, account) c.HTML(200, "index.html", viewModel) } diff --git a/pkg/api/api_account.go b/pkg/api/api_account.go index 719fde3f531..28c72c3bb50 100644 --- a/pkg/api/api_account.go +++ b/pkg/api/api_account.go @@ -20,7 +20,7 @@ func (self *HttpServer) getAccount(c *gin.Context, auth *authContext) { var account = auth.userAccount model := accountInfoDto{ - Login: account.Login, + Name: account.Name, Email: account.Email, AccountName: account.AccountName, } diff --git a/pkg/api/api_dtos.go b/pkg/api/api_dtos.go index b6af2c58145..ff6cc6670a9 100644 --- a/pkg/api/api_dtos.go +++ b/pkg/api/api_dtos.go @@ -1,8 +1,8 @@ package api type accountInfoDto struct { - Login string `json:"login"` Email string `json:"email"` + Name string `json:"name"` AccountName string `json:"accountName"` Collaborators []*collaboratorInfoDto `json:"collaborators"` } diff --git a/pkg/api/api_google_oauth.go b/pkg/api/api_google_oauth.go new file mode 100644 index 00000000000..5f833a23a54 --- /dev/null +++ b/pkg/api/api_google_oauth.go @@ -0,0 +1,111 @@ +package api + +import ( + "encoding/json" + "net/http" + + log "github.com/alecthomas/log4go" + "github.com/gin-gonic/gin" + "github.com/golang/oauth2" + "github.com/torkelo/grafana-pro/pkg/models" + "github.com/torkelo/grafana-pro/pkg/stores" +) + +var oauthCfg *oauth2.Config + +func init() { + addRoutes(func(self *HttpServer) { + if !self.cfg.Http.GoogleOAuth.Enabled { + return + } + + self.router.GET("/login/google", self.loginGoogle) + self.router.GET("/oauth2callback", self.oauthCallback) + + options := &oauth2.Options{ + ClientID: self.cfg.Http.GoogleOAuth.ClientId, + ClientSecret: self.cfg.Http.GoogleOAuth.ClientSecret, + RedirectURL: "http://localhost:3000/oauth2callback", + Scopes: []string{ + "https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/userinfo.email", + }, + } + + cfg, err := oauth2.NewConfig(options, + "https://accounts.google.com/o/oauth2/auth", + "https://accounts.google.com/o/oauth2/token") + + if err != nil { + log.Error("Failed to init google auth %v", err) + } + + oauthCfg = cfg + }) +} + +func (self *HttpServer) loginGoogle(c *gin.Context) { + url := oauthCfg.AuthCodeURL("", "online", "auto") + c.Redirect(302, url) +} + +type googleUserInfoDto struct { + Email string `json:"email"` + GivenName string `json:"givenName"` + FamilyName string `json:"familyName"` + Name string `json:"name"` +} + +func (self *HttpServer) oauthCallback(c *gin.Context) { + code := c.Request.URL.Query()["code"][0] + log.Info("OAuth code: %v", code) + + transport, err := oauthCfg.NewTransportWithCode(code) + if err != nil { + c.String(500, "Failed to exchange oauth token: "+err.Error()) + return + } + + client := http.Client{Transport: transport} + resp, err := client.Get("https://www.googleapis.com/oauth2/v1/userinfo?alt=json") + if err != nil { + c.String(500, err.Error()) + return + } + + var userInfo googleUserInfoDto + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&userInfo) + if err != nil { + c.String(500, err.Error()) + return + } + + if len(userInfo.Email) < 5 { + c.String(500, "Invalid email") + return + } + + // try find existing account + account, err := self.store.GetAccountByLogin(userInfo.Email) + + // create account if missing + if err == stores.ErrAccountNotFound { + account = &models.Account{ + Login: userInfo.Email, + Email: userInfo.Email, + Name: userInfo.Name, + } + + if err = self.store.CreateAccount(account); err != nil { + log.Error("Failed to create account %v", err) + c.String(500, "Failed to create account") + return + } + } + + // login + loginUserWithAccount(account, c) + + c.Redirect(302, "/") +} diff --git a/pkg/api/api_login.go b/pkg/api/api_login.go index 97821b70c1e..01fcfa95369 100644 --- a/pkg/api/api_login.go +++ b/pkg/api/api_login.go @@ -1,10 +1,15 @@ package api -import "github.com/gin-gonic/gin" +import ( + "github.com/gin-gonic/gin" + "github.com/torkelo/grafana-pro/pkg/models" + + log "github.com/alecthomas/log4go" +) func init() { addRoutes(func(self *HttpServer) { - self.router.GET("/login/*_", self.index) + self.router.GET("/login", self.index) self.router.POST("/login", self.loginPost) self.router.POST("/logout", self.logoutPost) }) @@ -35,9 +40,7 @@ func (self *HttpServer) loginPost(c *gin.Context) { return } - session, _ := sessionStore.Get(c.Request, "grafana-session") - session.Values["accountId"] = account.Id - session.Save(c.Request, c.Writer) + loginUserWithAccount(account, c) var resp = &LoginResultDto{} resp.Status = "Logged in" @@ -46,6 +49,18 @@ func (self *HttpServer) loginPost(c *gin.Context) { c.JSON(200, resp) } +func loginUserWithAccount(account *models.Account, c *gin.Context) { + if account == nil { + log.Error("Account login with nil account") + } + session, err := sessionStore.Get(c.Request, "grafana-session") + if err != nil { + log.Error("Failed to get session %v", err) + } + session.Values["accountId"] = account.Id + session.Save(c.Request, c.Writer) +} + func (self *HttpServer) logoutPost(c *gin.Context) { session, _ := sessionStore.Get(c.Request, "grafana-session") session.Values = nil diff --git a/pkg/api/api_models.go b/pkg/api/api_models.go index 22b5d5bb669..de2b8762d13 100644 --- a/pkg/api/api_models.go +++ b/pkg/api/api_models.go @@ -1,5 +1,13 @@ package api +import ( + "crypto/md5" + "fmt" + "strings" + + "github.com/torkelo/grafana-pro/pkg/models" +) + type saveDashboardCommand struct { Id string `json:"id"` Title string `json:"title"` @@ -15,7 +23,9 @@ type IndexDto struct { } type CurrentUserDto struct { - Login string `json:"login"` + Login string `json:"login"` + Email string `json:"email"` + GravatarUrl string `json:"gravatarUrl"` } type LoginResultDto struct { @@ -26,3 +36,17 @@ type LoginResultDto struct { func newErrorResponse(message string) *errorResponse { return &errorResponse{Message: message} } + +func initCurrentUserDto(userDto *CurrentUserDto, account *models.Account) { + if account != nil { + userDto.Login = account.Login + userDto.Email = account.Email + userDto.GravatarUrl = getGravatarUrl(account.Email) + } +} + +func getGravatarUrl(text string) string { + hasher := md5.New() + hasher.Write([]byte(strings.ToLower(text))) + return fmt.Sprintf("https://secure.gravatar.com/avatar/%x?s=90&default=mm", hasher.Sum(nil)) +} diff --git a/pkg/configuration/configuration.go b/pkg/configuration/configuration.go index 3bf6038df74..3f74b7a4896 100644 --- a/pkg/configuration/configuration.go +++ b/pkg/configuration/configuration.go @@ -1,11 +1,34 @@ package configuration type Cfg struct { - httpPort string - DashboardSource DashboardSourceCfg + Http HttpCfg +} + +type HttpCfg struct { + Port string + GoogleOAuth GoogleOAuthCfg +} + +type GoogleOAuthCfg struct { + Enabled bool + ClientId string + ClientSecret string } type DashboardSourceCfg struct { sourceType string path string } + +func NewCfg(port string) *Cfg { + return &Cfg{ + Http: HttpCfg{ + Port: port, + GoogleOAuth: GoogleOAuthCfg{ + Enabled: true, + ClientId: "106011922963-4pvl05e9urtrm8bbqr0vouosj3e8p8kb.apps.googleusercontent.com", + ClientSecret: "K2evIa4QhfbhhAm3SO72t2Zv", + }, + }, + } +} diff --git a/pkg/models/account.go b/pkg/models/account.go index 8697a691302..cd4e8469fdc 100644 --- a/pkg/models/account.go +++ b/pkg/models/account.go @@ -26,6 +26,7 @@ type Account struct { Email string AccountName string Password string + Name string NextDashboardId int UsingAccountId int Collaborators []CollaboratorLink diff --git a/pkg/server/server.go b/pkg/server/server.go index ea8c5c425f4..0a71ed44d41 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -2,6 +2,7 @@ package server import ( "github.com/torkelo/grafana-pro/pkg/api" + "github.com/torkelo/grafana-pro/pkg/configuration" "github.com/torkelo/grafana-pro/pkg/stores" ) @@ -10,9 +11,10 @@ type Server struct { Store stores.Store } -func NewServer(port string) (*Server, error) { +func NewServer(cfg *configuration.Cfg) (*Server, error) { store := stores.New() - httpServer := api.NewHttpServer(port, store) + + httpServer := api.NewHttpServer(cfg, store) return &Server{ HttpServer: httpServer, diff --git a/pkg/stores/rethinkdb_accounts.go b/pkg/stores/rethinkdb_accounts.go index 2a5632fb4de..ec5161475eb 100644 --- a/pkg/stores/rethinkdb_accounts.go +++ b/pkg/stores/rethinkdb_accounts.go @@ -56,7 +56,7 @@ func (self *rethinkStore) GetAccountByLogin(emailOrName string) (*models.Account var account models.Account err = resp.One(&account) if err != nil { - return nil, errors.New("Not found") + return nil, ErrAccountNotFound } return &account, nil diff --git a/pkg/stores/store.go b/pkg/stores/store.go index 79e577d143e..a8ec5b512a2 100644 --- a/pkg/stores/store.go +++ b/pkg/stores/store.go @@ -1,6 +1,8 @@ package stores import ( + "errors" + "github.com/torkelo/grafana-pro/pkg/models" ) @@ -17,6 +19,11 @@ type Store interface { Close() } +// Typed errors +var ( + ErrAccountNotFound = errors.New("Account not found") +) + func New() Store { return NewRethinkStore(&RethinkCfg{DatabaseName: "grafana"}) } diff --git a/todo.txt b/todo.txt new file mode 100644 index 00000000000..c54a7da8413 --- /dev/null +++ b/todo.txt @@ -0,0 +1,4 @@ +# Security +- OAuth and email, unique? +- Form authentication token +- Password hash diff --git a/views/index.html b/views/index.html index 69870fabea7..40045c7ea58 100644 --- a/views/index.html +++ b/views/index.html @@ -46,9 +46,7 @@