Torkel Ödegaard 11 лет назад
Родитель
Сommit
a8f915f049
55 измененных файлов с 1829 добавлено и 1315 удалено
  1. 1 0
      .bra.toml
  2. 5 0
      .gitignore
  3. 60 0
      _vendor/grafana.coffee
  4. 57 1
      conf/grafana.ini
  5. 1 1
      grafana
  6. BIN
      grafana-pro
  7. 0 84
      pkg/api/api.go
  8. 0 147
      pkg/api/api_account.go
  9. 0 70
      pkg/api/api_auth.go
  10. 0 87
      pkg/api/api_dashboard.go
  11. 0 29
      pkg/api/api_dtos.go
  12. 0 63
      pkg/api/api_logger.go
  13. 0 70
      pkg/api/api_login.go
  14. 0 52
      pkg/api/api_models.go
  15. 0 1
      pkg/api/api_oauth.go
  16. 0 112
      pkg/api/api_oauth_github.go
  17. 0 113
      pkg/api/api_oauth_google.go
  18. 0 44
      pkg/api/api_register.go
  19. 0 36
      pkg/api/api_render.go
  20. 0 36
      pkg/api/api_routing.go
  21. 0 1
      pkg/api/api_test.go
  22. 72 15
      pkg/cmd/web.go
  23. 69 0
      pkg/components/renderer/renderer.go
  24. 2 3
      pkg/components/renderer/renderer_test.go
  25. 58 0
      pkg/middleware/auth.go
  26. 59 0
      pkg/middleware/logger.go
  27. 86 0
      pkg/middleware/middleware.go
  28. 32 17
      pkg/models/account.go
  29. 38 0
      pkg/models/collaborator.go
  30. 20 10
      pkg/models/dashboards.go
  31. 9 0
      pkg/models/models.go
  32. 115 0
      pkg/routes/api/api_account.go
  33. 91 0
      pkg/routes/api/api_dashboard.go
  34. 38 0
      pkg/routes/api/api_register.go
  35. 32 0
      pkg/routes/api/api_render.go
  36. 65 0
      pkg/routes/dtos/models.go
  37. 51 0
      pkg/routes/index.go
  38. 56 0
      pkg/routes/login/login.go
  39. 74 0
      pkg/routes/login/login_oauth.go
  40. 0 7
      pkg/routes/routes.go
  41. 47 1
      pkg/setting/setting.go
  42. 15 0
      pkg/setting/setting_oauth.go
  43. 163 0
      pkg/social/social.go
  44. 202 0
      pkg/stores/rethink/rethink.go
  45. 11 10
      pkg/stores/rethink/rethink_dashboards.go
  46. 0 45
      pkg/stores/rethinkdb.go
  47. 0 136
      pkg/stores/rethinkdb_accounts.go
  48. 0 39
      pkg/stores/rethinkdb_setup.go
  49. 0 56
      pkg/stores/rethinkdb_test.go
  50. 116 0
      pkg/stores/sqlstore/sqlstore.go
  51. 99 0
      pkg/stores/sqlstore/sqlstore_accounts.go
  52. 64 0
      pkg/stores/sqlstore/sqlstore_dashboards.go
  53. 0 29
      pkg/stores/store.go
  54. 3 0
      pkg/utils/json.go
  55. 18 0
      views/404.html

+ 1 - 0
.bra.toml

@@ -4,6 +4,7 @@ watch_all = true
 watch_dirs = [
 	"$WORKDIR/pkg",
 	"$WORKDIR/views",
+	"$WORKDIR/conf",
 ]
 watch_exts = [".go", ".ini"]
 build_delay = 1500

+ 5 - 0
.gitignore

@@ -13,3 +13,8 @@ config.js
 *.sublime-workspace
 *.swp
 .idea/
