diff --git a/grafana b/grafana index 9b2476451ef..c62ee78cba9 160000 --- a/grafana +++ b/grafana @@ -1 +1 @@ -Subproject commit 9b2476451ef341285e1387c6eefe97c7995e300a +Subproject commit c62ee78cba92ce2733196a824b0f0b70e4c40bdb diff --git a/grafana-pro b/grafana-pro index ffa72bf8aa0..0a717ff5f02 100755 Binary files a/grafana-pro and b/grafana-pro differ diff --git a/pkg/api/api.go b/pkg/api/api.go index 59c5079dde1..3167ebff74f 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/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) -} diff --git a/pkg/api/api_account.go b/pkg/api/api_account.go new file mode 100644 index 00000000000..c4cf1aa94b7 --- /dev/null +++ b/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"}) +} diff --git a/pkg/api/api_auth.go b/pkg/api/api_auth.go new file mode 100644 index 00000000000..5d5a65aaada --- /dev/null +++ b/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) + } +} diff --git a/pkg/api/api_dashboard.go b/pkg/api/api_dashboard.go index 208b25c7b1f..36448aa7ecb 100644 --- a/pkg/api/api_dashboard.go +++ b/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 { diff --git a/pkg/api/api_login.go b/pkg/api/api_login.go index fa493ba65b9..487f1d237b1 100644 --- a/pkg/api/api_login.go +++ b/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) - } -} diff --git a/pkg/api/api_models.go b/pkg/api/api_models.go index f53d8cd0f4b..22b5d5bb669 100644 --- a/pkg/api/api_models.go +++ b/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 { diff --git a/pkg/api/api_routing.go b/pkg/api/api_routing.go new file mode 100644 index 00000000000..5fb703a6e54 --- /dev/null +++ b/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) +} diff --git a/pkg/models/account.go b/pkg/models/account.go new file mode 100644 index 00000000000..73d9cb2280c --- /dev/null +++ b/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 +} diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 53bd41d6bf8..2a507564a45 100644 --- a/pkg/models/dashboards.go +++ b/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"` diff --git a/pkg/stores/rethinkdb.go b/pkg/stores/rethinkdb.go index abb974fdd29..d4f5ee7aea5 100644 --- a/pkg/stores/rethinkdb.go +++ b/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 } diff --git a/pkg/stores/rethinkdb_accounts.go b/pkg/stores/rethinkdb_accounts.go index e2a337146ae..4b034b58804 100644 --- a/pkg/stores/rethinkdb_accounts.go +++ b/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 } diff --git a/pkg/stores/rethinkdb_test.go b/pkg/stores/rethinkdb_test.go index e90abb785e8..db092ae8080 100644 --- a/pkg/stores/rethinkdb_test.go +++ b/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) }) diff --git a/pkg/stores/store.go b/pkg/stores/store.go index 8319e7ca86f..0bd4de8516f 100644 --- a/pkg/stores/store.go +++ b/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() } diff --git a/views/index.html b/views/index.html index 4fd005da2e0..69870fabea7 100644 --- a/views/index.html +++ b/views/index.html @@ -1,6 +1,5 @@ - - + @@ -8,6 +7,7 @@ Grafana + @@ -20,18 +20,22 @@ - -
+
-