Просмотр исходного кода

Merge branch 'master' of github.com:torkelo/grafana-pro

Conflicts:
	grafana
Torkel Ödegaard 11 лет назад
Родитель
Сommit
cdabe50320

+ 1 - 1
grafana

@@ -1 +1 @@
-Subproject commit 9b2476451ef341285e1387c6eefe97c7995e300a
+Subproject commit c62ee78cba92ce2733196a824b0f0b70e4c40bdb


+ 10 - 10
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/models"
 	"github.com/torkelo/grafana-pro/pkg/stores"
 )
 
@@ -53,12 +54,20 @@ func (self *HttpServer) ListenAndServe() {
 	// register default route
 	self.router.GET("/", self.auth(), self.index)
 	self.router.GET("/dashboard/*_", self.auth(), self.index)
+	self.router.GET("/admin/*_", self.auth(), self.index)
+	self.router.GET("/account/*_", self.auth(), self.index)
 
 	self.router.Run(":" + self.port)
 }
 
 func (self *HttpServer) index(c *gin.Context) {
-	c.HTML(200, "index.html", &indexViewModel{title: "hello from go"})
+	viewModel := &IndexDto{}
+	userAccount, _ := c.Get("userAccount")
+	if userAccount != nil {
+		viewModel.User.Login = userAccount.(*models.UserAccount).Login
+	}
+
+	c.HTML(200, "index.html", viewModel)
 }
 
 func CacheHeadersMiddleware() gin.HandlerFunc {
@@ -66,12 +75,3 @@ func CacheHeadersMiddleware() gin.HandlerFunc {
 		c.Writer.Header().Add("Cache-Control", "max-age=0, public, must-revalidate, proxy-revalidate")
 	}
 }
-
-// Api Handler Registration
-var routeHandlers = make([]routeHandlerRegisterFn, 0)
-
-type routeHandlerRegisterFn func(self *HttpServer)
-
-func addRoutes(fn routeHandlerRegisterFn) {
-	routeHandlers = append(routeHandlers, fn)
-}

+ 45 - 0
pkg/api/api_account.go

@@ -0,0 +1,45 @@
+package api
+
+import "github.com/gin-gonic/gin"
+
+func init() {
+	addRoutes(func(self *HttpServer) {
+		self.addRoute("POST", "/api/account/collaborators/add", self.addCollaborator)
+	})
+}
+
+type addCollaboratorDto struct {
+	Email string `json:"email" binding:"required"`
+}
+
+func (self *HttpServer) addCollaborator(c *gin.Context, auth *authContext) {
+	var model addCollaboratorDto
+
+	if !c.EnsureBody(&model) {
+		c.JSON(400, gin.H{"status": "Collaborator not found"})
+		return
+	}
+
+	collaborator, err := self.store.GetUserAccountLogin(model.Email)
+	if err != nil {
+		c.JSON(404, gin.H{"status": "Collaborator not found"})
+		return
+	}
+
+	userAccount := auth.userAccount
+
+	if collaborator.Id == userAccount.Id {
+		c.JSON(400, gin.H{"status": "Cannot add yourself as collaborator"})
+		return
+	}
+
+	err = userAccount.AddCollaborator(collaborator.Id)
+	if err != nil {
+		c.JSON(400, gin.H{"status": err.Error()})
+		return
+	}
+
+	self.store.SaveUserAccount(userAccount)
+
+	c.JSON(200, gin.H{"status": "Collaborator added"})
+}

+ 48 - 0
pkg/api/api_auth.go

