mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'macaron'
This commit is contained in:
commit
a8f915f049
@ -4,6 +4,7 @@ watch_all = true
|
|||||||
watch_dirs = [
|
watch_dirs = [
|
||||||
"$WORKDIR/pkg",
|
"$WORKDIR/pkg",
|
||||||
"$WORKDIR/views",
|
"$WORKDIR/views",
|
||||||
|
"$WORKDIR/conf",
|
||||||
]
|
]
|
||||||
watch_exts = [".go", ".ini"]
|
watch_exts = [".go", ".ini"]
|
||||||
build_delay = 1500
|
build_delay = 1500
|
||||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -13,3 +13,8 @@ config.js
|
|||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
*.swp
|
*.swp
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
data/sessions
|
||||||
|
data/*.db
|
||||||
|
data/log
|
||||||
|
grafana-pro
|
||||||
|
60
_vendor/grafana.coffee
Normal file
60
_vendor/grafana.coffee
Normal file
@ -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
|
||||||
|
|
||||||
|
|
@ -8,7 +8,63 @@ root_url = %(protocol)s://%(domain)s:%(http_port)s/
|
|||||||
http_addr =
|
http_addr =
|
||||||
http_port = 3000
|
http_port = 3000
|
||||||
ssh_port = 22
|
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]
|
[log]
|
||||||
root_path =
|
root_path =
|
||||||
|
2
grafana
2
grafana
@ -1 +1 @@
|
|||||||
Subproject commit a9d9939bdde4b0d76854c41a39fe1e27a40c003c
|
Subproject commit 79beefe57c608b3cd933c5b1f772c8707731a64c
|
BIN
grafana-pro
BIN
grafana-pro
Binary file not shown.
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"})
|
|
||||||
}
|
|
@ -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"`
|
|
||||||
}
|
|
@ -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(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"})
|
|
||||||
}
|
|
@ -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))
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
package api
|
|
@ -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, "/")
|
|
||||||
}
|
|
@ -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, "/")
|
|
||||||
}
|
|
@ -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"})
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
package api
|
|
@ -1,13 +1,23 @@
|
|||||||
|
// Copyright 2014 Unknwon
|
||||||
|
// Copyright 2014 Torkel Ödegaard
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/Unknwon/macaron"
|
||||||
"github.com/codegangsta/cli"
|
"github.com/codegangsta/cli"
|
||||||
"github.com/siddontang/go-log/log"
|
"github.com/macaron-contrib/session"
|
||||||
"github.com/torkelo/grafana-pro/pkg/configuration"
|
|
||||||
"github.com/torkelo/grafana-pro/pkg/server"
|
"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/setting"
|
||||||
|
"github.com/torkelo/grafana-pro/pkg/social"
|
||||||
|
"github.com/torkelo/grafana-pro/pkg/stores/sqlstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
var CmdWeb = cli.Command{
|
var CmdWeb = cli.Command{
|
||||||
@ -18,23 +28,70 @@ var CmdWeb = cli.Command{
|
|||||||
Flags: []cli.Flag{},
|
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) {
|
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")
|
log.Info("Starting Grafana-Pro v.1-alpha")
|
||||||
|
|
||||||
setting.NewConfigContext()
|
m := newMacaron()
|
||||||
|
routes.Register(m)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
cfg := configuration.NewCfg(setting.HttpPort)
|
|
||||||
server, err := server.NewServer(cfg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
time.Sleep(time.Second)
|
log.Fatal(4, "Fail to start server: %v", err)
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = server.ListenAndServe()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("ListenAndServe failed: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Millisecond * 2000)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
69
pkg/components/renderer/renderer.go
Normal file
69
pkg/components/renderer/renderer.go
Normal file
@ -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))
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package components
|
package renderer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -12,8 +12,7 @@ func TestPhantomRender(t *testing.T) {
|
|||||||
|
|
||||||
Convey("Can render url", t, func() {
|
Convey("Can render url", t, func() {
|
||||||
tempDir, _ := ioutil.TempDir("", "img")
|
tempDir, _ := ioutil.TempDir("", "img")
|
||||||
renderer := &PhantomRenderer{ImagesDir: tempDir, PhantomDir: "../../_vendor/phantomjs/"}
|
png, err := RenderToPng("http://www.google.com")
|
||||||
png, err := renderer.RenderToPng("http://www.google.com")
|
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(exists(png), ShouldEqual, true)
|
So(exists(png), ShouldEqual, true)
|
||||||
|
|
58
pkg/middleware/auth.go
Normal file
58
pkg/middleware/auth.go
Normal file
@ -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
pkg/middleware/logger.go
Normal file
59
pkg/middleware/logger.go
Normal file
@ -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
pkg/middleware/middleware.go
Normal file
86
pkg/middleware/middleware.go
Normal file
@ -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
|
||||||
|
}
|
@ -5,8 +5,23 @@ import (
|
|||||||
"time"
|
"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 {
|
type CollaboratorLink struct {
|
||||||
AccountId int
|
AccountId int64
|
||||||
Role string
|
Role string
|
||||||
Email string
|
Email string
|
||||||
ModifiedOn time.Time
|
ModifiedOn time.Time
|
||||||
@ -14,26 +29,26 @@ type CollaboratorLink struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OtherAccount struct {
|
type OtherAccount struct {
|
||||||
Id int `gorethink:"id"`
|
Id int64
|
||||||
Name string
|
Email string
|
||||||
Role string
|
Role string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
Id int `gorethink:"id"`
|
Id int64
|
||||||
Version int
|
Login string `xorm:"UNIQUE NOT NULL"`
|
||||||
Login string
|
Email string `xorm:"UNIQUE NOT NULL"`
|
||||||
Email string
|
Name string `xorm:"UNIQUE NOT NULL"`
|
||||||
AccountName string
|
FullName string
|
||||||
Password string
|
Password string
|
||||||
Name string
|
IsAdmin bool
|
||||||
|
Salt string `xorm:"VARCHAR(10)"`
|
||||||
Company string
|
Company string
|
||||||
NextDashboardId int
|
NextDashboardId int
|
||||||
UsingAccountId int
|
UsingAccountId int64
|
||||||
Collaborators []CollaboratorLink
|
Collaborators []CollaboratorLink `xorm:"-"`
|
||||||
CreatedOn time.Time
|
Created time.Time `xorm:"CREATED"`
|
||||||
ModifiedOn time.Time
|
Updated time.Time `xorm:"UPDATED"`
|
||||||
LastLoginOn time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (account *Account) AddCollaborator(newCollaborator *Account) error {
|
func (account *Account) AddCollaborator(newCollaborator *Account) error {
|
||||||
@ -54,7 +69,7 @@ func (account *Account) AddCollaborator(newCollaborator *Account) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (account *Account) RemoveCollaborator(accountId int) {
|
func (account *Account) RemoveCollaborator(accountId int64) {
|
||||||
list := account.Collaborators
|
list := account.Collaborators
|
||||||
for i, collaborator := range list {
|
for i, collaborator := range list {
|
||||||
if collaborator.AccountId == accountId {
|
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 {
|
for _, collaborator := range account.Collaborators {
|
||||||
if collaborator.AccountId == accountId {
|
if collaborator.AccountId == accountId {
|
||||||
return true
|
return true
|
||||||
|
38
pkg/models/collaborator.go
Normal file
38
pkg/models/collaborator.go
Normal file
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
@ -2,19 +2,32 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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 {
|
type Dashboard struct {
|
||||||
Id string `gorethink:"id,omitempty"`
|
Id int64
|
||||||
Slug string
|
Slug string `xorm:"index(IX_AccountIdSlug)"`
|
||||||
AccountId int
|
AccountId int64 `xorm:"index(IX_AccountIdSlug)"`
|
||||||
LastModifiedByUserId string
|
|
||||||
LastModifiedByDate time.Time
|
Created time.Time `xorm:"CREATED"`
|
||||||
CreatedDate time.Time
|
Updated time.Time `xorm:"UPDATED"`
|
||||||
|
|
||||||
Title string
|
Title string
|
||||||
Tags []string
|
Tags []string
|
||||||
@ -29,10 +42,7 @@ type SearchResult struct {
|
|||||||
|
|
||||||
func NewDashboard(title string) *Dashboard {
|
func NewDashboard(title string) *Dashboard {
|
||||||
dash := &Dashboard{}
|
dash := &Dashboard{}
|
||||||
dash.Id = ""
|
dash.Id = 0
|
||||||
dash.LastModifiedByDate = time.Now()
|
|
||||||
dash.CreatedDate = time.Now()
|
|
||||||
dash.LastModifiedByUserId = "123"
|
|
||||||
dash.Data = make(map[string]interface{})
|
dash.Data = make(map[string]interface{})
|
||||||
dash.Data["title"] = title
|
dash.Data["title"] = title
|
||||||
dash.Title = title
|
dash.Title = title
|
||||||
|
9
pkg/models/models.go
Normal file
9
pkg/models/models.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type OAuthType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
GITHUB OAuthType = iota + 1
|
||||||
|
GOOGLE
|
||||||
|
TWITTER
|
||||||
|
)
|
115
pkg/routes/api/api_account.go
Normal file
115
pkg/routes/api/api_account.go
Normal file
@ -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
pkg/routes/api/api_dashboard.go
Normal file
91
pkg/routes/api/api_dashboard.go
Normal file
@ -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
pkg/routes/api/api_register.go
Normal file
38
pkg/routes/api/api_register.go
Normal file
@ -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"})
|
||||||
|
}
|
32
pkg/routes/api/api_render.go
Normal file
32
pkg/routes/api/api_render.go
Normal file
@ -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
pkg/routes/dtos/models.go
Normal file
65
pkg/routes/dtos/models.go
Normal file
@ -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
pkg/routes/index.go
Normal file
51
pkg/routes/index.go
Normal file
@ -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
pkg/routes/login/login.go
Normal file
56
pkg/routes/login/login.go
Normal file
@ -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
pkg/routes/login/login_oauth.go
Normal file
74
pkg/routes/login/login_oauth.go
Normal file
@ -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("/")
|
||||||
|
}
|
@ -1,7 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import "github.com/torkelo/grafana-pro/pkg/setting"
|
|
||||||
|
|
||||||
func GlobalInit() {
|
|
||||||
setting.NewConfigContext()
|
|
||||||
}
|
|
@ -1,3 +1,6 @@
|
|||||||
|
// Copyright 2014 Unknwon
|
||||||
|
// Copyright 2014 Torkel Ödegaard
|
||||||
|
|
||||||
package setting
|
package setting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -11,6 +14,8 @@ import (
|
|||||||
|
|
||||||
"github.com/Unknwon/com"
|
"github.com/Unknwon/com"
|
||||||
"github.com/Unknwon/goconfig"
|
"github.com/Unknwon/goconfig"
|
||||||
|
"github.com/macaron-contrib/session"
|
||||||
|
|
||||||
"github.com/torkelo/grafana-pro/pkg/log"
|
"github.com/torkelo/grafana-pro/pkg/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -39,7 +44,12 @@ var (
|
|||||||
HttpAddr, HttpPort string
|
HttpAddr, HttpPort string
|
||||||
SshPort int
|
SshPort int
|
||||||
CertFile, KeyFile string
|
CertFile, KeyFile string
|
||||||
DisableRouterLog bool
|
RouterLogging bool
|
||||||
|
StaticRootPath string
|
||||||
|
|
||||||
|
// Session settings.
|
||||||
|
SessionProvider string
|
||||||
|
SessionConfig *session.Config
|
||||||
|
|
||||||
// Global setting objects.
|
// Global setting objects.
|
||||||
Cfg *goconfig.ConfigFile
|
Cfg *goconfig.ConfigFile
|
||||||
@ -48,6 +58,10 @@ var (
|
|||||||
ProdMode bool
|
ProdMode bool
|
||||||
RunUser string
|
RunUser string
|
||||||
IsWindows bool
|
IsWindows bool
|
||||||
|
|
||||||
|
// PhantomJs Rendering
|
||||||
|
ImagesDir string
|
||||||
|
PhantomDir string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -127,4 +141,36 @@ func NewConfigContext() {
|
|||||||
if port != "" {
|
if port != "" {
|
||||||
HttpPort = 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
pkg/setting/setting_oauth.go
Normal file
15
pkg/setting/setting_oauth.go
Normal file
@ -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
pkg/social/social.go
Normal file
163
pkg/social/social.go
Normal file
@ -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
pkg/stores/rethink/rethink.go
Normal file
202
pkg/stores/rethink/rethink.go
Normal file
@ -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
|
||||||
|
}
|
@ -1,15 +1,16 @@
|
|||||||
package stores
|
package rethink
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
log "github.com/alecthomas/log4go"
|
|
||||||
r "github.com/dancannon/gorethink"
|
r "github.com/dancannon/gorethink"
|
||||||
|
|
||||||
|
"github.com/torkelo/grafana-pro/pkg/log"
|
||||||
"github.com/torkelo/grafana-pro/pkg/models"
|
"github.com/torkelo/grafana-pro/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (self *rethinkStore) SaveDashboard(dash *models.Dashboard) error {
|
func SaveDashboard(dash *models.Dashboard) error {
|
||||||
resp, err := r.Table("dashboards").Insert(dash, r.InsertOpts{Conflict: "update"}).RunWrite(self.session)
|
resp, err := r.Table("dashboards").Insert(dash, r.InsertOpts{Conflict: "update"}).RunWrite(session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -23,10 +24,10 @@ func (self *rethinkStore) SaveDashboard(dash *models.Dashboard) error {
|
|||||||
return nil
|
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").
|
resp, err := r.Table("dashboards").
|
||||||
GetAllByIndex("AccountIdSlug", []interface{}{accountId, slug}).
|
GetAllByIndex("AccountIdSlug", []interface{}{accountId, slug}).
|
||||||
Run(self.session)
|
Run(session)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -41,10 +42,10 @@ func (self *rethinkStore) GetDashboard(slug string, accountId int) (*models.Dash
|
|||||||
return &dashboard, nil
|
return &dashboard, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *rethinkStore) DeleteDashboard(slug string, accountId int) error {
|
func DeleteDashboard(slug string, accountId int) error {
|
||||||
resp, err := r.Table("dashboards").
|
resp, err := r.Table("dashboards").
|
||||||
GetAllByIndex("AccountIdSlug", []interface{}{accountId, slug}).
|
GetAllByIndex("AccountIdSlug", []interface{}{accountId, slug}).
|
||||||
Delete().RunWrite(self.session)
|
Delete().RunWrite(session)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -57,10 +58,10 @@ func (self *rethinkStore) DeleteDashboard(slug string, accountId int) error {
|
|||||||
return nil
|
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").
|
docs, err := r.Table("dashboards").
|
||||||
GetAllByIndex("AccountId", []interface{}{accountId}).
|
GetAllByIndex("AccountId", []interface{}{accountId}).
|
||||||
Filter(r.Row.Field("Title").Match(".*")).Run(self.session)
|
Filter(r.Row.Field("Title").Match(".*")).Run(session)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
@ -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() {}
|
|
@ -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
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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
pkg/stores/sqlstore/sqlstore.go
Normal file
116
pkg/stores/sqlstore/sqlstore.go
Normal file
@ -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
pkg/stores/sqlstore/sqlstore_accounts.go
Normal file
99
pkg/stores/sqlstore/sqlstore_accounts.go
Normal file
@ -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
pkg/stores/sqlstore/sqlstore_dashboards.go
Normal file
64
pkg/stores/sqlstore/sqlstore_dashboards.go
Normal file
@ -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
|
||||||
|
}
|
@ -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
pkg/utils/json.go
Normal file
3
pkg/utils/json.go
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
type DynMap map[string]interface{}
|
18
views/404.html
Normal file
18
views/404.html
Normal file
@ -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>
|
Loading…
Reference in New Issue
Block a user