views.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  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/web"
  11. "github.com/qor5/x/i18n"
  12. . "github.com/theplant/htmlgo"
  13. "golang.org/x/text/language"
  14. "golang.org/x/text/language/display"
  15. "gorm.io/gorm"
  16. )
  17. func defaultLoginPage(vh *ViewHelper) web.PageFunc {
  18. return func(ctx *web.EventContext) (r web.PageResponse, err error) {
  19. // i18n start
  20. msgr := i18n.MustGetModuleMessages(ctx.R, I18nLoginKey, Messages_en_US).(*Messages)
  21. i18nBuilder := vh.I18n()
  22. var languagesHTML []HTMLComponent
  23. languages := i18nBuilder.GetSupportLanguages()
  24. if len(languages) > 1 {
  25. qn := i18nBuilder.GetQueryName()
  26. lang := ctx.R.FormValue(qn)
  27. if lang == "" {
  28. lang = i18nBuilder.GetCurrentLangFromCookie(ctx.R)
  29. }
  30. accept := ctx.R.Header.Get("Accept-Language")
  31. _, mi := language.MatchStrings(language.NewMatcher(languages), lang, accept)
  32. for i, l := range languages {
  33. u, _ := url.Parse(ctx.R.RequestURI)
  34. qs := u.Query()
  35. qs.Set(qn, l.String())
  36. u.RawQuery = qs.Encode()
  37. elem := Option(display.Self.Name(l)).
  38. Value(u.String())
  39. if i == mi {
  40. elem.Attr("selected", "selected")
  41. }
  42. languagesHTML = append(languagesHTML, elem)
  43. }
  44. }
  45. // i18n end
  46. fMsg := vh.GetFailFlashMessage(msgr, ctx.W, ctx.R)
  47. wMsg := vh.GetWarnFlashMessage(msgr, ctx.W, ctx.R)
  48. iMsg := vh.GetInfoFlashMessage(msgr, ctx.W, ctx.R)
  49. wIn := vh.GetWrongLoginInputFlash(ctx.W, ctx.R)
  50. if iMsg != "" && vh.GetInfoCodeFlash(ctx.W, ctx.R) == InfoCodePasswordSuccessfullyChanged {
  51. wMsg = ""
  52. }
  53. var oauthHTML HTMLComponent
  54. if vh.OAuthEnabled() {
  55. ul := Div().Class("flex flex-col justify-center mt-8 text-center")
  56. for _, provider := range vh.OAuthProviders() {
  57. ul.AppendChildren(
  58. A().
  59. Href(fmt.Sprintf("%s?provider=%s", vh.OAuthBeginURL(), provider.Key)).
  60. Class("px-6 py-3 mt-4 font-semibold text-gray-900 bg-white border-2 border-gray-500 rounded-md shadow outline-none hover:bg-yellow-50 hover:border-yellow-400 focus:outline-none").
  61. Children(
  62. provider.Logo,
  63. Text(provider.Text),
  64. ),
  65. )
  66. }
  67. oauthHTML = Div(
  68. ul,
  69. )
  70. }
  71. isRecaptchaEnabled := vh.RecaptchaEnabled()
  72. var userPassHTML HTMLComponent
  73. if vh.UserPassEnabled() {
  74. userPassHTML = Div(
  75. Form(
  76. Div(
  77. Label(msgr.AccountLabel).Class(DefaultViewCommon.LabelClass).For("account"),
  78. Input("account").Placeholder(msgr.AccountPlaceholder).Class(DefaultViewCommon.InputClass).
  79. Value(wIn.Account),
  80. ),
  81. Div(
  82. Label(msgr.PasswordLabel).Class(DefaultViewCommon.LabelClass).For("password"),
  83. DefaultViewCommon.PasswordInputWithRevealFunction("password", msgr.PasswordPlaceholder, "password", wIn.Password),
  84. ).Class("mt-6"),
  85. If(isRecaptchaEnabled,
  86. Div(
  87. // recaptcha response token
  88. Input("token").Id("token"),
  89. ).Class("hidden"),
  90. ),
  91. Div(
  92. Button(msgr.SignInBtn).Class(DefaultViewCommon.ButtonClass).
  93. ClassIf("g-recaptcha", isRecaptchaEnabled).AttrIf("data-sitekey", vh.RecaptchaSiteKey(), isRecaptchaEnabled).AttrIf("data-callback", "onSubmit", isRecaptchaEnabled),
  94. ).Class("mt-6"),
  95. ).Id("login-form").Method(http.MethodPost).Action(vh.PasswordLoginURL()),
  96. If(!vh.NoForgetPasswordLink(),
  97. Div(
  98. A(Text(msgr.ForgetPasswordLink)).Href(vh.ForgetPasswordPageURL()).
  99. Class("text-gray-500"),
  100. ).Class("text-right mt-2"),
  101. ),
  102. )
  103. }
  104. r.PageTitle = "Sign In"
  105. var bodyForm HTMLComponent
  106. bodyForm = Div(
  107. userPassHTML,
  108. oauthHTML,
  109. If(len(languagesHTML) > 0,
  110. Select(
  111. languagesHTML...,
  112. ).Class("mt-12 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500").
  113. Attr("onChange", "window.location.href=this.value"),
  114. ),
  115. ).Class(DefaultViewCommon.WrapperClass)
  116. r.Body = Div(
  117. Link(StyleCSSURL).Type("text/css").Rel("stylesheet"),
  118. If(isRecaptchaEnabled,
  119. Style(`.grecaptcha-badge { visibility: hidden; }`),
  120. Script("").Src("https://www.google.com/recaptcha/api.js"),
  121. Script(`
  122. function onSubmit(token) {
  123. document.getElementById("token").value = token;
  124. document.getElementById("login-form").submit();
  125. }
  126. `)),
  127. DefaultViewCommon.ErrNotice(fMsg),
  128. DefaultViewCommon.WarnNotice(wMsg),
  129. DefaultViewCommon.InfoNotice(iMsg),
  130. bodyForm,
  131. )
  132. return
  133. }
  134. }
  135. func defaultForgetPasswordPage(vh *ViewHelper) web.PageFunc {
  136. return func(ctx *web.EventContext) (r web.PageResponse, err error) {
  137. msgr := i18n.MustGetModuleMessages(ctx.R, I18nLoginKey, Messages_en_US).(*Messages)
  138. fMsg := vh.GetFailFlashMessage(msgr, ctx.W, ctx.R)
  139. wIn := vh.GetWrongForgetPasswordInputFlash(ctx.W, ctx.R)
  140. secondsToResend := vh.GetSecondsToRedoFlash(ctx.W, ctx.R)
  141. activeBtnText := msgr.SendResetPasswordEmailBtn
  142. activeBtnClass := DefaultViewCommon.ButtonClass
  143. inactiveBtnText := msgr.ResendResetPasswordEmailBtn
  144. inactiveBtnClass := "w-full px-6 py-3 tracking-wide text-white transition-colors duration-200 transform bg-gray-500 rounded-md"
  145. inactiveBtnTextWithInitSeconds := fmt.Sprintf("%s (%d)", inactiveBtnText, secondsToResend)
  146. doTOTP := ctx.R.URL.Query().Get("totp") == "1"
  147. actionURL := vh.SendResetPasswordLinkURL()
  148. if doTOTP {
  149. actionURL = MustSetQuery(actionURL, "totp", "1")
  150. }
  151. isRecaptchaEnabled := vh.RecaptchaEnabled()
  152. r.PageTitle = "Forget Your Password?"
  153. r.Body = Div(
  154. Link(StyleCSSURL).Type("text/css").Rel("stylesheet"),
  155. If(isRecaptchaEnabled,
  156. Style(`.grecaptcha-badge { visibility: hidden; }`),
  157. Script("").Src("https://www.google.com/recaptcha/api.js"),
  158. Script(`
  159. function onSubmit(token) {
  160. document.getElementById("token").value = token;
  161. document.getElementById("forget-form").submit();
  162. }
  163. `)),
  164. DefaultViewCommon.ErrNotice(fMsg),
  165. If(secondsToResend > 0,
  166. DefaultViewCommon.WarnNotice(msgr.SendEmailTooFrequentlyNotice),
  167. ),
  168. Div(
  169. H1(msgr.ForgotMyPasswordTitle).Class(DefaultViewCommon.TitleClass),
  170. Form(
  171. Div(
  172. Label(msgr.ForgetPasswordEmailLabel).Class(DefaultViewCommon.LabelClass).For("account"),
  173. Input("account").Placeholder(msgr.ForgetPasswordEmailPlaceholder).Class(DefaultViewCommon.InputClass).Value(wIn.Account),
  174. ),
  175. If(doTOTP,
  176. Div(
  177. Label(msgr.TOTPValidateCodeLabel).Class(DefaultViewCommon.LabelClass).For("otp"),
  178. Input("otp").Placeholder(msgr.TOTPValidateCodePlaceholder).
  179. Class(DefaultViewCommon.InputClass).
  180. Value(wIn.TOTP),
  181. ).Class("mt-6"),
  182. ),
  183. If(isRecaptchaEnabled,
  184. Div(
  185. // recaptcha response token
  186. Input("token").Id("token"),
  187. ).Class("hidden"),
  188. ),
  189. Div(
  190. If(secondsToResend > 0,
  191. Button(inactiveBtnTextWithInitSeconds).Id("submitBtn").Class(inactiveBtnClass).Disabled(true),
  192. ).Else(
  193. Button(activeBtnText).Class(activeBtnClass).
  194. ClassIf("g-recaptcha", isRecaptchaEnabled).AttrIf("data-sitekey", vh.RecaptchaSiteKey(), isRecaptchaEnabled).AttrIf("data-callback", "onSubmit", isRecaptchaEnabled),
  195. ),
  196. ).Class("mt-6"),
  197. ).Id("forget-form").Method(http.MethodPost).Action(actionURL),
  198. ).Class(DefaultViewCommon.WrapperClass),
  199. )
  200. if secondsToResend > 0 {
  201. ctx.Injector.TailHTML(fmt.Sprintf(`
  202. <script>
  203. (function(){
  204. var secondsToResend = %d;
  205. var btnText = "%s";
  206. var submitBtn = document.getElementById("submitBtn");
  207. var interv = setInterval(function(){
  208. secondsToResend--;
  209. if (secondsToResend === 0) {
  210. clearInterval(interv);
  211. submitBtn.innerText = btnText;
  212. submitBtn.className = "%s";
  213. submitBtn.disabled = false;
  214. return;
  215. }
  216. submitBtn.innerText = btnText + " (" + secondsToResend + ")" ;
  217. }, 1000);
  218. })();
  219. </script>
  220. `, secondsToResend, inactiveBtnText, activeBtnClass))
  221. }
  222. return
  223. }
  224. }
  225. func defaultResetPasswordLinkSentPage(vh *ViewHelper) web.PageFunc {
  226. return func(ctx *web.EventContext) (r web.PageResponse, err error) {
  227. msgr := i18n.MustGetModuleMessages(ctx.R, I18nLoginKey, Messages_en_US).(*Messages)
  228. a := ctx.R.URL.Query().Get("a")
  229. r.PageTitle = "Forget Your Password?"
  230. r.Body = Div(
  231. Link(StyleCSSURL).Type("text/css").Rel("stylesheet"),
  232. Div(
  233. H1(fmt.Sprintf("%s %s.", msgr.ResetPasswordLinkWasSentTo, a)).Class("leading-tight text-2xl mt-0 mb-4"),
  234. H2(msgr.ResetPasswordLinkSentPrompt).Class("leading-tight text-1xl mt-0"),
  235. ).Class(DefaultViewCommon.WrapperClass),
  236. )
  237. return
  238. }
  239. }
  240. func defaultResetPasswordPage(vh *ViewHelper) web.PageFunc {
  241. return func(ctx *web.EventContext) (r web.PageResponse, err error) {
  242. msgr := i18n.MustGetModuleMessages(ctx.R, I18nLoginKey, Messages_en_US).(*Messages)
  243. fMsg := vh.GetFailFlashMessage(msgr, ctx.W, ctx.R)
  244. if fMsg == "" {
  245. fMsg = vh.GetCustomErrorMessageFlash(ctx.W, ctx.R)
  246. }
  247. wIn := vh.GetWrongResetPasswordInputFlash(ctx.W, ctx.R)
  248. doTOTP := ctx.R.URL.Query().Get("totp") == "1"
  249. actionURL := vh.ResetPasswordURL()
  250. if doTOTP {
  251. actionURL = MustSetQuery(actionURL, "totp", "1")
  252. }
  253. var user interface{}
  254. r.PageTitle = "Reset Password"
  255. query := ctx.R.URL.Query()
  256. id := query.Get("id")
  257. if id == "" {
  258. r.Body = Div(Text("user not found"))
  259. return r, nil
  260. } else {
  261. user, err = vh.FindUserByID(id)
  262. if err != nil {
  263. if err == gorm.ErrRecordNotFound {
  264. r.Body = Div(Text("user not found"))
  265. return r, nil
  266. }
  267. r.Body = Div(Text("system error"))
  268. return r, nil
  269. }
  270. }
  271. token := query.Get("token")
  272. if token == "" {
  273. r.Body = Div(Text("invalid token"))
  274. return r, nil
  275. } else {
  276. storedToken, _, expired := user.(UserPasser).GetResetPasswordToken()
  277. if expired {
  278. r.Body = Div(Text("token expired"))
  279. return r, nil
  280. }
  281. if token != storedToken {
  282. r.Body = Div(Text("invalid token"))
  283. return r, nil
  284. }
  285. }
  286. r.Body = Div(
  287. Link(StyleCSSURL).Type("text/css").Rel("stylesheet"),
  288. Script("").Src(ZxcvbnJSURL),
  289. DefaultViewCommon.ErrNotice(fMsg),
  290. Div(
  291. H1(msgr.ResetYourPasswordTitle).Class(DefaultViewCommon.TitleClass),
  292. Form(
  293. Input("user_id").Type("hidden").Value(id),
  294. Input("token").Type("hidden").Value(token),
  295. Div(
  296. Label(msgr.ResetPasswordLabel).Class(DefaultViewCommon.LabelClass).For("password"),
  297. DefaultViewCommon.PasswordInputWithRevealFunction("password", msgr.ResetPasswordPlaceholder, "password", wIn.Password),
  298. DefaultViewCommon.PasswordStrengthMeter("password"),
  299. ),
  300. Div(
  301. Label(msgr.ResetPasswordConfirmLabel).Class(DefaultViewCommon.LabelClass).For("confirm_password"),
  302. DefaultViewCommon.PasswordInputWithRevealFunction("confirm_password", msgr.ResetPasswordConfirmPlaceholder, "confirm_password", wIn.ConfirmPassword),
  303. ).Class("mt-6"),
  304. If(doTOTP,
  305. Div(
  306. Label(msgr.TOTPValidateCodeLabel).Class(DefaultViewCommon.LabelClass).For("otp"),
  307. Input("otp").Placeholder(msgr.TOTPValidateCodePlaceholder).
  308. Class(DefaultViewCommon.InputClass).
  309. Value(wIn.TOTP),
  310. ).Class("mt-6"),
  311. ),
  312. Div(
  313. Button(msgr.Confirm).Class(DefaultViewCommon.ButtonClass),
  314. ).Class("mt-6"),
  315. ).Method(http.MethodPost).Action(actionURL),
  316. ).Class(DefaultViewCommon.WrapperClass),
  317. )
  318. return
  319. }
  320. }
  321. func defaultChangePasswordPage(vh *ViewHelper) web.PageFunc {
  322. return func(ctx *web.EventContext) (r web.PageResponse, err error) {
  323. msgr := i18n.MustGetModuleMessages(ctx.R, I18nLoginKey, Messages_en_US).(*Messages)
  324. fMsg := vh.GetFailFlashMessage(msgr, ctx.W, ctx.R)
  325. if fMsg == "" {
  326. fMsg = vh.GetCustomErrorMessageFlash(ctx.W, ctx.R)
  327. }
  328. wIn := vh.GetWrongChangePasswordInputFlash(ctx.W, ctx.R)
  329. r.PageTitle = "Change Password"
  330. r.Body = Div(
  331. Link(StyleCSSURL).Type("text/css").Rel("stylesheet"),
  332. Script("").Src(ZxcvbnJSURL),
  333. DefaultViewCommon.ErrNotice(fMsg),
  334. Div(
  335. H1(msgr.ChangePasswordTitle).Class(DefaultViewCommon.TitleClass),
  336. Form(
  337. Div(
  338. Label(msgr.ChangePasswordOldLabel).Class(DefaultViewCommon.LabelClass).For("old_password"),
  339. DefaultViewCommon.PasswordInputWithRevealFunction("old_password", msgr.ChangePasswordOldPlaceholder, "old_password", wIn.OldPassword),
  340. ),
  341. Div(
  342. Label(msgr.ChangePasswordNewLabel).Class(DefaultViewCommon.LabelClass).For("password"),
  343. DefaultViewCommon.PasswordInputWithRevealFunction("password", msgr.ChangePasswordNewPlaceholder, "password", wIn.NewPassword),
  344. DefaultViewCommon.PasswordStrengthMeter("password"),
  345. ).Class("mt-6"),
  346. Div(
  347. Label(msgr.ChangePasswordNewConfirmLabel).Class(DefaultViewCommon.LabelClass).For("confirm_password"),
  348. DefaultViewCommon.PasswordInputWithRevealFunction("confirm_password", msgr.ChangePasswordNewConfirmPlaceholder, "confirm_password", wIn.ConfirmPassword),
  349. ).Class("mt-6"),
  350. If(vh.TOTPEnabled(),
  351. Div(
  352. Label(msgr.TOTPValidateCodeLabel).Class(DefaultViewCommon.LabelClass).For("otp"),
  353. Input("otp").Placeholder(msgr.TOTPValidateCodePlaceholder).
  354. Class(DefaultViewCommon.InputClass).
  355. Value(wIn.TOTP),
  356. ).Class("mt-6"),
  357. ),
  358. Div(
  359. Button(msgr.Confirm).Class(DefaultViewCommon.ButtonClass),
  360. ).Class("mt-6"),
  361. ).Method(http.MethodPost).Action(vh.ChangePasswordURL()),
  362. ).Class(DefaultViewCommon.WrapperClass),
  363. )
  364. return
  365. }
  366. }
  367. func defaultTOTPSetupPage(vh *ViewHelper) web.PageFunc {
  368. return func(ctx *web.EventContext) (r web.PageResponse, err error) {
  369. msgr := i18n.MustGetModuleMessages(ctx.R, I18nLoginKey, Messages_en_US).(*Messages)
  370. fMsg := vh.GetFailFlashMessage(msgr, ctx.W, ctx.R)
  371. user := GetCurrentUser(ctx.R)
  372. u := user.(UserPasser)
  373. var QRCode bytes.Buffer
  374. // Generate key from TOTPSecret
  375. var key *otp.Key
  376. totpSecret := u.GetTOTPSecret()
  377. if len(totpSecret) == 0 {
  378. r.Body = DefaultViewCommon.ErrorBody("need setup totp")
  379. return
  380. }
  381. key, err = otp.NewKeyFromURL(
  382. fmt.Sprintf("otpauth://totp/%s:%s?issuer=%s&secret=%s",
  383. url.PathEscape(vh.TOTPIssuer()),
  384. url.PathEscape(u.GetAccountName()),
  385. url.QueryEscape(vh.TOTPIssuer()),
  386. url.QueryEscape(totpSecret),
  387. ),
  388. )
  389. img, err := key.Image(200, 200)
  390. if err != nil {
  391. r.Body = DefaultViewCommon.ErrorBody(err.Error())
  392. return
  393. }
  394. err = png.Encode(&QRCode, img)
  395. if err != nil {
  396. r.Body = DefaultViewCommon.ErrorBody(err.Error())
  397. return
  398. }
  399. r.PageTitle = "TOTP Setup"
  400. r.Body = Div(
  401. Link(StyleCSSURL).Type("text/css").Rel("stylesheet"),
  402. DefaultViewCommon.ErrNotice(fMsg),
  403. Div(
  404. Div(
  405. H1(msgr.TOTPSetupTitle).
  406. Class(DefaultViewCommon.TitleClass),
  407. Label(msgr.TOTPSetupScanPrompt),
  408. ),
  409. Div(
  410. Img(fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString(QRCode.Bytes()))),
  411. ).Class("my-2 flex items-center justify-center"),
  412. Div(
  413. Label(msgr.TOTPSetupSecretPrompt),
  414. ),
  415. Div(Label(u.GetTOTPSecret()).Class("text-sm font-bold")).Class("my-4"),
  416. Form(
  417. Label(msgr.TOTPSetupEnterCodePrompt),
  418. Input("otp").Placeholder(msgr.TOTPSetupCodePlaceholder).
  419. Class(DefaultViewCommon.InputClass).
  420. Class("mt-6"),
  421. Div(
  422. Button(msgr.Verify).Class(DefaultViewCommon.ButtonClass),
  423. ).Class("mt-6"),
  424. ).Method(http.MethodPost).Action(vh.ValidateTOTPURL()),
  425. ).Class(DefaultViewCommon.WrapperClass).Class("text-center"),
  426. )
  427. return
  428. }
  429. }
  430. func defaultTOTPValidatePage(vh *ViewHelper) web.PageFunc {
  431. return func(ctx *web.EventContext) (r web.PageResponse, err error) {
  432. msgr := i18n.MustGetModuleMessages(ctx.R, I18nLoginKey, Messages_en_US).(*Messages)
  433. fMsg := vh.GetFailFlashMessage(msgr, ctx.W, ctx.R)
  434. r.PageTitle = "TOTP Validate"
  435. r.Body = Div(
  436. Link(StyleCSSURL).Type("text/css").Rel("stylesheet"),
  437. DefaultViewCommon.ErrNotice(fMsg),
  438. Div(
  439. Div(
  440. H1(msgr.TOTPValidateTitle).
  441. Class(DefaultViewCommon.TitleClass),
  442. Label(msgr.TOTPValidateEnterCodePrompt),
  443. ),
  444. Form(
  445. Input("otp").Placeholder(msgr.TOTPValidateCodePlaceholder).
  446. Class(DefaultViewCommon.InputClass).
  447. Class("mt-6").
  448. Attr("autofocus", true),
  449. Div(
  450. Button(msgr.Verify).Class(DefaultViewCommon.ButtonClass),
  451. ).Class("mt-6"),
  452. ).Method(http.MethodPost).Action(vh.ValidateTOTPURL()),
  453. ).Class(DefaultViewCommon.WrapperClass).Class("text-center"),
  454. )
  455. return
  456. }
  457. }