Merge branch 'macaron'

This commit is contained in:
Torkel Ödegaard 2014-11-24 08:20:22 +01:00
commit a8f915f049
55 changed files with 1829 additions and 1315 deletions

View File

@ -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
View File

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

60
_vendor/grafana.coffee Normal file
View 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

View File

@ -8,7 +8,63 @@ root_url = %(protocol)s://%(domain)s:%(http_port)s/
http_addr =
http_port = 3000
ssh_port = 22
route_log = true
router_logging = false
[session]
; Either "memory", "file", default is "memory"
provider = file
; Provider config options
; memory: not have any config yet
; file: session file path, e.g. `data/sessions`
; redis: config like redis server addr, poolSize, password, e.g. `127.0.0.1:6379,100,gogs`
; mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table`
provider_config = data/sessions
; Session cookie name
cookie_name = grafana_pro_sess
; If you use session in https only, default is false
cookie_secure = false
; Enable set cookie, default is true
enable_set_cookie = true
; Session GC time interval, default is 86400
gc_time_interval = 86400
; Session life time, default is 86400
session_life_time = 86400
; session id hash func, Either "sha1", "sha256" or "md5" default is sha1
session_id_hashfunc = sha1
; Session hash key, default is use random string
session_id_hashkey =
[oauth]
enabled = true
[oauth.github]
enabled = true
client_id = de054205006b9baa2e17
client_secret = 72b7ea52d9f1096fdf36cea95e95362a307e0322
scopes = user:email
auth_url = https://github.com/login/oauth/authorize
token_url = https://github.com/login/oauth/access_token
[oauth.google]
enabled = true
client_id = 106011922963-4pvl05e9urtrm8bbqr0vouosj3e8p8kb.apps.googleusercontent.com
client_secret = K2evIa4QhfbhhAm3SO72t2Zv
scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
auth_url = https://accounts.google.com/o/oauth2/auth
token_url = https://accounts.google.com/o/oauth2/token
[database]
; Either "mysql", "postgres" or "sqlite3", it's your choice
type = sqlite3
host = 127.0.0.1:3306
name = grafana
user = root
PASSWD =
; For "postgres" only, either "disable", "require" or "verify-full"
ssl_mode = disable
; For "sqlite3" only
path = data/grafana.db
[log]
root_path =

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
package api

View File

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

View File

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

View File

@ -1,44 +0,0 @@
package api
import (
log "github.com/alecthomas/log4go"
"github.com/gin-gonic/gin"
"github.com/torkelo/grafana-pro/pkg/models"
)
func init() {
addRoutes(func(self *HttpServer) {
self.router.GET("/register/*_", self.index)
self.router.POST("/api/register/user", self.registerUserPost)
})
}
type registerAccountJsonModel struct {
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
Password2 bool `json:"remember2"`
}
func (self *HttpServer) registerUserPost(c *gin.Context) {
var registerModel registerAccountJsonModel
if !c.EnsureBody(&registerModel) {
c.JSON(400, gin.H{"status": "bad request"})
return
}
account := models.Account{
Login: registerModel.Email,
Email: registerModel.Email,
Password: registerModel.Password,
}
err := self.store.CreateAccount(&account)
if err != nil {
log.Error("Failed to create user account, email: %v, error: %v", registerModel.Email, err)
c.JSON(500, gin.H{"status": "failed to create account"})
return
}
c.JSON(200, gin.H{"status": "ok"})
}

View File

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

View File

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

View File

@ -1 +0,0 @@
package api

View File

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

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

View File

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

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

View File

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

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

View File

@ -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
View File

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

View 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)
// }

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

View 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(&registerModel) {
c.JSON(400, utils.DynMap{"status": "bad request"})
return
}
account := models.Account{
Login: registerModel.Email,
Email: registerModel.Email,
Password: registerModel.Password,
}
err := models.CreateAccount(&account)
if err != nil {
log.Error(2, "Failed to create user account, email: %v, error: %v", registerModel.Email, err)
c.JSON(500, utils.DynMap{"status": "failed to create account"})
return
}
c.JSON(200, utils.DynMap{"status": "ok"})
}

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

View 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("/")
}

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

@ -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
View File

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

18
views/404.html Normal file
View 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>