mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-08 15:28:05 +03:00
WIP new WebAdmin: two factor auth page
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
@@ -494,7 +494,7 @@ func loadAdminTemplates(templatesPath string) {
|
|||||||
filepath.Join(templatesPath, templateAdminDir, templateIPList),
|
filepath.Join(templatesPath, templateAdminDir, templateIPList),
|
||||||
}
|
}
|
||||||
mfaPaths := []string{
|
mfaPaths := []string{
|
||||||
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
|
filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
|
||||||
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
||||||
filepath.Join(templatesPath, templateAdminDir, templateMFA),
|
filepath.Join(templatesPath, templateAdminDir, templateMFA),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||||||
<div class="fs-4 text-gray-800">
|
<div class="fs-4 text-gray-800">
|
||||||
<span data-i18n="{{.Success}}"></span>
|
<span data-i18n="{{.Success}}"></span>
|
||||||
{{- if .Text}}
|
{{- if .Text}}
|
||||||
{{.Text}}
|
<span> {{.Text}}</span>
|
||||||
{{- end}}
|
{{- end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,60 +1,78 @@
|
|||||||
<!--
|
<!--
|
||||||
Copyright (C) 2019 Nicola Murino
|
Copyright (C) 2024 Nicola Murino
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
|
||||||
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,
|
https://keenthemes.com/products/templates-mega-bundle
|
||||||
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
|
KeenThemes HTML/CSS/JS components are allowed for use only within the
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
SFTPGo product and restricted to be used in a resealable HTML template
|
||||||
|
that can compete with KeenThemes products anyhow.
|
||||||
|
|
||||||
|
This WebUI is allowed for use only within the SFTPGo product and
|
||||||
|
therefore cannot be used in derivative works/products without an
|
||||||
|
explicit grant from the SFTPGo Team (support@sftpgo.com).
|
||||||
-->
|
-->
|
||||||
{{template "base" .}}
|
{{template "base" .}}
|
||||||
|
|
||||||
{{define "title"}}{{.Title}}{{end}}
|
{{- define "page_body"}}
|
||||||
|
{{- if .TOTPConfig.Enabled}}
|
||||||
{{define "extra_css"}}
|
<div class="notice d-flex bg-light-primary rounded border-primary border border-dashed p-6 mb-5">
|
||||||
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
|
<i class="ki-duotone ki-shield-tick fs-2tx text-primary me-4">
|
||||||
{{end}}
|
<span class="path1"></span>
|
||||||
|
<span class="path2"></span>
|
||||||
{{define "page_body"}}
|
</i>
|
||||||
|
<div class="d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap">
|
||||||
<div class="card shadow mb-4">
|
<div class="mb-3 mb-md-0 fw-semibold">
|
||||||
<div class="card-header py-3">
|
<h4 class="text-gray-900 fw-bold">
|
||||||
<h6 class="m-0 font-weight-bold text-primary">TOTP (Authenticator app)</h6>
|
<span data-i18n="2fa.msg_enabled"></span> {{- if gt (len .TOTPConfigs) 1 }}
|
||||||
|
({{$.TOTPConfig.ConfigName}}) {{- end}}
|
||||||
|
</h4>
|
||||||
|
<div class="fs-6 text-gray-800 pe-7">
|
||||||
|
<span data-i18n="2fa.msg_info"></span>
|
||||||
</div>
|
</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>
|
||||||
<div id="errorTOTPMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
<button type="button" id="disable_btn" class="btn btn-danger ms-4 px-6 align-self-center text-nowrap">
|
||||||
<span id="errorTOTPTxt"></span>
|
<span data-i18n="general.disable" class="indicator-label">
|
||||||
<button type="button" class="close" aria-label="Close" onclick="dismissErrorTOTPMsg();">
|
Disable
|
||||||
<span aria-hidden="true">×</span>
|
</span>
|
||||||
|
<span data-i18n="general.wait" class="indicator-progress">
|
||||||
|
Please wait...
|
||||||
|
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript">
|
</div>
|
||||||
function dismissErrorTOTPMsg(){
|
{{- end}}
|
||||||
$('#errorTOTPMsg').hide();
|
|
||||||
}
|
<div class="card shadow-sm">
|
||||||
</script>
|
<div class="card-header bg-light">
|
||||||
<div>
|
<h3 data-i18n="2fa.title" class="card-title section-title">Two-factor authentication using Authenticator apps</h3>
|
||||||
<p>Status: {{if .TOTPConfig.Enabled }}"Enabled". Current configuration: "{{.TOTPConfig.ConfigName}}"{{else}}"Disabled"{{end}}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row totpDisable">
|
<div class="card-body">
|
||||||
<div class="col-sm-12">
|
{{- if not .TOTPConfig.Enabled}}
|
||||||
<a id="idTOTPDisable" class="btn btn-warning" href="#" onclick="totpDisableAsk()" role="button">Disable</a>
|
<div class="notice d-flex bg-light-primary rounded border-primary border border-dashed p-6 mb-5">
|
||||||
|
<i class="ki-duotone ki-shield-tick fs-2tx text-primary me-4">
|
||||||
|
<span class="path1"></span>
|
||||||
|
<span class="path2"></span>
|
||||||
|
</i>
|
||||||
|
<div class="d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap">
|
||||||
|
<div class="mb-3 mb-md-0 fw-semibold">
|
||||||
|
<h4 class="text-gray-900 fw-bold">
|
||||||
|
<span data-i18n="2fa.msg_disabled">Secure Your Account</span>
|
||||||
|
</h4>
|
||||||
|
<div class="fs-6 text-gray-800 pe-7">
|
||||||
|
<span data-i18n="2fa.msg_info"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
</div>
|
||||||
<label for="idConfig" class="col-sm-2 col-form-label">Configuration</label>
|
</div>
|
||||||
<div class="col-sm-10">
|
{{- end}}
|
||||||
<select class="form-control selectpicker" id="idConfig" name="config_name">
|
<div class="form-group row mt-10">
|
||||||
<option value="">None</option>
|
<label for="id_config" data-i18n="general.configuration" class="col-md-3 col-form-label">Configuration</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<select id="id_config" name="config_name" class="form-select" data-control="i18n-select2" data-hide-search="true">
|
||||||
|
<option value="">---</option>
|
||||||
{{range .TOTPConfigs}}
|
{{range .TOTPConfigs}}
|
||||||
<option value="{{.}}" {{if eq . $.TOTPConfig.ConfigName}}selected{{end}}>{{.}}</option>
|
<option value="{{.}}" {{if eq . $.TOTPConfig.ConfigName}}selected{{end}}>{{.}}</option>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -62,369 +80,550 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group row totpGenerate">
|
<div class="d-flex justify-content-end mt-15">
|
||||||
<div class="col-sm-12">
|
{{- if .TOTPConfig.Enabled }}
|
||||||
<a id="idTOTPGenerate" class="btn btn-primary" href="#" onclick="totpGenerate()" role="button">Generate new secret</a>
|
<button type="button" id="generate_secret_btn" class="btn btn-light-primary px-10 me-10 d-none">
|
||||||
</div>
|
<span data-i18n="2fa.generate" class="indicator-label">
|
||||||
</div>
|
Generate new secret
|
||||||
|
</span>
|
||||||
<div id="idTOTPDetails" class="totpDetails">
|
<span data-i18n="general.wait" class="indicator-progress">
|
||||||
<div>
|
Please wait...
|
||||||
<p>Your new TOTP secret is: <span id="idSecret"></span></p>
|
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
|
||||||
<p>For quick setup, scan this QR code with your TOTP app:</p>
|
</span>
|
||||||
<img id="idQRCode" src="data:image/gif;base64, R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="QR code" />
|
</button>
|
||||||
</div>
|
{{- end}}
|
||||||
<br>
|
<button type="button" id="save_btn" class="btn btn-primary px-10 d-none">
|
||||||
<div>
|
<span id="save_label" class="indicator-label"></span>
|
||||||
<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>
|
<span data-i18n="general.wait" class="indicator-progress">
|
||||||
</div>
|
Please wait...
|
||||||
|
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
|
||||||
<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>
|
</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 fade show" style="display: none;" role="alert">
|
|
||||||
<span id="errorRecCodesTxt"></span>
|
|
||||||
<button type="button" class="close" aria-label="Close" onclick="dismissErrorRecCodesMsg();">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript">
|
|
||||||
function dismissErrorRecCodesMsg(){
|
|
||||||
$('#errorRecCodesMsg').hide();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<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>
|
||||||
<div class="form-group row viewRecoveryCodes">
|
</div>
|
||||||
<div class="col-sm-12">
|
|
||||||
<a class="btn btn-primary" href="#" onclick="getRecoveryCodes()" role="button">View</a>
|
{{- if .TOTPConfig.Enabled}}
|
||||||
|
<div class="accordion shadow-sm my-10" id="id_accordion">
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="accordion_rec_codes">
|
||||||
|
<button class="accordion-button section-title text-primary collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#accordion_rec_codes_body" aria-expanded="false" aria-controls="accordion_rec_codes_body">
|
||||||
|
<span data-i18n="2fa.recovery_codes">
|
||||||
|
Recovery codes
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="accordion_rec_codes_body" class="accordion-collapse collapse" aria-labelledby="accordion_rec_codes" data-bs-parent="#id_accordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<div class="fs-5 text-gray-800">
|
||||||
|
<p data-i18n="2fa.recovery_codes_msg1">Recovery codes are a set of one time use codes that can be used in place of the authentication code 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 two-factor configuration.</p>
|
||||||
|
<p data-i18n="2fa.recovery_codes_msg2">To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.</p>
|
||||||
|
<p data-i18n="2fa.recovery_codes_msg3" class="fs-4 fw-bold">If you generate new recovery codes, you automatically invalidate old ones.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="d-flex justify-content-end mt-10">
|
||||||
|
<button type="button" id="generate_recovery_code_btn" class="btn btn-light-primary px-10 me-10">
|
||||||
|
<span data-i18n="2fa.recovery_codes_generate" class="indicator-label">
|
||||||
|
Generate
|
||||||
|
</span>
|
||||||
|
<span data-i18n="general.wait" class="indicator-progress">
|
||||||
|
Please wait...
|
||||||
|
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" id="view_recovery_code_btn" class="btn btn-primary px-10">
|
||||||
|
<span data-i18n="2fa.recovery_codes_view" id="save_label" class="indicator-label">View</span>
|
||||||
|
<span data-i18n="general.wait" class="indicator-progress">
|
||||||
|
Please wait...
|
||||||
|
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{- end}}
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{define "dialog"}}
|
{{- end}}
|
||||||
<div class="modal fade" id="disableTOTPModal" tabindex="-1" role="dialog" aria-labelledby="disableTOTPModalLabel"
|
|
||||||
aria-hidden="true">
|
{{- define "modals"}}
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal fade" id="recovery_codes_modal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="disableTOTPModalLabel">
|
<h3 id="idRecoveryCodesTitle" class="modal-title"></h3>
|
||||||
Confirmation required
|
<div data-i18n="[aria-label]general.close" class="btn btn-icon btn-sm btn-active-light-primary" data-bs-dismiss="modal" aria-label="Close">
|
||||||
</h5>
|
<i class="ki-solid ki-cross fs-2x text-gray-700"></i>
|
||||||
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
|
</div>
|
||||||
<span aria-hidden="true">×</span>
|
</div>
|
||||||
</button>
|
<div class="modal-body">
|
||||||
|
<div id="idRecoveryCodesList" class="d-flex flex-column">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">Do you want to disable the TOTP configuration?</div>
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-secondary" type="button" data-dismiss="modal">
|
<button data-i18n="general.ok" class="btn btn-primary" type="button" data-bs-dismiss="modal">OK</button>
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<a class="btn btn-warning" href="#" onclick="totpDisable()">
|
|
||||||
Disable
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{define "extra_js"}}
|
<div class="modal fade" id="qrcode_modal" tabindex="-1">
|
||||||
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
|
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||||
<script type="text/javascript">
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 data-i18n="2fa.setup_title" class="modal-title">Set up two-factor authentication</h3>
|
||||||
|
<div data-i18n="[aria-label]general.close" class="btn btn-icon btn-sm btn-active-light-primary" data-bs-dismiss="modal" aria-label="Close">
|
||||||
|
<i class="ki-solid ki-cross fs-2x text-gray-700"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body scroll-y pt-10 pb-15 px-lg-17">
|
||||||
|
<div class="text-gray-700 fw-semibold fs-6 mb-10">
|
||||||
|
<span data-i18n="2fa.setup_msg">
|
||||||
|
Use your preferred Authenticator App (e.g. Microsoft Authenticator, Google Authenticator, Authy, 1Password etc. ) to scan the QR code. It will generate an authentication code for you to enter below.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="id_qr_code_container" class="pt-5 text-center">
|
||||||
|
</div>
|
||||||
|
<div class="notice d-flex bg-light-warning rounded border-warning border border-dashed my-10 p-6">
|
||||||
|
<i class="ki-duotone ki-information-5 fs-2tx text-warning me-4">
|
||||||
|
<span class="path1"></span>
|
||||||
|
<span class="path2"></span>
|
||||||
|
<span class="path3"></span>
|
||||||
|
</i>
|
||||||
|
<div class="d-flex flex-stack flex-grow-1">
|
||||||
|
<div class="fw-semibold">
|
||||||
|
<div class="fs-6 text-gray-800">
|
||||||
|
<span data-i18n="2fa.setup_help">If you have trouble using the QR code, select manual entry on your app, and enter the code:</span>
|
||||||
|
<span id="id_secret" class="fw-bold text-gray-900 pt-2"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
function totpGenerate() {
|
<div id="errorModalMsg" class="d-none rounded border-warning border border-dashed bg-light-warning d-flex align-items-center p-5 mb-10">
|
||||||
$('#errorTOTPMsg').hide();
|
<i class="ki-duotone ki-information-5 fs-3x text-warning me-5"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i>
|
||||||
let path = "{{.GenerateTOTPURL}}";
|
<div class="text-gray-700 fw-bold fs-5 d-flex flex-column pe-0 pe-sm-10">
|
||||||
$.ajax({
|
<span id="errorModalTxt"></span>
|
||||||
url: path,
|
</div>
|
||||||
type: 'POST',
|
<button id="id_dismiss_error_modal_msg" type="button" class="position-absolute position-sm-relative m-2 m-sm-0 top-0 end-0 btn btn-icon btn-sm btn-active-light-primary ms-sm-auto">
|
||||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
<i class="ki-solid ki-cross fs-2x text-gray-700"></i>
|
||||||
data: JSON.stringify({"config_name": $('#idConfig option:selected').val()}),
|
</button>
|
||||||
dataType: 'json',
|
</div>
|
||||||
contentType: 'application/json',
|
|
||||||
timeout: 15000,
|
<div class="fv-row">
|
||||||
success: function (result) {
|
<input data-i18n="[placeholder]login.auth_code" type="text" id="id_passcode" name="passcode" class="form-control form-control-lg form-control-solid" placeholder="Enter authentication code" spellcheck="false" />
|
||||||
$('.totpDisable').hide();
|
</div>
|
||||||
$('.totpGenerate').hide();
|
</div>
|
||||||
$('#idSecret').text(result.secret);
|
<div class="modal-footer">
|
||||||
$('#idQRCode').attr('src','data:image/png;base64, '+result.qr_code);
|
<button data-i18n="general.cancel" type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
$('.totpDetails').show();
|
<button type="button" class="btn btn-primary ms-6" id="passcode_btn">
|
||||||
window.scrollTo(0, $("#idTOTPDetails").offset().top);
|
<span data-i18n="general.submit" class="indicator-label">
|
||||||
},
|
Submit
|
||||||
error: function ($xhr, textStatus, errorThrown) {
|
</span>
|
||||||
let txt = "Failed to generate a new TOTP secret";
|
<span data-i18n="general.wait" class="indicator-progress">
|
||||||
if ($xhr) {
|
Please wait...
|
||||||
let json = $xhr.responseJSON;
|
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
|
||||||
if (json) {
|
</span>
|
||||||
if (json.message){
|
</button>
|
||||||
txt += ": " + json.message;
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
{{- define "extra_js"}}
|
||||||
|
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
|
||||||
|
const qrModal = new bootstrap.Modal('#qrcode_modal');
|
||||||
|
const recCodesModal = new bootstrap.Modal('#recovery_codes_modal');
|
||||||
|
|
||||||
|
function onConfigChanged() {
|
||||||
|
let selectedConfig = $('#id_config option:selected').val();
|
||||||
|
if (selectedConfig == ""){
|
||||||
|
$('#save_btn').addClass("d-none");
|
||||||
|
$('#generate_secret_btn').addClass("d-none");
|
||||||
} else {
|
} else {
|
||||||
txt += ": " + json.error;
|
//{{- if .TOTPConfig.Enabled }}
|
||||||
}
|
if (selectedConfig == "{{.TOTPConfig.ConfigName}}"){
|
||||||
}
|
$('#save_label').text($.t('general.submit'));
|
||||||
}
|
|
||||||
$('#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',
|
|
||||||
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 {
|
} else {
|
||||||
txt += ": " + json.error;
|
$('#save_label').text($.t('general.enable'));
|
||||||
}
|
}
|
||||||
|
$('#save_btn').removeClass("d-none");
|
||||||
|
$('#generate_secret_btn').removeClass("d-none");
|
||||||
|
//{{- else}}
|
||||||
|
$('#save_btn').removeClass("d-none");
|
||||||
|
$('#save_label').text($.t('general.enable'));
|
||||||
|
$('#generate_secret_btn').addClass("d-none");
|
||||||
|
//{{- end}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$('#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',
|
|
||||||
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',
|
|
||||||
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() {
|
function generateRecoveryCodes() {
|
||||||
$('#errorRecCodesMsg').hide();
|
el = document.querySelector('#generate_recovery_code_btn');
|
||||||
let path = "{{.RecCodesURL}}";
|
el.setAttribute('data-kt-indicator', 'on');
|
||||||
|
el.disabled = true;
|
||||||
|
|
||||||
$.ajax({
|
axios.post('{{.RecCodesURL}}', null, {
|
||||||
url: path,
|
|
||||||
type: 'POST',
|
|
||||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
|
||||||
dataType: 'json',
|
|
||||||
contentType: 'application/json',
|
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
success: function (result) {
|
headers: {
|
||||||
$('.viewRecoveryCodes').hide();
|
'X-CSRF-TOKEN': '{{.CSRFToken}}'
|
||||||
$('#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) {
|
validateStatus: function (status) {
|
||||||
let txt = "Failed to generate new recovery codes";
|
return status == 200;
|
||||||
if ($xhr) {
|
}
|
||||||
let json = $xhr.responseJSON;
|
}).then(function (response){
|
||||||
if (json) {
|
el.removeAttribute('data-kt-indicator');
|
||||||
if (json.message){
|
el.disabled = false;
|
||||||
txt += ": " + json.message;
|
$('#idRecoveryCodesTitle').text($.t('2fa.new_recovery_codes'));
|
||||||
|
let recList = $('#idRecoveryCodesList');
|
||||||
|
recList.empty();
|
||||||
|
$.each(response.data, function(key, item) {
|
||||||
|
itemCode = escapeHTML(item);
|
||||||
|
recList.append(`<li class="d-flex align-items-center py-2 fw-semibold fs-5 text-gray-800"><span class="bullet bullet-dot me-5"></span>${itemCode}</li>`);
|
||||||
|
});
|
||||||
|
recCodesModal.show();
|
||||||
|
}).catch(function (error){
|
||||||
|
el.removeAttribute('data-kt-indicator');
|
||||||
|
el.disabled = false;
|
||||||
|
ModalAlert.fire({
|
||||||
|
text: $.t('2fa.recovery_codes_gen_err'),
|
||||||
|
icon: "warning",
|
||||||
|
confirmButtonText: $.t('general.ok'),
|
||||||
|
customClass: {
|
||||||
|
confirmButton: "btn btn-primary"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewRecoveryCodes() {
|
||||||
|
el = document.querySelector('#view_recovery_code_btn');
|
||||||
|
el.setAttribute('data-kt-indicator', 'on');
|
||||||
|
el.disabled = true;
|
||||||
|
|
||||||
|
axios.get('{{.RecCodesURL}}',{
|
||||||
|
timeout: 15000,
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': '{{.CSRFToken}}'
|
||||||
|
},
|
||||||
|
validateStatus: function (status) {
|
||||||
|
return status == 200;
|
||||||
|
}
|
||||||
|
}).then(function (response){
|
||||||
|
el.removeAttribute('data-kt-indicator');
|
||||||
|
el.disabled = false;
|
||||||
|
$('#idRecoveryCodesTitle').text($.t('2fa.recovery_codes'));
|
||||||
|
let recList = $('#idRecoveryCodesList');
|
||||||
|
recList.empty();
|
||||||
|
$.each(response.data, function(key, item) {
|
||||||
|
itemCode = escapeHTML(item.code);
|
||||||
|
let txtStyleClass = "";
|
||||||
|
if (item.used) {
|
||||||
|
txtStyleClass = "line-through";
|
||||||
|
}
|
||||||
|
recList.append(`<li class="d-flex align-items-center py-2 fw-semibold fs-5 text-gray-800 ${txtStyleClass}"><span class="bullet bullet-dot me-5"></span>${itemCode}</li>`);
|
||||||
|
});
|
||||||
|
recCodesModal.show();
|
||||||
|
}).catch(function (error){
|
||||||
|
el.removeAttribute('data-kt-indicator');
|
||||||
|
el.disabled = false;
|
||||||
|
ModalAlert.fire({
|
||||||
|
text: $.t('2fa.recovery_codes_get_err'),
|
||||||
|
icon: "warning",
|
||||||
|
confirmButtonText: $.t('general.ok'),
|
||||||
|
customClass: {
|
||||||
|
confirmButton: "btn btn-primary"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableConfig() {
|
||||||
|
ModalAlert.fire({
|
||||||
|
text: $.t('2fa.disable_question'),
|
||||||
|
icon: "warning",
|
||||||
|
confirmButtonText: $.t('general.confirm'),
|
||||||
|
cancelButtonText: $.t('general.cancel'),
|
||||||
|
customClass: {
|
||||||
|
confirmButton: "btn btn-danger",
|
||||||
|
cancelButton: 'btn btn-secondary'
|
||||||
|
}
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed){
|
||||||
|
doSaveConfig(document.querySelector('#disable_btn'), null, false, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePasscode() {
|
||||||
|
el = document.querySelector('#passcode_btn');
|
||||||
|
let errDivEl = $('#errorModalMsg');
|
||||||
|
let errTxtEl = $('#errorModalTxt');
|
||||||
|
let errorMessage = '2fa.auth_code_invalid';
|
||||||
|
let passcode = $('#id_passcode').val();
|
||||||
|
|
||||||
|
errDivEl.addClass("d-none");
|
||||||
|
if (passcode == "") {
|
||||||
|
setI18NData(errTxtEl, '2fa.auth_code_required');
|
||||||
|
errDivEl.removeClass("d-none");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.setAttribute('data-kt-indicator', 'on');
|
||||||
|
el.disabled = true;
|
||||||
|
|
||||||
|
axios.post("{{.ValidateTOTPURL}}", {
|
||||||
|
passcode: passcode,
|
||||||
|
config_name: $('#id_config option:selected').val(),
|
||||||
|
secret: $('#id_secret').text()
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
timeout: 15000,
|
||||||
|
'X-CSRF-TOKEN': '{{.CSRFToken}}'
|
||||||
|
},
|
||||||
|
validateStatus: function (status) {
|
||||||
|
return status == 200;
|
||||||
|
}
|
||||||
|
}).then(function (response){
|
||||||
|
el.removeAttribute('data-kt-indicator');
|
||||||
|
el.disabled = false;
|
||||||
|
if (!response.data.message) {
|
||||||
|
setI18NData(errTxtEl, errorMessage);
|
||||||
|
errDivEl.removeClass("d-none");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
qrModal.hide();
|
||||||
|
doSaveConfig(null, null, true, false);
|
||||||
|
}).catch(function (error){
|
||||||
|
el.removeAttribute('data-kt-indicator');
|
||||||
|
el.disabled = false;
|
||||||
|
setI18NData(errTxtEl, errorMessage);
|
||||||
|
errDivEl.removeClass("d-none");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmGenerateSecret() {
|
||||||
|
ModalAlert.fire({
|
||||||
|
text: $.t('2fa.generate_question'),
|
||||||
|
icon: "warning",
|
||||||
|
confirmButtonText: $.t('general.confirm'),
|
||||||
|
cancelButtonText: $.t('general.cancel'),
|
||||||
|
customClass: {
|
||||||
|
confirmButton: "btn btn-danger",
|
||||||
|
cancelButton: 'btn btn-secondary'
|
||||||
|
}
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed){
|
||||||
|
el = document.querySelector('#generate_secret_btn');
|
||||||
|
configName = $('#id_config option:selected').val();
|
||||||
|
generateSecret(el,configName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSecret(el, configName) {
|
||||||
|
if (!el || !configName){
|
||||||
|
confirmGenerateSecret();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let errorMessage = '2fa.auth_secret_gen_err';
|
||||||
|
$('#id_secret').text("");
|
||||||
|
el.setAttribute('data-kt-indicator', 'on');
|
||||||
|
el.disabled = true;
|
||||||
|
|
||||||
|
axios.post("{{.GenerateTOTPURL}}", {
|
||||||
|
config_name: configName
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
timeout: 15000,
|
||||||
|
'X-CSRF-TOKEN': '{{.CSRFToken}}'
|
||||||
|
},
|
||||||
|
validateStatus: function (status) {
|
||||||
|
return status == 200;
|
||||||
|
}
|
||||||
|
}).then(function (response){
|
||||||
|
el.removeAttribute('data-kt-indicator');
|
||||||
|
el.disabled = false;
|
||||||
|
if (!response.data.secret) {
|
||||||
|
ModalAlert.fire({
|
||||||
|
text: $.t(errorMessage),
|
||||||
|
icon: "warning",
|
||||||
|
confirmButtonText: $.t('general.ok'),
|
||||||
|
customClass: {
|
||||||
|
confirmButton: "btn btn-primary"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$('#id_secret').text(response.data.secret);
|
||||||
|
$('#errorModalMsg').addClass("d-none");
|
||||||
|
$('#id_passcode').val("");
|
||||||
|
let qrCodeContainer = document.getElementById("id_qr_code_container");
|
||||||
|
clearChilds(qrCodeContainer);
|
||||||
|
let qrCodeImg = document.createElement("img");
|
||||||
|
qrCodeImg.classList.add("mw-150px");
|
||||||
|
qrCodeImg.src = "{{.MFAURL}}/qrcode?url="+encodeURIComponent(response.data.url);
|
||||||
|
qrCodeImg.alt = $.t('general.qr_code');
|
||||||
|
qrCodeContainer.appendChild(qrCodeImg);
|
||||||
|
qrModal.show();
|
||||||
|
}).catch(function (error){
|
||||||
|
el.removeAttribute('data-kt-indicator');
|
||||||
|
el.disabled = false;
|
||||||
|
ModalAlert.fire({
|
||||||
|
text: $.t(errorMessage),
|
||||||
|
icon: "warning",
|
||||||
|
confirmButtonText: $.t('general.ok'),
|
||||||
|
customClass: {
|
||||||
|
confirmButton: "btn btn-primary"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function doSaveConfig(el, configName, full, disabled) {
|
||||||
|
if (!el){
|
||||||
|
el = document.querySelector('#save_btn');
|
||||||
|
}
|
||||||
|
if (!configName) {
|
||||||
|
configName = $('#id_config option:selected').val();
|
||||||
|
}
|
||||||
|
let errorMessage = '2fa.save_err';
|
||||||
|
let postData = {};
|
||||||
|
if (full){
|
||||||
|
postData.config_name = configName;
|
||||||
|
postData.secret = {
|
||||||
|
status: "Plain",
|
||||||
|
payload: $('#id_secret').text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (disabled){
|
||||||
|
postData.enabled = false;
|
||||||
} else {
|
} else {
|
||||||
txt += ": " + json.error;
|
postData.enabled = true;
|
||||||
}
|
}
|
||||||
|
el.setAttribute('data-kt-indicator', 'on');
|
||||||
|
el.disabled = true;
|
||||||
|
|
||||||
|
axios.post("{{.SaveTOTPURL}}", postData, {
|
||||||
|
headers: {
|
||||||
|
timeout: 15000,
|
||||||
|
'X-CSRF-TOKEN': '{{.CSRFToken}}'
|
||||||
|
},
|
||||||
|
validateStatus: function (status) {
|
||||||
|
return status == 200;
|
||||||
}
|
}
|
||||||
|
}).then(function (response){
|
||||||
|
el.removeAttribute('data-kt-indicator');
|
||||||
|
el.disabled = false;
|
||||||
|
if (!response.data.message) {
|
||||||
|
ModalAlert.fire({
|
||||||
|
text: $.t(errorMessage),
|
||||||
|
icon: "warning",
|
||||||
|
confirmButtonText: $.t('general.ok'),
|
||||||
|
customClass: {
|
||||||
|
confirmButton: "btn btn-primary"
|
||||||
}
|
}
|
||||||
$('#errorRecCodesTxt').text(txt);
|
});
|
||||||
$('#errorRecCodesMsg').show();
|
return;
|
||||||
window.scrollTo(0, $("#idRecoveryCodesCard").offset().top);
|
|
||||||
}
|
}
|
||||||
|
ModalAlert.fire({
|
||||||
|
text: $.t('general.config_saved'),
|
||||||
|
icon: "success",
|
||||||
|
confirmButtonText: $.t('general.ok'),
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-primary'
|
||||||
|
}
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed){
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(function (error) {
|
||||||
|
el.removeAttribute('data-kt-indicator');
|
||||||
|
el.disabled = false;
|
||||||
|
ModalAlert.fire({
|
||||||
|
text: $.t(errorMessage),
|
||||||
|
icon: "warning",
|
||||||
|
confirmButtonText: $.t('general.ok'),
|
||||||
|
customClass: {
|
||||||
|
confirmButton: "btn btn-primary"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConfig() {
|
||||||
|
let selectedConfig = $('#id_config option:selected').val();
|
||||||
|
let saveBtn = document.querySelector('#save_btn');
|
||||||
|
if (selectedConfig == "{{.TOTPConfig.ConfigName}}"){
|
||||||
|
doSaveConfig(saveBtn, selectedConfig, false, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
generateSecret(saveBtn, selectedConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).on("i18nshow", function(){
|
||||||
|
onConfigChanged();
|
||||||
|
|
||||||
|
var dismissErrorModalBtn = $('#id_dismiss_error_modal_msg');
|
||||||
|
if (dismissErrorModalBtn){
|
||||||
|
dismissErrorModalBtn.on("click",function(){
|
||||||
|
$('#errorModalMsg').addClass("d-none");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleConfigSelection() {
|
var disableBtn = $('#disable_btn');
|
||||||
var selectedConfig = $('#idConfig option:selected').val();
|
if (disableBtn){
|
||||||
if (selectedConfig == ""){
|
disableBtn.on("click", function(){
|
||||||
$('.totpGenerate').hide();
|
disableConfig();
|
||||||
} else {
|
});
|
||||||
$('.totpGenerate').show();
|
|
||||||
}
|
|
||||||
$('.totpDetails').hide();
|
|
||||||
{{if .TOTPConfig.Enabled }}
|
|
||||||
$('.totpDisable').show();
|
|
||||||
{{end}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function () {
|
var generateSecretBtn = $('#generate_secret_btn');
|
||||||
handleConfigSelection();
|
if (generateSecretBtn){
|
||||||
$('.totpDetails').hide();
|
generateSecretBtn.on("click", function(){
|
||||||
{{if not .TOTPConfig.Enabled }}
|
generateSecret();
|
||||||
$('.totpDisable').hide();
|
});
|
||||||
{{end}}
|
}
|
||||||
|
|
||||||
$('#idConfig').change(function() {
|
var saveBtn = $('#save_btn');
|
||||||
handleConfigSelection();
|
if (saveBtn){
|
||||||
|
saveBtn.on("click", function(){
|
||||||
|
saveConfig();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var generateRecoveryCodeBtn = $('#generate_recovery_code_btn');
|
||||||
|
if (generateRecoveryCodeBtn){
|
||||||
|
generateRecoveryCodeBtn.on("click", function(){
|
||||||
|
generateRecoveryCodes();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var viewRecoveryCodesBtn = $('#view_recovery_code_btn');
|
||||||
|
if (viewRecoveryCodesBtn){
|
||||||
|
viewRecoveryCodesBtn.on("click", function(){
|
||||||
|
viewRecoveryCodes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var passcodeBtn = $('#passcode_btn');
|
||||||
|
if (passcodeBtn){
|
||||||
|
passcodeBtn.on("click", function() {
|
||||||
|
validatePasscode();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var configSelect = $('#id_config');
|
||||||
|
if (configSelect){
|
||||||
|
configSelect.on("change", function(){
|
||||||
|
onConfigChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{- end}}
|
||||||
@@ -600,7 +600,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||||||
}
|
}
|
||||||
let postData = {
|
let postData = {
|
||||||
protocols: protocolsArray
|
protocols: protocolsArray
|
||||||
}
|
};
|
||||||
if (full){
|
if (full){
|
||||||
postData.config_name = configName;
|
postData.config_name = configName;
|
||||||
postData.secret = {
|
postData.secret = {
|
||||||
|
|||||||
Reference in New Issue
Block a user