✨ Email notification
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1 +1 @@
 | 
			
		||||
/dist
 | 
			
		||||
/dist
 | 
			
		||||
@@ -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")
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,15 @@ bind = "0.0.0.0:8444"
 | 
			
		||||
domain = "id.smartsheep.studio"
 | 
			
		||||
secret = "LtTjzAGFLshwXhN4ZD4nG5KlMv1MWcsvfv03TSZYnT1VhiAnLIZFTnHUwR0XhGgi"
 | 
			
		||||
 | 
			
		||||
use_registration_magic_token = true
 | 
			
		||||
 | 
			
		||||
[mailer]
 | 
			
		||||
name = "Alphabot <alphabot@smartsheep.studio>"
 | 
			
		||||
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_"
 | 
			
		||||
 
 | 
			
		||||
@@ -5,5 +5,4 @@
 | 
			
		||||
html, body {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    height: 100vh;
 | 
			
		||||
}
 | 
			
		||||
@@ -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(() => (
 | 
			
		||||
  <Router root={RootLayout}>
 | 
			
		||||
    <Route path="/" component={lazy(() => import("./pages/dashboard.tsx"))} />
 | 
			
		||||
    <Route path="/auth/login" component={lazy(() => import("./pages/auth/login.tsx"))} />
 | 
			
		||||
    <Route path="/auth/register" component={lazy(() => import("./pages/auth/register.tsx"))} />
 | 
			
		||||
    <Route path="/users/me/confirm" component={lazy(() => import("./pages/users/confirm.tsx"))} />
 | 
			
		||||
  </Router>
 | 
			
		||||
  <WellKnownProvider>
 | 
			
		||||
    <UserinfoProvider>
 | 
			
		||||
      <Router root={RootLayout}>
 | 
			
		||||
        <Route path="/" component={lazy(() => import("./pages/dashboard.tsx"))} />
 | 
			
		||||
        <Route path="/auth/login" component={lazy(() => import("./pages/auth/login.tsx"))} />
 | 
			
		||||
        <Route path="/auth/register" component={lazy(() => import("./pages/auth/register.tsx"))} />
 | 
			
		||||
        <Route path="/users/me/confirm" component={lazy(() => import("./pages/users/confirm.tsx"))} />
 | 
			
		||||
      </Router>
 | 
			
		||||
    </UserinfoProvider>
 | 
			
		||||
  </WellKnownProvider>
 | 
			
		||||
), root!);
 | 
			
		||||
 
 | 
			
		||||
