Sign up & Sign in

This commit is contained in:
2024-04-20 22:50:09 +08:00
parent 87cccefddb
commit e79441dbc5
26 changed files with 706 additions and 463 deletions

View File

@ -1,7 +1,12 @@
{
"next": "Next",
"email": "Email",
"username": "Username",
"nickname": "Nickname",
"password": "Password",
"magicToken": "Magic Token",
"signinTitle": "Sign In",
"signinCaption": "Sign in to Solarpass to explore entire Solar Network. Explore posts, discover communities, talk with your best friends. All these things in the Solar Network!"
"signinCaption": "Sign in to Solarpass to explore entire Solar Network. Explore posts, discover communities, talk with your best friends. All these things in the Solar Network!",
"signupTitle": "Sign Up",
"signupCaption": "Sign up to create an account on Solarpass, then you can explore the entire Solar Network! Enjoy the next-generation Internet Ecosystem!"
}

View File

@ -1,7 +1,12 @@
{
"next": "下一步",
"email": "邮件地址",
"username": "用户名",
"nickname": "昵称",
"password": "密码",
"magicToken": "魔法令牌",
"signinTitle": "登陆",
"signinCaption": "登陆 Solarpass 以探索整个 Solar Network浏览帖子、探索社区、和你的好朋友聊八卦一切尽在 Solar Network!"
"signinCaption": "登陆 Solarpass 以探索整个 Solar Network浏览帖子、探索社区、和你的好朋友聊八卦一切尽在 Solar Network!",
"signupTitle": "注册",
"signupCaption": "注册以在 Solarpass 创建一个账号,之后你就可以探索整个 Solar Network享受下一代互联网生态系统"
}

View File

