diff --git a/service/email/email.go b/service/email/email.go index 1084a4b..dbf9a2d 100644 --- a/service/email/email.go +++ b/service/email/email.go @@ -1,13 +1,17 @@ package email import ( + "bytes" "context" "crypto/rsa" "errors" "fmt" + "html/template" "math/rand" + "net/url" "time" + "github.com/golang-jwt/jwt/v5" "github.com/samber/lo" "github.com/wneessen/go-mail" "github.com/xmdhs/authlib-skin/config" @@ -76,29 +80,54 @@ func (e Email) SendEmail(ctx context.Context, to string, subject, body string) e return nil } -func (e Email) SendVerifyCode(ctx context.Context, email string, interval int) error { +var emailTemplate = lo.Must(template.New("email").Parse(`

{{ .msg }}

{{ .url }}`)) + +func (e Email) SendVerifyUrl(ctx context.Context, email string, interval int, host string) error { sendKey := []byte("SendEmail" + email) sendB, err := e.cache.Get(sendKey) if err != nil { - return fmt.Errorf("SendRegVerifyCode: %w", err) + return fmt.Errorf("SendVerifyUrl: %w", err) } - if sendB == nil { - return fmt.Errorf("SendRegVerifyCode: %w", ErrSendLimit) + if sendB != nil { + return fmt.Errorf("SendVerifyUrl: %w", ErrSendLimit) } err = e.cache.Put(sendKey, []byte{1}, time.Now().Add(time.Second*time.Duration(interval))) if err != nil { - return fmt.Errorf("SendRegVerifyCode: %w", err) + return fmt.Errorf("SendVerifyUrl: %w", err) } - code := lo.RandomString(8, append(lo.NumbersCharset, lo.UpperCaseLettersCharset...)) - - err = e.cache.Put([]byte("VerifyCode"+email), []byte(code), time.Now().Add(5*time.Minute)) + code, err := newJwtToken(e.pri, email) if err != nil { - return fmt.Errorf("SendRegVerifyCode: %w", err) + return fmt.Errorf("SendVerifyUrl: %w", err) } - err = e.SendEmail(ctx, email, "验证你的邮箱", fmt.Sprintf("验证码:%v,五分钟内有效", code)) + + u := url.URL{ + Host: host, + Scheme: "http", + Path: "/test?" + code, + } + + if e.config.WebBaseUrl != "" { + webBase, err := url.Parse(e.config.WebBaseUrl) + if err != nil { + return fmt.Errorf("SendVerifyUrl: %w", err) + } + u.Host = webBase.Host + u.Scheme = webBase.Scheme + } + + body := bytes.NewBuffer(nil) + err = emailTemplate.Execute(body, map[string]any{ + "msg": "点击下方链接验证你的邮箱,1 天内有效", + "url": u.String(), + }) if err != nil { - return fmt.Errorf("SendRegVerifyCode: %w", err) + return fmt.Errorf("SendVerifyUrl: %w", err) + } + + err = e.SendEmail(ctx, email, "验证你的邮箱", body.String()) + if err != nil { + return fmt.Errorf("SendVerifyUrl: %w", err) } return nil } @@ -106,20 +135,33 @@ func (e Email) SendVerifyCode(ctx context.Context, email string, interval int) e var ( ErrCodeNotValid = errors.New("验证码无效") ErrSendLimit = errors.New("邮件发送限制") + ErrTokenInvalid = errors.New("token 无效") ) -func (e Email) VerifyCode(email, code string) error { - key := []byte("VerifyCode" + email) - codeb, err := e.cache.Get(key) +func (e Email) VerifyJwt(email, jwtStr string) error { + token, err := jwt.ParseWithClaims(jwtStr, &jwt.RegisteredClaims{}, func(t *jwt.Token) (interface{}, error) { + return e.pri.PublicKey, nil + }) if err != nil { - return fmt.Errorf("VerifyCode: %w", err) + return fmt.Errorf("VerifyJwt: %w", err) } - if string(codeb) != code { - err := e.cache.Del(key) - if err != nil { - return fmt.Errorf("VerifyCode: %w", err) - } - return ErrCodeNotValid + sub, _ := token.Claims.GetSubject() + if !token.Valid || sub != email { + return fmt.Errorf("VerifyJwt: %w", ErrTokenInvalid) } return nil } + +func newJwtToken(jwtKey *rsa.PrivateKey, email string) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * 24 * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + Subject: email, + Issuer: "authlib-skin email verification", + }) + jwts, err := token.SignedString(jwtKey) + if err != nil { + return "", fmt.Errorf("newJwtToken: %w", err) + } + return jwts, nil +}