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

问题描述 投票:0回答:3

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

提前致谢。

firebase firebase-authentication
3个回答
121
投票

您可以在

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/v8/firebase.auth.Auth#verifypasswordresetcode


7
投票

2022 年 12 月答案

将下面代码中的

const firebaseConfig = {};
替换为您的 firebaseConfig,您就拥有了一个可用的自定义电子邮件身份验证处理程序页面。

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

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

注意: 当您设置自定义操作处理程序 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/[email protected]/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>

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

注意: 我不处理来自 firebase 的 url 中包含的

lang
apiKey
参数。我的用例不需要进行任何语言更改。

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


2
投票

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

invalid-code

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

© www.soinside.com 2019 - 2024. All rights reserved.