繁体   English   中英

Firebase - 自定义重置密码登陆页面

[英]Firebase - Customize reset password landing page

我可以在 Firebase 中自定义密码重置的登录页面吗? 我想本地化该页面,因为我的应用程序不是英文的。 有什么办法吗?

提前致谢。

您可以在Firebase Console -> Auth -> Email Templates -> Password Reset下自定义密码重置电子邮件,并将电子邮件中的链接更改为指向您自己的页面。 请注意, <code>占位符将被 URL 中的密码重置代码替换。

然后,在您的自定义页面中,您可以从 URL 读取密码重置代码并执行

firebase.auth().confirmPasswordReset(code, newPassword)
    .then(function() {
      // Success
    })
    .catch(function() {
      // Invalid code
    })

或者,您可以在显示密码重置表单之前先检查代码是否有效

firebase.auth().verifyPasswordResetCode(code)
    .then(function(email) {
      // Display a "new password" form with the user's email address
    })
    .catch(function() {
      // Invalid code
    })

https://firebase.google.com/docs/reference/js/firebase.auth.Auth#confirmPasswordReset https://firebase.google.com/docs/reference/js/firebase.auth.Auth#verifyPasswordResetCode

@Channing Huang 提供的答案是正确答案,但您还需要记住,它将返回的错误并不总是invalid-code

检查错误是否可能已过期,其中用户直到稍后才打开 URL 或可能在其他情况下。 如果过期,您可以再发送一封电子邮件。

2022 年 12 月答复

替换const firebaseConfig = {}; 在下面的代码中使用您的firebaseConfig并且您有一个有效的自定义电子邮件身份验证处理程序页面。

有很多理由想要使用自定义电子邮件处理程序页面来执行 firebase 身份验证操作。

  1. 使用您自己的自定义域,而不是 firebase 无品牌域,您的自定义电子邮件处理程序页面可以有您自己的样式和徽标等。我去了子域https://auth.mydomain.com ,下面的代码在索引中.html 在根目录。 因此附加了 firebase 电子邮件处理程序参数,电子邮件中的链接看起来像https://auth.mydomain.com/?mode=resetPassword&oobCode=longStringCode&apiKey=apiCodeString&lang=en
  2. 使用下面的样板代码并将其托管在 firebase 托管(此处未介绍)上非常容易。
  3. firebase 默认重置密码页面处理程序仅设置 >= 6 个字符的密码要求。 您还不能配置密码复杂性。 仅出于这个原因,创建自定义电子邮件操作处理程序页面就足够了。

注意:当您设置自定义操作处理程序 url 时,您的 url 指向的页面必须处理所有电子邮件操作模式。 例如,您不能只为密码重置设置自定义 url,并在模板控制台中使用默认的电子邮件验证 url。 您必须处理自定义电子邮件处理程序页面 url 上的所有模式 下面的代码处理所有模式。

在代码中,您会找到validatePasswordComplexity函数。 它当前设置为显示最低密码复杂性要求,如您在下面的屏幕截图中所见。 当用户输入所需的最小值时,红色警报将被移除。 例如,当用户输入特殊字符时,缺少特殊字符的红色警告将消失,依此类推,直到密码满足您的复杂性要求并且警告消失。 在满足复杂性要求之前,用户无法重置密码。 例如,如果您希望要求用户输入 2 个特殊字符,则更改hasMinSpecialChar正则表达式,将{1,}更改为{2,}

在此处输入图像描述