+
+data/sessions
+data/*.db
+data/log
+grafana-pro

+ 60 - 0
_vendor/grafana.coffee

@@ -0,0 +1,60 @@
+# Description:
+#   A way to interact with the Google Images API.
+#
+# Commands:
+#   hubot image me <query> - The Original. Queries Google Images for <query> and returns a random top result.
+#   hubot animate me <query> - The same thing as `image me`, except adds a few parameters to try to return an animated GIF instead.
+#   hubot mustache me <url> - Adds a mustache to the specified URL.
+#   hubot mustache me <query> - 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 = "<a href='#{link}'><img src='#{imageUrl}'></img></a>"
+  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
+
+

+ 57 - 1
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 =

+ 1 - 1
grafana

@@ -1 +1 @@
-Subproject commit a9d9939bdde4b0d76854c41a39fe1e27a40c003c
+Subproject commit 79beefe57c608b3cd933c5b1f772c8707731a64c


+ 0 - 84
pkg/api/api.go

@@ -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")
-	}
-}

+ 0 - 147
pkg/api/api_account.go

@@ -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)
-}

+ 0 - 70
pkg/api/api_auth.go

@@ -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)
-	}
-}

+ 0 - 87
pkg/api/api_dashboard.go

@@ -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"})
-}

+ 0 - 29
pkg/api/api_dtos.go

@@ -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"`
-}

+ 0 - 63
pkg/api/api_logger.go

@@ -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(),
-		)
-	}
-}

+ 0 - 70
pkg/api/api_login.go

@@ -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"})
-}

+ 0 - 52
pkg/api/api_models.go

@@ -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))
-}

+ 0 - 1
pkg/api/api_oauth.go

@@ -1 +0,0 @@
-package api

+ 0 - 112
pkg/api/api_oauth_github.go

@@ -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, "/")
-}

+ 0 - 113
pkg/api/api_oauth_google.go

@@ -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, "/")
-}

+ 0 - 44
pkg/api/api_register.go

@@ -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(&registerModel) {
-		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"})
-}

+ 0 - 36
pkg/api/api_render.go

@@ -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)
-}

+ 0 - 36
pkg/api/api_routing.go

@@ -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)
-}

+ 0 - 1
pkg/api/api_test.go

@@ -1 +0,0 @@
-package api

+ 72 - 15
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)
-
 }

+ 69 - 0
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))
+}

+ 2 - 3
pkg/components/phantom_renderer_test.go → 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)
 

+ 58 - 0
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
+	}
+}

+ 59 - 0
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)
+	}
+}

+ 86 - 0
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
+}

+ 32 - 17
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

+ 38 - 0
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(),
+	}
+}

+ 20 - 10
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

+ 9 - 0
pkg/models/models.go

@@ -0,0 +1,9 @@
+package models
+
+type OAuthType int
+
+const (
+	GITHUB OAuthType = iota + 1
+	GOOGLE
+	TWITTER
+)

+ 115 - 0
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)
+// }

+ 91 - 0
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})
+}

+ 38 - 0
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(&registerModel) {
+		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"})
+}

+ 32 - 0
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)
+}

+ 65 - 0
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"`
+}

+ 51 - 0
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)
+}

+ 56 - 0
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"})
+}

+ 74 - 0
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("/")
+}

+ 0 - 7
pkg/routes/routes.go

@@ -1,7 +0,0 @@
-package routes
-
-import "github.com/torkelo/grafana-pro/pkg/setting"
-
-func GlobalInit() {
-	setting.NewConfigContext()
-}

+ 47 - 1
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()
 }

+ 15 - 0
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

+ 163 - 0
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
+}

+ 202 - 0
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
+}

+ 11 - 10
pkg/stores/rethinkdb_dashboards.go → 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

+ 0 - 45
pkg/stores/rethinkdb.go

@@ -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() {}

+ 0 - 136
pkg/stores/rethinkdb_accounts.go

@@ -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
-}

+ 0 - 39
pkg/stores/rethinkdb_setup.go

@@ -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)
-	}
-
-}

+ 0 - 56
pkg/stores/rethinkdb_test.go

@@ -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)
-	})
-
-}

+ 116 - 0
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)
+}

+ 99 - 0
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
+}

+ 64 - 0
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
+}

+ 0 - 29
pkg/stores/store.go

@@ -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"})
-}

+ 3 - 0
pkg/utils/json.go

@@ -0,0 +1,3 @@
+package utils
+
+type DynMap map[string]interface{}

+ 18 - 0
views/404.html

@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="viewport" content="width=device-width">
+
+    <title>Grafana</title>
+    <link rel="stylesheet" href="/public/css/grafana.dark.min.css" title="Dark">
+    <link rel="icon" type="image/png" href="img/fav32.png">
+		<base href="/">
+  </head>
+
+  <body>
+		<h1>404</h1>
+	</body>
+
+</html>