Skip to content

Commit 95fd34e

Browse files
committed
Merge branch 'emailing'
2 parents 901685b + db0c442 commit 95fd34e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1043
-53
lines changed

conf/defaults.ini

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,20 @@ header_name = X-WEBAUTH-USER
175175
header_property = username
176176
auto_sign_up = true
177177

178+
#################################### SMTP / Emailing ##########################
179+
[smtp]
180+
enabled = false
181+
host = localhost:25
182+
user =
183+
password =
184+
cert_file =
185+
key_file =
186+
skip_verify = false
187+
from_address = admin@grafana.localhost
188+
189+
[emails]
190+
welcome_email_on_sign_up = false
191+
178192
#################################### Logging ##########################
179193
[log]
180194
# Either "console", "file", default is "console"

conf/sample.ini

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,20 @@
174174
;header_property = username
175175
;auto_sign_up = true
176176

177+
#################################### SMTP / Emailing ##########################
178+
[smtp]
179+
;enabled = false
180+
;host = localhost:25
181+
;user =
182+
;password =
183+
;cert_file =
184+
;key_file =
185+
;skip_verify = false
186+
;from_address = admin@grafana.localhost
187+
188+
[emails]
189+
;welcome_email_on_sign_up = false
190+
177191
#################################### Logging ##########################
178192
[log]
179193
# Either "console", "file", default is "console"

docker/blocks/smtp/Dockerfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM centos:centos7
2+
MAINTAINER Przemyslaw Ozgo <linux@ozgo.info>
3+
4+
RUN \
5+
yum update -y && \
6+
yum install -y net-snmp net-snmp-utils && \
7+
yum clean all
8+
9+
COPY bootstrap.sh /tmp/bootstrap.sh
10+
11+
EXPOSE 161
12+
13+
ENTRYPOINT ["/tmp/bootstrap.sh"]

docker/blocks/smtp/bootstrap.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/bin/sh
2+
3+
set -u
4+
5+
# User params
6+
USER_PARAMS=$@
7+
8+
# Internal params
9+
RUN_CMD="snmpd -f ${USER_PARAMS}"
10+
11+
#######################################
12+
# Echo/log function
13+
# Arguments:
14+
# String: value to log
15+
#######################################
16+
log() {
17+
if [[ "$@" ]]; then echo "[`date +'%Y-%m-%d %T'`] $@";
18+
else echo; fi
19+
}
20+
21+
# Launch
22+
log $RUN_CMD
23+
$RUN_CMD
24+
25+
# Exit immidiately in case of any errors or when we have interactive terminal
26+
if [[ $? != 0 ]] || test -t 0; then exit $?; fi
27+
log

docker/blocks/smtp/fig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
snmpd:
2+
build: blocks/snmpd
3+
ports:
4+
- "161:161"