自定义身份验证电子邮件模式处理程序

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Firebase Auth Handlers</title>
  <meta name="robots" content="noindex">
  <link rel="icon" type="image/png" href="favicon.png"/>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css" />
  <style>
    body {
      font-family: Roboto,sans-serif;
    }
    i {
      margin-left: -30px;
      cursor: pointer;
    }
    .button {
      background-color: #141645;
      border: none;
      color: white;
      padding: 11px 20px;
      text-align: center;
      text-decoration: none;
      display: inline-block;
      font-size: 16px;
      margin: 4px 2px;
      cursor: pointer;
      border-radius: 4px;
    }
    input {
      width: 200px;
      padding: 12px 20px;
      margin: 8px 0;
      display: inline-block;
      border: 1px solid #ccc;
      border-radius: 4px;
      box-sizing: border-box;
    }
    .red-alert {
      color: #B71C1C;
    }
    .center {
      position: absolute;
      left: 50%;
      top: 50%;
      -webkit-transform: translate(-50%, -50%);
      transform: translate(-50%, -50%);
      text-align: center;
    }
    #cover-spin {
      position:fixed;
      width:100%;
      left:0;right:0;top:0;bottom:0;
      background-color: rgba(255,255,255,0.7);
      z-index:9999;
    }

    @-webkit-keyframes spin {
      from {-webkit-transform:rotate(0deg);}
      to {-webkit-transform:rotate(360deg);}
    }

    @keyframes spin {
      from {transform:rotate(0deg);}
      to {transform:rotate(360deg);}
    }

    #cover-spin::after {
      content:'';
      display:block;
      position:absolute;
      left:48%;top:40%;
      width:40px;height:40px;
      border-style:solid;
      border-color:black;
      border-top-color:transparent;
      border-width: 4px;
      border-radius:50%;
      -webkit-animation: spin .8s linear infinite;
      animation: spin .8s linear infinite;
    }
  </style>
  <script>
    const AuthHandler = {
      init: props => {
        AuthHandler.conf = props
        AuthHandler.bindMode()
      },
      bindMode: () => {
        switch (AuthHandler.conf.mode) {
          case 'resetPassword':
            AuthHandler.setModeTitle('Password Reset')

            if (!AuthHandler.validateRequiredAuthParams()) {
              AuthHandler.displayErrorMessage(AuthHandler.conf.defaultErrorMessage)
              return
            }

            AuthHandler.handleResetPassword()
            break;
          case 'recoverEmail':
            AuthHandler.setModeTitle('Email Recovery')

            if (!AuthHandler.validateRequiredAuthParams()) {
              AuthHandler.displayErrorMessage(AuthHandler.conf.defaultErrorMessage)
              return
            }

            AuthHandler.handleRecoverEmail()
            break;
          case 'verifyEmail':
            AuthHandler.setModeTitle('Email Verification')

            if (!AuthHandler.validateRequiredAuthParams()) {
              AuthHandler.displayErrorMessage(AuthHandler.conf.defaultErrorMessage)
              return
            }

            AuthHandler.handleVerifyEmail()
            break;
          default:
            AuthHandler.displayErrorMessage(AuthHandler.conf.defaultErrorMessage)
            break;
        }
      },
      handleResetPassword: () => {
        AuthHandler.showLoadingSpinner()

        // Verify the code is valid before displaying the reset password form.
        AuthHandler.conf.verifyPasswordResetCode(AuthHandler.conf.auth, AuthHandler.conf.oobCode).then(() => {
          AuthHandler.hideLoadingSpinner()

          // Display the form if we have a valid reset code.
          AuthHandler.showPasswordResetForm()
          AuthHandler.conf.passwordField.addEventListener('input', AuthHandler.validatePasswordComplexity);

          AuthHandler.conf.passwordToggleButton.addEventListener('click', e => {
            AuthHandler.conf.passwordField.setAttribute(
                    'type',
                    AuthHandler.conf.passwordField.getAttribute('type') === 'password'
                            ? 'text' : 'password');
            e.target.classList.toggle('bi-eye');
          });

          AuthHandler.conf.passwordResetButton.addEventListener('click', () => {
            AuthHandler.hideMessages()

            // Test the password again. If it does not pass, errors will display.
            if (AuthHandler.validatePasswordComplexity(AuthHandler.conf.passwordField)) {
              AuthHandler.showLoadingSpinner()

              // Attempt to reset the password.
              AuthHandler.conf.confirmPasswordReset(
                      AuthHandler.conf.auth,
                      AuthHandler.conf.oobCode,
                      AuthHandler.conf.passwordField.value.trim()
              ).then(() => {
                AuthHandler.hidePasswordResetForm()
                AuthHandler.hideLoadingSpinner()
                AuthHandler.displaySuccessMessage('Password has been reset!')
              }).catch(() => {
                AuthHandler.hideLoadingSpinner()
                AuthHandler.displayErrorMessage('Password reset failed. Please try again.')
              })
            }
          });
        }).catch(() => {
          AuthHandler.hideLoadingSpinner()
          AuthHandler.hidePasswordResetForm()
          AuthHandler.displayErrorMessage('Invalid password reset code. Please try again.')
        });
      },
      handleRecoverEmail: () => {
        AuthHandler.showLoadingSpinner()

        let restoredEmail = null;

        AuthHandler.conf.checkActionCode(AuthHandler.conf.auth, AuthHandler.conf.oobCode).then(info => {
          restoredEmail = info['data']['email'];
          AuthHandler.conf.applyActionCode(AuthHandler.conf.auth, AuthHandler.conf.oobCode).then(() => {
            AuthHandler.conf.sendPasswordResetEmail(AuthHandler.conf.auth, restoredEmail).then(() => {
              AuthHandler.hideLoadingSpinner()
              AuthHandler.displaySuccessMessage(`Your email has been restored and a reset password email has been sent to ${restoredEmail}. For security, please reset your password immediately.`)
            }).catch(() => {
              AuthHandler.hideLoadingSpinner()
              AuthHandler.displaySuccessMessage(`Your email ${restoredEmail} has been restored. For security, please reset your password immediately.`)
            })
          }).catch(() => {
            AuthHandler.hideLoadingSpinner()
            AuthHandler.displayErrorMessage('Sorry, something went wrong recovering your email. Please try again or contact support.')
          })
        }).catch(() => {
          AuthHandler.hideLoadingSpinner()
          AuthHandler.displayErrorMessage('Invalid action code. Please try again.')
        })
      },
      handleVerifyEmail: () => {
        AuthHandler.showLoadingSpinner()
        AuthHandler.conf.applyActionCode(AuthHandler.conf.auth, AuthHandler.conf.oobCode).then(() => {
          AuthHandler.hideLoadingSpinner()
          AuthHandler.displaySuccessMessage('Email verified! Your account is now active. Time to send some messages!')
        }).catch(() => {
          AuthHandler.hideLoadingSpinner()
          AuthHandler.displayErrorMessage('Your code is invalid or has expired. Please try to verify your email address again by tapping the resend email button in app.')
        })
      },
      validateRequiredAuthParams: () => {
        // Mode is evaluated in the bindMode switch. If no mode will display default error message. So, we're just
        // checking for a valid oobCode here.
        return !!AuthHandler.conf.oobCode
      },
      setModeTitle: title => {
        AuthHandler.conf.modeTitle.innerText = title
      },
      validatePasswordComplexity: e => {
        const password = !!e.target ? e.target.value.trim() : e.value.trim()
        const isValidString = typeof password === 'string'

        /// Checks if password has minLength
        const hasMinLength = isValidString && password.length >= 8
        AuthHandler.conf.passwordHasMinLength.style.display = hasMinLength ? 'none' : ''

        /// Checks if password has at least 1 normal char letter matches
        const hasMinNormalChar = isValidString && password.toUpperCase().match(RegExp('^(.*?[A-Z]){1,}')) !== null
        AuthHandler.conf.passwordHasMinNormalChar.style.display = hasMinNormalChar ? 'none' : ''

        /// Checks if password has at least 1 uppercase letter matches
        const hasMinUppercase =
                isValidString && password.match(RegExp('^(.*?[A-Z]){1,}')) !== null
        AuthHandler.conf.passwordHasMinUppercase.style.display = hasMinUppercase ? 'none' : ''

        /// Checks if password has at least 1 numeric character matches
        const hasMinNumericChar =
                isValidString && password.match(RegExp('^(.*?[0-9]){1,}')) !== null
        AuthHandler.conf.passwordHasMinNumericChar.style.display = hasMinNumericChar ? 'none' : ''

        /// Checks if password has at least 1 special character matches
        const hasMinSpecialChar = isValidString && password.match(RegExp("^(.*?[\$&+,:;/=?@#|'<>.^*()_%!-]){1,}")) !== null
        AuthHandler.conf.passwordHasMinSpecialChar.style.display = hasMinSpecialChar ? 'none' : ''

        const passing = hasMinLength &&
                hasMinNormalChar &&
                hasMinUppercase &&
                hasMinNumericChar &&
                hasMinSpecialChar
        AuthHandler.conf.passwordIncreaseComplexity.style.display = passing ? 'none' : ''

        return passing
      },
      showLoadingSpinner: () => {
        AuthHandler.conf.loading.style.display = ''
      },
      hideLoadingSpinner: () => {
        AuthHandler.conf.loading.style.display = 'none'
      },
      showPasswordResetForm: () => {
        AuthHandler.conf.passwordForm.style.display = '';
      },
      hidePasswordResetForm: () => {
        AuthHandler.conf.passwordForm.style.display = 'none';
      },
      displaySuccessMessage: message => {
        AuthHandler.hideErrorMessage()
        AuthHandler.conf.success.innerText = message
        AuthHandler.conf.success.style.display = ''
      },
      hideSuccessMessage: () => {
        AuthHandler.conf.success.innerText = ''
        AuthHandler.conf.success.style.display = 'none'
      },
      displayErrorMessage: message => {
        AuthHandler.hideSuccessMessage()
        AuthHandler.conf.error.innerText = message
        AuthHandler.conf.error.style.display = ''
      },
      hideErrorMessage: () => {
        AuthHandler.conf.error.innerText = ''
        AuthHandler.conf.error.style.display = 'none'
      },
      hideMessages: () => {
        AuthHandler.hideErrorMessage()
        AuthHandler.hideSuccessMessage()
      },
    }
  </script>
