diff --git a/certs/rsa.pub b/certs/rsa.pub
deleted file mode 100644
index e197707..0000000
--- a/certs/rsa.pub
+++ /dev/null
@@ -1 +0,0 @@
-ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDgVXIw08Wgl5ZTLnnH8o0EiWwvfvqbLxp7OgMsZBWOrJiaSrrNjZOyIab3T5Az/1IoUgjGnKvetiVbNL9HLaHMwaN28Q2RR1z+cw/f3NDFydDYmcj1OA/HT011Xh+K39lOjfQptEMOTCtWLOcuzu21jQICqDgsp7BSu3Lt6ezrHO4+kDSyNjclT9iX+RovjK3snJM1rsstezx1yo+f7NBA5WUs1/PfEgvwDZKBuUIqRb8GGcLEQav6FpNicx/L+I5TNDZgoeSlWYCOjZimQy7VagF7iyBwoqj9htRx3F1gZdjqdJxQzYxFrehKg+j+P/JvIifAx2CWyi5/O9BpSkW7 littlesheep@LittleSheepdeMacBook-Pro.local
diff --git a/go.mod b/go.mod
index ffb3b18..1099eb3 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ require (
github.com/gofiber/fiber/v2 v2.52.0
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/google/uuid v1.5.0
+ github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/json-iterator/go v1.1.12
github.com/rs/zerolog v1.31.0
github.com/samber/lo v1.39.0
diff --git a/go.sum b/go.sum
index 52b4bf3..023490c 100644
--- a/go.sum
+++ b/go.sum
@@ -50,6 +50,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
+github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
diff --git a/pkg/cmd/main.go b/pkg/cmd/main.go
index 37283a5..5cab662 100644
--- a/pkg/cmd/main.go
+++ b/pkg/cmd/main.go
@@ -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 {
diff --git a/pkg/database/migrator.go b/pkg/database/migrator.go
index 93a0e47..a2d5bfb 100644
--- a/pkg/database/migrator.go
+++ b/pkg/database/migrator.go
@@ -13,6 +13,7 @@ func RunMigration(source *gorm.DB) error {
&models.AccountContact{},
&models.AuthSession{},
&models.AuthChallenge{},
+ &models.MagicToken{},
); err != nil {
return err
}
diff --git a/pkg/models/accounts.go b/pkg/models/accounts.go
index 6060059..6977d06 100644
--- a/pkg/models/accounts.go
+++ b/pkg/models/accounts.go
@@ -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 (
diff --git a/pkg/models/auth.go b/pkg/models/auth.go
index f8ab4bb..64baa89 100644
--- a/pkg/models/auth.go
+++ b/pkg/models/auth.go
@@ -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"`
}
diff --git a/pkg/models/tokens.go b/pkg/models/tokens.go
new file mode 100644
index 0000000..9c432a1
--- /dev/null
+++ b/pkg/models/tokens.go
@@ -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"`
+}
diff --git a/pkg/server/accounts_api.go b/pkg/server/accounts_api.go
index 53c9a1b..583285f 100644
--- a/pkg/server/accounts_api.go
+++ b/pkg/server/accounts_api.go
@@ -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)
+}
diff --git a/pkg/server/startup.go b/pkg/server/startup.go
index 22a61de..bef1af7 100644
--- a/pkg/server/startup.go
+++ b/pkg/server/startup.go
@@ -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)
diff --git a/pkg/server/well_known_api.go b/pkg/server/well_known_api.go
new file mode 100644
index 0000000..147123d
--- /dev/null
+++ b/pkg/server/well_known_api.go
@@ -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"),
+ })
+}
diff --git a/pkg/services/accounts.go b/pkg/services/accounts.go
index d79b85f..436c66a 100644
--- a/pkg/services/accounts.go
+++ b/pkg/services/accounts.go
@@ -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
+ })
}
diff --git a/pkg/services/mailer.go b/pkg/services/mailer.go
new file mode 100644
index 0000000..74301fe
--- /dev/null
+++ b/pkg/services/mailer.go
@@ -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")},
+ )
+}
diff --git a/pkg/services/tokens.go b/pkg/services/tokens.go
new file mode 100644
index 0000000..69da356
--- /dev/null
+++ b/pkg/services/tokens.go
@@ -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)
+}
diff --git a/settings.toml b/settings.toml
new file mode 100644
index 0000000..433206a
--- /dev/null
+++ b/settings.toml
@@ -0,0 +1,12 @@
+debug = true
+
+name = "Goatpass"
+maintainer = "SmartSheep Studio"
+
+bind = "0.0.0.0:8444"
+domain = "id.smartsheep.studio"
+secret = "LtTjzAGFLshwXhN4ZD4nG5KlMv1MWcsvfv03TSZYnT1VhiAnLIZFTnHUwR0XhGgi"
+
+[database]
+dsn = "host=localhost dbname=hy_passport port=5432 sslmode=disable"
+prefix = "passport_"
diff --git a/settings.yaml b/settings.yaml
deleted file mode 100644
index c708ecd..0000000
--- a/settings.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-debug: true
-
-bind: 0.0.0.0:8444
-domain: id.smartsheep.studio
-secret: LtTjzAGFLshwXhN4ZD4nG5KlMv1MWcsvfv03TSZYnT1VhiAnLIZFTnHUwR0XhGgi
-
-database:
- dsn: host=localhost dbname=hy_passport port=5432 sslmode=disable
- prefix: passport_
diff --git a/view/src/index.tsx b/view/src/index.tsx
index 1d59c90..6bc7487 100644
--- a/view/src/index.tsx
+++ b/view/src/index.tsx
@@ -5,19 +5,18 @@ import { render } from "solid-js/web";
import "./index.css";
import "./assets/fonts/fonts.css";
+import { lazy } from "solid-js";
import { Route, Router } from "@solidjs/router";
import RootLayout from "./layouts/RootLayout.tsx";
-import DashboardPage from "./pages/dashboard.tsx";
-import LoginPage from "./pages/auth/login.tsx";
-import RegisterPage from "./pages/auth/register.tsx";
const root = document.getElementById("root");
render(() => (
What's a nice day!
+ +Hold on, we are working on it. Almost finished.
+