feat: add apis for sending email verification, and verifying an email code

This commit is contained in:
Derrick Hammer 2024-02-26 08:14:30 -05:00
parent 25ebb00765
commit 8965395fdf
Signed by: pcfreak30
GPG Key ID: C997C339BE476FF2
2 changed files with 91 additions and 1 deletions

View File

@ -2,9 +2,12 @@ package account
import ( import (
"crypto/ed25519" "crypto/ed25519"
"crypto/rand"
"errors" "errors"
"time" "time"
"git.lumeweb.com/LumeWeb/portal/mailer"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
"git.lumeweb.com/LumeWeb/portal/config" "git.lumeweb.com/LumeWeb/portal/config"
@ -24,6 +27,7 @@ type AccountServiceParams struct {
Db *gorm.DB Db *gorm.DB
Config *config.Manager Config *config.Manager
Identity ed25519.PrivateKey Identity ed25519.PrivateKey
Mailer *mailer.Mailer
} }
var Module = fx.Module("account", var Module = fx.Module("account",
@ -36,10 +40,11 @@ type AccountServiceDefault struct {
db *gorm.DB db *gorm.DB
config *config.Manager config *config.Manager
identity ed25519.PrivateKey identity ed25519.PrivateKey
mailer *mailer.Mailer
} }
func NewAccountService(params AccountServiceParams) *AccountServiceDefault { func NewAccountService(params AccountServiceParams) *AccountServiceDefault {
return &AccountServiceDefault{db: params.Db, config: params.Config, identity: params.Identity} return &AccountServiceDefault{db: params.Db, config: params.Config, identity: params.Identity, mailer: params.Mailer}
} }
func (s *AccountServiceDefault) EmailExists(email string) (bool, *models.User, error) { func (s *AccountServiceDefault) EmailExists(email string) (bool, *models.User, error) {
@ -96,6 +101,66 @@ func (s *AccountServiceDefault) CreateAccount(email string, password string) (*m
return &user, nil return &user, nil
} }
func (s *AccountServiceDefault) SendEmailVerification(user *models.User) error {
token := GenerateSecurityToken()
var verification models.EmailVerification
verification.UserID = user.ID
verification.Token = token
verification.ExpiresAt = time.Now().Add(time.Hour)
err := s.db.Create(&verification).Error
if err != nil {
return NewAccountError(ErrKeyDatabaseOperationFailed, err)
}
vars := map[string]interface{}{
"FirstName": user.FirstName,
"Email": user.Email,
"VerificationCode": token,
"ExpireTime": verification.ExpiresAt,
"PortalName": s.config.Config().Core.PortalName,
}
return s.mailer.TemplateSend(mailer.TPL_VERIFY_EMAIL, vars, vars, user.Email)
}
func (s AccountServiceDefault) VerifyEmail(email string, token string) error {
var verification models.EmailVerification
verification.Token = token
result := s.db.Model(&verification).
Preload("User").
Where(&verification).
First(&verification)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return NewAccountError(ErrKeyUserNotFound, result.Error)
}
return NewAccountError(ErrKeyDatabaseOperationFailed, result.Error)
}
if verification.ExpiresAt.Before(time.Now()) {
return NewAccountError(ErrKeySecurityTokenExpired, nil)
}
if verification.User.Email != email {
return NewAccountError(ErrKeySecurityInvalidToken, nil)
}
err := s.updateAccountInfo(verification.UserID, models.User{Verified: true})
if err != nil {
return err
}
return nil
}
func (s AccountServiceDefault) UpdateAccountName(userId uint, firstName string, lastName string) error { func (s AccountServiceDefault) UpdateAccountName(userId uint, firstName string, lastName string) error {
return s.updateAccountInfo(userId, models.User{FirstName: firstName, LastName: lastName}) return s.updateAccountInfo(userId, models.User{FirstName: firstName, LastName: lastName})
} }
@ -378,6 +443,19 @@ func (s AccountServiceDefault) DNSLinkExists(hash []byte) (bool, *models.DNSLink
return true, model.(*models.DNSLink), nil return true, model.(*models.DNSLink), nil
} }
func GenerateSecurityToken() string {
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, 6)
_, err := rand.Read(b)
if err != nil {
panic(err)
}
for i := 0; i < 6; i++ {
b[i] = charset[b[i]%byte(len(charset))]
}
return string(b)
}
func (s AccountServiceDefault) doLogin(user *models.User, ip string) (string, error) { func (s AccountServiceDefault) doLogin(user *models.User, ip string) (string, error) {
purpose := JWTPurposeLogin purpose := JWTPurposeLogin

View File

@ -45,6 +45,10 @@ const (
// General errors // General errors
ErrKeyDatabaseOperationFailed = "ErrDatabaseOperationFailed" ErrKeyDatabaseOperationFailed = "ErrDatabaseOperationFailed"
// Security token errors
ErrKeySecurityTokenExpired = "ErrSecurityTokenExpired"
ErrKeySecurityInvalidToken = "ErrSecurityInvalidToken"
) )
var defaultErrorMessages = map[string]string{ var defaultErrorMessages = map[string]string{
@ -87,6 +91,10 @@ var defaultErrorMessages = map[string]string{
// General errors // General errors
ErrKeyDatabaseOperationFailed: "A database operation failed.", ErrKeyDatabaseOperationFailed: "A database operation failed.",
// Security token errors
ErrKeySecurityTokenExpired: "The security token has expired.",
ErrKeySecurityInvalidToken: "The security token is invalid.",
} }
var ( var (
@ -130,6 +138,10 @@ var (
// General errors // General errors
ErrKeyDatabaseOperationFailed: http.StatusInternalServerError, ErrKeyDatabaseOperationFailed: http.StatusInternalServerError,
ErrKeyHashingFailed: http.StatusInternalServerError, ErrKeyHashingFailed: http.StatusInternalServerError,
// Security token errors
ErrKeySecurityTokenExpired: http.StatusUnauthorized,
ErrKeySecurityInvalidToken: http.StatusUnauthorized,
} }
) )