</head>
<body>
<div class="center">
  <div id="cover-spin" style="display: none;"></div>
  <p>
    <image src="https://via.placeholder.com/400x70/000000/FFFFFF?text=Your+Logo"/>
  </p>
  <p id="mode-title" style="font-size: 20px; font-weight: bold;"></p>
  <p id="error" class="red-alert" style="display: none;"></p>
  <p id="success" style="display: none;"></p>
  <div id="password-form" style="min-width: 700px; min-height: 300px; display: none;">
    <label for="password">New Password</label>
    <input id="password" type="password" minlength="8" maxlength="35" autocomplete="off" placeholder="Enter new password" style="margin-left: 10px;" required>
    <i class="bi bi-eye-slash" id="toggle-password"></i>
    <button id="reset-button" type="button" class="button" style="margin-left: 20px;">Reset</button>
    <p class="red-alert" id="increase-complexity" style="display: none;"><strong>Increase Complexity</strong></p>
    <p class="red-alert" id="has-min-length" style="display: none;">Minimum 8 characters</p>
    <p class="red-alert" id="has-min-normal-char" style="display: none;">Minimum 1 normal characters</p>
    <p class="red-alert" id="has-min-uppercase" style="display: none;">Minimum 1 uppercase characters</p>
    <p class="red-alert" id="has-min-numeric-char" style="display: none;">Minimum 1 numeric characters</p>
    <p class="red-alert" id="has-min-special-char" style="display: none;">Minimum 1 special characters</p>
  </div>
