✨ Email notification
This commit is contained in:
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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"),
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
Reference in New Issue
Block a user