✨ Email notification
This commit is contained in:
parent
20119cb177
commit
3c58cb8f0a
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1 @@
|
|||||||
/dist
|
/dist
|
@ -1,22 +1,11 @@
|
|||||||
package security
|
package security
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"code.smartsheep.studio/hydrogen/passport/pkg/models"
|
"code.smartsheep.studio/hydrogen/passport/pkg/models"
|
||||||
|
"fmt"
|
||||||
"github.com/samber/lo"
|
"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 {
|
func VerifyFactor(factor models.AuthFactor, code string) error {
|
||||||
switch factor.Type {
|
switch factor.Type {
|
||||||
case models.PasswordAuthFactor:
|
case models.PasswordAuthFactor:
|
||||||
@ -25,6 +14,12 @@ func VerifyFactor(factor models.AuthFactor, code string) error {
|
|||||||
nil,
|
nil,
|
||||||
fmt.Errorf("invalid password"),
|
fmt.Errorf("invalid password"),
|
||||||
)
|
)
|
||||||
|
case models.EmailPasswordFactor:
|
||||||
|
return lo.Ternary(
|
||||||
|
code == factor.Secret,
|
||||||
|
nil,
|
||||||
|
fmt.Errorf("invalid verification code"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -4,7 +4,9 @@ import (
|
|||||||
"code.smartsheep.studio/hydrogen/passport/pkg/database"
|
"code.smartsheep.studio/hydrogen/passport/pkg/database"
|
||||||
"code.smartsheep.studio/hydrogen/passport/pkg/models"
|
"code.smartsheep.studio/hydrogen/passport/pkg/models"
|
||||||
"code.smartsheep.studio/hydrogen/passport/pkg/services"
|
"code.smartsheep.studio/hydrogen/passport/pkg/services"
|
||||||
|
"fmt"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/spf13/viper"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -23,14 +25,23 @@ func getPrincipal(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
func doRegister(c *fiber.Ctx) error {
|
func doRegister(c *fiber.Ctx) error {
|
||||||
var data struct {
|
var data struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Nick string `json:"nick"`
|
Nick string `json:"nick"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
MagicToken string `json:"magic_token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := BindAndValidate(c, &data); err != nil {
|
if err := BindAndValidate(c, &data); err != nil {
|
||||||
return err
|
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(
|
if user, err := services.CreateAccount(
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.smartsheep.studio/hydrogen/passport/pkg/security"
|
|
||||||
"code.smartsheep.studio/hydrogen/passport/pkg/services"
|
"code.smartsheep.studio/hydrogen/passport/pkg/services"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
@ -14,7 +13,7 @@ func requestFactorToken(c *fiber.Ctx) error {
|
|||||||
return fiber.NewError(fiber.StatusNotFound, err.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())
|
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||||
} else if !sent {
|
} else if !sent {
|
||||||
return c.SendStatus(fiber.StatusNoContent)
|
return c.SendStatus(fiber.StatusNoContent)
|
||||||
|
@ -7,7 +7,8 @@ import (
|
|||||||
|
|
||||||
func getMetadata(c *fiber.Ctx) error {
|
func getMetadata(c *fiber.Ctx) error {
|
||||||
return c.JSON(fiber.Map{
|
return c.JSON(fiber.Map{
|
||||||
"name": viper.GetString("name"),
|
"name": viper.GetString("name"),
|
||||||
"domain": viper.GetString("domain"),
|
"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/models"
|
||||||
"code.smartsheep.studio/hydrogen/passport/pkg/security"
|
"code.smartsheep.studio/hydrogen/passport/pkg/security"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
"gorm.io/datatypes"
|
"gorm.io/datatypes"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@ -54,11 +55,16 @@ func CreateAccount(name, nick, email, password string) (models.Account, error) {
|
|||||||
Type: models.PasswordAuthFactor,
|
Type: models.PasswordAuthFactor,
|
||||||
Secret: security.HashPassword(password),
|
Secret: security.HashPassword(password),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Type: models.EmailPasswordFactor,
|
||||||
|
Secret: uuid.NewString()[:8],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Contacts: []models.AccountContact{
|
Contacts: []models.AccountContact{
|
||||||
{
|
{
|
||||||
Type: models.EmailAccountContact,
|
Type: models.EmailAccountContact,
|
||||||
Content: email,
|
Content: email,
|
||||||
|
IsPrimary: true,
|
||||||
VerifiedAt: nil,
|
VerifiedAt: nil,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -80,14 +86,9 @@ func CreateAccount(name, nick, email, password string) (models.Account, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ConfirmAccount(code string) error {
|
func ConfirmAccount(code string) error {
|
||||||
var token models.MagicToken
|
token, err := ValidateMagicToken(code, models.ConfirmMagicToken)
|
||||||
if err := database.C.Where(&models.MagicToken{
|
if err != nil {
|
||||||
Code: code,
|
|
||||||
Type: models.ConfirmMagicToken,
|
|
||||||
}).First(&token).Error; err != nil {
|
|
||||||
return err
|
return err
|
||||||
} else if token.AssignTo == nil {
|
|
||||||
return fmt.Errorf("account was not found")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var user models.Account
|
var user models.Account
|
||||||
|
@ -3,8 +3,26 @@ package services
|
|||||||
import (
|
import (
|
||||||
"code.smartsheep.studio/hydrogen/passport/pkg/database"
|
"code.smartsheep.studio/hydrogen/passport/pkg/database"
|
||||||
"code.smartsheep.studio/hydrogen/passport/pkg/models"
|
"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) {
|
func LookupFactor(id uint) (models.AuthFactor, error) {
|
||||||
var factor models.AuthFactor
|
var factor models.AuthFactor
|
||||||
err := database.C.Where(models.AuthFactor{
|
err := database.C.Where(models.AuthFactor{
|
||||||
@ -22,3 +40,30 @@ func LookupFactorsByUser(uid uint) ([]models.AuthFactor, error) {
|
|||||||
|
|
||||||
return factors, err
|
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"
|
"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) {
|
func NewMagicToken(mode models.MagicTokenType, assignTo *models.Account, expiredAt *time.Time) (models.MagicToken, error) {
|
||||||
var uid uint
|
var uid uint
|
||||||
if assignTo != nil {
|
if assignTo != nil {
|
||||||
@ -36,8 +63,8 @@ func NotifyMagicToken(token models.MagicToken) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var user models.Account
|
var user models.Account
|
||||||
if err := database.C.Where(&models.MagicToken{
|
if err := database.C.Where(&models.Account{
|
||||||
AssignTo: token.AssignTo,
|
BaseModel: models.BaseModel{ID: *token.AssignTo},
|
||||||
}).Preload("Contacts").First(&user).Error; err != nil {
|
}).Preload("Contacts").First(&user).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -46,15 +73,16 @@ func NotifyMagicToken(token models.MagicToken) error {
|
|||||||
var content string
|
var content string
|
||||||
switch token.Type {
|
switch token.Type {
|
||||||
case models.ConfirmMagicToken:
|
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"))
|
subject = fmt.Sprintf("[%s] Confirm your registration", viper.GetString("name"))
|
||||||
content = fmt.Sprintf("We got a create account request with this email recently.\n"+
|
content = fmt.Sprintf(
|
||||||
"So we need you to click the link below to confirm your registeration.\n"+
|
ConfirmRegistrationTemplate,
|
||||||
"Confirmnation Link: %s\n"+
|
user.Name,
|
||||||
"If you didn't do that, you can ignore this email.\n\n"+
|
viper.GetString("name"),
|
||||||
"%s\n"+
|
viper.GetString("maintainer"),
|
||||||
"Best wishes",
|
link,
|
||||||
link, viper.GetString("maintainer"))
|
viper.GetString("maintainer"),
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported magic token type to notify")
|
return fmt.Errorf("unsupported magic token type to notify")
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,15 @@ bind = "0.0.0.0:8444"
|
|||||||
domain = "id.smartsheep.studio"
|
domain = "id.smartsheep.studio"
|
||||||
secret = "LtTjzAGFLshwXhN4ZD4nG5KlMv1MWcsvfv03TSZYnT1VhiAnLIZFTnHUwR0XhGgi"
|
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]
|
[database]
|
||||||
dsn = "host=localhost dbname=hy_passport port=5432 sslmode=disable"
|
dsn = "host=localhost dbname=hy_passport port=5432 sslmode=disable"
|
||||||
prefix = "passport_"
|
prefix = "passport_"
|
||||||
|
@ -5,5 +5,4 @@
|
|||||||
html, body {
|
html, body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: 100vh;
|
|
||||||
}
|
}
|
@ -9,14 +9,20 @@ import { lazy } from "solid-js";
|
|||||||
import { Route, Router } from "@solidjs/router";
|
import { Route, Router } from "@solidjs/router";
|
||||||
|
|
||||||
import RootLayout from "./layouts/RootLayout.tsx";
|
import RootLayout from "./layouts/RootLayout.tsx";
|
||||||
|
import { UserinfoProvider } from "./stores/userinfo.tsx";
|
||||||
|
import { WellKnownProvider } from "./stores/wellKnown.tsx";
|
||||||
|
|
||||||
const root = document.getElementById("root");
|
const root = document.getElementById("root");
|
||||||
|
|
||||||
render(() => (
|
render(() => (
|
||||||
<Router root={RootLayout}>
|
<WellKnownProvider>
|
||||||
<Route path="/" component={lazy(() => import("./pages/dashboard.tsx"))} />
|
<UserinfoProvider>
|
||||||
<Route path="/auth/login" component={lazy(() => import("./pages/auth/login.tsx"))} />
|
<Router root={RootLayout}>
|
||||||
<Route path="/auth/register" component={lazy(() => import("./pages/auth/register.tsx"))} />
|
<Route path="/" component={lazy(() => import("./pages/dashboard.tsx"))} />
|
||||||
<Route path="/users/me/confirm" component={lazy(() => import("./pages/users/confirm.tsx"))} />
|
<Route path="/auth/login" component={lazy(() => import("./pages/auth/login.tsx"))} />
|
||||||
</Router>
|
<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!);
|
), root!);
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import Navbar from "./shared/Navbar.tsx";
|
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 { createSignal, Show } from "solid-js";
|
||||||
|
import { readWellKnown } from "../stores/wellKnown.tsx";
|
||||||
|
|
||||||
export default function RootLayout(props: any) {
|
export default function RootLayout(props: any) {
|
||||||
const [ready, setReady] = createSignal(false);
|
const [ready, setReady] = createSignal(false);
|
||||||
|
|
||||||
readProfiles().then(() => setReady(true));
|
Promise.all([readWellKnown(), readProfiles()]).then(() => setReady(true));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={ready()} fallback={
|
<Show when={ready()} fallback={
|
||||||
@ -15,11 +16,8 @@ export default function RootLayout(props: any) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
<UserinfoProvider>
|
<Navbar />
|
||||||
<Navbar />
|
<main class="h-[calc(100vh-68px)] mt-[68px]">{props.children}</main>
|
||||||
|
|
||||||
<main class="h-[calc(100vh-68px)]">{props.children}</main>
|
|
||||||
</UserinfoProvider>
|
|
||||||
</Show>
|
</Show>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { For, Match, Switch } from "solid-js";
|
import { For, Match, Switch } from "solid-js";
|
||||||
import { clearUserinfo, useUserinfo } from "../../stores/userinfo.tsx";
|
import { clearUserinfo, useUserinfo } from "../../stores/userinfo.tsx";
|
||||||
import { useNavigate } from "@solidjs/router";
|
import { useNavigate } from "@solidjs/router";
|
||||||
|
import { useWellKnown } from "../../stores/wellKnown.tsx";
|
||||||
|
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
label: string;
|
label: string;
|
||||||
@ -10,6 +11,7 @@ interface MenuItem {
|
|||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
const nav: MenuItem[] = [{ label: "Dashboard", href: "/" }];
|
const nav: MenuItem[] = [{ label: "Dashboard", href: "/" }];
|
||||||
|
|
||||||
|
const wellKnown = useWellKnown();
|
||||||
const userinfo = useUserinfo();
|
const userinfo = useUserinfo();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@ -19,7 +21,7 @@ export default function Navbar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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="navbar-start">
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<div tabIndex={0} role="button" class="btn btn-ghost lg:hidden">
|
<div tabIndex={0} role="button" class="btn btn-ghost lg:hidden">
|
||||||
@ -52,7 +54,7 @@ export default function Navbar() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<a href="/" class="btn btn-ghost text-xl">
|
<a href="/" class="btn btn-ghost text-xl">
|
||||||
Goatpass
|
{wellKnown?.name ?? "Goatpass"}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-center hidden lg:flex">
|
<div class="navbar-center hidden lg:flex">
|
||||||
|
@ -50,7 +50,7 @@ export default function LoginPage() {
|
|||||||
if (!data.factor) return;
|
if (!data.factor) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await fetch(`/api/auth/factors/${data.id}`, {
|
const res = await fetch(`/api/auth/factors/${data.factor}`, {
|
||||||
method: "POST"
|
method: "POST"
|
||||||
});
|
});
|
||||||
if (res.status !== 200 && res.status !== 204) {
|
if (res.status !== 200 && res.status !== 204) {
|
||||||
@ -93,6 +93,7 @@ export default function LoginPage() {
|
|||||||
setStage("choosing");
|
setStage("choosing");
|
||||||
setTitle("Continue verifying");
|
setTitle("Continue verifying");
|
||||||
setSubtitle("You passed one check, but that's not enough.");
|
setSubtitle("You passed one check, but that's not enough.");
|
||||||
|
setChallenge(data["challenge"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setLoading(false);
|
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) {
|
function getFactorName(factor: any) {
|
||||||
switch (factor.type) {
|
switch (factor.type) {
|
||||||
case 0:
|
case 0:
|
||||||
return "Password Verification";
|
return "Password Verification";
|
||||||
|
case 1:
|
||||||
|
return "Email Verification Code";
|
||||||
default:
|
default:
|
||||||
return "Unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
@ -171,6 +179,7 @@ export default function LoginPage() {
|
|||||||
{item =>
|
{item =>
|
||||||
<input class="join-item btn" type="radio" name="factor"
|
<input class="join-item btn" type="radio" name="factor"
|
||||||
value={item.id}
|
value={item.id}
|
||||||
|
disabled={getFactorAvailable(item)}
|
||||||
aria-label={getFactorName(item)}
|
aria-label={getFactorName(item)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { createSignal, Show } from "solid-js";
|
import { createSignal, Show } from "solid-js";
|
||||||
|
import { useWellKnown } from "../../stores/wellKnown.tsx";
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
const [title, setTitle] = createSignal("Create an account");
|
const [title, setTitle] = createSignal("Create an account");
|
||||||
@ -8,6 +9,8 @@ export default function RegisterPage() {
|
|||||||
const [loading, setLoading] = createSignal(false);
|
const [loading, setLoading] = createSignal(false);
|
||||||
const [done, setDone] = createSignal(false);
|
const [done, setDone] = createSignal(false);
|
||||||
|
|
||||||
|
const metadata = useWellKnown();
|
||||||
|
|
||||||
async function submit(evt: SubmitEvent) {
|
async function submit(evt: SubmitEvent) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|
||||||
@ -23,6 +26,7 @@ export default function RegisterPage() {
|
|||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
setError(await res.text());
|
setError(await res.text());
|
||||||
} else {
|
} else {
|
||||||
|
setError(null);
|
||||||
setTitle("Congratulations!");
|
setTitle("Congratulations!");
|
||||||
setSubtitle("Your account has been created and activation email has sent to your inbox!");
|
setSubtitle("Your account has been created and activation email has sent to your inbox!");
|
||||||
setDone(true);
|
setDone(true);
|
||||||
@ -32,83 +36,100 @@ export default function RegisterPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="w-full h-full flex justify-center items-center">
|
<div class="w-full h-full flex justify-center items-center">
|
||||||
<div class="card w-[480px] max-w-screen shadow-xl">
|
<div>
|
||||||
<div class="card-body">
|
<div class="card w-[480px] max-w-screen shadow-xl">
|
||||||
<div id="header" class="text-center mb-5">
|
<div class="card-body">
|
||||||
<h1 class="text-xl font-bold">{title()}</h1>
|
<div id="header" class="text-center mb-5">
|
||||||
<p>{subtitle()}</p>
|
<h1 class="text-xl font-bold">{title()}</h1>
|
||||||
</div>
|
<p>{subtitle()}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Show when={error()}>
|
<Show when={error()}>
|
||||||
<div id="alerts" class="mt-1">
|
<div id="alerts" class="mt-1">
|
||||||
<div role="alert" class="alert alert-error">
|
<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"
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
|
||||||
viewBox="0 0 24 24">
|
viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<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" />
|
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="capitalize">{error()}</span>
|
<span class="capitalize">{error()}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Show>
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={!done()}>
|
<Show when={!done()}>
|
||||||
<form id="form" onSubmit={submit}>
|
<form id="form" onSubmit={submit}>
|
||||||
<label class="form-control w-full">
|
<label class="form-control w-full">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text">Username</span>
|
<span class="label-text">Username</span>
|
||||||
<span class="label-text-alt font-bold">Cannot be modify</span>
|
<span class="label-text-alt font-bold">Cannot be modify</span>
|
||||||
</div>
|
</div>
|
||||||
<input name="name" type="text" placeholder="Type here" class="input input-bordered w-full" />
|
<input name="name" type="text" placeholder="Type here" class="input input-bordered w-full" />
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text-alt">Lowercase alphabet and numbers only, maximum 16 characters</span>
|
<span class="label-text-alt">Lowercase alphabet and numbers only, maximum 16 characters</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<label class="form-control w-full">
|
<label class="form-control w-full">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text">Nickname</span>
|
<span class="label-text">Nickname</span>
|
||||||
</div>
|
</div>
|
||||||
<input name="nick" type="text" placeholder="Type here" class="input input-bordered w-full" />
|
<input name="nick" type="text" placeholder="Type here" class="input input-bordered w-full" />
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text-alt">Maximum length is 24 characters</span>
|
<span class="label-text-alt">Maximum length is 24 characters</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<label class="form-control w-full">
|
<label class="form-control w-full">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text">Email Address</span>
|
<span class="label-text">Email Address</span>
|
||||||
</div>
|
</div>
|
||||||
<input name="email" type="email" placeholder="Type here" class="input input-bordered w-full" />
|
<input name="email" type="email" placeholder="Type here" class="input input-bordered w-full" />
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text-alt">Do not accept address with plus sign</span>
|
<span class="label-text-alt">Do not accept address with plus sign</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<label class="form-control w-full">
|
<label class="form-control w-full">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text">Password</span>
|
<span class="label-text">Password</span>
|
||||||
</div>
|
</div>
|
||||||
<input name="password" type="password" placeholder="Type here" class="input input-bordered w-full" />
|
<input name="password" type="password" placeholder="Type here"
|
||||||
<div class="label">
|
class="input input-bordered w-full" />
|
||||||
<span class="label-text-alt">Must be secure</span>
|
<div class="label">
|
||||||
</div>
|
<span class="label-text-alt">Must be secure</span>
|
||||||
</label>
|
</div>
|
||||||
|
</label>
|
||||||
<button type="submit" class="btn btn-primary btn-block mt-3" disabled={loading()}>
|
<Show when={!metadata?.open_registration}>
|
||||||
<Show when={loading()} fallback={"Next"}>
|
<label class="form-control w-full">
|
||||||
<span class="loading loading-spinner"></span>
|
<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>
|
</Show>
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={done()}>
|
<button type="submit" class="btn btn-primary btn-block mt-3" disabled={loading()}>
|
||||||
<div class="py-12 text-center">
|
<Show when={loading()} fallback={"Next"}>
|
||||||
<h2 class="text-lg font-bold">What's next?</h2>
|
<span class="loading loading-spinner"></span>
|
||||||
<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>{" "}
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-sm text-center mt-3">
|
<div class="text-sm text-center mt-3">
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { createSignal, Show } from "solid-js";
|
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() {
|
export default function ConfirmRegistrationPage() {
|
||||||
const [error, setError] = createSignal<string | null>(null);
|
const [error, setError] = createSignal<string | null>(null);
|
||||||
@ -7,6 +8,8 @@ export default function ConfirmRegistrationPage() {
|
|||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
async function doConfirm() {
|
async function doConfirm() {
|
||||||
if (!searchParams["tk"]) {
|
if (!searchParams["tk"]) {
|
||||||
setError("Bad Request: Code was not exists");
|
setError("Bad Request: Code was not exists");
|
||||||
@ -23,6 +26,8 @@ export default function ConfirmRegistrationPage() {
|
|||||||
setError(await res.text());
|
setError(await res.text());
|
||||||
} else {
|
} else {
|
||||||
setStatus("Confirmed. Redirecting to dashboard...");
|
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()],
|
plugins: [devtools({ autoname: true }), solid()],
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": "http://localhost:8444"
|
"/api": "http://localhost:8444",
|
||||||
|
"/.well-known": "http://localhost:8444"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user