✨ Account confirm
This commit is contained in:
parent
d4aef5277f
commit
20119cb177
@ -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
|
|
1
go.mod
1
go.mod
@ -7,6 +7,7 @@ require (
|
|||||||
github.com/gofiber/fiber/v2 v2.52.0
|
github.com/gofiber/fiber/v2 v2.52.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||||
github.com/google/uuid v1.5.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/json-iterator/go v1.1.12
|
||||||
github.com/rs/zerolog v1.31.0
|
github.com/rs/zerolog v1.31.0
|
||||||
github.com/samber/lo v1.39.0
|
github.com/samber/lo v1.39.0
|
||||||
|
2
go.sum
2
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/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 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||||
|
@ -23,7 +23,7 @@ func main() {
|
|||||||
viper.AddConfigPath(".")
|
viper.AddConfigPath(".")
|
||||||
viper.AddConfigPath("..")
|
viper.AddConfigPath("..")
|
||||||
viper.SetConfigName("settings")
|
viper.SetConfigName("settings")
|
||||||
viper.SetConfigType("yaml")
|
viper.SetConfigType("toml")
|
||||||
|
|
||||||
// Load settings
|
// Load settings
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
@ -13,6 +13,7 @@ func RunMigration(source *gorm.DB) error {
|
|||||||
&models.AccountContact{},
|
&models.AccountContact{},
|
||||||
&models.AuthSession{},
|
&models.AuthSession{},
|
||||||
&models.AuthChallenge{},
|
&models.AuthChallenge{},
|
||||||
|
&models.MagicToken{},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/samber/lo"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/datatypes"
|
"gorm.io/datatypes"
|
||||||
@ -24,10 +25,18 @@ type Account struct {
|
|||||||
Challenges []AuthChallenge `json:"challenges"`
|
Challenges []AuthChallenge `json:"challenges"`
|
||||||
Factors []AuthFactor `json:"factors"`
|
Factors []AuthFactor `json:"factors"`
|
||||||
Contacts []AccountContact `json:"contacts"`
|
Contacts []AccountContact `json:"contacts"`
|
||||||
|
MagicTokens []MagicToken `json:"-" gorm:"foreignKey:AssignTo"`
|
||||||
ConfirmedAt *time.Time `json:"confirmed_at"`
|
ConfirmedAt *time.Time `json:"confirmed_at"`
|
||||||
Permissions datatypes.JSONType[[]string] `json:"permissions"`
|
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
|
type AccountContactType = int8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -18,7 +18,7 @@ type AuthFactor struct {
|
|||||||
BaseModel
|
BaseModel
|
||||||
|
|
||||||
Type int8 `json:"type"`
|
Type int8 `json:"type"`
|
||||||
Secret string `json:"secret"`
|
Secret string `json:"-"`
|
||||||
Config JSONMap `json:"config"`
|
Config JSONMap `json:"config"`
|
||||||
AccountID uint `json:"account_id"`
|
AccountID uint `json:"account_id"`
|
||||||
}
|
}
|
||||||
|
19
pkg/models/tokens.go
Normal file
19
pkg/models/tokens.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type MagicTokenType = int8
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConfirmMagicToken = MagicTokenType(iota)
|
||||||
|
RegistrationMagicToken
|
||||||
|
)
|
||||||
|
|
||||||
|
type MagicToken struct {
|
||||||
|
BaseModel
|
||||||
|
|
||||||
|
Code string `json:"code"`
|
||||||
|
Type int8 `json:"type"`
|
||||||
|
AssignTo *uint `json:"assign_to"`
|
||||||
|
ExpiredAt *time.Time `json:"expired_at"`
|
||||||
|
}
|
@ -44,3 +44,19 @@ func doRegister(c *fiber.Ctx) error {
|
|||||||
return c.JSON(user)
|
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)
|
||||||
|
}
|
||||||
|
@ -19,10 +19,13 @@ func NewServer() {
|
|||||||
JSONDecoder: jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal,
|
JSONDecoder: jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
A.Get("/.well-known", getMetadata)
|
||||||
|
|
||||||
api := A.Group("/api").Name("API")
|
api := A.Group("/api").Name("API")
|
||||||
{
|
{
|
||||||
api.Get("/users/me", auth, getPrincipal)
|
api.Get("/users/me", auth, getPrincipal)
|
||||||
api.Post("/users", doRegister)
|
api.Post("/users", doRegister)
|
||||||
|
api.Post("/users/me/confirm", doRegisterConfirm)
|
||||||
|
|
||||||
api.Put("/auth", startChallenge)
|
api.Put("/auth", startChallenge)
|
||||||
api.Post("/auth", doChallenge)
|
api.Post("/auth", doChallenge)
|
||||||
|
13
pkg/server/well_known_api.go
Normal file
13
pkg/server/well_known_api.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getMetadata(c *fiber.Ctx) error {
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"name": viper.GetString("name"),
|
||||||
|
"domain": viper.GetString("domain"),
|
||||||
|
})
|
||||||
|
}
|
@ -5,7 +5,10 @@ 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/samber/lo"
|
||||||
"gorm.io/datatypes"
|
"gorm.io/datatypes"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetAccount(id uint) (models.Account, error) {
|
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 {
|
if err := database.C.Create(&user).Error; err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
} else {
|
|
||||||
return user, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tk, err := NewMagicToken(models.ConfirmMagicToken, &user, nil); err != nil {
|
||||||
|
return user, err
|
||||||
|
} else if err := NotifyMagicToken(tk); err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfirmAccount(code string) error {
|
||||||
|
var token models.MagicToken
|
||||||
|
if err := database.C.Where(&models.MagicToken{
|
||||||
|
Code: code,
|
||||||
|
Type: models.ConfirmMagicToken,
|
||||||
|
}).First(&token).Error; err != nil {
|
||||||
|
return err
|
||||||
|
} else if token.AssignTo == nil {
|
||||||
|
return fmt.Errorf("account was not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
var user models.Account
|
||||||
|
if err := database.C.Where(&models.Account{
|
||||||
|
BaseModel: models.BaseModel{ID: *token.AssignTo},
|
||||||
|
}).First(&user).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return database.C.Transaction(func(tx *gorm.DB) error {
|
||||||
|
user.ConfirmedAt = lo.ToPtr(time.Now())
|
||||||
|
|
||||||
|
if err := database.C.Delete(&token).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := database.C.Save(&user).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
51
pkg/services/mailer.go
Normal file
51
pkg/services/mailer.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net/smtp"
|
||||||
|
"net/textproto"
|
||||||
|
|
||||||
|
"github.com/jordan-wright/email"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SendMail(target string, subject string, content string) error {
|
||||||
|
mail := &email.Email{
|
||||||
|
To: []string{target},
|
||||||
|
From: viper.GetString("mailer.name"),
|
||||||
|
Subject: subject,
|
||||||
|
Text: []byte(content),
|
||||||
|
Headers: textproto.MIMEHeader{},
|
||||||
|
}
|
||||||
|
return mail.SendWithTLS(
|
||||||
|
fmt.Sprintf("%s:%d", viper.GetString("mailer.smtp_host"), viper.GetInt("mailer.smtp_port")),
|
||||||
|
smtp.PlainAuth(
|
||||||
|
"",
|
||||||
|
viper.GetString("mailer.username"),
|
||||||
|
viper.GetString("mailer.password"),
|
||||||
|
viper.GetString("mailer.smtp_host"),
|
||||||
|
),
|
||||||
|
&tls.Config{ServerName: viper.GetString("mailer.smtp_host")},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendMailHTML(target string, subject string, content string) error {
|
||||||
|
mail := &email.Email{
|
||||||
|
To: []string{target},
|
||||||
|
From: viper.GetString("mailer.name"),
|
||||||
|
Subject: subject,
|
||||||
|
HTML: []byte(content),
|
||||||
|
Headers: textproto.MIMEHeader{},
|
||||||
|
}
|
||||||
|
return mail.SendWithTLS(
|
||||||
|
fmt.Sprintf("%s:%d", viper.GetString("mailer.smtp_host"), viper.GetInt("mailer.smtp_port")),
|
||||||
|
smtp.PlainAuth(
|
||||||
|
"",
|
||||||
|
viper.GetString("mailer.username"),
|
||||||
|
viper.GetString("mailer.password"),
|
||||||
|
viper.GetString("mailer.smtp_host"),
|
||||||
|
),
|
||||||
|
&tls.Config{ServerName: viper.GetString("mailer.smtp_host")},
|
||||||
|
)
|
||||||
|
}
|
63
pkg/services/tokens.go
Normal file
63
pkg/services/tokens.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.smartsheep.studio/hydrogen/passport/pkg/database"
|
||||||
|
"code.smartsheep.studio/hydrogen/passport/pkg/models"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewMagicToken(mode models.MagicTokenType, assignTo *models.Account, expiredAt *time.Time) (models.MagicToken, error) {
|
||||||
|
var uid uint
|
||||||
|
if assignTo != nil {
|
||||||
|
uid = assignTo.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
token := models.MagicToken{
|
||||||
|
Code: strings.Replace(uuid.NewString(), "-", "", -1),
|
||||||
|
Type: mode,
|
||||||
|
AssignTo: &uid,
|
||||||
|
ExpiredAt: expiredAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.C.Save(&token).Error; err != nil {
|
||||||
|
return token, err
|
||||||
|
} else {
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotifyMagicToken(token models.MagicToken) error {
|
||||||
|
if token.AssignTo == nil {
|
||||||
|
return fmt.Errorf("could notify a non-assign magic token")
|
||||||
|
}
|
||||||
|
|
||||||
|
var user models.Account
|
||||||
|
if err := database.C.Where(&models.MagicToken{
|
||||||
|
AssignTo: token.AssignTo,
|
||||||
|
}).Preload("Contacts").First(&user).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var subject string
|
||||||
|
var content string
|
||||||
|
switch token.Type {
|
||||||
|
case models.ConfirmMagicToken:
|
||||||
|
link := fmt.Sprintf("%s/users/me/confirm?tk=%s", viper.GetString("domain"), token.Code)
|
||||||
|
subject = fmt.Sprintf("[%s] Confirm your registration", viper.GetString("name"))
|
||||||
|
content = fmt.Sprintf("We got a create account request with this email recently.\n"+
|
||||||
|
"So we need you to click the link below to confirm your registeration.\n"+
|
||||||
|
"Confirmnation Link: %s\n"+
|
||||||
|
"If you didn't do that, you can ignore this email.\n\n"+
|
||||||
|
"%s\n"+
|
||||||
|
"Best wishes",
|
||||||
|
link, viper.GetString("maintainer"))
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported magic token type to notify")
|
||||||
|
}
|
||||||
|
|
||||||
|
return SendMail(user.GetPrimaryEmail().Content, subject, content)
|
||||||
|
}
|
12
settings.toml
Normal file
12
settings.toml
Normal file
@ -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_"
|
@ -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_
|
|
@ -5,19 +5,18 @@ import { render } from "solid-js/web";
|
|||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import "./assets/fonts/fonts.css";
|
import "./assets/fonts/fonts.css";
|
||||||
|
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 DashboardPage from "./pages/dashboard.tsx";
|
|
||||||
import LoginPage from "./pages/auth/login.tsx";
|
|
||||||
import RegisterPage from "./pages/auth/register.tsx";
|
|
||||||
|
|
||||||
const root = document.getElementById("root");
|
const root = document.getElementById("root");
|
||||||
|
|
||||||
render(() => (
|
render(() => (
|
||||||
<Router root={RootLayout}>
|
<Router root={RootLayout}>
|
||||||
<Route path="/" component={DashboardPage} />
|
<Route path="/" component={lazy(() => import("./pages/dashboard.tsx"))} />
|
||||||
<Route path="/auth/login" component={LoginPage} />
|
<Route path="/auth/login" component={lazy(() => import("./pages/auth/login.tsx"))} />
|
||||||
<Route path="/auth/register" component={RegisterPage} />
|
<Route path="/auth/register" component={lazy(() => import("./pages/auth/register.tsx"))} />
|
||||||
|
<Route path="/users/me/confirm" component={lazy(() => import("./pages/users/confirm.tsx"))} />
|
||||||
</Router>
|
</Router>
|
||||||
), root!);
|
), root!);
|
||||||
|
@ -110,6 +110,10 @@ export default function RegisterPage() {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm text-center mt-3">
|
||||||
|
<a href="/auth/login" class="link">Already had an account? Login now!</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useUserinfo } from "../stores/userinfo.tsx";
|
import { useUserinfo } from "../stores/userinfo.tsx";
|
||||||
|
import { Show } from "solid-js";
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const userinfo = useUserinfo();
|
const userinfo = useUserinfo();
|
||||||
@ -7,6 +8,22 @@ export default function DashboardPage() {
|
|||||||
<div class="container mx-auto pt-12">
|
<div class="container mx-auto pt-12">
|
||||||
<h1 class="text-2xl font-bold">Welcome, {userinfo?.displayName}</h1>
|
<h1 class="text-2xl font-bold">Welcome, {userinfo?.displayName}</h1>
|
||||||
<p>What's a nice day!</p>
|
<p>What's a nice day!</p>
|
||||||
|
|
||||||
|
<div id="alerts">
|
||||||
|
<Show when={!userinfo?.meta?.confirmed_at}>
|
||||||
|
<div role="alert" class="alert alert-warning mt-5">
|
||||||
|
<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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<span>Your account isn't confirmed yet. Please check your inbox and confirm your account.</span> <br />
|
||||||
|
<span>Otherwise your account will be deactivate after 48 hours.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
65
view/src/pages/users/confirm.tsx
Normal file
65
view/src/pages/users/confirm.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { createSignal, Show } from "solid-js";
|
||||||
|
import { useSearchParams } from "@solidjs/router";
|
||||||
|
|
||||||
|
export default function ConfirmRegistrationPage() {
|
||||||
|
const [error, setError] = createSignal<string | null>(null);
|
||||||
|
const [status, setStatus] = createSignal("Confirming your account...");
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
async function doConfirm() {
|
||||||
|
if (!searchParams["tk"]) {
|
||||||
|
setError("Bad Request: Code was not exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch("/api/users/me/confirm", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
code: searchParams["tk"]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (res.status !== 200) {
|
||||||
|
setError(await res.text());
|
||||||
|
} else {
|
||||||
|
setStatus("Confirmed. Redirecting to dashboard...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doConfirm();
|
||||||
|
|
||||||
|
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">Confirm your account</h1>
|
||||||
|
<p>Hold on, we are working on it. Almost finished.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-16 text-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<div>
|
||||||
|
<span class="loading loading-lg loading-bars"></span>
|
||||||
|
</div>
|
||||||
|
<span>{status()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={error()} fallback={<div class="mt-16"></div>}>
|
||||||
|
<div id="alerts" class="mt-16">
|
||||||
|
<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>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user