我可以在 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/v8/firebase.auth.Auth#verifypasswordresetcode
将下面代码中的
const firebaseConfig = {};
替换为您的 firebaseConfig,您就拥有了一个可用的自定义电子邮件身份验证处理程序页面。
有很多理由想要使用自定义电子邮件处理程序页面进行 Firebase 身份验证操作。
https://auth.mydomain.com
,下面的代码位于根目录下的 index.html 中。因此,Firebase 电子邮件处理程序参数被附加,电子邮件中的链接看起来像 https://auth.mydomain.com/?mode=resetPassword&oobCode=longStringCode&apiKey=apiCodeString&lang=en
注意: 当您设置自定义操作处理程序 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 身份验证一切顺利,用户将看到这一点。对于每种操作模式,都会相应地显示成功和错误消息。
@Channing Huang 提供的答案是正确的答案,但您还需要记住,它返回的错误并不总是
invalid-code
。
检查错误是否可能已过期,即用户稍后才打开 URL 或可能是其他情况。如果过期,您可以再发送一封电子邮件。