Torkel Ödegaard 11 gadi atpakaļ
vecāks
revīzija
b0b77d667c

+ 1 - 1
grafana

@@ -1 +1 @@
-Subproject commit 5dfeddf583176b52ef36945ec5b6e73a7cdf8646
+Subproject commit 071ac0dc85e48be546315dde196f90f01ad7b274

+ 3 - 1
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)

+ 7 - 5
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)
 }

+ 1 - 1
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,
 	}

+ 1 - 1
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"`
 }

+ 111 - 0
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, "/")
+}

+ 20 - 5
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

+ 25 - 1
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))
+}

+ 25 - 2
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",
+			},
+		},
+	}
+}

+ 1 - 0
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

+ 4 - 2
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,

+ 1 - 1
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

+ 7 - 0
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"})
 }

+ 4 - 0
todo.txt

@@ -0,0 +1,4 @@
+# Security
+- OAuth and email, unique?
+- Form authentication token
+- Password hash

+ 1 - 3
views/index.html

@@ -46,9 +46,7 @@
 
 	<script>
 		window.grafanaBootData = {
-				user: {
-			    login: [[.User.Login]]
-				}
+		  user:[[.User]]
 		};
 	</script>
 </html>