@@ -0,0 +1,48 @@
+package api
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/torkelo/grafana-pro/pkg/models"
+)
+
+type authContext struct {
+	account     *models.UserAccount
+	userAccount *models.UserAccount
+}
+
+func (auth *authContext) getAccountId() int {
+	return auth.account.Id
+}
+
+func (self *HttpServer) authDenied(c *gin.Context) {
+	c.Writer.Header().Set("Location", "/login")
+	c.Abort(302)
+}
+
+func (self *HttpServer) auth() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		session, _ := sessionStore.Get(c.Request, "grafana-session")
+
+		if c.Request.URL.Path != "/login" && session.Values["userAccountId"] == nil {
+			self.authDenied(c)
+			return
+		}
+
+		account, err := self.store.GetAccount(session.Values["userAccountId"].(int))
+		if err != nil {
+			self.authDenied(c)
+			return
+		}
+
+		usingAccount, err := self.store.GetAccount(session.Values["usingAccountId"].(int))
+		if err != nil {
+			self.authDenied(c)
+			return
+		}
+
+		c.Set("userAccount", account)
+		c.Set("usingAccount", usingAccount)
+
+		session.Save(c.Request, c.Writer)
+	}
+}

+ 33 - 11
pkg/api/api_dashboard.go

@@ -8,29 +8,51 @@ import (
 
 func init() {
 	addRoutes(func(self *HttpServer) {
-		self.router.GET("/api/dashboards/:id", self.auth(), self.getDashboard)
-		self.router.GET("/api/search/", self.auth(), self.search)
-		self.router.POST("/api/dashboard", self.auth(), self.postDashboard)
+		self.addRoute("GET", "/api/dashboards/:slug", self.getDashboard)
+		self.addRoute("GET", "/api/search/", self.search)
+		self.addRoute("POST", "/api/dashboard/", self.postDashboard)
+		self.addRoute("DELETE", "/api/dashboard/:slug", self.deleteDashboard)
 	})
 }
 
-func (self *HttpServer) getDashboard(c *gin.Context) {
-	id := c.Params.ByName("id")
-	accountId, err := c.Get("accountId")
+func (self *HttpServer) getDashboard(c *gin.Context, auth *authContext) {
+	slug := c.Params.ByName("slug")
 
-	dash, err := self.store.GetDashboard(id, accountId.(int))
+	dash, err := self.store.GetDashboard(slug, auth.getAccountId())
 	if err != nil {
 		c.JSON(404, newErrorResponse("Dashboard not found"))
 		return
 	}
 
+	dash.Data["id"] = dash.Id
+
 	c.JSON(200, dash.Data)
 }
 
-func (self *HttpServer) search(c *gin.Context) {
+func (self *HttpServer) deleteDashboard(c *gin.Context, auth *authContext) {
+	slug := c.Params.ByName("slug")
+
+	dash, err := self.store.GetDashboard(slug, auth.getAccountId())
+	if err != nil {
+		c.JSON(404, newErrorResponse("Dashboard not found"))
+		return
+	}
+
+	err = self.store.DeleteDashboard(slug, auth.getAccountId())
+	if err != nil {
+		c.JSON(500, newErrorResponse("Failed to delete dashboard: "+err.Error()))
+		return
+	}
+
+	var resp = map[string]interface{}{"title": dash.Title}
+
+	c.JSON(200, resp)
+}
+
+func (self *HttpServer) search(c *gin.Context, auth *authContext) {
 	query := c.Params.ByName("q")
 
-	results, err := self.store.Query(query)
+	results, err := self.store.Query(query, auth.getAccountId())
 	if err != nil {
 		log.Error("Store query error: %v", err)
 		c.JSON(500, newErrorResponse("Failed"))
@@ -40,14 +62,14 @@ func (self *HttpServer) search(c *gin.Context) {
 	c.JSON(200, results)
 }
 
-func (self *HttpServer) postDashboard(c *gin.Context) {
+func (self *HttpServer) postDashboard(c *gin.Context, auth *authContext) {
 	var command saveDashboardCommand
 
 	if c.EnsureBody(&command) {
 		dashboard := models.NewDashboard("test")
 		dashboard.Data = command.Dashboard
 		dashboard.Title = dashboard.Data["title"].(string)
-		dashboard.AccountId = 1
+		dashboard.AccountId = auth.getAccountId()
 		dashboard.UpdateSlug()
 
 		if dashboard.Data["id"] != nil {

+ 8 - 21
pkg/api/api_login.go

@@ -35,34 +35,21 @@ func (self *HttpServer) loginPost(c *gin.Context) {
 	}
 
 	session, _ := sessionStore.Get(c.Request, "grafana-session")
-	session.Values["login"] = true
-	session.Values["accountId"] = account.DatabaseId
-
+	session.Values["userAccountId"] = account.Id
+	session.Values["usingAccountId"] = account.UsingAccountId
 	session.Save(c.Request, c.Writer)
 
-	c.JSON(200, gin.H{"status": "you are logged in"})
+	var resp = &LoginResultDto{}
+	resp.Status = "Logged in"
+	resp.User.Login = account.Login
+
+	c.JSON(200, resp)
 }
 
 func (self *HttpServer) logoutPost(c *gin.Context) {
 	session, _ := sessionStore.Get(c.Request, "grafana-session")
-	session.Values["login"] = nil
+	session.Values = nil
 	session.Save(c.Request, c.Writer)
 
 	c.JSON(200, gin.H{"status": "logged out"})
 }
-
-func (self *HttpServer) auth() gin.HandlerFunc {
-	return func(c *gin.Context) {
-		session, _ := sessionStore.Get(c.Request, "grafana-session")
-
-		if c.Request.URL.Path != "/login" && session.Values["login"] == nil {
-			c.Writer.Header().Set("Location", "/login")
-			c.Abort(302)
-			return
-		}
-
-		c.Set("accountId", session.Values["accountId"])
-
-		session.Save(c.Request, c.Writer)
-	}
-}

+ 11 - 2
pkg/api/api_models.go

@@ -10,8 +10,17 @@ type errorResponse struct {
 	Message string `json:"message"`
 }
 
-type indexViewModel struct {
-	title string
+type IndexDto struct {
+	User CurrentUserDto
+}
+
+type CurrentUserDto struct {
+	Login string `json:"login"`
+}
+
+type LoginResultDto struct {
+	Status string         `json:"status"`
+	User   CurrentUserDto `json:"user"`
 }
 
 func newErrorResponse(message string) *errorResponse {

+ 36 - 0
pkg/api/api_routing.go

@@ -0,0 +1,36 @@
+package api
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/torkelo/grafana-pro/pkg/models"
+)
+
+type routeHandlerRegisterFn func(self *HttpServer)
+type routeHandlerFn func(c *gin.Context, auth *authContext)
+
+var routeHandlers = make([]routeHandlerRegisterFn, 0)
+
+func getRouteHandlerWrapper(handler routeHandlerFn) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		authContext := authContext{
+			account:     c.MustGet("usingAccount").(*models.UserAccount),
+			userAccount: c.MustGet("userAccount").(*models.UserAccount),
+		}
+		handler(c, &authContext)
+	}
+}
+
+func (self *HttpServer) addRoute(method string, path string, handler routeHandlerFn) {
+	switch method {
+	case "GET":
+		self.router.GET(path, self.auth(), getRouteHandlerWrapper(handler))
+	case "POST":
+		self.router.POST(path, self.auth(), getRouteHandlerWrapper(handler))
+	case "DELETE":
+		self.router.DELETE(path, self.auth(), getRouteHandlerWrapper(handler))
+	}
+}
+
+func addRoutes(fn routeHandlerRegisterFn) {
+	routeHandlers = append(routeHandlers, fn)
+}

+ 43 - 0
pkg/models/account.go

@@ -0,0 +1,43 @@
+package models
+
+import (
+	"errors"
+	"time"
+)
+
+type CollaboratorLink struct {
+	AccountId  int
+	Role       string
+	ModifiedOn time.Time
+	CreatedOn  time.Time
+}
+
+type UserAccount struct {
+	Id              int `gorethink:"id"`
+	UserName        string
+	Login           string
+	Email           string
+	Password        string
+	NextDashboardId int
+	UsingAccountId  int
+	Collaborators   []CollaboratorLink
+	CreatedOn       time.Time
+	ModifiedOn      time.Time
+}
+
+func (account *UserAccount) AddCollaborator(accountId int) error {
+	for _, collaborator := range account.Collaborators {
+		if collaborator.AccountId == accountId {
+			return errors.New("Collaborator already exists")
+		}
+	}
+
+	account.Collaborators = append(account.Collaborators, CollaboratorLink{
+		AccountId:  accountId,
+		Role:       "admin",
+		CreatedOn:  time.Now(),
+		ModifiedOn: time.Now(),
+	})
+
+	return nil
+}

+ 0 - 25
pkg/models/dashboards.go

@@ -21,31 +21,6 @@ type Dashboard struct {
 	Data  map[string]interface{}
 }
 
-type UserAccountLink struct {
-	UserId     int
-	Role       string
-	ModifiedOn time.Time
-	CreatedOn  time.Time
-}
-
-type UserAccount struct {
-	DatabaseId      int `gorethink:"id"`
-	UserName        string
-	Login           string
-	Email           string
-	Password        string
-	NextDashboardId int
-	UsingAccountId  int
-	GrantedAccess   []UserAccountLink
-	CreatedOn       time.Time
-	ModifiedOn      time.Time
-}
-
-type UserContext struct {
-	UserId    string
-	AccountId string
-}
-
 type SearchResult struct {
 	Id    string `json:"id"`
 	Title string `json:"title"`

+ 24 - 3
pkg/stores/rethinkdb.go

@@ -1,6 +1,7 @@
 package stores
 
 import (
+	"errors"
 	"time"
 
 	log "github.com/alecthomas/log4go"
@@ -44,6 +45,10 @@ func NewRethinkStore(config *RethinkCfg) *rethinkStore {
 		return []interface{}{row.Field("AccountId"), row.Field("Slug")}
 	}).Exec(session)
 
+	r.Db(config.DatabaseName).Table("dashboards").IndexCreateFunc("AccountId", func(row r.Term) interface{} {
+		return []interface{}{row.Field("AccountId")}
+	}).Exec(session)
+
 	r.Db(config.DatabaseName).Table("accounts").IndexCreateFunc("AccountLogin", func(row r.Term) interface{} {
 		return []interface{}{row.Field("Login")}
 	}).Exec(session)
@@ -59,7 +64,7 @@ func NewRethinkStore(config *RethinkCfg) *rethinkStore {
 }
 
 func (self *rethinkStore) SaveDashboard(dash *models.Dashboard) error {
-	resp, err := r.Table("dashboards").Insert(dash, r.InsertOpts{Upsert: true}).RunWrite(self.session)
+	resp, err := r.Table("dashboards").Insert(dash, r.InsertOpts{Conflict: "update"}).RunWrite(self.session)
 	if err != nil {
 		return err
 	}
@@ -88,9 +93,25 @@ func (self *rethinkStore) GetDashboard(slug string, accountId int) (*models.Dash
 	return &dashboard, nil
 }
 
-func (self *rethinkStore) Query(query string) ([]*models.SearchResult, error) {
+func (self *rethinkStore) DeleteDashboard(slug string, accountId int) error {
+	resp, err := r.Table("dashboards").
+		GetAllByIndex("AccountIdSlug", []interface{}{accountId, slug}).
+		Delete().RunWrite(self.session)
+
+	if err != nil {
+		return err
+	}
+
+	if resp.Deleted != 1 {
+		return errors.New("Did not find dashboard to delete")
+	}
+
+	return nil
+}
+
+func (self *rethinkStore) Query(query string, accountId int) ([]*models.SearchResult, error) {
+	docs, err := r.Table("dashboards").GetAllByIndex("AccountId", []interface{}{accountId}).Filter(r.Row.Field("Title").Match(".*")).Run(self.session)
 
-	docs, err := r.Table("dashboards").Filter(r.Row.Field("Title").Match(".*")).Run(self.session)
 	if err != nil {
 		return nil, err
 	}

+ 28 - 7
pkg/stores/rethinkdb_accounts.go

@@ -10,17 +10,19 @@ import (
 func (self *rethinkStore) getNextAccountId() (int, error) {
 	resp, err := r.Table("master").Get("ids").Update(map[string]interface{}{
 		"NextAccountId": r.Row.Field("NextAccountId").Add(1),
-	}, r.UpdateOpts{ReturnVals: true}).RunWrite(self.session)
+	}, r.UpdateOpts{ReturnChanges: true}).RunWrite(self.session)
 
 	if err != nil {
 		return 0, err
 	}
 
-	if resp.NewValue == nil {
+	change := resp.Changes[0]
+
+	if change.NewValue == nil {
 		return 0, errors.New("Failed to get new value after incrementing account id")
 	}
 
-	return int(resp.NewValue.(map[string]interface{})["NextAccountId"].(float64)), nil
+	return int(change.NewValue.(map[string]interface{})["NextAccountId"].(float64)), nil
 }
 
 func (self *rethinkStore) SaveUserAccount(account *models.UserAccount) error {
@@ -29,7 +31,8 @@ func (self *rethinkStore) SaveUserAccount(account *models.UserAccount) error {
 		return err
 	}
 
-	account.DatabaseId = accountId
+	account.Id = accountId
+	account.UsingAccountId = accountId
 
 	resp, err := r.Table("accounts").Insert(account).RunWrite(self.session)
 	if err != nil {
@@ -59,18 +62,36 @@ func (self *rethinkStore) GetUserAccountLogin(emailOrName string) (*models.UserA
 	return &account, nil
 }
 
+func (self *rethinkStore) GetAccount(id int) (*models.UserAccount, error) {
+	resp, err := r.Table("accounts").Get(id).Run(self.session)
+
+	if err != nil {
+		return nil, err
+	}
+
+	var account models.UserAccount
+	err = resp.One(&account)
+	if err != nil {
+		return nil, errors.New("Not found")
+	}
+
+	return &account, nil
+}
+
 func (self *rethinkStore) getNextDashboardNumber(accountId int) (int, error) {
 	resp, err := r.Table("accounts").Get(accountId).Update(map[string]interface{}{
 		"NextDashboardId": r.Row.Field("NextDashboardId").Add(1),
-	}, r.UpdateOpts{ReturnVals: true}).RunWrite(self.session)
+	}, r.UpdateOpts{ReturnChanges: true}).RunWrite(self.session)
 
 	if err != nil {
 		return 0, err
 	}
 
-	if resp.NewValue == nil {
+	change := resp.Changes[0]
+
+	if change.NewValue == nil {
 		return 0, errors.New("Failed to get next dashboard id, no new value after update")
 	}
 
-	return int(resp.NewValue.(map[string]interface{})["NextDashboardId"].(float64)), nil
+	return int(change.NewValue.(map[string]interface{})["NextDashboardId"].(float64)), nil
 }

+ 3 - 3
pkg/stores/rethinkdb_test.go

@@ -38,17 +38,17 @@ func TestRethinkStore(t *testing.T) {
 		account := &models.UserAccount{UserName: "torkelo", Email: "mupp", Login: "test@test.com"}
 		err := store.SaveUserAccount(account)
 		So(err, ShouldBeNil)
-		So(account.DatabaseId, ShouldNotEqual, 0)
+		So(account.Id, ShouldNotEqual, 0)
 
 		read, err := store.GetUserAccountLogin("test@test.com")
 		So(err, ShouldBeNil)
-		So(read.DatabaseId, ShouldEqual, account.DatabaseId)
+		So(read.Id, ShouldEqual, account.DatabaseId)
 	})
 
 	Convey("can get next dashboard id", t, func() {
 		account := &models.UserAccount{UserName: "torkelo", Email: "mupp"}
 		err := store.SaveUserAccount(account)
-		dashId, err := store.getNextDashboardNumber(account.DatabaseId)
+		dashId, err := store.getNextDashboardNumber(account.Id)
 		So(err, ShouldBeNil)
 		So(dashId, ShouldEqual, 1)
 	})

+ 4 - 2
pkg/stores/store.go

@@ -5,11 +5,13 @@ import (
 )
 
 type Store interface {
-	GetDashboard(title string, accountId int) (*models.Dashboard, error)
+	GetDashboard(slug string, accountId int) (*models.Dashboard, error)
 	SaveDashboard(dash *models.Dashboard) error
-	Query(query string) ([]*models.SearchResult, error)
+	DeleteDashboard(slug string, accountId int) error
+	Query(query string, acccountId int) ([]*models.SearchResult, error)
 	SaveUserAccount(acccount *models.UserAccount) error
 	GetUserAccountLogin(emailOrName string) (*models.UserAccount, error)
+	GetAccount(id int) (*models.UserAccount, error)
 	Close()
 }
 

+ 20 - 8
views/index.html

@@ -1,6 +1,5 @@
 <!DOCTYPE html>
-  <!--[if IE 8]>         <html class="no-js lt-ie9" lang="en"> <![endif]-->
-  <!--[if gt IE 8]><!--> <html class="no-js" lang="en"> <!--<![endif]-->
+<html lang="en">
   <head>
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
@@ -8,6 +7,7 @@
 
     <title>Grafana</title>
     <link rel="stylesheet" href="/public/css/grafana.dark.min.css" title="Dark">
+    <link rel="icon" type="image/png" href="img/fav32.png">
 		<base href="/">
 
     <!-- build:js app/app.js -->
@@ -20,18 +20,22 @@
   </head>
 
   <body ng-cloak ng-controller="GrafanaCtrl">
-
     <link rel="stylesheet" href="/public/css/grafana.light.min.css" ng-if="grafana.style === 'light'">
 
-		<div class="pro-container" ng-class="{'pro-sidemenu-open': showProSideMenu}">
+		<div class="pro-container" ng-class="{'pro-sidemenu-open': grafana.sidemenu}">
 
-			<aside class="pro-sidemenu" ng-if="showProSideMenu">
+			<aside class="pro-sidemenu" ng-if="grafana.sidemenu">
 				<div ng-include="'app/partials/pro/sidemenu.html'"></div>
 			</aside>
 
-			<div ng-repeat='alert in dashAlerts.list' class="alert-{{alert.severity}} dashboard-notice" ng-show="$last">
-				<button type="button" class="close" ng-click="dashAlerts.clear(alert)" style="padding-right:50px">&times;</button>
-				<strong>{{alert.title}}</strong> <span ng-bind-html='alert.text'></span> <div style="padding-right:10px" class='pull-right small'> {{$index + 1}} alert(s) </div>
+			<div class="page-alert-list">
+				<div ng-repeat='alert in dashAlerts.list' class="alert-{{alert.severity}} alert">
+					<button type="button" class="alert-close" ng-click="dashAlerts.clear(alert)">
+						<i class="icon-remove-sign"></i>
+					</button>
+					<div class="alert-title">{{alert.title}}</div>
+					<div ng-bind-html='alert.text'></div>
+				</div>
 			</div>
 
 			<div ng-view class="pro-main-view"></div>
@@ -39,4 +43,12 @@
 		</div>
 
   </body>
+
+	<script>
+		window.grafanaBootData = {
+				user: {
+			    login: [[.User.Login]]
+				}
+		};
+	</script>
 </html>