123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 |
- package login
- import (
- "bytes"
- "encoding/base64"
- "fmt"
- "image/png"
- "net/http"
- "net/url"
- "github.com/pquerna/otp"
- "github.com/qor5/admin/presets"
- v "github.com/qor5/ui/vuetify"
- "github.com/qor5/web"
- "github.com/qor5/x/i18n"
- "github.com/qor5/x/login"
- . "github.com/theplant/htmlgo"
- "golang.org/x/text/language"
- "golang.org/x/text/language/display"
- )
- type languageItem struct {
- Label string
- Value string
- }
- func defaultLoginPage(vh *login.ViewHelper, pb *presets.Builder) web.PageFunc {
- return pb.PlainLayout(func(ctx *web.EventContext) (r web.PageResponse, err error) {
- // i18n start
- msgr := i18n.MustGetModuleMessages(ctx.R, login.I18nLoginKey, login.Messages_en_US).(*login.Messages)
- i18nBuilder := vh.I18n()
- var langs []languageItem
- var currLangVal string
- if ls := i18nBuilder.GetSupportLanguages(); len(ls) > 1 {
- qn := i18nBuilder.GetQueryName()
- 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 {
- u, _ := url.Parse(ctx.R.RequestURI)
- qs := u.Query()
- qs.Set(qn, l.String())
- u.RawQuery = qs.Encode()
- if i == mi {
- currLangVal = u.String()
- }
- langs = append(langs, languageItem{
- Label: display.Self.Name(l),
- Value: u.String(),
- })
- }
- }
- // i18n end
- var oauthHTML HTMLComponent
- if vh.OAuthEnabled() {
- ul := Div().Class("d-flex flex-column justify-center mt-8 text-center")
- for _, provider := range vh.OAuthProviders() {
- ul.AppendChildren(
- v.VBtn("").
- Block(true).
- Large(true).
- Class("mt-4").
- Outlined(true).
- Href(fmt.Sprintf("%s?provider=%s", vh.OAuthBeginURL(), provider.Key)).
- Children(
- Div(
- provider.Logo,
- ).Class("mr-2"),
- Text(provider.Text),
- ),
- )
- }
- oauthHTML = Div(
- ul,
- )
- }
- wIn := vh.GetWrongLoginInputFlash(ctx.W, ctx.R)
- isRecaptchaEnabled := vh.RecaptchaEnabled()
- if isRecaptchaEnabled {
- DefaultViewCommon.InjectRecaptchaAssets(ctx, "login-form", "token")
- }
- var userPassHTML HTMLComponent
- if vh.UserPassEnabled() {
- userPassHTML = Div(
- Form(
- Div(
- Label(msgr.AccountLabel).Class(DefaultViewCommon.LabelClass).For("account"),
- DefaultViewCommon.Input("account", msgr.AccountPlaceholder, wIn.Account),
- ),
- Div(
- Label(msgr.PasswordLabel).Class(DefaultViewCommon.LabelClass).For("password"),
- DefaultViewCommon.PasswordInput("password", msgr.PasswordPlaceholder, wIn.Password, true),
- ).Class("mt-6"),
- If(isRecaptchaEnabled,
- // recaptcha response token
- Input("token").Id("token").Type("hidden"),
- ),
- DefaultViewCommon.FormSubmitBtn(msgr.SignInBtn).
- ClassIf("g-recaptcha", isRecaptchaEnabled).
- AttrIf("data-sitekey", vh.RecaptchaSiteKey(), isRecaptchaEnabled).
- AttrIf("data-callback", "onSubmit", isRecaptchaEnabled),
- ).Id("login-form").Method(http.MethodPost).Action(vh.PasswordLoginURL()),
- If(!vh.NoForgetPasswordLink(),
- Div(
- A(Text(msgr.ForgetPasswordLink)).Href(vh.ForgetPasswordPageURL()).
- Class("grey--text text--darken-1"),
- ).Class("text-right mt-2"),
- ),
- )
- }
- r.PageTitle = msgr.LoginPageTitle
- var bodyForm HTMLComponent
- bodyForm = Div(
- userPassHTML,
- oauthHTML,
- If(len(langs) > 0,
- v.VSelect().
- Items(langs).
- ItemText("Label").
- ItemValue("Value").
- Attr(web.InitContextVars, fmt.Sprintf(`{currLangVal: '%s'}`, currLangVal)).
- Attr("v-model", `vars.currLangVal`).
- Attr("@change", `window.location.href=vars.currLangVal`).
- Outlined(true).
- Dense(true).
- Class("mt-12").
- HideDetails(true),
- ),
- ).Class(DefaultViewCommon.WrapperClass).Style(DefaultViewCommon.WrapperStyle)
- r.Body = Div(
- DefaultViewCommon.Notice(vh, msgr, ctx.W, ctx.R),
- bodyForm,
- )
- return
- })
- }
- 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)
- wIn := vh.GetWrongForgetPasswordInputFlash(ctx.W, ctx.R)
- secondsToResend := vh.GetSecondsToRedoFlash(ctx.W, ctx.R)
- activeBtnText := msgr.SendResetPasswordEmailBtn
- inactiveBtnText := msgr.ResendResetPasswordEmailBtn
- inactiveBtnTextWithInitSeconds := fmt.Sprintf("%s (%d)", inactiveBtnText, secondsToResend)
- doTOTP := ctx.R.URL.Query().Get("totp") == "1"
- actionURL := vh.SendResetPasswordLinkURL()
- if doTOTP {
- actionURL = login.MustSetQuery(actionURL, "totp", "1")
- }
- isRecaptchaEnabled := vh.RecaptchaEnabled()
- if isRecaptchaEnabled {
- DefaultViewCommon.InjectRecaptchaAssets(ctx, "forget-form", "token")
- }
- r.PageTitle = msgr.ForgetPasswordPageTitle
- r.Body = Div(
- DefaultViewCommon.Notice(vh, msgr, ctx.W, ctx.R),
- If(secondsToResend > 0,
- DefaultViewCommon.WarnNotice(msgr.SendEmailTooFrequentlyNotice),
- ),
- Div(
- H1(msgr.ForgotMyPasswordTitle).Class(DefaultViewCommon.TitleClass),
- Form(
- Div(
- Label(msgr.ForgetPasswordEmailLabel).Class(DefaultViewCommon.LabelClass).For("account"),
- DefaultViewCommon.Input("account", msgr.ForgetPasswordEmailPlaceholder, wIn.Account),
- ),
- If(doTOTP,
- Div(
- Label(msgr.TOTPValidateCodeLabel).Class(DefaultViewCommon.LabelClass).For("otp"),
- DefaultViewCommon.Input("otp", msgr.TOTPValidateCodePlaceholder, wIn.TOTP),
- ).Class("mt-6"),
- ),
- If(isRecaptchaEnabled,
- // recaptcha response token
- Input("token").Id("token").Type("hidden"),
- ),
- DefaultViewCommon.FormSubmitBtn(inactiveBtnTextWithInitSeconds).
- Attr("id", "disabledBtn").
- ClassIf("d-none", secondsToResend <= 0),
- DefaultViewCommon.FormSubmitBtn(activeBtnText).
- ClassIf("g-recaptcha", isRecaptchaEnabled).
- AttrIf("data-sitekey", vh.RecaptchaSiteKey(), isRecaptchaEnabled).
- AttrIf("data-callback", "onSubmit", isRecaptchaEnabled).
- Attr("id", "submitBtn").
- ClassIf("d-none", secondsToResend > 0),
- ).Id("forget-form").Method(http.MethodPost).Action(actionURL),
- ).Class(DefaultViewCommon.WrapperClass).Style(DefaultViewCommon.WrapperStyle),
- )
- if secondsToResend > 0 {
- ctx.Injector.TailHTML(fmt.Sprintf(`
- <script>
- (function(){
- var secondsToResend = %d;
- var btnText = "%s";
- var disabledBtn = document.getElementById("disabledBtn");
- var submitBtn = document.getElementById("submitBtn");
- var interv = setInterval(function(){
- secondsToResend--;
- if (secondsToResend === 0) {
- clearInterval(interv);
- disabledBtn.classList.add("d-none");
- submitBtn.innerText = btnText;
- submitBtn.classList.remove("d-none");
- return;
- }
- disabledBtn.innerText = btnText + " (" + secondsToResend + ")" ;
- }, 1000);
- })();
- </script>
- `, secondsToResend, inactiveBtnText))
- }
- return
- })
- }
- func defaultResetPasswordLinkSentPage(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)
- a := ctx.R.URL.Query().Get("a")
- r.PageTitle = msgr.ResetPasswordLinkSentPageTitle
- r.Body = Div(
- DefaultViewCommon.Notice(vh, msgr, ctx.W, ctx.R),
- Div(
- H1(fmt.Sprintf("%s %s.", msgr.ResetPasswordLinkWasSentTo, a)).Class("text-h5"),
- H2(msgr.ResetPasswordLinkSentPrompt).Class("text-body-1 mt-2"),
- ).Class(DefaultViewCommon.WrapperClass).Style(DefaultViewCommon.WrapperStyle),
- )
- return
- })
- }
- func defaultResetPasswordPage(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)
- wIn := vh.GetWrongResetPasswordInputFlash(ctx.W, ctx.R)
- doTOTP := ctx.R.URL.Query().Get("totp") == "1"
- actionURL := vh.ResetPasswordURL()
- if doTOTP {
- actionURL = login.MustSetQuery(actionURL, "totp", "1")
- }
- var user interface{}
- r.PageTitle = msgr.ResetPasswordPageTitle
- query := ctx.R.URL.Query()
- id := query.Get("id")
- if id == "" {
- r.Body = Div(Text("user not found"))
- return r, nil
- } else {
- user, err = vh.FindUserByID(id)
- if err != nil {
- if err == login.ErrUserNotFound {
- r.Body = Div(Text("user not found"))
- return r, nil
- }
- panic(err)
- }
- }
- token := query.Get("token")
- if token == "" {
- r.Body = Div(Text("invalid token"))
- return r, nil
- } else {
- storedToken, _, expired := user.(login.UserPasser).GetResetPasswordToken()
- if expired {
- r.Body = Div(Text("token expired"))
- return r, nil
- }
- if token != storedToken {
- r.Body = Div(Text("invalid token"))
- return r, nil
- }
- }
- DefaultViewCommon.InjectZxcvbn(ctx)
- r.Body = Div(
- DefaultViewCommon.Notice(vh, msgr, ctx.W, ctx.R),
- Div(
- H1(msgr.ResetYourPasswordTitle).Class(DefaultViewCommon.TitleClass),
- Form(
- Input("user_id").Type("hidden").Value(id),
- Input("token").Type("hidden").Value(token),
- Div(
- Label(msgr.ResetPasswordLabel).Class(DefaultViewCommon.LabelClass).For("password"),
- DefaultViewCommon.PasswordInputWithStrengthMeter(DefaultViewCommon.PasswordInput("password", msgr.ResetPasswordLabel, wIn.Password, true), "password", wIn.Password),
- ),
- Div(
- Label(msgr.ResetPasswordConfirmLabel).Class(DefaultViewCommon.LabelClass).For("confirm_password"),
- DefaultViewCommon.PasswordInput("confirm_password", msgr.ResetPasswordConfirmPlaceholder, wIn.ConfirmPassword, true),
- ).Class("mt-6"),
- If(doTOTP,
- Div(
- Label(msgr.TOTPValidateCodeLabel).Class(DefaultViewCommon.LabelClass).For("otp"),
- DefaultViewCommon.Input("otp", msgr.TOTPValidateCodePlaceholder, wIn.TOTP),
- ).Class("mt-6"),
- ),
- DefaultViewCommon.FormSubmitBtn(msgr.Confirm),
- ).Method(http.MethodPost).Action(actionURL),
- ).Class(DefaultViewCommon.WrapperClass).Style(DefaultViewCommon.WrapperStyle),
- )
- return
- })
- }
- func defaultChangePasswordPage(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)
- wIn := vh.GetWrongChangePasswordInputFlash(ctx.W, ctx.R)
- DefaultViewCommon.InjectZxcvbn(ctx)
- r.PageTitle = msgr.ChangePasswordPageTitle
- r.Body = Div(
- DefaultViewCommon.Notice(vh, msgr, ctx.W, ctx.R),
- Div(
- H1(msgr.ChangePasswordTitle).Class(DefaultViewCommon.TitleClass),
- Form(
- Div(
- Label(msgr.ChangePasswordOldLabel).Class(DefaultViewCommon.LabelClass).For("old_password"),
- DefaultViewCommon.PasswordInput("old_password", msgr.ChangePasswordOldPlaceholder, wIn.OldPassword, true),
- ),
- Div(
- Label(msgr.ChangePasswordNewLabel).Class(DefaultViewCommon.LabelClass).For("password"),
- DefaultViewCommon.PasswordInputWithStrengthMeter(DefaultViewCommon.PasswordInput("password", msgr.ChangePasswordNewPlaceholder, wIn.NewPassword, true), "password", wIn.NewPassword),
- ).Class("mt-6"),
- Div(
- Label(msgr.ChangePasswordNewConfirmLabel).Class(DefaultViewCommon.LabelClass).For("confirm_password"),
- DefaultViewCommon.PasswordInput("confirm_password", msgr.ChangePasswordNewConfirmPlaceholder, wIn.ConfirmPassword, true),
- ).Class("mt-6"),
- If(vh.TOTPEnabled(),
- Div(
- Label(msgr.TOTPValidateCodeLabel).Class(DefaultViewCommon.LabelClass).For("otp"),
- DefaultViewCommon.Input("otp", msgr.TOTPValidateCodePlaceholder, wIn.TOTP),
- ).Class("mt-6"),
- ),
- DefaultViewCommon.FormSubmitBtn(msgr.Confirm),
- ).Method(http.MethodPost).Action(vh.ChangePasswordURL()),
- ).Class(DefaultViewCommon.WrapperClass).Style(DefaultViewCommon.WrapperStyle),
- )
- return
- })
- }
- func changePasswordDialog(vh *login.ViewHelper, ctx *web.EventContext, showVar string, content HTMLComponent) HTMLComponent {
- pmsgr := presets.MustGetMessages(ctx.R)
- return v.VDialog(
- v.VCard(
- content,
- v.VCardActions(
- v.VSpacer(),
- v.VBtn(pmsgr.Cancel).
- Depressed(true).
- Class("ml-2").
- On("click", fmt.Sprintf("vars.%s = false", showVar)),
- v.VBtn(pmsgr.OK).
- Color("primary").
- Depressed(true).
- Dark(true).
- Attr("@click", web.Plaid().EventFunc("login_changePassword").Go()),
- ),
- ),
- ).MaxWidth("600px").
- Attr("v-model", fmt.Sprintf("vars.%s", showVar)).
- Attr(web.InitContextVars, fmt.Sprintf(`{%s: false}`, showVar))
- }
- func defaultChangePasswordDialogContent(vh *login.ViewHelper, pb *presets.Builder) func(ctx *web.EventContext) HTMLComponent {
- return func(ctx *web.EventContext) HTMLComponent {
- msgr := i18n.MustGetModuleMessages(ctx.R, login.I18nLoginKey, login.Messages_en_US).(*login.Messages)
- return Div(
- v.VCardTitle(Text(msgr.ChangePasswordTitle)),
- v.VCardText(
- Div(
- DefaultViewCommon.PasswordInput("old_password", msgr.ChangePasswordOldPlaceholder, "", true).
- Outlined(false).
- Label(msgr.ChangePasswordOldLabel).
- FieldName("old_password"),
- ),
- Div(
- DefaultViewCommon.PasswordInputWithStrengthMeter(
- DefaultViewCommon.PasswordInput("password", msgr.ChangePasswordNewPlaceholder, "", true).
- Outlined(false).
- Label(msgr.ChangePasswordNewLabel).
- FieldName("password"),
- "password", ""),
- ).Class("mt-12"),
- Div(
- DefaultViewCommon.PasswordInput("confirm_password", msgr.ChangePasswordNewConfirmPlaceholder, "", true).
- Outlined(false).
- Label(msgr.ChangePasswordNewConfirmLabel).
- FieldName("confirm_password"),
- ).Class("mt-12"),
- If(vh.TOTPEnabled(),
- Div(
- DefaultViewCommon.Input("otp", msgr.TOTPValidateCodePlaceholder, "").
- Outlined(false).
- Label(msgr.TOTPValidateCodeLabel).
- FieldName("otp"),
- ).Class("mt-12"),
- ),
- ),
- )
- }
- }
- func defaultTOTPSetupPage(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)
- user := login.GetCurrentUser(ctx.R)
- u := user.(login.UserPasser)
- var QRCode bytes.Buffer
- // Generate key from TOTPSecret
- var key *otp.Key
- totpSecret := u.GetTOTPSecret()
- if len(totpSecret) == 0 {
- r.Body = DefaultViewCommon.ErrorBody("need setup totp")
- return
- }
- key, err = otp.NewKeyFromURL(
- fmt.Sprintf("otpauth://totp/%s:%s?issuer=%s&secret=%s",
- url.PathEscape(vh.TOTPIssuer()),
- url.PathEscape(u.GetAccountName()),
- url.QueryEscape(vh.TOTPIssuer()),
- url.QueryEscape(totpSecret),
- ),
- )
- img, err := key.Image(200, 200)
- if err != nil {
- r.Body = DefaultViewCommon.ErrorBody(err.Error())
- return
- }
- err = png.Encode(&QRCode, img)
- if err != nil {
- r.Body = DefaultViewCommon.ErrorBody(err.Error())
- return
- }
- r.PageTitle = msgr.TOTPSetupPageTitle
- r.Body = Div(
- DefaultViewCommon.Notice(vh, msgr, ctx.W, ctx.R),
- Div(
- Div(
- H1(msgr.TOTPSetupTitle).
- Class(DefaultViewCommon.TitleClass),
- Label(msgr.TOTPSetupScanPrompt),
- ),
- Div(
- Img(fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString(QRCode.Bytes()))),
- ).Class("d-flex justify-center my-2"),
- Div(
- Label(msgr.TOTPSetupSecretPrompt),
- ),
- Div(Label(u.GetTOTPSecret())).Class("font-weight-bold my-4"),
- Form(
- Label(msgr.TOTPSetupEnterCodePrompt),
- DefaultViewCommon.Input("otp", msgr.TOTPSetupCodePlaceholder, "").Class("mt-6"),
- DefaultViewCommon.FormSubmitBtn(msgr.Verify),
- ).Method(http.MethodPost).Action(vh.ValidateTOTPURL()),
- ).Class(DefaultViewCommon.WrapperClass).Style(DefaultViewCommon.WrapperStyle).Class("text-center"),
- )
- return
- })
- }
- func defaultTOTPValidatePage(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)
- r.PageTitle = msgr.TOTPValidatePageTitle
- r.Body = Div(
- DefaultViewCommon.Notice(vh, msgr, ctx.W, ctx.R),
- Div(
- Div(
- H1(msgr.TOTPValidateTitle).
- Class(DefaultViewCommon.TitleClass),
- Label(msgr.TOTPValidateEnterCodePrompt),
- ),
- Form(
- DefaultViewCommon.Input("otp", msgr.TOTPValidateCodePlaceholder, "").Autofocus(true).Class("mt-6"),
- DefaultViewCommon.FormSubmitBtn(msgr.Verify),
- ).Method(http.MethodPost).Action(vh.ValidateTOTPURL()),
- ).Class(DefaultViewCommon.WrapperClass).Style(DefaultViewCommon.WrapperStyle).Class("text-center"),
- )
- return
- })
- }
|