WIP: new WebClient UI

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino
2023-11-04 10:54:52 +01:00
parent 2fdcb44c14
commit 9322701615
79 changed files with 7688 additions and 15710 deletions

View File

@@ -1,274 +1,393 @@
<!--
Copyright (C) 2019-2023 Nicola Murino
Copyright (C) 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 WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
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.
https://keenthemes.com/products/templates-mega-bundle
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/>.
KeenThemes HTML/CSS/JS components are allowed for use only within the
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).
-->
{{define "base"}}
{{- define "base"}}
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{.Branding.Name}} - {{template "title" .}}</title>
<meta charset="utf-8" />
<meta name="description" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" href="{{.StaticURL}}{{.Branding.FaviconPath}}" />
{{- template "fonts" .StaticURL }}
<link href="{{.StaticURL}}/assets/plugins/global/plugins.bundle.css" rel="stylesheet" type="text/css" />
<link href="{{.StaticURL}}/assets/css/style.bundle.css" rel="stylesheet" type="text/css" />
{{- template "globalstyle" }}
{{- block "extra_css" .}}{{- end}}
{{- range .Branding.ExtraCSS}}
<link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
{{- end}}
{{- template "commonjs" }}
</head>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>{{.Branding.Name}} - {{template "title" .}}</title>
<link rel="shortcut icon" href="{{.StaticURL}}{{.Branding.FaviconPath}}" />
<!-- Custom fonts for this template-->
<link href="{{.StaticURL}}/vendor/fontawesome-free/css/fontawesome.min.css" rel="stylesheet" type="text/css">
<link href="{{.StaticURL}}/vendor/fontawesome-free/css/solid.min.css" rel="stylesheet" type="text/css">
<link href="{{.StaticURL}}/vendor/fontawesome-free/css/regular.min.css" rel="stylesheet" type="text/css">
<!-- Custom styles for this template-->
<link href="{{.StaticURL}}{{.Branding.DefaultCSS}}" rel="stylesheet">
<style>
{{template "commoncss" .}}
</style>
{{block "extra_css" .}}{{end}}
{{range .Branding.ExtraCSS}}
<link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
{{end}}
</head>
<body id="page-top">
<!-- Page Wrapper -->
<div id="wrapper">
{{if .LoggedUser.Username}}
<!-- Sidebar -->
<ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">
<!-- Sidebar - Brand -->
<div class="sidebar-brand d-flex align-items-center justify-content-center">
<div class="sidebar-brand-icon">
<img src="{{.StaticURL}}{{.Branding.LogoPath}}" alt="logo" style="width: 2rem; height: auto;">
</div>
<div class="sidebar-brand-text mx-3" style="text-transform: none;">{{.Branding.ShortName}}</div>
</div>
<!-- Divider -->
<hr class="sidebar-divider my-0">
<li class="nav-item {{if eq .CurrentURL .FilesURL}}active{{end}}">
<a class="nav-link" href="{{.FilesURL}}">
<i class="fas fa-folder-open"></i>
<span>{{.FilesTitle}}</span>
</a>
</li>
{{if .LoggedUser.CanManageShares}}
<li class="nav-item {{if eq .CurrentURL .SharesURL}}active{{end}}">
<a class="nav-link" href="{{.SharesURL}}">
<i class="fas fa-share-alt"></i>
<span>{{.SharesTitle}}</span></a>
</li>
{{end}}
<li class="nav-item {{if eq .CurrentURL .ProfileURL}}active{{end}}">
<a class="nav-link" href="{{.ProfileURL}}">
<i class="fas fa-user"></i>
<span>{{.ProfileTitle}}</span></a>
</li>
{{if .LoggedUser.CanManageMFA}}
<li class="nav-item {{if eq .CurrentURL .MFAURL}}active{{end}}">
<a class="nav-link" href="{{.MFAURL}}">
<i class="fas fa-user-lock"></i>
<span>{{.MFATitle}}</span></a>
</li>
{{end}}
<!-- Divider -->
<hr class="sidebar-divider d-none d-md-block">
<!-- Sidebar Toggler (Sidebar) -->
<div class="text-center d-none d-md-inline">
<button class="rounded-circle border-0" id="sidebarToggle"></button>
</div>
</ul>
<!-- End of Sidebar -->
{{end}}
<!-- Content Wrapper -->
<div id="content-wrapper" class="d-flex flex-column">
<!-- Main Content -->
<div id="content">
{{if .LoggedUser.Username}}
<!-- Topbar -->
<nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
<button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
<i class="fa fa-bars"></i>
</button>
<!-- Topbar Navbar -->
<ul class="navbar-nav ml-auto">
{{block "additionalnavitems" .}}{{end}}
<!-- Nav Item - User Information -->
<li class="nav-item dropdown no-arrow">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<span class="mr-2 d-none d-lg-inline text-gray-600 small">{{.LoggedUser.Username}}</span>
<i class="fas fa-user fa-fw"></i>
</a>
<!-- Dropdown - User Information -->
<div class="dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="userDropdown">
{{if .LoggedUser.CanChangePassword}}
<a class="dropdown-item" href="{{.ChangePwdURL}}">
<i class="fas fa-key fa-sm fa-fw mr-2 text-gray-400"></i>
Change password
</a>
<div class="dropdown-divider"></div>
{{end}}
<a class="dropdown-item" href="#" data-toggle="modal" data-target="#logoutModal">
<i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
Logout
</a>
<body data-kt-app-header-fixed="true" data-kt-app-header-fixed-mobile="true" data-kt-app-toolbar-enabled="true" data-kt-app-sidebar-enabled="true" data-kt-app-sidebar-fixed="true" data-kt-app-sidebar-push-header="true" data-kt-app-sidebar-push-toolbar="true" data-kt-app-sidebar-push-footer="true" class="app-default">
{{- template "theme-setup"}}
<div class="d-flex flex-column flex-root app-root" id="kt_app_root">
<div class="app-page flex-column flex-column-fluid " id="kt_app_page">
{{- if .LoggedUser.Username}}
<div id="kt_app_header" class="app-header" data-kt-sticky="true" data-kt-sticky-activate="{default: true, lg: true}" data-kt-sticky-name="app-header-minimize" data-kt-sticky-offset="{default: '200px', lg: '300px'}" data-kt-sticky-animation="false">
<div class="app-container container-fluid d-flex align-items-stretch flex-stack " id="kt_app_header_container">
<div class="d-flex align-items-center d-block d-lg-none ms-n3" title="Show sidebar menu">
<div class="btn btn-icon btn-color-gray-800 btn-active-color-primary w-35px h-35px me-1" id="kt_app_sidebar_mobile_toggle">
<i class="ki-duotone ki-abstract-14 fs-2">
<span class="path1"></span>
<span class="path2"></span>
</i>
</div>
</li>
</ul>
</nav>
<!-- End of Topbar -->
{{end}}
<!-- Begin Page Content -->
<div class="container-fluid">
{{template "page_body" .}}
</div>
<!-- /.container-fluid -->
</div>
<!-- End of Main Content -->
{{if .LoggedUser.Username}}
<!-- Footer -->
<footer class="sticky-footer bg-white">
<div class="container my-auto">
<div class="copyright text-center my-auto">
<span>SFTPGo {{.Version}}</span>
<span>
<img alt="Logo" src="{{.StaticURL}}{{.Branding.LogoPath}}" class="h-30px" />
</span>
</div>
<div class="app-navbar flex-lg-grow-1" id="kt_app_header_navbar">
<div class="app-navbar-item d-flex align-items-center flex-lg-grow-1 me-2 me-lg-0">
</div>
<!-- <div class="d-flex align-self-center flex-center flex-shrink-0">
</div> -->
<div class="d-flex align-self-center flex-center flex-shrink-0">
{{- block "additionalnavitems" .}}{{- end}}
{{- if ne .CurrentURL .EditURL }}
<div class="d-flex align-items-center ms-2 ms-lg-3">
<a href="#" class="btn btn-icon btn-active-light-primary w-35px h-35px w-md-40px h-md-40px" data-kt-menu-trigger="{default:'click', lg: 'hover'}" data-kt-menu-attach="parent" data-kt-menu-placement="bottom-end">
<i class="ki-duotone ki-night-day theme-light-show fs-2">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
<span class="path6"></span>
<span class="path7"></span>
<span class="path8"></span>
<span class="path9"></span>
<span class="path10"></span>
</i>
<i class="ki-duotone ki-moon theme-dark-show fs-2">
<span class="path1"></span>
<span class="path2"></span>
</i>
</a>
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-title-gray-700 menu-icon-gray-500 menu-active-bg menu-state-color fw-semibold py-4 fs-base w-150px" data-kt-menu="true" data-kt-element="theme-mode-menu">
<div class="menu-item px-3 my-0">
<a href="#" class="menu-link px-3 py-2" data-kt-element="mode" data-kt-value="light">
<span class="menu-icon" data-kt-element="icon">
<i class="ki-duotone ki-night-day fs-2">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
<span class="path6"></span>
<span class="path7"></span>
<span class="path8"></span>
<span class="path9"></span>
<span class="path10"></span>
</i>
</span>
<span class="menu-title">Light</span>
</a>
</div>
<div class="menu-item px-3 my-0">
<a href="#" class="menu-link px-3 py-2" data-kt-element="mode" data-kt-value="dark">
<span class="menu-icon" data-kt-element="icon">
<i class="ki-duotone ki-moon fs-2">
<span class="path1"></span>
<span class="path2"></span>
</i>
</span>
<span class="menu-title">Dark</span>
</a>
</div>
<div class="menu-item px-3 my-0">
<a href="#" class="menu-link px-3 py-2" data-kt-element="mode" data-kt-value="system">
<span class="menu-icon" data-kt-element="icon">
<i class="ki-duotone ki-screen fs-2">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
</i>
</span>
<span class="menu-title">System</span>
</a>
</div>
</div>
</div>
{{- end}}
<div class="d-flex align-items-center ms-2 ms-lg-3">
<div class="btn btn-icon btn-active-light-primary w-35px h-35px w-md-40px h-md-40px" data-kt-menu-trigger="{default:'click', lg: 'hover'}" data-kt-menu-attach="parent" data-kt-menu-placement="bottom-end">
<i class="ki-duotone ki-user fs-2">
<i class="path1"></i>
<i class="path2"></i>
</i>
</div>
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-title-gray-700 menu-icon-gray-500 menu-active-bg menu-state-color fw-semibold py-4 w-250px" data-kt-menu="true">
<div class="menu-item px-3 my-0">
<div class="menu-content d-flex align-items-center px-3 py-2">
<span class="text-gray-700">User: {{.LoggedUser.Username}}</span>
</div>
</div>
<div class="separator my-2"></div>
<div class="menu-item px-3 my-0">
<a href="{{.ProfileURL}}" class="menu-link px-3 py-2">
<span class="menu-title">Profile</span>
</a>
</div>
{{- if .LoggedUser.CanChangePassword}}
<div class="menu-item px-3 my-0">
<a href="{{.ChangePwdURL}}" class="menu-link px-3 py-2">
<span class="menu-title">Change password</span>
</a>
</div>
{{- end}}
<div class="menu-item px-3 my-0">
<a href="#" class="menu-link px-3 py-2" onclick="doLogout();">
<span class="menu-title">Sign out</span>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</footer>
<!-- End of Footer -->
{{end}}
{{- end}}
<div class="{{- if .LoggedUser.Username}}app-wrapper{{- end}} flex-column flex-row-fluid " id="kt_app_wrapper">
{{- if .LoggedUser.Username}}
<!-- <div id="kt_app_toolbar" class="app-toolbar">
<div id="kt_app_toolbar_container" class="app-container container-fluid d-flex flex-lg-column py-3 py-lg-6 ">
</div>
<!-- End of Content Wrapper -->
</div>
<!-- End of Page Wrapper -->
<!-- Scroll to Top Button-->
<a class="scroll-to-top rounded" href="#page-top">
<i class="fas fa-angle-up"></i>
</a>
{{if .LoggedUser.Username}}
<!-- Logout Modal-->
<div class="modal fade" id="logoutModal" tabindex="-1" role="dialog" aria-labelledby="modalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalLabel">Ready to Leave?</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">Select "Logout" below if you are ready to end your current session.</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
<a class="btn btn-primary" href="{{.LogoutURL}}">Logout</a>
</div>
</div> -->
<div id="kt_app_sidebar" class="app-sidebar flex-column" data-kt-drawer="true" data-kt-drawer-name="app-sidebar" data-kt-drawer-activate="{default: true, lg: false}" data-kt-drawer-overlay="true" data-kt-drawer-width="300px" data-kt-drawer-direction="start" data-kt-drawer-toggle="#kt_app_sidebar_mobile_toggle">
<div class="app-sidebar-header flex-column mx-10 pt-8" id="kt_app_sidebar_header">
<div class="d-flex flex-stack d-none d-lg-flex mb-13">
<div class="app-sidebar-logo">
<img alt="Logo" src="{{.StaticURL}}{{.Branding.LogoPath}}" class="h-40px app-sidebar-logo-default" />
<span class="text-sidebar fs-4 fw-semibold ps-5">{{.Branding.ShortName}}</span>
</div>
</div>
</div>
<div class="app-sidebar-navs flex-column-fluid pb-6" id="kt_app_sidebar_navs">
<div id="kt_app_sidebar_navs_wrappers" class="hover-scroll-y my-2 mx-4" data-kt-scroll="true" data-kt-scroll-activate="true" data-kt-scroll-height="auto" data-kt-scroll-dependencies="#kt_app_sidebar_header" data-kt-scroll-wrappers="#kt_app_sidebar_navs" data-kt-scroll-offset="5px">
<div id="#kt_app_sidebar_menu" data-kt-menu="true" data-kt-menu-expand="false" class="menu menu-column menu-rounded menu-sub-indention menu-active-bg mb-7">
<div class="menu-item">
<a class="menu-link {{- if eq .CurrentURL .FilesURL}} active{{- end}}" href="{{.FilesURL}}">
<span class="menu-icon">
<i class="ki-duotone ki-folder fs-1">
<span class="path1"></span>
<span class="path2"></span>
</i>
</span>
<span class="menu-title">{{.FilesTitle}}</span>
</a>
</div>
{{- if .LoggedUser.CanManageShares}}
<div class="menu-item">
<a class="menu-link {{- if eq .CurrentURL .SharesURL}} active{{- end}}" href="{{.SharesURL}}">
<span class="menu-icon">
<i class="ki-duotone ki-share fs-1">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
<span class="path6"></span>
</i>
</span>
<span class="menu-title">{{.SharesTitle}}</span>
</a>
</div>
{{- end}}
{{- if .LoggedUser.CanManageMFA}}
<div class="menu-item">
<a class="menu-link {{- if eq .CurrentURL .MFAURL}} active{{- end}}" href="{{.MFAURL}}">
<span class="menu-icon">
<i class="ki-duotone ki-shield fs-1">
<span class="path1"></span>
<span class="path2"></span>
</i>
</span>
<span class="menu-title">{{.MFATitle}}</span>
</a>
</div>
{{- end}}
</div>
</div>
</div>
</div>
{{- end}}
<div class="app-main flex-column flex-row-fluid " id="kt_app_main">
<div class="d-flex flex-column flex-column-fluid">
<div id="kt_app_content" class="app-content flex-column-fluid">
<div id="kt_app_content_container" class="app-container container-fluid">
{{template "page_body" .}}
</div>
</div>
</div>
{{- if .LoggedUser.Username}}
<div id="kt_app_footer" class="app-footer">
<div class="app-container container-fluid d-flex flex-column flex-md-row flex-center flex-md-stack py-3 align-items-center justify-content-center">
<div class="text-dark order-2 order-md-1">
<span class="text-gray-700 fw-semibold me-1">SFTPGo {{.Version}}</span>
</div>
</div>
</div>
{{- end}}
</div>
</div>
</div>
</div>
</div>
{{end}}
<div id="kt_scrolltop" class="scrolltop" data-kt-scrolltop="true">
<i class="ki-duotone ki-arrow-up">
<span class="path1"></span>
<span class="path2"></span>
</i>
</div>
<div class="page-loader flex-column">
<span class="spinner-border text-primary" role="status"></span>
<span id="loading_message" class="text-muted fs-4 fw-semibold mt-5"></span>
</div>
{{block "dialog" .}}{{end}}
<div class="modal fade" tabindex="-1" id="modal_alert">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-body">
<div class="d-flex flex-center flex-column">
<span id="modal_alert_icon">
</span>
<div class="mt-10 text-center">
<span id="modal_alert_text" class="fs-6 text-dark fw-semibold"></span>
</div>
</div>
</div>
<div class="modal-footer border-0 justify-content-center">
<button id="modal_alert_cancel" type="button" class="btn btn-secondary m-2"></button>
<button id="modal_alert_ok" type="button" class="btn btn-primary m-2"></button>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript-->
<script src="{{.StaticURL}}/vendor/jquery/jquery.min.js"></script>
<script src="{{.StaticURL}}/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
{{- block "modals" .}}{{- end}}
<script src="{{.StaticURL}}/assets/plugins/global/plugins.bundle.js"></script>
<script src="{{.StaticURL}}/assets/js/scripts.bundle.js"></script>
{{- template "basejs" }}
<script type="text/javascript">
var ModalAlert = function () {
var modal;
var promiseResolve;
var isResolved;
<!-- Core plugin JavaScript-->
<script src="{{.StaticURL}}/vendor/jquery-easing/jquery.easing.min.js"></script>
function resolvePromise(result) {
if (!isResolved) {
isResolved = true;
promiseResolve({
isConfirmed: result
});
}
}
<!-- Custom scripts for all pages-->
<script src="{{.StaticURL}}/js/sb-admin-2.min.js"></script>
return {
fire: function (params) {
if (!modal){
modal = new bootstrap.Modal('#modal_alert');
<script type="text/javascript">
function escapeHTML(str) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
}
modal._element.addEventListener('hide.bs.modal',function() {
resolvePromise(false);
});
function unescapeHTML(escapedStr) {
var div = document.createElement('div');
div.innerHTML = escapedStr;
var child = div.childNodes[0];
return child ? child.nodeValue : '';
}
$('#modal_alert_cancel').on("click", function(){
resolvePromise(false);
modal.hide();
});
function escapeHTMLForceSafe(str) {
return str
.replace(/&/g, '_')
.replace(/</g, '_')
.replace(/>/g, '_')
.replace(/\"/g, '_')
.replace(/\'/g, '_');
}
$('#modal_alert_ok').on("click", function(){
resolvePromise(true);
modal.hide();
});
}
function fixedEncodeURIComponent(str) {
return encodeURIComponent(unescapeHTML(str)).replace(/[!'()*]/g, function (c) {
return '%' + c.charCodeAt(0).toString(16);
});
}
let okBtn = $("#modal_alert_ok");
let cancelBtn = $("#modal_alert_cancel");
function replaceSlash(str){
return str.replace(/\//g,'\u2215');
}
okBtn.removeClass();
okBtn.addClass(params.customClass.confirmButton);
okBtn.addClass("m-2");
okBtn.text(params.confirmButtonText);
if (params.cancelButtonText){
cancelBtn.text(params.cancelButtonText);
cancelBtn.removeClass();
cancelBtn.addClass(params.customClass.cancelButton);
cancelBtn.addClass("m-2");
} else {
cancelBtn.addClass("d-none");
}
function b64EncodeUnicode(str) {
return btoa(encodeURIComponent(str));
}
$("#modal_alert_text").text(params.text);
function UnicodeDecodeB64(str) {
return decodeURIComponent(atob(str));
}
switch (params.icon){
case "warning":
$("#modal_alert_icon").html(`<i class="ki-duotone ki-information fs-5x text-danger">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
</i>`);
break;
case "success":
$("#modal_alert_icon").html(`<i class="ki-duotone ki-information-2 fs-5x text-success">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
</i>`);
break;
default:
$("#modal_alert_icon").html(`<i class="ki-duotone ki-question-2 fs-5x text-primary">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
</i>`);
}
function fileSizeIEC(a,b,c,d,e){
return (b=Math,c=b.log,d=1024,e=c(a)/c(d)|0,a/b.pow(d,e)).toFixed(1)
+' '+(e?'KMGTPEZY'[--e]+'iB':'Bytes')
}
</script>
return new Promise(function(resolve, reject) {
promiseResolve = resolve;
isResolved = false;
modal.show();
});
}
}
}();
<!-- Page level plugins -->
{{block "extra_js" .}}{{end}}
</body>
function doLogout() {
ModalAlert.fire({
text: `Are you sure you want to sign out?`,
icon: "question",
confirmButtonText: "Sign out",
cancelButtonText: 'Cancel',
customClass: {
confirmButton: "btn btn-primary",
cancelButton: 'btn btn-secondary'
}
}).then((result) =>{
if (result.isConfirmed){
window.location.replace('{{.LogoutURL}}');
}
});
}
</script>
{{- block "extra_js" .}}{{- end}}
</body>
</html>
{{end}}
{{- end}}

View File

@@ -1,91 +1,60 @@
<!--
Copyright (C) 2019-2023 Nicola Murino
Copyright (C) 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 WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
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.
https://keenthemes.com/products/templates-mega-bundle
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/>.
KeenThemes HTML/CSS/JS components are allowed for use only within the
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).
-->
{{define "baselogin"}}
{{- define "baselogin"}}
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{.Branding.Name}} - {{template "title" .}}</title>
<meta charset="utf-8" />
<meta name="description" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" href="{{.StaticURL}}{{.Branding.FaviconPath}}" />
{{- template "fonts" .StaticURL }}
<link href="{{.StaticURL}}/assets/plugins/global/plugins.bundle.css" rel="stylesheet" type="text/css" />
<link href="{{.StaticURL}}/assets/css/style.bundle.css" rel="stylesheet" type="text/css" />
{{- template "globalstyle" }}
{{- range .Branding.ExtraCSS}}
<link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
{{- end}}
{{- template "commonjs" }}
</head>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>{{.Branding.Name}} - {{template "title" .}}</title>
<link rel="shortcut icon" href="{{.StaticURL}}{{.Branding.FaviconPath}}" />
<!-- Custom styles for this template-->
<link href="{{.StaticURL}}{{.Branding.DefaultCSS}}" rel="stylesheet">
<style>
{{template "commoncss" .}}
</style>
{{range .Branding.ExtraCSS}}
<link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
{{end}}
</head>
<body class="bg-gradient-primary">
<div class="container">
<!-- Outer Row -->
<div class="row justify-content-center">
<div class="col-xl-10 col-lg-12 col-md-9">
<div class="card o-hidden border-0 shadow-lg my-5">
<div class="card-body p-0">
<!-- Nested Row within Card Body -->
<div class="row d-lg-none login-image">
<div class="col-lg-12 d-block d-lg-none bg-login-image">
</div>
</div>
<div class="row">
<div class="col-lg-5 d-none d-lg-block bg-login-image">
</div>
<div class="col-lg-7">
<div class="p-5">
<div class="text-center">
<h1 class="h4 text-gray-900 mb-4">{{.Branding.ShortName}} - {{.Version}}</h1>
</div>
{{template "content" .}}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript-->
<script src="{{.StaticURL}}/vendor/jquery/jquery.min.js"></script>
<script src="{{.StaticURL}}/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- Core plugin JavaScript-->
<script src="{{.StaticURL}}/vendor/jquery-easing/jquery.easing.min.js"></script>
<!-- Custom scripts for all pages-->
<script src="{{.StaticURL}}/js/sb-admin-2.min.js"></script>
</body>
<body class="app-blank">
{{- template "theme-setup"}}
<div class="d-flex flex-column flex-root">
<div class="d-flex flex-column flex-column-fluid bgi-position-y-bottom position-x-center bgi-no-repeat bgi-size-contain bgi-attachment-fixed">
<div class="d-flex flex-center flex-column flex-column-fluid p-10 pb-lg-20">
<div class="w-lg-500px w-md-450px w-sm-400px bg-body rounded shadow-sm p-10 p-lg-15 mx-auto">
{{template "content" .}}
</div>
</div>
</div>
</div>
<script src="{{.StaticURL}}/assets/plugins/global/plugins.bundle.js"></script>
<script src="{{.StaticURL}}/assets/js/scripts.bundle.js"></script>
<script type="text/javascript">
KTUtil.onDOMContentLoaded(function () {
$('#sign_in_form').submit(function (event) {
let submitButton = document.querySelector('#sign_in_submit');
submitButton.setAttribute('data-kt-indicator', 'on');
submitButton.disabled = true;
return true;
});
});
</script>
</body>
</html>
{{end}}
{{- end}}

