support reset password by email reset link

This commit is contained in:
MaysWind
2023-08-26 23:37:02 +08:00
parent c66bc62c41
commit f31ef1649f
42 changed files with 1298 additions and 30 deletions
+126
View File
@@ -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
}
+3 -2
View File
@@ -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
+1
View File
@@ -14,6 +14,7 @@ const (
SystemSubcategoryDefault = 0
SystemSubcategorySetting = 1
SystemSubcategoryDatabase = 2
SystemSubcategoryMail = 3
)
// Sub categories of normal error
+9
View File
@@ -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
View File
@@ -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")
)
+3
View File
@@ -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")
)
+24
View File
@@ -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
}
+15
View File
@@ -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
}
+11
View File
@@ -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.",
},
}
+7
View File
@@ -0,0 +1,7 @@
package locales
// LocaleInfo represents locale info
type LocaleInfo struct {
Aliases []string
Content *LocaleTextItems
}
+11
View File
@@ -0,0 +1,11 @@
package locales
var zhHans = &LocaleTextItems{
ForgetPasswordMailTextItems: &ForgetPasswordMailTextItems{
Title: "重置密码",
SalutationFormat: "%s 你好,",
DescriptionAboveBtn: "我们刚才收到重置您密码的请求。您可以点击下方链接重置您的密码。",
ResetPassword: "重置密码",
DescriptionBelowBtnFormat: "如果您没有请求重置密码,请直接忽略本邮件。如果您无法点击上述链接,请复制下方的地址然后在您的浏览器中粘贴。重置密码链接将在 %v 分钟后过期。",
},
}
+63
View File
@@ -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
}
+8
View File
@@ -0,0 +1,8 @@
package mail
// MailMessage represents an email entity
type MailMessage struct {
To string
Subject string
Body string
}
+6
View File
@@ -0,0 +1,6 @@
package mail
// Mailer is email sender interface
type Mailer interface {
SendMail(message *MailMessage) error
}
+37
View File
@@ -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)
}
+19
View File
@@ -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)),
}
+12
View File
@@ -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"`
}
+16
View File
@@ -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
+81
View File
@@ -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
}
+5
View File
@@ -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
View File
@@ -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 = ""
+42
View File
@@ -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
}