|
|
<%inherit file="base/root.mako"/>
|
|
|
|
|
|
<%def name="title()">
|
|
|
${_('Setup authenticator app')}
|
|
|
%if c.rhodecode_name:
|
|
|
· ${h.branding(c.rhodecode_name)}
|
|
|
%endif
|
|
|
</%def>
|
|
|
<style>body{background-color:#eeeeee;}</style>
|
|
|
|
|
|
<div class="loginbox">
|
|
|
<div class="header-account">
|
|
|
<div id="header-inner" class="title">
|
|
|
<div id="logo">
|
|
|
% if c.rhodecode_name:
|
|
|
<div class="branding">
|
|
|
<a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
|
|
|
</div>
|
|
|
% endif
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="loginwrapper">
|
|
|
<h1>Setup the authenticator app</h1>
|
|
|
<p>Authenticator apps like <a href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2' target="_blank" rel="noopener noreferrer">Google Authenticator</a>, etc. generate one-time passwords that are used as a second factor to verify you identity.</p>
|
|
|
<rhodecode-toast id="notifications"></rhodecode-toast>
|
|
|
|
|
|
<div id="setup_2fa">
|
|
|
<div class="sign-in-title">
|
|
|
<h1>${_('Scan the QR code')}</h1>
|
|
|
</div>
|
|
|
<p>Use an authenticator app to scan.</p>
|
|
|
<img src="data:image/png;base64, ${qr}"/>
|
|
|
<p>${_('Unable to scan?')} <a id="toggleLink">${_('Click here')}</a></p>
|
|
|
<div id="secretDiv" class="hidden">
|
|
|
<p>${_('Copy and use this code to manually setup an authenticator app')}</p>
|
|
|
<input type="text" id="secretField" value=${key}>
|
|
|
<i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="" title="${_('Copy the secret key')}"></i>
|
|
|
</div>
|
|
|
<div id="codesPopup" class="modal">
|
|
|
<div class="modal-content">
|
|
|
<ul id="recoveryCodesList"></ul>
|
|
|
<button id="copyAllBtn" class="btn btn-primary">Copy All</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
<br><br>
|
|
|
<div id="verify_2fa">
|
|
|
${h.secure_form(h.route_path('setup_2fa'), request=request, id='totp_form')}
|
|
|
<div class="form mt-4">
|
|
|
<div class="field">
|
|
|
<p>
|
|
|
<div class="label">
|
|
|
<label for="totp" class="form-label text-dark font-weight-bold" style="text-align: left;">${_('Verify the code from the app')}:</label>
|
|
|
</div>
|
|
|
</p>
|
|
|
<p>
|
|
|
<div>
|
|
|
<div class="input-group">
|
|
|
${h.text('totp', class_='form-control', style='width: 40%;')}
|
|
|
<div id="formErrors">
|
|
|
%if 'totp' in errors:
|
|
|
<span class="error-message">${errors.get('totp')}</span>
|
|
|
<br />
|
|
|
%endif
|
|
|
</div>
|
|
|
<div class="input-group-append">
|
|
|
${h.submit('save',_('Verify'),class_="btn btn-primary", style='width: 40%;', disabled=not codes_viewed)}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<script>
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
let clipboardIcons = document.querySelectorAll('.clipboard-action');
|
|
|
|
|
|
clipboardIcons.forEach(function(icon) {
|
|
|
icon.addEventListener('click', function() {
|
|
|
var inputField = document.getElementById('secretField');
|
|
|
inputField.select();
|
|
|
document.execCommand('copy');
|
|
|
|
|
|
});
|
|
|
});
|
|
|
});
|
|
|
</script>
|
|
|
<script>
|
|
|
document.getElementById('toggleLink').addEventListener('click', function() {
|
|
|
let hiddenField = document.getElementById('secretDiv');
|
|
|
if (hiddenField.classList.contains('hidden')) {
|
|
|
hiddenField.classList.remove('hidden');
|
|
|
}
|
|
|
});
|
|
|
</script>
|
|
|
<script>
|
|
|
const recovery_codes_string = '${recovery_codes}';
|
|
|
const cleaned_recovery_codes_string = recovery_codes_string
|
|
|
.replace(/"/g, '"')
|
|
|
.replace(/'/g, "'");
|
|
|
|
|
|
const recovery_codes = JSON.parse(cleaned_recovery_codes_string);
|
|
|
|
|
|
const cleaned_recovery_codes = recovery_codes.map(code => code.replace(/['"]/g, ''));
|
|
|
|
|
|
function showRecoveryCodesPopup() {
|
|
|
const popup = document.getElementById("codesPopup");
|
|
|
const codesList = document.getElementById("recoveryCodesList");
|
|
|
const verify_btn = document.getElementById('save')
|
|
|
|
|
|
if (verify_btn.disabled) {
|
|
|
codesList.innerHTML = "";
|
|
|
|
|
|
cleaned_recovery_codes.forEach(code => {
|
|
|
const listItem = document.createElement("li");
|
|
|
listItem.textContent = code;
|
|
|
codesList.appendChild(listItem);
|
|
|
});
|
|
|
|
|
|
popup.style.display = "block";
|
|
|
verify_btn.disabled = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
document.getElementById("save").addEventListener("mouseover", showRecoveryCodesPopup);
|
|
|
|
|
|
const popup = document.getElementById("codesPopup");
|
|
|
const closeButton = document.querySelector(".close");
|
|
|
window.onclick = function(event) {
|
|
|
if (event.target === popup || event.target === closeButton) {
|
|
|
popup.style.display = "none";
|
|
|
}
|
|
|
}
|
|
|
|
|
|
document.getElementById("copyAllBtn").addEventListener("click", function() {
|
|
|
const codesListItems = document.querySelectorAll("#recoveryCodesList li");
|
|
|
const allCodes = Array.from(codesListItems).map(item => item.textContent).join(", ");
|
|
|
|
|
|
const textarea = document.createElement('textarea');
|
|
|
textarea.value = allCodes;
|
|
|
document.body.appendChild(textarea);
|
|
|
|
|
|
textarea.select();
|
|
|
document.execCommand('copy');
|
|
|
|
|
|
document.body.removeChild(textarea);
|
|
|
});
|
|
|
</script>
|
|
|
|