views.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. package login
  2. import (
  3. "bytes"
  4. "encoding/base64"
  5. "fmt"
  6. "image/png"
  7. "net/http"
  8. "net/url"
  9. "github.com/pquerna/otp"
  10. "github.com/qor5/admin/presets"
  11. v "github.com/qor5/ui/vuetify"
  12. "github.com/qor5/web"
  13. "github.com/qor5/x/i18n"
  14. "github.com/qor5/x/login"
  15. . "github.com/theplant/htmlgo"
  16. "golang.org/x/text/language"
  17. "golang.org/x/text/language/display"
  18. )
  19. type languageItem struct {
  20. Label string
  21. Value string
  22. }
  23. func defaultLoginPage(vh *login.ViewHelper, pb *presets.Builder) web.PageFunc {
  24. return pb.PlainLayout(func(ctx *web.EventContext) (r web.PageResponse, err error) {
  25. // i18n start
  26. msgr := i18n.MustGetModuleMessages(ctx.R, login.I18nLoginKey, login.Messages_en_US).(*login.Messages)
  27. i18nBuilder := vh.I18n()
  28. var langs []languageItem
  29. var currLangVal string
  30. if ls := i18nBuilder.GetSupportLanguages(); len(ls) > 1 {
  31. qn := i18nBuilder.GetQueryName()
  32. lang := ctx.R.FormValue(qn)
  33. if lang == "" {
  34. lang = i18nBuilder.GetCurrentLangFromCookie(ctx.R)
  35. }
  36. accept := ctx.R.Header.Get("Accept-Language")
  37. _, mi := language.MatchStrings(language.NewMatcher(ls), lang, accept)
  38. for i, l := range ls {
  39. u, _ := url.Parse(ctx.R.RequestURI)
  40. qs := u.Query()
  41. qs.Set(qn, l.String())
  42. u.RawQuery = qs.Encode()
  43. if i == mi {
  44. currLangVal = u.String()
  45. }
  46. langs = append(langs, languageItem{
  47. Label: display.Self.Name(l),
  48. Value: u.String(),
  49. })
  50. }
  51. }
  52. // i18n end
  53. var oauthHTML HTMLComponent
  54. if vh.OAuthEnabled() {
  55. ul := Div().Class("d-flex flex-column justify-center mt-8 text-center")
  56. for _, provider := range vh.OAuthProviders() {
  57. ul.AppendChildren(
  58. v.VBtn("").
  59. Block(true).
  60. Large(true).
  61. Class("mt-4").
  62. Outlined(true).
  63. Href(fmt.Sprintf("%s?provider=%s", vh.OAuthBeginURL(), provider.Key)).
  64. Children(
  65. Div(
  66. provider.Logo,
  67. ).Class("mr-2"),
  68. Text(provider.Text),
  69. ),
  70. )
  71. }
  72. oauthHTML = Div(
  73. ul,
  74. )
  75. }
  76. wIn := vh.GetWrongLoginInputFlash(ctx.W, ctx.R)
  77. isRecaptchaEnabled := vh.RecaptchaEnabled()
  78. if isRecaptchaEnabled {
  79. DefaultViewCommon.InjectRecaptchaAssets(ctx, "login-form", "token")
  80. }
  81. var userPassHTML HTMLComponent
  82. if vh.UserPassEnabled() {
  83. userPassHTML = Div(
  84. Form(
  85. Div(
  86. Label(msgr.AccountLabel).Class(DefaultViewCommon.LabelClass).For("account"),
  87. DefaultViewCommon.Input("account", msgr.AccountPlaceholder, wIn.Account),
  88. ),
  89. Div(
  90. Label(msgr.PasswordLabel).Class(DefaultViewCommon.LabelClass).For("password"),
  91. DefaultViewCommon.PasswordInput("password", msgr.PasswordPlaceholder, wIn.Password, true),
  92. ).Class("mt-6"),
  93. If(isRecaptchaEnabled,
  94. // recaptcha response token
  95. Input("token").Id("token").Type("hidden"),
  96. ),
  97. DefaultViewCommon.FormSubmitBtn(msgr.SignInBtn).
  98. ClassIf("g-recaptcha", isRecaptchaEnabled).
  99. AttrIf("data-sitekey", vh.RecaptchaSiteKey(), isRecaptchaEnabled).
  100. AttrIf("data-callback", "onSubmit", isRecaptchaEnabled),
  101. ).Id("login-form").Method(http.MethodPost).Action(vh.PasswordLoginURL()),
  102. If(!vh.NoForgetPasswordLink(),
  103. Div(
  104. A(Text(msgr.ForgetPasswordLink)).Href(vh.ForgetPasswordPageURL()).
  105. Class("grey--text text--darken-1"),
  106. ).Class("text-right mt-2"),
  107. ),
  108. )
  109. }
  110. r.PageTitle = msgr.LoginPageTitle
  111. var bodyForm HTMLComponent
  112. bodyForm = Div(
  113. userPassHTML,
  114. oauthHTML,
  115. If(len(langs) > 0,
  116. v.VSelect().
  117. Items(langs).
  118. ItemText("Label").
  119. ItemValue("Value").
  120. Attr(web.InitContextVars, fmt.Sprintf(`{currLangVal: '%s'}`, currLangVal)).
  121. Attr("v-model", `vars.currLangVal`).
  122. Attr("@change", `window.location.href=vars.currLangVal`).
  123. Outlined(true).
  124. Dense(true).
  125. Class("mt-12").
  126. HideDetails(true),
  127. ),
  128. ).Class(DefaultViewCommon.WrapperClass).Style(DefaultViewCommon.WrapperStyle)
  129. r.Body = Div(
  130. DefaultViewCommon.Notice(vh, msgr, ctx.W, ctx.R),
  131. bodyForm,
  132. )
  133. return
  134. })
  135. }
  136. func defaultForgetPasswordPage(vh *login.ViewHelper, pb *presets.Builder) web.PageFunc {
  137. return pb.PlainLayout(func(ctx *web.EventContext) (r web.PageResponse, err error) {
  138. msgr := i18n.MustGetModuleMessages(ctx.R, login.I18nLoginKey, login.Messages_en_US).(*login.Messages)
  139. wIn := vh.GetWrongForgetPasswordInputFlash(ctx.W, ctx.R)
  140. secondsToResend := vh.GetSecondsToRedoFlash(ctx.W, ctx.R)
  141. activeBtnText := msgr.SendResetPasswordEmailBtn
  142. inactiveBtnText := msgr.ResendResetPasswordEmailBtn
  143. inactiveBtnTextWithInitSeconds := fmt.Sprintf("%s (%d)", inactiveBtnText, secondsToResend)
  144. doTOTP := ctx.R.URL.Query().Get("totp") == "1"
  145. actionURL := vh.SendResetPasswordLinkURL()
  146. if doTOTP {
  147. actionURL = login.MustSetQuery(actionURL, "totp", "1")
  148. }
  149. isRecaptchaEnabled := vh.RecaptchaEnabled()
  150. if isRecaptchaEnabled {
  151. DefaultViewCommon.InjectRecaptchaAssets(ctx, "forget-form", "token")
  152. }
  153. r.PageTitle = msgr.ForgetPasswordPageTitle
  154. r.Body = Div(
  155. DefaultViewCommon.Notice(vh, msgr, ctx.W, ctx.R),
  156. If(secondsToResend > 0,
  157. DefaultViewCommon.WarnNotice(msgr.SendEmailTooFrequentlyNotice),
  158. ),
  159. Div(
  160. H1(msgr.ForgotMyPasswordTitle).Class(DefaultViewCommon.TitleClass),
  161. Form(
  162. Div(
  163. Label(msgr.ForgetPasswordEmailLabel).Class(DefaultViewCommon.LabelClass).For("account"),
  164. DefaultViewCommon.Input("account", msgr.ForgetPasswordEmailPlaceholder, wIn.Account),
  165. ),
  166. If(doTOTP,
  167. Div(
  168. Label(msgr.TOTPValidateCodeLabel).Class(DefaultViewCommon.LabelClass).For("otp"),
  169. DefaultViewCommon.Input("otp", msgr.TOTPValidateCodePlaceholder, wIn.TOTP),
  170. ).Class("mt-6"),
  171. ),
  172. If(isRecaptchaEnabled,
  173. // recaptcha response token
  174. Input("token").Id("token").Type("hidden"),
  175. ),
  176. DefaultViewCommon.FormSubmitBtn(inactiveBtnTextWithInitSeconds).
  177. Attr("id", "disabledBtn").
  178. ClassIf("d-none", secondsToResend <= 0),
  179. DefaultViewCommon.FormSubmitBtn(activeBtnText).
  180. ClassIf("g-recaptcha", isRecaptchaEnabled).
  181. AttrIf("data-sitekey", vh.RecaptchaSiteKey(), isRecaptchaEnabled).
  182. AttrIf("data-callback", "onSubmit", isRecaptchaEnabled).
  183. Attr("id", "submitBtn").
  184. ClassIf("d-none", secondsToResend > 0),
  185. ).Id("forget-form").Method(http.MethodPost).Action(actionURL),
  186. ).Class(DefaultViewCommon.WrapperClass).Style(DefaultViewCommon.WrapperStyle),
  187. )
  188. if secondsToResend > 0 {
  189. ctx.Injector.TailHTML(fmt.Sprintf(`
  190. <script>
  191. (function(){
  192. var secondsToResend = %d;
  193. var btnText = "%s";
  194. var disabledBtn = document.getElementById("disabledBtn");
  195. var submitBtn = document.getElementById("submitBtn");
  196. var interv = setInterval(function(){
  197. secondsToResend--;
  198. if (secondsToResend === 0) {
  199. clearInterval(interv);
  200. disabledBtn.classList.add("d-none");
  201. submitBtn.innerText = btnText;
  202. submitBtn.classList.remove("d-none");
  203. return;
  204. }
  205. disabledBtn.innerText = btnText + " (" + secondsToResend + ")" ;
  206. }, 1000);
  207. })();
  208. </script>
  209. `, secondsToResend, inactiveBtnText))
  210. }
  211. return
  212. })
  213. }
  214. func defaultResetPasswordLinkSentPage(vh *login.ViewHelper, pb *presets.Builder) web.PageFunc {
  215. return pb.PlainLayout(func(ctx *web.EventContext) (r web.PageResponse, err error) {
  216. msgr := i18n.MustGetModuleMessages(ctx.R, login.I18nLoginKey, login.Messages_en_US).(*login.Messages)
  217. a := ctx.R.URL.Query().Get("a")
  218. r.PageTitle = msgr.ResetPasswordLinkSentPageTitle
  219. r.Body = Div(
  220. DefaultViewCommon.Notice(vh, msgr, ctx.W, ctx.R),
  221. Div(
  222. H1(fmt.Sprintf("%s %s.", msgr.ResetPasswordLinkWasSentTo, a)).Class("text-h5"),
  223. H2(msgr.ResetPasswordLinkSentPrompt).Class("text-body-1 mt-2"),
  224. ).Class(DefaultViewCommon.WrapperClass).Style(DefaultViewCommon.WrapperStyle),
  225. )
  226. return
  227. })
  228. }
  229. func defaultResetPasswordPage(vh *login.ViewHelper, pb *presets.Builder) web.PageFunc {
  230. return pb.PlainLayout(func(ctx *web.EventContext) (r web.PageResponse, err error) {
  231. msgr := i18n.MustGetModuleMessages(ctx.R, login.I18nLoginKey, login.Messages_en_US).(*login.Messages)
  232. wIn := vh.GetWrongResetPasswordInputFlash(ctx.W, ctx.R)
  233. doTOTP := ctx.R.URL.Query().Get("totp") == "1"
  234. actionURL := vh.ResetPasswordURL()
  235. if doTOTP {
  236. actionURL = login.MustSetQuery(actionURL, "totp", "1")
  237. }
  238. var user interface{}
  239. r.PageTitle = msgr.ResetPasswordPageTitle
  240. query := ctx.R.URL.Query()
  241. id := query.Get("id")
  242. if id == "" {
  243. r.Body = Div(Text("user not found"))
  244. return r, nil
  245. } else {
  246. user, err = vh.FindUserByID(id)
  247. if err != nil {
  248. if err == login.ErrUserNotFound {
  249. r.Body = Div(Text("user not found"))
  250. return r, nil
  251. }
  252. panic(err)
  253. }
  254. }
  255. token := query.Get("token")
  256. if token == "" {
  257. r.Body = Div(Text("invalid token"))
  258. return r, nil
  259. } else {
  260. storedToken, _, expired := user.(login.UserPasser).GetResetPasswordToken()
  261. if expired {
  262. r.Body = Div(Text("token expired"))
  263. return r, nil
  264. }
  265. if token != storedToken {
  266. r.Body = Div(Text("invalid token"))
  267. return r, nil
  268. }
  269. }
  270. DefaultViewCommon.InjectZxcvbn(ctx)
  271. r.Body = Div(
  272. DefaultViewCommon.Notice(vh, msgr, ctx.W, ctx.R),
  273. Div(
  274. H1(msgr.ResetYourPasswordTitle).Class(DefaultViewCommon.TitleClass),
  275. Form(
  276. Input("user_id").Type("hidden").Value(id),
  277. Input("token").Type("hidden").Value(token),
  278. Div(
  279. Label(msgr.ResetPasswordLabel).Class(DefaultViewCommon.LabelClass).For("password"),
  280. DefaultViewCommon.PasswordInputWithStrengthMeter(DefaultViewCommon.PasswordInput("password", msgr.ResetPasswordLabel, wIn.Password, true), "password", wIn.Password),
  281. ),
  282. Div(
  283. Label(msgr.ResetPasswordConfirmLabel).Class(DefaultViewCommon.LabelClass).For("confirm_password"),
  284. DefaultViewCommon.PasswordInput("confirm_password", msgr.ResetPasswordConfirmPlaceholder, wIn.ConfirmPassword, true),
  285. ).Class("mt-6"),
  286. If(doTOTP,
  287. Div(
  288. Label(msgr.TOTPValidateCodeLabel).Class(DefaultViewCommon.LabelClass).For("otp"),
  289. DefaultViewCommon.Input("otp", msgr.TOTPValidateCodePlaceholder, wIn.TOTP),
  290. ).Class("mt-6"),
  291. ),
  292. DefaultViewCommon.FormSubmitBtn(msgr.Confirm),
  293. ).Method(http.MethodPost).Action(actionURL),
  294. ).Class(DefaultViewCommon.WrapperClass).Style(DefaultViewCommon.WrapperStyle),
  295. )
  296. return
  297. })
  298. }
  299. func defaultChangePasswordPage(vh *login.ViewHelper, pb *presets.Builder) web.PageFunc {
  300. return pb.PlainLayout(func(ctx *web.EventContext) (r web.PageResponse, err error) {
  301. msgr := i18n.MustGetModuleMessages(ctx.R, login.I18nLoginKey, login.Messages_en_US).(*login.Messages)
  302. wIn := vh.GetWrongChangePasswordInputFlash(ctx.W, ctx.R)
  303. DefaultViewCommon.InjectZxcvbn(ctx)
  304. r.PageTitle = msgr.ChangePasswordPageTitle
  305. r.Body = Div(
  306. DefaultViewCommon.Notice(vh, msgr, ctx.W, ctx.R),
  307. Div(
  308. H1(msgr.ChangePasswordTitle).Class(DefaultViewCommon.TitleClass),
  309. Form(
  310. Div(
  311. Label(msgr.ChangePasswordOldLabel).Class(DefaultViewCommon.LabelClass).For("old_password"),
  312. DefaultViewCommon.PasswordInput("old_password", msgr.ChangePasswordOldPlaceholder, wIn.OldPassword, true),
  313. ),
  314. Div(
  315. Label(msgr.ChangePasswordNewLabel).Class(DefaultViewCommon.LabelClass).For("password"),
  316. DefaultViewCommon.PasswordInputWithStrengthMeter(DefaultViewCommon.PasswordInput("password", msgr.ChangePasswordNewPlaceholder, wIn.NewPassword, true), "password", wIn.NewPassword),
  317. ).Class("mt-6"),
  318. Div(
  319. Label(msgr.ChangePasswordNewConfirmLabel).Class(DefaultViewCommon.LabelClass).For("confirm_password"),
  320. DefaultViewCommon.PasswordInput("confirm_password", msgr.ChangePasswordNewConfirmPlaceholder, wIn.ConfirmPassword, true),
  321. ).Class("mt-6"),
  322. If(vh.TOTPEnabled(),
  323. Div(
  324. Label(msgr.TOTPValidateCodeLabel).Class(DefaultViewCommon.LabelClass).For("otp"),
  325. DefaultViewCommon.Input("otp", msgr.TOTPValidateCodePlaceholder, wIn.TOTP),
  326. ).Class("mt-6"),
  327. ),
  328. DefaultViewCommon.FormSubmitBtn(msgr.Confirm),
  329. ).Method(http.MethodPost).Action(vh.ChangePasswordURL()),
  330. ).Class(DefaultViewCommon.WrapperClass).Style(DefaultViewCommon.WrapperStyle),
  331. )
  332. return
  333. })
  334. }
  335. func changePasswordDialog(vh *login.ViewHelper, ctx *web.EventContext, showVar string, content HTMLComponent) HTMLComponent {
  336. pmsgr := presets.MustGetMessages(ctx.R)
  337. return v.VDialog(
  338. v.VCard(
  339. content,
  340. v.VCardActions(
  341. v.VSpacer(),
  342. v.VBtn(pmsgr.Cancel).
  343. Depressed(true).
  344. Class("ml-2").
  345. On("click", fmt.Sprintf("vars.%s = false", showVar)),
  346. v.VBtn(pmsgr.OK).
  347. Color("primary").
  348. Depressed(true).
  349. Dark(true).
  350. Attr("@click", web.Plaid().EventFunc("login_changePassword").Go()),
  351. ),
  352. ),
  353. ).MaxWidth("600px").
  354. Attr("v-model", fmt.Sprintf("vars.%s", showVar)).
  355. Attr(web.InitContextVars, fmt.Sprintf(`{%s: false}`, showVar))
  356. }
  357. func defaultChangePasswordDialogContent(vh *login.ViewHelper, pb *presets.Builder) func(ctx *web.EventContext) HTMLComponent {
  358. return func(ctx *web.EventContext) HTMLComponent {
  359. msgr := i18n.MustGetModuleMessages(ctx.R, login.I18nLoginKey, login.Messages_en_US).(*login.Messages)
  360. return Div(
  361. v.VCardTitle(Text(msgr.ChangePasswordTitle)),
  362. v.VCardText(
  363. Div(
  364. DefaultViewCommon.PasswordInput("old_password", msgr.ChangePasswordOldPlaceholder, "", true).
  365. Outlined(false).
  366. Label(msgr.ChangePasswordOldLabel).
  367. FieldName("old_password"),
  368. ),
  369. Div(
  370. DefaultViewCommon.PasswordInputWithStrengthMeter(
  371. DefaultViewCommon.PasswordInput("password", msgr.ChangePasswordNewPlaceholder, "", true).
  372. Outlined(false).
  373. Label(msgr.ChangePasswordNewLabel).
  374. FieldName("password"),
  375. "password", ""),
  376. ).Class("mt-12"),
  377. Div(
  378. DefaultViewCommon.PasswordInput("confirm_password", msgr.ChangePasswordNewConfirmPlaceholder, "", true).
  379. Outlined(false).
  380. Label(msgr.ChangePasswordNewConfirmLabel).
  381. FieldName("confirm_password"),
  382. ).Class("mt-12"),
  383. If(vh.TOTPEnabled(),
  384. Div(
  385. DefaultViewCommon.Input("otp", msgr.TOTPValidateCodePlaceholder, "").
  386. Outlined(false).
  387. Label(msgr.TOTPValidateCodeLabel).
  388. FieldName("otp"),
  389. ).Class("mt-12"),
  390. ),
  391. ),
  392. )
  393. }
  394. }
  395. func defaultTOTPSetupPage(vh *login.ViewHelper, pb *presets.Builder) web.PageFunc {
  396. return pb.PlainLayout(func(ctx *web.EventContext) (r web.PageResponse, err error) {
  397. msgr := i18n.MustGetModuleMessages(ctx.R, login.I18nLoginKey, login.Messages_en_US).(*login.Messages)
  398. user := login.GetCurrentUser(ctx.R)
  399. u := user.(login.UserPasser)
  400. var QRCode bytes.Buffer
  401. // Generate key from TOTPSecret
  402. var key *otp.Key
  403. totpSecret := u.GetTOTPSecret()
  404. if len(totpSecret) == 0 {
  405. r.Body = DefaultViewCommon.ErrorBody("need setup totp")
  406. return
  407. }
  408. key, err = otp.NewKeyFromURL(
  409. fmt.Sprintf("otpauth://totp/%s:%s?issuer=%s&secret=%s",
  410. url.PathEscape(vh.TOTPIssuer()),
  411. url.PathEscape(u.GetAccountName()),
  412. url.QueryEscape(vh.TOTPIssuer()),
  413. url.QueryEscape(totpSecret),
  414. ),
  415. )
  416. img, err := key.Image(200, 200)
  417. if err != nil {
  418. r.Body = DefaultViewCommon.ErrorBody(err.Error())
  419. return
  420. }
  421. err = png.Encode(&QRCode, img)
  422. if err != nil {
  423. r.Body = DefaultViewCommon.ErrorBody(err.Error())
  424. return
  425. }
  426. r.PageTitle = msgr.TOTPSetupPageTitle
  427. r.Body = Div(
  428. DefaultViewCommon.Notice(vh, msgr, ctx.W, ctx.R),
  429. Div(
  430. Div(
  431. H1(msgr.TOTPSetupTitle).
  432. Class(DefaultViewCommon.TitleClass),
  433. Label(msgr.TOTPSetupScanPrompt),
  434. ),
  435. Div(
  436. Img(fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString(QRCode.Bytes()))),
  437. ).Class("d-flex justify-center my-2"),
  438. Div(
  439. Label(msgr.TOTPSetupSecretPrompt),
  440. ),
  441. Div(Label(u.GetTOTPSecret())).Class("font-weight-bold my-4"),
  442. Form(
  443. Label(msgr.TOTPSetupEnterCodePrompt),
  444. DefaultViewCommon.Input("otp", msgr.TOTPSetupCodePlaceholder, "").Class("mt-6"),
  445. DefaultViewCommon.FormSubmitBtn(msgr.Verify),
  446. ).Method(http.MethodPost).Action(vh.ValidateTOTPURL()),
  447. ).Class(DefaultViewCommon.WrapperClass).Style(DefaultViewCommon.WrapperStyle).Class("text-center"),
  448. )
  449. return
  450. })
  451. }
  452. func defaultTOTPValidatePage(vh *login.ViewHelper, pb *presets.Builder) web.PageFunc {
  453. return pb.PlainLayout(func(ctx *web.EventContext) (r web.PageResponse, err error) {
  454. msgr := i18n.MustGetModuleMessages(ctx.R, login.I18nLoginKey, login.Messages_en_US).(*login.Messages)
  455. r.PageTitle = msgr.TOTPValidatePageTitle
  456. r.Body = Div(
  457. DefaultViewCommon.Notice(vh, msgr, ctx.W, ctx.R),
  458. Div(
  459. Div(
  460. H1(msgr.TOTPValidateTitle).
  461. Class(DefaultViewCommon.TitleClass),
  462. Label(msgr.TOTPValidateEnterCodePrompt),
  463. ),
  464. Form(
  465. DefaultViewCommon.Input("otp", msgr.TOTPValidateCodePlaceholder, "").Autofocus(true).Class("mt-6"),
  466. DefaultViewCommon.FormSubmitBtn(msgr.Verify),
  467. ).Method(http.MethodPost).Action(vh.ValidateTOTPURL()),
  468. ).Class(DefaultViewCommon.WrapperClass).Style(DefaultViewCommon.WrapperStyle).Class("text-center"),
  469. )
  470. return
  471. })
  472. }