diff --git a/.bra.toml b/.bra.toml index 1fb047534e5..0b3ce160c2f 100644 --- a/.bra.toml +++ b/.bra.toml @@ -4,6 +4,7 @@ watch_all = true watch_dirs = [ "$WORKDIR/pkg", "$WORKDIR/views", + "$WORKDIR/conf", ] watch_exts = [".go", ".ini"] build_delay = 1500 diff --git a/.gitignore b/.gitignore index e753c73af89..e1ba17ed3d5 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,8 @@ config.js *.sublime-workspace *.swp .idea/ + +data/sessions +data/*.db +data/log +grafana-pro diff --git a/_vendor/grafana.coffee b/_vendor/grafana.coffee new file mode 100644 index 00000000000..2467fc0ccd6 --- /dev/null +++ b/_vendor/grafana.coffee @@ -0,0 +1,60 @@ +# Description: +# A way to interact with the Google Images API. +# +# Commands: +# hubot image me - The Original. Queries Google Images for and returns a random top result. +# hubot animate me - The same thing as `image me`, except adds a few parameters to try to return an animated GIF instead. +# hubot mustache me - Adds a mustache to the specified URL. +# hubot mustache me - Searches Google Images for the specified query and mustaches it. + +module.exports = (robot) -> + robot.hear /grafana (.*)/i, (msg) -> + sendUrl msg.match[1] + + robot.router.get '/hubot/test', (req, res) -> + sendUrl() + res.send 'OK ' + +imageMe = (msg, cb) -> + cb 'http://localhost:3000/render/dashboard/solo/grafana-play-home?from=now-1h&to=now&panelId=4&fullscreen' + +sendUrl = (params) -> + https = require 'https' + querystring = require 'querystring' + opts = params.split(' ') + dashboard = opts[0] + panelId = opts[1] + from = opts[2] + + imageUrl = "http://localhost:3000/render/dashboard/solo/#{dashboard}/?panelId=#{panelId}" + link = "http://localhost:3000/dashboard/db/#{dashboard}/?panelId=#{panelId}&fullscreen" + if from + imageUrl += "&from=#{from}" + link += "&from=#{from}" + + console.log 'imageUrl: ' + imageUrl + + hipchat = {} + hipchat.format = 'json' + hipchat.auth_token = process.env.HUBOT_HIPCHAT_TOKEN + console.log 'token: ' + hipchat.auth_token + + hipchat.room_id = '877465' + hipchat.message = "" + hipchat.from = "hubot" + hipchat.message_format = "html" + + params = querystring.stringify(hipchat) + + path = "/v1/rooms/message/?#{params}" + + data = '' + + https.get {host: 'api.hipchat.com', path: path}, (res) -> + res.on 'data', (chunk) -> + data += chunk.toString() + res.on 'end', () -> + json = JSON.parse(data) + console.log "Hipchat response ", data + + diff --git a/conf/grafana.ini b/conf/grafana.ini index 322fc720698..e38f094d39a 100644 --- a/conf/grafana.ini +++ b/conf/grafana.ini @@ -8,7 +8,63 @@ root_url = %(protocol)s://%(domain)s:%(http_port)s/ http_addr = http_port = 3000 ssh_port = 22 -route_log = true +router_logging = false + +[session] +; Either "memory", "file", default is "memory" +provider = file +; Provider config options +; memory: not have any config yet +; file: session file path, e.g. `data/sessions` +; redis: config like redis server addr, poolSize, password, e.g. `127.0.0.1:6379,100,gogs` +; mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table` +provider_config = data/sessions +; Session cookie name +cookie_name = grafana_pro_sess +; If you use session in https only, default is false +cookie_secure = false +; Enable set cookie, default is true +enable_set_cookie = true +; Session GC time interval, default is 86400 +gc_time_interval = 86400 +; Session life time, default is 86400 +session_life_time = 86400 +; session id hash func, Either "sha1", "sha256" or "md5" default is sha1 +session_id_hashfunc = sha1 +; Session hash key, default is use random string +session_id_hashkey = + +[oauth] +enabled = true + +[oauth.github] +enabled = true +client_id = de054205006b9baa2e17 +client_secret = 72b7ea52d9f1096fdf36cea95e95362a307e0322 +scopes = user:email +auth_url = https://github.com/login/oauth/authorize +token_url = https://github.com/login/oauth/access_token + +[oauth.google] +enabled = true +client_id = 106011922963-4pvl05e9urtrm8bbqr0vouosj3e8p8kb.apps.googleusercontent.com +client_secret = K2evIa4QhfbhhAm3SO72t2Zv +scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email +auth_url = https://accounts.google.com/o/oauth2/auth +token_url = https://accounts.google.com/o/oauth2/token + +[database] +; Either "mysql", "postgres" or "sqlite3", it's your choice +type = sqlite3 +host = 127.0.0.1:3306 +name = grafana +user = root +PASSWD = +; For "postgres" only, either "disable", "require" or "verify-full" +ssl_mode = disable +; For "sqlite3" only +path = data/grafana.db + [log] root_path = diff --git a/grafana b/grafana index a9d9939bdde..79beefe57c6 160000 --- a/grafana +++ b/grafana @@ -1 +1 @@ -Subproject commit a9d9939bdde4b0d76854c41a39fe1e27a40c003c +Subproject commit 79beefe57c608b3cd933c5b1f772c8707731a64c diff --git a/grafana-pro b/grafana-pro deleted file mode 100755 index e0bb72cb5d5..00000000000 Binary files a/grafana-pro and /dev/null differ diff --git a/pkg/api/api.go b/pkg/api/api.go deleted file mode 100644 index bdc44e0c94b..00000000000 --- a/pkg/api/api.go +++ /dev/null @@ -1,84 +0,0 @@ -package api - -import ( - "fmt" - "html/template" - - "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/log" - "github.com/torkelo/grafana-pro/pkg/models" - "github.com/torkelo/grafana-pro/pkg/setting" - "github.com/torkelo/grafana-pro/pkg/stores" -) - -type HttpServer struct { - port string - shutdown chan bool - store stores.Store - renderer *components.PhantomRenderer - router *gin.Engine - cfg *configuration.Cfg -} - -var sessionStore = sessions.NewCookieStore([]byte("something-very-secret")) - -func NewHttpServer(cfg *configuration.Cfg, store stores.Store) *HttpServer { - self := &HttpServer{} - self.cfg = cfg - self.port = cfg.Http.Port - self.store = store - self.renderer = &components.PhantomRenderer{ImagesDir: "data/png", PhantomDir: "_vendor/phantomjs"} - - return self -} - -func (self *HttpServer) ListenAndServe() { - defer func() { self.shutdown <- true }() - - gin.SetMode(gin.ReleaseMode) - self.router = gin.New() - self.router.Use(gin.Recovery(), apiLogger(), CacheHeadersMiddleware()) - - self.router.Static("/public", "./public") - self.router.Static("/app", "./public/app") - self.router.Static("/img", "./public/img") - - // register & parse templates - templates := template.New("templates") - templates.Delims("[[", "]]") - templates.ParseFiles("./views/index.html") - self.router.SetHTMLTemplate(templates) - - for _, fn := range routeHandlers { - fn(self) - } - - // 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) - - listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort) - log.Info("Listen: %v://%s%s", setting.Protocol, listenAddr, setting.AppSubUrl) - self.router.Run(listenAddr) -} - -func (self *HttpServer) index(c *gin.Context) { - viewModel := &IndexDto{} - userAccount, _ := c.Get("userAccount") - account, _ := userAccount.(*models.Account) - initCurrentUserDto(&viewModel.User, account) - - c.HTML(200, "index.html", viewModel) -} - -func CacheHeadersMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - c.Writer.Header().Add("Cache-Control", "max-age=0, public, must-revalidate, proxy-revalidate") - } -} diff --git a/pkg/api/api_account.go b/pkg/api/api_account.go deleted file mode 100644 index 28c72c3bb50..00000000000 --- a/pkg/api/api_account.go +++ /dev/null @@ -1,147 +0,0 @@ -package api - -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) - }) -} - -func (self *HttpServer) getAccount(c *gin.Context, auth *authContext) { - var account = auth.userAccount - - model := accountInfoDto{ - Name: account.Name, - Email: account.Email, - AccountName: account.AccountName, - } - - for _, collaborator := range account.Collaborators { - 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{"message": "Invalid request"}) - return - } - - collaborator, err := self.store.GetAccountByLogin(model.Email) - if err != nil { - c.JSON(404, gin.H{"message": "Collaborator not found"}) - return - } - - userAccount := auth.userAccount - - if collaborator.Id == userAccount.Id { - c.JSON(400, gin.H{"message": "Cannot add yourself as collaborator"}) - return - } - - err = userAccount.AddCollaborator(collaborator) - if err != nil { - c.JSON(400, gin.H{"message": err.Error()}) - return - } - - err = self.store.UpdateAccount(userAccount) - if err != nil { - c.JSON(500, gin.H{"message": err.Error()}) - return - } - - 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 deleted file mode 100644 index 69ed3872748..00000000000 --- a/pkg/api/api_auth.go +++ /dev/null @@ -1,70 +0,0 @@ -package api - -import ( - "errors" - "strconv" - - "github.com/torkelo/grafana-pro/pkg/models" - - "github.com/gin-gonic/gin" - "github.com/gorilla/sessions" -) - -type authContext struct { - account *models.Account - userAccount *models.Account -} - -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 authGetRequestAccountId(c *gin.Context, session *sessions.Session) (int, error) { - accountId := session.Values["accountId"] - - urlQuery := c.Request.URL.Query() - if len(urlQuery["render"]) > 0 { - accId, _ := strconv.Atoi(urlQuery["accountId"][0]) - session.Values["accountId"] = accId - accountId = accId - } - - if accountId == nil { - return -1, errors.New("Auth: session account id not found") - } - - return accountId.(int), nil -} - -func (self *HttpServer) auth() gin.HandlerFunc { - return func(c *gin.Context) { - session, _ := sessionStore.Get(c.Request, "grafana-session") - accountId, err := authGetRequestAccountId(c, session) - - if err != nil && c.Request.URL.Path != "/login" { - self.authDenied(c) - return - } - - account, err := self.store.GetAccount(accountId) - if err != nil { - self.authDenied(c) - return - } - - usingAccount, err := self.store.GetAccount(account.UsingAccountId) - 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 deleted file mode 100644 index 36448aa7ecb..00000000000 --- a/pkg/api/api_dashboard.go +++ /dev/null @@ -1,87 +0,0 @@ -package api - -import ( - log "github.com/alecthomas/log4go" - "github.com/gin-gonic/gin" - "github.com/torkelo/grafana-pro/pkg/models" -) - -func init() { - addRoutes(func(self *HttpServer) { - 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, 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 - } - - dash.Data["id"] = dash.Id - - c.JSON(200, dash.Data) -} - -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, auth.getAccountId()) - if err != nil { - log.Error("Store query error: %v", err) - c.JSON(500, newErrorResponse("Failed")) - return - } - - c.JSON(200, results) -} - -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 = auth.getAccountId() - dashboard.UpdateSlug() - - if dashboard.Data["id"] != nil { - dashboard.Id = dashboard.Data["id"].(string) - } - - err := self.store.SaveDashboard(dashboard) - if err == nil { - c.JSON(200, gin.H{"status": "success", "slug": dashboard.Slug}) - return - } - } - - c.JSON(500, gin.H{"error": "bad request"}) -} diff --git a/pkg/api/api_dtos.go b/pkg/api/api_dtos.go deleted file mode 100644 index ff6cc6670a9..00000000000 --- a/pkg/api/api_dtos.go +++ /dev/null @@ -1,29 +0,0 @@ -package api - -type accountInfoDto struct { - Email string `json:"email"` - Name string `json:"name"` - 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 otherAccountDto struct { - Id int `json:"id"` - Name string `json:"name"` - Role string `json:"role"` - IsUsing bool `json:"isUsing"` -} diff --git a/pkg/api/api_logger.go b/pkg/api/api_logger.go deleted file mode 100644 index f332889f1fc..00000000000 --- a/pkg/api/api_logger.go +++ /dev/null @@ -1,63 +0,0 @@ -package api - -import ( - "strings" - "time" - - "github.com/gin-gonic/gin" - - "github.com/torkelo/grafana-pro/pkg/log" -) - -var ( - green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) - white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) - yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109}) - red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) - reset = string([]byte{27, 91, 48, 109}) -) - -func ignoreLoggingRequest(code int, contentType string) bool { - return code == 304 || - strings.HasPrefix(contentType, "application/javascript") || - strings.HasPrefix(contentType, "text/") || - strings.HasPrefix(contentType, "application/x-font-woff") -} - -func apiLogger() gin.HandlerFunc { - return func(c *gin.Context) { - // Start timer - start := time.Now() - - // Process request - c.Next() - - code := c.Writer.Status() - contentType := c.Writer.Header().Get("Content-Type") - - // ignore logging some requests - if ignoreLoggingRequest(code, contentType) { - return - } - - // save the IP of the requester - requester := c.Request.Header.Get("X-Real-IP") - // if the requester-header is empty, check the forwarded-header - if len(requester) == 0 { - requester = c.Request.Header.Get("X-Forwarded-For") - } - // if the requester is still empty, use the hard-coded address from the socket - if len(requester) == 0 { - requester = c.Request.RemoteAddr - } - - end := time.Now() - latency := end.Sub(start) - log.Info("[http] %s %s %3d %12v %s", - c.Request.Method, c.Request.URL.Path, - code, - latency, - c.Errors.String(), - ) - } -} diff --git a/pkg/api/api_login.go b/pkg/api/api_login.go deleted file mode 100644 index 01fcfa95369..00000000000 --- a/pkg/api/api_login.go +++ /dev/null @@ -1,70 +0,0 @@ -package api - -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.POST("/login", self.loginPost) - self.router.POST("/logout", self.logoutPost) - }) -} - -type loginJsonModel struct { - Email string `json:"email" binding:"required"` - Password string `json:"password" binding:"required"` - Remember bool `json:"remember"` -} - -func (self *HttpServer) loginPost(c *gin.Context) { - var loginModel loginJsonModel - - if !c.EnsureBody(&loginModel) { - c.JSON(400, gin.H{"status": "bad request"}) - return - } - - account, err := self.store.GetAccountByLogin(loginModel.Email) - if err != nil { - c.JSON(400, gin.H{"status": err.Error()}) - return - } - - if loginModel.Password != account.Password { - c.JSON(401, gin.H{"status": "unauthorized"}) - return - } - - loginUserWithAccount(account, c) - - var resp = &LoginResultDto{} - resp.Status = "Logged in" - resp.User.Login = account.Login - - 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 - session.Save(c.Request, c.Writer) - - c.JSON(200, gin.H{"status": "logged out"}) -} diff --git a/pkg/api/api_models.go b/pkg/api/api_models.go deleted file mode 100644 index de2b8762d13..00000000000 --- a/pkg/api/api_models.go +++ /dev/null @@ -1,52 +0,0 @@ -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"` - Dashboard map[string]interface{} -} - -type errorResponse struct { - Message string `json:"message"` -} - -type IndexDto struct { - User CurrentUserDto -} - -type CurrentUserDto struct { - Login string `json:"login"` - Email string `json:"email"` - GravatarUrl string `json:"gravatarUrl"` -} - -type LoginResultDto struct { - Status string `json:"status"` - User CurrentUserDto `json:"user"` -} - -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)) -} diff --git a/pkg/api/api_oauth.go b/pkg/api/api_oauth.go deleted file mode 100644 index 778f64ec17c..00000000000 --- a/pkg/api/api_oauth.go +++ /dev/null @@ -1 +0,0 @@ -package api diff --git a/pkg/api/api_oauth_github.go b/pkg/api/api_oauth_github.go deleted file mode 100644 index 7e6a96babc8..00000000000 --- a/pkg/api/api_oauth_github.go +++ /dev/null @@ -1,112 +0,0 @@ -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 ( - githubOAuthConfig *oauth2.Config - githubRedirectUrl string = "http://localhost:3000/oauth2/github/callback" - githubAuthUrl string = "https://github.com/login/oauth/authorize" - githubTokenUrl string = "https://github.com/login/oauth/access_token" -) - -func init() { - addRoutes(func(self *HttpServer) { - if !self.cfg.Http.GithubOAuth.Enabled { - return - } - - self.router.GET("/oauth2/github", self.oauthGithub) - self.router.GET("/oauth2/github/callback", self.oauthGithubCallback) - - options := &oauth2.Options{ - ClientID: self.cfg.Http.GithubOAuth.ClientId, - ClientSecret: self.cfg.Http.GithubOAuth.ClientSecret, - RedirectURL: githubRedirectUrl, - Scopes: []string{"user:email"}, - } - - cfg, err := oauth2.NewConfig(options, githubAuthUrl, githubTokenUrl) - - if err != nil { - log.Error("Failed to init github auth %v", err) - } - - githubOAuthConfig = cfg - }) -} - -func (self *HttpServer) oauthGithub(c *gin.Context) { - url := githubOAuthConfig.AuthCodeURL("", "online", "auto") - c.Redirect(302, url) -} - -type githubUserInfoDto struct { - Login string `json:"login"` - Name string `json:"name"` - Email string `json:"email"` - Company string `json:"company"` -} - -func (self *HttpServer) oauthGithubCallback(c *gin.Context) { - code := c.Request.URL.Query()["code"][0] - log.Info("OAuth code: %v", code) - - transport, err := githubOAuthConfig.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://api.github.com/user") - if err != nil { - c.String(500, err.Error()) - return - } - - var userInfo githubUserInfoDto - 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.Login, - Email: userInfo.Email, - Name: userInfo.Name, - Company: userInfo.Company, - } - - 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, "/") -} diff --git a/pkg/api/api_oauth_google.go b/pkg/api/api_oauth_google.go deleted file mode 100644 index efa27e2d1f8..00000000000 --- a/pkg/api/api_oauth_google.go +++ /dev/null @@ -1,113 +0,0 @@ -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 ( - googleOAuthConfig *oauth2.Config - googleRedirectUrl string = "http://localhost:3000/oauth2/google/callback" - googleAuthUrl string = "https://accounts.google.com/o/oauth2/auth" - googleTokenUrl string = "https://accounts.google.com/o/oauth2/token" - googleScopeProfile string = "https://www.googleapis.com/auth/userinfo.profile" - googleScopeEmail string = "https://www.googleapis.com/auth/userinfo.email" -) - -func init() { - addRoutes(func(self *HttpServer) { - if !self.cfg.Http.GoogleOAuth.Enabled { - return - } - - self.router.GET("/oauth2/google", self.oauthGoogle) - self.router.GET("/oauth2/google/callback", self.oauthGoogleCallback) - - options := &oauth2.Options{ - ClientID: self.cfg.Http.GoogleOAuth.ClientId, - ClientSecret: self.cfg.Http.GoogleOAuth.ClientSecret, - RedirectURL: googleRedirectUrl, - Scopes: []string{googleScopeEmail, googleScopeProfile}, - } - - cfg, err := oauth2.NewConfig(options, googleAuthUrl, googleTokenUrl) - - if err != nil { - log.Error("Failed to init google auth %v", err) - } - - googleOAuthConfig = cfg - }) -} - -func (self *HttpServer) oauthGoogle(c *gin.Context) { - url := googleOAuthConfig.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) oauthGoogleCallback(c *gin.Context) { - code := c.Request.URL.Query()["code"][0] - log.Info("OAuth code: %v", code) - - transport, err := googleOAuthConfig.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, "/") -} diff --git a/pkg/api/api_register.go b/pkg/api/api_register.go deleted file mode 100644 index f735b14ecce..00000000000 --- a/pkg/api/api_register.go +++ /dev/null @@ -1,44 +0,0 @@ -package api - -import ( - log "github.com/alecthomas/log4go" - "github.com/gin-gonic/gin" - "github.com/torkelo/grafana-pro/pkg/models" -) - -func init() { - addRoutes(func(self *HttpServer) { - self.router.GET("/register/*_", self.index) - self.router.POST("/api/register/user", self.registerUserPost) - }) -} - -type registerAccountJsonModel struct { - Email string `json:"email" binding:"required"` - Password string `json:"password" binding:"required"` - Password2 bool `json:"remember2"` -} - -func (self *HttpServer) registerUserPost(c *gin.Context) { - var registerModel registerAccountJsonModel - - if !c.EnsureBody(®isterModel) { - c.JSON(400, gin.H{"status": "bad request"}) - return - } - - account := models.Account{ - Login: registerModel.Email, - Email: registerModel.Email, - Password: registerModel.Password, - } - - err := self.store.CreateAccount(&account) - if err != nil { - log.Error("Failed to create user account, email: %v, error: %v", registerModel.Email, err) - c.JSON(500, gin.H{"status": "failed to create account"}) - return - } - - c.JSON(200, gin.H{"status": "ok"}) -} diff --git a/pkg/api/api_render.go b/pkg/api/api_render.go deleted file mode 100644 index 21d38a2bf97..00000000000 --- a/pkg/api/api_render.go +++ /dev/null @@ -1,36 +0,0 @@ -package api - -import ( - "strconv" - - "github.com/gin-gonic/gin" - "github.com/torkelo/grafana-pro/pkg/components" - "github.com/torkelo/grafana-pro/pkg/utils" -) - -func init() { - addRoutes(func(self *HttpServer) { - self.addRoute("GET", "/render/*url", self.renderToPng) - }) -} - -func (self *HttpServer) renderToPng(c *gin.Context, auth *authContext) { - accountId := auth.getAccountId() - queryReader := utils.NewUrlQueryReader(c.Request.URL) - queryParams := "?render&accountId=" + strconv.Itoa(accountId) + "&" + c.Request.URL.RawQuery - - renderOpts := &components.RenderOpts{ - Url: c.Params.ByName("url") + queryParams, - Width: queryReader.Get("width", "800"), - Height: queryReader.Get("height", "400"), - } - - renderOpts.Url = "http://localhost:3000" + renderOpts.Url - - pngPath, err := self.renderer.RenderToPng(renderOpts) - if err != nil { - c.HTML(500, "error.html", nil) - } - - c.File(pngPath) -} diff --git a/pkg/api/api_routing.go b/pkg/api/api_routing.go deleted file mode 100644 index f15dffaff06..00000000000 --- a/pkg/api/api_routing.go +++ /dev/null @@ -1,36 +0,0 @@ -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.Account), - userAccount: c.MustGet("userAccount").(*models.Account), - } - 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/api/api_test.go b/pkg/api/api_test.go deleted file mode 100644 index 778f64ec17c..00000000000 --- a/pkg/api/api_test.go +++ /dev/null @@ -1 +0,0 @@ -package api diff --git a/pkg/cmd/web.go b/pkg/cmd/web.go index a5bfc902f81..a0d58d7ec71 100644 --- a/pkg/cmd/web.go +++ b/pkg/cmd/web.go @@ -1,13 +1,23 @@ +// Copyright 2014 Unknwon +// Copyright 2014 Torkel Ödegaard + package cmd import ( - "time" + "fmt" + "net/http" + "path" + "github.com/Unknwon/macaron" "github.com/codegangsta/cli" - "github.com/siddontang/go-log/log" - "github.com/torkelo/grafana-pro/pkg/configuration" - "github.com/torkelo/grafana-pro/pkg/server" + "github.com/macaron-contrib/session" + + "github.com/torkelo/grafana-pro/pkg/log" + "github.com/torkelo/grafana-pro/pkg/middleware" + "github.com/torkelo/grafana-pro/pkg/routes" "github.com/torkelo/grafana-pro/pkg/setting" + "github.com/torkelo/grafana-pro/pkg/social" + "github.com/torkelo/grafana-pro/pkg/stores/sqlstore" ) var CmdWeb = cli.Command{ @@ -18,23 +28,70 @@ var CmdWeb = cli.Command{ Flags: []cli.Flag{}, } +func newMacaron() *macaron.Macaron { + m := macaron.New() + m.Use(middleware.Logger()) + m.Use(macaron.Recovery()) + + mapStatic(m, "public", "public") + mapStatic(m, "public/app", "app") + mapStatic(m, "public/img", "img") + + m.Use(session.Sessioner(session.Options{ + Provider: setting.SessionProvider, + Config: *setting.SessionConfig, + })) + + m.Use(macaron.Renderer(macaron.RenderOptions{ + Directory: path.Join(setting.StaticRootPath, "views"), + IndentJSON: macaron.Env != macaron.PROD, + Delims: macaron.Delims{Left: "[[", Right: "]]"}, + })) + + m.Use(middleware.GetContextHandler()) + return m +} + +func mapStatic(m *macaron.Macaron, dir string, prefix string) { + m.Use(macaron.Static( + path.Join(setting.StaticRootPath, dir), + macaron.StaticOptions{ + SkipLogging: true, + Prefix: prefix, + }, + )) +} + func runWeb(*cli.Context) { + setting.NewConfigContext() + setting.InitServices() + sqlstore.Init() + social.NewOAuthService() + + // init database + sqlstore.LoadModelsConfig() + if err := sqlstore.NewEngine(); err != nil { + log.Fatal(4, "fail to initialize orm engine: %v", err) + } + log.Info("Starting Grafana-Pro v.1-alpha") - setting.NewConfigContext() + m := newMacaron() + routes.Register(m) - cfg := configuration.NewCfg(setting.HttpPort) - server, err := server.NewServer(cfg) - if err != nil { - time.Sleep(time.Second) - panic(err) + var err error + listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort) + log.Info("Listen: %v://%s%s", setting.Protocol, listenAddr, setting.AppSubUrl) + switch setting.Protocol { + case setting.HTTP: + err = http.ListenAndServe(listenAddr, m) + case setting.HTTPS: + err = http.ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, m) + default: + log.Fatal(4, "Invalid protocol: %s", setting.Protocol) } - err = server.ListenAndServe() if err != nil { - log.Error("ListenAndServe failed: ", err) + log.Fatal(4, "Fail to start server: %v", err) } - - time.Sleep(time.Millisecond * 2000) - } diff --git a/pkg/components/renderer/renderer.go b/pkg/components/renderer/renderer.go new file mode 100644 index 00000000000..6140af41216 --- /dev/null +++ b/pkg/components/renderer/renderer.go @@ -0,0 +1,69 @@ +package renderer + +import ( + "crypto/md5" + "encoding/hex" + "io" + "os" + "os/exec" + "path/filepath" + "time" + + "github.com/torkelo/grafana-pro/pkg/log" + "github.com/torkelo/grafana-pro/pkg/setting" +) + +type RenderOpts struct { + Url string + Width string + Height string +} + +func RenderToPng(params *RenderOpts) (string, error) { + log.Info("PhantomRenderer::renderToPng url %v", params.Url) + binPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, "phantomjs")) + scriptPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, "render.js")) + pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, getHash(params.Url))) + pngPath = pngPath + ".png" + + cmd := exec.Command(binPath, scriptPath, "url="+params.Url, "width="+params.Width, "height="+params.Height, "png="+pngPath) + stdout, err := cmd.StdoutPipe() + + if err != nil { + return "", err + } + stderr, err := cmd.StderrPipe() + if err != nil { + return "", err + } + + err = cmd.Start() + if err != nil { + return "", err + } + + go io.Copy(os.Stdout, stdout) + go io.Copy(os.Stdout, stderr) + + done := make(chan error) + go func() { + cmd.Wait() + close(done) + }() + + select { + case <-time.After(10 * time.Second): + if err := cmd.Process.Kill(); err != nil { + log.Error(4, "failed to kill: %v", err) + } + case <-done: + } + + return pngPath, nil +} + +func getHash(text string) string { + hasher := md5.New() + hasher.Write([]byte(text)) + return hex.EncodeToString(hasher.Sum(nil)) +} diff --git a/pkg/components/phantom_renderer_test.go b/pkg/components/renderer/renderer_test.go similarity index 75% rename from pkg/components/phantom_renderer_test.go rename to pkg/components/renderer/renderer_test.go index ffec267bebb..0798a88786f 100644 --- a/pkg/components/phantom_renderer_test.go +++ b/pkg/components/renderer/renderer_test.go @@ -1,4 +1,4 @@ -package components +package renderer import ( "io/ioutil" @@ -12,8 +12,7 @@ func TestPhantomRender(t *testing.T) { Convey("Can render url", t, func() { tempDir, _ := ioutil.TempDir("", "img") - renderer := &PhantomRenderer{ImagesDir: tempDir, PhantomDir: "../../_vendor/phantomjs/"} - png, err := renderer.RenderToPng("http://www.google.com") + png, err := RenderToPng("http://www.google.com") So(err, ShouldBeNil) So(exists(png), ShouldEqual, true) diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go new file mode 100644 index 00000000000..3c66baf3ce0 --- /dev/null +++ b/pkg/middleware/auth.go @@ -0,0 +1,58 @@ +package middleware + +import ( + "errors" + "strconv" + + "github.com/Unknwon/macaron" + "github.com/macaron-contrib/session" + + "github.com/torkelo/grafana-pro/pkg/models" +) + +func authGetRequestAccountId(c *Context, sess session.Store) (int64, error) { + accountId := sess.Get("accountId") + + urlQuery := c.Req.URL.Query() + if len(urlQuery["render"]) > 0 { + accId, _ := strconv.Atoi(urlQuery["accountId"][0]) + sess.Set("accountId", accId) + accountId = accId + } + + if accountId == nil { + return -1, errors.New("Auth: session account id not found") + } + + return accountId.(int64), nil +} + +func authDenied(c *Context) { + c.Redirect("/login") +} + +func Auth() macaron.Handler { + return func(c *Context, sess session.Store) { + accountId, err := authGetRequestAccountId(c, sess) + + if err != nil && c.Req.URL.Path != "/login" { + authDenied(c) + return + } + + account, err := models.GetAccount(accountId) + if err != nil { + authDenied(c) + return + } + + usingAccount, err := models.GetAccount(account.UsingAccountId) + if err != nil { + authDenied(c) + return + } + + c.UserAccount = account + c.Account = usingAccount + } +} diff --git a/pkg/middleware/logger.go b/pkg/middleware/logger.go new file mode 100644 index 00000000000..fce0f6b8349 --- /dev/null +++ b/pkg/middleware/logger.go @@ -0,0 +1,59 @@ +// Copyright 2013 Martini Authors +// Copyright 2014 Unknwon +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package middleware + +import ( + "fmt" + "net/http" + "runtime" + "time" + + "github.com/Unknwon/macaron" + "github.com/torkelo/grafana-pro/pkg/log" +) + +var isWindows bool + +func init() { + isWindows = runtime.GOOS == "windows" +} + +// Logger returns a middleware handler that logs the request as it goes in and the response as it goes out. +func Logger() macaron.Handler { + return func(res http.ResponseWriter, req *http.Request, c *macaron.Context) { + start := time.Now() + + rw := res.(macaron.ResponseWriter) + c.Next() + + content := fmt.Sprintf("Completed %s %v %s in %v", req.URL.Path, rw.Status(), http.StatusText(rw.Status()), time.Since(start)) + if !isWindows { + switch rw.Status() { + case 200: + content = fmt.Sprintf("\033[1;32m%s\033[0m", content) + return + case 304: + return + content = fmt.Sprintf("\033[1;33m%s\033[0m", content) + case 404: + content = fmt.Sprintf("\033[1;31m%s\033[0m", content) + case 500: + content = fmt.Sprintf("\033[1;36m%s\033[0m", content) + } + } + log.Info(content) + } +} diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go new file mode 100644 index 00000000000..c519a4a1b68 --- /dev/null +++ b/pkg/middleware/middleware.go @@ -0,0 +1,86 @@ +package middleware + +import ( + "encoding/json" + "strconv" + + "github.com/Unknwon/macaron" + "github.com/macaron-contrib/session" + + "github.com/torkelo/grafana-pro/pkg/log" + "github.com/torkelo/grafana-pro/pkg/models" +) + +type Context struct { + *macaron.Context + Session session.Store + + Account *models.Account + UserAccount *models.Account + + IsSigned bool +} + +func (c *Context) GetAccountId() int64 { + return c.Account.Id +} + +func GetContextHandler() macaron.Handler { + return func(c *macaron.Context, sess session.Store) { + ctx := &Context{ + Context: c, + Session: sess, + } + + c.Map(ctx) + } +} + +// Handle handles and logs error by given status. +func (ctx *Context) Handle(status int, title string, err error) { + if err != nil { + log.Error(4, "%s: %v", title, err) + if macaron.Env != macaron.PROD { + ctx.Data["ErrorMsg"] = err + } + } + + switch status { + case 404: + ctx.Data["Title"] = "Page Not Found" + case 500: + ctx.Data["Title"] = "Internal Server Error" + } + + ctx.HTML(status, strconv.Itoa(status)) +} + +func (ctx *Context) JsonApiErr(status int, message string, err error) { + resp := make(map[string]interface{}) + + if err != nil { + log.Error(4, "%s: %v", message, err) + if macaron.Env != macaron.PROD { + resp["error"] = err + } + } + + switch status { + case 404: + resp["message"] = "Not Found" + case 500: + resp["message"] = "Internal Server Error" + } + + if message != "" { + resp["message"] = message + } + + ctx.HTML(status, "index") +} + +func (ctx *Context) JsonBody(model interface{}) bool { + b, _ := ctx.Req.Body().Bytes() + err := json.Unmarshal(b, &model) + return err == nil +} diff --git a/pkg/models/account.go b/pkg/models/account.go index 7d198e72d28..6c8d87d422c 100644 --- a/pkg/models/account.go +++ b/pkg/models/account.go @@ -5,8 +5,23 @@ import ( "time" ) +var ( + CreateAccount func(acccount *Account) error + UpdateAccount func(acccount *Account) error + GetAccountByLogin func(emailOrName string) (*Account, error) + GetAccount func(accountId int64) (*Account, error) + GetOtherAccountsFor func(accountId int64) ([]*OtherAccount, error) + GetCollaboratorsForAccount func(accountId int64) ([]*CollaboratorInfo, error) + AddCollaborator func(collaborator *Collaborator) error +) + +// Typed errors +var ( + ErrAccountNotFound = errors.New("Account not found") +) + type CollaboratorLink struct { - AccountId int + AccountId int64 Role string Email string ModifiedOn time.Time @@ -14,26 +29,26 @@ type CollaboratorLink struct { } type OtherAccount struct { - Id int `gorethink:"id"` - Name string - Role string + Id int64 + Email string + Role string } type Account struct { - Id int `gorethink:"id"` - Version int - Login string - Email string - AccountName string + Id int64 + Login string `xorm:"UNIQUE NOT NULL"` + Email string `xorm:"UNIQUE NOT NULL"` + Name string `xorm:"UNIQUE NOT NULL"` + FullName string Password string - Name string + IsAdmin bool + Salt string `xorm:"VARCHAR(10)"` Company string NextDashboardId int - UsingAccountId int - Collaborators []CollaboratorLink - CreatedOn time.Time - ModifiedOn time.Time - LastLoginOn time.Time + UsingAccountId int64 + Collaborators []CollaboratorLink `xorm:"-"` + Created time.Time `xorm:"CREATED"` + Updated time.Time `xorm:"UPDATED"` } func (account *Account) AddCollaborator(newCollaborator *Account) error { @@ -54,7 +69,7 @@ func (account *Account) AddCollaborator(newCollaborator *Account) error { return nil } -func (account *Account) RemoveCollaborator(accountId int) { +func (account *Account) RemoveCollaborator(accountId int64) { list := account.Collaborators for i, collaborator := range list { if collaborator.AccountId == accountId { @@ -64,7 +79,7 @@ func (account *Account) RemoveCollaborator(accountId int) { } } -func (account *Account) HasCollaborator(accountId int) bool { +func (account *Account) HasCollaborator(accountId int64) bool { for _, collaborator := range account.Collaborators { if collaborator.AccountId == accountId { return true diff --git a/pkg/models/collaborator.go b/pkg/models/collaborator.go new file mode 100644 index 00000000000..c02dd0f8b17 --- /dev/null +++ b/pkg/models/collaborator.go @@ -0,0 +1,38 @@ +package models + +import ( + "time" +) + +const ( + ROLE_READ_WRITE = "ReadWrite" + ROLE_READ = "Read" +) + +type RoleType string + +type Collaborator struct { + Id int64 + AccountId int64 `xorm:"not null unique(uix_account_id_for_account_id)"` // The account that can use another account + Role RoleType `xorm:"not null"` // Permission type + ForAccountId int64 `xorm:"not null unique(uix_account_id_for_account_id)"` // The account being given access to + Created time.Time + Updated time.Time +} + +// read only projection +type CollaboratorInfo struct { + AccountId int64 + Role string + Email string +} + +func NewCollaborator(accountId int64, forAccountId int64, role RoleType) *Collaborator { + return &Collaborator{ + AccountId: accountId, + ForAccountId: forAccountId, + Role: role, + Created: time.Now(), + Updated: time.Now(), + } +} diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 2a507564a45..54d033afaa3 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -2,19 +2,32 @@ package models import ( "encoding/json" + "errors" "io" "regexp" "strings" "time" ) +var ( + GetDashboard func(slug string, accountId int64) (*Dashboard, error) + SaveDashboard func(dash *Dashboard) error + DeleteDashboard func(slug string, accountId int64) error + SearchQuery func(query string, acccountId int64) ([]*SearchResult, error) +) + +// Typed errors +var ( + ErrDashboardNotFound = errors.New("Account not found") +) + type Dashboard struct { - Id string `gorethink:"id,omitempty"` - Slug string - AccountId int - LastModifiedByUserId string - LastModifiedByDate time.Time - CreatedDate time.Time + Id int64 + Slug string `xorm:"index(IX_AccountIdSlug)"` + AccountId int64 `xorm:"index(IX_AccountIdSlug)"` + + Created time.Time `xorm:"CREATED"` + Updated time.Time `xorm:"UPDATED"` Title string Tags []string @@ -29,10 +42,7 @@ type SearchResult struct { func NewDashboard(title string) *Dashboard { dash := &Dashboard{} - dash.Id = "" - dash.LastModifiedByDate = time.Now() - dash.CreatedDate = time.Now() - dash.LastModifiedByUserId = "123" + dash.Id = 0 dash.Data = make(map[string]interface{}) dash.Data["title"] = title dash.Title = title diff --git a/pkg/models/models.go b/pkg/models/models.go new file mode 100644 index 00000000000..189e594576b --- /dev/null +++ b/pkg/models/models.go @@ -0,0 +1,9 @@ +package models + +type OAuthType int + +const ( + GITHUB OAuthType = iota + 1 + GOOGLE + TWITTER +) diff --git a/pkg/routes/api/api_account.go b/pkg/routes/api/api_account.go new file mode 100644 index 00000000000..8579fc0a4ce --- /dev/null +++ b/pkg/routes/api/api_account.go @@ -0,0 +1,115 @@ +package api + +import ( + "github.com/torkelo/grafana-pro/pkg/middleware" + "github.com/torkelo/grafana-pro/pkg/models" + "github.com/torkelo/grafana-pro/pkg/routes/dtos" + "github.com/torkelo/grafana-pro/pkg/utils" +) + +func GetAccount(c *middleware.Context) { + model := dtos.AccountInfo{ + Name: c.UserAccount.Name, + Email: c.UserAccount.Email, + } + + collaborators, err := models.GetCollaboratorsForAccount(c.UserAccount.Id) + if err != nil { + c.JsonApiErr(500, "Failed to fetch collaboratos", err) + return + } + + for _, collaborator := range collaborators { + model.Collaborators = append(model.Collaborators, &dtos.Collaborator{ + AccountId: collaborator.AccountId, + Role: collaborator.Role, + Email: collaborator.Email, + }) + } + + c.JSON(200, model) +} + +func AddCollaborator(c *middleware.Context) { + var model dtos.AddCollaboratorCommand + + if !c.JsonBody(&model) { + c.JSON(400, utils.DynMap{"message": "Invalid request"}) + return + } + + accountToAdd, err := models.GetAccountByLogin(model.Email) + if err != nil { + c.JSON(404, utils.DynMap{"message": "Collaborator not found"}) + return + } + + if accountToAdd.Id == c.UserAccount.Id { + c.JSON(400, utils.DynMap{"message": "Cannot add yourself as collaborator"}) + return + } + + var collaborator = models.NewCollaborator(accountToAdd.Id, c.UserAccount.Id, models.ROLE_READ_WRITE) + + err = models.AddCollaborator(collaborator) + if err != nil { + c.JSON(400, utils.DynMap{"message": err.Error()}) + return + } + + c.Status(204) +} + +func GetOtherAccounts(c *middleware.Context) { + + otherAccounts, err := models.GetOtherAccountsFor(c.UserAccount.Id) + if err != nil { + c.JSON(500, utils.DynMap{"message": err.Error()}) + return + } + + var result []*dtos.OtherAccount + result = append(result, &dtos.OtherAccount{ + Id: c.UserAccount.Id, + Role: "owner", + IsUsing: c.UserAccount.Id == c.UserAccount.UsingAccountId, + Name: c.UserAccount.Email, + }) + + for _, other := range otherAccounts { + result = append(result, &dtos.OtherAccount{ + Id: other.Id, + Role: other.Role, + Name: other.Email, + IsUsing: other.Id == c.UserAccount.UsingAccountId, + }) + } + + c.JSON(200, result) +} + +// func SetUsingAccount(c *middleware.Context) { +// 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/routes/api/api_dashboard.go b/pkg/routes/api/api_dashboard.go new file mode 100644 index 00000000000..8362c97ef5e --- /dev/null +++ b/pkg/routes/api/api_dashboard.go @@ -0,0 +1,91 @@ +package api + +import ( + "github.com/torkelo/grafana-pro/pkg/middleware" + "github.com/torkelo/grafana-pro/pkg/models" + "github.com/torkelo/grafana-pro/pkg/routes/dtos" + "github.com/torkelo/grafana-pro/pkg/utils" +) + +func GetDashboard(c *middleware.Context) { + slug := c.Params(":slug") + + dash, err := models.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 := models.GetDashboard(slug, c.GetAccountId()) + if err != nil { + c.JsonApiErr(404, "Dashboard not found", nil) + return + } + + err = models.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 := models.SearchQuery(query, c.GetAccountId()) + if err != nil { + c.JsonApiErr(500, "Search failed", err) + return + } + + c.JSON(200, results) +} + +func convertToStringArray(arr []interface{}) []string { + b := make([]string, len(arr)) + for i := range arr { + b[i] = arr[i].(string) + } + + return b +} + +func PostDashboard(c *middleware.Context) { + var command dtos.SaveDashboardCommand + + if !c.JsonBody(&command) { + c.JsonApiErr(400, "bad request", nil) + return + } + + dashboard := models.NewDashboard("test") + dashboard.Data = command.Dashboard + dashboard.Title = dashboard.Data["title"].(string) + dashboard.AccountId = c.GetAccountId() + dashboard.Tags = convertToStringArray(dashboard.Data["tags"].([]interface{})) + dashboard.UpdateSlug() + + if dashboard.Data["id"] != nil { + dashboard.Id = int64(dashboard.Data["id"].(float64)) + } + + err := models.SaveDashboard(dashboard) + if err != nil { + c.JsonApiErr(500, "Failed to save dashboard", err) + return + } + + c.JSON(200, utils.DynMap{"status": "success", "slug": dashboard.Slug}) +} diff --git a/pkg/routes/api/api_register.go b/pkg/routes/api/api_register.go new file mode 100644 index 00000000000..fa3f941d814 --- /dev/null +++ b/pkg/routes/api/api_register.go @@ -0,0 +1,38 @@ +package api + +import ( + "github.com/torkelo/grafana-pro/pkg/log" + "github.com/torkelo/grafana-pro/pkg/middleware" + "github.com/torkelo/grafana-pro/pkg/models" + "github.com/torkelo/grafana-pro/pkg/utils" +) + +type registerAccountJsonModel struct { + Email string `json:"email" binding:"required"` + Password string `json:"password" binding:"required"` + Password2 bool `json:"remember2"` +} + +func CreateAccount(c *middleware.Context) { + var registerModel registerAccountJsonModel + + if !c.JsonBody(®isterModel) { + c.JSON(400, utils.DynMap{"status": "bad request"}) + return + } + + account := models.Account{ + Login: registerModel.Email, + Email: registerModel.Email, + Password: registerModel.Password, + } + + err := models.CreateAccount(&account) + if err != nil { + log.Error(2, "Failed to create user account, email: %v, error: %v", registerModel.Email, err) + c.JSON(500, utils.DynMap{"status": "failed to create account"}) + return + } + + c.JSON(200, utils.DynMap{"status": "ok"}) +} diff --git a/pkg/routes/api/api_render.go b/pkg/routes/api/api_render.go new file mode 100644 index 00000000000..833e1067268 --- /dev/null +++ b/pkg/routes/api/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) +} diff --git a/pkg/routes/dtos/models.go b/pkg/routes/dtos/models.go new file mode 100644 index 00000000000..c60d5614db3 --- /dev/null +++ b/pkg/routes/dtos/models.go @@ -0,0 +1,65 @@ +package dtos + +import ( + "crypto/md5" + "fmt" + "strings" + + "github.com/torkelo/grafana-pro/pkg/models" +) + +type LoginResult struct { + Status string `json:"status"` + User CurrentUser `json:"user"` +} + +type CurrentUser struct { + Login string `json:"login"` + Email string `json:"email"` + GravatarUrl string `json:"gravatarUrl"` +} + +type AccountInfo struct { + Email string `json:"email"` + Name string `json:"name"` + Collaborators []*Collaborator `json:"collaborators"` +} + +type OtherAccount struct { + Id int64 `json:"id"` + Name string `json:"name"` + Role string `json:"role"` + IsUsing bool `json:"isUsing"` +} + +type Collaborator struct { + AccountId int64 `json:"accountId"` + Email string `json:"email"` + Role string `json:"role"` +} + +type AddCollaboratorCommand struct { + Email string `json:"email" binding:"required"` +} + +func NewCurrentUser(account *models.Account) *CurrentUser { + model := &CurrentUser{} + if account != nil { + model.Login = account.Login + model.Email = account.Email + model.GravatarUrl = getGravatarUrl(account.Email) + } + return model +} + +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)) +} + +type SaveDashboardCommand struct { + Id string `json:"id"` + Title string `json:"title"` + Dashboard map[string]interface{} `json:"dashboard"` +} diff --git a/pkg/routes/index.go b/pkg/routes/index.go new file mode 100644 index 00000000000..95530223583 --- /dev/null +++ b/pkg/routes/index.go @@ -0,0 +1,51 @@ +package routes + +import ( + "github.com/Unknwon/macaron" + "github.com/torkelo/grafana-pro/pkg/middleware" + "github.com/torkelo/grafana-pro/pkg/routes/api" + "github.com/torkelo/grafana-pro/pkg/routes/dtos" + "github.com/torkelo/grafana-pro/pkg/routes/login" +) + +func Register(m *macaron.Macaron) { + auth := middleware.Auth() + + // index + m.Get("/", auth, Index) + m.Post("/logout", login.LogoutPost) + m.Post("/login", login.LoginPost) + + // login + m.Get("/login", Index) + m.Get("/login/:name", login.OAuthLogin) + + // account + m.Get("/account/", auth, Index) + m.Get("/api/account/", auth, api.GetAccount) + m.Post("/api/account/collaborators/add", auth, api.AddCollaborator) + m.Get("/api/account/others", auth, api.GetOtherAccounts) + + // user register + m.Get("/register/*_", Index) + m.Post("/api/account", api.CreateAccount) + + // dashboards + m.Get("/dashboard/*", auth, Index) + m.Get("/api/dashboards/:slug", auth, api.GetDashboard) + m.Get("/api/search/", auth, api.Search) + m.Post("/api/dashboard/", auth, api.PostDashboard) + m.Delete("/api/dashboard/:slug", auth, api.DeleteDashboard) + + // rendering + m.Get("/render/*", auth, api.RenderToPng) +} + +func Index(ctx *middleware.Context) { + ctx.Data["User"] = dtos.NewCurrentUser(ctx.UserAccount) + ctx.HTML(200, "index") +} + +func NotFound(ctx *middleware.Context) { + ctx.Handle(404, "index", nil) +} diff --git a/pkg/routes/login/login.go b/pkg/routes/login/login.go new file mode 100644 index 00000000000..93c9c41322f --- /dev/null +++ b/pkg/routes/login/login.go @@ -0,0 +1,56 @@ +package login + +import ( + "github.com/torkelo/grafana-pro/pkg/log" + "github.com/torkelo/grafana-pro/pkg/middleware" + "github.com/torkelo/grafana-pro/pkg/models" + "github.com/torkelo/grafana-pro/pkg/routes/dtos" + "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 + } + + account, err := models.GetAccountByLogin(loginModel.Email) + if err != nil { + c.JSON(401, utils.DynMap{"status": "unauthorized"}) + return + } + + if loginModel.Password != account.Password { + c.JSON(401, utils.DynMap{"status": "unauthorized"}) + return + } + + loginUserWithAccount(account, c) + + var resp = &dtos.LoginResult{} + resp.Status = "Logged in" + resp.User.Login = account.Login + + c.JSON(200, resp) +} + +func loginUserWithAccount(account *models.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/routes/login/login_oauth.go b/pkg/routes/login/login_oauth.go new file mode 100644 index 00000000000..f2321154b4b --- /dev/null +++ b/pkg/routes/login/login_oauth.go @@ -0,0 +1,74 @@ +package login + +import ( + "errors" + "fmt" + + "github.com/torkelo/grafana-pro/pkg/log" + "github.com/torkelo/grafana-pro/pkg/middleware" + "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.NewTransportWithCode(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) + + account, err := models.GetAccountByLogin(userInfo.Email) + + // create account if missing + if err == models.ErrAccountNotFound { + account = &models.Account{ + Login: userInfo.Email, + Email: userInfo.Email, + Name: userInfo.Name, + Company: userInfo.Company, + } + + if err = models.CreateAccount(account); err != nil { + ctx.Handle(500, "Failed to create account", err) + return + } + } else if err != nil { + ctx.Handle(500, "Unexpected error", err) + } + + // login + loginUserWithAccount(account, ctx) + + ctx.Redirect("/") +} diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go deleted file mode 100644 index db9eb4de0e4..00000000000 --- a/pkg/routes/routes.go +++ /dev/null @@ -1,7 +0,0 @@ -package routes - -import "github.com/torkelo/grafana-pro/pkg/setting" - -func GlobalInit() { - setting.NewConfigContext() -} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index cfee65428b7..16184cfb9bc 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -1,3 +1,6 @@ +// Copyright 2014 Unknwon +// Copyright 2014 Torkel Ödegaard + package setting import ( @@ -11,6 +14,8 @@ import ( "github.com/Unknwon/com" "github.com/Unknwon/goconfig" + "github.com/macaron-contrib/session" + "github.com/torkelo/grafana-pro/pkg/log" ) @@ -39,7 +44,12 @@ var ( HttpAddr, HttpPort string SshPort int CertFile, KeyFile string - DisableRouterLog bool + RouterLogging bool + StaticRootPath string + + // Session settings. + SessionProvider string + SessionConfig *session.Config // Global setting objects. Cfg *goconfig.ConfigFile @@ -48,6 +58,10 @@ var ( ProdMode bool RunUser string IsWindows bool + + // PhantomJs Rendering + ImagesDir string + PhantomDir string ) func init() { @@ -127,4 +141,36 @@ func NewConfigContext() { if port != "" { HttpPort = port } + + StaticRootPath = Cfg.MustValue("server", "static_root_path", workDir) + RouterLogging = Cfg.MustBool("server", "router_logging", false) + + // PhantomJS rendering + ImagesDir = "data/png" + PhantomDir = "_vendor/phantomjs" + + LogRootPath = Cfg.MustValue("log", "root_path", path.Join(workDir, "/data/log")) +} + +func initSessionService() { + SessionProvider = Cfg.MustValueRange("session", "provider", "memory", []string{"memory", "file"}) + + SessionConfig = new(session.Config) + SessionConfig.ProviderConfig = strings.Trim(Cfg.MustValue("session", "provider_config"), "\" ") + SessionConfig.CookieName = Cfg.MustValue("session", "cookie_name", "grafana_pro_sess") + SessionConfig.CookiePath = AppSubUrl + SessionConfig.Secure = Cfg.MustBool("session", "cookie_secure") + SessionConfig.EnableSetCookie = Cfg.MustBool("session", "enable_set_cookie", true) + SessionConfig.Gclifetime = Cfg.MustInt64("session", "gc_interval_time", 86400) + SessionConfig.Maxlifetime = Cfg.MustInt64("session", "session_life_time", 86400) + + if SessionProvider == "file" { + os.MkdirAll(path.Dir(SessionConfig.ProviderConfig), os.ModePerm) + } + + log.Info("Session Service Enabled") +} + +func InitServices() { + initSessionService() } diff --git a/pkg/setting/setting_oauth.go b/pkg/setting/setting_oauth.go new file mode 100644 index 00000000000..43a022079b7 --- /dev/null +++ b/pkg/setting/setting_oauth.go @@ -0,0 +1,15 @@ +package setting + +type OAuthInfo struct { + ClientId, ClientSecret string + Scopes []string + AuthUrl, TokenUrl string + Enabled bool +} + +type OAuther struct { + GitHub, Google, Twitter bool + OAuthInfos map[string]*OAuthInfo +} + +var OAuthService *OAuther diff --git a/pkg/social/social.go b/pkg/social/social.go new file mode 100644 index 00000000000..87231278a74 --- /dev/null +++ b/pkg/social/social.go @@ -0,0 +1,163 @@ +package social + +import ( + "encoding/json" + "net/http" + "strconv" + "strings" + + "github.com/gogits/gogs/models" + "github.com/golang/oauth2" + "github.com/torkelo/grafana-pro/pkg/log" + "github.com/torkelo/grafana-pro/pkg/setting" +) + +type BasicUserInfo struct { + Identity string + Name string + Email string + Login string + Company string +} + +type SocialConnector interface { + Type() int + UserInfo(transport *oauth2.Transport) (*BasicUserInfo, error) + + AuthCodeURL(state, accessType, prompt string) string + NewTransportWithCode(code string) (*oauth2.Transport, error) +} + +var ( + SocialBaseUrl = "/login/" + SocialMap = make(map[string]SocialConnector) +) + +func NewOAuthService() { + if !setting.Cfg.MustBool("oauth", "enabled") { + return + } + + setting.OAuthService = &setting.OAuther{} + setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo) + + allOauthes := []string{"github", "google", "twitter"} + + // Load all OAuth config data. + for _, name := range allOauthes { + info := &setting.OAuthInfo{ + ClientId: setting.Cfg.MustValue("oauth."+name, "client_id"), + ClientSecret: setting.Cfg.MustValue("oauth."+name, "client_secret"), + Scopes: setting.Cfg.MustValueArray("oauth."+name, "scopes", " "), + AuthUrl: setting.Cfg.MustValue("oauth."+name, "auth_url"), + TokenUrl: setting.Cfg.MustValue("oauth."+name, "token_url"), + Enabled: setting.Cfg.MustBool("oauth."+name, "enabled"), + } + + if !info.Enabled { + continue + } + + opts := &oauth2.Options{ + ClientID: info.ClientId, + ClientSecret: info.ClientSecret, + RedirectURL: strings.TrimSuffix(setting.AppUrl, "/") + SocialBaseUrl + name, + Scopes: info.Scopes, + } + + setting.OAuthService.OAuthInfos[name] = info + config, err := oauth2.NewConfig(opts, info.AuthUrl, info.TokenUrl) + + if err != nil { + log.Error(3, "Failed to init oauth service", err) + continue + } + + // GitHub. + if name == "github" { + setting.OAuthService.GitHub = true + SocialMap["github"] = &SocialGithub{Config: config} + } + + // Google. + if name == "google" { + setting.OAuthService.Google = true + SocialMap["google"] = &SocialGoogle{Config: config} + } + } +} + +type SocialGithub struct { + *oauth2.Config +} + +func (s *SocialGithub) Type() int { + return int(models.GITHUB) +} + +func (s *SocialGithub) UserInfo(transport *oauth2.Transport) (*BasicUserInfo, error) { + var data struct { + Id int `json:"id"` + Name string `json:"login"` + Email string `json:"email"` + } + + var err error + client := http.Client{Transport: transport} + r, err := client.Get("https://api.github.com/user") + if err != nil { + return nil, err + } + + defer r.Body.Close() + + if err = json.NewDecoder(r.Body).Decode(&data); err != nil { + return nil, err + } + + return &BasicUserInfo{ + Identity: strconv.Itoa(data.Id), + Name: data.Name, + Email: data.Email, + }, nil +} + +// ________ .__ +// / _____/ ____ ____ ____ | | ____ +// / \ ___ / _ \ / _ \ / ___\| | _/ __ \ +// \ \_\ ( <_> | <_> ) /_/ > |_\ ___/ +// \______ /\____/ \____/\___ /|____/\___ > +// \/ /_____/ \/ + +type SocialGoogle struct { + *oauth2.Config +} + +func (s *SocialGoogle) Type() int { + return int(models.GOOGLE) +} + +func (s *SocialGoogle) UserInfo(transport *oauth2.Transport) (*BasicUserInfo, error) { + var data struct { + Id string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + } + var err error + + reqUrl := "https://www.googleapis.com/oauth2/v1/userinfo" + client := http.Client{Transport: transport} + r, err := client.Get(reqUrl) + if err != nil { + return nil, err + } + defer r.Body.Close() + if err = json.NewDecoder(r.Body).Decode(&data); err != nil { + return nil, err + } + return &BasicUserInfo{ + Identity: data.Id, + Name: data.Name, + Email: data.Email, + }, nil +} diff --git a/pkg/stores/rethink/rethink.go b/pkg/stores/rethink/rethink.go new file mode 100644 index 00000000000..0b22614e689 --- /dev/null +++ b/pkg/stores/rethink/rethink.go @@ -0,0 +1,202 @@ +package rethink + +import ( + "errors" + "time" + + r "github.com/dancannon/gorethink" + + "github.com/torkelo/grafana-pro/pkg/log" + "github.com/torkelo/grafana-pro/pkg/models" +) + +var ( + session *r.Session + dbName string = "grafana" +) + +func Init() { + log.Info("Initializing rethink storage") + + var err error + session, err = r.Connect(r.ConnectOpts{ + Address: "localhost:28015", + Database: dbName, + MaxIdle: 10, + IdleTimeout: time.Second * 10, + }) + + if err != nil { + log.Error(3, "Failed to connect to rethink database %v", err) + } + + createRethinkDBTablesAndIndices() + + models.GetAccount = GetAccount + models.GetAccountByLogin = GetAccountByLogin + + models.GetDashboard = GetDashboard + models.SearchQuery = SearchQuery + models.DeleteDashboard = DeleteDashboard + models.SaveDashboard = SaveDashboard +} + +func createRethinkDBTablesAndIndices() { + + r.DbCreate(dbName).Exec(session) + + // create tables + r.Db(dbName).TableCreate("dashboards").Exec(session) + r.Db(dbName).TableCreate("accounts").Exec(session) + r.Db(dbName).TableCreate("master").Exec(session) + + // create dashboard accountId + slug index + r.Db(dbName).Table("dashboards").IndexCreateFunc("AccountIdSlug", func(row r.Term) interface{} { + return []interface{}{row.Field("AccountId"), row.Field("Slug")} + }).Exec(session) + + r.Db(dbName).Table("dashboards").IndexCreate("AccountId").Exec(session) + r.Db(dbName).Table("accounts").IndexCreate("Login").Exec(session) + + // create account collaborator index + r.Db(dbName).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(3, "Failed to insert master ids row", err) + } +} + +func getNextAccountId() (int, error) { + resp, err := r.Table("master").Get("ids").Update(map[string]interface{}{ + "NextAccountId": r.Row.Field("NextAccountId").Add(1), + }, r.UpdateOpts{ReturnChanges: true}).RunWrite(session) + + if err != nil { + return 0, err + } + + change := resp.Changes[0] + + if change.NewValue == nil { + return 0, errors.New("Failed to get new value after incrementing account id") + } + + return int(change.NewValue.(map[string]interface{})["NextAccountId"].(float64)), nil +} + +func CreateAccount(account *models.Account) error { + accountId, err := getNextAccountId() + if err != nil { + return err + } + + account.Id = accountId + account.UsingAccountId = accountId + + resp, err := r.Table("accounts").Insert(account).RunWrite(session) + if err != nil { + return err + } + + if resp.Inserted == 0 { + return errors.New("Failed to insert acccount") + } + + return nil +} + +func GetAccountByLogin(emailOrName string) (*models.Account, error) { + resp, err := r.Table("accounts").GetAllByIndex("Login", emailOrName).Run(session) + + if err != nil { + return nil, err + } + + var account models.Account + err = resp.One(&account) + if err != nil { + return nil, models.ErrAccountNotFound + } + + return &account, nil +} + +func GetAccount(id int) (*models.Account, error) { + resp, err := r.Table("accounts").Get(id).Run(session) + + if err != nil { + return nil, err + } + + var account models.Account + err = resp.One(&account) + if err != nil { + return nil, errors.New("Not found") + } + + return &account, nil +} + +func UpdateAccount(account *models.Account) error { + resp, err := r.Table("accounts").Update(account).RunWrite(session) + if err != nil { + return err + } + + if resp.Replaced == 0 && resp.Unchanged == 0 { + return errors.New("Could not find account to update") + } + + return nil +} + +func 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{ReturnChanges: true}).RunWrite(session) + + if err != nil { + return 0, err + } + + 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(change.NewValue.(map[string]interface{})["NextDashboardId"].(float64)), nil +} + +func 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(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/rethink/rethink_dashboards.go similarity index 70% rename from pkg/stores/rethinkdb_dashboards.go rename to pkg/stores/rethink/rethink_dashboards.go index 838d5715403..c8d008b56dc 100644 --- a/pkg/stores/rethinkdb_dashboards.go +++ b/pkg/stores/rethink/rethink_dashboards.go @@ -1,15 +1,16 @@ -package stores +package rethink import ( "errors" - log "github.com/alecthomas/log4go" r "github.com/dancannon/gorethink" + + "github.com/torkelo/grafana-pro/pkg/log" "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) +func SaveDashboard(dash *models.Dashboard) error { + resp, err := r.Table("dashboards").Insert(dash, r.InsertOpts{Conflict: "update"}).RunWrite(session) if err != nil { return err } @@ -23,10 +24,10 @@ func (self *rethinkStore) SaveDashboard(dash *models.Dashboard) error { return nil } -func (self *rethinkStore) GetDashboard(slug string, accountId int) (*models.Dashboard, error) { +func GetDashboard(slug string, accountId int) (*models.Dashboard, error) { resp, err := r.Table("dashboards"). GetAllByIndex("AccountIdSlug", []interface{}{accountId, slug}). - Run(self.session) + Run(session) if err != nil { return nil, err @@ -41,10 +42,10 @@ func (self *rethinkStore) GetDashboard(slug string, accountId int) (*models.Dash return &dashboard, nil } -func (self *rethinkStore) DeleteDashboard(slug string, accountId int) error { +func DeleteDashboard(slug string, accountId int) error { resp, err := r.Table("dashboards"). GetAllByIndex("AccountIdSlug", []interface{}{accountId, slug}). - Delete().RunWrite(self.session) + Delete().RunWrite(session) if err != nil { return err @@ -57,10 +58,10 @@ func (self *rethinkStore) DeleteDashboard(slug string, accountId int) error { return nil } -func (self *rethinkStore) Query(query string, accountId int) ([]*models.SearchResult, error) { +func SearchQuery(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) + Filter(r.Row.Field("Title").Match(".*")).Run(session) if err != nil { return nil, err diff --git a/pkg/stores/rethinkdb.go b/pkg/stores/rethinkdb.go deleted file mode 100644 index eb9a016c2a9..00000000000 --- a/pkg/stores/rethinkdb.go +++ /dev/null @@ -1,45 +0,0 @@ -package stores - -import ( - "time" - - r "github.com/dancannon/gorethink" - - "github.com/torkelo/grafana-pro/pkg/log" -) - -type rethinkStore struct { - session *r.Session -} - -type RethinkCfg struct { - DatabaseName string -} - -type Account struct { - Id int `gorethink:"id"` - NextDashboardId int -} - -func NewRethinkStore(config *RethinkCfg) *rethinkStore { - log.Info("Initializing rethink storage") - - session, err := r.Connect(r.ConnectOpts{ - Address: "localhost:28015", - Database: config.DatabaseName, - MaxIdle: 10, - IdleTimeout: time.Second * 10, - }) - - if err != nil { - log.Error(3, "Failed to connect to rethink database %v", err) - } - - createRethinkDBTablesAndIndices(config, session) - - return &rethinkStore{ - session: session, - } -} - -func (self *rethinkStore) Close() {} diff --git a/pkg/stores/rethinkdb_accounts.go b/pkg/stores/rethinkdb_accounts.go deleted file mode 100644 index ec5161475eb..00000000000 --- a/pkg/stores/rethinkdb_accounts.go +++ /dev/null @@ -1,136 +0,0 @@ -package stores - -import ( - "errors" - - r "github.com/dancannon/gorethink" - "github.com/torkelo/grafana-pro/pkg/models" -) - -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{ReturnChanges: true}).RunWrite(self.session) - - if err != nil { - return 0, err - } - - change := resp.Changes[0] - - if change.NewValue == nil { - return 0, errors.New("Failed to get new value after incrementing account id") - } - - return int(change.NewValue.(map[string]interface{})["NextAccountId"].(float64)), nil -} - -func (self *rethinkStore) CreateAccount(account *models.Account) error { - accountId, err := self.getNextAccountId() - if err != nil { - return err - } - - account.Id = accountId - account.UsingAccountId = accountId - - resp, err := r.Table("accounts").Insert(account).RunWrite(self.session) - if err != nil { - return err - } - - if resp.Inserted == 0 { - return errors.New("Failed to insert acccount") - } - - return nil -} - -func (self *rethinkStore) GetAccountByLogin(emailOrName string) (*models.Account, error) { - resp, err := r.Table("accounts").GetAllByIndex("Login", emailOrName).Run(self.session) - - if err != nil { - return nil, err - } - - var account models.Account - err = resp.One(&account) - if err != nil { - return nil, ErrAccountNotFound - } - - return &account, nil -} - -func (self *rethinkStore) GetAccount(id int) (*models.Account, error) { - resp, err := r.Table("accounts").Get(id).Run(self.session) - - if err != nil { - return nil, err - } - - var account models.Account - err = resp.One(&account) - if err != nil { - return nil, errors.New("Not found") - } - - return &account, nil -} - -func (self *rethinkStore) UpdateAccount(account *models.Account) error { - resp, err := r.Table("accounts").Update(account).RunWrite(self.session) - if err != nil { - return err - } - - if resp.Replaced == 0 && resp.Unchanged == 0 { - return errors.New("Could not find account to update") - } - - return 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{ReturnChanges: true}).RunWrite(self.session) - - if err != nil { - return 0, err - } - - 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(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_setup.go b/pkg/stores/rethinkdb_setup.go deleted file mode 100644 index 0f16474eabb..00000000000 --- a/pkg/stores/rethinkdb_setup.go +++ /dev/null @@ -1,39 +0,0 @@ -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/rethinkdb_test.go b/pkg/stores/rethinkdb_test.go deleted file mode 100644 index 4de4dc48d8e..00000000000 --- a/pkg/stores/rethinkdb_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package stores - -import ( - "testing" - - "github.com/dancannon/gorethink" - . "github.com/smartystreets/goconvey/convey" - "github.com/torkelo/grafana-pro/pkg/models" -) - -func TestRethinkStore(t *testing.T) { - store := NewRethinkStore(&RethinkCfg{DatabaseName: "tests"}) - defer gorethink.DbDrop("tests").Exec(store.session) - - Convey("Insert dashboard", t, func() { - dashboard := models.NewDashboard("test") - dashboard.AccountId = 1 - - err := store.SaveDashboard(dashboard) - So(err, ShouldBeNil) - So(dashboard.Id, ShouldNotBeEmpty) - - read, err := store.GetDashboard("test", 1) - So(err, ShouldBeNil) - So(read, ShouldNotBeNil) - }) - - Convey("can get next account id", t, func() { - id, err := store.getNextAccountId() - So(err, ShouldBeNil) - So(id, ShouldNotEqual, 0) - - id2, err := store.getNextAccountId() - So(id2, ShouldEqual, id+1) - }) - - Convey("can create account", t, func() { - account := &models.Account{UserName: "torkelo", Email: "mupp", Login: "test@test.com"} - err := store.CreateAccount(account) - So(err, ShouldBeNil) - So(account.Id, ShouldNotEqual, 0) - - read, err := store.GetUserAccountLogin("test@test.com") - So(err, ShouldBeNil) - So(read.Id, ShouldEqual, account.DatabaseId) - }) - - Convey("can get next dashboard id", t, func() { - account := &models.Account{UserName: "torkelo", Email: "mupp"} - err := store.CreateAccount(account) - dashId, err := store.getNextDashboardNumber(account.Id) - So(err, ShouldBeNil) - So(dashId, ShouldEqual, 1) - }) - -} diff --git a/pkg/stores/sqlstore/sqlstore.go b/pkg/stores/sqlstore/sqlstore.go new file mode 100644 index 00000000000..e18db0f88c5 --- /dev/null +++ b/pkg/stores/sqlstore/sqlstore.go @@ -0,0 +1,116 @@ +package sqlstore + +import ( + "fmt" + "os" + "path" + "strings" + + "github.com/torkelo/grafana-pro/pkg/models" + "github.com/torkelo/grafana-pro/pkg/setting" + + "github.com/go-xorm/xorm" + _ "github.com/mattn/go-sqlite3" +) + +var ( + x *xorm.Engine + tables []interface{} + + HasEngine bool + + DbCfg struct { + Type, Host, Name, User, Pwd, Path, SslMode string + } + + UseSQLite3 bool +) + +func Init() { + tables = append(tables, new(models.Account), new(models.Dashboard), new(models.Collaborator)) + + models.CreateAccount = CreateAccount + models.GetAccount = GetAccount + models.GetAccountByLogin = GetAccountByLogin + models.GetOtherAccountsFor = GetOtherAccountsFor + models.GetDashboard = GetDashboard + models.SaveDashboard = SaveDashboard + models.SearchQuery = SearchQuery + models.DeleteDashboard = DeleteDashboard + models.GetCollaboratorsForAccount = GetCollaboratorsForAccount + models.AddCollaborator = AddCollaborator +} + +func LoadModelsConfig() { + DbCfg.Type = setting.Cfg.MustValue("database", "type") + if DbCfg.Type == "sqlite3" { + UseSQLite3 = true + } + DbCfg.Host = setting.Cfg.MustValue("database", "host") + DbCfg.Name = setting.Cfg.MustValue("database", "name") + DbCfg.User = setting.Cfg.MustValue("database", "user") + if len(DbCfg.Pwd) == 0 { + DbCfg.Pwd = setting.Cfg.MustValue("database", "passwd") + } + DbCfg.SslMode = setting.Cfg.MustValue("database", "ssl_mode") + DbCfg.Path = setting.Cfg.MustValue("database", "path", "data/grafana.db") +} + +func NewEngine() (err error) { + if err = SetEngine(); err != nil { + return err + } + if err = x.Sync2(tables...); err != nil { + return fmt.Errorf("sync database struct error: %v\n", err) + } + return nil +} + +func SetEngine() (err error) { + x, err = getEngine() + if err != nil { + return fmt.Errorf("models.init(fail to connect to database): %v", err) + } + + logPath := path.Join(setting.LogRootPath, "xorm.log") + os.MkdirAll(path.Dir(logPath), os.ModePerm) + + f, err := os.Create(logPath) + if err != nil { + return fmt.Errorf("models.init(fail to create xorm.log): %v", err) + } + x.Logger = xorm.NewSimpleLogger(f) + + x.ShowSQL = true + x.ShowInfo = true + x.ShowDebug = true + x.ShowErr = true + x.ShowWarn = true + return nil +} + +func getEngine() (*xorm.Engine, error) { + cnnstr := "" + switch DbCfg.Type { + case "mysql": + cnnstr = fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8", + DbCfg.User, DbCfg.Pwd, DbCfg.Host, DbCfg.Name) + case "postgres": + var host, port = "127.0.0.1", "5432" + fields := strings.Split(DbCfg.Host, ":") + if len(fields) > 0 && len(strings.TrimSpace(fields[0])) > 0 { + host = fields[0] + } + if len(fields) > 1 && len(strings.TrimSpace(fields[1])) > 0 { + port = fields[1] + } + cnnstr = fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s", + DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode) + case "sqlite3": + os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm) + cnnstr = "file:" + DbCfg.Path + "?cache=shared&mode=rwc" + default: + return nil, fmt.Errorf("Unknown database type: %s", DbCfg.Type) + } + return xorm.NewEngine(DbCfg.Type, cnnstr) +} diff --git a/pkg/stores/sqlstore/sqlstore_accounts.go b/pkg/stores/sqlstore/sqlstore_accounts.go new file mode 100644 index 00000000000..d7320455ce2 --- /dev/null +++ b/pkg/stores/sqlstore/sqlstore_accounts.go @@ -0,0 +1,99 @@ +package sqlstore + +import ( + "github.com/torkelo/grafana-pro/pkg/models" +) + +func CreateAccount(account *models.Account) error { + var err error + + sess := x.NewSession() + defer sess.Close() + + if err = sess.Begin(); err != nil { + return err + } + + if _, err = sess.Insert(account); err != nil { + sess.Rollback() + return err + } else if err = sess.Commit(); err != nil { + return err + } + + return nil +} + +func GetAccount(id int64) (*models.Account, error) { + var err error + + var account models.Account + has, err := x.Id(id).Get(&account) + + if err != nil { + return nil, err + } else if has == false { + return nil, models.ErrAccountNotFound + } + + if account.UsingAccountId == 0 { + account.UsingAccountId = account.Id + } + + return &account, nil +} + +func GetAccountByLogin(emailOrLogin string) (*models.Account, error) { + var err error + + account := &models.Account{Login: emailOrLogin} + has, err := x.Get(account) + + if err != nil { + return nil, err + } else if has == false { + return nil, models.ErrAccountNotFound + } + + return account, nil +} + +func GetCollaboratorsForAccount(accountId int64) ([]*models.CollaboratorInfo, error) { + collaborators := make([]*models.CollaboratorInfo, 0) + + sess := x.Table("Collaborator") + sess.Join("INNER", "Account", "Account.id=Collaborator.account_Id") + sess.Where("Collaborator.for_account_id=?", accountId) + err := sess.Find(&collaborators) + + return collaborators, err +} + +func AddCollaborator(collaborator *models.Collaborator) error { + var err error + + sess := x.NewSession() + defer sess.Close() + + if err = sess.Begin(); err != nil { + return err + } + + if _, err = sess.Insert(collaborator); err != nil { + sess.Rollback() + return err + } else if err = sess.Commit(); err != nil { + return err + } + + return nil +} + +func GetOtherAccountsFor(accountId int64) ([]*models.OtherAccount, error) { + collaborators := make([]*models.OtherAccount, 0) + sess := x.Table("Collaborator") + sess.Join("INNER", "Account", "Account.id=Collaborator.account_Id") + sess.Where("Collaborator.account_id=?", accountId) + err := sess.Find(&collaborators) + return collaborators, err +} diff --git a/pkg/stores/sqlstore/sqlstore_dashboards.go b/pkg/stores/sqlstore/sqlstore_dashboards.go new file mode 100644 index 00000000000..dc3c81565a8 --- /dev/null +++ b/pkg/stores/sqlstore/sqlstore_dashboards.go @@ -0,0 +1,64 @@ +package sqlstore + +import ( + "github.com/torkelo/grafana-pro/pkg/models" +) + +func SaveDashboard(dash *models.Dashboard) error { + var err error + + sess := x.NewSession() + defer sess.Close() + + if err = sess.Begin(); err != nil { + return err + } + + if dash.Id == 0 { + _, err = sess.Insert(dash) + } else { + _, err = sess.Update(dash) + } + + if err != nil { + sess.Rollback() + return err + } else if err = sess.Commit(); err != nil { + return err + } + + return nil +} + +func GetDashboard(slug string, accountId int64) (*models.Dashboard, error) { + + dashboard := models.Dashboard{Slug: slug, AccountId: accountId} + has, err := x.Get(&dashboard) + if err != nil { + return nil, err + } else if has == false { + return nil, models.ErrDashboardNotFound + } + + return &dashboard, nil +} + +func SearchQuery(query string, accountId int64) ([]*models.SearchResult, error) { + sess := x.Limit(100, 0).Where("account_id=?", accountId) + sess.Table("Dashboard") + + results := make([]*models.SearchResult, 0) + err := sess.Find(&results) + + return results, err +} + +func DeleteDashboard(slug string, accountId int64) error { + sess := x.NewSession() + defer sess.Close() + + rawSql := "DELETE FROM Dashboard WHERE account_id=? and slug=?" + _, err := sess.Exec(rawSql, accountId, slug) + + return err +} diff --git a/pkg/stores/store.go b/pkg/stores/store.go deleted file mode 100644 index a8ec5b512a2..00000000000 --- a/pkg/stores/store.go +++ /dev/null @@ -1,29 +0,0 @@ -package stores - -import ( - "errors" - - "github.com/torkelo/grafana-pro/pkg/models" -) - -type Store interface { - GetDashboard(slug string, accountId int) (*models.Dashboard, error) - SaveDashboard(dash *models.Dashboard) error - DeleteDashboard(slug string, accountId int) error - Query(query string, acccountId int) ([]*models.SearchResult, error) - CreateAccount(acccount *models.Account) error - UpdateAccount(acccount *models.Account) error - GetAccountByLogin(emailOrName string) (*models.Account, error) - GetAccount(accountId int) (*models.Account, error) - GetOtherAccountsFor(accountId int) ([]*models.OtherAccount, error) - Close() -} - -// Typed errors -var ( - ErrAccountNotFound = errors.New("Account not found") -) - -func New() Store { - return NewRethinkStore(&RethinkCfg{DatabaseName: "grafana"}) -} diff --git a/pkg/utils/json.go b/pkg/utils/json.go new file mode 100644 index 00000000000..0685209c8e0 --- /dev/null +++ b/pkg/utils/json.go @@ -0,0 +1,3 @@ +package utils + +type DynMap map[string]interface{} diff --git a/views/404.html b/views/404.html new file mode 100644 index 00000000000..7f975a35e3b --- /dev/null +++ b/views/404.html @@ -0,0 +1,18 @@ + + + + + + + + Grafana + + + + + + +

404

+ + +