✨ An entire complete sign in user flow
This commit is contained in:
parent
e79441dbc5
commit
ee6e7324b2
@ -4,32 +4,28 @@
|
|||||||
<option name="autoReloadType" value="ALL" />
|
<option name="autoReloadType" value="ALL" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":sparkles: New ticket ways">
|
<list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":sparkles: Sign up & Sign in">
|
||||||
<change afterPath="$PROJECT_DIR$/pkg/server/ui/signin.go" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/pkg/server/ui/accounts.go" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/pkg/server/ui/signup.go" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/pkg/server/ui/mfa.go" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/pkg/views/signup.gohtml" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/pkg/services/mfa.go" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/Passport.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/Passport.iml" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/pkg/views/mfa-apply.gohtml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/dataSources.local.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/dataSources.local.xml" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/pkg/views/mfa.gohtml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/dataSources/74bcf3ef-a2b9-435b-b9e5-f32902a33b25.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/dataSources/74bcf3ef-a2b9-435b-b9e5-f32902a33b25.xml" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/pkg/views/users/me/index.gohtml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/dataSources/74bcf3ef-a2b9-435b-b9e5-f32902a33b25/storage_v2/_src_/database/hy_passport.gNOKQQ/schema/public.abK9xQ.meta" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/dataSources/74bcf3ef-a2b9-435b-b9e5-f32902a33b25/storage_v2/_src_/database/hy_passport.gNOKQQ/schema/public.abK9xQ.meta" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/go.mod" beforeDir="false" afterPath="$PROJECT_DIR$/go.mod" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/go.sum" beforeDir="false" afterPath="$PROJECT_DIR$/go.sum" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/i18n/locale.en.json" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/i18n/locale.en.json" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/pkg/i18n/locale.en.json" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/i18n/locale.en.json" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/i18n/locale.zh.json" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/i18n/locale.zh.json" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/pkg/i18n/locale.zh.json" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/i18n/locale.zh.json" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/server/accounts_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/accounts_api.go" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/pkg/server/startup.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/startup.go" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/server/auth_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/auth_api.go" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/server/friendships_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/friendships_api.go" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/server/notifications_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/notifications_api.go" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/server/notify_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/notify_api.go" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/server/page_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/page_api.go" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/server/ui/auth.go" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/server/ui/index.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/ui/index.go" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/pkg/server/ui/index.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/ui/index.go" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/server/utils.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/utils/request.go" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/pkg/server/ui/signin.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/ui/signin.go" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/pkg/server/ui/signup.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/server/ui/signup.go" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/pkg/services/factors.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/services/factors.go" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/services/ticket.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/services/ticket.go" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/pkg/services/ticket.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/services/ticket.go" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/pkg/services/ticket_token.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/services/ticket_token.go" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/pkg/utils/request.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/utils/request.go" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/views/layouts/auth.gohtml" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/views/layouts/auth.gohtml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/pkg/views/layouts/auth.gohtml" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/views/layouts/auth.gohtml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/views/partials/header.gohtml" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/views/partials/header.gohtml" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/pkg/views/signin.gohtml" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/views/signin.gohtml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/pkg/views/signin.gohtml" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/views/signin.gohtml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/pkg/views/signup.gohtml" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/views/signup.gohtml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/settings.toml" beforeDir="false" afterPath="$PROJECT_DIR$/settings.toml" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@ -93,6 +89,7 @@
|
|||||||
<component name="RecentsManager">
|
<component name="RecentsManager">
|
||||||
<key name="CopyFile.RECENT_KEYS">
|
<key name="CopyFile.RECENT_KEYS">
|
||||||
<recent name="$PROJECT_DIR$/pkg/views" />
|
<recent name="$PROJECT_DIR$/pkg/views" />
|
||||||
|
<recent name="$PROJECT_DIR$/pkg/server/ui" />
|
||||||
<recent name="$PROJECT_DIR$/pkg" />
|
<recent name="$PROJECT_DIR$/pkg" />
|
||||||
</key>
|
</key>
|
||||||
<key name="MoveFile.RECENT_KEYS">
|
<key name="MoveFile.RECENT_KEYS">
|
||||||
@ -137,7 +134,8 @@
|
|||||||
<component name="VcsManagerConfiguration">
|
<component name="VcsManagerConfiguration">
|
||||||
<MESSAGE value=":recycle: Refactor frontend" />
|
<MESSAGE value=":recycle: Refactor frontend" />
|
||||||
<MESSAGE value=":sparkles: New ticket ways" />
|
<MESSAGE value=":sparkles: New ticket ways" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value=":sparkles: New ticket ways" />
|
<MESSAGE value=":sparkles: Sign up & Sign in" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value=":sparkles: Sign up & Sign in" />
|
||||||
</component>
|
</component>
|
||||||
<component name="VgoProject">
|
<component name="VgoProject">
|
||||||
<settings-migrated>true</settings-migrated>
|
<settings-migrated>true</settings-migrated>
|
||||||
|
@ -4,9 +4,14 @@
|
|||||||
"username": "Username",
|
"username": "Username",
|
||||||
"nickname": "Nickname",
|
"nickname": "Nickname",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
|
"unknown": "Unknown",
|
||||||
"magicToken": "Magic Token",
|
"magicToken": "Magic Token",
|
||||||
"signinTitle": "Sign In",
|
"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!",
|
||||||
|
"signinRequired": "You need to sign in before do that.",
|
||||||
"signupTitle": "Sign Up",
|
"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!"
|
"signupCaption": "Sign up to create an account on Solarpass, then you can explore the entire Solar Network! Enjoy the next-generation Internet Ecosystem!",
|
||||||
|
"mfaTitle": "Multi Factor Authenticate",
|
||||||
|
"mfaCaption": "We need use one more way to verify it is you.",
|
||||||
|
"mfaFactorEmail": "OTP through your email"
|
||||||
}
|
}
|
@ -4,9 +4,14 @@
|
|||||||
"username": "用户名",
|
"username": "用户名",
|
||||||
"nickname": "昵称",
|
"nickname": "昵称",
|
||||||
"password": "密码",
|
"password": "密码",
|
||||||
|
"unknown": "未知",
|
||||||
"magicToken": "魔法令牌",
|
"magicToken": "魔法令牌",
|
||||||
"signinTitle": "登陆",
|
"signinTitle": "登陆",
|
||||||
"signinCaption": "登陆 Solarpass 以探索整个 Solar Network,浏览帖子、探索社区、和你的好朋友聊八卦,一切尽在 Solar Network!",
|
"signinCaption": "登陆 Solarpass 以探索整个 Solar Network,浏览帖子、探索社区、和你的好朋友聊八卦,一切尽在 Solar Network!",
|
||||||
|
"signinRequired": "你需要在那之前登陆",
|
||||||
"signupTitle": "注册",
|
"signupTitle": "注册",
|
||||||
"signupCaption": "注册以在 Solarpass 创建一个账号,之后你就可以探索整个 Solar Network,享受下一代互联网生态系统!"
|
"signupCaption": "注册以在 Solarpass 创建一个账号,之后你就可以探索整个 Solar Network,享受下一代互联网生态系统!",
|
||||||
|
"mfaTitle": "多因素验证",
|
||||||
|
"mfaCaption": "我们需要另一个方法来确认你是你。",
|
||||||
|
"mfaFactorEmail": "电子邮寄一次性验证码"
|
||||||
}
|
}
|
||||||
|
@ -130,7 +130,7 @@ func NewServer() {
|
|||||||
URL: "/favicon.png",
|
URL: "/favicon.png",
|
||||||
}))
|
}))
|
||||||
|
|
||||||
ui.MapUserInterface(A)
|
ui.MapUserInterface(A, authFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Listen() {
|
func Listen() {
|
||||||
|
7
pkg/server/ui/accounts.go
Normal file
7
pkg/server/ui/accounts.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import "github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
func selfUserinfoPage(c *fiber.Ctx) error {
|
||||||
|
return c.Render("views/users/me/index", fiber.Map{})
|
||||||
|
}
|
@ -1,13 +1,41 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import "github.com/gofiber/fiber/v2"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MapUserInterface(A *fiber.App, authFunc func(c *fiber.Ctx, overrides ...string) error) {
|
||||||
|
authCheckWare := func(c *fiber.Ctx) error {
|
||||||
|
var token string
|
||||||
|
if cookie := c.Cookies(services.CookieAccessKey); len(cookie) > 0 {
|
||||||
|
token = cookie
|
||||||
|
}
|
||||||
|
fmt.Println(token)
|
||||||
|
|
||||||
|
c.Locals("token", token)
|
||||||
|
|
||||||
|
if err := authFunc(c); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
uri := c.Request().URI().FullURI()
|
||||||
|
return c.Redirect(fmt.Sprintf("/sign-in?redirect_uri=%s", string(uri)))
|
||||||
|
} else {
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func MapUserInterface(A *fiber.App) {
|
|
||||||
pages := A.Group("/").Name("Pages")
|
pages := A.Group("/").Name("Pages")
|
||||||
|
|
||||||
pages.Get("/sign-up", signupPage)
|
pages.Get("/sign-up", signupPage)
|
||||||
pages.Get("/sign-in", signinPage)
|
pages.Get("/sign-in", signinPage)
|
||||||
|
pages.Get("/mfa", mfaRequestPage)
|
||||||
|
pages.Get("/mfa/apply", mfaApplyPage)
|
||||||
|
|
||||||
pages.Post("/sign-up", signupAction)
|
pages.Post("/sign-up", signupAction)
|
||||||
pages.Post("/sign-in", signinAction)
|
pages.Post("/sign-in", signinAction)
|
||||||
|
pages.Post("/mfa", mfaRequestAction)
|
||||||
|
pages.Post("/mfa/apply", mfaApplyAction)
|
||||||
|
|
||||||
|
pages.Get("/users/me", authCheckWare, selfUserinfoPage)
|
||||||
}
|
}
|
||||||
|
194
pkg/server/ui/mfa.go
Normal file
194
pkg/server/ui/mfa.go
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"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/samber/lo"
|
||||||
|
"github.com/sujit-baniya/flash"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mfaRequestPage(c *fiber.Ctx) error {
|
||||||
|
ticketId := c.QueryInt("ticket", 0)
|
||||||
|
|
||||||
|
ticket, err := services.GetTicket(uint(ticketId))
|
||||||
|
if err != nil {
|
||||||
|
return flash.WithInfo(c, fiber.Map{
|
||||||
|
"message": "you must provide ticket id to perform multi-factor authenticate",
|
||||||
|
}).Redirect("/sign-in")
|
||||||
|
}
|
||||||
|
user, err := services.GetAccount(ticket.AccountID)
|
||||||
|
if err != nil {
|
||||||
|
return flash.WithInfo(c, fiber.Map{
|
||||||
|
"message": "ticket related user just weirdly disappear",
|
||||||
|
}).Redirect("/sign-in")
|
||||||
|
}
|
||||||
|
factors, err := services.ListUserFactor(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return flash.WithInfo(c, fiber.Map{
|
||||||
|
"message": fmt.Sprintf("unable to get your factors: %v", err.Error()),
|
||||||
|
}).Redirect("/sign-in")
|
||||||
|
}
|
||||||
|
|
||||||
|
factors = lo.Filter(factors, func(item models.AuthFactor, index int) bool {
|
||||||
|
return item.Type != models.PasswordAuthFactor
|
||||||
|
})
|
||||||
|
|
||||||
|
localizer := c.Locals("localizer").(*i18n.Localizer)
|
||||||
|
|
||||||
|
next, _ := localizer.LocalizeMessage(&i18n.Message{ID: "next"})
|
||||||
|
title, _ := localizer.LocalizeMessage(&i18n.Message{ID: "mfaTitle"})
|
||||||
|
caption, _ := localizer.LocalizeMessage(&i18n.Message{ID: "mfaCaption"})
|
||||||
|
|
||||||
|
return c.Render("views/mfa", fiber.Map{
|
||||||
|
"info": flash.Get(c)["message"],
|
||||||
|
"redirect_uri": flash.Get(c)["redirect_uri"],
|
||||||
|
"ticket_id": ticket.ID,
|
||||||
|
"factors": lo.Map(factors, func(item models.AuthFactor, index int) fiber.Map {
|
||||||
|
return fiber.Map{
|
||||||
|
"name": services.GetFactorName(item.Type, localizer),
|
||||||
|
"id": item.ID,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
"i18n": fiber.Map{
|
||||||
|
"next": next,
|
||||||
|
"title": title,
|
||||||
|
"caption": caption,
|
||||||
|
},
|
||||||
|
}, "views/layouts/auth")
|
||||||
|
}
|
||||||
|
|
||||||
|
func mfaRequestAction(c *fiber.Ctx) error {
|
||||||
|
var data struct {
|
||||||
|
TicketID uint `form:"ticket_id" validate:"required"`
|
||||||
|
FactorID uint `form:"factor_id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectBackUri := "/sign-in"
|
||||||
|
err := utils.BindAndValidate(c, &data)
|
||||||
|
|
||||||
|
if data.TicketID > 0 {
|
||||||
|
redirectBackUri = fmt.Sprintf("/mfa?ticket=%d", data.TicketID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return flash.WithInfo(c, fiber.Map{
|
||||||
|
"message": err.Error(),
|
||||||
|
}).Redirect(redirectBackUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
factor, err := services.GetFactor(data.FactorID)
|
||||||
|
if err != nil {
|
||||||
|
return flash.WithInfo(c, fiber.Map{
|
||||||
|
"message": fmt.Sprintf("factor was not found: %v", err.Error()),
|
||||||
|
}).Redirect(redirectBackUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = services.GetFactorCode(factor)
|
||||||
|
if err != nil {
|
||||||
|
return flash.WithInfo(c, fiber.Map{
|
||||||
|
"message": fmt.Sprintf("unable to get factor code: %v", err.Error()),
|
||||||
|
}).Redirect(redirectBackUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
return flash.WithData(c, fiber.Map{
|
||||||
|
"redirect_uri": utils.GetRedirectUri(c),
|
||||||
|
}).Redirect(fmt.Sprintf("/mfa/apply?ticket=%d&factor=%d", data.TicketID, factor.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func mfaApplyPage(c *fiber.Ctx) error {
|
||||||
|
ticketId := c.QueryInt("ticket", 0)
|
||||||
|
factorId := c.QueryInt("factor", 0)
|
||||||
|
|
||||||
|
ticket, err := services.GetTicket(uint(ticketId))
|
||||||
|
if err != nil {
|
||||||
|
return flash.WithInfo(c, fiber.Map{
|
||||||
|
"message": fmt.Sprintf("unable to find your ticket: %v", err.Error()),
|
||||||
|
}).Redirect("/sign-in")
|
||||||
|
}
|
||||||
|
factor, err := services.GetFactor(uint(factorId))
|
||||||
|
if err != nil {
|
||||||
|
return flash.WithInfo(c, fiber.Map{
|
||||||
|
"message": fmt.Sprintf("unable to find your factors: %v", err.Error()),
|
||||||
|
}).Redirect("/sign-in")
|
||||||
|
}
|
||||||
|
|
||||||
|
localizer := c.Locals("localizer").(*i18n.Localizer)
|
||||||
|
|
||||||
|
next, _ := localizer.LocalizeMessage(&i18n.Message{ID: "next"})
|
||||||
|
password, _ := localizer.LocalizeMessage(&i18n.Message{ID: "password"})
|
||||||
|
title, _ := localizer.LocalizeMessage(&i18n.Message{ID: "mfaTitle"})
|
||||||
|
caption, _ := localizer.LocalizeMessage(&i18n.Message{ID: "mfaCaption"})
|
||||||
|
|
||||||
|
return c.Render("views/mfa-apply", fiber.Map{
|
||||||
|
"info": flash.Get(c)["message"],
|
||||||
|
"label": services.GetFactorName(factor.Type, localizer),
|
||||||
|
"ticket_id": ticket.ID,
|
||||||
|
"factor_id": factor.ID,
|
||||||
|
"i18n": fiber.Map{
|
||||||
|
"next": next,
|
||||||
|
"password": password,
|
||||||
|
"title": title,
|
||||||
|
"caption": caption,
|
||||||
|
},
|
||||||
|
}, "views/layouts/auth")
|
||||||
|
}
|
||||||
|
|
||||||
|
func mfaApplyAction(c *fiber.Ctx) error {
|
||||||
|
var data struct {
|
||||||
|
TicketID uint `form:"ticket_id" validate:"required"`
|
||||||
|
FactorID uint `form:"factor_id" validate:"required"`
|
||||||
|
Code string `form:"code" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectBackUri := "/sign-in"
|
||||||
|
err := utils.BindAndValidate(c, &data)
|
||||||
|
|
||||||
|
if data.TicketID > 0 {
|
||||||
|
redirectBackUri = fmt.Sprintf("/mfa/apply?ticket=%d&factor=%d", data.TicketID, data.FactorID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return flash.WithInfo(c, fiber.Map{
|
||||||
|
"message": err.Error(),
|
||||||
|
}).Redirect(redirectBackUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
ticket, err := services.GetTicket(data.TicketID)
|
||||||
|
if err != nil {
|
||||||
|
return flash.WithInfo(c, fiber.Map{
|
||||||
|
"message": fmt.Sprintf("unable to find your ticket: %v", err.Error()),
|
||||||
|
}).Redirect("/sign-in")
|
||||||
|
}
|
||||||
|
factor, err := services.GetFactor(data.FactorID)
|
||||||
|
if err != nil {
|
||||||
|
return flash.WithInfo(c, fiber.Map{
|
||||||
|
"message": fmt.Sprintf("factor was not found: %v", err.Error()),
|
||||||
|
}).Redirect(redirectBackUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
ticket, err = services.ActiveTicketWithMFA(ticket, factor, data.Code)
|
||||||
|
if err != nil {
|
||||||
|
return flash.WithInfo(c, fiber.Map{
|
||||||
|
"message": fmt.Sprintf("invalid multi-factor authenticate code: %v", err.Error()),
|
||||||
|
}).Redirect(redirectBackUri)
|
||||||
|
} else if ticket.IsAvailable() != nil {
|
||||||
|
return flash.WithInfo(c, fiber.Map{
|
||||||
|
"message": "ticket weirdly still unavailable after multi-factor authenticate",
|
||||||
|
}).Redirect("/sign-in")
|
||||||
|
}
|
||||||
|
|
||||||
|
access, refresh, err := services.ExchangeToken(*ticket.GrantToken)
|
||||||
|
if err != nil {
|
||||||
|
return flash.WithInfo(c, fiber.Map{
|
||||||
|
"message": fmt.Sprintf("failed to exchange token: %v", err.Error()),
|
||||||
|
}).Redirect("/sign-in")
|
||||||
|
} else {
|
||||||
|
services.SetJwtCookieSet(c, access, refresh)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Redirect(lo.FromPtr(utils.GetRedirectUri(c, "/users/me")))
|
||||||
|
}
|
@ -6,6 +6,7 @@ import (
|
|||||||
"git.solsynth.dev/hydrogen/passport/pkg/utils"
|
"git.solsynth.dev/hydrogen/passport/pkg/utils"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
"github.com/samber/lo"
|
||||||
"github.com/sujit-baniya/flash"
|
"github.com/sujit-baniya/flash"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -18,9 +19,17 @@ func signinPage(c *fiber.Ctx) error {
|
|||||||
signup, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signupTitle"})
|
signup, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signupTitle"})
|
||||||
title, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signinTitle"})
|
title, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signinTitle"})
|
||||||
caption, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signinCaption"})
|
caption, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signinCaption"})
|
||||||
|
requiredNotify, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signinRequired"})
|
||||||
|
|
||||||
|
var info any
|
||||||
|
if flash.Get(c)["message"] != nil {
|
||||||
|
info = flash.Get(c)["message"]
|
||||||
|
} else {
|
||||||
|
info = requiredNotify
|
||||||
|
}
|
||||||
|
|
||||||
return c.Render("views/signin", fiber.Map{
|
return c.Render("views/signin", fiber.Map{
|
||||||
"info": flash.Get(c)["message"],
|
"info": info,
|
||||||
"i18n": fiber.Map{
|
"i18n": fiber.Map{
|
||||||
"next": next,
|
"next": next,
|
||||||
"username": username,
|
"username": username,
|
||||||
@ -66,12 +75,19 @@ func signinAction(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ticket.IsAvailable() != nil {
|
if ticket.IsAvailable() != nil {
|
||||||
|
return flash.WithData(c, fiber.Map{
|
||||||
|
"redirect_uri": utils.GetRedirectUri(c),
|
||||||
|
}).Redirect(fmt.Sprintf("/mfa?ticket=%d", ticket.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
access, refresh, err := services.ExchangeToken(*ticket.GrantToken)
|
||||||
|
if err != nil {
|
||||||
return flash.WithInfo(c, fiber.Map{
|
return flash.WithInfo(c, fiber.Map{
|
||||||
"message": "multi factor authenticate required",
|
"message": fmt.Sprintf("failed to exchange token: %v", err.Error()),
|
||||||
}).Redirect("/sign-in")
|
}).Redirect("/sign-in")
|
||||||
} else {
|
} else {
|
||||||
return flash.WithInfo(c, fiber.Map{
|
services.SetJwtCookieSet(c, access, refresh)
|
||||||
"message": "done",
|
|
||||||
}).Redirect("/sign-in")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return c.Redirect(lo.FromPtr(utils.GetRedirectUri(c, "/users/me")))
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"git.solsynth.dev/hydrogen/passport/pkg/utils"
|
"git.solsynth.dev/hydrogen/passport/pkg/utils"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
"github.com/samber/lo"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/sujit-baniya/flash"
|
"github.com/sujit-baniya/flash"
|
||||||
)
|
)
|
||||||
@ -81,6 +82,6 @@ func signupAction(c *fiber.Ctx) error {
|
|||||||
} else {
|
} else {
|
||||||
return flash.WithInfo(c, fiber.Map{
|
return flash.WithInfo(c, fiber.Map{
|
||||||
"message": "account has been created. now you can sign in!",
|
"message": "account has been created. now you can sign in!",
|
||||||
}).Redirect("/sign-in")
|
}).Redirect(lo.FromPtr(utils.GetRedirectUri(c, "/sign-in")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ Thank you for your cooperation in helping us maintain the security of your accou
|
|||||||
Best regards,
|
Best regards,
|
||||||
%s`
|
%s`
|
||||||
|
|
||||||
func GetPasswordFactor(userId uint) (models.AuthFactor, error) {
|
func GetPasswordTypeFactor(userId uint) (models.AuthFactor, error) {
|
||||||
var factor models.AuthFactor
|
var factor models.AuthFactor
|
||||||
err := database.C.Where(models.AuthFactor{
|
err := database.C.Where(models.AuthFactor{
|
||||||
Type: models.PasswordAuthFactor,
|
Type: models.PasswordAuthFactor,
|
||||||
@ -53,6 +53,15 @@ func ListUserFactor(userId uint) ([]models.AuthFactor, error) {
|
|||||||
return factors, err
|
return factors, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CountUserFactor(userId uint) int64 {
|
||||||
|
var count int64
|
||||||
|
database.C.Where(models.AuthFactor{
|
||||||
|
AccountID: userId,
|
||||||
|
}).Model(&models.AuthFactor{}).Count(&count)
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
func GetFactorCode(factor models.AuthFactor) (bool, error) {
|
func GetFactorCode(factor models.AuthFactor) (bool, error) {
|
||||||
switch factor.Type {
|
switch factor.Type {
|
||||||
case models.EmailPasswordFactor:
|
case models.EmailPasswordFactor:
|
||||||
|
18
pkg/services/mfa.go
Normal file
18
pkg/services/mfa.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||||
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetFactorName(w models.AuthFactorType, localizer *i18n.Localizer) string {
|
||||||
|
unknown, _ := localizer.LocalizeMessage(&i18n.Message{ID: "unknown"})
|
||||||
|
mfaEmail, _ := localizer.LocalizeMessage(&i18n.Message{ID: "mfaFactorEmail"})
|
||||||
|
|
||||||
|
switch w {
|
||||||
|
case models.EmailPasswordFactor:
|
||||||
|
return mfaEmail
|
||||||
|
default:
|
||||||
|
return unknown
|
||||||
|
}
|
||||||
|
}
|
@ -27,18 +27,23 @@ func DetectRisk(user models.Account, ip, ua string) bool {
|
|||||||
|
|
||||||
func NewTicket(user models.Account, ip, ua string) (models.AuthTicket, error) {
|
func NewTicket(user models.Account, ip, ua string) (models.AuthTicket, error) {
|
||||||
var ticket models.AuthTicket
|
var ticket models.AuthTicket
|
||||||
if err := database.C.Where(models.AuthTicket{
|
if err := database.C.
|
||||||
AccountID: user.ID,
|
Where("account_id = ? AND expired_at < ? AND available_at IS NULL", time.Now(), user.ID).
|
||||||
}).First(&ticket).Error; err == nil {
|
First(&ticket).Error; err == nil {
|
||||||
return ticket, nil
|
return ticket, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requireMFA := DetectRisk(user, ip, ua)
|
||||||
|
if count := CountUserFactor(user.ID); count <= 1 {
|
||||||
|
requireMFA = false
|
||||||
|
}
|
||||||
|
|
||||||
ticket = models.AuthTicket{
|
ticket = models.AuthTicket{
|
||||||
Claims: []string{"*"},
|
Claims: []string{"*"},
|
||||||
Audiences: []string{"passport"},
|
Audiences: []string{"passport"},
|
||||||
IpAddress: ip,
|
IpAddress: ip,
|
||||||
UserAgent: ua,
|
UserAgent: ua,
|
||||||
RequireMFA: DetectRisk(user, ip, ua),
|
RequireMFA: requireMFA,
|
||||||
RequireAuthenticate: true,
|
RequireAuthenticate: true,
|
||||||
ExpiredAt: lo.ToPtr(time.Now().Add(2 * time.Hour)),
|
ExpiredAt: lo.ToPtr(time.Now().Add(2 * time.Hour)),
|
||||||
AvailableAt: nil,
|
AvailableAt: nil,
|
||||||
@ -85,16 +90,19 @@ func ActiveTicketWithPassword(ticket models.AuthTicket, password string) (models
|
|||||||
return ticket, nil
|
return ticket, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if factor, err := GetPasswordFactor(ticket.AccountID); err != nil {
|
if factor, err := GetPasswordTypeFactor(ticket.AccountID); err != nil {
|
||||||
return ticket, fmt.Errorf("unable to active ticket: %v", err)
|
return ticket, fmt.Errorf("unable to active ticket: %v", err)
|
||||||
} else if err = CheckFactor(factor, password); err != nil {
|
} else if err = CheckFactor(factor, password); err != nil {
|
||||||
return ticket, err
|
return ticket, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ticket.AvailableAt = lo.ToPtr(time.Now())
|
ticket.RequireAuthenticate = false
|
||||||
|
|
||||||
if !ticket.RequireAuthenticate && !ticket.RequireMFA {
|
if !ticket.RequireAuthenticate && !ticket.RequireMFA {
|
||||||
ticket.AvailableAt = lo.ToPtr(time.Now())
|
ticket.AvailableAt = lo.ToPtr(time.Now())
|
||||||
|
ticket.GrantToken = lo.ToPtr(uuid.NewString())
|
||||||
|
ticket.AccessToken = lo.ToPtr(uuid.NewString())
|
||||||
|
ticket.RefreshToken = lo.ToPtr(uuid.NewString())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := database.C.Save(&ticket).Error; err != nil {
|
if err := database.C.Save(&ticket).Error; err != nil {
|
||||||
@ -119,6 +127,9 @@ func ActiveTicketWithMFA(ticket models.AuthTicket, factor models.AuthFactor, cod
|
|||||||
|
|
||||||
if !ticket.RequireAuthenticate && !ticket.RequireMFA {
|
if !ticket.RequireAuthenticate && !ticket.RequireMFA {
|
||||||
ticket.AvailableAt = lo.ToPtr(time.Now())
|
ticket.AvailableAt = lo.ToPtr(time.Now())
|
||||||
|
ticket.GrantToken = lo.ToPtr(uuid.NewString())
|
||||||
|
ticket.AccessToken = lo.ToPtr(uuid.NewString())
|
||||||
|
ticket.RefreshToken = lo.ToPtr(uuid.NewString())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := database.C.Save(&ticket).Error; err != nil {
|
if err := database.C.Save(&ticket).Error; err != nil {
|
||||||
@ -128,10 +139,10 @@ func ActiveTicketWithMFA(ticket models.AuthTicket, factor models.AuthFactor, cod
|
|||||||
return ticket, nil
|
return ticket, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegenSession(session models.AuthTicket) (models.AuthTicket, error) {
|
func RegenSession(ticket models.AuthTicket) (models.AuthTicket, error) {
|
||||||
session.GrantToken = lo.ToPtr(uuid.NewString())
|
ticket.GrantToken = lo.ToPtr(uuid.NewString())
|
||||||
session.AccessToken = lo.ToPtr(uuid.NewString())
|
ticket.AccessToken = lo.ToPtr(uuid.NewString())
|
||||||
session.RefreshToken = lo.ToPtr(uuid.NewString())
|
ticket.RefreshToken = lo.ToPtr(uuid.NewString())
|
||||||
err := database.C.Save(&session).Error
|
err := database.C.Save(&ticket).Error
|
||||||
return session, err
|
return ticket, err
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,8 @@ func GetToken(ticket models.AuthTicket) (string, string, error) {
|
|||||||
return refresh, access, fmt.Errorf("unable to encode token, access or refresh token id missing")
|
return refresh, access, fmt.Errorf("unable to encode token, access or refresh token id missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
accessDuration := time.Duration(viper.GetInt64("access_token_duration")) * time.Second
|
accessDuration := time.Duration(viper.GetInt64("security.access_token_duration")) * time.Second
|
||||||
refreshDuration := time.Duration(viper.GetInt64("refresh_token_duration")) * time.Second
|
refreshDuration := time.Duration(viper.GetInt64("security.refresh_token_duration")) * time.Second
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
sub := strconv.Itoa(int(ticket.AccountID))
|
sub := strconv.Itoa(int(ticket.AccountID))
|
||||||
|
@ -3,6 +3,8 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"github.com/sujit-baniya/flash"
|
||||||
)
|
)
|
||||||
|
|
||||||
var validation = validator.New(validator.WithRequiredStructEnabled())
|
var validation = validator.New(validator.WithRequiredStructEnabled())
|
||||||
@ -16,3 +18,15 @@ func BindAndValidate(c *fiber.Ctx, out any) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetRedirectUri(c *fiber.Ctx, fallback ...string) *string {
|
||||||
|
if len(c.Query("redirect_uri")) > 0 {
|
||||||
|
return lo.ToPtr(c.Query("redirect_uri"))
|
||||||
|
} else if val, ok := flash.Get(c)["redirect_uri"].(*string); ok {
|
||||||
|
return val
|
||||||
|
} else if len(fallback) > 0 {
|
||||||
|
return &fallback[0]
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -55,8 +55,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
width: 100%;
|
|
||||||
max-width: 800px;
|
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background-color: var(--md-sys-color-secondary-container);
|
background-color: var(--md-sys-color-secondary-container);
|
||||||
|
47
pkg/views/mfa-apply.gohtml
Normal file
47
pkg/views/mfa-apply.gohtml
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<div class="left-part">
|
||||||
|
<img class="logo" alt="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="/mfa/apply" method="POST">
|
||||||
|
<label>
|
||||||
|
<input name="ticket_id" value="{{.ticket_id}}" hidden>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input name="factor_id" value="{{.factor_id}}" hidden>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="factor-label">{{.label}}</div>
|
||||||
|
|
||||||
|
<md-outlined-text-field
|
||||||
|
class="block-field"
|
||||||
|
name="code"
|
||||||
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
|
label={{.i18n.password}}
|
||||||
|
>
|
||||||
|
</md-outlined-text-field>
|
||||||
|
|
||||||
|
<div class="action-form-buttons">
|
||||||
|
<md-filled-button type="submit">{{.i18n.next}}</md-filled-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.factor-label {
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.factor-label {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
61
pkg/views/mfa.gohtml
Normal file
61
pkg/views/mfa.gohtml
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<div class="left-part">
|
||||||
|
<img class="logo" alt="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="/mfa" method="POST">
|
||||||
|
<label>
|
||||||
|
<input name="ticket_id" value="{{.ticket_id}}" hidden>
|
||||||
|
</label>
|
||||||
|
{{if ne .redirect_uri nil}}
|
||||||
|
<label>
|
||||||
|
<input name="redirect_uri" value="{{.redirect_uri}}" hidden>
|
||||||
|
</label>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="block-field factor-list" role="radiogroup">
|
||||||
|
{{range $_, $element := .factors}}
|
||||||
|
<div class="factor-label">
|
||||||
|
<md-radio
|
||||||
|
aria-label="{{$element.name}}"
|
||||||
|
id="factor-{{$element.id}}"
|
||||||
|
value="{{$element.id}}"
|
||||||
|
touch-target="wrapper"
|
||||||
|
name="factor_id"
|
||||||
|
>
|
||||||
|
</md-radio>
|
||||||
|
<label for="factor-{{$element.id}}">{{$element.name}}</label>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-form-buttons">
|
||||||
|
<md-filled-button type="submit">{{.i18n.next}}</md-filled-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.factor-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.factor-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.factor-label label {
|
||||||
|
display: inline-flex;
|
||||||
|
place-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-family: Roboto, system-ui;
|
||||||
|
color: var(--md-sys-color-on-background);
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,5 +1,5 @@
|
|||||||
<div class="left-part">
|
<div class="left-part">
|
||||||
<img class="logo" src="/favicon.png" width="64" height="64"/>
|
<img class="logo" alt="Logo" src="/favicon.png" width="64" height="64"/>
|
||||||
|
|
||||||
<h1 class="title">{{.i18n.title}}</h1>
|
<h1 class="title">{{.i18n.title}}</h1>
|
||||||
<p class="caption">{{.i18n.caption}}</p>
|
<p class="caption">{{.i18n.caption}}</p>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div class="left-part">
|
<div class="left-part">
|
||||||
<img class="logo" src="/favicon.png" width="64" height="64"/>
|
<img class="logo" alt="Logo" src="/favicon.png" width="64" height="64"/>
|
||||||
|
|
||||||
<h1 class="title">{{.i18n.title}}</h1>
|
<h1 class="title">{{.i18n.title}}</h1>
|
||||||
<p class="caption">{{.i18n.caption}}</p>
|
<p class="caption">{{.i18n.caption}}</p>
|
||||||
|
3
pkg/views/users/me/index.gohtml
Normal file
3
pkg/views/users/me/index.gohtml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<h1>
|
||||||
|
You are accepted!
|
||||||
|
</h1>
|
@ -1,9 +1,9 @@
|
|||||||
name = "Goatpass"
|
name = "Goatpass"
|
||||||
maintainer = "SmartSheep Studio"
|
maintainer = "Solsynth LLC"
|
||||||
|
|
||||||
bind = "0.0.0.0:8444"
|
bind = "0.0.0.0:8444"
|
||||||
grpc_bind = "0.0.0.0:7444"
|
grpc_bind = "0.0.0.0:7444"
|
||||||
domain = "id.smartsheep.studio"
|
domain = "localhost"
|
||||||
secret = "LtTjzAGFLshwXhN4ZD4nG5KlMv1MWcsvfv03TSZYnT1VhiAnLIZFTnHUwR0XhGgi"
|
secret = "LtTjzAGFLshwXhN4ZD4nG5KlMv1MWcsvfv03TSZYnT1VhiAnLIZFTnHUwR0XhGgi"
|
||||||
|
|
||||||
content = "uploads"
|
content = "uploads"
|
||||||
|
Loading…
Reference in New Issue
Block a user