diff --git a/pkg/api/account.go b/pkg/api/account.go new file mode 100644 index 00000000000..1972781ed52 --- /dev/null +++ b/pkg/api/account.go @@ -0,0 +1,123 @@ +package api + +import ( + "github.com/torkelo/grafana-pro/pkg/bus" + "github.com/torkelo/grafana-pro/pkg/middleware" + m "github.com/torkelo/grafana-pro/pkg/models" + "github.com/torkelo/grafana-pro/pkg/utils" +) + +func GetAccount(c *middleware.Context) { + query := m.GetAccountInfoQuery{Id: c.UserAccount.Id} + err := bus.Dispatch(&query) + + if err != nil { + c.JsonApiErr(500, "Failed to fetch collaboratos", err) + return + } + + c.JSON(200, query.Result) +} + +func AddCollaborator(c *middleware.Context) { + var cmd m.AddCollaboratorCommand + + if !c.JsonBody(&cmd) { + c.JsonApiErr(400, "Invalid request", nil) + return + } + + userQuery := m.GetAccountByLoginQuery{Login: cmd.Email} + err := bus.Dispatch(&userQuery) + if err != nil { + c.JsonApiErr(404, "Collaborator not found", nil) + return + } + + accountToAdd := userQuery.Result + + if accountToAdd.Id == c.UserAccount.Id { + c.JsonApiErr(400, "Cannot add yourself as collaborator", nil) + return + } + + cmd.AccountId = accountToAdd.Id + cmd.ForAccountId = c.UserAccount.Id + cmd.Role = m.ROLE_READ_WRITE + + err = bus.Dispatch(&cmd) + if err != nil { + c.JsonApiErr(500, "Could not add collaborator", err) + return + } + + c.JsonOK("Collaborator added") +} + +func GetOtherAccounts(c *middleware.Context) { + query := m.GetOtherAccountsQuery{AccountId: c.UserAccount.Id} + err := bus.Dispatch(&query) + + if err != nil { + c.JSON(500, utils.DynMap{"message": err.Error()}) + return + } + + result := append(query.Result, &m.OtherAccountDTO{ + Id: c.UserAccount.Id, + Role: "owner", + Email: c.UserAccount.Email, + }) + + for _, ac := range result { + if ac.Id == c.UserAccount.UsingAccountId { + ac.IsUsing = true + break + } + } + + c.JSON(200, result) +} + +func validateUsingAccount(accountId int64, otherId int64) bool { + if accountId == otherId { + return true + } + + query := m.GetOtherAccountsQuery{AccountId: accountId} + err := bus.Dispatch(&query) + if err != nil { + return false + } + + // validate that the account id in the list + valid := false + for _, other := range query.Result { + if other.Id == otherId { + valid = true + } + } + return valid +} + +func SetUsingAccount(c *middleware.Context) { + usingAccountId := c.ParamsInt64(":id") + + if !validateUsingAccount(c.UserAccount.Id, usingAccountId) { + c.JsonApiErr(401, "Not a valid account", nil) + return + } + + cmd := m.SetUsingAccountCommand{ + AccountId: c.UserAccount.Id, + UsingAccountId: usingAccountId, + } + + err := bus.Dispatch(&cmd) + if err != nil { + c.JsonApiErr(500, "Failed to update account", err) + return + } + + c.JsonOK("Active account changed") +} diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go new file mode 100644 index 00000000000..b206c183cd2 --- /dev/null +++ b/pkg/api/dashboard.go @@ -0,0 +1,73 @@ +package api + +import ( + "github.com/torkelo/grafana-pro/pkg/bus" + "github.com/torkelo/grafana-pro/pkg/middleware" + m "github.com/torkelo/grafana-pro/pkg/models" + "github.com/torkelo/grafana-pro/pkg/utils" +) + +func GetDashboard(c *middleware.Context) { + slug := c.Params(":slug") + + dash, err := m.GetDashboard(slug, c.GetAccountId()) + if err != nil { + c.JsonApiErr(404, "Dashboard not found", nil) + return + } + + dash.Data["id"] = dash.Id + + c.JSON(200, dash.Data) +} + +func DeleteDashboard(c *middleware.Context) { + slug := c.Params(":slug") + + dash, err := m.GetDashboard(slug, c.GetAccountId()) + if err != nil { + c.JsonApiErr(404, "Dashboard not found", nil) + return + } + + err = m.DeleteDashboard(slug, c.GetAccountId()) + if err != nil { + c.JsonApiErr(500, "Failed to delete dashboard", err) + return + } + + var resp = map[string]interface{}{"title": dash.Title} + + c.JSON(200, resp) +} + +func Search(c *middleware.Context) { + query := c.Query("q") + + results, err := m.SearchQuery(query, c.GetAccountId()) + if err != nil { + c.JsonApiErr(500, "Search failed", err) + return + } + + c.JSON(200, results) +} + +func PostDashboard(c *middleware.Context) { + var cmd m.SaveDashboardCommand + + if !c.JsonBody(&cmd) { + c.JsonApiErr(400, "bad request", nil) + return + } + + cmd.AccountId = c.GetAccountId() + + err := bus.Dispatch(&cmd) + if err != nil { + c.JsonApiErr(500, "Failed to save dashboard", err) + return + } + + c.JSON(200, utils.DynMap{"status": "success", "slug": cmd.Result.Slug}) +} diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go new file mode 100644 index 00000000000..3d0c0a53c54 --- /dev/null +++ b/pkg/api/dataproxy.go @@ -0,0 +1,53 @@ +package api + +import ( + "net/http" + "net/http/httputil" + "net/url" + + "github.com/torkelo/grafana-pro/pkg/bus" + "github.com/torkelo/grafana-pro/pkg/middleware" + m "github.com/torkelo/grafana-pro/pkg/models" + "github.com/torkelo/grafana-pro/pkg/utils" +) + +func NewReverseProxy(ds *m.DataSource, proxyPath string) *httputil.ReverseProxy { + target, _ := url.Parse(ds.Url) + + director := func(req *http.Request) { + req.URL.Scheme = target.Scheme + req.URL.Host = target.Host + + reqQueryVals := req.URL.Query() + + if ds.Type == m.DS_INFLUXDB { + req.URL.Path = utils.JoinUrlFragments(target.Path, "db/"+ds.Database+"/"+proxyPath) + reqQueryVals.Add("u", ds.User) + reqQueryVals.Add("p", ds.Password) + req.URL.RawQuery = reqQueryVals.Encode() + } else { + req.URL.Path = utils.JoinUrlFragments(target.Path, proxyPath) + } + } + + return &httputil.ReverseProxy{Director: director} +} + +// TODO: need to cache datasources +func ProxyDataSourceRequest(c *middleware.Context) { + id := c.ParamsInt64(":id") + + query := m.GetDataSourceByIdQuery{ + Id: id, + AccountId: c.GetAccountId(), + } + + err := bus.Dispatch(&query) + if err != nil { + c.JsonApiErr(500, "Unable to load datasource meta data", err) + } + + proxyPath := c.Params("*") + proxy := NewReverseProxy(&query.Result, proxyPath) + proxy.ServeHTTP(c.RW(), c.Req.Request) +} diff --git a/pkg/api/dataproxy_test.go b/pkg/api/dataproxy_test.go new file mode 100644 index 00000000000..cd226b206eb --- /dev/null +++ b/pkg/api/dataproxy_test.go @@ -0,0 +1,58 @@ +package api + +import ( + "net/http" + "net/url" + "testing" + + . "github.com/smartystreets/goconvey/convey" + + m "github.com/torkelo/grafana-pro/pkg/models" +) + +func TestAccountDataAccess(t *testing.T) { + + Convey("When getting graphite datasource proxy", t, func() { + ds := m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE} + proxy := NewReverseProxy(&ds, "/render") + + requestUrl, _ := url.Parse("http://grafana.com/sub") + req := http.Request{URL: requestUrl} + + proxy.Director(&req) + + Convey("Can translate request url and path", func() { + So(req.URL.Host, ShouldEqual, "graphite:8080") + So(req.URL.Path, ShouldEqual, "/render") + }) + }) + + Convey("When getting influxdb datasource proxy", t, func() { + ds := m.DataSource{ + Type: m.DS_INFLUXDB, + Url: "http://influxdb:8083", + Database: "site", + User: "user", + Password: "password", + } + + proxy := NewReverseProxy(&ds, "") + + requestUrl, _ := url.Parse("http://grafana.com/sub") + req := http.Request{URL: requestUrl} + + proxy.Director(&req) + + Convey("Should add db to url", func() { + So(req.URL.Path, ShouldEqual, "/db/site/") + }) + + Convey("Should add username and password", func() { + queryVals := req.URL.Query() + So(queryVals["u"][0], ShouldEqual, "user") + So(queryVals["p"][0], ShouldEqual, "password") + }) + + }) + +} diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go new file mode 100644 index 00000000000..b78aa5320eb --- /dev/null +++ b/pkg/api/datasources.go @@ -0,0 +1,93 @@ +package api + +import ( + "github.com/torkelo/grafana-pro/pkg/api/dtos" + "github.com/torkelo/grafana-pro/pkg/bus" + "github.com/torkelo/grafana-pro/pkg/middleware" + m "github.com/torkelo/grafana-pro/pkg/models" +) + +func GetDataSources(c *middleware.Context) { + query := m.GetDataSourcesQuery{AccountId: c.Account.Id} + err := bus.Dispatch(&query) + + if err != nil { + c.JsonApiErr(500, "Failed to query datasources", err) + return + } + + result := make([]*dtos.DataSource, len(query.Result)) + for i, ds := range query.Result { + result[i] = &dtos.DataSource{ + Id: ds.Id, + AccountId: ds.AccountId, + Name: ds.Name, + Url: ds.Url, + Type: ds.Type, + Access: ds.Access, + Password: ds.Password, + Database: ds.Database, + User: ds.User, + BasicAuth: ds.BasicAuth, + } + } + + c.JSON(200, result) +} + +func DeleteDataSource(c *middleware.Context) { + id := c.ParamsInt64(":id") + + if id <= 0 { + c.JsonApiErr(400, "Missing valid datasource id", nil) + return + } + + cmd := &m.DeleteDataSourceCommand{Id: id, AccountId: c.UserAccount.Id} + + err := bus.Dispatch(cmd) + if err != nil { + c.JsonApiErr(500, "Failed to delete datasource", err) + return + } + + c.JsonOK("Data source deleted") +} + +func AddDataSource(c *middleware.Context) { + cmd := m.AddDataSourceCommand{} + + if !c.JsonBody(&cmd) { + c.JsonApiErr(400, "Validation failed", nil) + return + } + + cmd.AccountId = c.Account.Id + + err := bus.Dispatch(&cmd) + if err != nil { + c.JsonApiErr(500, "Failed to add datasource", err) + return + } + + c.JsonOK("Datasource added") +} + +func UpdateDataSource(c *middleware.Context) { + cmd := m.UpdateDataSourceCommand{} + + if !c.JsonBody(&cmd) { + c.JsonApiErr(400, "Validation failed", nil) + return + } + + cmd.AccountId = c.Account.Id + + err := bus.Dispatch(&cmd) + if err != nil { + c.JsonApiErr(500, "Failed to update datasource", err) + return + } + + c.JsonOK("Datasource updated") +} diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go new file mode 100644 index 00000000000..b4854039685 --- /dev/null +++ b/pkg/api/frontendsettings.go @@ -0,0 +1,68 @@ +package api + +import ( + "strconv" + + "github.com/torkelo/grafana-pro/pkg/bus" + "github.com/torkelo/grafana-pro/pkg/middleware" + m "github.com/torkelo/grafana-pro/pkg/models" +) + +func getFrontendSettings(c *middleware.Context) (map[string]interface{}, error) { + accountDataSources := make([]*m.DataSource, 0) + + if c.Account != nil { + query := m.GetDataSourcesQuery{AccountId: c.Account.Id} + err := bus.Dispatch(&query) + + if err != nil { + return nil, err + } + + accountDataSources = query.Result + } + + datasources := make(map[string]interface{}) + + for i, ds := range accountDataSources { + url := ds.Url + + if ds.Access == m.DS_ACCESS_PROXY { + url = "/api/datasources/proxy/" + strconv.FormatInt(ds.Id, 10) + } + + var dsMap = map[string]interface{}{ + "type": ds.Type, + "url": url, + } + + if ds.Type == m.DS_INFLUXDB { + if ds.Access == m.DS_ACCESS_DIRECT { + dsMap["username"] = ds.User + dsMap["password"] = ds.Password + dsMap["url"] = url + "/db/" + ds.Database + } + } + + // temp hack, first is always default + // TODO: implement default ds account setting + if i == 0 { + dsMap["default"] = true + } + + datasources[ds.Name] = dsMap + } + + // add grafana backend data source + datasources["grafana"] = map[string]interface{}{ + "type": "grafana", + "url": "", + "grafanaDB": true, + } + + jsonObj := map[string]interface{}{ + "datasources": datasources, + } + + return jsonObj, nil +} diff --git a/pkg/api/login.go b/pkg/api/login.go new file mode 100644 index 00000000000..d1dcba14484 --- /dev/null +++ b/pkg/api/login.go @@ -0,0 +1,61 @@ +package api + +import ( + "github.com/torkelo/grafana-pro/pkg/api/dtos" + "github.com/torkelo/grafana-pro/pkg/bus" + "github.com/torkelo/grafana-pro/pkg/log" + "github.com/torkelo/grafana-pro/pkg/middleware" + m "github.com/torkelo/grafana-pro/pkg/models" + "github.com/torkelo/grafana-pro/pkg/utils" +) + +type loginJsonModel struct { + Email string `json:"email" binding:"required"` + Password string `json:"password" binding:"required"` + Remember bool `json:"remember"` +} + +func LoginPost(c *middleware.Context) { + var loginModel loginJsonModel + + if !c.JsonBody(&loginModel) { + c.JSON(400, utils.DynMap{"status": "bad request"}) + return + } + + userQuery := m.GetAccountByLoginQuery{Login: loginModel.Email} + err := bus.Dispatch(&userQuery) + + if err != nil { + c.JsonApiErr(401, "Invalid username or password", err) + return + } + + account := userQuery.Result + + if loginModel.Password != account.Password { + c.JsonApiErr(401, "Invalid username or password", err) + return + } + + loginUserWithAccount(account, c) + + var resp = &dtos.LoginResult{} + resp.Status = "Logged in" + resp.User.Login = account.Login + + c.JSON(200, resp) +} + +func loginUserWithAccount(account *m.Account, c *middleware.Context) { + if account == nil { + log.Error(3, "Account login with nil account") + } + + c.Session.Set("accountId", account.Id) +} + +func LogoutPost(c *middleware.Context) { + c.Session.Delete("accountId") + c.JSON(200, utils.DynMap{"status": "logged out"}) +} diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go new file mode 100644 index 00000000000..e12ff127326 --- /dev/null +++ b/pkg/api/login_oauth.go @@ -0,0 +1,78 @@ +package api + +import ( + "errors" + "fmt" + + "github.com/torkelo/grafana-pro/pkg/bus" + "github.com/torkelo/grafana-pro/pkg/log" + "github.com/torkelo/grafana-pro/pkg/middleware" + m "github.com/torkelo/grafana-pro/pkg/models" + "github.com/torkelo/grafana-pro/pkg/setting" + "github.com/torkelo/grafana-pro/pkg/social" +) + +func OAuthLogin(ctx *middleware.Context) { + if setting.OAuthService == nil { + ctx.Handle(404, "login.OAuthLogin(oauth service not enabled)", nil) + return + } + + name := ctx.Params(":name") + connect, ok := social.SocialMap[name] + if !ok { + ctx.Handle(404, "login.OAuthLogin(social login not enabled)", errors.New(name)) + return + } + + code := ctx.Query("code") + if code == "" { + ctx.Redirect(connect.AuthCodeURL("", "online", "auto")) + return + } + log.Info("code: %v", code) + + // handle call back + transport, err := connect.NewTransportFromCode(code) + if err != nil { + ctx.Handle(500, "login.OAuthLogin(NewTransportWithCode)", err) + return + } + + log.Trace("login.OAuthLogin(Got token)") + + userInfo, err := connect.UserInfo(transport) + if err != nil { + ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err) + return + } + + log.Info("login.OAuthLogin(social login): %s", userInfo) + + userQuery := m.GetAccountByLoginQuery{Login: userInfo.Email} + err = bus.Dispatch(&userQuery) + + // create account if missing + if err == m.ErrAccountNotFound { + cmd := &m.CreateAccountCommand{ + Login: userInfo.Email, + Email: userInfo.Email, + Name: userInfo.Name, + Company: userInfo.Company, + } + + if err = bus.Dispatch(&cmd); err != nil { + ctx.Handle(500, "Failed to create account", err) + return + } + + userQuery.Result = &cmd.Result + } else if err != nil { + ctx.Handle(500, "Unexpected error", err) + } + + // login + loginUserWithAccount(userQuery.Result, ctx) + + ctx.Redirect("/") +} diff --git a/pkg/api/register.go b/pkg/api/register.go new file mode 100644 index 00000000000..265a6038d73 --- /dev/null +++ b/pkg/api/register.go @@ -0,0 +1,26 @@ +package api + +import ( + "github.com/torkelo/grafana-pro/pkg/bus" + "github.com/torkelo/grafana-pro/pkg/middleware" + m "github.com/torkelo/grafana-pro/pkg/models" +) + +func CreateAccount(c *middleware.Context) { + var cmd m.CreateAccountCommand + + if !c.JsonBody(&cmd) { + c.JsonApiErr(400, "Validation error", nil) + return + } + + cmd.Login = cmd.Email + err := bus.Dispatch(&cmd) + + if err != nil { + c.JsonApiErr(500, "failed to create account", err) + return + } + + c.JsonOK("Account created") +} diff --git a/pkg/api/render.go b/pkg/api/render.go new file mode 100644 index 00000000000..833e1067268 --- /dev/null +++ b/pkg/api/render.go @@ -0,0 +1,32 @@ +package api + +import ( + "net/http" + "strconv" + + "github.com/torkelo/grafana-pro/pkg/components/renderer" + "github.com/torkelo/grafana-pro/pkg/middleware" + "github.com/torkelo/grafana-pro/pkg/utils" +) + +func RenderToPng(c *middleware.Context) { + accountId := c.GetAccountId() + queryReader := utils.NewUrlQueryReader(c.Req.URL) + queryParams := "?render&accountId=" + strconv.FormatInt(accountId, 10) + "&" + c.Req.URL.RawQuery + + renderOpts := &renderer.RenderOpts{ + Url: c.Params("*") + queryParams, + Width: queryReader.Get("width", "800"), + Height: queryReader.Get("height", "400"), + } + + renderOpts.Url = "http://localhost:3000/" + renderOpts.Url + + pngPath, err := renderer.RenderToPng(renderOpts) + if err != nil { + c.HTML(500, "error.html", nil) + } + + c.Resp.Header().Set("Content-Type", "image/png") + http.ServeFile(c.Resp, c.Req.Request, pngPath) +}