View File

@@ -1,62 +1,75 @@
<!--
Copyright (C) 2019-2023 Nicola Murino
Copyright (C) 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 WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
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.
https://keenthemes.com/products/templates-mega-bundle
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/>.
KeenThemes HTML/CSS/JS components are allowed for use only within the
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" .}}
{{define "title"}}{{.Title}}{{end}}
{{- define "title"}}{{.Title}}{{- 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">Change password</h6>
{{- define "page_body"}}
<div class="card shadow-sm">
<div class="card-header bg-light">
<h3 class="card-title text-primary">Change password</h3>
</div>
<div class="card-body">
{{if .Error}}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
{{.Error}}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{{end}}
<form id="user_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
{{- template "errmsg" .Error}}
<form id="change_pwd_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
<div class="form-group row">
<label for="idCurrentPassword" class="col-sm-2 col-form-label">Current password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idCurrentPassword" name="current_password" autocomplete="new-password" spellcheck="false" required>
<label class="col-md-3 col-form-label required">Current password</label>
<div class="col-md-9">
<input type="password" class="form-control" placeholder="" name="current_password"
spellcheck="false" required />
</div>
</div>
<div class="form-group row">
<label for="idNewPassword1" class="col-sm-2 col-form-label">New password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idNewPassword1" name="new_password1" autocomplete="new-password" spellcheck="false" required>
<div class="form-group row mt-10">
<label class="col-md-3 col-form-label required">New password</label>
<div class="col-md-9">
<input type="password" class="form-control" placeholder="" name="new_password1"
autocomplete="new-password" spellcheck="false" required />
</div>
</div>
<div class="form-group row">
<label for="idNewPassword2" class="col-sm-2 col-form-label">Confirm password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idNewPassword2" name="new_password2" autocomplete="new-password" spellcheck="false" required>
<div class="form-group row mt-10">
<label class="col-md-3 col-form-label required">Confirm password</label>
<div class="col-md-9">
<input type="password" class="form-control" placeholder="" name="new_password2"
autocomplete="new-password" spellcheck="false" required />
</div>
</div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5">Change my password</button>
<div class="d-flex justify-content-end mt-12">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="form_submit" class="btn btn-primary px-10">
<span class="indicator-label">
Submit
</span>
<span class="indicator-progress">
Please wait... <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span>
</button>
</div>
</form>
</div>
</div>
{{end}}
{{end}}
{{- define "extra_js"}}
<script type="text/javascript">
KTUtil.onDOMContentLoaded(function () {
$('#change_pwd_form').submit(function (event) {
let submitButton = document.querySelector('#form_submit');
submitButton.setAttribute('data-kt-indicator', 'on');
submitButton.disabled = true;
});
});
</script>
{{- end}}

View File

@@ -1,209 +1,164 @@
<!--
Copyright (C) 2019-2023 Nicola Murino
Copyright (C) 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 WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
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.
https://keenthemes.com/products/templates-mega-bundle
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/>.
KeenThemes HTML/CSS/JS components are allowed for use only within the
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" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "extra_css"}}
<link href="{{.StaticURL}}/vendor/codemirror/codemirror.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/codemirror/addon/dialog/dialog.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/codemirror/addon/search/matchesonscrollbar.css" rel="stylesheet">
{{- define "extra_css"}}
<style>
.CodeMirror {
border: 1px solid #eee;
height: 30em;
font-size: 13px;
}
.shortcut {font-family: monospace; color: #666;}
</style>
{{end}}
{{define "additionalnavitems"}}
<li class="nav-item dropdown no-arrow mx-1">
<a class="nav-link dropdown-toggle" href="#" id="infoDropdown" role="button"
data-toggle="modal" data-target="#infoModal">
<i class="fas fa-info fa-fw"></i>
</a>
</li>
<div class="topbar-divider d-none d-sm-block"></div>
{{end}}
{{define "page_body"}}
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span>
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span>
</button>
</div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
.cm-editor {
height: 100%;
width: 100%;
}
</script>
<div class="card shadow mb-4">
<div class="card-header">
<h6 class="d-flex justify-content-between align-items-center">
<span class="font-weight-bold text-primary">{{- if .ReadOnly}}View{{- else}}Edit{{- end}} file "{{.Path}}"</span>
<span class="btn-toolbar">
<a id="idBack" class="btn btn-secondary mx-1 my-1" href='{{.FilesURL}}?path={{.CurrentDir}}' role="button">Back</a>
{{if not .ReadOnly}}
<a id="idSave" class="btn btn-primary mx-1 my-1" href="#" onclick="saveFile()" role="button">Save</a>
{{end}}
</span>
</h6>
</style>
{{- end}}
{{- define "additionalnavitems"}}
<div class="d-flex align-items-center ms-2 ms-lg-3" id="kt_header_user_menu_toggle">
<div class="btn btn-icon btn-active-light-primary w-35px h-35px w-md-40px h-md-40px" data-bs-toggle="modal" data-bs-target="#info_modal">
<i class="ki-duotone ki-information-2 fs-2">
<i class="path1"></i>
<i class="path2"></i>
<i class="path3"></i>
</i>
</div>
<div class="card-body">
<div class="col-sm-12">
<textarea id="editor" name="editor"></textarea>
</div>
{{- end}}
{{- define "page_body"}}
{{- template "errmsg" ""}}
<div class="card shadow-sm">
<div class="card-header">
{{- if .ReadOnly}}
<h6 class="card-title">View file "{{.Path}}"</h6>
{{- else}}
<h6 class="card-title">Edit file "{{.Path}}"</h6>
{{- end}}
<div class="card-toolbar">
<a class="btn btn-light-primary px-10 me-5" href='{{.FilesURL}}?path={{.CurrentDir}}' role="button">Back</a>
{{- if not .ReadOnly}}
<a id="save_button" type="button" class="btn btn-primary px-10" href="#" role="button" onclick="saveFile();">
<span class="indicator-label">Save</span>
<span class="indicator-progress">Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span>
</a>
{{- end}}
</div>
</div>
<div class="card-body">
<div id="editor" class="col-sm-12 border border-light-primary"></div>
</div>
</div>
{{end}}
{{- end}}
{{define "dialog"}}
<div class="modal fade" id="infoModal" tabindex="-1" role="dialog" aria-labelledby="infoModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
{{- define "modals"}}
<div class="modal fade" id="info_modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="infoModalLabel">
Editor keybindings
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h3 class="modal-title">Editor keybindings</h3>
<div class="btn btn-icon btn-sm btn-active-light-primary ms-2" data-bs-dismiss="modal" aria-label="Close">
<i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i>
</div>
</div>
<div class="modal-body">
<p>
<span class="shortcut">Ctrl-F / Cmd-F</span> => Start searching
</p>
<p>
<span class="shortcut">Ctrl-G / Cmd-G</span> => Find next
</p>
<p>
<span class="shortcut">Shift-Ctrl-G / Shift-Cmd-G</span> => Find previous
</p>
<p>
<span class="shortcut">Shift-Ctrl-F / Cmd-Option-F</span> => Replace
</p>
<p>
<span class="shortcut">Shift-Ctrl-R / Shift-Cmd-Option-F</span> => Replace all
<span class="shortcut">Ctrl-F / Cmd-F</span> => Open search panel
</p>
<p>
<span class="shortcut">Alt-G</span> => Jump to line
</p>
<p>
<span class="shortcut">Alt-F</span> => Persistent search: enter to find next, Shift-Enter to find previous
<span class="shortcut">Tab</span> => Indent more
</p>
<p>
<span class="shortcut">Shift-Tab</span> => Indent less
</p>
</div>
<div class="modal-footer">
<button class="btn btn-primary" type="button" data-dismiss="modal">OK</button>
<button class="btn btn-primary" type="button" data-bs-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>
{{end}}
{{- end}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/codemirror/codemirror.js"></script>
<script src="{{.StaticURL}}/vendor/codemirror/meta.js"></script>
<script src="{{.StaticURL}}/vendor/codemirror/addon/selection/active-line.js"></script>
<script src="{{.StaticURL}}/vendor/codemirror/addon/dialog/dialog.js"></script>
<script src="{{.StaticURL}}/vendor/codemirror/addon/search/searchcursor.js"></script>
<script src="{{.StaticURL}}/vendor/codemirror/addon/search/search.js"></script>
<script src="{{.StaticURL}}/vendor/codemirror/addon/scroll/annotatescrollbar.js"></script>
<script src="{{.StaticURL}}/vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
<script src="{{.StaticURL}}/vendor/codemirror/addon/search/jump-to-line.js"></script>
{{- define "extra_js"}}
<script src="{{.StaticURL}}/vendor/codemirror/cm6.bundle.min.js"></script>
<script type="text/javascript">
$(document).ready(function () {
var cm = CodeMirror.fromTextArea(document.getElementById("editor"), {
lineNumbers: true,
styleActiveLine: true,
extraKeys: {"Alt-F": "findPersistent"},
{{if .ReadOnly}}
readOnly: true,
{{end}}
autofocus: true
});
var filename = "{{.Path}}";
var extension = filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
mode = CodeMirror.findModeByExtension(extension);
if (mode != null) {
cm.setOption("mode", mode.mode);
}
cm.setValue("{{.Data}}");
setInterval(keepAlive, 300000);
});
var cmView;
function keepAlive() {
$.ajax({
url: '{{.ProfileURL}}',
axios.get('{{.ProfileURL}}',{
timeout: 15000
});
}).catch(function (error){});
}
{{if not .ReadOnly}}
//{{- if not .ReadOnly}}
function saveFile() {
$('#idSave').addClass("disabled");
$('#errorMsg').hide();
$('#errorMsg').addClass("d-none");
let saveButton = document.querySelector('#save_button');
saveButton.setAttribute('data-kt-indicator', 'on');
saveButton.disabled = true;
async function uploadFile() {
var errorMessage = "Error saving file";
let response;
try {
var uploadPath = '{{.FileURL}}?path='+encodeURIComponent('{{.CurrentDir}}/{{.Name}}');
var cm = document.querySelector('.CodeMirror').CodeMirror;
var blob = new Blob([cm.getValue()]);
response = await fetch(uploadPath, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
credentials: 'same-origin',
redirect: 'error',
body: blob
});
if (response.status == 201){
window.location.href = '{{.FilesURL}}?path='+encodeURIComponent('{{.CurrentDir}}');
} else {
let jsonResponse;
try {
jsonResponse = await response.json();
} catch(e){
throw Error(errorMessage);
}
if (jsonResponse.message) {
errorMessage = jsonResponse.message;
}
if (jsonResponse.error) {
errorMessage += ": " + jsonResponse.error;
}
throw Error(errorMessage);
let uploadPath = '{{.FileURL}}?path='+encodeURIComponent('{{.CurrentDir}}/{{.Name}}');
let blob = new Blob([cmView.state.doc.toString()]);
axios.post(uploadPath, blob, {
headers: {
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
timeout: 600000,
validateStatus: function (status) {
return status == 201;
}
}).then(function (response) {
window.location.replace('{{.FilesURL}}?path='+encodeURIComponent('{{.CurrentDir}}'));
}).catch(function (error) {
saveButton.removeAttribute('data-kt-indicator');
saveButton.disabled = false;
let errorMessage = "Error saving file";
if (error && error.response) {
if (error.response.data.message) {
errorMessage = error.response.data.message;
}
if (error.response.data.error) {
errorMessage += ": " + error.response.data.error;
}
} catch (e){
throw Error(errorMessage+": " +e.message);
}
}
uploadFile().catch(function(error){
$('#idSave').removeClass("disabled");
$('#errorTxt').text(error.message);
$('#errorMsg').show();
$('#errorTxt').text(errorMessage);
$('#errorMsg').removeClass("d-none");
});
}
{{end}}
//{{- end}}
KTUtil.onDOMContentLoaded(function () {
let options = {
oneDark: KTThemeMode.getMode() == "dark"
};
//{{- if .ReadOnly}}
options.readOnly = true;
//{{- end}}
cmView = cm6.createEditorView(undefined, document.getElementById("editor"));
cmView.setState(cm6.createEditorState("{{.Data}}", options));
setInterval(keepAlive, 300000);
});
</script>
{{end}}
{{- end}}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
<!--
Copyright (C) 2023 Nicola Murino
This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
https://keenthemes.com/products/templates-mega-bundle
KeenThemes HTML/CSS/JS components are allowed for use only within the
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 "baselogin" .}}
{{- define "title"}}Forgot password{{- end}}
{{- define "content"}}
<form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST">
<div class="container mb-10">
<div class="row align-items-center">
<div class="col-5 align-items-center">
<a href="{{.LoginURL}}">
<img alt="Logo" src="{{.StaticURL}}{{.Branding.LogoPath}}" class="h-80px h-md-90px h-lg-100px" />
</a>
</div>
<div class="col-7">
<a href="{{.LoginURL}}" class="text-dark mb-3 ms-3 fs-1 fw-bold">
{{.Branding.ShortName}}
</a>
</div>
</div>
</div>
<div class="text-center mb-10">
<h2 class="text-dark mb-3">
Forgot Password ?
</h2>
<div class="text-gray-600 fw-semibold fs-4">
Enter your account username below, you will receive a password reset code by email.
</div>
</div>
{{template "errmsg" .Error}}
<div class="fv-row mb-10">
<input class="form-control form-control-lg form-control-solid" type="text" placeholder="Your username" name="username" spellcheck="false" required />
</div>
<div class="text-center">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5">
<span class="indicator-label">Send Reset Code</span>
<span class="indicator-progress">Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span>
</button>
</div>
</form>
{{- end}}

View File

@@ -1,69 +1,79 @@
<!--
Copyright (C) 2019-2023 Nicola Murino
Copyright (C) 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 WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
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.
https://keenthemes.com/products/templates-mega-bundle
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/>.
KeenThemes HTML/CSS/JS components are allowed for use only within the
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 "baselogin" .}}
{{- template "baselogin" .}}
{{define "title"}}Login{{end}}
{{- define "title"}}Login{{- end}}
{{define "content"}}
{{if .Error}}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
{{.Error}}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{{end}}
<form id="login_form" action="{{.CurrentURL}}" method="POST"
class="user-custom">
{{if not .FormDisabled}}
<div class="form-group">
<input type="text" class="form-control form-control-user-custom"
id="inputUsername" name="username" placeholder="Username" spellcheck="false" required>
</div>
<div class="form-group">
<input type="password" class="form-control form-control-user-custom"
id="inputPassword" name="password" placeholder="Password" autocomplete="current-password" spellcheck="false" required>
{{if .ForgotPwdURL}}
<div class="text-right">
<a class="small" href="{{.ForgotPwdURL}}">Forgot password?</a>
</div>
{{end}}
</div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary btn-user-custom btn-block">
Login
</button>
{{end}}
{{if .OpenIDLoginURL}}
<hr>
<a href="{{.OpenIDLoginURL}}" class="btn btn-secondary btn-user-custom btn-block">
Login with OpenID
</a>
{{end}}
</form>
{{if .AltLoginURL}}
<hr>
<div class="text-center">
<a class="small" href="{{.AltLoginURL}}">{{.AltLoginName}}</a>
</div>
{{end}}
{{if and .Branding.DisclaimerName .Branding.DisclaimerPath}}
<hr>
<div class="text-center">
<a class="small" href="{{.StaticURL}}{{.Branding.DisclaimerPath}}" target="_blank">{{.Branding.DisclaimerName}}</a>
</div>
{{end}}
{{end}}
{{- define "content"}}
<form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST">
<div class="container mb-10">
<div class="row align-items-center">
<div class="col-5 align-items-center">
<img alt="Logo" src="{{.StaticURL}}{{.Branding.LogoPath}}" class="h-80px h-md-90px h-lg-100px" />
</div>
<div class="col-7">
<h1 class="text-dark mb-3 ms-3">
{{.Branding.ShortName}}
</h1>
</div>
</div>
</div>
{{template "errmsg" .Error}}
{{- if not .FormDisabled}}
<div class="fv-row mb-10">
<input class="form-control form-control-lg form-control-solid" type="text" name="username" placeholder="Username" spellcheck="false" required />
</div>
<div class="fv-row mb-10">
<input class="form-control form-control-lg form-control-solid" type="password" name="password" placeholder="Password" autocomplete="current-password" spellcheck="false" required />
<div class="d-flex justify-content-end mt-2">
{{- if .ForgotPwdURL}}
<a href="{{.ForgotPwdURL}}" class="link-primary fs-6 fw-bold">Forgot Password ?</a>
{{- end}}
</div>
</div>
{{- end}}
<div class="text-center">
{{- if not .FormDisabled}}
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5">
<span class="indicator-label">Sign in</span>
<span class="indicator-progress">Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span>
</button>
{{- end}}
{{- if .OpenIDLoginURL}}
{{- if not .FormDisabled}}
<div class="text-center text-muted text-uppercase fw-bold mb-5">or</div>
{{- end}}
<a href="{{.OpenIDLoginURL}}" class="btn btn-flex flex-center btn-light btn-lg w-100 mb-5">
<img alt="Logo" src="{{.StaticURL}}/img/openid-logo.png" class="h-20px me-3" />Sign in with OpenID</a>
{{- end}}
</div>
</form>
{{- if or .AltLoginURL (and .Branding.DisclaimerName .Branding.DisclaimerPath) }}
<div class="d-flex flex-center flex-column-auto pt-10 mt-5">
<div class="d-flex align-items-center fw-semibold fs-6">
{{- if .AltLoginURL}}
<a href="{{.AltLoginURL}}" class="text-muted text-hover-primary px-2">{{.AltLoginName}}</a>
{{- end}}
{{- if and .Branding.DisclaimerName .Branding.DisclaimerPath}}
<a href="{{.StaticURL}}{{.Branding.DisclaimerPath}}" target="_blank" class="text-muted text-hover-primary px-2">{{.Branding.DisclaimerName}}</a>
{{- end}}
</div>
</div>
{{- end}}
{{- end}}

View File

@@ -1,74 +1,82 @@
<!--
Copyright (C) 2019-2023 Nicola Murino
Copyright (C) 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 WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
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.
https://keenthemes.com/products/templates-mega-bundle
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/>.
KeenThemes HTML/CSS/JS components are allowed for use only within the
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" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "page_body"}}
{{if .LoggedUser.Username}}
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">{{.Title}}</h6>
{{- define "title"}}{{.Title}}{{- end}}
{{- define "page_body"}}
<div class="d-flex flex-center flex-column flex-column-fluid p-10 pb-lg-20">
{{- if not .LoggedUser.Username}}
{{- if .LoginURL}}
<div class="mb-12">
<a href="{{.LoginURL}}">
<img alt="Logo" src="{{.StaticURL}}{{.Branding.LogoPath}}" class="h-80px h-md-90px h-lg-100px" />
</a>
<a href="{{.LoginURL}}" class="text-dark fs-1 fw-bold ms-3 ps-5">
{{.Branding.ShortName}}
</a>
</div>
<div class="card-body">
{{if .Error}}
<div class="card mb-2 border-left-warning">
<div class="card-body text-form-error">{{.Error}}</div>
</div>
{{end}}
{{if .Success}}
<div class="card mb-2 border-left-success">
<div class="card-body">{{.Success}}</div>
</div>
{{end}}
{{- else}}
<div class="mb-12">
<span>
<img alt="Logo" src="{{.StaticURL}}{{.Branding.LogoPath}}" class="h-80px h-md-90px h-lg-100px" />
</span>
<span class="text-dark fs-1 fw-bold ms-3 ps-5">
{{.Branding.ShortName}}
</span>
</div>
</div>
{{else}}
<div class="row justify-content-center">
<div class="col-xl-8 col-lg-9 col-md-9">
<div class="card o-hidden border-0 shadow-lg my-5">
<div class="card-body p-0">
<div class="row justify-content-center">
<div class="col-lg-9">
<div class="p-5">
<div class="text-center">
<h1 class="h4 text-gray-900 mb-4">{{.Title}}</h1>
</div>
{{if .Error}}
<div class="card mb-4 border-left-warning">
<div class="card-body text-form-error">{{.Error}}</div>
</div>
{{end}}
{{if .Success}}
<div class="card mb-4 border-left-success">
<div class="card-body">{{.Success}}</div>
</div>
{{end}}
{{- end}}
{{- end}}
<div class="card shadow-sm w-lg-600px">
<div class="card-header bg-light">
<h3 class="card-title text-primary">{{.Title}}</h3>
</div>
<div class="card-body">
{{- if .Error}}
<div class="notice d-flex bg-light-warning rounded border-warning border border-dashed p-6">
<i class="ki-duotone ki-information 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-5 text-gray-800">
{{.Error}}
</div>
</div>
</div>
</div>
{{- end}}
{{- if .Success}}
<div class="notice d-flex bg-light-primary rounded border-primary border border-dashed p-6">
<i class="ki-duotone ki-information-2 fs-2tx text-primary 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-5 text-gray-800">
{{.Success}}
</div>
</div>
</div>
</div>
{{- end}}
</div>
</div>
</div>
{{end}}
{{end}}
{{- end}}

File diff suppressed because it is too large Load Diff

View File

@@ -1,146 +1,166 @@
<!--
Copyright (C) 2019-2023 Nicola Murino
Copyright (C) 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 WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
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.
https://keenthemes.com/products/templates-mega-bundle
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/>.
KeenThemes HTML/CSS/JS components are allowed for use only within the
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" .}}
{{define "title"}}{{.Title}}{{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">My profile - {{.LoggedUser.Username}}</h6>
{{- define "page_body"}}
<div class="card shadow-sm">
<div class="card-header bg-light">
<h3 class="card-title text-primary">My profile - {{.LoggedUser.Username}}</h3>
</div>
<div class="card-body">
{{if .Error}}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
{{.Error}}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{{end}}
<form id="profile_form" action="{{.CurrentURL}}" method="POST">
{{- template "errmsg" .Error}}
<form id="page_form" action="{{.CurrentURL}}" method="POST">
<div class="form-group row">
<label for="idEmail" class="col-sm-2 col-form-label">Email</label>
<div class="col-sm-10">
<label class="col-md-3 col-form-label">Email</label>
<div class="col-md-9">
<input type="text" class="form-control" id="idEmail" name="email" placeholder="" spellcheck="false"
value="{{.Email}}" maxlength="255" autocomplete="nope" {{if not .LoggedUser.CanChangeInfo}}readonly{{end}}>
</div>
</div>
<div class="form-group row">
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-10">
<div class="form-group row mt-10">
<label class="col-md-3 col-form-label">Description</label>
<div class="col-md-9">
<input type="text" class="form-control" id="idDescription" name="description" placeholder=""
value="{{.Description}}" maxlength="255" {{if not .LoggedUser.CanChangeInfo}}readonly{{end}}>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="idAllowAPIKeyAuth" name="allow_api_key_auth" {{if not .LoggedUser.CanChangeAPIKeyAuth}}disabled="disabled"{{end}}
{{if .AllowAPIKeyAuth}}checked{{end}} aria-describedby="allowAPIKeyAuthHelpBlock">
<label for="idAllowAPIKeyAuth" class="form-check-label">Allow API key authentication</label>
<small id="allowAPIKeyAuthHelpBlock" class="form-text text-muted">
Allow to impersonate yourself, in REST API, with an API key. If this permission is not granted, your credentials are required to use the REST API on your behalf
</small>
<div class="form-group row align-items-center mt-10">
<label class="col-md-3 col-form-label">API key authentication</label>
<div class="col-md-9">
<div class="form-check form-switch form-check-custom form-check-solid">
<input class="form-check-input" type="checkbox" id="idAllowAPIKeyAuth" name="allow_api_key_auth" {{if not .LoggedUser.CanChangeAPIKeyAuth}}disabled="disabled"{{end}} {{if .AllowAPIKeyAuth}}checked="checked"{{end}}/>
<label class="form-check-label fw-semibold text-gray-800" for="idAllowAPIKeyAuth">
Allow to impersonate yourself, in REST API, using an API key
</label>
</div>
</div>
</div>
{{if .LoggedUser.CanManagePublicKeys}}
<div class="card bg-light mb-3">
<div class="card-header">
Public keys
{{- if .LoggedUser.CanManagePublicKeys}}
<div class="card card-rounded mt-10">
<div class="card-header bg-light">
<h3 class="card-title">Public keys</h3>
</div>
<div class="card-body">
<div class="form-group row">
<div class="col-md-12 form_field_pk_outer">
{{range $idx, $val := .PublicKeys}}
<div class="row form_field_pk_outer_row">
<div class="form-group col-md-11">
<textarea class="form-control" id="idPublicKey{{$idx}}" name="public_keys" rows="4"
placeholder="Paste your public key here">{{$val}}</textarea>
<div id="public_keys">
<div class="form-group">
<div data-repeater-list="public_keys">
{{- range $idx, $val := .PublicKeys}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-9">
<textarea class="form-control mt-3 mt-md-8" name="public_key" rows="4"
placeholder="Paste your public key here">{{$val}}</textarea>
</div>
<div class="col-md-3">
<a href="javascript:;" data-repeater-delete
class="btn btn-light-danger mt-3 mt-md-8">
<i class="ki-duotone ki-trash fs-5">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
Delete
</a>
</div>
</div>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_pk_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
{{- else}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-9">
<textarea class="form-control mt-3 mt-md-8" name="public_key" rows="4"
placeholder="Paste your public key here"></textarea>
</div>
<div class="col-md-3">
<a href="javascript:;" data-repeater-delete
class="btn btn-light-danger mt-3 mt-md-8">
<i class="ki-duotone ki-trash fs-5">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
Delete
</a>
</div>
</div>
</div>
{{- end}}
</div>
{{else}}
<div class="row form_field_pk_outer_row">
<div class="form-group col-md-11">
<textarea class="form-control" id="idPublicKey0" name="public_keys" rows="4"
placeholder="Paste your public key here"></textarea>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_pk_btn_frm_field" disabled>
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="row mx-1">
<button type="button" class="btn btn-secondary add_new_pk_field_btn">
<i class="fas fa-plus"></i> Add new public key
</button>
<div class="form-group mt-5">
<a href="javascript:;" data-repeater-create class="btn btn-light-primary">
<i class="ki-duotone ki-plus fs-3"></i>
Add
</a>
</div>
</div>
</div>
</div>
{{end}}
{{if .CanSubmit}}
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5">Submit</button>
{{end}}
{{- end}}
<div class="d-flex justify-content-end mt-12">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="form_submit" class="btn btn-primary px-10">
<span class="indicator-label">
Submit
</span>
<span class="indicator-progress">
Please wait... <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span>
</button>
</div>
</form>
</div>
</div>
{{end}}
{{- end}}
{{define "extra_js"}}
{{if .LoggedUser.CanManagePublicKeys}}
{{- define "extra_js"}}
{{- if .LoggedUser.CanManagePublicKeys}}
<script src="{{.StaticURL}}/assets/plugins/custom/formrepeater/formrepeater.bundle.js"></script>
<script type="text/javascript">
$(document).ready(function () {
$("body").on("click", ".add_new_pk_field_btn", function () {
let index = $(".form_field_pk_outer").find(".form_field_pk_outer_row").length;
while (document.getElementById("idPublicKey"+index) != null){
index++;
KTUtil.onDOMContentLoaded(function () {
$('#public_keys').repeater({
initEmpty: false,
show: function () {
$(this).slideDown();
},
hide: function (deleteElement) {
$(this).slideUp(deleteElement);
}
$(".form_field_pk_outer").append(`
<div class="row form_field_pk_outer_row">
<div class="form-group col-md-11">
<textarea class="form-control" id="idPublicKey${index}" name="public_keys" rows="4"
placeholder="Paste your public key here"></textarea>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_pk_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`);
});
$("body").on("click", ".remove_pk_btn_frm_field", function () {
$(this).closest(".form_field_pk_outer_row").remove();
$("#page_form").submit(function (event) {
let submitButton = document.querySelector('#form_submit');
submitButton.setAttribute('data-kt-indicator', 'on');
submitButton.disabled = true;
return true;
});
});
</script>
{{end}}
{{end}}
{{- end}}
{{- end}}

View File

@@ -0,0 +1,64 @@
<!--
Copyright (C) 2023 Nicola Murino
This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
https://keenthemes.com/products/templates-mega-bundle
KeenThemes HTML/CSS/JS components are allowed for use only within the
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 "baselogin" .}}
{{- define "title"}}Reset password{{- end}}
{{- define "content"}}
<form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST">
<div class="container mb-10">
<div class="row align-items-center">
<div class="col-5 align-items-center">
<a href="{{.LoginURL}}">
<img alt="Logo" src="{{.StaticURL}}{{.Branding.LogoPath}}" class="h-80px h-md-90px h-lg-100px" />
</a>
</div>
<div class="col-7">
<a href="{{.LoginURL}}" class="text-dark mb-3 ms-3 fs-1 fw-bold">
{{.Branding.ShortName}}
</a>
</div>
</div>
</div>
<div class="text-center mb-10">
<h2 class="text-dark mb-3">
Reset Password
</h2>
<div class="text-gray-600 fw-semibold fs-4">
Check your email for the confirmation code
</div>
</div>
{{template "errmsg" .Error}}
<div class="fv-row mb-10">
<input class="form-control form-control-lg form-control-solid" type="text" placeholder="Confirmation code" name="code" spellcheck="false" required />
</div>
<div class="fv-row mb-10">
<input class="form-control form-control-lg form-control-solid" type="password" name="password" placeholder="New Password" autocomplete="new-password" spellcheck="false" required />
</div>
<div class="fv-row mb-10">
<input class="form-control form-control-lg form-control-solid" type="password" name="confirm_password" placeholder="Confirm Password" autocomplete="new-password" spellcheck="false" required />
</div>
<div class="text-center">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5">
<span class="indicator-label">Update Password & Login</span>
<span class="indicator-progress">Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span>
</button>
</div>
</form>
{{- end}}

View File

@@ -1,230 +1,237 @@
<!--
Copyright (C) 2019-2023 Nicola Murino
Copyright (C) 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 WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
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.
https://keenthemes.com/products/templates-mega-bundle
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/>.
KeenThemes HTML/CSS/JS components are allowed for use only within the
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" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "extra_css"}}
<link href="{{.StaticURL}}/vendor/tempusdominus/css/tempusdominus-bootstrap-4.min.css" rel="stylesheet">
<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">{{if .IsAdd}}Add a new share{{else}}Edit share{{end}}</h6>
{{- define "page_body"}}
<div class="card shadow-sm">
<div class="card-header bg-light">
<h3 class="card-title text-primary">{{if .IsAdd}}Add a new share{{else}}Edit share{{end}}</h3>
</div>
<div class="card-body">
{{if .Error}}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
{{.Error}}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{{end}}
{{- template "errmsg" .Error}}
<form id="share_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
<div class="form-group row">
<label for="idName" class="col-sm-2 col-form-label">Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idName" name="name" placeholder=""
value="{{.Share.Name}}" maxlength="255" autocomplete="nope" required {{if not .IsAdd}}readonly{{end}}>
<label class="col-md-3 col-form-label">Name</label>
<div class="col-md-9">
<input type="text" class="form-control" placeholder="" name="name" value="{{.Share.Name}}"
maxlength="255" autocomplete="nope" required {{if not .IsAdd}}readonly{{end}} />
</div>
</div>
<div class="form-group row">
<label for="idScope" class="col-sm-2 col-form-label">Scope</label>
<div class="col-sm-10">
<select class="form-control selectpicker" id="idScope" name="scope" aria-describedby="scopeHelpBlock">
<div class="form-group row mt-10">
<label class="col-md-3 col-form-label">Scope</label>
<div class="col-md-9">
<select name="scope" class="form-select" data-control="select2" data-hide-search="true">
<option value="1" {{if eq .Share.Scope 1 }}selected{{end}}>Read</option>
<option value="2" {{if eq .Share.Scope 2 }}selected{{end}}>Write</option>
<option value="3" {{if eq .Share.Scope 3 }}selected{{end}}>Read/Write</option>
</select>
<small id="scopeHelpBlock" class="form-text text-muted">
<div class="form-text">
For scope "Write" and "Read&Write" you have to define one path and it must be a directory
</small>
</div>
</div>
</div>
<div class="card bg-light mb-3">
<div class="card-header">
Paths
<div class="card card-rounded mt-10">
<div class="card-header bg-light">
<h3 class="card-title">Paths</h3>
</div>
<div class="card-body">
<div class="form-group row">
<div class="col-md-12 form_field_path_outer">
{{range $idx, $val := .Share.Paths}}
<div class="row form_field_path_outer_row">
<div class="form-group col-md-11">
<input type="text" class="form-control" id="idPath{{$idx}}" name="paths"
placeholder="file or directory path, i.e. /dir or /dir/file.txt" value="{{$val}}" maxlength="255">
<div id="paths">
<div class="form-group">
<div data-repeater-list="paths">
{{- range $idx, $val := .Share.Paths}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-9">
<input type="text" class="form-control mt-3 mt-md-8"
placeholder="file or directory path, i.e. /dir or /dir/file.txt"
name="path" value="{{$val}}" />
</div>
<div class="col-md-3">
<a href="javascript:;" data-repeater-delete
class="btn btn-light-danger mt-3 mt-md-8">
<i class="ki-duotone ki-trash fs-5">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
Delete
</a>
</div>
</div>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_path_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
{{- else}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-9">
<input type="text" class="form-control mt-3 mt-md-8"
placeholder="file or directory path, i.e. /dir or /dir/file.txt"
name="path" value="" />
</div>
<div class="col-md-3">
<a href="javascript:;" data-repeater-delete
class="btn btn-light-danger mt-3 mt-md-8">
<i class="ki-duotone ki-trash fs-5">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
Delete
</a>
</div>
</div>
</div>
{{- end}}
</div>
{{else}}
<div class="row form_field_path_outer_row">
<div class="form-group col-md-11">
<input type="text" class="form-control" id="idPath0" name="paths"
placeholder="file or directory path, i.e. /dir or /dir/file.txt" value="" maxlength="512">
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_path_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{end}}
</div>
<div class="form-group mt-5">
<a href="javascript:;" data-repeater-create class="btn btn-light-primary">
<i class="ki-duotone ki-plus fs-3"></i>
Add
</a>
</div>
</div>
<div class="row mx-1">
<button type="button" class="btn btn-secondary add_new_path_field_btn">
<i class="fas fa-plus"></i> Add a path
</button>
</div>
</div>
</div>
<div class="form-group row">
<label for="idPassword" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idPassword" name="password" autocomplete="new-password" placeholder="" spellcheck="false"
value="{{.Share.Password}}" aria-describedby="passwordHelpBlock">
<small id="passwordHelpBlock" class="form-text text-muted">
<div class="form-group row mt-10">
<label class="col-md-3 col-form-label">Password</label>
<div class="col-md-9">
<input type="password" class="form-control" name="password" autocomplete="new-password"
placeholder="" spellcheck="false" value="{{.Share.Password}}" />
<div class="form-text">
If set the share will be password-protected
</small>
</div>
</div>
<div class="form-group row">
<label for="idExpiration" class="col-sm-2 col-form-label">Expiration</label>
<div class="col-sm-10 input-group date" id="expirationDateTimePicker" data-target-input="nearest">
<input type="text" class="form-control datetimepicker-input" id="idExpiration"
data-target="#expirationDateTimePicker" placeholder="none">
<div class="input-group-append" data-target="#expirationDateTimePicker" data-toggle="datetimepicker">
<div class="input-group-text"><i class="fas fa-calendar"></i></div>
</div>
</div>
</div>
<div class="form-group row">
<label for="idMaxTokens" class="col-sm-2 col-form-label">Max tokens</label>
<div class="col-sm-10">
<input type="number" min="0" class="form-control" id="idMaxTokens" name="max_tokens" placeholder=""
value="{{.Share.MaxTokens}}" aria-describedby="maxTokensHelpBlock">
<small id="maxTokensHelpBlock" class="form-text text-muted">
<div class="form-group row mt-10">
<label class="col-md-3 col-form-label">Expiration</label>
<div class="col-md-9">
<div class="input-group" data-td-target-input="nearest" data-td-target-toggle="nearest">
<input id="id_expiration" type="text" class="form-control" data-td-target="#id_expiration" />
<span class="input-group-text" data-td-target="#id_expiration" data-td-toggle="datetimepicker">
<i class="ki-duotone ki-calendar fs-2">
<span class="path1"></span>
<span class="path2"></span>
</i>
</span>
</div>
</div>
</div>
<div class="form-group row mt-10">
<label class="col-md-3 col-form-label">Max tokens</label>
<div class="col-md-9">
<input type="number" min="0" class="form-control" name="max_tokens" value="{{.Share.MaxTokens}}" />
<div class="form-text">
Maximum number of times this share can be accessed. 0 means no limit
</small>
</div>
</div>
</div>
<div class="form-group row">
<label for="idAllowedIP" class="col-sm-2 col-form-label">Allowed IP/Mask</label>
<div class="col-sm-10">
<textarea class="form-control" id="idAllowedIP" name="allowed_ip" rows="3" placeholder=""
aria-describedby="allowedIPHelpBlock">{{.Share.GetAllowedFromAsString}}</textarea>
<small id="allowedIPHelpBlock" class="form-text text-muted">
<div class="form-group row mt-10">
<label class="col-md-3 col-form-label">Allowed IP/Mask</label>
<div class="col-md-9">
<textarea class="form-control" name="allowed_ip" rows="3"
placeholder="">{{.Share.GetAllowedFromAsString}}</textarea>
<div class="form-text">
Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32"
</small>
</div>
</div>
</div>
<div class="form-group row">
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-10">
<textarea class="form-control" id="idDescription" name="description" rows="3">{{.Share.Description}}</textarea>
<div class="form-group row mt-10">
<label class="col-md-3 col-form-label">Description</label>
<div class="col-md-9">
<textarea class="form-control" name="description" rows="3"
placeholder="">{{.Share.Description}}</textarea>
</div>
</div>
<input type="hidden" name="expiration_date" id="hidden_start_datetime" value="">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5">Submit</button>
<div class="d-flex justify-content-end mt-12">
<input type="hidden" name="expiration_date" id="hidden_start_datetime" value="">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="form_submit" class="btn btn-primary px-10">
<span class="indicator-label">
Submit
</span>
<span class="indicator-progress">
Please wait... <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span>
</button>
</div>
</form>
</div>
</div>
{{end}}
{{- end}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/moment/js/moment.min.js"></script>
<script src="{{.StaticURL}}/vendor/tempusdominus/js/tempusdominus-bootstrap-4.min.js"></script>
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
{{- define "extra_js"}}
<script src="{{.StaticURL}}/assets/plugins/custom/formrepeater/formrepeater.bundle.js"></script>
<script type="text/javascript">
$(document).ready(function () {
KTUtil.onDOMContentLoaded(function () {
$('#paths').repeater({
initEmpty: false,
$('#expirationDateTimePicker').datetimepicker({
format: 'YYYY-MM-DD HH:mm',
sideBySide: true,
minDate: moment(),
buttons: {
showClear: false,
showClose: true,
showToday: false
}
});
show: function () {
$(this).slideDown();
},
{{ if gt .Share.ExpiresAt 0 }}
var input_dt = moment({{.Share.ExpiresAt }}).format('YYYY-MM-DD HH:mm');
$('#idExpiration').val(input_dt);
$('#expirationDateTimePicker').datetimepicker('viewDate', input_dt);
{{ end }}
hide: function (deleteElement) {
$(this).slideUp(deleteElement);
}
});
$("#share_form").submit(function (event) {
var dt = $('#idExpiration').val();
if (dt) {
var d = $('#expirationDateTimePicker').datetimepicker('viewDate');
if (d) {
var dateString = moment.utc(d).format('YYYY-MM-DD HH:mm:ss');
$('#hidden_start_datetime').val(dateString);
const picker = new tempusDominus.TempusDominus(document.getElementById("id_expiration"), {
localization: {
format: 'yyyy-MM-dd HH:mm'
}
});
//{{ if gt .Share.ExpiresAt 0 }}
let input_dt = moment('{{.Share.ExpiresAt }}','x').toDate();
picker.dates.setValue(tempusDominus.DateTime.convert(input_dt));
//{{ end }}
$("#share_form").submit(function (event) {
let dt = $('#id_expiration').val();
if (dt) {
let d = picker.viewDate;
if (d) {
let dateString = moment.utc(d).format('YYYY-MM-DD HH:mm:ss');
$('#hidden_start_datetime').val(dateString);
} else {
$('#hidden_start_datetime').val("");
}
} else {
$('#hidden_start_datetime').val("");
}
} else {
$('#hidden_start_datetime').val("");
}
return true;
let submitButton = document.querySelector('#form_submit');
submitButton.setAttribute('data-kt-indicator', 'on');
submitButton.disabled = true;
return true;
});
});
$("body").on("click", ".add_new_path_field_btn", function () {
let index = $(".form_field_path_outer").find(".form_field_path_outer_row").length;
while (document.getElementById("idPath"+index) != null){
index++;
}
$(".form_field_path_outer").append(`
<div class="row form_field_path_outer_row">
<div class="form-group col-md-11">
<input type="text" class="form-control" id="idPath${index}" name="paths"
placeholder="file or directory path, i.e. /dir or /dir/file.txt" value="" maxlength="512">
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_path_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`);
});
$("body").on("click", ".remove_path_btn_frm_field", function () {
$(this).closest(".form_field_path_outer_row").remove();
});
});
</script>
{{end}}
{{- end}}

View File

@@ -1,572 +0,0 @@
<!--
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/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/dataTables.checkboxes.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/filepond/filepond.min.css" rel="stylesheet" />
<style>
div.dataTables_wrapper span.selected-info,
div.dataTables_wrapper span.selected-item {
margin-left: 0.5em;
}
</style>
{{end}}
{{define "page_body"}}
<div class="card shadow my-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold"><a href="{{.FilesURL}}?path=%2F"><i class="fas fa-home"></i>&nbsp;Home</a>&nbsp;{{range .Paths}}{{if eq .Href ""}}/{{.DirName}}{{else}}<a href="{{.Href}}">/{{.DirName}}</a>{{end}}{{end}}</h6>
</div>
<div class="card-body">
{{if .Error}}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
{{.Error}}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{{end}}
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span>
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span>
</button>
</div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div id="tableContainer" class="table-responsive">
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
<thead>
<tr>
<th></th>
<th>Type</th>
<th>Name</th>
<th>Size</th>
<th>Last modified</th>
</tr>
</thead>
</table>
</div>
</div>
</div>
{{end}}
{{define "dialog"}}
<div class="modal fade" id="uploadFilesModal" tabindex="-1" role="dialog" aria-labelledby="uploadFilesModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="uploadFilesModalLabel">
Upload one or more files
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="upload_files_form" action="{{.FilesURL}}?path={{.CurrentDir}}" method="POST" enctype="multipart/form-data">
<div class="modal-body">
<div id="uploadErrorMsg" class="card mb-4 border-left-warning" style="display: none;">
<div id="uploadErrorTxt" class="card-body text-form-error"></div>
</div>
<input type="file" id="files_name" name="filenames" required multiple>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="spinnerModal" tabindex="-1" role="dialog" data-keyboard="false" data-backdrop="static">
<div class="modal-dialog modal-dialog-centered justify-content-center" role="document">
<span style="color: #333333;" class="fa fa-spinner fa-spin fa-3x"></span>
</div>
</div>
{{end}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.checkboxes.min.js"></script>
<script src="{{.StaticURL}}/vendor/filepond/filepond.min.js"></script>
<script type="text/javascript">
var spinnerDone = false;
function shortenData(d, cutoff) {
if ( typeof d !== 'string' ) {
return d;
}
if ( d.length <= cutoff ) {
return escapeHTML(d);
}
var shortened = d.substr(0, cutoff-1);
return escapeHTML(shortened)+'&#8230;';
}
function getIconForFile(filename) {
var extension = filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
switch (extension) {
case "doc":
case "docx":
case "odt":
case "wps":
return "far fa-file-word";
case "ppt":
case "pptx":
return "far fa-file-powerpoint";
case "xls":
case "xlsx":
case "ods":
return "far fa-file-excel";
case "pdf":
return "far fa-file-pdf";
case "webm":
case "mkv":
case "flv":
case "vob":
case "ogv":
case "ogg":
case "avi":
case "ts":
case "mov":
case "wmv":
case "asf":
case "mpeg":
case "mpv":
case "3gp":
case "mp4":
return "far fa-file-video";
case "jpeg":
case "jpg":
case "png":
case "gif":
case "webp":
case "tiff":
case "psd":
case "bmp":
case "svg":
case "jp2":
return "far fa-file-image";
case "go":
case "sh":
case "bat":
case "java":
case "php":
case "cs":
case "asp":
case "aspx":
case "css":
case "html":
case "xhtml":
case "htm":
case "js":
case "jsp":
case "py":
case "rb":
case "cgi":
case "c":
case "cpp":
case "h":
case "hpp":
case "kt":
case "ktm":
case "kts":
case "swift":
case "r":
return "far fa-file-code";
case "zip":
case "zipx":
case "7z":
case "rar":
case "tar":
case "gz":
case "bz2":
case "zstd":
case "zst":
case "sz":
case "lz":
case "lz4":
case "xz":
case "jar":
return "far fa-file-archive";
case "txt":
case "rtf":
case "json":
case "xml":
case "yaml":
case "toml":
case "log":
case "csv":
case "ini":
case "cfg":
return "far fa-file-alt";
default:
return "far fa-file";
}
}
function getNameFromMeta(meta) {
return meta.split('_').slice(1).join('_');
}
const isDirectoryEntry = item => isEntry(item) && (getAsEntry(item) || {}).isDirectory;
const isEntry = item => 'webkitGetAsEntry' in item;
const getAsEntry = item => item.webkitGetAsEntry();
$(document).ready(function () {
$('#spinnerModal').on('shown.bs.modal', function () {
if (spinnerDone){
$('#spinnerModal').modal('hide');
}
});
{{if gt .Scope 1}}
FilePond.create(document.getElementById("files_name"),{
allowMultiple: true,
name: 'filenames',
maxFiles: 30,
credits: false,
required: true,
onwarning: function(error){
if (error.code == 0){
$('#uploadErrorTxt').text('You can upload a maximum of 30 files');
$('#uploadErrorMsg').show();
setTimeout(function () {
$('#uploadErrorMsg').hide();
}, 10000);
}
},
beforeAddFile: (fileItem) => new Promise(resolve => {
let num = 0;
FilePond.find(document.getElementById("files_name")).getFiles().forEach(function(val){
if (val.filename == fileItem.filename){
num++;
}
});
resolve(num == 1);
})
});
$('#tableContainer').on("dragover", function(ev){
ev.preventDefault();
$('#tableContainer').css('opacity','0.5');
});
$('#tableContainer').on("dragend dragleave", function(ev){
ev.preventDefault();
$('#tableContainer').css('opacity','1');
});
$('#tableContainer').on("drop", function(ev){
ev.preventDefault();
$('#tableContainer').css('opacity','1');
let filesDropped = false;
if (ev.originalEvent.dataTransfer.items) {
[...ev.originalEvent.dataTransfer.items].forEach((item, i) => {
if (item.kind === 'file') {
// if this is a directory just open the upload dialog
if (!isDirectoryEntry(item)){
FilePond.find(document.getElementById("files_name")).addFile(item.getAsFile());
filesDropped = true;
}
}
});
} else {
[...ev.originalEvent.dataTransfer.files].forEach((file, i) => {
FilePond.find(document.getElementById("files_name")).addFile(file);
filesDropped = true;
});
}
if (filesDropped && !$('#uploadFilesModal').hasClass('show')){
$('#uploadFilesModal').modal('show');
}
});
{{end}}
$("#upload_files_form").submit(function (event){
event.preventDefault();
let files = FilePond.find(document.getElementById("files_name")).getFiles();
let has_errors = false;
let index = 0;
let success = 0;
spinnerDone = false;
$('#uploadFilesModal').modal('hide');
$('#spinnerModal').modal('show');
$('#errorMsg').hide();
function uploadFile() {
if (index >= files.length || has_errors){
$('#spinnerModal').modal('hide');
spinnerDone = true;
if (!has_errors){
location.reload();
}
return;
}
async function saveFile() {
let errorMessage = "Error uploading files";
let response;
try {
let f = files[index].file;
let uploadPath = '{{.UploadBaseURL}}'+fixedEncodeURIComponent("/"+escapeHTML(f.name));
let lastModified;
try {
lastModified = f.lastModified;
} catch (e) {
console.log("unable to get last modified time from file: "+e.message);
lastModified = "";
}
response = await fetch(uploadPath, {
method: 'POST',
headers: {
'X-SFTPGO-MTIME': lastModified
},
credentials: 'same-origin',
redirect: 'error',
body: f
});
} catch (e){
throw Error(errorMessage+": " +e.message);
}
if (response.status == 201){
index++;
success++;
uploadFile();
} else {
let jsonResponse;
try {
jsonResponse = await response.json();
} catch(e){
throw Error(errorMessage);
}
if (jsonResponse.message) {
errorMessage = jsonResponse.message;
}
if (jsonResponse.error) {
errorMessage += ": " + jsonResponse.error;
}
throw Error(errorMessage);
}
}
saveFile().catch(function(error){
index++;
has_errors = true;
$('#errorTxt').text(error.message);
$('#errorMsg').show();
uploadFile();
});
}
uploadFile();
});
$.fn.dataTable.ext.buttons.refresh = {
text: '<i class="fas fa-sync-alt"></i>',
name: 'refresh',
titleAttr: "Refresh",
action: function (e, dt, node, config) {
location.reload();
}
};
$.fn.dataTable.ext.buttons.download = {
text: '<i class="fas fa-download"></i>',
name: 'download',
titleAttr: "Download zip",
action: function (e, dt, node, config) {
var filesArray = [];
var selected = dt.column(0).checkboxes.selected();
for (i = 0; i < selected.length; i++) {
filesArray.push(getNameFromMeta(selected[i]));
}
var files = encodeURIComponent(JSON.stringify(filesArray));
var downloadURL = '{{.DownloadURL}}';
var currentDir = '{{.CurrentDir}}';
var ts = new Date().getTime().toString();
window.open(`${downloadURL}?path=${currentDir}&files=${files}&_=${ts}`);
},
enabled: false
};
$.fn.dataTable.ext.buttons.addFiles = {
text: '<i class="fas fa-file-upload"></i>',
name: 'addFiles',
titleAttr: "Upload files",
action: function (e, dt, node, config) {
//FilePond.find(document.getElementById("files_name")).removeFiles();
$('#uploadFilesModal').modal('show');
},
enabled: true
};
let table = $('#dataTable').DataTable({
"ajax": {
"url": "{{.DirsURL}}?path={{.CurrentDir}}",
"dataSrc": "",
"error": function ($xhr, textStatus, errorThrown) {
$(".dataTables_processing").hide();
let txt = "Failed to get directory listing";
if ($xhr) {
let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
}
}
}
$('#errorTxt').text(txt);
$('#errorMsg').show();
}
},
"deferRender": true,
"processing": true,
"lengthMenu": [ 10, 25, 50, 100, 250, 500 ],
"stateSave": true,
"stateDuration": 0,
"stateSaveParams": function (settings, data) {
data.sftpgo_dir = '{{.CurrentDir}}';
},
"stateLoadParams": function (settings, data) {
if (!data.sftpgo_dir || data.sftpgo_dir != '{{.CurrentDir}}'){
data.start = 0;
data.search.search = "";
}
data.checkboxes = [];
},
"columns": [
{ "data": "meta" },
{ "data": "type" },
{
"data": "name",
"render": function (data, type, row) {
if (type === 'display') {
var title = "";
var cssClass = "";
var shortened = shortenData(data, 70);
data = escapeHTML(data);
if (shortened != data){
title = escapeHTML(data);
cssClass = "ellipsis";
}
if (row["type"] == "1") {
return `<i class="fas fa-folder"></i>&nbsp;<a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`;
}
if (row["size"] === "") {
return `<i class="fas fa-external-link-alt"></i>&nbsp;<a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`;
}
var icon = getIconForFile(data);
return `<i class="${icon}"></i>&nbsp;<a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`;
}
return data;
}
},
{
"data": "size",
"render": function (data, type, row) {
if (type === 'display') {
if (data || data === 0){
return fileSizeIEC(data);
}
return "";
}
return data;
}
},
{ "data": "last_modified" }
],
"buttons": [],
"lengthChange": true,
"columnDefs": [
{
"targets": [0],
"checkboxes": {
"selectCallback": function (nodes, selected) {
var selectedItems = table.column(0).checkboxes.selected().length;
var selectedText = "";
if (selectedItems == 1) {
selectedText = "1 item selected";
} else if (selectedItems > 1) {
selectedText = `${selectedItems} items selected`;
}
table.button('download:name').enable(selectedItems > 0);
$('#dataTable_info').find('span').remove();
$("#dataTable_info").append('<span class="selected-info"><span class="selected-item">' + selectedText + '</span></span>');
}
},
"orderable": false,
"searchable": false
},
{
"targets": [1],
"visible": false,
"searchable": false
},
{
"targets": [3, 4],
"searchable": false
}
],
"scrollX": false,
"scrollY": false,
"responsive": true,
"language": {
"loadingRecords": "",
"emptyTable": "No files or folders"
},
"initComplete": function (settings, json) {
table.button().add(0, 'refresh');
//table.button().add(0, 'pageLength');
table.button().add(0, 'download');
{{if gt .Scope 1}}
table.button().add(0, 'addFiles');
{{end}}
table.buttons().container().appendTo('.col-md-6:eq(0)', table.table().container());
},
"orderFixed": [1, 'asc'],
"order": [2, 'asc']
});
new $.fn.dataTable.FixedHeader(table);
$.fn.dataTable.ext.errMode = 'none';
});
</script>
{{end}}

View File

@@ -1,40 +1,48 @@
<!--
Copyright (C) 2019-2023 Nicola Murino
Copyright (C) 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 WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
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.
https://keenthemes.com/products/templates-mega-bundle
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/>.
KeenThemes HTML/CSS/JS components are allowed for use only within the
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 "baselogin" .}}
{{- template "baselogin" .}}
{{define "title"}}Share login{{end}}
{{- define "title"}}Share login{{- end}}
{{define "content"}}
{{if .Error}}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
{{.Error}}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{{end}}
<form id="login_form" action="{{.CurrentURL}}" method="POST" autocomplete="off"
class="user-custom">
<div class="form-group">
<input type="password" class="form-control form-control-user-custom"
id="inputSharePassword" name="share_password" placeholder="Password" spellcheck="false" required>
</div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary btn-user-custom btn-block">
Login
</button>
</form>
{{end}}
{{- define "content"}}
<form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST">
<div class="container mb-10">
<div class="row align-items-center">
<div class="col-5 align-items-center">
<img alt="Logo" src="{{.StaticURL}}{{.Branding.LogoPath}}" class="h-80px h-md-90px h-lg-100px" />
</div>
<div class="col-7">
<h1 class="text-dark mb-3 ms-3">
{{.Branding.ShortName}}
</h1>
</div>
</div>
</div>
{{template "errmsg" .Error}}
<div class="fv-row mb-10">
<input class="form-control form-control-lg form-control-solid" type="password" name="share_password" placeholder="Password" spellcheck="false" required />
</div>
<div class="text-center">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5">
<span class="indicator-label">Sign in</span>
<span class="indicator-progress">Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span>
</button>
</div>
</form>
{{- end}}

View File

@@ -1,115 +1,87 @@
<!--
Copyright (C) 2019-2023 Nicola Murino
Copyright (C) 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 WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
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.
https://keenthemes.com/products/templates-mega-bundle
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/>.
KeenThemes HTML/CSS/JS components are allowed for use only within the
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" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "extra_css"}}
<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
{{end}}
{{- define "extra_css"}}
<link href="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css" rel="stylesheet" type="text/css"/>
{{- end}}
{{define "page_body"}}
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span>
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span>
</button>
</div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">View and manage shares</h6>
<div class="card shadow-sm">
<div class="card-header bg-light">
<h3 class="card-title text-primary">View and manage shares</h3>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
<div id="card_body" class="card-body">
{{- template "errmsg" ""}}
<div id="loader" class="align-items-center text-center my-10">
<span class="spinner-border w-15px h-15px text-muted align-middle me-2"></span>
<span class="text-gray-600">Loading...</span>
</div>
<div id="card_content" class="d-none">
<div class="d-flex flex-stack mb-5">
<div class="d-flex align-items-center position-relative my-1">
<i class="ki-duotone ki-magnifier fs-1 position-absolute ms-6">
<span class="path1"></span>
<span class="path2"></span>
</i>
<input type="text" data-share-table-filter="search"
class="form-control form-control-solid w-200px ps-15" placeholder="Search" />
</div>
<div class="d-flex justify-content-end" data-share-table-toolbar="base">
<a href="{{.ShareURL}}" class="btn btn-primary">
<i class="ki-duotone ki-plus fs-2"></i>
Add
</a>
</div>
</div>
<table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5">
<thead>
<tr>
<th>ID</th>
<tr class="text-start text-muted fw-bold fs-6 gs-0">
<th>Name</th>
<th>Scope</th>
<th>Info</th>
<th></th>
<th class="min-w-100px"></th>
</tr>
</thead>
<tbody>
{{range .Shares}}
<tr>
<td>{{.ShareID}}</td>
<td>{{.Name}}</td>
<td>{{.GetScopeAsString}}</td>
<td>{{.GetInfoString}}</td>
<td>{{if .IsExpired}}1{{else}}0{{end}}</td>
</tr>
{{end}}
</tbody>
<tbody class="text-gray-800 fw-semibold"></tbody>
</table>
</div>
</div>
</div>
{{end}}
{{- end}}
{{define "dialog"}}
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel"
aria-hidden="true">
{{- define "modals"}}
<div class="modal fade" id="link_modal" tabindex="-1">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">
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 delete the selected share?</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">
Cancel
</button>
<a class="btn btn-warning" href="#" onclick="deleteAction()">
Delete
</a>
</div>
</div>
</div>
</div>
<div class="modal fade" id="linkModal" tabindex="-1" role="dialog" aria-labelledby="linkModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="linkModalLabel">
<div class="modal-header border-0">
<h3 class="modal-title">
Share access links
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</h3>
<div class="btn btn-icon btn-sm btn-active-color-primary" data-bs-dismiss="modal" aria-label="Close">
<i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i>
</div>
</div>
<div class="modal-body">
<div class="modal-body fs-5">
<div id="readShare">
<p>You can download the shared contents, as single zip file, using this <a id="readLink" href="#" target="_blank">link</a>.</p>
<p>If the share consists of a single directory you can browse and download files using this <a id="readBrowseLink" href="#" target="_blank">page</a>.</p>
@@ -122,8 +94,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
This share is no longer accessible because it has expired
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" type="button" data-dismiss="modal">
<div class="modal-footer border-0">
<button class="btn btn-primary" type="button" data-bs-dismiss="modal">
OK
</button>
</div>
@@ -133,161 +105,177 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{end}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script>
<script src="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js"></script>
<script type="text/javascript">
function deleteAction() {
let table = $('#dataTable').DataTable();
table.button('delete:name').enable(false);
let shareID = table.row({ selected: true }).data()[0];
let path = '{{.ShareURL}}' + "/" + fixedEncodeURIComponent(shareID);
$('#deleteModal').modal('hide');
$('#errorMsg').hide();
function deleteAction(shareID) {
let errDivEl = $('#errorMsg');
let errTxtEl = $('#errorTxt');
errDivEl.addClass("d-none");
$.ajax({
url: path,
type: 'DELETE',
dataType: 'json',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
timeout: 15000,
success: function (result) {
window.location.href = '{{.SharesURL}}';
},
error: function ($xhr, textStatus, errorThrown) {
let txt = "Unable to delete the selected share";
if ($xhr) {
let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
ModalAlert.fire({
text: `Do you want to delete the selected share? This action is irreversible`,
icon: "warning",
confirmButtonText: "Delete",
cancelButtonText: 'Cancel',
customClass: {
confirmButton: "btn btn-danger",
cancelButton: 'btn btn-secondary'
}
}).then((result) => {
if (result.isConfirmed){
$('#loading_message').text("");
KTApp.showPageLoading();
let path = '{{.ShareURL}}' + "/" + fixedEncodeURIComponent(shareID);
axios.delete(path, {
timeout: 15000,
headers: {
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
validateStatus: function (status) {
return status == 200;
}
}).then(function(response){
location.reload();
}).catch(function(error){
KTApp.hidePageLoading();
let errorMessage = `Unable to delete the selected share`;
if (error && error.response) {
if (error.response.data.message) {
errorMessage = error.response.data.message;
}
if (error.response.data.error) {
errorMessage += ": " + error.response.data.error;
}
}
}
$('#errorTxt').text(txt);
$('#errorMsg').show();
errTxtEl.text(errorMessage);
errDivEl.removeClass("d-none");
});
}
});
}
$(document).ready(function () {
$.fn.dataTable.ext.buttons.add = {
text: '<i class="fas fa-plus"></i>',
name: 'add',
titleAttr: "Add",
action: function (e, dt, node, config) {
window.location.href = '{{.ShareURL}}';
}
};
function editAction(shareID) {
window.location.replace('{{.ShareURL}}' + "/" + fixedEncodeURIComponent(shareID));
}
$.fn.dataTable.ext.buttons.edit = {
text: '<i class="fas fa-pen"></i>',
name: 'edit',
titleAttr: "Edit",
action: function (e, dt, node, config) {
var shareID = dt.row({ selected: true }).data()[0];
var path = '{{.ShareURL}}' + "/" + fixedEncodeURIComponent(shareID);
window.location.href = path;
},
enabled: false
};
$.fn.dataTable.ext.buttons.delete = {
text: '<i class="fas fa-trash"></i>',
name: 'delete',
titleAttr: "Delete",
action: function (e, dt, node, config) {
$('#deleteModal').modal('show');
},
enabled: false
};
$.fn.dataTable.ext.buttons.link = {
text: '<i class="fas fa-link"></i>',
name: 'link',
titleAttr: "Link",
action: function (e, dt, node, config) {
var shareData = dt.row({ selected: true }).data();
var shareID = shareData[0];
var shareScope = shareData[2];
var isExpired = shareData[4];
if (isExpired == "1"){
$('#expiredShare').show();
function showShareLink(shareID, shareScope, isExpired) {
if (isExpired == "1") {
$('#expiredShare').show();
$('#writeShare').hide();
$('#readShare').hide();
} else {
let shareURL = '{{.BasePublicSharesURL}}' + "/" + fixedEncodeURIComponent(shareID);
if (shareScope == 'Read') {
$('#expiredShare').hide();
$('#writeShare').hide();
$('#readShare').hide();
$('#readShare').show();
$('#readLink').attr("href", shareURL);
$('#readLink').attr("title", shareURL);
$('#readUncompressedLink').attr("href", shareURL + "?compress=false");
$('#readUncompressedLink').attr("title", shareURL + "?compress=false");
$('#readBrowseLink').attr("href", shareURL + "/browse");
$('#readBrowseLink').attr("title", shareURL + "/browse");
} else {
var shareURL = '{{.BasePublicSharesURL}}' + "/" + fixedEncodeURIComponent(shareID);
if (shareScope == 'Read'){
$('#expiredShare').hide();
$('#writeShare').hide();
$('#readShare').show();
$('#readLink').attr("href", shareURL);
$('#readLink').attr("title", shareURL);
$('#readUncompressedLink').attr("href", shareURL+"?compress=false");
$('#readUncompressedLink').attr("title", shareURL+"?compress=false");
$('#readBrowseLink').attr("href", shareURL+"/browse");
$('#readBrowseLink').attr("title", shareURL+"/browse");
} else {
$('#expiredShare').hide();
$('#writeShare').show();
$('#readShare').hide();
$('#writePageLink').attr("href", shareURL+"/upload");
$('#writePageLink').attr("title", shareURL+"/upload");
$('#expiredShare').hide();
$('#writeShare').show();
$('#readShare').hide();
$('#writePageLink').attr("href", shareURL + "/upload");
$('#writePageLink').attr("title", shareURL + "/upload");
}
}
$('#link_modal').modal('show');
}
KTUtil.onDOMContentLoaded(function () {
const tableData = [];
{{- range .Shares}}
tableData.push(['{{.Name}}','{{.GetScopeAsString}}','{{.GetInfoString}}','{{.ShareID}}','{{- if .IsExpired}}1{{- else}}0{{- end}}']);
{{- end}}
let dt = $('#dataTable').DataTable({
data: tableData,
columnDefs: [
{
targets: 3,
searchable: false,
orderable: false,
className: 'text-end',
render: function (data, type, row) {
if (type === 'display') {
return `<div class="d-flex justify-content-end">
<div class="ms-2">
<a href="#" onclick="showShareLink('${row[3]}','${row[1]}','${row[4]}')" class="btn btn-sm btn-icon btn-light btn-active-light-primary">
<i class="ki-duotone ki-fasten fs-5 m-0">
<span class="path1"></span>
<span class="path2"></span>
</i>
</a>
</div>
<div class="ms-2">
<button type="button" class="btn btn-sm btn-icon btn-light btn-active-light-primary"
data-kt-menu-trigger="click" data-kt-menu-placement="bottom-end">
<i class="ki-duotone ki-dots-square fs-5 m-0">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
</i>
</button>
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-600 menu-state-bg-light-primary fw-semibold fs-7 w-150px py-4" data-kt-menu="true">
<div class="menu-item px-3">
<a href="#" onclick="editAction('${row[3]}');" class="menu-link px-3">Edit</a>
</div>
<div class="menu-item px-3">
<a href="#" onclick="deleteAction('${row[3]}');" class="menu-link text-danger px-3">Delete</a>
</div>
</div>
</div>
</div>`;
}
return "";
}
},
{
targets: [0, 2],
render: function (data, type, row) {
if (type === 'display') {
return escapeHTML(data);
}
return data
}
}
$('#linkModal').modal('show');
},
enabled: false
};
var table = $('#dataTable').DataTable({
"select": {
"style": "single",
"blurable": true
},
"stateSave": true,
"stateDuration": 0,
"buttons": [],
"columnDefs": [
{
"targets": [0, 4],
"visible": false,
"searchable": false
}
],
"scrollX": false,
"scrollY": false,
"responsive": true,
"language": {
"emptyTable": "No share defined"
deferRender: true,
stateSave: true,
stateDuration: 0,
stateLoadParams: function (settings, data) {
if (data.search.search){
const filterSearch = document.querySelector('[data-share-table-filter="search"]');
filterSearch.value = data.search.search;
}
},
language: {
emptyTable: "No share defined"
},
"order": [[1, 'asc']]
order: [[1, 'asc']],
initComplete: function(settings, json) {
$('#loader').addClass("d-none");
$('#card_content').removeClass("d-none");
$.fn.dataTable.Api(settings).columns.adjust().draw();
KTMenu.createInstances();
}
});
new $.fn.dataTable.FixedHeader( table );
table.button().add(0,'link');
table.button().add(0,'delete');
table.button().add(0,'edit');
table.button().add(0,'add');
table.buttons().container().appendTo('.col-md-6:eq(0)', table.table().container());
table.on('select deselect', function () {
var selectedRows = table.rows({ selected: true }).count();
table.button('edit:name').enable(selectedRows == 1);
table.button('clone:name').enable(selectedRows == 1);
table.button('delete:name').enable(selectedRows == 1);
table.button('link:name').enable(selectedRows == 1);
document.querySelector('[data-share-table-filter="search"]').addEventListener('keyup', function(e){
dt.search(e.target.value, true, false).draw();
});
dt.on('draw', function () {
KTMenu.createInstances();
});
});
</script>
{{end}}

View File

@@ -1,160 +1,170 @@
<!--
Copyright (C) 2019-2023 Nicola Murino
Copyright (C) 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 WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
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.
https://keenthemes.com/products/templates-mega-bundle
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/>.
KeenThemes HTML/CSS/JS components are allowed for use only within the
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 "title"}}{{.Title}}{{- end}}
{{define "page_body"}}
<div class="row justify-content-center">
<div class="col-xl-5 col-lg-6 col-md-8">
<div class="card shadow-lg my-5">
<div class="card-header py-3">
<h6 id="default_title" class="m-0 font-weight-bold text-primary">Upload one or more files to share "{{.Share.Name}}"</h6>
<h6 id="success_title" class="m-0 font-weight-bold text-primary" style="display: none;">Upload completed to share "{{.Share.Name}}"</h6>
</div>
<div class="card-body">
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
<span id="errorTxt"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div id="successMsg" class="card mb-4 border-left-success" style="display: none;">
<div id="successTxt" class="card-body">
<p>File/s uploaded successfully</p>
<p>If you want to upload other files click <a href="javascript:refreshPage();">here</a></p>
{{- define "page_body"}}
<div class="d-flex flex-center flex-column flex-column-fluid p-10 pb-lg-20">
<div class="mb-12">
<span>
<img alt="Logo" src="{{.StaticURL}}{{.Branding.LogoPath}}" class="h-80px" />
</span>
<span class="text-gray-800 fs-2 fw-semibold ps-5">
{{.Branding.ShortName}}
</span>
</div>
<div class="card shadow-sm w-lg-600px">
<div class="card-header bg-light">
<h3 class="card-title text-primary">Upload one or more files to share "{{.Share.Name}}"</h3>
</div>
<div class="card-body">
{{- template "errmsg" ""}}
<form id="upload_files_form" action="{{.UploadBasePath}}" method="POST" enctype="multipart/form-data">
<div class="fv-row">
<div class="dropzone" id="upload_files">
<div class="dz-message needsclick align-items-center">
<i class="ki-duotone ki-file-up fs-3x text-primary"><span class="path1"></span><span class="path2"></span></i>
<div class="ms-4">
<h3 class="fs-5 fw-bold text-gray-900 mb-1">Drop files here or click to upload.</h3>
</div>
</div>
</div>
</div>
<form id="upload_files_form" action="#" method="POST" enctype="multipart/form-data">
<div class="modal-body">
<input type="file" class="form-control-file" id="files_name" name="filenames" required multiple>
</div>
<button type="submit" class="btn btn-primary float-right mt-3 px-5">Submit</button>
</form>
</div>
<div class="d-flex justify-content-end mt-10">
<button type="button" id="upload_files_button" class="btn btn-primary">Submit</button>
</div>
</form>
</div>
</div>
</div>
{{end}}
{{- end}}
{{define "dialog"}}
<div class="modal fade" id="spinnerModal" tabindex="-1" role="dialog" data-keyboard="false" data-backdrop="static">
<div class="modal-dialog modal-dialog-centered justify-content-center" role="document">
<span style="color: #333333;" class="fa fa-spinner fa-spin fa-3x"></span>
</div>
</div>
{{end}}
{{define "extra_js"}}
{{- define "extra_js"}}
<script type="text/javascript">
var spinnerDone = false;
function refreshPage() {
location.reload();
function uploadFiles(files) {
let has_errors = false;
let index = 0;
let success = 0;
$('#errorMsg').addClass("d-none");
$('#loading_message').text("");
KTApp.showPageLoading();
function uploadFile() {
if (index >= files.length || has_errors) {
KTApp.hidePageLoading();
if (!has_errors) {
ModalAlert.fire({
text: `File/s uploaded successfully`,
icon: "success",
confirmButtonText: "OK",
customClass: {
confirmButton: 'btn btn-primary'
}
}).then((result) => {
if (result.isConfirmed){
location.reload();
}
});
}
return;
}
let f = files[index];
let uploadPath = '{{.UploadBasePath}}/'+fixedEncodeURIComponent(escapeHTML(f.name));
let lastModified;
try {
lastModified = f.lastModified;
} catch (e) {
console.log("unable to get last modified time from file: " + e.message);
lastModified = "";
}
let uploadTxt = f.name;
if (files.length > 1){
uploadTxt = `Upload ${index+1}/${files.length}: ${uploadTxt}`;
}
$('#loading_message').text(uploadTxt);
axios.post(uploadPath, f, {
headers: {
'X-SFTPGO-MTIME': lastModified,
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
onUploadProgress: function (progressEvent) {
if (!progressEvent.total){
return;
}
const percentage = Math.round((100 * progressEvent.loaded) / progressEvent.total);
if (percentage > 0 && percentage < 100){
$('#loading_message').text(`${uploadTxt} ${percentage}%`);
}
},
validateStatus: function (status) {
return status == 201;
}
}).then(function (response) {
index++;
success++;
uploadFile();
}).catch(function (error) {
let errorMessage = "Error uploading files";
if (error && error.response) {
if (error.response.data.message) {
errorMessage = error.response.data.message;
}
if (error.response.data.error) {
errorMessage += ": " + error.response.data.error;
}
}
index++;
has_errors = true;
$('#errorTxt').text(errorMessage);
$('#errorMsg').removeClass("d-none");
uploadFile();
});
}
uploadFile();
}
$(document).ready(function () {
$('#spinnerModal').on('shown.bs.modal', function () {
if (spinnerDone){
$('#spinnerModal').modal('hide');
}
});
$("#upload_files_form").submit(function (event){
event.preventDefault();
let files = $("#files_name")[0].files;
let has_errors = false;
let index = 0;
let success = 0;
spinnerDone = false;
$('#spinnerModal').modal('show');
$('#errorMsg').hide();
function uploadFile() {
if (index >= files.length || has_errors){
$('#spinnerModal').modal('hide');
spinnerDone = true;
if (!has_errors){
$('#errorMsg').hide();
$('#upload_files_form').hide();
$('#default_title').hide();
$('#success_title').show();
$('#successMsg').show();
}
return;
}
async function saveFile() {
let errorMessage = "Error uploading files";
let response;
try {
let f = files[index];
let uploadPath = '{{.UploadBasePath}}/'+fixedEncodeURIComponent(escapeHTML(f.name));
let lastModified;
try {
lastModified = f.lastModified;
} catch (e) {
console.log("unable to get last modified time from file: "+e.message);
lastModified = "";
}
response = await fetch(uploadPath, {
method: 'POST',
headers: {
'X-SFTPGO-MTIME': lastModified
},
credentials: 'same-origin',
redirect: 'error',
body: f
});
} catch (e){
throw Error(errorMessage+": " +e.message);
}
if (response.status == 201){
index++;
success++;
uploadFile();
} else {
let jsonResponse;
try {
jsonResponse = await response.json();
} catch(e){
throw Error(errorMessage);
}
if (jsonResponse.message) {
errorMessage = jsonResponse.message;
}
if (jsonResponse.error) {
errorMessage += ": " + jsonResponse.error;
}
throw Error(errorMessage);
}
}
saveFile().catch(function(error){
index++;
has_errors = true;
$('#errorTxt').text(error.message);
$('#errorMsg').show();
uploadFile();
KTUtil.onDOMContentLoaded(function () {
var dropzone = new Dropzone("#upload_files", {
url: "{{.UploadBasePath}}",
paramName: "filenames",
maxFiles: 200,
maxFilesize: null,
autoQueue: false,
addRemoveLinks: true,
autoProcessQueue: false,
filesizeBase: 1000,
init: function() {
var dropzone = this;
$("#upload_files_button").click(function(){
uploadFiles(dropzone.getAcceptedFiles());
});
}
});
uploadFile();
dropzone.on("addedfile", file => {
file.previewElement.querySelector(".dz-progress").style.display = 'none';
});
});
</script>
{{end}}
{{- end}}

View File

@@ -1,44 +1,61 @@
<!--
Copyright (C) 2019-2023 Nicola Murino
Copyright (C) 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 WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
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.
https://keenthemes.com/products/templates-mega-bundle
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/>.
KeenThemes HTML/CSS/JS components are allowed for use only within the
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 "baselogin" .}}
{{- template "baselogin" .}}
{{define "title"}}Two-Factor recovery{{end}}
{{- define "title"}}Two-Factor recovery{{- end}}
{{define "content"}}
{{if .Error}}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
{{.Error}}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{{end}}
<form id="login_form" action="{{.CurrentURL}}" method="POST" autocomplete="off"
class="user-custom">
<div class="form-group">
<input type="text" class="form-control form-control-user-custom"
id="inputRecoveryCode" name="recovery_code" placeholder="Recovery code" spellcheck="false" required>
</div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary btn-user-custom btn-block">
Verify
</button>
</form>
<hr>
<div>
<p>You can enter one of your recovery codes in case you lost access to your mobile device.</p>
</div>
{{end}}
{{- define "content"}}
<form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST">
<div class="container mb-10">
<div class="row align-items-center">
<div class="col-5 align-items-center">
<img alt="Logo" src="{{.StaticURL}}{{.Branding.LogoPath}}" class="h-80px h-md-90px h-lg-100px" />
</div>
<div class="col-7">
<h1 class="text-dark mb-3 ms-3">
{{.Branding.ShortName}}
</h1>
</div>
</div>
</div>
{{template "errmsg" .Error}}
<div class="fv-row mb-10">
<input class="form-control form-control-lg form-control-solid" type="text" name="recovery_code" placeholder="Recovery code" spellcheck="false" required />
</div>
<div class="text-center">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5">
<span class="indicator-label">Verify</span>
<span class="indicator-progress">Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span>
</button>
</div>
</form>
<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">
<div class="fs-6 text-gray-800">
You can enter one of your recovery codes in case you lost access to your mobile device.
</div>
</div>
</div>
</div>
{{- end}}

View File

@@ -1,49 +1,67 @@
<!--
Copyright (C) 2019-2023 Nicola Murino
Copyright (C) 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 WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
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.
https://keenthemes.com/products/templates-mega-bundle
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/>.
KeenThemes HTML/CSS/JS components are allowed for use only within the
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 "baselogin" .}}
{{- template "baselogin" .}}
{{define "title"}}Two-Factor authentication{{end}}
{{- define "title"}}Two-Factor authentication{{- end}}
{{define "content"}}
{{if .Error}}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
{{.Error}}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{{end}}
<form id="login_form" action="{{.CurrentURL}}" method="POST" autocomplete="off"
class="user-custom">
<div class="form-group">
<input type="text" class="form-control form-control-user-custom"
id="inputPasscode" name="passcode" placeholder="Authentication code" spellcheck="false" required>
</div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary btn-user-custom btn-block">
Verify
</button>
</form>
<hr>
<div>
<p>Open the two-factor authentication app on your device to view your authentication code and verify your identity.</p>
</div>
<hr>
<div>
<p><strong>Having problems?</strong></p>
<p><a href="{{.RecoveryURL}}">Enter a two-factor recovery code</a></p>
</div>
{{end}}
{{- define "content"}}
<form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST">
<div class="container mb-10">
<div class="row align-items-center">
<div class="col-5 align-items-center">
<img alt="Logo" src="{{.StaticURL}}{{.Branding.LogoPath}}" class="h-80px h-md-90px h-lg-100px" />
</div>
<div class="col-7">
<h1 class="text-dark mb-3 ms-3">
{{.Branding.ShortName}}
</h1>
</div>
</div>
</div>
{{template "errmsg" .Error}}
<div class="fv-row mb-10">
<input class="form-control form-control-lg form-control-solid" type="text" placeholder="Authentication code" name="passcode" spellcheck="false" required />
</div>
<div class="text-center">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5">
<span class="indicator-label">Verify</span>
<span class="indicator-progress">Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span>
</button>
</div>
</form>
<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">
<div class="fs-6 text-gray-700">
Open the two-factor authentication app on your device to view your authentication code and verify your identity.
</div>
<div class="fs-6 text-gray-800 mt-5">
<p class="fw-bold">Having problems?</p>
<p><a href="{{.RecoveryURL}}">Enter a two-factor recovery code</a></p>
</div>
</div>
</div>
</div>
{{- end}}

View File

@@ -1,17 +1,17 @@
<!--
Copyright (C) 2019-2023 Nicola Murino
Copyright (C) 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 WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
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.
https://keenthemes.com/products/templates-mega-bundle
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/>.
KeenThemes HTML/CSS/JS components are allowed for use only within the
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).
-->
<!DOCTYPE html>
<html lang="en">