From cd9306df45ac30ac6e4264d3ebcc76e3f59ef0b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 21 Sep 2014 15:01:59 +0200 Subject: [PATCH] Lots of progress on account management --- grafana | 2 +- pkg/api/api_account.go | 98 +++++++++++++++++++++++++++--- pkg/api/api_auth.go | 6 +- pkg/api/api_dtos.go | 11 ++++ pkg/api/api_login.go | 6 +- pkg/models/account.go | 33 +++++++++- pkg/stores/rethinkdb.go | 89 +-------------------------- pkg/stores/rethinkdb_accounts.go | 32 +++++++++- pkg/stores/rethinkdb_dashboards.go | 79 ++++++++++++++++++++++++ pkg/stores/rethinkdb_setup.go | 39 ++++++++++++ pkg/stores/store.go | 3 +- ̈́ | 29 +++++++++ 12 files changed, 317 insertions(+), 110 deletions(-) create mode 100644 pkg/stores/rethinkdb_dashboards.go create mode 100644 pkg/stores/rethinkdb_setup.go create mode 100644 ̈́ diff --git a/grafana b/grafana index 639a44d9962..e0dc530e943 160000 --- a/grafana +++ b/grafana @@ -1 +1 @@ -Subproject commit 639a44d99629ba38e67eb04df8c8c9622093de70 +Subproject commit e0dc530e943d52453aa06fc47596d3f6f0261f2c diff --git a/pkg/api/api_account.go b/pkg/api/api_account.go index d2efdf2730a..719fde3f531 100644 --- a/pkg/api/api_account.go +++ b/pkg/api/api_account.go @@ -1,11 +1,18 @@ package api -import "github.com/gin-gonic/gin" +import ( + "strconv" + + "github.com/gin-gonic/gin" +) func init() { addRoutes(func(self *HttpServer) { self.addRoute("POST", "/api/account/collaborators/add", self.addCollaborator) + self.addRoute("POST", "/api/account/collaborators/remove", self.removeCollaborator) self.addRoute("GET", "/api/account/", self.getAccount) + self.addRoute("GET", "/api/account/others", self.getOtherAccounts) + self.addRoute("POST", "/api/account/using/:id", self.setUsingAccount) }) } @@ -22,44 +29,119 @@ func (self *HttpServer) getAccount(c *gin.Context, auth *authContext) { model.Collaborators = append(model.Collaborators, &collaboratorInfoDto{ AccountId: collaborator.AccountId, Role: collaborator.Role, + Email: collaborator.Email, }) } c.JSON(200, model) } +func (self *HttpServer) getOtherAccounts(c *gin.Context, auth *authContext) { + var account = auth.userAccount + + otherAccounts, err := self.store.GetOtherAccountsFor(account.Id) + if err != nil { + c.JSON(500, gin.H{"message": err.Error()}) + return + } + + var result []*otherAccountDto + result = append(result, &otherAccountDto{ + Id: account.Id, + Role: "owner", + IsUsing: account.Id == account.UsingAccountId, + Name: account.Email, + }) + + for _, other := range otherAccounts { + result = append(result, &otherAccountDto{ + Id: other.Id, + Role: other.Role, + Name: other.Name, + IsUsing: other.Id == account.UsingAccountId, + }) + } + + c.JSON(200, result) +} + 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"}) + c.JSON(400, gin.H{"message": "Invalid request"}) return } collaborator, err := self.store.GetAccountByLogin(model.Email) if err != nil { - c.JSON(404, gin.H{"status": "Collaborator not found"}) + c.JSON(404, gin.H{"message": "Collaborator not found"}) return } userAccount := auth.userAccount if collaborator.Id == userAccount.Id { - c.JSON(400, gin.H{"status": "Cannot add yourself as collaborator"}) + c.JSON(400, gin.H{"message": "Cannot add yourself as collaborator"}) return } - err = userAccount.AddCollaborator(collaborator.Id) + err = userAccount.AddCollaborator(collaborator) if err != nil { - c.JSON(400, gin.H{"status": err.Error()}) + c.JSON(400, gin.H{"message": err.Error()}) return } err = self.store.UpdateAccount(userAccount) if err != nil { - c.JSON(500, gin.H{"status": err.Error()}) + c.JSON(500, gin.H{"message": err.Error()}) return } - c.JSON(200, gin.H{"status": "Collaborator added"}) + c.Abort(204) +} + +func (self *HttpServer) removeCollaborator(c *gin.Context, auth *authContext) { + var model removeCollaboratorDto + if !c.EnsureBody(&model) { + c.JSON(400, gin.H{"message": "Invalid request"}) + return + } + + account := auth.userAccount + account.RemoveCollaborator(model.AccountId) + + err := self.store.UpdateAccount(account) + if err != nil { + c.JSON(500, gin.H{"message": err.Error()}) + return + } + + c.Abort(204) +} + +func (self *HttpServer) setUsingAccount(c *gin.Context, auth *authContext) { + idString := c.Params.ByName("id") + id, _ := strconv.Atoi(idString) + + account := auth.userAccount + otherAccount, err := self.store.GetAccount(id) + if err != nil { + c.JSON(500, gin.H{"message": err.Error()}) + return + } + + if otherAccount.Id != account.Id && !otherAccount.HasCollaborator(account.Id) { + c.Abort(401) + return + } + + account.UsingAccountId = otherAccount.Id + err = self.store.UpdateAccount(account) + if err != nil { + c.JSON(500, gin.H{"message": err.Error()}) + return + } + + c.Abort(204) } diff --git a/pkg/api/api_auth.go b/pkg/api/api_auth.go index 17c2934e1f6..8ff49d9ec43 100644 --- a/pkg/api/api_auth.go +++ b/pkg/api/api_auth.go @@ -23,18 +23,18 @@ 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 { + if c.Request.URL.Path != "/login" && session.Values["accountId"] == nil { self.authDenied(c) return } - account, err := self.store.GetAccount(session.Values["userAccountId"].(int)) + account, err := self.store.GetAccount(session.Values["accountId"].(int)) if err != nil { self.authDenied(c) return } - usingAccount, err := self.store.GetAccount(session.Values["usingAccountId"].(int)) + usingAccount, err := self.store.GetAccount(account.UsingAccountId) if err != nil { self.authDenied(c) return diff --git a/pkg/api/api_dtos.go b/pkg/api/api_dtos.go index 4644eb5f782..b6af2c58145 100644 --- a/pkg/api/api_dtos.go +++ b/pkg/api/api_dtos.go @@ -16,3 +16,14 @@ type collaboratorInfoDto struct { type addCollaboratorDto struct { Email string `json:"email" binding:"required"` } + +type removeCollaboratorDto struct { + AccountId int `json:"accountId" binding:"required"` +} + +type otherAccountDto struct { + Id int `json:"id"` + Name string `json:"name"` + Role string `json:"role"` + IsUsing bool `json:"isUsing"` +} diff --git a/pkg/api/api_login.go b/pkg/api/api_login.go index 36ddd012b69..97821b70c1e 100644 --- a/pkg/api/api_login.go +++ b/pkg/api/api_login.go @@ -26,7 +26,8 @@ func (self *HttpServer) loginPost(c *gin.Context) { account, err := self.store.GetAccountByLogin(loginModel.Email) if err != nil { - c.JSON(400, gin.H{"status": "some error"}) + c.JSON(400, gin.H{"status": err.Error()}) + return } if loginModel.Password != account.Password { @@ -35,8 +36,7 @@ func (self *HttpServer) loginPost(c *gin.Context) { } session, _ := sessionStore.Get(c.Request, "grafana-session") - session.Values["userAccountId"] = account.Id - session.Values["usingAccountId"] = account.UsingAccountId + session.Values["accountId"] = account.Id session.Save(c.Request, c.Writer) var resp = &LoginResultDto{} diff --git a/pkg/models/account.go b/pkg/models/account.go index ebcbb2515f0..8697a691302 100644 --- a/pkg/models/account.go +++ b/pkg/models/account.go @@ -8,10 +8,17 @@ import ( type CollaboratorLink struct { AccountId int Role string + Email string ModifiedOn time.Time CreatedOn time.Time } +type OtherAccount struct { + Id int `gorethink:"id"` + Name string + Role string +} + type Account struct { Id int `gorethink:"id"` Version int @@ -27,15 +34,16 @@ type Account struct { LastLoginOn time.Time } -func (account *Account) AddCollaborator(accountId int) error { +func (account *Account) AddCollaborator(newCollaborator *Account) error { for _, collaborator := range account.Collaborators { - if collaborator.AccountId == accountId { + if collaborator.AccountId == newCollaborator.Id { return errors.New("Collaborator already exists") } } account.Collaborators = append(account.Collaborators, CollaboratorLink{ - AccountId: accountId, + AccountId: newCollaborator.Id, + Email: newCollaborator.Email, Role: "admin", CreatedOn: time.Now(), ModifiedOn: time.Now(), @@ -43,3 +51,22 @@ func (account *Account) AddCollaborator(accountId int) error { return nil } + +func (account *Account) RemoveCollaborator(accountId int) { + list := account.Collaborators + for i, collaborator := range list { + if collaborator.AccountId == accountId { + account.Collaborators = append(list[:i], list[i+1:]...) + break + } + } +} + +func (account *Account) HasCollaborator(accountId int) bool { + for _, collaborator := range account.Collaborators { + if collaborator.AccountId == accountId { + return true + } + } + return false +} diff --git a/pkg/stores/rethinkdb.go b/pkg/stores/rethinkdb.go index d4f5ee7aea5..834597a742d 100644 --- a/pkg/stores/rethinkdb.go +++ b/pkg/stores/rethinkdb.go @@ -1,12 +1,10 @@ package stores import ( - "errors" "time" log "github.com/alecthomas/log4go" r "github.com/dancannon/gorethink" - "github.com/torkelo/grafana-pro/pkg/models" ) type rethinkStore struct { @@ -36,96 +34,11 @@ func NewRethinkStore(config *RethinkCfg) *rethinkStore { log.Crash("Failed to connect to rethink database %v", err) } - r.DbCreate(config.DatabaseName).Exec(session) - r.Db(config.DatabaseName).TableCreate("dashboards").Exec(session) - r.Db(config.DatabaseName).TableCreate("accounts").Exec(session) - r.Db(config.DatabaseName).TableCreate("master").Exec(session) - - r.Db(config.DatabaseName).Table("dashboards").IndexCreateFunc("AccountIdSlug", func(row r.Term) interface{} { - 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) - - _, err = r.Table("master").Insert(map[string]interface{}{"id": "ids", "NextAccountId": 0}).RunWrite(session) - if err != nil { - log.Error("Failed to insert master ids row", err) - } + createRethinkDBTablesAndIndices(config, session) return &rethinkStore{ session: session, } } -func (self *rethinkStore) SaveDashboard(dash *models.Dashboard) error { - resp, err := r.Table("dashboards").Insert(dash, r.InsertOpts{Conflict: "update"}).RunWrite(self.session) - if err != nil { - return err - } - - log.Info("Inserted: %v, Errors: %v, Updated: %v", resp.Inserted, resp.Errors, resp.Updated) - log.Info("First error:", resp.FirstError) - if len(resp.GeneratedKeys) > 0 { - dash.Id = resp.GeneratedKeys[0] - } - - return nil -} - -func (self *rethinkStore) GetDashboard(slug string, accountId int) (*models.Dashboard, error) { - resp, err := r.Table("dashboards").GetAllByIndex("AccountIdSlug", []interface{}{accountId, slug}).Run(self.session) - if err != nil { - return nil, err - } - - var dashboard models.Dashboard - err = resp.One(&dashboard) - if err != nil { - return nil, err - } - - return &dashboard, nil -} - -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) - - if err != nil { - return nil, err - } - - results := make([]*models.SearchResult, 0, 50) - var dashboard models.Dashboard - for docs.Next(&dashboard) { - results = append(results, &models.SearchResult{ - Title: dashboard.Title, - Id: dashboard.Slug, - }) - } - - return results, nil -} - func (self *rethinkStore) Close() {} diff --git a/pkg/stores/rethinkdb_accounts.go b/pkg/stores/rethinkdb_accounts.go index 85ea000ca32..2a5632fb4de 100644 --- a/pkg/stores/rethinkdb_accounts.go +++ b/pkg/stores/rethinkdb_accounts.go @@ -47,7 +47,7 @@ func (self *rethinkStore) CreateAccount(account *models.Account) error { } func (self *rethinkStore) GetAccountByLogin(emailOrName string) (*models.Account, error) { - resp, err := r.Table("accounts").GetAllByIndex("AccountLogin", []interface{}{emailOrName}).Run(self.session) + resp, err := r.Table("accounts").GetAllByIndex("Login", emailOrName).Run(self.session) if err != nil { return nil, err @@ -84,8 +84,8 @@ func (self *rethinkStore) UpdateAccount(account *models.Account) error { return err } - if resp.Replaced != 1 { - return errors.New("Could not fund account to uodate") + if resp.Replaced == 0 && resp.Unchanged == 0 { + return errors.New("Could not find account to update") } return nil @@ -108,3 +108,29 @@ func (self *rethinkStore) getNextDashboardNumber(accountId int) (int, error) { return int(change.NewValue.(map[string]interface{})["NextDashboardId"].(float64)), nil } + +func (self *rethinkStore) GetOtherAccountsFor(accountId int) ([]*models.OtherAccount, error) { + resp, err := r.Table("accounts"). + GetAllByIndex("CollaboratorAccountId", accountId). + Map(func(row r.Term) interface{} { + return map[string]interface{}{ + "id": row.Field("id"), + "Name": row.Field("Email"), + "Role": row.Field("Collaborators").Filter(map[string]interface{}{ + "AccountId": accountId, + }).Nth(0).Field("Role"), + } + }).Run(self.session) + + if err != nil { + return nil, err + } + + var list []*models.OtherAccount + err = resp.All(&list) + if err != nil { + return nil, errors.New("Failed to read available accounts") + } + + return list, nil +} diff --git a/pkg/stores/rethinkdb_dashboards.go b/pkg/stores/rethinkdb_dashboards.go new file mode 100644 index 00000000000..838d5715403 --- /dev/null +++ b/pkg/stores/rethinkdb_dashboards.go @@ -0,0 +1,79 @@ +package stores + +import ( + "errors" + + log "github.com/alecthomas/log4go" + r "github.com/dancannon/gorethink" + "github.com/torkelo/grafana-pro/pkg/models" +) + +func (self *rethinkStore) SaveDashboard(dash *models.Dashboard) error { + resp, err := r.Table("dashboards").Insert(dash, r.InsertOpts{Conflict: "update"}).RunWrite(self.session) + if err != nil { + return err + } + + log.Info("Inserted: %v, Errors: %v, Updated: %v", resp.Inserted, resp.Errors, resp.Updated) + log.Info("First error:", resp.FirstError) + if len(resp.GeneratedKeys) > 0 { + dash.Id = resp.GeneratedKeys[0] + } + + return nil +} + +func (self *rethinkStore) GetDashboard(slug string, accountId int) (*models.Dashboard, error) { + resp, err := r.Table("dashboards"). + GetAllByIndex("AccountIdSlug", []interface{}{accountId, slug}). + Run(self.session) + + if err != nil { + return nil, err + } + + var dashboard models.Dashboard + err = resp.One(&dashboard) + if err != nil { + return nil, err + } + + return &dashboard, nil +} + +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) + + if err != nil { + return nil, err + } + + results := make([]*models.SearchResult, 0, 50) + var dashboard models.Dashboard + for docs.Next(&dashboard) { + results = append(results, &models.SearchResult{ + Title: dashboard.Title, + Id: dashboard.Slug, + }) + } + + return results, nil +} diff --git a/pkg/stores/rethinkdb_setup.go b/pkg/stores/rethinkdb_setup.go new file mode 100644 index 00000000000..0f16474eabb --- /dev/null +++ b/pkg/stores/rethinkdb_setup.go @@ -0,0 +1,39 @@ +package stores + +import ( + log "github.com/alecthomas/log4go" + r "github.com/dancannon/gorethink" +) + +func createRethinkDBTablesAndIndices(config *RethinkCfg, session *r.Session) { + + r.DbCreate(config.DatabaseName).Exec(session) + + // create tables + r.Db(config.DatabaseName).TableCreate("dashboards").Exec(session) + r.Db(config.DatabaseName).TableCreate("accounts").Exec(session) + r.Db(config.DatabaseName).TableCreate("master").Exec(session) + + // create dashboard accountId + slug index + r.Db(config.DatabaseName).Table("dashboards").IndexCreateFunc("AccountIdSlug", func(row r.Term) interface{} { + return []interface{}{row.Field("AccountId"), row.Field("Slug")} + }).Exec(session) + + r.Db(config.DatabaseName).Table("dashboards").IndexCreate("AccountId").Exec(session) + r.Db(config.DatabaseName).Table("accounts").IndexCreate("Login").Exec(session) + + // create account collaborator index + r.Db(config.DatabaseName).Table("accounts"). + IndexCreateFunc("CollaboratorAccountId", func(row r.Term) interface{} { + return row.Field("Collaborators").Map(func(row r.Term) interface{} { + return row.Field("AccountId") + }) + }, r.IndexCreateOpts{Multi: true}).Exec(session) + + // make sure master ids row exists + _, err := r.Table("master").Insert(map[string]interface{}{"id": "ids", "NextAccountId": 0}).RunWrite(session) + if err != nil { + log.Error("Failed to insert master ids row", err) + } + +} diff --git a/pkg/stores/store.go b/pkg/stores/store.go index d7d66b77200..79e577d143e 100644 --- a/pkg/stores/store.go +++ b/pkg/stores/store.go @@ -12,7 +12,8 @@ type Store interface { CreateAccount(acccount *models.Account) error UpdateAccount(acccount *models.Account) error GetAccountByLogin(emailOrName string) (*models.Account, error) - GetAccount(id int) (*models.Account, error) + GetAccount(accountId int) (*models.Account, error) + GetOtherAccountsFor(accountId int) ([]*models.OtherAccount, error) Close() } diff --git a/̈́ b/̈́ new file mode 100644 index 00000000000..849fe9f3cc5 --- /dev/null +++ b/̈́ @@ -0,0 +1,29 @@ +package api + +type accountInfoDto struct { + Login string `json:"login"` + Email string `json:"email"` + AccountName string `json:"accountName"` + Collaborators []*collaboratorInfoDto `json:"collaborators"` +} + +type collaboratorInfoDto struct { + AccountId int `json:"accountId"` + Email string `json:"email"` + Role string `json:"role"` +} + +type addCollaboratorDto struct { + Email string `json:"email" binding:"required"` +} + +type removeCollaboratorDto struct { + AccountId int `json:"accountId" binding:"required"` +} + +type usingAccountDto struct { + AccountId int `json:"accountId"` + Email string `json:"email"` + Role string `json:"role"` + IsUsing bool +}