</div>

<script type="module">
  // https://firebase.google.com/docs/web/setup#available-libraries
  // https://firebase.google.com/docs/web/alt-setup
  import { initializeApp } from 'https://www.gstatic.com/firebasejs/9.15.0/firebase-app.js';
  import {
    applyActionCode,
    checkActionCode,
    confirmPasswordReset,
    getAuth,
    sendPasswordResetEmail,
    verifyPasswordResetCode,
  } from 'https://www.gstatic.com/firebasejs/9.15.0/firebase-auth.js';

  // Replace {} with your firebaseConfig
  // https://firebase.google.com/docs/web/learn-more#config-object
  const firebaseConfig = {};

  const app = initializeApp(firebaseConfig);
  const auth = getAuth(app);

  document.addEventListener('DOMContentLoaded', () => {
    // Get the mode and oobCode from url params
    const params = new Proxy(new URLSearchParams(window.location.search), {
      get: (searchParams, prop) => searchParams.get(prop),
    });

    AuthHandler.init({
      app,
      auth,
      applyActionCode,
      checkActionCode,
      confirmPasswordReset,
      getAuth,
      sendPasswordResetEmail,
      verifyPasswordResetCode,
      // Used by all modes to display error or success messages
      error: document.getElementById('error'),
      success: document.getElementById('success'),
      // https://firebase.google.com/docs/auth/custom-email-handler#create_the_email_action_handler_page
      mode: params.mode,
      oobCode: params.oobCode,
      modeTitle: document.getElementById('mode-title'),
      loading: document.getElementById('cover-spin'),
      // Password reset elements
      passwordForm: document.getElementById('password-form'),
      passwordField: document.getElementById('password'),
      passwordResetButton: document.getElementById('reset-button'),
      passwordToggleButton: document.getElementById('toggle-password'),
      passwordHasMinLength: document.getElementById('has-min-length'),
      passwordHasMinNormalChar: document.getElementById('has-min-normal-char'),
      passwordHasMinUppercase: document.getElementById('has-min-uppercase'),
      passwordHasMinNumericChar: document.getElementById('has-min-numeric-char'),
      passwordHasMinSpecialChar: document.getElementById('has-min-special-char'),
      passwordIncreaseComplexity: document.getElementById('increase-complexity'),
      defaultErrorMessage: 'Invalid auth parameters. Please try again.',
    });
  });
</script>
</body>
</html>

注意:我选择不使用新的模块化 tree shakable 做事方式,因为我不想设置 webpack,并选择了alt 设置样式,这样我就可以使用最新的 firebasejs 版本,在撰写本文时,是 v9.15.0。 如果您担心单页自定义电子邮件处理程序页面膨胀,请查看 tree shaking 和 webpack。 我选择了配置更少的超快速选项。

注意:我没有处理来自 firebase 的 url 中包含的langapiKey参数。 我的用例不需要进行任何语言更改。

点击重置后,如果 firebase 身份验证一切顺利,用户将看到这个。 对于每种操作模式,成功和错误消息将相应显示。

在此处输入图像描述

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM