Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Login screen #685

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
9 changes: 4 additions & 5 deletions example/admin/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,19 @@ func initLoginSessionBuilder(db *gorm.DB, pb *presets.Builder, ab *activity.Buil
Goth: google.New(loginGoogleKey, loginGoogleSecret, baseURL+"/auth/callback?provider="+models.OAuthProviderGoogle),
Key: models.OAuthProviderGoogle,
Text: "LoginProviderGoogleText",
Logo: RawHTML(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="16px" height="16px"><path fill="#fbc02d" d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12 s5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24s8.955,20,20,20 s20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"></path><path fill="#e53935" d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039 l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"></path><path fill="#4caf50" d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36 c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"></path><path fill="#1565c0" d="M43.611,20.083L43.595,20L42,20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571 c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z"></path></svg>`),
Logo: RawHTML(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="24px" height="24px"><path fill="#fbc02d" d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12 s5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24s8.955,20,20,20 s20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"></path><path fill="#e53935" d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039 l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"></path><path fill="#4caf50" d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36 c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"></path><path fill="#1565c0" d="M43.611,20.083L43.595,20L42,20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571 c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z"></path></svg>`),
},
&login.Provider{
Goth: microsoftonline.New(loginMicrosoftOnlineKey, loginMicrosoftOnlineSecret, baseURL+"/auth/callback"),
Key: models.OAuthProviderMicrosoftOnline,
Text: "LoginProviderMicrosoftText",
Logo: RawHTML(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="16px" height="16px"><path fill="#f35325" d="M2 2h20v20H2z"/><path fill="#81bc06" d="M24 2h20v20H24z"/><path fill="#05a6f0" d="M2 24h20v20H2z"/><path fill="#ffba08" d="M24 24h20v20H24z"/></svg>`),
Logo: RawHTML(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="24px" height="24px"><path fill="#f35325" d="M2 2h20v20H2z"/><path fill="#81bc06" d="M24 2h20v20H24z"/><path fill="#05a6f0" d="M2 24h20v20H2z"/><path fill="#ffba08" d="M24 24h20v20H24z"/></svg>`),
},
&login.Provider{
Goth: github.New(loginGithubKey, loginGithubSecret, baseURL+"/auth/callback?provider="+models.OAuthProviderGithub),
Key: models.OAuthProviderGithub,
Text: "LoginProviderGithubText",
Logo: RawHTML(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="16px" height="16px"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>`),
Logo: RawHTML(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="24px" height="24px"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>`),
},
).
HomeURLFunc(func(r *http.Request, user interface{}) string {
Expand Down Expand Up @@ -144,8 +144,7 @@ func initLoginSessionBuilder(db *gorm.DB, pb *presets.Builder, ab *activity.Buil
return nil
}
}).TOTP(false).MaxRetryCount(0)

loginBuilder.LoginPageFunc(loginPage(loginBuilder.ViewHelper(), pb))
loginBuilder.LoginPageFunc(plogin.NewAdvancedLoginPage(nil)(loginBuilder.ViewHelper(), pb))

genInitialUser(db)

Expand Down
62 changes: 62 additions & 0 deletions example/integration/login_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package integration_test

import (
"net/http"
"testing"

"github.com/qor5/web/v3/multipartestutils"

"github.com/qor5/admin/v3/example/admin"
)

func TestLogin(t *testing.T) {
h := admin.TestL18nHandler(TestDB)

dbr, _ := TestDB.DB()
profileData.TruncatePut(dbr)

cases := []multipartestutils.TestCase{
{
Name: "view by en",
Debug: true,
ReqFunc: func() *http.Request {
req := multipartestutils.NewMultipartBuilder().
PageURL("/auth/login").
BuildEventFuncRequest()
req.Header.Add("accept-language", "en")
return req
},
ExpectPageBodyContainsInOrder: []string{`Welcome`, `Qor Admin System`, `Email`, `Password`, `Sign in`, `Forget your password?`},
},
{
Name: "view by zh",
Debug: true,
ReqFunc: func() *http.Request {
req := multipartestutils.NewMultipartBuilder().
PageURL("/auth/login").
BuildEventFuncRequest()
req.Header.Add("accept-language", "zh")
return req
},
ExpectPageBodyContainsInOrder: []string{`欢迎`, `Qor 管理系统`, `邮箱`, `密码`, `登录`, `忘记密码?`},
},
{
Name: "view by ja",
Debug: true,
ReqFunc: func() *http.Request {
req := multipartestutils.NewMultipartBuilder().
PageURL("/auth/login").
BuildEventFuncRequest()
req.Header.Add("accept-language", "ja")
return req
},
ExpectPageBodyContainsInOrder: []string{`ようこそ`, `Qor 管理システム`, `メールアドレス`, `パスワード`, `サインイン`, `パスワードをお忘れですか?`},
},
}

for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
multipartestutils.RunCase(t, c, h)
})
}
}
56 changes: 56 additions & 0 deletions login/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ type Messages struct {
Available string
Unavailable string
SuccessfullyRename string

LoginWelcomeLabel string
LoginTitleLabel string

LoginAccountLabel string
LoginAccountPlaceholder string
LoginPasswordLabel string
LoginPasswordPlaceholder string
LoginSignInButtonLabel string
LoginForgetPasswordLabel string

LoginProviderGoogleText string
LoginProviderMicrosoftText string
LoginProviderGithubText string
}