@@ -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 (
 | 
			
		||||
    <Show when={ready()} fallback={
 | 
			
		||||
@@ -15,11 +16,8 @@ export default function RootLayout(props: any) {
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    }>
 | 
			
		||||
      <UserinfoProvider>
 | 
			
		||||
        <Navbar />
 | 
			
		||||
 | 
			
		||||
        <main class="h-[calc(100vh-68px)]">{props.children}</main>
 | 
			
		||||
      </UserinfoProvider>
 | 
			
		||||
      <Navbar />
 | 
			
		||||
      <main class="h-[calc(100vh-68px)] mt-[68px]">{props.children}</main>
 | 
			
		||||
    </Show>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -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 (
 | 
			
		||||
    <div class="navbar bg-base-100 shadow-md px-5">
 | 
			
		||||
    <div class="navbar bg-base-100 shadow-md px-5 z-10 fixed top-0">
 | 
			
		||||
      <div class="navbar-start">
 | 
			
		||||
        <div class="dropdown">
 | 
			
		||||
          <div tabIndex={0} role="button" class="btn btn-ghost lg:hidden">
 | 
			
		||||
@@ -52,7 +54,7 @@ export default function Navbar() {
 | 
			
		||||
          </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
        <a href="/" class="btn btn-ghost text-xl">
 | 
			
		||||
          Goatpass
 | 
			
		||||
          {wellKnown?.name ?? "Goatpass"}
 | 
			
		||||
        </a>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="navbar-center hidden lg:flex">
 | 
			
		||||
 
 | 
			
		||||
@@ -50,7 +50,7 @@ export default function LoginPage() {
 | 
			
		||||
      if (!data.factor) return;
 | 
			
		||||
 | 
			
		||||
      setLoading(true);
 | 
			
		||||
      const res = await fetch(`/api/auth/factors/${data.id}`, {
 | 
			
		||||
      const res = await fetch(`/api/auth/factors/${data.factor}`, {
 | 
			
		||||
        method: "POST"
 | 
			
		||||
      });
 | 
			
		||||
      if (res.status !== 200 && res.status !== 204) {
 | 
			
		||||
@@ -93,6 +93,7 @@ export default function LoginPage() {
 | 
			
		||||
          setStage("choosing");
 | 
			
		||||
          setTitle("Continue verifying");
 | 
			
		||||
          setSubtitle("You passed one check, but that's not enough.");
 | 
			
		||||
          setChallenge(data["challenge"]);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      setLoading(false);
 | 
			
		||||
@@ -120,10 +121,17 @@ export default function LoginPage() {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function getFactorAvailable(factor: any) {
 | 
			
		||||
    const blacklist: number[] = challenge()?.blacklist_factors ?? [];
 | 
			
		||||
    return blacklist.includes(factor.id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function getFactorName(factor: any) {
 | 
			
		||||
    switch (factor.type) {
 | 
			
		||||
      case 0:
 | 
			
		||||
        return "Password Verification";
 | 
			
		||||
      case 1:
 | 
			
		||||
        return "Email Verification Code";
 | 
			
		||||
      default:
 | 
			
		||||
        return "Unknown";
 | 
			
		||||
    }
 | 
			
		||||
@@ -171,6 +179,7 @@ export default function LoginPage() {
 | 
			
		||||
                      {item =>
 | 
			
		||||
                        <input class="join-item btn" type="radio" name="factor"
 | 
			
		||||
                               value={item.id}
 | 
			
		||||
                               disabled={getFactorAvailable(item)}
 | 
			
		||||
                               aria-label={getFactorName(item)}
 | 
			
		||||
                        />
 | 
			
		||||
                      }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { createSignal, Show } from "solid-js";
 | 
			
		||||
import { useWellKnown } from "../../stores/wellKnown.tsx";
 | 
			
		||||
 | 
			
		||||
export default function RegisterPage() {
 | 
			
		||||
  const [title, setTitle] = createSignal("Create an account");
 | 
			
		||||
@@ -8,6 +9,8 @@ export default function RegisterPage() {
 | 
			
		||||
  const [loading, setLoading] = createSignal(false);
 | 
			
		||||
  const [done, setDone] = createSignal(false);
 | 
			
		||||
 | 
			
		||||
  const metadata = useWellKnown();
 | 
			
		||||
 | 
			
		||||
  async function submit(evt: SubmitEvent) {
 | 
			
		||||
    evt.preventDefault();
 | 
			
		||||
 | 
			
		||||
@@ -23,6 +26,7 @@ export default function RegisterPage() {
 | 
			
		||||
    if (res.status !== 200) {
 | 
			
		||||
      setError(await res.text());
 | 
			
		||||
    } else {
 | 
			
		||||
      setError(null);
 | 
			
		||||
      setTitle("Congratulations!");
 | 
			
		||||
      setSubtitle("Your account has been created and activation email has sent to your inbox!");
 | 
			
		||||
      setDone(true);
 | 
			
		||||
@@ -32,83 +36,100 @@ export default function RegisterPage() {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div class="w-full h-full flex justify-center items-center">
 | 
			
		||||
      <div class="card w-[480px] max-w-screen shadow-xl">
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
          <div id="header" class="text-center mb-5">
 | 
			
		||||
            <h1 class="text-xl font-bold">{title()}</h1>
 | 
			
		||||
            <p>{subtitle()}</p>
 | 
			
		||||
          </div>
 | 
			
		||||
      <div>
 | 
			
		||||
        <div class="card w-[480px] max-w-screen shadow-xl">
 | 
			
		||||
          <div class="card-body">
 | 
			
		||||
            <div id="header" class="text-center mb-5">
 | 
			
		||||
              <h1 class="text-xl font-bold">{title()}</h1>
 | 
			
		||||
              <p>{subtitle()}</p>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
          <Show when={error()}>
 | 
			
		||||
            <div id="alerts" class="mt-1">
 | 
			
		||||
              <div role="alert" class="alert alert-error">
 | 
			
		||||
                <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
 | 
			
		||||
                     viewBox="0 0 24 24">
 | 
			
		||||
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
 | 
			
		||||
                        d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
 | 
			
		||||
                </svg>
 | 
			
		||||
                <span class="capitalize">{error()}</span>
 | 
			
		||||
            <Show when={error()}>
 | 
			
		||||
              <div id="alerts" class="mt-1">
 | 
			
		||||
                <div role="alert" class="alert alert-error">
 | 
			
		||||
                  <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
 | 
			
		||||
                       viewBox="0 0 24 24">
 | 
			
		||||
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
 | 
			
		||||
                          d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
 | 
			
		||||
                  </svg>
 | 
			
		||||
                  <span class="capitalize">{error()}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </Show>
 | 
			
		||||
            </Show>
 | 
			
		||||
 | 
			
		||||
          <Show when={!done()}>
 | 
			
		||||
            <form id="form" onSubmit={submit}>
 | 
			
		||||
              <label class="form-control w-full">
 | 
			
		||||
                <div class="label">
 | 
			
		||||
                  <span class="label-text">Username</span>
 | 
			
		||||
                  <span class="label-text-alt font-bold">Cannot be modify</span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <input name="name" type="text" placeholder="Type here" class="input input-bordered w-full" />
 | 
			
		||||
                <div class="label">
 | 
			
		||||
                  <span class="label-text-alt">Lowercase alphabet and numbers only, maximum 16 characters</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </label>
 | 
			
		||||
              <label class="form-control w-full">
 | 
			
		||||
                <div class="label">
 | 
			
		||||
                  <span class="label-text">Nickname</span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <input name="nick" type="text" placeholder="Type here" class="input input-bordered w-full" />
 | 
			
		||||
                <div class="label">
 | 
			
		||||
                  <span class="label-text-alt">Maximum length is 24 characters</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </label>
 | 
			
		||||
              <label class="form-control w-full">
 | 
			
		||||
                <div class="label">
 | 
			
		||||
                  <span class="label-text">Email Address</span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <input name="email" type="email" placeholder="Type here" class="input input-bordered w-full" />
 | 
			
		||||
                <div class="label">
 | 
			
		||||
                  <span class="label-text-alt">Do not accept address with plus sign</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </label>
 | 
			
		||||
              <label class="form-control w-full">
 | 
			
		||||
                <div class="label">
 | 
			
		||||
                  <span class="label-text">Password</span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <input name="password" type="password" placeholder="Type here" class="input input-bordered w-full" />
 | 
			
		||||
                <div class="label">
 | 
			
		||||
                  <span class="label-text-alt">Must be secure</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </label>
 | 
			
		||||
 | 
			
		||||
              <button type="submit" class="btn btn-primary btn-block mt-3" disabled={loading()}>
 | 
			
		||||
                <Show when={loading()} fallback={"Next"}>
 | 
			
		||||
                  <span class="loading loading-spinner"></span>
 | 
			
		||||
            <Show when={!done()}>
 | 
			
		||||
              <form id="form" onSubmit={submit}>
 | 
			
		||||
                <label class="form-control w-full">
 | 
			
		||||
                  <div class="label">
 | 
			
		||||
                    <span class="label-text">Username</span>
 | 
			
		||||
                    <span class="label-text-alt font-bold">Cannot be modify</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <input name="name" type="text" placeholder="Type here" class="input input-bordered w-full" />
 | 
			
		||||
                  <div class="label">
 | 
			
		||||
                    <span class="label-text-alt">Lowercase alphabet and numbers only, maximum 16 characters</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </label>
 | 
			
		||||
                <label class="form-control w-full">
 | 
			
		||||
                  <div class="label">
 | 
			
		||||
                    <span class="label-text">Nickname</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <input name="nick" type="text" placeholder="Type here" class="input input-bordered w-full" />
 | 
			
		||||
                  <div class="label">
 | 
			
		||||
                    <span class="label-text-alt">Maximum length is 24 characters</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </label>
 | 
			
		||||
                <label class="form-control w-full">
 | 
			
		||||
                  <div class="label">
 | 
			
		||||
                    <span class="label-text">Email Address</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <input name="email" type="email" placeholder="Type here" class="input input-bordered w-full" />
 | 
			
		||||
                  <div class="label">
 | 
			
		||||
                    <span class="label-text-alt">Do not accept address with plus sign</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </label>
 | 
			
		||||
                <label class="form-control w-full">
 | 
			
		||||
                  <div class="label">
 | 
			
		||||
                    <span class="label-text">Password</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <input name="password" type="password" placeholder="Type here"
 | 
			
		||||
                         class="input input-bordered w-full" />
 | 
			
		||||
                  <div class="label">
 | 
			
		||||
                    <span class="label-text-alt">Must be secure</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </label>
 | 
			
		||||
                <Show when={!metadata?.open_registration}>
 | 
			
		||||
                  <label class="form-control w-full">
 | 
			
		||||
                    <div class="label">
 | 
			
		||||
                      <span class="label-text">Magic Token</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <input name="magic_token" type="password" placeholder="Type here"
 | 
			
		||||
                           class="input input-bordered w-full" />
 | 
			
		||||
                    <div class="label">
 | 
			
		||||
                      <span class="label-text-alt">
 | 
			
		||||
                        This server enabled invitation only, so you need a magic token.
 | 
			
		||||
                      </span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </label>
 | 
			
		||||
                </Show>
 | 
			
		||||
              </button>
 | 
			
		||||
            </form>
 | 
			
		||||
          </Show>
 | 
			
		||||
 | 
			
		||||
          <Show when={done()}>
 | 
			
		||||
            <div class="py-12 text-center">
 | 
			
		||||
              <h2 class="text-lg font-bold">What's next?</h2>
 | 
			
		||||
              <span>
 | 
			
		||||
                <button type="submit" class="btn btn-primary btn-block mt-3" disabled={loading()}>
 | 
			
		||||
                  <Show when={loading()} fallback={"Next"}>
 | 
			
		||||
                    <span class="loading loading-spinner"></span>
 | 
			
		||||
                  </Show>
 | 
			
		||||
                </button>
 | 
			
		||||
              </form>
 | 
			
		||||
            </Show>
 | 
			
		||||
 | 
			
		||||
            <Show when={done()}>
 | 
			
		||||
              <div class="py-12 text-center">
 | 
			
		||||
                <h2 class="text-lg font-bold">What's next?</h2>
 | 
			
		||||
                <span>
 | 
			
		||||
                <a href="/auth/login" class="link">Go login</a>{" "}
 | 
			
		||||
                then you can take part in the entire smartsheep community.
 | 
			
		||||
                  then you can take part in the entire smartsheep community.
 | 
			
		||||
              </span>
 | 
			
		||||
            </div>
 | 
			
		||||
          </Show>
 | 
			
		||||
              </div>
 | 
			
		||||
            </Show>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="text-sm text-center mt-3">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import { createSignal, Show } from "solid-js";
 | 
			
		||||
import { useSearchParams } from "@solidjs/router";
 | 
			
		||||
import { useNavigate, useSearchParams } from "@solidjs/router";
 | 
			
		||||
import { readProfiles } from "../../stores/userinfo.tsx";
 | 
			
		||||
 | 
			
		||||
export default function ConfirmRegistrationPage() {
 | 
			
		||||
  const [error, setError] = createSignal<string | null>(null);
 | 
			
		||||
@@ -7,6 +8,8 @@ export default function ConfirmRegistrationPage() {
 | 
			
		||||
 | 
			
		||||
  const [searchParams] = useSearchParams();
 | 
			
		||||
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
  async function doConfirm() {
 | 
			
		||||
    if (!searchParams["tk"]) {
 | 
			
		||||
      setError("Bad Request: Code was not exists");
 | 
			
		||||
@@ -23,6 +26,8 @@ export default function ConfirmRegistrationPage() {
 | 
			
		||||
      setError(await res.text());
 | 
			
		||||
    } else {
 | 
			
		||||
      setStatus("Confirmed. Redirecting to dashboard...");
 | 
			
		||||
      await readProfiles();
 | 
			
		||||
      navigate("/");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								view/src/stores/wellKnown.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								view/src/stores/wellKnown.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
import { createContext, useContext } from "solid-js";
 | 
			
		||||
import { createStore } from "solid-js/store";
 | 
			
		||||
 | 
			
		||||
const WellKnownContext = createContext<any>();
 | 
			
		||||
 | 
			
		||||
const [wellKnown, setWellKnown] = createStore<any>(null);
 | 
			
		||||
 | 
			
		||||
export async function readWellKnown() {
 | 
			
		||||
  const res = await fetch("/.well-known")
 | 
			
		||||
  setWellKnown(await res.json())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function WellKnownProvider(props: any) {
 | 
			
		||||
  return (
 | 
			
		||||
    <WellKnownContext.Provider value={wellKnown}>
 | 
			
		||||
      {props.children}
 | 
			
		||||
    </WellKnownContext.Provider>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useWellKnown() {
 | 
			
		||||
  return useContext(WellKnownContext);
 | 
			
		||||
}
 | 
			
		||||
@@ -6,7 +6,8 @@ export default defineConfig({
 | 
			
		||||
  plugins: [devtools({ autoname: true }), solid()],
 | 
			
		||||
  server: {
 | 
			
		||||
    proxy: {
 | 
			
		||||
      "/api": "http://localhost:8444"
 | 
			
		||||
      "/api": "http://localhost:8444",
 | 
			
		||||
      "/.well-known": "http://localhost:8444"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user