182 lines
5.3 KiB
Go
Raw Normal View History

2024-01-07 01:56:32 +08:00
package services
import (
2024-03-12 23:23:16 +08:00
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/nex/localize"
"strings"
"time"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
"git.solsynth.dev/hypernet/passport/pkg/internal/gap"
2024-10-27 00:06:23 +08:00
"git.solsynth.dev/hypernet/pusher/pkg/pushkit"
2024-01-29 16:11:59 +08:00
"github.com/google/uuid"
"github.com/nats-io/nats.go"
"github.com/pquerna/otp/totp"
2024-10-27 00:06:23 +08:00
"github.com/rs/zerolog/log"
"github.com/samber/lo"
2024-01-29 16:11:59 +08:00
"github.com/spf13/viper"
2024-01-07 01:56:32 +08:00
)
func GetPasswordTypeFactor(userId uint) (models.AuthFactor, error) {
2024-04-20 19:04:33 +08:00
var factor models.AuthFactor
err := database.C.Where(models.AuthFactor{
Type: models.PasswordAuthFactor,
AccountID: userId,
}).First(&factor).Error
return factor, err
}
func GetFactor(id uint) (models.AuthFactor, error) {
2024-01-07 01:56:32 +08:00
var factor models.AuthFactor
err := database.C.Where(models.AuthFactor{
BaseModel: models.BaseModel{ID: id},
}).First(&factor).Error
return factor, err
}
2024-04-20 19:04:33 +08:00
func ListUserFactor(userId uint) ([]models.AuthFactor, error) {
2024-01-07 01:56:32 +08:00
var factors []models.AuthFactor
err := database.C.Where(models.AuthFactor{
2024-04-20 19:04:33 +08:00
AccountID: userId,
2024-01-07 01:56:32 +08:00
}).Find(&factors).Error
return factors, err
}
2024-01-29 16:11:59 +08:00
func CountUserFactor(userId uint) int64 {
var count int64
database.C.Where(models.AuthFactor{
AccountID: userId,
}).Model(&models.AuthFactor{}).Count(&count)
return count
}
func GetFactorCode(factor models.AuthFactor, ip string) (bool, error) {
2024-01-29 16:11:59 +08:00
switch factor.Type {
case models.InAppNotifyFactor:
var user models.Account
if err := database.C.Where(&models.Account{
BaseModel: models.BaseModel{ID: factor.AccountID},
}).First(&user).Error; err != nil {
return true, err
}
secret := uuid.NewString()[:6]
identifier := fmt.Sprintf("%s%d", gap.FactorOtpPrefix, factor.ID)
_, err := gap.Jt.Publish(identifier, []byte(secret))
if err != nil {
return true, fmt.Errorf("error during publish message: %v", err)
} else {
log.Info().Uint("factor", factor.ID).Str("secret", secret).Msg("Published one-time-password to JetStream...")
}
err = PushNotification(models.Notification{
Topic: "passport.security.otp",
Title: localize.L.GetLocalizedString("subjectLoginOneTimePassword", user.Language),
Body: fmt.Sprintf(localize.L.GetLocalizedString("shortBodyLoginOneTimePassword", user.Language), secret),
Account: user,
AccountID: user.ID,
Metadata: map[string]any{"secret": secret},
}, true)
if err != nil {
log.Warn().Err(err).Uint("factor", factor.ID).Msg("Failed to delivery one-time-password via notify...")
return true, nil
}
return true, nil
2024-01-29 16:11:59 +08:00
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
}
secret := uuid.NewString()[:6]
identifier := fmt.Sprintf("%s%d", gap.FactorOtpPrefix, factor.ID)
_, err := gap.Jt.Publish(identifier, []byte(secret))
if err != nil {
return true, fmt.Errorf("error during publish message: %v", err)
} else {
log.Info().Uint("factor", factor.ID).Str("secret", secret).Msg("Published one-time-password to JetStream...")
2024-01-29 16:11:59 +08:00
}
subject := fmt.Sprintf("[%s] %s", viper.GetString("name"), localize.L.GetLocalizedString("subjectLoginOneTimePassword", user.Language))
content := localize.L.RenderLocalizedTemplateHTML("email-otp.tmpl", user.Language, map[string]any{
"Code": secret,
"User": user,
"IP": ip,
"Date": time.Now().Format(time.DateTime),
})
err = gap.Px.PushEmail(pushkit.EmailDeliverRequest{
To: user.GetPrimaryEmail().Content,
2024-10-27 00:06:23 +08:00
Email: pushkit.EmailData{
Subject: subject,
2025-02-01 18:27:57 +08:00
HTML: &content,
},
})
if err != nil {
2024-06-26 20:05:28 +08:00
log.Warn().Err(err).Uint("factor", factor.ID).Msg("Failed to delivery one-time-password via mail...")
return true, nil
2024-01-29 16:11:59 +08:00
}
return true, nil
default:
return false, nil
}
}
2024-04-20 19:04:33 +08:00
func CheckFactor(factor models.AuthFactor, code string) error {
switch factor.Type {
case models.PasswordAuthFactor:
return lo.Ternary(
VerifyPassword(code, factor.Secret),
nil,
fmt.Errorf("invalid password"),
)
case models.TimeOtpFactor:
return lo.Ternary(
totp.Validate(code, factor.Secret),
nil,
fmt.Errorf("invalid verification code"),
)
case models.InAppNotifyFactor:
2024-04-20 19:04:33 +08:00
case models.EmailPasswordFactor:
identifier := fmt.Sprintf("%s%d", gap.FactorOtpPrefix, factor.ID)
sub, err := gap.Jt.PullSubscribe(identifier, "otp_validator", nats.BindStream("OTPs"))
if err != nil {
log.Error().Err(err).Msg("Error subscribing to subject when validating factor code...")
return fmt.Errorf("error subscribing to subject: %v", err)
}
defer sub.Unsubscribe()
msgs, err := sub.Fetch(1, nats.MaxWait(3*time.Second))
if err != nil {
log.Error().Err(err).Msg("Error fetching message when validating factor code...")
return fmt.Errorf("error fetching message: %v", err)
}
if len(msgs) > 0 {
msg := msgs[0]
if !strings.EqualFold(code, string(msg.Data)) {
return fmt.Errorf("invalid verification code")
}
log.Info().Uint("factor", factor.ID).Str("secret", code).Msg("Verified one-time-password...")
2025-01-27 16:39:14 +08:00
if err := msg.AckSync(); err != nil {
log.Warn().Err(err).Uint("factor", factor.ID).Msg("Failed to acknowledge message when validating factor code...")
}
return nil
}
return fmt.Errorf("one-time-password not found or expired")
2024-04-20 19:04:33 +08:00
}
return nil
}