簡體   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