func (m *Messages) UnreadMessages(n int) string {
Expand All @@ -48,6 +62,20 @@ var Messages_en_US = &Messages{
Available: "Available",
Unavailable: "Unavailable",
SuccessfullyRename: "Successfully renamed",

LoginWelcomeLabel: "Welcome",
LoginTitleLabel: "Qor Admin System",

LoginAccountLabel: "Email",
LoginAccountPlaceholder: "Please enter your email",
LoginPasswordLabel: "Password",
LoginPasswordPlaceholder: "Please enter your password",
LoginSignInButtonLabel: "Sign in",
LoginForgetPasswordLabel: "Forget your password?",

LoginProviderGoogleText: "Sign in with Google",
LoginProviderMicrosoftText: "Sign in with Microsoft",
LoginProviderGithubText: "Sign in with Github",
}

var Messages_zh_CN = &Messages{
Expand All @@ -68,6 +96,20 @@ var Messages_zh_CN = &Messages{
Available: "可用",
Unavailable: "不可用",
SuccessfullyRename: "成功重命名",

LoginWelcomeLabel: "欢迎",
LoginTitleLabel: "Qor 管理系统",

LoginAccountLabel: "邮箱",
LoginAccountPlaceholder: "请输入您的邮箱",
LoginPasswordLabel: "密码",
LoginPasswordPlaceholder: "请输入您的密码",
LoginSignInButtonLabel: "登录",
LoginForgetPasswordLabel: "忘记密码?",

LoginProviderGoogleText: "使用 Google 登录",
LoginProviderMicrosoftText: "使用 Microsoft 登录",
LoginProviderGithubText: "使用 Github 登录",
}

var Messages_ja_JP = &Messages{
Expand All @@ -88,4 +130,18 @@ var Messages_ja_JP = &Messages{
Available: "利用可能",
Unavailable: "利用不可",
SuccessfullyRename: "名前が変更されました",

LoginWelcomeLabel: "ようこそ",
LoginTitleLabel: "Qor 管理システム",

LoginAccountLabel: "メールアドレス",
LoginAccountPlaceholder: "メールアドレスを入力してください",
LoginPasswordLabel: "パスワード",
LoginPasswordPlaceholder: "パスワードを入力してください",
LoginSignInButtonLabel: "サインイン",
LoginForgetPasswordLabel: "パスワードをお忘れですか?",

LoginProviderGoogleText: "Google でログイン",
LoginProviderMicrosoftText: "Microsoft でログイン",
LoginProviderGithubText: "Github でログイン",
}
1 change: 1 addition & 0 deletions login/view_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ func (vc *ViewCommon) FormSubmitBtn(
label string,
) *VBtnBuilder {
return VBtn(label).
Variant(VariantFlat).
Color("primary").
Block(true).
Size(SizeLarge).
Expand Down
206 changes: 206 additions & 0 deletions login/views.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,212 @@
})
}

type OAuthProviderDisplay struct {
Logo HTMLComponent
Text string
}

type AdvancedLoginPageConfig struct {
WelcomeLabel string
TitleLabel string

Check warning on line 153 in login/views.go

View check run for this annotation

Codecov / codecov/patch

login/views.go#L146-L153

Added lines #L146 - L153 were not covered by tests
AccountLabel string
AccountPlaceholder string
PasswordLabel string
PasswordPlaceholder string
SignInButtonLabel string
ForgetPasswordLabel string
OAuthProviderDisplay func(provider *login.Provider) OAuthProviderDisplay
BrandLogo HTMLComponent
LeftImage HTMLComponent
}

Check warning on line 164 in login/views.go

View check run for this annotation

Codecov / codecov/patch

login/views.go#L155-L164

Added lines #L155 - L164 were not covered by tests
func NewAdvancedLoginPage(customize func(ctx *web.EventContext, config *AdvancedLoginPageConfig) (*AdvancedLoginPageConfig, error)) func(vh *login.ViewHelper, pb *presets.Builder) web.PageFunc {
return func(vh *login.ViewHelper, pb *presets.Builder) web.PageFunc {
return pb.PlainLayout(func(ctx *web.EventContext) (r web.PageResponse, err error) {
msgr := i18n.MustGetModuleMessages(ctx.R, I18nAdminLoginKey, Messages_en_US).(*Messages)

config := &AdvancedLoginPageConfig{
WelcomeLabel: msgr.LoginWelcomeLabel,
TitleLabel: msgr.LoginTitleLabel,
AccountLabel: msgr.LoginAccountLabel,
AccountPlaceholder: msgr.LoginAccountPlaceholder,
PasswordLabel: msgr.LoginPasswordLabel,
PasswordPlaceholder: msgr.LoginPasswordPlaceholder,
SignInButtonLabel: msgr.LoginSignInButtonLabel,
ForgetPasswordLabel: msgr.LoginForgetPasswordLabel,
OAuthProviderDisplay: func(provider *login.Provider) OAuthProviderDisplay {
return OAuthProviderDisplay{
Logo: provider.Logo,
Text: i18n.T(ctx.R, I18nAdminLoginKey, provider.Text),
}
},
BrandLogo: nil,
LeftImage: VImg().Class("fill-height").Cover(true).Src("https://cdn.vuetifyjs.com/images/parallax/material2.jpg"),
}

Check warning on line 187 in login/views.go

View check run for this annotation

Codecov / codecov/patch

login/views.go#L185-L187

Added lines #L185 - L187 were not covered by tests
if customize != nil {
config, err = customize(ctx, config)
if err != nil {
return r, err
}
}

// i18n start

Check warning on line 195 in login/views.go

View check run for this annotation

Codecov / codecov/patch

login/views.go#L189-L195

Added lines #L189 - L195 were not covered by tests
i18nBuilder := vh.I18n()
var langs []languageItem
var currLangVal string
qn := i18nBuilder.GetQueryName()
if ls := i18nBuilder.GetSupportLanguages(); len(ls) > 1 {
lang := ctx.R.FormValue(qn)
if lang == "" {
lang = i18nBuilder.GetCurrentLangFromCookie(ctx.R)
}
accept := ctx.R.Header.Get("Accept-Language")
_, mi := language.MatchStrings(language.NewMatcher(ls), lang, accept)
for i, l := range ls {
if i == mi {
currLangVal = l.String()
}
langs = append(langs, languageItem{
Label: display.Self.Name(l),
Value: l.String(),
})
}
}
// i18n end

Check warning on line 218 in login/views.go

View check run for this annotation

Codecov / codecov/patch

login/views.go#L215-L218

Added lines #L215 - L218 were not covered by tests
logoCompo := func() *HTMLTagBuilder {
return Div().Class("d-flex flex-row ga-6 px-6 py-2 rounded").Style("background-color: white;").Children(
RawHTML(`<svg width="61" height="24" viewBox="0 0 61 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.6667 -1.5H61V18.75H40.6667V-1.5ZM47.4445 5.25H54.2222V12H47.4445V5.25ZM47.4445 12V18.75H54.2222L47.4445 12Z" fill="#17A2F5"/>
<path d="M33.889 5.25041H27.1112V12.0004H33.889V5.25041Z" fill="#17A2F5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 -1.5H20.3332V18.75L0 18.75V-1.5ZM6.77777 5.25H13.5555V12H6.77777V5.25ZM20.3333 25.5L20.3332 18.75L13.5555 18.75L20.3333 25.5Z" fill="#17A2F5"/>
</svg>`),
If(config.BrandLogo != nil, Components(
VDivider().Color(ColorGreyDarken3).Vertical(true),
config.BrandLogo,
)),
)
}

leftCompo := VCol().Cols(0).Md(6).Class("hidden-md-and-down").Children(
config.LeftImage,
Div().Class("position-absolute").Style("top: 32px; left: 32px;").Children(
logoCompo(),
),
)

var oauthCompo HTMLComponent
if vh.OAuthEnabled() {
buttons := []HTMLComponent{}
for _, provider := range vh.OAuthProviders() {
display := config.OAuthProviderDisplay(provider)
buttons = append(buttons,
VBtn("").Class("bg-grey-lighten-4").
Block(true).
Size(SizeLarge).
Variant(VariantFlat).
Href(fmt.Sprintf("%s?provider=%s", vh.OAuthBeginURL(), provider.Key)).
Children(
Div().Class("d-flex flex-row ga-2 text-body-1").Children(
display.Logo,
Div().Class("text-body1").Text(display.Text),
),
),
)
}
if len(buttons) > 0 {
oauthCompo = Div().Class("d-flex flex-column ga-6").Children(buttons...)
}
}

Check warning on line 263 in login/views.go

View check run for this annotation

Codecov / codecov/patch

login/views.go#L263

Added line #L263 was not covered by tests
wIn := vh.GetWrongLoginInputFlash(ctx.W, ctx.R)
isRecaptchaEnabled := vh.RecaptchaEnabled()
if isRecaptchaEnabled {
DefaultViewCommon.InjectRecaptchaAssets(ctx, "login-form", "token")
}

Check warning on line 269 in login/views.go

View check run for this annotation

Codecov / codecov/patch

login/views.go#L267-L269

Added lines #L267 - L269 were not covered by tests
var userPassCompo HTMLComponent
if vh.UserPassEnabled() {
compo := Form().Class("d-flex flex-column").Id("login-form").Method(http.MethodPost).Action(vh.PasswordLoginURL())
compo.AppendChildren(
DefaultViewCommon.Input("account", config.AccountPlaceholder, wIn.Account).Class("mb-5").Label(config.AccountLabel),
DefaultViewCommon.PasswordInput("password", config.PasswordPlaceholder, wIn.Password, true).Class("mb-5").Label(config.PasswordLabel),
If(isRecaptchaEnabled,
// recaptcha response token
Input("token").Id("token").Type("hidden"),
),
DefaultViewCommon.FormSubmitBtn(config.SignInButtonLabel).
ClassIf("g-recaptcha", isRecaptchaEnabled).
AttrIf("data-sitekey", vh.RecaptchaSiteKey(), isRecaptchaEnabled).
AttrIf("data-callback", "onSubmit", isRecaptchaEnabled),
)

userPassCompo = Div().Class("d-flex flex-column").Children(
compo,
If(!vh.NoForgetPasswordLink(),
A(Text(config.ForgetPasswordLabel)).Href(vh.ForgetPasswordPageURL()).Class("align-self-end mt-2 mb-7 grey--text text-subtitle-2 text--darken-1"),
),
VDivider().Color(ColorGreyDarken3).Class("mt-2 mb-10"),
)
}

Check warning on line 294 in login/views.go

View check run for this annotation

Codecov / codecov/patch

login/views.go#L294

Added line #L294 was not covered by tests
var langCompo HTMLComponent
if len(langs) > 0 {
ctx.Injector.HeadHTML(`
<style>
.transparent-language-select.vx-select-wrap .v-input .v-field {
background-color: transparent;
}
.transparent-language-select.vx-select-wrap .v-input .v-field .v-select__selection-text {
font-size: 14px !important;
font-weight: 400;
}
.transparent-language-select.vx-select-wrap .v-input .v-field .v-field__outline {
display: none;
}
</style>
`)
langCompo = web.Scope().VSlot(" { locals : selectLocals } ").Init(fmt.Sprintf(`{currLangVal: '%s'}`, currLangVal)).Children(
vx.VXSelect().Class("transparent-language-select").
Items(langs).
ItemTitle("Label").
ItemValue("Value").
Attr("v-model", `selectLocals.currLangVal`).
Attr("@update:model-value", web.Plaid().MergeQuery(true).Query(qn, web.Var("selectLocals.currLangVal")).PushState(true).Go()),
)
}

Check warning on line 320 in login/views.go

View check run for this annotation

Codecov / codecov/patch

login/views.go#L320

Added line #L320 was not covered by tests
rightCompo := VCol().Cols(12).Md(6).Class("d-flex flex-column justify-center align-center").Children(
Div().Class("d-flex flex-column pa-4").Style("max-width: 455px; width: 100%").Children(
Div().Class("d-flex flex-row align-center ga-2 mb-5").Children(
Div().Class("hidden-lg-and-up mb-4").Children(
logoCompo(),
),
VSpacer(),
langCompo,
),
Div().Text(config.WelcomeLabel).Class("mb-4 text-h4"),
Div().Text(config.TitleLabel).Class("mb-16").Style("font-size: 42px; font-weight: 510;"),
userPassCompo,
oauthCompo,
),
)

r.PageTitle = config.TitleLabel
r.Body = Components(
DefaultViewCommon.Notice(vh, i18n.MustGetModuleMessages(ctx.R, login.I18nLoginKey, login.Messages_en_US).(*login.Messages), ctx.W, ctx.R),
VRow().NoGutters(true).
Attr(":class", "$vuetify.display.mdAndDown ? 'fill-height justify-center bg-grey-lighten-5':'fill-height justify-center'").Children(
leftCompo,
rightCompo,
),
)

return
})
}
}

Check warning on line 351 in login/views.go

View check run for this annotation

Codecov / codecov/patch

login/views.go#L348-L351

Added lines #L348 - L351 were not covered by tests
func defaultForgetPasswordPage(vh *login.ViewHelper, pb *presets.Builder) web.PageFunc {
return pb.PlainLayout(func(ctx *web.EventContext) (r web.PageResponse, err error) {
msgr := i18n.MustGetModuleMessages(ctx.R, login.I18nLoginKey, login.Messages_en_US).(*login.Messages)
Expand Down
Loading