Email notification

This commit is contained in:
2024-01-29 16:11:59 +08:00
parent 20119cb177
commit 3c58cb8f0a
18 changed files with 280 additions and 127 deletions

View File

@ -1,22 +1,11 @@
package security
import (
"fmt"
"code.smartsheep.studio/hydrogen/passport/pkg/models"
"fmt"
"github.com/samber/lo"
)
func GetFactorCode(factor models.AuthFactor) (bool, error) {
switch factor.Type {
case models.EmailPasswordFactor:
// TODO
return true, nil
default:
return false, nil
}
}
func VerifyFactor(factor models.AuthFactor, code string) error {
switch factor.Type {
case models.PasswordAuthFactor:
@ -25,6 +14,12 @@ func VerifyFactor(factor models.AuthFactor, code string) error {
nil,
fmt.Errorf("invalid password"),
)
case models.EmailPasswordFactor:
return lo.Ternary(
code == factor.Secret,
nil,
fmt.Errorf("invalid verification code"),
)
}
return nil

View File

@ -4,7 +4,9 @@ import (
"code.smartsheep.studio/hydrogen/passport/pkg/database"
"code.smartsheep.studio/hydrogen/passport/pkg/models"
"code.smartsheep.studio/hydrogen/passport/pkg/services"
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/spf13/viper"
"gorm.io/gorm/clause"
)
@ -23,14 +25,23 @@ func getPrincipal(c *fiber.Ctx) error {
func doRegister(c *fiber.Ctx) error {
var data struct {
Name string `json:"name"`
Nick string `json:"nick"`
Email string `json:"email"`
Password string `json:"password"`
Name string `json:"name"`
Nick string `json:"nick"`
Email string `json:"email"`
Password string `json:"password"`
MagicToken string `json:"magic_token"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
} else if viper.GetBool("use_registration_magic_token") && len(data.MagicToken) <= 0 {
return fmt.Errorf("missing magic token in request")
}
if tk, err := services.ValidateMagicToken(data.MagicToken, models.RegistrationMagicToken); err != nil {
return err
} else {
database.C.Delete(&tk)
}
if user, err := services.CreateAccount(

View File

@ -1,7 +1,6 @@
package server
import (
"code.smartsheep.studio/hydrogen/passport/pkg/security"
"code.smartsheep.studio/hydrogen/passport/pkg/services"
"github.com/gofiber/fiber/v2"
)
@ -14,7 +13,7 @@ func requestFactorToken(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if sent, err := security.GetFactorCode(factor); err != nil {
if sent, err := services.GetFactorCode(factor); err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
} else if !sent {
return c.SendStatus(fiber.StatusNoContent)

View File

@ -7,7 +7,8 @@ import (
func getMetadata(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"name": viper.GetString("name"),
"domain": viper.GetString("domain"),
"name": viper.GetString("name"),
"domain": viper.GetString("domain"),
"open_registration": !viper.GetBool("use_registration_magic_token"),
})
}

View File

@ -5,6 +5,7 @@ import (
"code.smartsheep.studio/hydrogen/passport/pkg/models"
"code.smartsheep.studio/hydrogen/passport/pkg/security"
"fmt"
"github.com/google/uuid"
"github.com/samber/lo"
"gorm.io/datatypes"
"gorm.io/gorm"
@ -54,11 +55,16 @@ func CreateAccount(name, nick, email, password string) (models.Account, error) {
Type: models.PasswordAuthFactor,
Secret: security.HashPassword(password),
},
{
Type: models.EmailPasswordFactor,
Secret: uuid.NewString()[:8],
},
},
Contacts: []models.AccountContact{
{
Type: models.EmailAccountContact,
Content: email,
IsPrimary: true,
VerifiedAt: nil,
},
},
@ -80,14 +86,9 @@ func CreateAccount(name, nick, email, password string) (models.Account, error) {
}
func ConfirmAccount(code string) error {
var token models.MagicToken
if err := database.C.Where(&models.MagicToken{
Code: code,
Type: models.ConfirmMagicToken,
}).First(&token).Error; err != nil {
token, err := ValidateMagicToken(code, models.ConfirmMagicToken)
if err != nil {
return err
} else if token.AssignTo == nil {
return fmt.Errorf("account was not found")
}
var user models.Account

View File

@ -3,8 +3,26 @@ package services
import (
"code.smartsheep.studio/hydrogen/passport/pkg/database"
"code.smartsheep.studio/hydrogen/passport/pkg/models"
"fmt"
"github.com/google/uuid"
"github.com/spf13/viper"
)
const EmailPasswordTemplate = `Dear %s,
We hope this message finds you well.
As part of our ongoing commitment to ensuring the security of your account, we require you to complete the login process by entering the verification code below:
Your Login Verification Code: %s
Please use the provided code within the next 2 hours to complete your login.
If you did not request this code, please update your information, maybe your username or email has been leak.
Thank you for your cooperation in helping us maintain the security of your account.
Best regards,
%s`
func LookupFactor(id uint) (models.AuthFactor, error) {
var factor models.AuthFactor
err := database.C.Where(models.AuthFactor{
@ -22,3 +40,30 @@ func LookupFactorsByUser(uid uint) ([]models.AuthFactor, error) {
return factors, err
}
func GetFactorCode(factor models.AuthFactor) (bool, error) {
switch factor.Type {
case models.EmailPasswordFactor:
var user models.Account
if err := database.C.Where(&models.Account{
BaseModel: models.BaseModel{ID: factor.AccountID},
}).Preload("Contacts").First(&user).Error; err != nil {
return true, err
}
factor.Secret = uuid.NewString()[:8]
if err := database.C.Save(&factor).Error; err != nil {
return true, err
}
subject := fmt.Sprintf("[%s] Login verification code", viper.GetString("name"))
content := fmt.Sprintf(EmailPasswordTemplate, user.Name, factor.Secret, viper.GetString("maintainer"))
if err := SendMail(user.GetPrimaryEmail().Content, subject, content); err != nil {
return true, err
}
return true, nil
default:
return false, nil
}
}

View File

@ -10,6 +10,33 @@ import (
"time"
)
const ConfirmRegistrationTemplate = `Dear %s,
Thank you for choosing to register with %s. We are excited to welcome you to our community and appreciate your trust in us.
Your registration details have been successfully received, and you are now a valued member of %s. Here are the confirm link of your registration:
%s
As a confirmed registered member, you will have access to all our services.
We encourage you to explore our services and take full advantage of the resources available to you.
Once again, thank you for choosing us. We look forward to serving you and hope you have a positive experience with us.
Best regards,
%s`
func ValidateMagicToken(code string, mode models.MagicTokenType) (models.MagicToken, error) {
var tk models.MagicToken
if err := database.C.Where(models.MagicToken{Code: code, Type: mode}).First(&tk).Error; err != nil {
return tk, err
} else if tk.ExpiredAt != nil && time.Now().Unix() >= tk.ExpiredAt.Unix() {
return tk, fmt.Errorf("token has been expired")
}
return tk, nil
}
func NewMagicToken(mode models.MagicTokenType, assignTo *models.Account, expiredAt *time.Time) (models.MagicToken, error) {
var uid uint
if assignTo != nil {
@ -36,8 +63,8 @@ func NotifyMagicToken(token models.MagicToken) error {
}
var user models.Account
if err := database.C.Where(&models.MagicToken{
AssignTo: token.AssignTo,
if err := database.C.Where(&models.Account{
BaseModel: models.BaseModel{ID: *token.AssignTo},
}).Preload("Contacts").First(&user).Error; err != nil {
return err
}
@ -46,15 +73,16 @@ func NotifyMagicToken(token models.MagicToken) error {
var content string
switch token.Type {
case models.ConfirmMagicToken:
link := fmt.Sprintf("%s/users/me/confirm?tk=%s", viper.GetString("domain"), token.Code)
link := fmt.Sprintf("https://%s/users/me/confirm?tk=%s", viper.GetString("domain"), token.Code)
subject = fmt.Sprintf("[%s] Confirm your registration", viper.GetString("name"))
content = fmt.Sprintf("We got a create account request with this email recently.\n"+
"So we need you to click the link below to confirm your registeration.\n"+
"Confirmnation Link: %s\n"+
"If you didn't do that, you can ignore this email.\n\n"+
"%s\n"+
"Best wishes",
link, viper.GetString("maintainer"))
content = fmt.Sprintf(
ConfirmRegistrationTemplate,
user.Name,
viper.GetString("name"),
viper.GetString("maintainer"),
link,
viper.GetString("maintainer"),
)
default:
return fmt.Errorf("unsupported magic token type to notify")
}