mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-17 00:12:11 +08:00
add email verification
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
)
|
||||
|
||||
// AuthorizationsApi represents authorization api
|
||||
@@ -48,6 +49,13 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (interface{}, *err
|
||||
return nil, errs.ErrUserIsDisabled
|
||||
}
|
||||
|
||||
if settings.Container.Current.EnableUserForceVerifyEmail && !user.EmailVerified {
|
||||
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because user has not verified email", credential.LoginName)
|
||||
return nil, errs.NewErrorWithContext(errs.ErrEmailIsNotVerified, map[string]string{
|
||||
"email": user.Email,
|
||||
})
|
||||
}
|
||||
|
||||
err = a.users.UpdateUserLastLoginTime(c, user.Uid)
|
||||
|
||||
if err != nil {
|
||||
@@ -121,6 +129,16 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (interfac
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.Disabled {
|
||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||
return nil, errs.ErrUserIsDisabled
|
||||
}
|
||||
|
||||
if settings.Container.Current.EnableUserForceVerifyEmail && !user.EmailVerified {
|
||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" has not verified email", user.Uid)
|
||||
return nil, errs.ErrEmailIsNotVerified
|
||||
}
|
||||
|
||||
oldTokenClaims := c.GetTokenClaims()
|
||||
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
|
||||
|
||||
@@ -173,6 +191,16 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.Disabled {
|
||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||
return nil, errs.ErrUserIsDisabled
|
||||
}
|
||||
|
||||
if settings.Container.Current.EnableUserForceVerifyEmail && !user.EmailVerified {
|
||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" has not verified email", user.Uid)
|
||||
return nil, errs.ErrEmailIsNotVerified
|
||||
}
|
||||
|
||||
err = a.twoFactorAuthorizations.GetAndUseUserTwoFactorRecoveryCode(c, uid, credential.RecoveryCode, user.Salt)
|
||||
|
||||
if err != nil {
|
||||
@@ -205,8 +233,9 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont
|
||||
|
||||
func (a *AuthorizationsApi) getAuthResponse(token string, need2FA bool, user *models.User) *models.AuthResponse {
|
||||
return &models.AuthResponse{
|
||||
Token: token,
|
||||
Need2FA: need2FA,
|
||||
User: user.ToUserBasicInfo(),
|
||||
Token: token,
|
||||
Need2FA: need2FA,
|
||||
NeedVerifyEmail: false,
|
||||
User: user.ToUserBasicInfo(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
)
|
||||
|
||||
// ForgetPasswordsApi represents user forget password api
|
||||
@@ -51,7 +52,7 @@ func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.Context) (
|
||||
return nil, errs.ErrUserIsDisabled
|
||||
}
|
||||
|
||||
if !user.EmailVerified {
|
||||
if settings.Container.Current.ForgetPasswordRequireVerifyEmail && !user.EmailVerified {
|
||||
log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] user \"uid:%d\" has not verified email", user.Uid)
|
||||
return nil, errs.ErrEmailIsNotVerified
|
||||
}
|
||||
@@ -99,7 +100,7 @@ func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.Context) (interfac
|
||||
return nil, errs.ErrUserIsDisabled
|
||||
}
|
||||
|
||||
if !user.EmailVerified {
|
||||
if settings.Container.Current.ForgetPasswordRequireVerifyEmail && !user.EmailVerified {
|
||||
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] user \"uid:%d\" has not verified email", user.Uid)
|
||||
return nil, errs.ErrEmailIsNotVerified
|
||||
}
|
||||
|
||||
+152
-2
@@ -73,8 +73,13 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (interface{}, *errs.Erro
|
||||
log.InfofWithRequestId(c, "[users.UserRegisterHandler] user \"%s\" has registered successfully, uid is %d", user.Username, user.Uid)
|
||||
|
||||
authResp := &models.AuthResponse{
|
||||
Need2FA: false,
|
||||
User: user.ToUserBasicInfo(),
|
||||
Need2FA: false,
|
||||
NeedVerifyEmail: settings.Container.Current.EnableUserForceVerifyEmail,
|
||||
User: user.ToUserBasicInfo(),
|
||||
}
|
||||
|
||||
if authResp.NeedVerifyEmail {
|
||||
return authResp, nil
|
||||
}
|
||||
|
||||
token, claims, err := a.tokens.CreateToken(c, user)
|
||||
@@ -93,6 +98,69 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (interface{}, *errs.Erro
|
||||
return authResp, nil
|
||||
}
|
||||
|
||||
// UserEmailVerifyHandler sets user email address verified
|
||||
func (a *UsersApi) UserEmailVerifyHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||
var userVerifyEmailReq models.UserVerifyEmailRequest
|
||||
err := c.ShouldBindJSON(&userVerifyEmailReq)
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
user, err := a.users.GetUserById(c, uid)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.ErrorfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.Disabled {
|
||||
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||
return nil, errs.ErrUserIsDisabled
|
||||
}
|
||||
|
||||
if user.EmailVerified {
|
||||
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" email has been verified", user.Uid)
|
||||
return nil, errs.ErrEmailIsVerified
|
||||
}
|
||||
|
||||
err = a.users.SetUserEmailVerified(c, user.Username)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to update user \"uid:%d\" email address verified, because %s", user.Uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
err = a.tokens.DeleteTokensByTypeBeforeTime(c, uid, core.USER_TOKEN_TYPE_EMAIL_VERIFY, now)
|
||||
|
||||
if err == nil {
|
||||
log.InfofWithRequestId(c, "[users.UserEmailVerifyHandler] revoke old email verify tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
|
||||
} else {
|
||||
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to revoke old email verify tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
}
|
||||
|
||||
resp := &models.UserVerifyEmailResponse{}
|
||||
|
||||
if userVerifyEmailReq.RequestNewToken {
|
||||
token, claims, err := a.tokens.CreateToken(c, user)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.NewToken = token
|
||||
resp.User = user.ToUserBasicInfo()
|
||||
c.SetTextualToken(token)
|
||||
c.SetTokenClaims(claims)
|
||||
|
||||
log.InfofWithRequestId(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" token created, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// UserProfileHandler returns user profile of current user
|
||||
func (a *UsersApi) UserProfileHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
@@ -283,3 +351,85 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// UserSendVerifyEmailByUnloginUserHandler sends unlogin user verify email
|
||||
func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||
var userResendVerifyEmailReq models.UserResendVerifyEmailRequest
|
||||
err := c.ShouldBindJSON(&userResendVerifyEmailReq)
|
||||
|
||||
user, err := a.users.GetUserByEmail(c, userResendVerifyEmailReq.Email)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if !a.users.IsPasswordEqualsUserPassword(userResendVerifyEmailReq.Password, user) {
|
||||
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] request password not equals to the user password")
|
||||
return nil, errs.ErrUserPasswordWrong
|
||||
}
|
||||
|
||||
if user.Disabled {
|
||||
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||
return nil, errs.ErrUserIsDisabled
|
||||
}
|
||||
|
||||
if user.EmailVerified {
|
||||
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] user \"uid:%d\" email has been verified", user.Uid)
|
||||
return nil, errs.ErrEmailIsVerified
|
||||
}
|
||||
|
||||
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.ErrTokenGenerating
|
||||
}
|
||||
|
||||
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// UserSendVerifyEmailByLoginedUserHandler sends logined user verify email
|
||||
func (a *UsersApi) UserSendVerifyEmailByLoginedUserHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
user, err := a.users.GetUserById(c, uid)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.EmailVerified {
|
||||
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] user \"uid:%d\" email has been verified", user.Uid)
|
||||
return nil, errs.ErrEmailIsVerified
|
||||
}
|
||||
|
||||
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.ErrTokenGenerating
|
||||
}
|
||||
|
||||
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
+39
-2
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
||||
)
|
||||
|
||||
@@ -177,9 +178,9 @@ func (l *UserDataCli) SendPasswordResetMail(c *cli.Context, username string) err
|
||||
return err
|
||||
}
|
||||
|
||||
if !user.EmailVerified {
|
||||
if settings.Container.Current.ForgetPasswordRequireVerifyEmail && !user.EmailVerified {
|
||||
log.BootWarnf("[user_data.SendPasswordResetMail] user \"uid:%d\" has not verified email", user.Uid)
|
||||
return errs.ErrEmptyIsNotVerified
|
||||
return errs.ErrEmailIsNotVerified
|
||||
}
|
||||
|
||||
token, _, err := l.tokens.CreatePasswordResetToken(nil, user)
|
||||
@@ -233,6 +234,42 @@ func (l *UserDataCli) DisableUser(c *cli.Context, username string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResendVerifyEmail resends an email with account activation link
|
||||
func (l *UserDataCli) ResendVerifyEmail(c *cli.Context, username string) error {
|
||||
if username == "" {
|
||||
log.BootErrorf("[user_data.ResendVerifyEmail] user name is empty")
|
||||
return errs.ErrUsernameIsEmpty
|
||||
}
|
||||
|
||||
user, err := l.users.GetUserByUsername(nil, username)
|
||||
|
||||
if err != nil {
|
||||
log.BootErrorf("[user_data.ResendVerifyEmail] failed to get user by user name \"%s\", because %s", username, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if user.EmailVerified {
|
||||
log.BootWarnf("[user_data.ResendVerifyEmail] user \"uid:%d\" email has been verified", user.Uid)
|
||||
return errs.ErrEmailIsVerified
|
||||
}
|
||||
|
||||
token, _, err := l.tokens.CreateEmailVerifyToken(nil, user)
|
||||
|
||||
if err != nil {
|
||||
log.BootErrorf("[user_data.ResendVerifyEmail] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return errs.ErrTokenGenerating
|
||||
}
|
||||
|
||||
err = l.users.SendVerifyEmail(user, token, "")
|
||||
|
||||
if err != nil {
|
||||
log.BootErrorf("[user_data.ResendVerifyEmail] cannot send email to \"%s\", because %s", user.Email, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetUserEmailVerified sets user email address verified
|
||||
func (l *UserDataCli) SetUserEmailVerified(c *cli.Context, username string) error {
|
||||
if username == "" {
|
||||
|
||||
@@ -13,6 +13,7 @@ type TokenType byte
|
||||
const (
|
||||
USER_TOKEN_TYPE_NORMAL TokenType = 1
|
||||
USER_TOKEN_TYPE_REQUIRE_2FA TokenType = 2
|
||||
USER_TOKEN_TYPE_EMAIL_VERIFY TokenType = 3
|
||||
USER_TOKEN_TYPE_PASSWORD_RESET TokenType = 4
|
||||
)
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ type Error struct {
|
||||
HttpStatusCode int
|
||||
Message string
|
||||
BaseError []error
|
||||
Context interface{}
|
||||
}
|
||||
|
||||
// Error returns the error message
|
||||
@@ -81,6 +82,19 @@ func NewIncompleteOrIncorrectSubmissionError(err error) *Error {
|
||||
ErrIncompleteOrIncorrectSubmission.Message, err)
|
||||
}
|
||||
|
||||
// NewErrorWithContext returns a new error instance with specified context
|
||||
func NewErrorWithContext(baseError *Error, context interface{}) *Error {
|
||||
return &Error{
|
||||
Category: baseError.Category,
|
||||
SubCategory: baseError.SubCategory,
|
||||
Index: baseError.Index,
|
||||
HttpStatusCode: baseError.HttpStatusCode,
|
||||
Message: baseError.Message,
|
||||
BaseError: baseError.BaseError,
|
||||
Context: context,
|
||||
}
|
||||
}
|
||||
|
||||
// Or would return the error from err parameter if the this error is defined in this project,
|
||||
// or return the default error
|
||||
func Or(err error, defaultErr *Error) *Error {
|
||||
|
||||
@@ -19,5 +19,6 @@ var (
|
||||
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")
|
||||
ErrEmailVerifyTokenIsInvalidOrExpired = NewNormalError(NormalSubcategoryToken, 13, http.StatusBadRequest, "email verify token is invalid or expired")
|
||||
ErrPasswordResetTokenIsInvalidOrExpired = NewNormalError(NormalSubcategoryToken, 14, http.StatusBadRequest, "password reset token is invalid or expired")
|
||||
)
|
||||
|
||||
@@ -27,4 +27,5 @@ var (
|
||||
ErrEmailIsEmptyOrInvalid = NewNormalError(NormalSubcategoryUser, 18, http.StatusBadRequest, "email is empty or invalid")
|
||||
ErrNewPasswordEqualsOldInvalid = NewNormalError(NormalSubcategoryUser, 19, http.StatusBadRequest, "new password equals old password")
|
||||
ErrEmailIsNotVerified = NewNormalError(NormalSubcategoryUser, 20, http.StatusBadRequest, "email is not verified")
|
||||
ErrEmailIsVerified = NewNormalError(NormalSubcategoryUser, 21, http.StatusBadRequest, "email is verified")
|
||||
)
|
||||
|
||||
@@ -2,9 +2,19 @@ package locales
|
||||
|
||||
// LocaleTextItems represents all text items need to be translated
|
||||
type LocaleTextItems struct {
|
||||
VerifyEmailTextItems *VerifyEmailTextItems
|
||||
ForgetPasswordMailTextItems *ForgetPasswordMailTextItems
|
||||
}
|
||||
|
||||
// VerifyEmailTextItems represents text items need to be translated in verify mail
|
||||
type VerifyEmailTextItems struct {
|
||||
Title string
|
||||
SalutationFormat string
|
||||
DescriptionAboveBtn string
|
||||
VerifyEmail string
|
||||
DescriptionBelowBtnFormat string
|
||||
}
|
||||
|
||||
// ForgetPasswordMailTextItems represents text items need to be translated in forget password mail
|
||||
type ForgetPasswordMailTextItems struct {
|
||||
Title string
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
package locales
|
||||
|
||||
var en = &LocaleTextItems{
|
||||
VerifyEmailTextItems: &VerifyEmailTextItems{
|
||||
Title: "Verify Email",
|
||||
SalutationFormat: "Hi %s,",
|
||||
DescriptionAboveBtn: "Please click the link below to confirm your email address.",
|
||||
VerifyEmail: "Verify Email",
|
||||
DescriptionBelowBtnFormat: "If you did not sign up for %s account, please simply disregard this email. If you cannot click the link above, please copy the above url and paste it into your browser. The verify email link will be expired after %v minutes.",
|
||||
},
|
||||
ForgetPasswordMailTextItems: &ForgetPasswordMailTextItems{
|
||||
Title: "Reset Your Password",
|
||||
SalutationFormat: "Hi %s,",
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
package locales
|
||||
|
||||
var zhHans = &LocaleTextItems{
|
||||
VerifyEmailTextItems: &VerifyEmailTextItems{
|
||||
Title: "验证邮箱",
|
||||
SalutationFormat: "%s 您好,",
|
||||
DescriptionAboveBtn: "请点击下方的链接确认您的邮箱地址。",
|
||||
VerifyEmail: "验证邮箱",
|
||||
DescriptionBelowBtnFormat: "如果您没有注册 %s 账户,请直接忽略本邮件。如果您无法点击上述链接,请复制下方的地址然后在您的浏览器中粘贴。邮箱验证链接将在 %v 分钟后过期。",
|
||||
},
|
||||
ForgetPasswordMailTextItems: &ForgetPasswordMailTextItems{
|
||||
Title: "重置密码",
|
||||
SalutationFormat: "%s 你好,",
|
||||
SalutationFormat: "%s 您好,",
|
||||
DescriptionAboveBtn: "我们刚才收到重置您密码的请求。您可以点击下方链接重置您的密码。",
|
||||
ResetPassword: "重置密码",
|
||||
DescriptionBelowBtnFormat: "如果您没有请求重置密码,请直接忽略本邮件。如果您无法点击上述链接,请复制下方的地址然后在您的浏览器中粘贴。重置密码链接将在 %v 分钟后过期。",
|
||||
|
||||
@@ -56,6 +56,25 @@ func JWTTwoFactorAuthorization(c *core.Context) {
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// JWTEmailVerifyAuthorization verifies whether current request is email verification
|
||||
func JWTEmailVerifyAuthorization(c *core.Context) {
|
||||
claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_ARGUMENT)
|
||||
|
||||
if err != nil {
|
||||
utils.PrintJsonErrorResult(c, errs.ErrEmailVerifyTokenIsInvalidOrExpired)
|
||||
return
|
||||
}
|
||||
|
||||
if claims.Type != core.USER_TOKEN_TYPE_EMAIL_VERIFY {
|
||||
log.WarnfWithRequestId(c, "[authorization.JWTEmailVerifyAuthorization] user \"uid:%d\" token is not for email verification", claims.Uid)
|
||||
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidToken)
|
||||
return
|
||||
}
|
||||
|
||||
c.SetTokenClaims(claims)
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// JWTResetPasswordAuthorization verifies whether current request is password reset
|
||||
func JWTResetPasswordAuthorization(c *core.Context) {
|
||||
claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_ARGUMENT)
|
||||
|
||||
@@ -18,6 +18,7 @@ func ServerSettingsCookie(config *settings.Config) core.MiddlewareHandlerFunc {
|
||||
settingsArr := []string{
|
||||
buildBooleanSetting("r", config.EnableUserRegister),
|
||||
buildBooleanSetting("f", config.EnableUserForgetPassword),
|
||||
buildBooleanSetting("v", config.EnableUserVerifyEmail),
|
||||
buildBooleanSetting("e", config.EnableDataExport),
|
||||
buildStringSetting("m", strings.Replace(config.MapProvider, "_", "-", -1)),
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ package models
|
||||
|
||||
// AuthResponse returns a view-object of user authorization
|
||||
type AuthResponse struct {
|
||||
Token string `json:"token"`
|
||||
Need2FA bool `json:"need2FA"`
|
||||
User *UserBasicInfo `json:"user"`
|
||||
Token string `json:"token"`
|
||||
Need2FA bool `json:"need2FA"`
|
||||
NeedVerifyEmail bool `json:"needVerifyEmail"`
|
||||
User *UserBasicInfo `json:"user"`
|
||||
}
|
||||
|
||||
@@ -108,6 +108,23 @@ type UserRegisterRequest struct {
|
||||
FirstDayOfWeek WeekDay `json:"firstDayOfWeek" binding:"min=0,max=6"`
|
||||
}
|
||||
|
||||
// UserVerifyEmailRequest represents all parameters of user verify email request
|
||||
type UserVerifyEmailRequest struct {
|
||||
RequestNewToken bool `json:"requestNewToken" binding:"omitempty"`
|
||||
}
|
||||
|
||||
// UserVerifyEmailResponse represents all response parameters after user have verified email
|
||||
type UserVerifyEmailResponse struct {
|
||||
NewToken string `json:"newToken,omitempty"`
|
||||
User *UserBasicInfo `json:"user"`
|
||||
}
|
||||
|
||||
// UserResendVerifyEmailRequest represents all parameters of user resend verify email request
|
||||
type UserResendVerifyEmailRequest struct {
|
||||
Email string `json:"email" binding:"omitempty,max=100,validEmail"`
|
||||
Password string `json:"password" binding:"omitempty,min=6,max=128"`
|
||||
}
|
||||
|
||||
// UserProfileUpdateRequest represents all parameters of user updating profile request
|
||||
type UserProfileUpdateRequest struct {
|
||||
Email string `json:"email" binding:"omitempty,notBlank,max=100,validEmail"`
|
||||
@@ -147,6 +164,7 @@ type UserProfileResponse struct {
|
||||
ShortDateFormat ShortDateFormat `json:"shortDateFormat"`
|
||||
LongTimeFormat LongTimeFormat `json:"longTimeFormat"`
|
||||
ShortTimeFormat ShortTimeFormat `json:"shortTimeFormat"`
|
||||
EmailVerified bool `json:"emailVerified"`
|
||||
LastLoginAt int64 `json:"lastLoginAt"`
|
||||
}
|
||||
|
||||
@@ -229,6 +247,7 @@ func (u *User) ToUserProfileResponse() *UserProfileResponse {
|
||||
ShortDateFormat: u.ShortDateFormat,
|
||||
LongTimeFormat: u.LongTimeFormat,
|
||||
ShortTimeFormat: u.ShortTimeFormat,
|
||||
EmailVerified: u.EmailVerified,
|
||||
LastLoginAt: u.LastLoginUnixTime,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func (s *ForgetPasswordService) SendPasswordResetEmail(c *core.Context, user *mo
|
||||
expireTimeInMinutes := s.CurrentConfig().PasswordResetTokenExpiredTimeDuration.Minutes()
|
||||
passwordResetUrl := fmt.Sprintf(passwordResetUrlFormat, s.CurrentConfig().RootUrl, url.QueryEscape(passwordResetToken))
|
||||
|
||||
tmpl, err := templates.GetTemplate("email/password_reset")
|
||||
tmpl, err := templates.GetTemplate(templates.TEMPLATE_PASSWORD_RESET)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -88,6 +88,11 @@ func (s *TokenService) CreateRequire2FAToken(c *core.Context, user *models.User)
|
||||
return s.createToken(c, user, core.USER_TOKEN_TYPE_REQUIRE_2FA, s.getUserAgent(c), s.CurrentConfig().TemporaryTokenExpiredTimeDuration)
|
||||
}
|
||||
|
||||
// CreateEmailVerifyToken generates a new email verify token and saves to database
|
||||
func (s *TokenService) CreateEmailVerifyToken(c *core.Context, user *models.User) (string, *core.UserTokenClaims, error) {
|
||||
return s.createToken(c, user, core.USER_TOKEN_TYPE_EMAIL_VERIFY, s.getUserAgent(c), s.CurrentConfig().EmailVerifyTokenExpiredTimeDuration)
|
||||
}
|
||||
|
||||
// CreatePasswordResetToken generates a new password reset token and saves to database
|
||||
func (s *TokenService) CreatePasswordResetToken(c *core.Context, user *models.User) (string, *core.UserTokenClaims, error) {
|
||||
return s.createToken(c, user, core.USER_TOKEN_TYPE_PASSWORD_RESET, s.getUserAgent(c), s.CurrentConfig().PasswordResetTokenExpiredTimeDuration)
|
||||
@@ -165,6 +170,18 @@ func (s *TokenService) DeleteTokensBeforeTime(c *core.Context, uid int64, expire
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteTokensByTypeBeforeTime deletes tokens that is specified type and created before specific time
|
||||
func (s *TokenService) DeleteTokensByTypeBeforeTime(c *core.Context, uid int64, tokenType core.TokenType, expireTime int64) error {
|
||||
if uid <= 0 {
|
||||
return errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
return s.TokenDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||
_, err := sess.Where("uid=? AND token_type=? AND created_unix_time<?", uid, tokenType, expireTime).Delete(&models.TokenRecord{})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// ParseFromTokenId returns token model according to token id
|
||||
func (s *TokenService) ParseFromTokenId(tokenId string) (*models.TokenRecord, error) {
|
||||
pairs := strings.Split(tokenId, ":")
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"xorm.io/xorm"
|
||||
@@ -8,14 +11,22 @@ import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/datastore"
|
||||
"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"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/uuid"
|
||||
)
|
||||
|
||||
const verifyEmailUrlFormat = "%sdesktop/#/verify_email?token=%s"
|
||||
|
||||
// UserService represents user service
|
||||
type UserService struct {
|
||||
ServiceUsingDB
|
||||
ServiceUsingConfig
|
||||
ServiceUsingMailer
|
||||
ServiceUsingUuid
|
||||
}
|
||||
|
||||
@@ -25,6 +36,12 @@ var (
|
||||
ServiceUsingDB: ServiceUsingDB{
|
||||
container: datastore.Container,
|
||||
},
|
||||
ServiceUsingConfig: ServiceUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
ServiceUsingMailer: ServiceUsingMailer{
|
||||
container: mail.Container,
|
||||
},
|
||||
ServiceUsingUuid: ServiceUsingUuid{
|
||||
container: uuid.Container,
|
||||
},
|
||||
@@ -390,6 +407,60 @@ func (s *UserService) ExistsEmail(c *core.Context, email string) (bool, error) {
|
||||
return s.UserDB().NewSession(c).Cols("email").Where("email=? AND deleted=?", email, false).Exist(&models.User{})
|
||||
}
|
||||
|
||||
// SendVerifyEmail sends verify email according to specified parameters
|
||||
func (s *UserService) SendVerifyEmail(user *models.User, verifyEmailToken string, backupLocale string) error {
|
||||
if !s.CurrentConfig().EnableSMTP {
|
||||
return errs.ErrSMTPServerNotEnabled
|
||||
}
|
||||
|
||||
locale := user.Language
|
||||
|
||||
if locale == "" {
|
||||
locale = backupLocale
|
||||
}
|
||||
|
||||
localeTextItems := locales.GetLocaleTextItems(locale)
|
||||
verifyEmailTextItems := localeTextItems.VerifyEmailTextItems
|
||||
|
||||
expireTimeInMinutes := s.CurrentConfig().EmailVerifyTokenExpiredTimeDuration.Minutes()
|
||||
verifyEmailUrl := fmt.Sprintf(verifyEmailUrlFormat, s.CurrentConfig().RootUrl, url.QueryEscape(verifyEmailToken))
|
||||
|
||||
tmpl, err := templates.GetTemplate(templates.TEMPLATE_VERIFY_EMAIL)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
templateParams := map[string]interface{}{
|
||||
"AppName": s.CurrentConfig().AppName,
|
||||
"VerifyEmail": map[string]interface{}{
|
||||
"Title": verifyEmailTextItems.Title,
|
||||
"Salutation": fmt.Sprintf(verifyEmailTextItems.SalutationFormat, user.Nickname),
|
||||
"DescriptionAboveBtn": verifyEmailTextItems.DescriptionAboveBtn,
|
||||
"VerifyEmailUrl": verifyEmailUrl,
|
||||
"VerifyEmail": verifyEmailTextItems.VerifyEmail,
|
||||
"DescriptionBelowBtn": fmt.Sprintf(verifyEmailTextItems.DescriptionBelowBtnFormat, s.CurrentConfig().AppName, expireTimeInMinutes),
|
||||
},
|
||||
}
|
||||
|
||||
var bodyBuffer bytes.Buffer
|
||||
err = tmpl.Execute(&bodyBuffer, templateParams)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
message := &mail.MailMessage{
|
||||
To: user.Email,
|
||||
Subject: verifyEmailTextItems.Title,
|
||||
Body: bodyBuffer.String(),
|
||||
}
|
||||
|
||||
err = s.SendMail(message)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// IsPasswordEqualsUserPassword returns whether the given password is correct
|
||||
func (s *UserService) IsPasswordEqualsUserPassword(password string, user *models.User) bool {
|
||||
return user.Password == utils.EncodePassword(password, user.Salt)
|
||||
|
||||
+15
-3
@@ -116,6 +116,7 @@ const (
|
||||
defaultSecretKey string = "ezbookkeeping"
|
||||
defaultTokenExpiredTime uint32 = 604800 // 7 days
|
||||
defaultTemporaryTokenExpiredTime uint32 = 300 // 5 minutes
|
||||
defaultEmailVerifyTokenExpiredTime uint32 = 3600 // 60 minutes
|
||||
defaultPasswordResetTokenExpiredTime uint32 = 3600 // 60 minutes
|
||||
|
||||
defaultExchangeRatesDataRequestTimeout uint32 = 10000 // 10 seconds
|
||||
@@ -200,14 +201,19 @@ type Config struct {
|
||||
TokenExpiredTimeDuration time.Duration
|
||||
TemporaryTokenExpiredTime uint32
|
||||
TemporaryTokenExpiredTimeDuration time.Duration
|
||||
EmailVerifyTokenExpiredTime uint32
|
||||
EmailVerifyTokenExpiredTimeDuration time.Duration
|
||||
PasswordResetTokenExpiredTime uint32
|
||||
PasswordResetTokenExpiredTimeDuration time.Duration
|
||||
EnableRequestIdHeader bool
|
||||
|
||||
// User
|
||||
EnableUserRegister bool
|
||||
EnableUserForgetPassword bool
|
||||
AvatarProvider string
|
||||
EnableUserRegister bool
|
||||
EnableUserVerifyEmail bool
|
||||
EnableUserForceVerifyEmail bool
|
||||
EnableUserForgetPassword bool
|
||||
ForgetPasswordRequireVerifyEmail bool
|
||||
AvatarProvider string
|
||||
|
||||
// Data
|
||||
EnableDataExport bool
|
||||
@@ -481,6 +487,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.EmailVerifyTokenExpiredTime = getConfigItemUint32Value(configFile, sectionName, "email_verify_token_expired_time", defaultEmailVerifyTokenExpiredTime)
|
||||
config.EmailVerifyTokenExpiredTimeDuration = time.Duration(config.EmailVerifyTokenExpiredTime) * time.Second
|
||||
|
||||
config.PasswordResetTokenExpiredTime = getConfigItemUint32Value(configFile, sectionName, "password_reset_token_expired_time", defaultPasswordResetTokenExpiredTime)
|
||||
config.PasswordResetTokenExpiredTimeDuration = time.Duration(config.PasswordResetTokenExpiredTime) * time.Second
|
||||
|
||||
@@ -491,7 +500,10 @@ 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.EnableUserVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "enable_email_verify", false)
|
||||
config.EnableUserForceVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "enable_force_email_verify", false)
|
||||
config.EnableUserForgetPassword = getConfigItemBoolValue(configFile, sectionName, "enable_forget_password", false)
|
||||
config.ForgetPasswordRequireVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "forget_password_require_email_verify", false)
|
||||
|
||||
if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == "" {
|
||||
config.AvatarProvider = ""
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package templates
|
||||
|
||||
type KnownTemplate string
|
||||
|
||||
// Known templates
|
||||
const (
|
||||
TEMPLATE_VERIFY_EMAIL KnownTemplate = "email/verify_email"
|
||||
TEMPLATE_PASSWORD_RESET KnownTemplate = "email/password_reset"
|
||||
)
|
||||
@@ -9,16 +9,16 @@ import (
|
||||
const templateBasePath = "templates"
|
||||
const templateFileExtension = "tmpl"
|
||||
|
||||
var templateCache = make(map[string]*CachedTemplate)
|
||||
var templateCache = make(map[KnownTemplate]*CachedTemplate)
|
||||
|
||||
// CachedTemplate represents a cached template
|
||||
type CachedTemplate struct {
|
||||
templateName string
|
||||
templateName KnownTemplate
|
||||
templateContent *template.Template
|
||||
}
|
||||
|
||||
// GetTemplate returns a cached template instance according to the template name
|
||||
func GetTemplate(templateName string) (*template.Template, error) {
|
||||
func GetTemplate(templateName KnownTemplate) (*template.Template, error) {
|
||||
fullPath := filepath.Join(templateBasePath, fmt.Sprintf("%s.%s", templateName, templateFileExtension))
|
||||
|
||||
cachedTemplate, exists := templateCache[templateName]
|
||||
|
||||
+8
-2
@@ -45,12 +45,18 @@ func PrintJsonErrorResult(c *core.Context, err *errs.Error) {
|
||||
}
|
||||
}
|
||||
|
||||
c.AbortWithStatusJSON(err.HttpStatusCode, gin.H{
|
||||
result := gin.H{
|
||||
"success": false,
|
||||
"errorCode": err.Code(),
|
||||
"errorMessage": errorMessage,
|
||||
"path": c.Request.URL.Path,
|
||||
})
|
||||
}
|
||||
|
||||
if err.Context != nil {
|
||||
result["context"] = err.Context
|
||||
}
|
||||
|
||||
c.AbortWithStatusJSON(err.HttpStatusCode, result)
|
||||
}
|
||||
|
||||
// PrintDataErrorResult writes error response in custom content type to current http context
|
||||
|
||||
Reference in New Issue
Block a user