Account confirm

This commit is contained in:
2024-01-29 00:32:39 +08:00
parent d4aef5277f
commit 20119cb177
20 changed files with 326 additions and 20 deletions

View File

@ -23,7 +23,7 @@ func main() {
viper.AddConfigPath(".")
viper.AddConfigPath("..")
viper.SetConfigName("settings")
viper.SetConfigType("yaml")
viper.SetConfigType("toml")
// Load settings
if err := viper.ReadInConfig(); err != nil {

View File

@ -13,6 +13,7 @@ func RunMigration(source *gorm.DB) error {
&models.AccountContact{},
&models.AuthSession{},
&models.AuthChallenge{},
&models.MagicToken{},
); err != nil {
return err
}

View File

@ -1,6 +1,7 @@
package models
import (
"github.com/samber/lo"
"time"
"gorm.io/datatypes"
@ -24,10 +25,18 @@ type Account struct {
Challenges []AuthChallenge `json:"challenges"`
Factors []AuthFactor `json:"factors"`
Contacts []AccountContact `json:"contacts"`
MagicTokens []MagicToken `json:"-" gorm:"foreignKey:AssignTo"`
ConfirmedAt *time.Time `json:"confirmed_at"`
Permissions datatypes.JSONType[[]string] `json:"permissions"`
}
func (v Account) GetPrimaryEmail() AccountContact {
val, _ := lo.Find(v.Contacts, func(item AccountContact) bool {
return item.Type == EmailAccountContact && item.IsPrimary
})
return val
}
type AccountContactType = int8
const (

View File

@ -18,7 +18,7 @@ type AuthFactor struct {
BaseModel
Type int8 `json:"type"`
Secret string `json:"secret"`
Secret string `json:"-"`
Config JSONMap `json:"config"`
AccountID uint `json:"account_id"`
}

19
pkg/models/tokens.go Normal file
View File

@ -0,0 +1,19 @@
package models
import "time"
type MagicTokenType = int8
const (
ConfirmMagicToken = MagicTokenType(iota)
RegistrationMagicToken
)
type MagicToken struct {
BaseModel
Code string `json:"code"`
Type int8 `json:"type"`
AssignTo *uint `json:"assign_to"`
ExpiredAt *time.Time `json:"expired_at"`
}

View File

@ -44,3 +44,19 @@ func doRegister(c *fiber.Ctx) error {
return c.JSON(user)
}
}
func doRegisterConfirm(c *fiber.Ctx) error {
var data struct {
Code string `json:"code"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
if err := services.ConfirmAccount(data.Code); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.SendStatus(fiber.StatusOK)
}

View File

@ -19,10 +19,13 @@ func NewServer() {
JSONDecoder: jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal,
})
A.Get("/.well-known", getMetadata)
api := A.Group("/api").Name("API")
{
api.Get("/users/me", auth, getPrincipal)
api.Post("/users", doRegister)
api.Post("/users/me/confirm", doRegisterConfirm)
api.Put("/auth", startChallenge)
api.Post("/auth", doChallenge)

View File

@ -0,0 +1,13 @@
package server
import (
"github.com/gofiber/fiber/v2"
"github.com/spf13/viper"
)
func getMetadata(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"name": viper.GetString("name"),
"domain": viper.GetString("domain"),
})
}

View File

@ -5,7 +5,10 @@ import (
"code.smartsheep.studio/hydrogen/passport/pkg/models"
"code.smartsheep.studio/hydrogen/passport/pkg/security"
"fmt"
"github.com/samber/lo"
"gorm.io/datatypes"
"gorm.io/gorm"
"time"
)
func GetAccount(id uint) (models.Account, error) {
@ -65,7 +68,45 @@ func CreateAccount(name, nick, email, password string) (models.Account, error) {
if err := database.C.Create(&user).Error; err != nil {
return user, err
} else {
return user, nil
}
if tk, err := NewMagicToken(models.ConfirmMagicToken, &user, nil); err != nil {
return user, err
} else if err := NotifyMagicToken(tk); err != nil {
return user, err
}
return user, nil
}
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 {
return err
} else if token.AssignTo == nil {
return fmt.Errorf("account was not found")
}
var user models.Account
if err := database.C.Where(&models.Account{
BaseModel: models.BaseModel{ID: *token.AssignTo},
}).First(&user).Error; err != nil {
return err
}
return database.C.Transaction(func(tx *gorm.DB) error {
user.ConfirmedAt = lo.ToPtr(time.Now())
if err := database.C.Delete(&token).Error; err != nil {
return err
}
if err := database.C.Save(&user).Error; err != nil {
return err
}
return nil
})
}

51
pkg/services/mailer.go Normal file
View File

@ -0,0 +1,51 @@
package services
import (
"crypto/tls"
"fmt"
"net/smtp"
"net/textproto"
"github.com/jordan-wright/email"
"github.com/spf13/viper"
)
func SendMail(target string, subject string, content string) error {
mail := &email.Email{
To: []string{target},
From: viper.GetString("mailer.name"),
Subject: subject,
Text: []byte(content),
Headers: textproto.MIMEHeader{},
}
return mail.SendWithTLS(
fmt.Sprintf("%s:%d", viper.GetString("mailer.smtp_host"), viper.GetInt("mailer.smtp_port")),
smtp.PlainAuth(
"",
viper.GetString("mailer.username"),
viper.GetString("mailer.password"),
viper.GetString("mailer.smtp_host"),
),
&tls.Config{ServerName: viper.GetString("mailer.smtp_host")},
)
}
func SendMailHTML(target string, subject string, content string) error {
mail := &email.Email{
To: []string{target},
From: viper.GetString("mailer.name"),
Subject: subject,
HTML: []byte(content),
Headers: textproto.MIMEHeader{},
}
return mail.SendWithTLS(
fmt.Sprintf("%s:%d", viper.GetString("mailer.smtp_host"), viper.GetInt("mailer.smtp_port")),
smtp.PlainAuth(
"",
viper.GetString("mailer.username"),
viper.GetString("mailer.password"),
viper.GetString("mailer.smtp_host"),
),
&tls.Config{ServerName: viper.GetString("mailer.smtp_host")},
)
}

63
pkg/services/tokens.go Normal file
View File

@ -0,0 +1,63 @@
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"
"strings"
"time"
)
func NewMagicToken(mode models.MagicTokenType, assignTo *models.Account, expiredAt *time.Time) (models.MagicToken, error) {
var uid uint
if assignTo != nil {
uid = assignTo.ID
}
token := models.MagicToken{
Code: strings.Replace(uuid.NewString(), "-", "", -1),
Type: mode,
AssignTo: &uid,
ExpiredAt: expiredAt,
}
if err := database.C.Save(&token).Error; err != nil {
return token, err
} else {
return token, nil
}
}
func NotifyMagicToken(token models.MagicToken) error {
if token.AssignTo == nil {
return fmt.Errorf("could notify a non-assign magic token")
}
var user models.Account
if err := database.C.Where(&models.MagicToken{
AssignTo: token.AssignTo,
}).Preload("Contacts").First(&user).Error; err != nil {
return err
}
var subject string
var content string
switch token.Type {
case models.ConfirmMagicToken:
link := fmt.Sprintf("%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"))
default:
return fmt.Errorf("unsupported magic token type to notify")
}
return SendMail(user.GetPrimaryEmail().Content, subject, content)
}