@ -2,6 +2,7 @@ package server
import (
"fmt"
"git.solsynth.dev/hydrogen/passport/pkg/utils"
"strconv"
"time"
@ -83,7 +84,7 @@ func editUserinfo(c *fiber.Ctx) error {
Birthday time.Time `json:"birthday"`
}
if err := BindAndValidate(c, &data); err != nil {
if err := utils.BindAndValidate(c, &data); err != nil {
return err
}
@ -133,7 +134,7 @@ func doRegister(c *fiber.Ctx) error {
MagicToken string `json:"magic_token"`
}
if err := BindAndValidate(c, &data); err != nil {
if err := utils.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")
@ -162,7 +163,7 @@ func doRegisterConfirm(c *fiber.Ctx) error {
Code string `json:"code" validate:"required"`
}
if err := BindAndValidate(c, &data); err != nil {
if err := utils.BindAndValidate(c, &data); err != nil {
return err
}

View File

@ -2,6 +2,7 @@ package server
import (
"fmt"
"git.solsynth.dev/hydrogen/passport/pkg/utils"
"time"
"github.com/gofiber/fiber/v2"
@ -15,7 +16,7 @@ func doAuthenticate(c *fiber.Ctx) error {
Password string `json:"password" validate:"required"`
}
if err := BindAndValidate(c, &data); err != nil {
if err := utils.BindAndValidate(c, &data); err != nil {
return err
}
@ -47,7 +48,7 @@ func doMultiFactorAuthenticate(c *fiber.Ctx) error {
Code string `json:"code" validate:"required"`
}
if err := BindAndValidate(c, &data); err != nil {
if err := utils.BindAndValidate(c, &data); err != nil {
return err
}
@ -84,7 +85,7 @@ func getToken(c *fiber.Ctx) error {
GrantType string `json:"grant_type" form:"grant_type"`
}
if err := BindAndValidate(c, &data); err != nil {
if err := utils.BindAndValidate(c, &data); err != nil {
return err
}

View File

@ -3,6 +3,7 @@ package server
import (
"git.solsynth.dev/hydrogen/passport/pkg/models"
"git.solsynth.dev/hydrogen/passport/pkg/services"
"git.solsynth.dev/hydrogen/passport/pkg/utils"
"github.com/gofiber/fiber/v2"
)
@ -70,7 +71,7 @@ func editFriendship(c *fiber.Ctx) error {
Status uint8 `json:"status"`
}
if err := BindAndValidate(c, &data); err != nil {
if err := utils.BindAndValidate(c, &data); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}

View File

@ -1,6 +1,7 @@
package server
import (
"git.solsynth.dev/hydrogen/passport/pkg/utils"
"time"
"git.solsynth.dev/hydrogen/passport/pkg/database"
@ -72,7 +73,7 @@ func addNotifySubscriber(c *fiber.Ctx) error {
DeviceID string `json:"device_id" validate:"required"`
}
if err := BindAndValidate(c, &data); err != nil {
if err := utils.BindAndValidate(c, &data); err != nil {
return err
}

View File

@ -3,6 +3,7 @@ package server
import (
"git.solsynth.dev/hydrogen/passport/pkg/models"
"git.solsynth.dev/hydrogen/passport/pkg/services"
"git.solsynth.dev/hydrogen/passport/pkg/utils"
"github.com/gofiber/fiber/v2"
)
@ -18,7 +19,7 @@ func notifyUser(c *fiber.Ctx) error {
UserID uint `json:"user_id" validate:"required"`
}
if err := BindAndValidate(c, &data); err != nil {
if err := utils.BindAndValidate(c, &data); err != nil {
return err
}

View File

@ -3,6 +3,7 @@ package server
import (
"git.solsynth.dev/hydrogen/passport/pkg/database"
"git.solsynth.dev/hydrogen/passport/pkg/models"
"git.solsynth.dev/hydrogen/passport/pkg/utils"
"github.com/gofiber/fiber/v2"
)
@ -47,7 +48,7 @@ func editPersonalPage(c *fiber.Ctx) error {
Links []models.AccountPageLinks `json:"links"`
}
if err := BindAndValidate(c, &data); err != nil {
if err := utils.BindAndValidate(c, &data); err != nil {
return err
}

View File

@ -1,26 +0,0 @@
package ui
import (
"github.com/gofiber/fiber/v2"
"github.com/nicksnyder/go-i18n/v2/i18n"
)
func signinPage(c *fiber.Ctx) error {
localizer := c.Locals("localizer").(*i18n.Localizer)
next, _ := localizer.LocalizeMessage(&i18n.Message{ID: "next"})
username, _ := localizer.LocalizeMessage(&i18n.Message{ID: "username"})
password, _ := localizer.LocalizeMessage(&i18n.Message{ID: "password"})
title, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signinTitle"})
caption, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signinCaption"})
return c.Render("views/signin", fiber.Map{
"i18n": fiber.Map{
"next": next,
"username": username,
"password": password,
"title": title,
"caption": caption,
},
}, "views/layouts/auth")
}

View File

@ -4,5 +4,10 @@ import "github.com/gofiber/fiber/v2"
func MapUserInterface(A *fiber.App) {
pages := A.Group("/").Name("Pages")
pages.Get("/sign-up", signupPage)
pages.Get("/sign-in", signinPage)
pages.Post("/sign-up", signupAction)
pages.Post("/sign-in", signinAction)
}

77
pkg/server/ui/signin.go Normal file
View File

@ -0,0 +1,77 @@
package ui
import (
"fmt"
"git.solsynth.dev/hydrogen/passport/pkg/services"
"git.solsynth.dev/hydrogen/passport/pkg/utils"
"github.com/gofiber/fiber/v2"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/sujit-baniya/flash"
)
func signinPage(c *fiber.Ctx) error {
localizer := c.Locals("localizer").(*i18n.Localizer)
next, _ := localizer.LocalizeMessage(&i18n.Message{ID: "next"})
username, _ := localizer.LocalizeMessage(&i18n.Message{ID: "username"})
password, _ := localizer.LocalizeMessage(&i18n.Message{ID: "password"})
signup, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signupTitle"})
title, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signinTitle"})
caption, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signinCaption"})
return c.Render("views/signin", fiber.Map{
"info": flash.Get(c)["message"],
"i18n": fiber.Map{
"next": next,
"username": username,
"password": password,
"signup": signup,
"title": title,
"caption": caption,
},
}, "views/layouts/auth")
}
func signinAction(c *fiber.Ctx) error {
var data struct {
Username string `form:"username" validate:"required"`
Password string `form:"password" validate:"required"`
}
if err := utils.BindAndValidate(c, &data); err != nil {
return flash.WithInfo(c, fiber.Map{
"message": err.Error(),
}).Redirect("/sign-in")
}
user, err := services.LookupAccount(data.Username)
if err != nil {
return flash.WithInfo(c, fiber.Map{
"message": fmt.Sprintf("account was not found: %v", err.Error()),
}).Redirect("/sign-in")
}
ticket, err := services.NewTicket(user, c.IP(), c.Get(fiber.HeaderUserAgent))
if err != nil {
return flash.WithInfo(c, fiber.Map{
"message": fmt.Sprintf("unable setup ticket: %v", err.Error()),
}).Redirect("/sign-in")
}
ticket, err = services.ActiveTicketWithPassword(ticket, data.Password)
if err != nil {
return flash.WithInfo(c, fiber.Map{
"message": fmt.Sprintf("invalid password: %v", err.Error()),
}).Redirect("/sign-in")
}
if ticket.IsAvailable() != nil {
return flash.WithInfo(c, fiber.Map{
"message": "multi factor authenticate required",
}).Redirect("/sign-in")
} else {
return flash.WithInfo(c, fiber.Map{
"message": "done",
}).Redirect("/sign-in")
}
}

86
pkg/server/ui/signup.go Normal file
View File

@ -0,0 +1,86 @@
package ui
import (
"fmt"
"git.solsynth.dev/hydrogen/passport/pkg/database"
"git.solsynth.dev/hydrogen/passport/pkg/models"
"git.solsynth.dev/hydrogen/passport/pkg/services"
"git.solsynth.dev/hydrogen/passport/pkg/utils"
"github.com/gofiber/fiber/v2"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/spf13/viper"
"github.com/sujit-baniya/flash"
)
func signupPage(c *fiber.Ctx) error {
localizer := c.Locals("localizer").(*i18n.Localizer)
next, _ := localizer.LocalizeMessage(&i18n.Message{ID: "next"})
email, _ := localizer.LocalizeMessage(&i18n.Message{ID: "email"})
nickname, _ := localizer.LocalizeMessage(&i18n.Message{ID: "nickname"})
username, _ := localizer.LocalizeMessage(&i18n.Message{ID: "username"})
password, _ := localizer.LocalizeMessage(&i18n.Message{ID: "password"})
magicToken, _ := localizer.LocalizeMessage(&i18n.Message{ID: "magicToken"})
signin, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signinTitle"})
title, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signupTitle"})
caption, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signupCaption"})
return c.Render("views/signup", fiber.Map{
"info": flash.Get(c)["message"],
"use_magic_token": viper.GetBool("use_registration_magic_token"),
"i18n": fiber.Map{
"next": next,
"email": email,
"username": username,
"nickname": nickname,
"password": password,
"magic_token": magicToken,
"signin": signin,
"title": title,
"caption": caption,
},
}, "views/layouts/auth")
}
func signupAction(c *fiber.Ctx) error {
var data struct {
Name string `form:"name" validate:"required,lowercase,alphanum,min=4,max=16"`
Nick string `form:"nick" validate:"required,min=4,max=24"`
Email string `form:"email" validate:"required,email"`
Password string `form:"password" validate:"required,min=4,max=32"`
MagicToken string `form:"magic_token"`
}
if err := utils.BindAndValidate(c, &data); err != nil {
return flash.WithInfo(c, fiber.Map{
"message": err.Error(),
}).Redirect("/sign-up")
} else if viper.GetBool("use_registration_magic_token") && len(data.MagicToken) <= 0 {
return flash.WithInfo(c, fiber.Map{
"message": "magic token was required",
}).Redirect("/sign-up")
} else if viper.GetBool("use_registration_magic_token") {
if tk, err := services.ValidateMagicToken(data.MagicToken, models.RegistrationMagicToken); err != nil {
return flash.WithInfo(c, fiber.Map{
"message": fmt.Sprintf("magic token was invalid: %v", err.Error()),
}).Redirect("/sign-up")
} else {
database.C.Delete(&tk)
}
}
if _, err := services.CreateAccount(
data.Name,
data.Nick,
data.Email,
data.Password,
); err != nil {
return flash.WithInfo(c, fiber.Map{
"message": err.Error(),
}).Redirect("/sign-up")
} else {
return flash.WithInfo(c, fiber.Map{
"message": "account has been created. now you can sign in!",
}).Redirect("/sign-in")
}
}

View File

@ -34,14 +34,15 @@ func NewTicket(user models.Account, ip, ua string) (models.AuthTicket, error) {
}
ticket = models.AuthTicket{
Claims: []string{"*"},
Audiences: []string{"passport"},
IpAddress: ip,
UserAgent: ua,
RequireMFA: DetectRisk(user, ip, ua),
ExpiredAt: lo.ToPtr(time.Now().Add(2 * time.Hour)),
AvailableAt: nil,
AccountID: user.ID,
Claims: []string{"*"},
Audiences: []string{"passport"},
IpAddress: ip,
UserAgent: ua,
RequireMFA: DetectRisk(user, ip, ua),
RequireAuthenticate: true,
ExpiredAt: lo.ToPtr(time.Now().Add(2 * time.Hour)),
AvailableAt: nil,
AccountID: user.ID,
}
err := database.C.Save(&ticket).Error
@ -81,7 +82,7 @@ func ActiveTicketWithPassword(ticket models.AuthTicket, password string) (models
if ticket.AvailableAt != nil {
return ticket, nil
} else if !ticket.RequireAuthenticate {
return ticket, fmt.Errorf("detected risk, multi factor authentication required")
return ticket, nil
}
if factor, err := GetPasswordFactor(ticket.AccountID); err != nil {

View File

@ -1,4 +1,4 @@
package server
package utils
import (
"github.com/go-playground/validator/v10"

View File

@ -5,8 +5,16 @@
<body>
<div class="wrapper-container">
<div class="wrapper-card">
{{embed}}
<div class="wrapper-middleware">
{{if ne .info nil}}
<div class="animate__animated animate__fadeInDown alert">
<div class="content">{{.info}}</div>
</div>
{{end}}
<div class="wrapper-card">
{{embed}}
</div>
</div>
</div>
</body>
@ -20,9 +28,19 @@
align-items: center;
}
.wrapper-card {
.wrapper-middleware {
width: 100%;
max-width: 720px;
min-width: 0;
max-width: min(800px, 100dvw);
margin: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.wrapper-card {
transition: all .3s;
height: auto;
overflow: auto;
@ -31,9 +49,25 @@
justify-content: center;
border-radius: 28px;
padding: 56px;
margin: 2rem;
gap: 0 2rem;
background-color: #ebf1fa;
background-color: var(--md-sys-color-surface);
color: var(--md-sys-color-on-surface)
}
.alert {
width: 100%;
max-width: 800px;
padding: 16px;
border-radius: 16px;
background-color: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
display: flex;
gap: 8px;
}
.alert .content {
flex-grow: 1;
text-transform: capitalize;
}
.logo {

View File

@ -10,6 +10,11 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
/>
<script type="importmap">
{
"imports": {
@ -28,10 +33,49 @@
<title>Solarpass</title>
<style>
:root, :host {
--md-sys-color-background: #fbf8ff;
--md-sys-color-on-background: #1b1b20;
--md-sys-color-surface: #fbf8ff;
--md-sys-color-surface-dim: #dcd9df;
--md-sys-color-surface-bright: #fbf8ff;
--md-sys-color-surface-container-lowest: #ffffff;
--md-sys-color-surface-container-low: #f6f2f9;
--md-sys-color-surface-container: #f0edf3;
--md-sys-color-surface-container-high: #eae7ed;
--md-sys-color-surface-container-highest: #e4e1e8;
--md-sys-color-on-surface: #1b1b20;
--md-sys-color-surface-variant: #e3e1ee;
--md-sys-color-on-surface-variant: #464650;
--md-sys-color-inverse-surface: #303035;
--md-sys-color-inverse-on-surface: #f3eff6;
--md-sys-color-outline: #777681;
--md-sys-color-outline-variant: #c7c5d2;
--md-sys-color-shadow: #000000;
--md-sys-color-scrim: #000000;
--md-sys-color-surface-tint: #53589d;
--md-sys-color-primary: #373c7e;
--md-sys-color-on-primary: #ffffff;
--md-sys-color-primary-container: #5b60a5;
--md-sys-color-on-primary-container: #ffffff;
--md-sys-color-inverse-primary: #bec2ff;
--md-sys-color-secondary: #5b5c79;
--md-sys-color-on-secondary: #ffffff;
--md-sys-color-secondary-container: #e2e1ff;
--md-sys-color-on-secondary-container: #454662;
--md-sys-color-tertiary: #662d5e;
--md-sys-color-on-tertiary: #ffffff;
--md-sys-color-tertiary-container: #8e5084;
--md-sys-color-on-tertiary-container: #ffffff;
--md-sys-color-error: #ba1a1a;
--md-sys-color-on-error: #ffffff;
--md-sys-color-error-container: #ffdad6;
--md-sys-color-on-error-container: #410002;
}
html, body {
padding: 0;
margin: 0;
font-family: Roboto Mono, monospace;
}
</style>
</head>

View File

@ -8,7 +8,7 @@
<div class="right-part">
<div class="responsive-title-gap"></div>
<form class="action-form">
<form class="action-form" action="/sign-in" method="POST">
<md-outlined-text-field
class="block-field"
name="username"
@ -28,6 +28,7 @@
</md-outlined-text-field>
<div class="action-form-buttons">
<md-text-button type="button" href="/sign-up">{{.i18n.signup}}</md-text-button>
<md-filled-button type="submit">{{.i18n.next}}</md-filled-button>
</div>
</form>

75
pkg/views/signup.gohtml Normal file
View File

@ -0,0 +1,75 @@
<div class="left-part">
<img class="logo" src="/favicon.png" width="64" height="64"/>
<h1 class="title">{{.i18n.title}}</h1>
<p class="caption">{{.i18n.caption}}</p>
</div>
<div class="right-part">
<div class="responsive-title-gap"></div>
<form class="action-form" action="/sign-up" method="POST">
<div class="columns-two">
<md-outlined-text-field
class="block-field"
name="name"
type="text"
autocomplete="username"
label={{.i18n.username}}
>
</md-outlined-text-field>
<md-outlined-text-field
class="block-field"
name="nick"
type="text"
autocomplete="nickname"
label={{.i18n.nickname}}
>
</md-outlined-text-field>
</div>
<md-outlined-text-field
class="block-field"
name="email"
type="email"
autocomplete="email"
label={{.i18n.email}}
>
</md-outlined-text-field>
<md-outlined-text-field
class="block-field"
name="password"
type="password"
autocomplete="new-password"
label={{.i18n.password}}
>
</md-outlined-text-field>
{{if eq .use_magic_token true}}
<md-outlined-text-field
class="block-field"
name="magic_token"
type="password"
autocomplete="off"
label={{.i18n.magic_token}}
>
</md-outlined-text-field>
{{end}}
<div class="action-form-buttons">
<md-text-button type="button" href="/sign-in">{{.i18n.signin}}</md-text-button>
<md-filled-button type="submit">{{.i18n.next}}</md-filled-button>
</div>
</form>
</div>
<style>
.columns-two {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
</style>