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 = [
|
||||
"$WORKDIR/pkg",
|
||||
"$WORKDIR/views",
|
||||
"$WORKDIR/conf",
|
||||
]
|
||||
watch_exts = [".go", ".ini"]
|
||||
build_delay = 1500
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -13,3 +13,8 @@ config.js
|
||||
*.sublime-workspace
|
||||
*.swp
|
||||
.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_port = 3000
|
||||
ssh_port = 22
|
||||
route_log = true
|
||||
router_logging = false
|
||||
|
||||
[session]
|
||||
; Either "memory", "file", default is "memory"
|
||||
provider = file
|
||||
; Provider config options
|
||||
; memory: not have any config yet
|
||||
; file: session file path, e.g. `data/sessions`
|
||||
; redis: config like redis server addr, poolSize, password, e.g. `127.0.0.1:6379,100,gogs`
|
||||
; mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table`
|
||||
provider_config = data/sessions
|
||||
; Session cookie name
|
||||
cookie_name = grafana_pro_sess
|
||||
; If you use session in https only, default is false
|
||||
cookie_secure = false
|
||||
; Enable set cookie, default is true
|
||||
enable_set_cookie = true
|
||||
; Session GC time interval, default is 86400
|
||||
gc_time_interval = 86400
|
||||
; Session life time, default is 86400
|
||||
session_life_time = 86400
|
||||
; session id hash func, Either "sha1", "sha256" or "md5" default is sha1
|
||||
session_id_hashfunc = sha1
|
||||
; Session hash key, default is use random string
|
||||
session_id_hashkey =
|
||||
|
||||
[oauth]
|
||||
enabled = true
|
||||
|
||||
[oauth.github]
|
||||
enabled = true
|
||||
client_id = de054205006b9baa2e17
|
||||
client_secret = 72b7ea52d9f1096fdf36cea95e95362a307e0322
|
||||
scopes = user:email
|
||||
auth_url = https://github.com/login/oauth/authorize
|
||||
token_url = https://github.com/login/oauth/access_token
|
||||
|
||||
[oauth.google]
|
||||
enabled = true
|
||||
client_id = 106011922963-4pvl05e9urtrm8bbqr0vouosj3e8p8kb.apps.googleusercontent.com
|
||||
client_secret = K2evIa4QhfbhhAm3SO72t2Zv
|
||||
scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
|
||||
auth_url = https://accounts.google.com/o/oauth2/auth
|
||||
token_url = https://accounts.google.com/o/oauth2/token
|
||||
|
||||
[database]
|
||||
; Either "mysql", "postgres" or "sqlite3", it's your choice
|
||||
type = sqlite3
|
||||
host = 127.0.0.1:3306
|
||||
name = grafana
|
||||
user = root
|
||||
PASSWD =
|
||||
; For "postgres" only, either "disable", "require" or "verify-full"
|
||||
ssl_mode = disable
|
||||
; For "sqlite3" only
|
||||
path = data/grafana.db
|
||||
|
||||
|
||||
[log]
|
||||
root_path =
|
||||
|
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
|
||||
|
||||
import (
|
||||
"time"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/Unknwon/macaron"
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/siddontang/go-log/log"
|
||||
"github.com/torkelo/grafana-pro/pkg/configuration"
|
||||
"github.com/torkelo/grafana-pro/pkg/server"
|
||||
"github.com/macaron-contrib/session"
|
||||
|
||||
"github.com/torkelo/grafana-pro/pkg/log"
|
||||
"github.com/torkelo/grafana-pro/pkg/middleware"
|
||||
"github.com/torkelo/grafana-pro/pkg/routes"
|
||||
"github.com/torkelo/grafana-pro/pkg/setting"
|
||||
"github.com/torkelo/grafana-pro/pkg/social"
|
||||
"github.com/torkelo/grafana-pro/pkg/stores/sqlstore"
|
||||
)
|
||||
|
||||
var CmdWeb = cli.Command{
|
||||
@ -18,23 +28,70 @@ var CmdWeb = cli.Command{
|
||||
Flags: []cli.Flag{},
|
||||
}
|
||||
|
||||
func newMacaron() *macaron.Macaron {
|
||||
m := macaron.New()
|
||||
m.Use(middleware.Logger())
|
||||
m.Use(macaron.Recovery())
|
||||
|
||||
mapStatic(m, "public", "public")
|
||||
mapStatic(m, "public/app", "app")
|
||||
mapStatic(m, "public/img", "img")
|
||||
|
||||
m.Use(session.Sessioner(session.Options{
|
||||
Provider: setting.SessionProvider,
|
||||
Config: *setting.SessionConfig,
|
||||
}))
|
||||
|
||||
m.Use(macaron.Renderer(macaron.RenderOptions{
|
||||
Directory: path.Join(setting.StaticRootPath, "views"),
|
||||
IndentJSON: macaron.Env != macaron.PROD,
|
||||
Delims: macaron.Delims{Left: "[[", Right: "]]"},
|
||||
}))
|
||||
|
||||
m.Use(middleware.GetContextHandler())
|
||||
return m
|
||||
}
|
||||
|
||||
func mapStatic(m *macaron.Macaron, dir string, prefix string) {
|
||||
m.Use(macaron.Static(
|
||||
path.Join(setting.StaticRootPath, dir),
|
||||
macaron.StaticOptions{
|
||||
SkipLogging: true,
|
||||
Prefix: prefix,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
func runWeb(*cli.Context) {
|
||||
setting.NewConfigContext()
|
||||
setting.InitServices()
|
||||
sqlstore.Init()
|
||||
social.NewOAuthService()
|
||||
|
||||
// init database
|
||||
sqlstore.LoadModelsConfig()
|
||||
if err := sqlstore.NewEngine(); err != nil {
|
||||
log.Fatal(4, "fail to initialize orm engine: %v", err)
|
||||
}
|
||||
|
||||
log.Info("Starting Grafana-Pro v.1-alpha")
|
||||
|
||||
setting.NewConfigContext()
|
||||
m := newMacaron()
|
||||
routes.Register(m)
|
||||
|
||||
cfg := configuration.NewCfg(setting.HttpPort)
|
||||
server, err := server.NewServer(cfg)
|
||||
if err != nil {
|
||||
time.Sleep(time.Second)
|
||||
panic(err)
|
||||
var err error
|
||||
listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
|
||||
log.Info("Listen: %v://%s%s", setting.Protocol, listenAddr, setting.AppSubUrl)
|
||||
switch setting.Protocol {
|
||||
case setting.HTTP:
|
||||
err = http.ListenAndServe(listenAddr, m)
|
||||
case setting.HTTPS:
|
||||
err = http.ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, m)
|
||||
default:
|
||||
log.Fatal(4, "Invalid protocol: %s", setting.Protocol)
|
||||
}
|
||||
|
||||
err = server.ListenAndServe()
|
||||
if err != nil {
|
||||
log.Error("ListenAndServe failed: ", err)
|
||||
log.Fatal(4, "Fail to start server: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond * 2000)
|
||||
|
||||
}
|
||||
|
69
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 (
|
||||
"io/ioutil"
|
||||
@ -12,8 +12,7 @@ func TestPhantomRender(t *testing.T) {
|
||||
|
||||
Convey("Can render url", t, func() {
|
||||
tempDir, _ := ioutil.TempDir("", "img")
|
||||
renderer := &PhantomRenderer{ImagesDir: tempDir, PhantomDir: "../../_vendor/phantomjs/"}
|
||||
png, err := renderer.RenderToPng("http://www.google.com")
|
||||
png, err := RenderToPng("http://www.google.com")
|
||||
So(err, ShouldBeNil)
|
||||
So(exists(png), ShouldEqual, true)
|
||||
|
58
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"
|
||||
)
|
||||
|
||||
var (
|
||||
CreateAccount func(acccount *Account) error
|
||||
UpdateAccount func(acccount *Account) error
|
||||
GetAccountByLogin func(emailOrName string) (*Account, error)
|
||||
GetAccount func(accountId int64) (*Account, error)
|
||||
GetOtherAccountsFor func(accountId int64) ([]*OtherAccount, error)
|
||||
GetCollaboratorsForAccount func(accountId int64) ([]*CollaboratorInfo, error)
|
||||
AddCollaborator func(collaborator *Collaborator) error
|
||||
)
|
||||
|
||||
// Typed errors
|
||||
var (
|
||||
ErrAccountNotFound = errors.New("Account not found")
|
||||
)
|
||||
|
||||
type CollaboratorLink struct {
|
||||
AccountId int
|
||||
AccountId int64
|
||||
Role string
|
||||
Email string
|
||||
ModifiedOn time.Time
|
||||
@ -14,26 +29,26 @@ type CollaboratorLink struct {
|
||||
}
|
||||
|
||||
type OtherAccount struct {
|
||||
Id int `gorethink:"id"`
|
||||
Name string
|
||||
Role string
|
||||
Id int64
|
||||
Email string
|
||||
Role string
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
Id int `gorethink:"id"`
|
||||
Version int
|
||||
Login string
|
||||
Email string
|
||||
AccountName string
|
||||
Id int64
|
||||
Login string `xorm:"UNIQUE NOT NULL"`
|
||||
Email string `xorm:"UNIQUE NOT NULL"`
|
||||
Name string `xorm:"UNIQUE NOT NULL"`
|
||||
FullName string
|
||||
Password string
|
||||
Name string
|
||||
IsAdmin bool
|
||||
Salt string `xorm:"VARCHAR(10)"`
|
||||
Company string
|
||||
NextDashboardId int
|
||||
UsingAccountId int
|
||||
Collaborators []CollaboratorLink
|
||||
CreatedOn time.Time
|
||||
ModifiedOn time.Time
|
||||
LastLoginOn time.Time
|
||||
UsingAccountId int64
|
||||
Collaborators []CollaboratorLink `xorm:"-"`
|
||||
Created time.Time `xorm:"CREATED"`
|
||||
Updated time.Time `xorm:"UPDATED"`
|
||||
}
|
||||
|
||||
func (account *Account) AddCollaborator(newCollaborator *Account) error {
|
||||
@ -54,7 +69,7 @@ func (account *Account) AddCollaborator(newCollaborator *Account) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (account *Account) RemoveCollaborator(accountId int) {
|
||||
func (account *Account) RemoveCollaborator(accountId int64) {
|
||||
list := account.Collaborators
|
||||
for i, collaborator := range list {
|
||||
if collaborator.AccountId == accountId {
|
||||
@ -64,7 +79,7 @@ func (account *Account) RemoveCollaborator(accountId int) {
|
||||
}
|
||||
}
|
||||
|
||||
func (account *Account) HasCollaborator(accountId int) bool {
|
||||
func (account *Account) HasCollaborator(accountId int64) bool {
|
||||
for _, collaborator := range account.Collaborators {
|
||||
if collaborator.AccountId == accountId {
|
||||
return true
|
||||
|
38
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 (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
GetDashboard func(slug string, accountId int64) (*Dashboard, error)
|
||||
SaveDashboard func(dash *Dashboard) error
|
||||
DeleteDashboard func(slug string, accountId int64) error
|
||||
SearchQuery func(query string, acccountId int64) ([]*SearchResult, error)
|
||||
)
|
||||
|
||||
// Typed errors
|
||||
var (
|
||||
ErrDashboardNotFound = errors.New("Account not found")
|
||||
)
|
||||
|
||||
type Dashboard struct {
|
||||
Id string `gorethink:"id,omitempty"`
|
||||
Slug string
|
||||
AccountId int
|
||||
LastModifiedByUserId string
|
||||
LastModifiedByDate time.Time
|
||||
CreatedDate time.Time
|
||||
Id int64
|
||||
Slug string `xorm:"index(IX_AccountIdSlug)"`
|
||||
AccountId int64 `xorm:"index(IX_AccountIdSlug)"`
|
||||
|
||||
Created time.Time `xorm:"CREATED"`
|
||||
Updated time.Time `xorm:"UPDATED"`
|
||||
|
||||
Title string
|
||||
Tags []string
|
||||
@ -29,10 +42,7 @@ type SearchResult struct {
|
||||
|
||||
func NewDashboard(title string) *Dashboard {
|
||||
dash := &Dashboard{}
|
||||
dash.Id = ""
|
||||
dash.LastModifiedByDate = time.Now()
|
||||
dash.CreatedDate = time.Now()
|
||||
dash.LastModifiedByUserId = "123"
|
||||
dash.Id = 0
|
||||
dash.Data = make(map[string]interface{})
|
||||
dash.Data["title"] = title
|
||||
dash.Title = title
|
||||
|
9
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
|
||||
|
||||
import (
|
||||
@ -11,6 +14,8 @@ import (
|
||||
|
||||
"github.com/Unknwon/com"
|
||||
"github.com/Unknwon/goconfig"
|
||||
"github.com/macaron-contrib/session"
|
||||
|
||||
"github.com/torkelo/grafana-pro/pkg/log"
|
||||
)
|
||||
|
||||
@ -39,7 +44,12 @@ var (
|
||||
HttpAddr, HttpPort string
|
||||
SshPort int
|
||||
CertFile, KeyFile string
|
||||
DisableRouterLog bool
|
||||
RouterLogging bool
|
||||
StaticRootPath string
|
||||
|
||||
// Session settings.
|
||||
SessionProvider string
|
||||
SessionConfig *session.Config
|
||||
|
||||
// Global setting objects.
|
||||
Cfg *goconfig.ConfigFile
|
||||
@ -48,6 +58,10 @@ var (
|
||||
ProdMode bool
|
||||
RunUser string
|
||||
IsWindows bool
|
||||
|
||||
// PhantomJs Rendering
|
||||
ImagesDir string
|
||||
PhantomDir string
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -127,4 +141,36 @@ func NewConfigContext() {
|
||||
if port != "" {
|
||||
HttpPort = port
|
||||
}
|
||||
|
||||
StaticRootPath = Cfg.MustValue("server", "static_root_path", workDir)
|
||||
RouterLogging = Cfg.MustBool("server", "router_logging", false)
|
||||
|
||||
// PhantomJS rendering
|
||||
ImagesDir = "data/png"
|
||||
PhantomDir = "_vendor/phantomjs"
|
||||
|
||||
LogRootPath = Cfg.MustValue("log", "root_path", path.Join(workDir, "/data/log"))
|
||||
}
|
||||
|
||||
func initSessionService() {
|
||||
SessionProvider = Cfg.MustValueRange("session", "provider", "memory", []string{"memory", "file"})
|
||||
|
||||
SessionConfig = new(session.Config)
|
||||
SessionConfig.ProviderConfig = strings.Trim(Cfg.MustValue("session", "provider_config"), "\" ")
|
||||
SessionConfig.CookieName = Cfg.MustValue("session", "cookie_name", "grafana_pro_sess")
|
||||
SessionConfig.CookiePath = AppSubUrl
|
||||
SessionConfig.Secure = Cfg.MustBool("session", "cookie_secure")
|
||||
SessionConfig.EnableSetCookie = Cfg.MustBool("session", "enable_set_cookie", true)
|
||||
SessionConfig.Gclifetime = Cfg.MustInt64("session", "gc_interval_time", 86400)
|
||||
SessionConfig.Maxlifetime = Cfg.MustInt64("session", "session_life_time", 86400)
|
||||
|
||||
if SessionProvider == "file" {
|
||||
os.MkdirAll(path.Dir(SessionConfig.ProviderConfig), os.ModePerm)
|
||||
}
|
||||
|
||||
log.Info("Session Service Enabled")
|
||||
}
|
||||
|
||||
func InitServices() {
|
||||
initSessionService()
|
||||
}
|
||||
|
15
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 (
|
||||
"errors"
|
||||
|
||||
log "github.com/alecthomas/log4go"
|
||||
r "github.com/dancannon/gorethink"
|
||||
|
||||
"github.com/torkelo/grafana-pro/pkg/log"
|
||||
"github.com/torkelo/grafana-pro/pkg/models"
|
||||
)
|
||||
|
||||
func (self *rethinkStore) SaveDashboard(dash *models.Dashboard) error {
|
||||
resp, err := r.Table("dashboards").Insert(dash, r.InsertOpts{Conflict: "update"}).RunWrite(self.session)
|
||||
func SaveDashboard(dash *models.Dashboard) error {
|
||||
resp, err := r.Table("dashboards").Insert(dash, r.InsertOpts{Conflict: "update"}).RunWrite(session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -23,10 +24,10 @@ func (self *rethinkStore) SaveDashboard(dash *models.Dashboard) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *rethinkStore) GetDashboard(slug string, accountId int) (*models.Dashboard, error) {
|
||||
func GetDashboard(slug string, accountId int) (*models.Dashboard, error) {
|
||||
resp, err := r.Table("dashboards").
|
||||
GetAllByIndex("AccountIdSlug", []interface{}{accountId, slug}).
|
||||
Run(self.session)
|
||||
Run(session)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -41,10 +42,10 @@ func (self *rethinkStore) GetDashboard(slug string, accountId int) (*models.Dash
|
||||
return &dashboard, nil
|
||||
}
|
||||
|
||||
func (self *rethinkStore) DeleteDashboard(slug string, accountId int) error {
|
||||
func DeleteDashboard(slug string, accountId int) error {
|
||||
resp, err := r.Table("dashboards").
|
||||
GetAllByIndex("AccountIdSlug", []interface{}{accountId, slug}).
|
||||
Delete().RunWrite(self.session)
|
||||
Delete().RunWrite(session)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@ -57,10 +58,10 @@ func (self *rethinkStore) DeleteDashboard(slug string, accountId int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *rethinkStore) Query(query string, accountId int) ([]*models.SearchResult, error) {
|
||||
func SearchQuery(query string, accountId int) ([]*models.SearchResult, error) {
|
||||
docs, err := r.Table("dashboards").
|
||||
GetAllByIndex("AccountId", []interface{}{accountId}).
|
||||
Filter(r.Row.Field("Title").Match(".*")).Run(self.session)
|
||||
Filter(r.Row.Field("Title").Match(".*")).Run(session)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
@ -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