diff --git a/.gitignore b/.gitignore index 9b1c8b1..3e22129 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -/dist +/dist \ No newline at end of file diff --git a/pkg/security/factors.go b/pkg/security/factors.go index c075e8f..1db0d6e 100644 --- a/pkg/security/factors.go +++ b/pkg/security/factors.go @@ -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 diff --git a/pkg/server/accounts_api.go b/pkg/server/accounts_api.go index 583285f..64498c0 100644 --- a/pkg/server/accounts_api.go +++ b/pkg/server/accounts_api.go @@ -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( diff --git a/pkg/server/factors_api.go b/pkg/server/factors_api.go index 5aad68f..868810d 100644 --- a/pkg/server/factors_api.go +++ b/pkg/server/factors_api.go @@ -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) diff --git a/pkg/server/well_known_api.go b/pkg/server/well_known_api.go index 147123d..4de1cea 100644 --- a/pkg/server/well_known_api.go +++ b/pkg/server/well_known_api.go @@ -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"), }) } diff --git a/pkg/services/accounts.go b/pkg/services/accounts.go index 436c66a..90c7406 100644 --- a/pkg/services/accounts.go +++ b/pkg/services/accounts.go @@ -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 diff --git a/pkg/services/factors.go b/pkg/services/factors.go index 9ab4c70..15af4fc 100644 --- a/pkg/services/factors.go +++ b/pkg/services/factors.go @@ -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 + } +} diff --git a/pkg/services/tokens.go b/pkg/services/tokens.go index 69da356..e714ddf 100644 --- a/pkg/services/tokens.go +++ b/pkg/services/tokens.go @@ -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") } diff --git a/settings.toml b/settings.toml index 433206a..49c1df2 100644 --- a/settings.toml +++ b/settings.toml @@ -7,6 +7,15 @@ bind = "0.0.0.0:8444" domain = "id.smartsheep.studio" secret = "LtTjzAGFLshwXhN4ZD4nG5KlMv1MWcsvfv03TSZYnT1VhiAnLIZFTnHUwR0XhGgi" +use_registration_magic_token = true + +[mailer] +name = "Alphabot " +smtp_host = "smtp.exmail.qq.com" +smtp_port = 465 +username = "alphabot@smartsheep.studio" +password = "gz937Zxxzfcd9SeH" + [database] dsn = "host=localhost dbname=hy_passport port=5432 sslmode=disable" prefix = "passport_" diff --git a/view/src/index.css b/view/src/index.css index d82d844..55b7c9b 100644 --- a/view/src/index.css +++ b/view/src/index.css @@ -5,5 +5,4 @@ html, body { padding: 0; margin: 0; - height: 100vh; } \ No newline at end of file diff --git a/view/src/index.tsx b/view/src/index.tsx index 6bc7487..5dee3c7 100644 --- a/view/src/index.tsx +++ b/view/src/index.tsx @@ -9,14 +9,20 @@ import { lazy } from "solid-js"; import { Route, Router } from "@solidjs/router"; import RootLayout from "./layouts/RootLayout.tsx"; +import { UserinfoProvider } from "./stores/userinfo.tsx"; +import { WellKnownProvider } from "./stores/wellKnown.tsx"; const root = document.getElementById("root"); render(() => ( - - import("./pages/dashboard.tsx"))} /> - import("./pages/auth/login.tsx"))} /> - import("./pages/auth/register.tsx"))} /> - import("./pages/users/confirm.tsx"))} /> - + + + + import("./pages/dashboard.tsx"))} /> + import("./pages/auth/login.tsx"))} /> + import("./pages/auth/register.tsx"))} /> + import("./pages/users/confirm.tsx"))} /> + + + ), root!); diff --git a/view/src/layouts/RootLayout.tsx b/view/src/layouts/RootLayout.tsx index dfe956c..0a5d582 100644 --- a/view/src/layouts/RootLayout.tsx +++ b/view/src/layouts/RootLayout.tsx @@ -1,11 +1,12 @@ import Navbar from "./shared/Navbar.tsx"; -import { readProfiles, UserinfoProvider } from "../stores/userinfo.tsx"; +import { readProfiles } from "../stores/userinfo.tsx"; import { createSignal, Show } from "solid-js"; +import { readWellKnown } from "../stores/wellKnown.tsx"; export default function RootLayout(props: any) { const [ready, setReady] = createSignal(false); - readProfiles().then(() => setReady(true)); + Promise.all([readWellKnown(), readProfiles()]).then(() => setReady(true)); return ( }> - - - -
{props.children}
-
+ +
{props.children}
); } \ No newline at end of file diff --git a/view/src/layouts/shared/Navbar.tsx b/view/src/layouts/shared/Navbar.tsx index 36906e7..95268f0 100644 --- a/view/src/layouts/shared/Navbar.tsx +++ b/view/src/layouts/shared/Navbar.tsx @@ -1,6 +1,7 @@ import { For, Match, Switch } from "solid-js"; import { clearUserinfo, useUserinfo } from "../../stores/userinfo.tsx"; import { useNavigate } from "@solidjs/router"; +import { useWellKnown } from "../../stores/wellKnown.tsx"; interface MenuItem { label: string; @@ -10,6 +11,7 @@ interface MenuItem { export default function Navbar() { const nav: MenuItem[] = [{ label: "Dashboard", href: "/" }]; + const wellKnown = useWellKnown(); const userinfo = useUserinfo(); const navigate = useNavigate(); @@ -19,7 +21,7 @@ export default function Navbar() { } return ( -