sftpgo/templates/webadmin/mfa.html
Nicola Murino 04ab8e72f6
WebUI: make error messages user dismissible
Fixes #1171

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-02-10 18:07:23 +01:00

420 lines
No EOL
16 KiB
HTML

<!--
Copyright (C) 2019-2023 Nicola Murino
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "extra_css"}}
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
{{end}}
{{define "page_body"}}
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">TOTP (Authenticator app)</h6>
</div>
<div class="card-body">
<div id="successTOTPMsg" class="card mb-4 border-left-success" style="display: none;">
<div id="successTOTPTxt" class="card-body"></div>
</div>
<div id="errorTOTPMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
<span id="errorTOTPTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div>
<p>Status: {{if .TOTPConfig.Enabled }}"Enabled". Current configuration: "{{.TOTPConfig.ConfigName}}"{{else}}"Disabled"{{end}}</p>
</div>
<div class="form-group row totpDisable">
<div class="col-sm-12">
<a id="idTOTPDisable" class="btn btn-warning" href="#" onclick="totpDisableAsk()" role="button">Disable</a>
</div>
</div>
<div class="form-group row">
<label for="idConfig" class="col-sm-2 col-form-label">Configuration</label>
<div class="col-sm-10">
<select class="form-control selectpicker" id="idConfig" name="config_name">
<option value="">None</option>
{{range .TOTPConfigs}}
<option value="{{.}}" {{if eq . $.TOTPConfig.ConfigName}}selected{{end}}>{{.}}</option>
{{end}}
</select>
</div>
</div>
<div class="form-group row totpGenerate">
<div class="col-sm-12">
<a id="idTOTPGenerate" class="btn btn-primary" href="#" onclick="totpGenerate()" role="button">Generate new secret</a>
</div>
</div>
<div id="idTOTPDetails" class="totpDetails">
<div>
<p>Your new TOTP secret is: <span id="idSecret"></span></p>
<p>For quick setup, scan this QR code with your TOTP app:</p>
<img id="idQRCode" src="data:image/gif;base64, R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="QR code" />
</div>
<br>
<div>
<p>After you configured your app, enter a test code below to ensure everything works correctly. Recovery codes are automatically generated if missing or most of them have already been used</p>
</div>
<div class="input-group">
<input type="text" class="form-control" id="idPasscode" name="passcode" value="" placeholder="Authentication code" spellcheck="false">
<span class="input-group-append">
<a id="idTOTPSave" class="btn btn-primary" href="#" onclick="totpValidate()" role="button">Verify and save</a>
</span>
</div>
</div>
</div>
</div>
{{if .TOTPConfig.Enabled }}
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Recovery codes</h6>
</div>
<div id="idRecoveryCodesCard" class="card-body">
<div id="successRecCodesMsg" class="card mb-4 border-left-success" style="display: none;">
<div id="successRecCodesTxt" class="card-body"></div>
</div>
<div id="errorRecCodesMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
<span id="errorRecCodesTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div>
<p>Recovery codes are a set of one time use codes that can be used in place of the TOTP to login to the web UI. You can use them if you lose access to your phone to login to your account and disable or regenerate TOTP configuration.</p>
<p>To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.</p>
</div>
<div class="form-group row viewRecoveryCodes">
<div class="col-sm-12">
<a class="btn btn-primary" href="#" onclick="getRecoveryCodes()" role="button">View</a>
</div>
</div>
<div id="idRecoveryCodes" style="display: none;">
<ul id="idRecoveryCodesList" class="list-group">
</ul>
<br>
</div>
<div>
<p>If you generate new recovery codes, you automatically invalidate old ones.</p>
</div>
<div class="form-group row">
<div class="col-sm-12">
<a class="btn btn-primary" href="#" onclick="generateRecoveryCodes()" role="button">Generate</a>
</div>
</div>
</div>
</div>
{{end}}
{{end}}
{{define "dialog"}}
<div class="modal fade" id="disableTOTPModal" tabindex="-1" role="dialog" aria-labelledby="disableTOTPModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="disableTOTPModalLabel">
Confirmation required
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">Do you want to disable the TOTP configuration?</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">
Cancel
</button>
<a class="btn btn-warning" href="#" onclick="totpDisable()">
Disable
</a>
</div>
</div>
</div>
</div>
{{end}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
<script type="text/javascript">
function totpGenerate() {
$('#errorTOTPMsg').hide();
let path = "{{.GenerateTOTPURL}}";
$.ajax({
url: path,
type: 'POST',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"config_name": $('#idConfig option:selected').val()}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
timeout: 15000,
success: function (result) {
$('.totpDisable').hide();
$('.totpGenerate').hide();
$('#idSecret').text(result.secret);
$('#idQRCode').attr('src','data:image/png;base64, '+result.qr_code);
$('.totpDetails').show();
window.scrollTo(0, $("#idTOTPDetails").offset().top);
},
error: function ($xhr, textStatus, errorThrown) {
let txt = "Failed to generate a new TOTP secret";
if ($xhr) {
let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
}
}
}
$('#errorTOTPTxt').text(txt);
$('#errorTOTPMsg').show();
}
});
}
function totpValidate() {
$('#errorTOTPMsg').hide();
let passcode = $('#idPasscode').val();
if (passcode == "") {
$('#errorTOTPTxt').text("The verification code is required");
$('#errorTOTPMsg').show();
return;
}
var path = "{{.ValidateTOTPURL}}";
$.ajax({
url: path,
type: 'POST',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"passcode": passcode, "config_name": $('#idConfig option:selected').val(), "secret": $('#idSecret').text()}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
timeout: 15000,
success: function (result) {
totpSave();
},
error: function ($xhr, textStatus, errorThrown) {
var txt = "Failed to validate the provided passcode";
if ($xhr) {
var json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
}
}
}
$('#errorTOTPTxt').text(txt);
$('#errorTOTPMsg').show();
}
});
}
function totpSave() {
let path = "{{.SaveTOTPURL}}";
$('#errorTOTPMsg').hide();
$.ajax({
url: path,
type: 'POST',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"enabled": true, "config_name": $('#idConfig option:selected').val(), "secret": {"status": "Plain", "payload": $('#idSecret').text()}}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
timeout: 15000,
success: function (result) {
$('#successTOTPTxt').text("Configuration saved");
$('#successTOTPMsg').show();
setTimeout(function () {
location.reload();
}, 3000);
},
error: function ($xhr, textStatus, errorThrown) {
let txt = "Failed to save the new configuration";
if ($xhr) {
let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
}
}
}
$('#errorTOTPTxt').text(txt);
$('#errorTOTPMsg').show();
}
});
}
function totpDisableAsk() {
$('#disableTOTPModal').modal('show');
}
function totpDisable() {
$('#disableTOTPModal').modal('hide');
$('#errorTOTPMsg').hide();
let path = "{{.SaveTOTPURL}}";
$.ajax({
url: path,
type: 'POST',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"enabled": false}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
timeout: 15000,
success: function (result) {
location.reload();
},
error: function ($xhr, textStatus, errorThrown) {
var txt = "Failed to disable the current configuration";
if ($xhr) {
var json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
}
}
}
$('#errorTOTPTxt').text(txt);
$('#errorTOTPMsg').show();
}
});
}
function getRecoveryCodes() {
$('#errorRecCodesMsg').hide();
let path = "{{.RecCodesURL}}";
$.ajax({
url: path,
type: 'GET',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
timeout: 15000,
success: function (result) {
$('.viewRecoveryCodes').hide();
$('#idRecoveryCodesList').empty();
$.each(result, function(key, item) {
if (item.used) {
$('#idRecoveryCodesList').append(`<li class="list-group-item" style="text-decoration: line-through;">${item.code}</li>`);
} else {
$('#idRecoveryCodesList').append(`<li class="list-group-item">${item.code}</li>`);
}
});
$('#idRecoveryCodes').show();
window.scrollTo(0, $("#idRecoveryCodesCard").offset().top);
},
error: function ($xhr, textStatus, errorThrown) {
let txt = "Failed to get your recovery codes";
if ($xhr) {
let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
}
}
}
$('#errorRecCodesTxt').text(txt);
$('#errorRecCodesMsg').show();
window.scrollTo(0, $("#idRecoveryCodesCard").offset().top);
}
});
}
function generateRecoveryCodes() {
$('#errorRecCodesMsg').hide();
let path = "{{.RecCodesURL}}";
$.ajax({
url: path,
type: 'POST',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
dataType: 'json',
contentType: 'application/json; charset=utf-8',
timeout: 15000,
success: function (result) {
$('.viewRecoveryCodes').hide();
$('#idRecoveryCodesList').empty();
$.each(result, function(key, item) {
$('#idRecoveryCodesList').append(`<li class="list-group-item">${item}</li>`);
});
$('#idRecoveryCodes').show();
$('#successRecCodesTxt').text('Recovery codes generated successfully');
$('#successRecCodesMsg').show();
window.scrollTo(0, $("#idRecoveryCodesCard").offset().top);
setTimeout(function () {
$('#successRecCodesMsg').hide();
}, 5000);
},
error: function ($xhr, textStatus, errorThrown) {
let txt = "Failed to generate new recovery codes";
if ($xhr) {
let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
}
}
}
$('#errorRecCodesTxt').text(txt);
$('#errorRecCodesMsg').show();
window.scrollTo(0, $("#idRecoveryCodesCard").offset().top);
}
});
}
function handleConfigSelection() {
var selectedConfig = $('#idConfig option:selected').val();
if (selectedConfig == ""){
$('.totpGenerate').hide();
} else {
$('.totpGenerate').show();
}
$('.totpDetails').hide();
{{if .TOTPConfig.Enabled }}
$('.totpDisable').show();
{{end}}
}
$(document).ready(function () {
handleConfigSelection();
$('.totpDetails').hide();
{{if not .TOTPConfig.Enabled }}
$('.totpDisable').hide();
{{end}}
$('#idConfig').change(function() {
handleConfigSelection();
});
});
</script>
{{end}}