mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
More work on email and notification infra #1456
This commit is contained in:
parent
c709a28f02
commit
89418a155a
5
main.go
5
main.go
@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/services/eventpublisher"
|
"github.com/grafana/grafana/pkg/services/eventpublisher"
|
||||||
"github.com/grafana/grafana/pkg/services/mailer"
|
"github.com/grafana/grafana/pkg/services/mailer"
|
||||||
|
"github.com/grafana/grafana/pkg/services/notifications"
|
||||||
"github.com/grafana/grafana/pkg/services/search"
|
"github.com/grafana/grafana/pkg/services/search"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@ -59,6 +60,10 @@ func main() {
|
|||||||
plugins.Init()
|
plugins.Init()
|
||||||
mailer.Init()
|
mailer.Init()
|
||||||
|
|
||||||
|
if err := notifications.Init(); err != nil {
|
||||||
|
log.Fatal(3, "Notification service failed to initialize", err)
|
||||||
|
}
|
||||||
|
|
||||||
if setting.ReportingEnabled {
|
if setting.ReportingEnabled {
|
||||||
go metrics.StartUsageReportLoop()
|
go metrics.StartUsageReportLoop()
|
||||||
}
|
}
|
||||||
|
@ -24,13 +24,6 @@ func GetApiKeys(c *middleware.Context) Response {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// bus.Dispatch(&m.SendEmailCommand{
|
|
||||||
// To: []string{"torkel@raintank.io"},
|
|
||||||
// From: "grafana@test.com",
|
|
||||||
// Subject: "Test from Grafana2",
|
|
||||||
// Body: "Body! hej hoppas allt är bra",
|
|
||||||
// })
|
|
||||||
|
|
||||||
return Json(200, result)
|
return Json(200, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ type SendEmailCommand struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SendResetPasswordEmailCommand struct {
|
type SendResetPasswordEmailCommand struct {
|
||||||
Email string
|
User *User
|
||||||
}
|
}
|
||||||
|
|
||||||
// create mail content
|
// create mail content
|
||||||
|
69
pkg/services/notifications/codes.go
Normal file
69
pkg/services/notifications/codes.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Unknwon/com"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
// create a time limit code
|
||||||
|
// code format: 12 length date time string + 6 minutes string + 40 sha1 encoded string
|
||||||
|
func CreateTimeLimitCode(data string, minutes int, startInf interface{}) string {
|
||||||
|
format := "200601021504"
|
||||||
|
|
||||||
|
var start, end time.Time
|
||||||
|
var startStr, endStr string
|
||||||
|
|
||||||
|
if startInf == nil {
|
||||||
|
// Use now time create code
|
||||||
|
start = time.Now()
|
||||||
|
startStr = start.Format(format)
|
||||||
|
} else {
|
||||||
|
// use start string create code
|
||||||
|
startStr = startInf.(string)
|
||||||
|
start, _ = time.ParseInLocation(format, startStr, time.Local)
|
||||||
|
startStr = start.Format(format)
|
||||||
|
}
|
||||||
|
|
||||||
|
end = start.Add(time.Minute * time.Duration(minutes))
|
||||||
|
endStr = end.Format(format)
|
||||||
|
|
||||||
|
// create sha1 encode string
|
||||||
|
sh := sha1.New()
|
||||||
|
sh.Write([]byte(data + setting.SecretKey + startStr + endStr + com.ToStr(minutes)))
|
||||||
|
encoded := hex.EncodeToString(sh.Sum(nil))
|
||||||
|
|
||||||
|
code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded)
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify time limit code
|
||||||
|
func VerifyTimeLimitCode(data string, minutes int, code string) bool {
|
||||||
|
if len(code) <= 18 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// split code
|
||||||
|
start := code[:12]
|
||||||
|
lives := code[12:18]
|
||||||
|
if d, err := com.StrTo(lives).Int(); err == nil {
|
||||||
|
minutes = d
|
||||||
|
}
|
||||||
|
|
||||||
|
// right active code
|
||||||
|
retCode := CreateTimeLimitCode(data, minutes, start)
|
||||||
|
if retCode == code && minutes > 0 {
|
||||||
|
// check time is expired or not
|
||||||
|
before, _ := time.ParseInLocation("200601021504", start, time.Local)
|
||||||
|
now := time.Now()
|
||||||
|
if before.Add(time.Minute*time.Duration(minutes)).Unix() > now.Unix() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
@ -5,60 +5,23 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SendEmailCommand struct {
|
// Create New mail message use MailFrom and MailUser
|
||||||
To []string
|
func newMailMessageFrom(To []string, from, subject, body string) m.SendEmailCommand {
|
||||||
From string
|
return m.NewSendEmailCommand(To, from, subject, body)
|
||||||
Subject string
|
|
||||||
Body string
|
|
||||||
Type string
|
|
||||||
Massive bool
|
|
||||||
Info string
|
|
||||||
}
|
|
||||||
|
|
||||||
type SendResetPasswordEmailCommand struct {
|
|
||||||
Email string
|
|
||||||
}
|
|
||||||
|
|
||||||
// create mail content
|
|
||||||
func (m *SendEmailCommand) Content() string {
|
|
||||||
// set mail type
|
|
||||||
contentType := "text/plain; charset=UTF-8"
|
|
||||||
if m.Type == "html" {
|
|
||||||
contentType = "text/html; charset=UTF-8"
|
|
||||||
}
|
|
||||||
|
|
||||||
// create mail content
|
|
||||||
content := "From: " + m.From + "\r\nSubject: " + m.Subject + "\r\nContent-Type: " + contentType + "\r\n\r\n" + m.Body
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create html mail command
|
|
||||||
func NewSendEmailCommand(To []string, From, Subject, Body string) SendEmailCommand {
|
|
||||||
return SendEmailCommand{
|
|
||||||
To: To,
|
|
||||||
From: From,
|
|
||||||
Subject: Subject,
|
|
||||||
Body: Body,
|
|
||||||
Type: "html",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create New mail message use MailFrom and MailUser
|
// Create New mail message use MailFrom and MailUser
|
||||||
func NewMailMessageFrom(To []string, from, subject, body string) SendEmailCommand {
|
func newMailMessage(To string, subject, body string) m.SendEmailCommand {
|
||||||
return NewSendEmailCommand(To, from, subject, body)
|
return newMailMessageFrom([]string{To}, setting.Smtp.FromAddress, subject, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create New mail message use MailFrom and MailUser
|
func getMailTmplData(u *m.User) map[interface{}]interface{} {
|
||||||
func NewMailMessage(To string, subject, body string) SendEmailCommand {
|
|
||||||
return NewMailMessageFrom([]string{To}, setting.Smtp.FromAddress, subject, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetMailTmplData(u *m.User) map[interface{}]interface{} {
|
|
||||||
data := make(map[interface{}]interface{}, 10)
|
data := make(map[interface{}]interface{}, 10)
|
||||||
data["AppUrl"] = setting.AppUrl
|
data["AppUrl"] = setting.AppUrl
|
||||||
data["BuildVersion"] = setting.BuildVersion
|
data["BuildVersion"] = setting.BuildVersion
|
||||||
data["BuildStamp"] = setting.BuildStamp
|
data["BuildStamp"] = setting.BuildStamp
|
||||||
data["BuildCommit"] = setting.BuildCommit
|
data["BuildCommit"] = setting.BuildCommit
|
||||||
|
data["Subject"] = map[string]interface{}{}
|
||||||
if u != nil {
|
if u != nil {
|
||||||
data["User"] = u
|
data["User"] = u
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,77 @@
|
|||||||
package notifications
|
package notifications
|
||||||
|
|
||||||
import "github.com/grafana/grafana/pkg/bus"
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"html/template"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
func Init() {
|
"github.com/Unknwon/com"
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var mailTemplates *template.Template
|
||||||
|
var tmplResetPassword = "reset_password.html"
|
||||||
|
|
||||||
|
func Init() error {
|
||||||
bus.AddHandler("email", sendResetPasswordEmail)
|
bus.AddHandler("email", sendResetPasswordEmail)
|
||||||
}
|
|
||||||
|
|
||||||
func sendResetPasswordEmail(cmd *SendResetPasswordEmailCommand) error {
|
mailTemplates = template.New("name")
|
||||||
|
mailTemplates.Funcs(template.FuncMap{
|
||||||
|
"Subject": subjectTemplateFunc,
|
||||||
|
})
|
||||||
|
|
||||||
|
templatePattern := filepath.Join(setting.StaticRootPath, "emails/*.html")
|
||||||
|
_, err := mailTemplates.ParseGlob(templatePattern)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !util.IsEmail(setting.Smtp.FromAddress) {
|
||||||
|
return errors.New("Invalid email address for smpt from_adress config")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var dispatchMail = func(cmd *m.SendEmailCommand) error {
|
||||||
|
return bus.Dispatch(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func subjectTemplateFunc(obj map[string]interface{}, value string) string {
|
||||||
|
obj["value"] = value
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendResetPasswordEmail(cmd *m.SendResetPasswordEmailCommand) error {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
|
var data = getMailTmplData(nil)
|
||||||
|
code := CreateUserActiveCode(cmd.User, nil)
|
||||||
|
data["Code"] = code
|
||||||
|
|
||||||
|
mailTemplates.ExecuteTemplate(&buffer, tmplResetPassword, data)
|
||||||
|
|
||||||
|
dispatchMail(&m.SendEmailCommand{
|
||||||
|
To: []string{cmd.User.Email},
|
||||||
|
From: setting.Smtp.FromAddress,
|
||||||
|
Subject: data["Subject"].(map[string]interface{})["value"].(string),
|
||||||
|
Body: buffer.String(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateUserActiveCode(u *m.User, startInf interface{}) string {
|
||||||
|
minutes := setting.EmailCodeValidMinutes
|
||||||
|
data := com.ToStr(u.Id) + u.Email + u.Login + u.Password + u.Rands
|
||||||
|
code := CreateTimeLimitCode(data, minutes, startInf)
|
||||||
|
|
||||||
|
// add tail hex username
|
||||||
|
code += hex.EncodeToString([]byte(u.Login))
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
37
pkg/services/notifications/notifications_test.go
Normal file
37
pkg/services/notifications/notifications_test.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNotifications(t *testing.T) {
|
||||||
|
|
||||||
|
Convey("Given the notifications service", t, func() {
|
||||||
|
bus.ClearBusHandlers()
|
||||||
|
|
||||||
|
setting.StaticRootPath = "../../../public/"
|
||||||
|
setting.Smtp.FromAddress = "from@address.com"
|
||||||
|
|
||||||
|
err := Init()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
var sentMail *m.SendEmailCommand
|
||||||
|
dispatchMail = func(cmd *m.SendEmailCommand) error {
|
||||||
|
sentMail = cmd
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
Convey("When sending reset email password", func() {
|
||||||
|
sendResetPasswordEmail(&m.SendResetPasswordEmailCommand{User: &m.User{Email: "asd@asd.com"}})
|
||||||
|
So(sentMail.Body, ShouldContainSubstring, "h2")
|
||||||
|
So(sentMail.Subject, ShouldEqual, "Welcome to Grafana")
|
||||||
|
So(sentMail.Body, ShouldNotContainSubstring, "Subject")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
@ -11,7 +11,7 @@ import (
|
|||||||
func TestSearch(t *testing.T) {
|
func TestSearch(t *testing.T) {
|
||||||
|
|
||||||
Convey("Given search query", t, func() {
|
Convey("Given search query", t, func() {
|
||||||
jsonDashIndex = NewJsonDashIndex("../../public/dashboards/")
|
jsonDashIndex = NewJsonDashIndex("../../../public/dashboards/")
|
||||||
query := Query{Limit: 2000}
|
query := Query{Limit: 2000}
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *FindPersistedDashboardsQuery) error {
|
bus.AddHandler("test", func(query *FindPersistedDashboardsQuery) error {
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
func TestJsonDashIndex(t *testing.T) {
|
func TestJsonDashIndex(t *testing.T) {
|
||||||
|
|
||||||
Convey("Given the json dash index", t, func() {
|
Convey("Given the json dash index", t, func() {
|
||||||
index := NewJsonDashIndex("../../public/dashboards/")
|
index := NewJsonDashIndex("../../../public/dashboards/")
|
||||||
|
|
||||||
Convey("Should be able to update index", func() {
|
Convey("Should be able to update index", func() {
|
||||||
err := index.updateIndex()
|
err := index.updateIndex()
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/search"
|
"github.com/grafana/grafana/pkg/services/search"
|
||||||
)
|
)
|
||||||
|
|
||||||
func insertTestDashboard(title string, orgId int64, tags ...interface{}) *m.Dashboard {
|
func insertTestDashboard(title string, orgId int64, tags ...interface{}) *m.Dashboard {
|
||||||
|
@ -67,11 +67,12 @@ var (
|
|||||||
EnforceDomain bool
|
EnforceDomain bool
|
||||||
|
|
||||||
// Security settings.
|
// Security settings.
|
||||||
SecretKey string
|
SecretKey string
|
||||||
LogInRememberDays int
|
LogInRememberDays int
|
||||||
CookieUserName string
|
CookieUserName string
|
||||||
CookieRememberName string
|
CookieRememberName string
|
||||||
DisableGravatar bool
|
DisableGravatar bool
|
||||||
|
EmailCodeValidMinutes int
|
||||||
|
|
||||||
// User settings
|
// User settings
|
||||||
AllowUserSignUp bool
|
AllowUserSignUp bool
|
||||||
|
@ -15,7 +15,6 @@ func TestLoadingSettings(t *testing.T) {
|
|||||||
Convey("Given the default ini files", func() {
|
Convey("Given the default ini files", func() {
|
||||||
NewConfigContext(&CommandLineArgs{HomePath: "../../"})
|
NewConfigContext(&CommandLineArgs{HomePath: "../../"})
|
||||||
|
|
||||||
So(AppName, ShouldEqual, "Grafana")
|
|
||||||
So(AdminUser, ShouldEqual, "admin")
|
So(AdminUser, ShouldEqual, "admin")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
18
pkg/util/validation.go
Normal file
18
pkg/util/validation.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
emailRegexPattern string = "^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
regexEmail = regexp.MustCompile(emailRegexPattern)
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsEmail(str string) bool {
|
||||||
|
return regexEmail.MatchString(strings.ToLower(str))
|
||||||
|
}
|
5
public/emails/reset_password.html
Normal file
5
public/emails/reset_password.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{{Subject .Subject "Welcome to Grafana"}}
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
{{.BuildVersion}}
|
||||||
|
</h2>
|
Loading…
Reference in New Issue
Block a user