mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-16 16:07:33 +08:00
support reset password by email reset link
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||
)
|
||||
|
||||
// ForgetPasswordsApi represents user forget password api
|
||||
type ForgetPasswordsApi struct {
|
||||
users *services.UserService
|
||||
tokens *services.TokenService
|
||||
forgetPasswords *services.ForgetPasswordService
|
||||
}
|
||||
|
||||
// Initialize a user api singleton instance
|
||||
var (
|
||||
ForgetPasswords = &ForgetPasswordsApi{
|
||||
users: services.Users,
|
||||
tokens: services.Tokens,
|
||||
forgetPasswords: services.ForgetPasswords,
|
||||
}
|
||||
)
|
||||
|
||||
// UserForgetPasswordRequestHandler generates password reset link and send user an email with this link
|
||||
func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||
var request models.ForgetPasswordRequest
|
||||
err := c.ShouldBindJSON(&request)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.ErrEmailIsEmptyOrInvalid
|
||||
}
|
||||
|
||||
user, err := a.users.GetUserByEmail(request.Email)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.ErrorfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
token, _, err := a.tokens.CreatePasswordResetToken(user, c)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.ErrTokenGenerating
|
||||
}
|
||||
|
||||
err = a.forgetPasswords.SendPasswordResetEmail(user, token)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// UserResetPasswordHandler resets user password by request parameters
|
||||
func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||
var request models.PasswordResetRequest
|
||||
err := c.ShouldBindJSON(&request)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
user, err := a.users.GetUserById(uid)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.ErrorfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.Email != request.Email {
|
||||
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] request email not equals the user email")
|
||||
return nil, errs.ErrEmptyIsInvalid
|
||||
}
|
||||
|
||||
if a.users.IsPasswordEqualsUserPassword(request.Password, user) {
|
||||
oldTokenClaims := c.GetTokenClaims()
|
||||
err = a.tokens.DeleteTokenByClaims(oldTokenClaims)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] failed to revoke password reset token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrNewPasswordEqualsOldInvalid
|
||||
}
|
||||
|
||||
userNew := &models.User{
|
||||
Uid: user.Uid,
|
||||
Salt: user.Salt,
|
||||
Password: request.Password,
|
||||
}
|
||||
|
||||
_, err = a.users.UpdateUser(userNew, false)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
err = a.tokens.DeleteTokensBeforeTime(uid, now)
|
||||
|
||||
if err == nil {
|
||||
log.InfofWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
|
||||
} else {
|
||||
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -11,8 +11,9 @@ type TokenType byte
|
||||
|
||||
// Token types
|
||||
const (
|
||||
USER_TOKEN_TYPE_NORMAL TokenType = 1
|
||||
USER_TOKEN_TYPE_REQUIRE_2FA TokenType = 2
|
||||
USER_TOKEN_TYPE_NORMAL TokenType = 1
|
||||
USER_TOKEN_TYPE_REQUIRE_2FA TokenType = 2
|
||||
USER_TOKEN_TYPE_RESET_PASSWORD TokenType = 3
|
||||
)
|
||||
|
||||
// UserTokenClaims represents user token
|
||||
|
||||
@@ -14,6 +14,7 @@ const (
|
||||
SystemSubcategoryDefault = 0
|
||||
SystemSubcategorySetting = 1
|
||||
SystemSubcategoryDatabase = 2
|
||||
SystemSubcategoryMail = 3
|
||||
)
|
||||
|
||||
// Sub categories of normal error
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package errs
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Error codes related to mail
|
||||
var (
|
||||
ErrSmtpServerNotEnabled = NewSystemError(SystemSubcategoryMail, 0, http.StatusInternalServerError, "smtp server is not enabled")
|
||||
ErrSmtpServerHostInvalid = NewSystemError(SystemSubcategoryMail, 1, http.StatusInternalServerError, "smtp server host is invalid")
|
||||
)
|
||||
+14
-13
@@ -6,17 +6,18 @@ import (
|
||||
|
||||
// Error codes related to tokens
|
||||
var (
|
||||
ErrTokenGenerating = NewNormalError(NormalSubcategoryToken, 0, http.StatusInternalServerError, "failed to generate token")
|
||||
ErrUnauthorizedAccess = NewNormalError(NormalSubcategoryToken, 1, http.StatusUnauthorized, "unauthorized access")
|
||||
ErrCurrentInvalidToken = NewNormalError(NormalSubcategoryToken, 2, http.StatusUnauthorized, "current token is invalid")
|
||||
ErrCurrentTokenExpired = NewNormalError(NormalSubcategoryToken, 3, http.StatusUnauthorized, "current token is expired")
|
||||
ErrCurrentInvalidTokenType = NewNormalError(NormalSubcategoryToken, 4, http.StatusUnauthorized, "current token type is invalid")
|
||||
ErrCurrentTokenRequire2FA = NewNormalError(NormalSubcategoryToken, 5, http.StatusUnauthorized, "current token requires two factor authorization")
|
||||
ErrCurrentTokenNotRequire2FA = NewNormalError(NormalSubcategoryToken, 6, http.StatusUnauthorized, "current token does not require two factor authorization")
|
||||
ErrInvalidToken = NewNormalError(NormalSubcategoryToken, 7, http.StatusBadRequest, "token is invalid")
|
||||
ErrInvalidTokenId = NewNormalError(NormalSubcategoryToken, 8, http.StatusBadRequest, "token id is invalid")
|
||||
ErrInvalidUserTokenId = NewNormalError(NormalSubcategoryToken, 9, http.StatusBadRequest, "user token id is invalid")
|
||||
ErrTokenRecordNotFound = NewNormalError(NormalSubcategoryToken, 10, http.StatusBadRequest, "token is not found")
|
||||
ErrTokenExpired = NewNormalError(NormalSubcategoryToken, 11, http.StatusBadRequest, "token is expired")
|
||||
ErrTokenIsEmpty = NewNormalError(NormalSubcategoryToken, 12, http.StatusBadRequest, "token is empty")
|
||||
ErrTokenGenerating = NewNormalError(NormalSubcategoryToken, 0, http.StatusInternalServerError, "failed to generate token")
|
||||
ErrUnauthorizedAccess = NewNormalError(NormalSubcategoryToken, 1, http.StatusUnauthorized, "unauthorized access")
|
||||
ErrCurrentInvalidToken = NewNormalError(NormalSubcategoryToken, 2, http.StatusUnauthorized, "current token is invalid")
|
||||
ErrCurrentTokenExpired = NewNormalError(NormalSubcategoryToken, 3, http.StatusUnauthorized, "current token is expired")
|
||||
ErrCurrentInvalidTokenType = NewNormalError(NormalSubcategoryToken, 4, http.StatusUnauthorized, "current token type is invalid")
|
||||
ErrCurrentTokenRequire2FA = NewNormalError(NormalSubcategoryToken, 5, http.StatusUnauthorized, "current token requires two factor authorization")
|
||||
ErrCurrentTokenNotRequire2FA = NewNormalError(NormalSubcategoryToken, 6, http.StatusUnauthorized, "current token does not require two factor authorization")
|
||||
ErrInvalidToken = NewNormalError(NormalSubcategoryToken, 7, http.StatusBadRequest, "token is invalid")
|
||||
ErrInvalidTokenId = NewNormalError(NormalSubcategoryToken, 8, http.StatusBadRequest, "token id is invalid")
|
||||
ErrInvalidUserTokenId = NewNormalError(NormalSubcategoryToken, 9, http.StatusBadRequest, "user token id is invalid")
|
||||
ErrTokenRecordNotFound = NewNormalError(NormalSubcategoryToken, 10, http.StatusBadRequest, "token is not found")
|
||||
ErrTokenExpired = NewNormalError(NormalSubcategoryToken, 11, http.StatusBadRequest, "token is expired")
|
||||
ErrTokenIsEmpty = NewNormalError(NormalSubcategoryToken, 12, http.StatusBadRequest, "token is empty")
|
||||
ErrPasswordResetTokenIsInvalidOrExpired = NewNormalError(NormalSubcategoryToken, 13, http.StatusBadRequest, "password reset token is invalid or expired")
|
||||
)
|
||||
|
||||
@@ -23,4 +23,7 @@ var (
|
||||
ErrUserRegistrationNotAllowed = NewNormalError(NormalSubcategoryUser, 14, http.StatusBadRequest, "user registration not allowed")
|
||||
ErrUserDefaultAccountIsInvalid = NewNormalError(NormalSubcategoryUser, 15, http.StatusBadRequest, "user default account is invalid")
|
||||
ErrUserIsDisabled = NewNormalError(NormalSubcategoryUser, 16, http.StatusBadRequest, "user is disabled")
|
||||
ErrEmptyIsInvalid = NewNormalError(NormalSubcategoryUser, 17, http.StatusBadRequest, "email is invalid")
|
||||
ErrEmailIsEmptyOrInvalid = NewNormalError(NormalSubcategoryUser, 18, http.StatusBadRequest, "email is empty or invalid")
|
||||
ErrNewPasswordEqualsOldInvalid = NewNormalError(NormalSubcategoryUser, 19, http.StatusBadRequest, "new password equals old password")
|
||||
)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package locales
|
||||
|
||||
// DefaultLanguage represents the default language
|
||||
var DefaultLanguage = en
|
||||
|
||||
// AllLanguages represents all the supported language
|
||||
var AllLanguages = map[string]*LocaleInfo{
|
||||
"en": {
|
||||
Content: en,
|
||||
},
|
||||
"zh-Hans": {
|
||||
Content: zhHans,
|
||||
},
|
||||
}
|
||||
|
||||
func GetLocaleTextItems(locale string) *LocaleTextItems {
|
||||
localeInfo, exists := AllLanguages[locale]
|
||||
|
||||
if exists {
|
||||
return localeInfo.Content
|
||||
}
|
||||
|
||||
return DefaultLanguage
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package locales
|
||||
|
||||
// LocaleTextItems represents all text items need to be translated
|
||||
type LocaleTextItems struct {
|
||||
ForgetPasswordMailTextItems *ForgetPasswordMailTextItems
|
||||
}
|
||||
|
||||
// ForgetPasswordMailTextItems represents text items need to be translated in forget password mail
|
||||
type ForgetPasswordMailTextItems struct {
|
||||
Title string
|
||||
SalutationFormat string
|
||||
DescriptionAboveBtn string
|
||||
ResetPassword string
|
||||
DescriptionBelowBtnFormat string
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package locales
|
||||
|
||||
var en = &LocaleTextItems{
|
||||
ForgetPasswordMailTextItems: &ForgetPasswordMailTextItems{
|
||||
Title: "Reset Your Password",
|
||||
SalutationFormat: "Hi %s,",
|
||||
DescriptionAboveBtn: "We recently received a request to reset your password. You can click the below link to reset your password.",
|
||||
ResetPassword: "Reset Password",
|
||||
DescriptionBelowBtnFormat: "If you did not request to reset your password, please simply disregard this email. If you cannot click the above link, please copy the above url and paste it into your browser. The password reset link will be expired after %v minutes.",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package locales
|
||||
|
||||
// LocaleInfo represents locale info
|
||||
type LocaleInfo struct {
|
||||
Aliases []string
|
||||
Content *LocaleTextItems
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package locales
|
||||
|
||||
var zhHans = &LocaleTextItems{
|
||||
ForgetPasswordMailTextItems: &ForgetPasswordMailTextItems{
|
||||
Title: "重置密码",
|
||||
SalutationFormat: "%s 你好,",
|
||||
DescriptionAboveBtn: "我们刚才收到重置您密码的请求。您可以点击下方链接重置您的密码。",
|
||||
ResetPassword: "重置密码",
|
||||
DescriptionBelowBtnFormat: "如果您没有请求重置密码,请直接忽略本邮件。如果您无法点击上述链接,请复制下方的地址然后在您的浏览器中粘贴。重置密码链接将在 %v 分钟后过期。",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
|
||||
"gopkg.in/mail.v2"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
// DefaultMailer represents default mailer
|
||||
type DefaultMailer struct {
|
||||
dialer *mail.Dialer
|
||||
fromAddress string
|
||||
}
|
||||
|
||||
// NewDefaultMailer returns a new default mailer
|
||||
func NewDefaultMailer(smtpConfig *settings.SmtpConfig) (*DefaultMailer, error) {
|
||||
host, portStr, err := net.SplitHostPort(smtpConfig.SmtpHost)
|
||||
|
||||
if err != nil {
|
||||
return nil, errs.ErrSmtpServerHostInvalid
|
||||
}
|
||||
|
||||
port, err := utils.StringToInt(portStr)
|
||||
|
||||
if err != nil {
|
||||
return nil, errs.ErrSmtpServerHostInvalid
|
||||
}
|
||||
|
||||
dialer := mail.NewDialer(host, port, smtpConfig.SmtpUser, smtpConfig.SmtpPasswd)
|
||||
dialer.TLSConfig = &tls.Config{
|
||||
ServerName: host,
|
||||
InsecureSkipVerify: smtpConfig.SmtpSkipTLSVerify,
|
||||
}
|
||||
|
||||
mailer := &DefaultMailer{
|
||||
dialer: dialer,
|
||||
fromAddress: smtpConfig.FromAddress,
|
||||
}
|
||||
|
||||
return mailer, nil
|
||||
}
|
||||
|
||||
// SendMail sends an email according to argument
|
||||
func (m *DefaultMailer) SendMail(message *MailMessage) error {
|
||||
if m.dialer == nil {
|
||||
return errs.ErrSmtpServerNotEnabled
|
||||
}
|
||||
|
||||
mailMessage := mail.NewMessage()
|
||||
mailMessage.SetHeader("From", m.fromAddress)
|
||||
mailMessage.SetHeader("To", message.To)
|
||||
mailMessage.SetHeader("Subject", message.Subject)
|
||||
mailMessage.SetBody("text/html", message.Body)
|
||||
|
||||
err := m.dialer.DialAndSend(mailMessage)
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package mail
|
||||
|
||||
// MailMessage represents an email entity
|
||||
type MailMessage struct {
|
||||
To string
|
||||
Subject string
|
||||
Body string
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package mail
|
||||
|
||||
// Mailer is email sender interface
|
||||
type Mailer interface {
|
||||
SendMail(message *MailMessage) error
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
)
|
||||
|
||||
// MailerContainer contains the current mailer
|
||||
type MailerContainer struct {
|
||||
Current Mailer
|
||||
}
|
||||
|
||||
// Initialize a mailer container singleton instance
|
||||
var (
|
||||
Container = &MailerContainer{}
|
||||
)
|
||||
|
||||
// InitializeMailer initializes the current mailer according to the config
|
||||
func InitializeMailer(config *settings.Config) error {
|
||||
if !config.EnableSmtp {
|
||||
Container.Current = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
mailer, err := NewDefaultMailer(config.SmtpConfig)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Container.Current = mailer
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendMail sends an email according to argument
|
||||
func (u *MailerContainer) SendMail(message *MailMessage) error {
|
||||
return u.Current.SendMail(message)
|
||||
}
|
||||
@@ -56,6 +56,25 @@ func JWTTwoFactorAuthorization(c *core.Context) {
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// JWTResetPasswordAuthorization verifies whether current request is password reset
|
||||
func JWTResetPasswordAuthorization(c *core.Context) {
|
||||
claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_ARGUMENT)
|
||||
|
||||
if err != nil {
|
||||
utils.PrintJsonErrorResult(c, errs.ErrPasswordResetTokenIsInvalidOrExpired)
|
||||
return
|
||||
}
|
||||
|
||||
if claims.Type != core.USER_TOKEN_TYPE_RESET_PASSWORD {
|
||||
log.WarnfWithRequestId(c, "[authorization.JWTResetPasswordAuthorization] user \"uid:%d\" token is not for password request", claims.Uid)
|
||||
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidToken)
|
||||
return
|
||||
}
|
||||
|
||||
c.SetTokenClaims(claims)
|
||||
c.Next()
|
||||
}
|
||||
|
||||
func jwtAuthorization(c *core.Context, source TokenSourceType) {
|
||||
claims, err := getTokenClaims(c, source)
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ func ServerSettingsCookie(config *settings.Config) core.MiddlewareHandlerFunc {
|
||||
return func(c *core.Context) {
|
||||
settingsArr := []string{
|
||||
buildBooleanSetting("r", config.EnableUserRegister),
|
||||
buildBooleanSetting("f", config.EnableUserForgetPassword),
|
||||
buildBooleanSetting("e", config.EnableDataExport),
|
||||
buildStringSetting("m", strings.Replace(config.MapProvider, "_", "-", -1)),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package models
|
||||
|
||||
// ForgetPasswordRequest represents all parameters of forget password request
|
||||
type ForgetPasswordRequest struct {
|
||||
Email string `json:"email" binding:"required,notBlank,max=100,validEmail"`
|
||||
}
|
||||
|
||||
// PasswordResetRequest represents all parameters of reset password request
|
||||
type PasswordResetRequest struct {
|
||||
Email string `json:"email" binding:"required,notBlank,max=100,validEmail"`
|
||||
Password string `json:"password" binding:"required,min=6,max=128"`
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package services
|
||||
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/datastore"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/mail"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/uuid"
|
||||
)
|
||||
@@ -36,6 +38,20 @@ func (s *ServiceUsingConfig) CurrentConfig() *settings.Config {
|
||||
return s.container.Current
|
||||
}
|
||||
|
||||
// ServiceUsingMailer represents a service that need to use mailer
|
||||
type ServiceUsingMailer struct {
|
||||
container *mail.MailerContainer
|
||||
}
|
||||
|
||||
// SendMail sends an email according to argument
|
||||
func (s *ServiceUsingMailer) SendMail(message *mail.MailMessage) error {
|
||||
if s.container.Current == nil {
|
||||
return errs.ErrSmtpServerNotEnabled
|
||||
}
|
||||
|
||||
return s.container.Current.SendMail(message)
|
||||
}
|
||||
|
||||
// ServiceUsingUuid represents a service that need to use uuid
|
||||
type ServiceUsingUuid struct {
|
||||
container *uuid.UuidContainer
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/locales"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/mail"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/templates"
|
||||
)
|
||||
|
||||
const passwordResetUrlFormat = "%sdesktop/#/resetpassword?token=%s"
|
||||
|
||||
// ForgetPasswordService represents forget password service
|
||||
type ForgetPasswordService struct {
|
||||
ServiceUsingConfig
|
||||
ServiceUsingMailer
|
||||
}
|
||||
|
||||
// Initialize a forget password service singleton instance
|
||||
var (
|
||||
ForgetPasswords = &ForgetPasswordService{
|
||||
ServiceUsingConfig: ServiceUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
ServiceUsingMailer: ServiceUsingMailer{
|
||||
container: mail.Container,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// SendPasswordResetEmail sends password reset email according to specified parameters
|
||||
func (s *ForgetPasswordService) SendPasswordResetEmail(user *models.User, passwordResetToken string) error {
|
||||
if !s.CurrentConfig().EnableSmtp {
|
||||
return errs.ErrSmtpServerNotEnabled
|
||||
}
|
||||
|
||||
localeTextItems := locales.GetLocaleTextItems(user.Language)
|
||||
forgetPasswordTextItems := localeTextItems.ForgetPasswordMailTextItems
|
||||
|
||||
expireTimeInMinutes := s.CurrentConfig().ForgetPasswordTokenExpiredTimeDuration.Minutes()
|
||||
passwordResetUrl := fmt.Sprintf(passwordResetUrlFormat, s.CurrentConfig().RootUrl, url.QueryEscape(passwordResetToken))
|
||||
|
||||
tmpl, err := templates.GetTemplate("email/password_reset")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
templateParams := map[string]interface{}{
|
||||
"ForgetPasswordMail": map[string]interface{}{
|
||||
"Title": forgetPasswordTextItems.Title,
|
||||
"Salutation": fmt.Sprintf(forgetPasswordTextItems.SalutationFormat, user.Nickname),
|
||||
"DescriptionAboveBtn": forgetPasswordTextItems.DescriptionAboveBtn,
|
||||
"ResetPasswordUrl": passwordResetUrl,
|
||||
"ResetPassword": forgetPasswordTextItems.ResetPassword,
|
||||
"DescriptionBelowBtn": fmt.Sprintf(forgetPasswordTextItems.DescriptionBelowBtnFormat, expireTimeInMinutes),
|
||||
},
|
||||
}
|
||||
|
||||
var bodyBuffer bytes.Buffer
|
||||
err = tmpl.Execute(&bodyBuffer, templateParams)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
message := &mail.MailMessage{
|
||||
To: user.Email,
|
||||
Subject: forgetPasswordTextItems.Title,
|
||||
Body: bodyBuffer.String(),
|
||||
}
|
||||
|
||||
err = s.SendMail(message)
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -88,6 +88,11 @@ func (s *TokenService) CreateRequire2FAToken(user *models.User, ctx *core.Contex
|
||||
return s.createToken(user, core.USER_TOKEN_TYPE_REQUIRE_2FA, s.getUserAgent(ctx), s.CurrentConfig().TemporaryTokenExpiredTimeDuration)
|
||||
}
|
||||
|
||||
// CreatePasswordResetToken generates a new password reset token and saves to database
|
||||
func (s *TokenService) CreatePasswordResetToken(user *models.User, ctx *core.Context) (string, *core.UserTokenClaims, error) {
|
||||
return s.createToken(user, core.USER_TOKEN_TYPE_RESET_PASSWORD, s.getUserAgent(ctx), s.CurrentConfig().ForgetPasswordTokenExpiredTimeDuration)
|
||||
}
|
||||
|
||||
// DeleteToken deletes given token from database
|
||||
func (s *TokenService) DeleteToken(tokenRecord *models.TokenRecord) error {
|
||||
if tokenRecord.Uid <= 0 {
|
||||
|
||||
+55
-12
@@ -113,9 +113,10 @@ const (
|
||||
defaultLogMode string = "console"
|
||||
defaultLoglevel Level = LOGLEVEL_INFO
|
||||
|
||||
defaultSecretKey string = "ezbookkeeping"
|
||||
defaultTokenExpiredTime uint32 = 604800 // 7 days
|
||||
defaultTemporaryTokenExpiredTime uint32 = 300 // 5 minutes
|
||||
defaultSecretKey string = "ezbookkeeping"
|
||||
defaultTokenExpiredTime uint32 = 604800 // 7 days
|
||||
defaultTemporaryTokenExpiredTime uint32 = 300 // 5 minutes
|
||||
defaultForgetPasswordTokenExpiredTime uint32 = 3600 // 60 minutes
|
||||
|
||||
defaultExchangeRatesDataRequestTimeout uint32 = 10000 // 10 seconds
|
||||
)
|
||||
@@ -137,6 +138,15 @@ type DatabaseConfig struct {
|
||||
ConnectionMaxLifeTime uint32
|
||||
}
|
||||
|
||||
// SmtpConfig represents the smtp setting config
|
||||
type SmtpConfig struct {
|
||||
SmtpHost string
|
||||
SmtpUser string
|
||||
SmtpPasswd string
|
||||
SmtpSkipTLSVerify bool
|
||||
FromAddress string
|
||||
}
|
||||
|
||||
// Config represents the global setting config
|
||||
type Config struct {
|
||||
// Global
|
||||
@@ -167,6 +177,10 @@ type Config struct {
|
||||
EnableQueryLog bool
|
||||
AutoUpdateDatabase bool
|
||||
|
||||
// Mail
|
||||
EnableSmtp bool
|
||||
SmtpConfig *SmtpConfig
|
||||
|
||||
// Log
|
||||
LogModes []string
|
||||
EnableConsoleLog bool
|
||||
@@ -180,17 +194,20 @@ type Config struct {
|
||||
UuidServerId uint8
|
||||
|
||||
// Secret
|
||||
SecretKey string
|
||||
EnableTwoFactor bool
|
||||
TokenExpiredTime uint32
|
||||
TokenExpiredTimeDuration time.Duration
|
||||
TemporaryTokenExpiredTime uint32
|
||||
TemporaryTokenExpiredTimeDuration time.Duration
|
||||
EnableRequestIdHeader bool
|
||||
SecretKey string
|
||||
EnableTwoFactor bool
|
||||
TokenExpiredTime uint32
|
||||
TokenExpiredTimeDuration time.Duration
|
||||
TemporaryTokenExpiredTime uint32
|
||||
TemporaryTokenExpiredTimeDuration time.Duration
|
||||
ForgetPasswordTokenExpiredTime uint32
|
||||
ForgetPasswordTokenExpiredTimeDuration time.Duration
|
||||
EnableRequestIdHeader bool
|
||||
|
||||
// User
|
||||
EnableUserRegister bool
|
||||
AvatarProvider string
|
||||
EnableUserRegister bool
|
||||
EnableUserForgetPassword bool
|
||||
AvatarProvider string
|
||||
|
||||
// Data
|
||||
EnableDataExport bool
|
||||
@@ -246,6 +263,12 @@ func LoadConfiguration(configFilePath string) (*Config, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = loadMailConfiguration(config, cfgFile, "mail")
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = loadLogConfiguration(config, cfgFile, "log")
|
||||
|
||||
if err != nil {
|
||||
@@ -394,6 +417,22 @@ func loadDatabaseConfiguration(config *Config, configFile *ini.File, sectionName
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadMailConfiguration(config *Config, configFile *ini.File, sectionName string) error {
|
||||
config.EnableSmtp = getConfigItemBoolValue(configFile, sectionName, "enable_smtp", false)
|
||||
|
||||
smtpConfig := &SmtpConfig{}
|
||||
smtpConfig.SmtpHost = getConfigItemStringValue(configFile, sectionName, "smtp_host")
|
||||
smtpConfig.SmtpUser = getConfigItemStringValue(configFile, sectionName, "smtp_user")
|
||||
smtpConfig.SmtpPasswd = getConfigItemStringValue(configFile, sectionName, "smtp_passwd")
|
||||
smtpConfig.SmtpSkipTLSVerify = getConfigItemBoolValue(configFile, sectionName, "smtp_skip_tls_verify", false)
|
||||
|
||||
smtpConfig.FromAddress = getConfigItemStringValue(configFile, sectionName, "from_address")
|
||||
|
||||
config.SmtpConfig = smtpConfig
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadLogConfiguration(config *Config, configFile *ini.File, sectionName string) error {
|
||||
config.LogModes = strings.Split(getConfigItemStringValue(configFile, sectionName, "mode", defaultLogMode), " ")
|
||||
|
||||
@@ -442,6 +481,9 @@ func loadSecurityConfiguration(config *Config, configFile *ini.File, sectionName
|
||||
config.TemporaryTokenExpiredTime = getConfigItemUint32Value(configFile, sectionName, "temporary_token_expired_time", defaultTemporaryTokenExpiredTime)
|
||||
config.TemporaryTokenExpiredTimeDuration = time.Duration(config.TemporaryTokenExpiredTime) * time.Second
|
||||
|
||||
config.ForgetPasswordTokenExpiredTime = getConfigItemUint32Value(configFile, sectionName, "forget_password_token_expired_time", defaultForgetPasswordTokenExpiredTime)
|
||||
config.ForgetPasswordTokenExpiredTimeDuration = time.Duration(config.ForgetPasswordTokenExpiredTime) * time.Second
|
||||
|
||||
config.EnableRequestIdHeader = getConfigItemBoolValue(configFile, sectionName, "request_id_header", true)
|
||||
|
||||
return nil
|
||||
@@ -449,6 +491,7 @@ func loadSecurityConfiguration(config *Config, configFile *ini.File, sectionName
|
||||
|
||||
func loadUserConfiguration(config *Config, configFile *ini.File, sectionName string) error {
|
||||
config.EnableUserRegister = getConfigItemBoolValue(configFile, sectionName, "enable_register", false)
|
||||
config.EnableUserForgetPassword = getConfigItemBoolValue(configFile, sectionName, "enable_forget_password", false)
|
||||
|
||||
if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == "" {
|
||||
config.AvatarProvider = ""
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const templateBasePath = "templates"
|
||||
const templateFileExtension = "tmpl"
|
||||
|
||||
var templateCache = make(map[string]*CachedTemplate)
|
||||
|
||||
// CachedTemplate represents a cached template
|
||||
type CachedTemplate struct {
|
||||
templateName string
|
||||
templateContent *template.Template
|
||||
}
|
||||
|
||||
// GetTemplate returns a cached template instance according to the template name
|
||||
func GetTemplate(templateName string) (*template.Template, error) {
|
||||
fullPath := filepath.Join(templateBasePath, fmt.Sprintf("%s.%s", templateName, templateFileExtension))
|
||||
|
||||
cachedTemplate, exists := templateCache[templateName]
|
||||
|
||||
if exists {
|
||||
return cachedTemplate.templateContent, nil
|
||||
}
|
||||
|
||||
tmpl, err := template.ParseFiles(fullPath)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
templateCache[templateName] = &CachedTemplate{
|
||||
templateName: templateName,
|
||||
templateContent: tmpl,
|
||||
}
|
||||
|
||||
return tmpl, err
|
||||
}
|
||||
Reference in New Issue
Block a user