main.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ import (
1414
"github.com/grafana/grafana/pkg/log"
1515
"github.com/grafana/grafana/pkg/metrics"
1616
"github.com/grafana/grafana/pkg/plugins"
17-
"github.com/grafana/grafana/pkg/search"
1817
"github.com/grafana/grafana/pkg/services/eventpublisher"
18+
"github.com/grafana/grafana/pkg/services/notifications"
19+
"github.com/grafana/grafana/pkg/services/search"
1920
"github.com/grafana/grafana/pkg/services/sqlstore"
2021
"github.com/grafana/grafana/pkg/setting"
2122
"github.com/grafana/grafana/pkg/social"
@@ -57,6 +58,10 @@ func main() {
5758
eventpublisher.Init()
5859
plugins.Init()
5960

61+
if err := notifications.Init(); err != nil {
62+
log.Fatal(3, "Notification service failed to initialize", err)
63+
}
64+
6065
if setting.ReportingEnabled {
6166
go metrics.StartUsageReportLoop()
6267
}

pkg/api/api.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ func Register(r *macaron.Macaron) {
4141
r.Get("/signup", Index)
4242
r.Post("/api/user/signup", bind(m.CreateUserCommand{}), SignUp)
4343

44+
// reset password
45+
r.Get("/user/password/send-reset-email", Index)
46+
r.Get("/user/password/reset", Index)
47+
48+
r.Post("/api/user/password/send-reset-email", bind(dtos.SendResetPasswordEmailForm{}), wrap(SendResetPasswordEmail))
49+
r.Post("/api/user/password/reset", bind(dtos.ResetUserPasswordForm{}), wrap(ResetPassword))
50+
4451
// dashboard snapshots
4552
r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
4653
r.Get("/dashboard/snapshot/*", Index)

pkg/api/dashboard.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"github.com/grafana/grafana/pkg/metrics"
1111
"github.com/grafana/grafana/pkg/middleware"
1212
m "github.com/grafana/grafana/pkg/models"
13-
"github.com/grafana/grafana/pkg/search"
13+
"github.com/grafana/grafana/pkg/services/search"
1414
"github.com/grafana/grafana/pkg/setting"
1515
"github.com/grafana/grafana/pkg/util"
1616
)

pkg/api/dtos/user.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,13 @@ type AdminUserListItem struct {
2727
Login string `json:"login"`
2828
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
2929
}
30+
31+
type SendResetPasswordEmailForm struct {
32+
UserOrEmail string `json:"userOrEmail" binding:"Required"`
33+
}
34+
35+
type ResetUserPasswordForm struct {
36+
Code string `json:"code"`
37+
NewPassword string `json:"newPassword"`
38+
ConfirmPassword string `json:"confirmPassword"`
39+
}

pkg/api/password.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package api
2+
3+
import (
4+
"github.com/grafana/grafana/pkg/api/dtos"
5+
"github.com/grafana/grafana/pkg/bus"
6+
"github.com/grafana/grafana/pkg/middleware"
7+
m "github.com/grafana/grafana/pkg/models"
8+
"github.com/grafana/grafana/pkg/util"
9+
)
10+
11+
func SendResetPasswordEmail(c *middleware.Context, form dtos.SendResetPasswordEmailForm) Response {
12+
userQuery := m.GetUserByLoginQuery{LoginOrEmail: form.UserOrEmail}
13+
14+
if err := bus.Dispatch(&userQuery); err != nil {
15+
return ApiError(404, "User does not exist", err)
16+
}
17+
18+
emailCmd := m.SendResetPasswordEmailCommand{User: userQuery.Result}
19+
if err := bus.Dispatch(&emailCmd); err != nil {
20+
return ApiError(500, "Failed to send email", err)
21+
}
22+
23+
return ApiSuccess("Email sent")
24+
}
25+
26+
func ResetPassword(c *middleware.Context, form dtos.ResetUserPasswordForm) Response {
27+
query := m.ValidateResetPasswordCodeQuery{Code: form.Code}
28+
29+
if err := bus.Dispatch(&query); err != nil {
30+
if err == m.ErrInvalidEmailCode {
31+
return ApiError(400, "Invalid or expired reset password code", nil)
32+
}
33+
return ApiError(500, "Unknown error validating email code", err)
34+
}
35+
36+
if form.NewPassword != form.ConfirmPassword {
37+
return ApiError(400, "Passwords do not match", nil)
38+
}
39+
40+
cmd := m.ChangeUserPasswordCommand{}
41+
cmd.UserId = query.Result.Id
42+
cmd.NewPassword = util.EncodePassword(form.NewPassword, query.Result.Salt)
43+
44+
if err := bus.Dispatch(&cmd); err != nil {
45+
return ApiError(500, "Failed to change user password", err)
46+
}
47+
48+
return ApiSuccess("User password changed")
49+
}

pkg/api/search.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package api
33
import (
44
"github.com/grafana/grafana/pkg/bus"
55
"github.com/grafana/grafana/pkg/middleware"
6-
"github.com/grafana/grafana/pkg/search"
6+
"github.com/grafana/grafana/pkg/services/search"
77
)
88

99
func Search(c *middleware.Context) {

pkg/api/signup.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package api
22

33
import (
44
"github.com/grafana/grafana/pkg/bus"
5+
"github.com/grafana/grafana/pkg/events"
56
"github.com/grafana/grafana/pkg/metrics"
67
"github.com/grafana/grafana/pkg/middleware"
78
m "github.com/grafana/grafana/pkg/models"
@@ -24,6 +25,13 @@ func SignUp(c *middleware.Context, cmd m.CreateUserCommand) {
2425

2526
user := cmd.Result
2627

28+
bus.Publish(&events.UserSignedUp{
29+
Id: user.Id,
30+
Name: user.Name,
31+
Email: user.Email,
32+
Login: user.Login,
33+
})
34+
2735
loginUserWithUser(&user, c)
2836

2937
c.JsonOK("User created and logged in")

pkg/events/events.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ type UserCreated struct {
7070
Email string `json:"email"`
7171
}
7272

73+
type UserSignedUp struct {
74+
Timestamp time.Time `json:"timestamp"`
75+
Id int64 `json:"id"`
76+
Name string `json:"name"`
77+
Login string `json:"login"`
78+
Email string `json:"email"`
79+
}
80+
7381
type UserUpdated struct {
7482
Timestamp time.Time `json:"timestamp"`
7583
Id int64 `json:"id"`

pkg/models/emails.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package models
2+
3+
import "errors"
4+
5+
var ErrInvalidEmailCode = errors.New("Invalid or expired email code")
6+
7+
type SendEmailCommand struct {
8+
To []string
9+
Template string
10+
Data map[string]interface{}
11+
Massive bool
12+
Info string
13+
}
14+
15+
type SendResetPasswordEmailCommand struct {
16+
User *User
17+
}
18+
19+
type ValidateResetPasswordCodeQuery struct {
20+
Code string
21+
Result *User
22+
}

pkg/models/user.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ type User struct {
3030
Updated time.Time
3131
}
3232

33+
func (u *User) NameOrFallback() string {
34+
if u.Name != "" {
35+
return u.Name
36+
} else if u.Login != "" {
37+
return u.Login
38+
} else {
39+
return u.Email
40+
}
41+
}
42+
3343
// ---------------------
3444
// COMMANDS
3545

pkg/services/notifications/codes.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package notifications
2+
3+
import (
4+
"crypto/sha1"
5+
"encoding/hex"
6+
"fmt"
7+
"time"
8+
9+
"github.com/Unknwon/com"
10+
m "github.com/grafana/grafana/pkg/models"
11+
"github.com/grafana/grafana/pkg/setting"
12+
)
13+
14+
const timeLimitCodeLength = 12 + 6 + 40
15+
16+
// create a time limit code
17+
// code format: 12 length date time string + 6 minutes string + 40 sha1 encoded string
18+
func createTimeLimitCode(data string, minutes int, startInf interface{}) string {
19+
format := "200601021504"
20+
21+
var start, end time.Time
22+
var startStr, endStr string
23+
24+
if startInf == nil {
25+
// Use now time create code
26+
start = time.Now()
27+
startStr = start.Format(format)
28+
} else {
29+
// use start string create code
30+
startStr = startInf.(string)
31+
start, _ = time.ParseInLocation(format, startStr, time.Local)
32+
startStr = start.Format(format)
33+
}
34+
35+
end = start.Add(time.Minute * time.Duration(minutes))
36+
endStr = end.Format(format)
37+
38+
// create sha1 encode string
39+
sh := sha1.New()
40+
sh.Write([]byte(data + setting.SecretKey + startStr + endStr + com.ToStr(minutes)))
41+
encoded := hex.EncodeToString(sh.Sum(nil))
42+
43+
code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded)
44+
return code
45+
}
46+
47+
// verify time limit code
48+
func validateUserEmailCode(user *m.User, code string) bool {
49+
if len(code) <= 18 {
50+
return false
51+
}
52+
53+
minutes := setting.EmailCodeValidMinutes
54+
code = code[:timeLimitCodeLength]
55+
56+
// split code
57+
start := code[:12]
58+
lives := code[12:18]
59+
if d, err := com.StrTo(lives).Int(); err == nil {
60+
minutes = d
61+
}
62+
63+
// right active code
64+
data := com.ToStr(user.Id) + user.Email + user.Login + user.Password + user.Rands
65+
retCode := createTimeLimitCode(data, minutes, start)
66+
fmt.Printf("code : %s\ncode2: %s", retCode, code)
67+
if retCode == code && minutes > 0 {
68+
// check time is expired or not
69+
before, _ := time.ParseInLocation("200601021504", start, time.Local)
70+
now := time.Now()
71+
if before.Add(time.Minute*time.Duration(minutes)).Unix() > now.Unix() {
72+
return true
73+
}
74+
}
75+
76+
return false
77+
}
78+
79+
func getLoginForEmailCode(code string) string {
80+
if len(code) <= timeLimitCodeLength {
81+
return ""
82+
}
83+
84+
// use tail hex username query user
85+
hexStr := code[timeLimitCodeLength:]
86+
b, _ := hex.DecodeString(hexStr)
87+
return string(b)
88+
}
89+
90+
func createUserEmailCode(u *m.User, startInf interface{}) string {
91+
minutes := setting.EmailCodeValidMinutes
92+
data := com.ToStr(u.Id) + u.Email + u.Login + u.Password + u.Rands
93+
code := createTimeLimitCode(data, minutes, startInf)
94+
95+
// add tail hex username
96+
code += hex.EncodeToString([]byte(u.Login))
97+
return code
98+
}

0 commit comments

Comments
 (0)