WebAdmin: completed base page

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2024-01-01 20:09:15 +01:00
parent 784b7585c1
commit ca880f6cbb
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
11 changed files with 395 additions and 773 deletions

2
go.mod
View file

@ -54,7 +54,7 @@ require (
github.com/rs/xid v1.5.0
github.com/rs/zerolog v1.31.0
github.com/sftpgo/sdk v0.1.6-0.20231105181545-b44c8058fc25
github.com/shirou/gopsutil/v3 v3.23.11
github.com/shirou/gopsutil/v3 v3.23.12
github.com/spf13/afero v1.11.0
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.2

4
go.sum
View file

@ -356,8 +356,8 @@ github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sftpgo/sdk v0.1.6-0.20231105181545-b44c8058fc25 h1:R8cTb41ZX5WSYw8q8ufTKQfOvXh7aLQWqdnteDY/96U=
github.com/sftpgo/sdk v0.1.6-0.20231105181545-b44c8058fc25/go.mod h1:6s/PFoLUd7FXG3wGlrdVhrA0SJOwri2h9kzTph/2oiU=
github.com/shirou/gopsutil/v3 v3.23.11 h1:i3jP9NjCPUz7FiZKxlMnODZkdSIp2gnzfrvsu9CuWEQ=
github.com/shirou/gopsutil/v3 v3.23.11/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=

View file

@ -42,7 +42,6 @@ import (
"github.com/drakkan/sftpgo/v2/internal/plugin"
"github.com/drakkan/sftpgo/v2/internal/smtp"
"github.com/drakkan/sftpgo/v2/internal/util"
"github.com/drakkan/sftpgo/v2/internal/version"
"github.com/drakkan/sftpgo/v2/internal/vfs"
)
@ -160,21 +159,6 @@ type basePage struct {
FolderQuotaScanURL string
StatusURL string
MaintenanceURL string
UsersTitle string
AdminsTitle string
ConnectionsTitle string
FoldersTitle string
GroupsTitle string
EventRulesTitle string
EventActionsTitle string
RolesTitle string
StatusTitle string
MaintenanceTitle string
DefenderTitle string
IPListsTitle string
EventsTitle string
ConfigsTitle string
Version string
CSRFToken string
IsEventManagerPage bool
IsIPManagerPage bool
@ -182,7 +166,7 @@ type basePage struct {
HasDefender bool
HasSearcher bool
HasExternalLogin bool
LoggedAdmin *dataprovider.Admin
LoggedUser *dataprovider.Admin
Branding UIBranding
}
@ -416,7 +400,7 @@ type userTemplateFields struct {
func loadAdminTemplates(templatesPath string) {
usersPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateUsers),
}
@ -730,22 +714,7 @@ func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request)
StatusURL: webStatusPath,
FolderQuotaScanURL: webScanVFolderPath,
MaintenanceURL: webMaintenancePath,
UsersTitle: pageUsersTitle,
AdminsTitle: pageAdminsTitle,
ConnectionsTitle: pageConnectionsTitle,
FoldersTitle: pageFoldersTitle,
GroupsTitle: pageGroupsTitle,
EventRulesTitle: pageEventRulesTitle,
EventActionsTitle: pageEventActionsTitle,
RolesTitle: pageRolesTitle,
StatusTitle: pageStatusTitle,
MaintenanceTitle: pageMaintenanceTitle,
DefenderTitle: pageDefenderTitle,
IPListsTitle: pageIPListsTitle,
EventsTitle: pageEventsTitle,
ConfigsTitle: pageConfigsTitle,
Version: version.GetAsString(),
LoggedAdmin: getAdminFromToken(r),
LoggedUser: getAdminFromToken(r),
IsEventManagerPage: isEventManagerResource(currentURL),
IsIPManagerPage: isIPListsResource(currentURL),
IsServerManagerPage: isServerManagerResource(currentURL),
@ -859,7 +828,7 @@ func (s *httpdServer) renderMFAPage(w http.ResponseWriter, r *http.Request) {
SaveTOTPURL: webAdminTOTPSavePath,
RecCodesURL: webAdminRecoveryCodesPath,
}
admin, err := dataprovider.AdminExists(data.LoggedAdmin.Username)
admin, err := dataprovider.AdminExists(data.LoggedUser.Username)
if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return
@ -873,7 +842,7 @@ func (s *httpdServer) renderProfilePage(w http.ResponseWriter, r *http.Request,
basePage: s.getBasePageData(pageProfileTitle, webAdminProfilePath, r),
Error: error,
}
admin, err := dataprovider.AdminExists(data.LoggedAdmin.Username)
admin, err := dataprovider.AdminExists(data.LoggedUser.Username)
if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return
@ -1015,7 +984,7 @@ func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, use
}
}
var roles []dataprovider.Role
if basePage.LoggedAdmin.Role == "" {
if basePage.LoggedUser.Role == "" {
var err error
roles, err = s.getWebRoles(w, r, 10, true)
if err != nil {
@ -1049,7 +1018,7 @@ func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, use
Filesystem: user.FsConfig,
IsUserPage: true,
IsGroupPage: false,
IsHidden: basePage.LoggedAdmin.Filters.Preferences.HideFilesystem(),
IsHidden: basePage.LoggedUser.Filters.Preferences.HideFilesystem(),
HasUsersBaseDir: dataprovider.HasUsersBaseDir(),
DirPath: user.HomeDir,
},

View file

@ -27,7 +27,24 @@
"error429": "Too Many Requests",
"error500": "Internal Server Error",
"errorPDF": "Unable to show PDF file",
"error_editor": "Cannot open file editor"
"error_editor": "Cannot open file editor",
"users": "Users",
"groups": "Groups",
"folders": "Virtual folders",
"connections": "Active sessions",
"event_manager": "Event Manager",
"event_rules": "Rules",
"event_actions": "Actions",
"ip_manager": "IP Manager",
"ip_lists": "IP Lists",
"defender": "Auto Block List",
"admins": "Admins",
"roles": "Roles",
"server_manager": "Server Manager",
"configs": "Configurations",
"logs": "Logs",
"maintenance": "Maintenance",
"status": "Status"
},
"setup": {
"desc": "To start using SFTPGo you need to create an administrator user",
@ -370,6 +387,7 @@
"required": "Password change is required. Set a new password to continue using your account"
},
"user": {
"view_manage": "View and manage users",
"username_reserved": "The specified username is reserved",
"username_invalid": "The specified username is not valid, the following characters are allowed: a-zA-Z0-9-_.~",
"home_required": "The home directory is mandatory",

View file

@ -27,7 +27,24 @@
"error429": "Troppe richieste",
"error500": "Errore interno del server",
"errorPDF": "Impossibile mostrare il file PDF",
"error_editor": "Impossibile aprire l'editor di file"
"error_editor": "Impossibile aprire l'editor di file",
"users": "Utenti",
"groups": "Gruppi",
"folders": "Cartelle virtuali",
"connections": "Sessioni attive",
"event_manager": "Gestione eventi",
"event_rules": "Regole",
"event_actions": "Azioni",
"ip_manager": "Gestione IP",
"ip_lists": "Liste IP",
"defender": "Blocchi automatici",
"admins": "Amministratori",
"roles": "Ruoli",
"server_manager": "Gestione server",
"configs": "Configurazioni",
"logs": "Registro eventi",
"maintenance": "Manutenzione",
"status": "Stato"
},
"setup": {
"desc": "Per iniziare a utilizzare SFTPGo devi creare un utente amministratore",
@ -370,6 +387,7 @@
"required": "È richiesta la modifica della password. Imposta una nuova password per continuare a utilizzare il tuo account"
},
"user": {
"view_manage": "Visualizza e gestisci utenti",
"username_reserved": "Il nome utente specificato è riservato",
"username_invalid": "Il nome utente specificato non è valido, sono consentiti i seguenti caratteri: a-zA-Z0-9-_.~",
"home_required": "La directory principale è obbligatoria",

View file

@ -182,7 +182,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
expirationTime: 7 * 24 * 60 * 60 * 1000, // 7 days
defaultVersion: '{{.Version}}'
}, {
loadPath: '{{.StaticURL}}/locales/{{"{{lng}}"}}/{{"{{ns}}"}}.json'
loadPath: '{{.StaticURL}}/locales/{{"{{lng}}"}}/{{"{{ns}}"}}.json?_='+new Date().getTime().toString()
}
]
}

View file

@ -1,345 +1,286 @@
<!--
Copyright (C) 2019 Nicola Murino
Copyright (C) 2024 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"}}
<!DOCTYPE html>
<html lang="en">
<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-->
{{- range .Branding.DefaultCSS}}
<link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
{{- end}}
<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 .LoggedAdmin.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">
{{ if .LoggedAdmin.HasPermission "view_users"}}
<li class="nav-item {{if eq .CurrentURL .UsersURL}}active{{end}}">
<a class="nav-link" href="{{.UsersURL}}">
<i class="fas fa-users"></i>
<span>{{.UsersTitle}}</span></a>
</li>
{{ end }}
{{ if .LoggedAdmin.HasPermission "manage_groups"}}
<li class="nav-item {{if eq .CurrentURL .GroupsURL}}active{{end}}">
<a class="nav-link" href="{{.GroupsURL}}">
<i class="fas fa-user-friends"></i>
<span>{{.GroupsTitle}}</span></a>
</li>
{{end}}
{{ if .LoggedAdmin.HasPermission "manage_folders"}}
<li class="nav-item {{if eq .CurrentURL .FoldersURL}}active{{end}}">
<a class="nav-link" href="{{.FoldersURL}}">
<i class="fas fa-folder"></i>
<span>{{.FoldersTitle}}</span></a>
</li>
{{end}}
{{ if .LoggedAdmin.HasPermission "view_conns"}}
<li class="nav-item {{if eq .CurrentURL .ConnectionsURL}}active{{end}}">
<a class="nav-link" href="{{.ConnectionsURL}}">
<i class="fas fa-exchange-alt"></i>
<span>{{.ConnectionsTitle}}</span></a>
</li>
{{end}}
{{ if .LoggedAdmin.HasPermission "manage_event_rules"}}
<li class="nav-item {{if .IsEventManagerPage}}active{{end}}">
<a class="nav-link {{if not .IsEventManagerPage}}collapsed{{end}}" href="#" data-toggle="collapse" data-target="#collapseEventManager"
aria-expanded="true" aria-controls="collapseEventManager">
<i class="fas fa-calendar-alt"></i>
<span>Event Manager</span>
</a>
<div id="collapseEventManager" class="collapse {{if .IsEventManagerPage}}show{{end}}" aria-labelledby="headingEventManager" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<a class="collapse-item {{if eq .CurrentURL .EventRulesURL}}active{{end}}" href="{{.EventRulesURL}}">{{.EventRulesTitle}}</a>
<a class="collapse-item {{if eq .CurrentURL .EventActionsURL}}active{{end}}" href="{{.EventActionsURL}}">{{.EventActionsTitle}}</a>
</div>
</div>
</li>
{{end}}
{{ if or (.LoggedAdmin.HasPermission "manage_ip_lists") (and .HasDefender (.LoggedAdmin.HasPermission "view_defender"))}}
<li class="nav-item {{if .IsIPManagerPage}}active{{end}}">
<a class="nav-link {{if not .IsIPManagerPage}}collapsed{{end}}" href="#" data-toggle="collapse" data-target="#collapseIPManager"
aria-expanded="true" aria-controls="collapseIPManager">
<i class="fas fa-shield-alt"></i>
<span>IP Manager</span>
</a>
<div id="collapseIPManager" class="collapse {{if .IsIPManagerPage}}show{{end}}" aria-labelledby="headingIPManager" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
{{ if .LoggedAdmin.HasPermission "manage_ip_lists"}}
<a class="collapse-item {{if eq .CurrentURL .IPListsURL}}active{{end}}" href="{{.IPListsURL}}">{{.IPListsTitle}}</a>
{{end}}
{{ if and .HasDefender (.LoggedAdmin.HasPermission "view_defender")}}
<a class="collapse-item {{if eq .CurrentURL .DefenderURL}}active{{end}}" href="{{.DefenderURL}}">{{.DefenderTitle}}</a>
{{end}}
</div>
</div>
</li>
{{end}}
{{ if .LoggedAdmin.HasPermission "manage_admins"}}
<li class="nav-item {{if eq .CurrentURL .AdminsURL}}active{{end}}">
<a class="nav-link" href="{{.AdminsURL}}">
<i class="fas fa-user-cog"></i>
<span>{{.AdminsTitle}}</span></a>
</li>
{{end}}
{{ if .LoggedAdmin.HasPermission "manage_roles"}}
<li class="nav-item {{if eq .CurrentURL .RolesURL}}active{{end}}">
<a class="nav-link" href="{{.RolesURL}}">
<i class="fas fa-user-lock"></i>
<span>{{.RolesTitle}}</span></a>
</li>
{{end}}
{{ if or (.LoggedAdmin.HasPermission "manage_system") (.LoggedAdmin.HasPermission "view_status") (and .HasSearcher (.LoggedAdmin.HasPermission "view_events"))}}
<li class="nav-item {{if .IsServerManagerPage}}active{{end}}">
<a class="nav-link {{if not .IsServerManagerPage}}collapsed{{end}}" href="#" data-toggle="collapse" data-target="#collapseServerManager"
aria-expanded="true" aria-controls="collapseServerManager">
<i class="fas fa-tools"></i>
<span>Server Manager</span>
</a>
<div id="collapseServerManager" class="collapse {{if .IsServerManagerPage}}show{{end}}" aria-labelledby="headingServerManager" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
{{ if .LoggedAdmin.HasPermission "manage_system"}}
<a class="collapse-item {{if eq .CurrentURL .ConfigsURL}}active{{end}}" href="{{.ConfigsURL}}">{{.ConfigsTitle}}</a>
{{end}}
{{ if and .HasSearcher (.LoggedAdmin.HasPermission "view_events")}}
<a class="collapse-item {{if eq .CurrentURL .EventsURL}}active{{end}}" href="{{.EventsURL}}">{{.EventsTitle}}</a>
{{end}}
{{ if .LoggedAdmin.HasPermission "manage_system"}}
<a class="collapse-item {{if eq .CurrentURL .MaintenanceURL}}active{{end}}" href="{{.MaintenanceURL}}">{{.MaintenanceTitle}}</a>
{{end}}
{{ if .LoggedAdmin.HasPermission "view_status"}}
<a class="collapse-item {{if eq .CurrentURL .StatusURL}}active{{end}}" href="{{.StatusURL}}">{{.StatusTitle}}</a>
{{end}}
</div>
</div>
</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 .LoggedAdmin.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">{{.LoggedAdmin.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 not .HasExternalLogin}}
<a class="dropdown-item" href="{{.ProfileURL}}">
<i class="fas fa-user fa-sm fa-fw mr-2 text-gray-400"></i>
Profile
</a>
<a class="dropdown-item" href="{{.ChangePwdURL}}">
<i class="fas fa-key fa-sm fa-fw mr-2 text-gray-400"></i>
Change password
</a>
{{if .LoggedAdmin.CanManageMFA}}
<a class="dropdown-item" href="{{.MFAURL}}">
<i class="fas fa-user-lock fa-sm fa-fw mr-2 text-gray-400"></i>
Two-Factor Auth
</a>
{{end}}
<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>
</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 .LoggedAdmin.Username}}
<!-- Footer -->
<footer class="sticky-footer bg-white">
<div class="container my-auto">
<div class="copyright text-center my-auto">
<span>SFTPGo {{.Version}}</span>
</div>
</div>
</footer>
<!-- End of Footer -->
{{end}}
</div>
<!-- End of Content Wrapper -->
{{- define "navitems"}}
{{- block "additionalnavitems" .}}{{- end}}
{{- template "theme-switcher"}}
<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="click" 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>
<!-- End of Page Wrapper -->
<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">
<div class="me-5">
<i class="ki-duotone ki-user fs-2">
<i class="path1"></i>
<i class="path2"></i>
</i>
</div>
<div class="d-flex flex-column">
<div class="fw-semibold d-flex align-items-center fs-5">
<span class="w-175px wrap-word">{{.LoggedUser.Username}}</span>
</div>
</div>
</div>
</div>
<div class="separator my-2"></div>
{{if not .HasExternalLogin}}
<div class="menu-item px-3 my-0">
<a href="{{.ProfileURL}}" class="menu-link px-3 py-2">
<span data-i18n="title.profile" class="menu-title">Profile</span>
</a>
</div>
<div class="menu-item px-3 my-0">
<a href="{{.ChangePwdURL}}" class="menu-link px-3 py-2">
<span data-i18n="title.change_password" class="menu-title">Change password</span>
</a>
</div>
{{if .LoggedUser.CanManageMFA}}
<div class="menu-item px-3 my-0">
<a href="{{.MFAURL}}" class="menu-link px-3 py-2">
<span data-i18n="title.two_factor_auth" class="menu-title">Two-factor authentication</span>
</a>
</div>
{{- end}}
{{- end}}
<div class="menu-item px-3 my-0">
<a id="id_logout_link" href="#" class="menu-link px-3 py-2">
<span data-i18n="login.signout" class="menu-title">Sign out</span>
</a>
</div>
</div>
</div>
{{- end}}
<!-- Scroll to Top Button-->
<a class="scroll-to-top rounded" href="#page-top">
<i class="fas fa-angle-up"></i>
{{- define "sidebaritems"}}
{{- if .LoggedUser.HasPermission "view_users"}}
<div class="menu-item">
<a class="menu-link {{- if eq .CurrentURL .UsersURL}} active{{- end}}" href="{{.UsersURL}}">
<span class="menu-icon">
<i class="ki-duotone ki-people fs-1">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
</span>
<span data-i18n="title.users" class="menu-title">Users</span>
</a>
{{if .LoggedAdmin.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>
{{- end}}
{{- if .LoggedUser.HasPermission "manage_groups"}}
<div class="menu-item">
<a class="menu-link {{- if eq .CurrentURL .GroupsURL}} active{{- end}}" href="{{.GroupsURL}}">
<span class="menu-icon">
<i class="ki-duotone ki-profile-user fs-1">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
</i>
</span>
<span data-i18n="title.groups" class="menu-title">Groups</span>
</a>
</div>
{{- end}}
{{- if .LoggedUser.HasPermission "manage_folders"}}
<div class="menu-item">
<a class="menu-link {{- if eq .CurrentURL .FoldersURL}} active{{- end}}" href="{{.FoldersURL}}">
<span class="menu-icon">
<i class="ki-duotone ki-folder fs-1">
<span class="path1"></span>
<span class="path2"></span>
</i>
</span>
<span data-i18n="title.folders" class="menu-title">Folders</span>
</a>
</div>
{{- end}}
{{- if .LoggedUser.HasPermission "view_conns"}}
<div class="menu-item">
<a class="menu-link {{- if eq .CurrentURL .ConnectionsURL}} active{{- end}}" href="{{.ConnectionsURL}}">
<span class="menu-icon">
<i class="ki-duotone ki-arrow-up-down fs-1">
<span class="path1"></span>
<span class="path2"></span>
</i>
</span>
<span data-i18n="title.connections" class="menu-title">Connections</span>
</a>
</div>
{{- end}}
{{ if .LoggedUser.HasPermission "manage_event_rules"}}
<div data-kt-menu-trigger="click" class="menu-item menu-accordion {{- if .IsEventManagerPage}} here show{{- end}}">
<span class="menu-link">
<span class="menu-icon">
<i class="ki-duotone ki-calendar fs-1">
<span class="path1"></span>
<span class="path2"></span>
</i>
</span>
<span data-i18n="title.event_manager" class="menu-title">Event Manager</span>
<span class="menu-arrow"></span>
</span>
<div class="menu-sub menu-sub-accordion">
<div class="menu-item">
<a class="menu-link {{- if eq .CurrentURL .EventRulesURL}} active{{- end}}" href="{{.EventRulesURL}}">
<span class="menu-bullet">
<span class="bullet bullet-dot"></span>
</span>
<span data-i18n="title.event_rules" class="menu-title fs-5 fw-semibold">Rules</span>
</a>
</div>
<div class="menu-item">
<a class="menu-link {{- if eq .CurrentURL .EventActionsURL}} active{{- end}}" href="{{.EventActionsURL}}">
<span class="menu-bullet">
<span class="bullet bullet-dot"></span>
</span>
<span data-i18n="title.event_actions" class="menu-title fs-5 fw-semibold">Actions</span>
</a>
</div>
</div>
{{end}}
</div>
{{- end}}
{{- if or (.LoggedUser.HasPermission "manage_ip_lists") (and .HasDefender (.LoggedUser.HasPermission "view_defender"))}}
<div data-kt-menu-trigger="click" class="menu-item menu-accordion {{- if .IsIPManagerPage}} here show{{- end}}">
<span class="menu-link">
<span class="menu-icon">
<i class="ki-duotone ki-security-check fs-1">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
</i>
</span>
<span data-i18n="title.ip_manager" class="menu-title">IP Manager</span>
<span class="menu-arrow"></span>
</span>
<div class="menu-sub menu-sub-accordion">
{{- if .LoggedUser.HasPermission "manage_ip_lists"}}
<div class="menu-item">
<a class="menu-link {{- if eq .CurrentURL .IPListsURL}} active{{- end}}" href="{{.IPListsURL}}">
<span class="menu-bullet">
<span class="bullet bullet-dot"></span>
</span>
<span data-i18n="title.ip_lists" class="menu-title fs-5 fw-semibold">IP lists</span>
</a>
</div>
{{- end}}
{{- if and .HasDefender (.LoggedUser.HasPermission "view_defender")}}
<div class="menu-item">
<a class="menu-link {{- if eq .CurrentURL .DefenderURL}} active{{- end}}" href="{{.DefenderURL}}">
<span class="menu-bullet">
<span class="bullet bullet-dot"></span>
</span>
<span data-i18n="title.defender" class="menu-title fs-5 fw-semibold">Auto Blocklist</span>
</a>
</div>
{{- end}}
</div>
</div>
{{- end}}
{{- if .LoggedUser.HasPermission "manage_admins"}}
<div class="menu-item">
<a class="menu-link {{- if eq .CurrentURL .AdminsURL}} active{{- end}}" href="{{.AdminsURL}}">
<span class="menu-icon">
<i class="ki-duotone ki-security-user fs-1">
<span class="path1"></span>
<span class="path2"></span>
</i>
</span>
<span data-i18n="title.admins" class="menu-title">Admins</span>
</a>
</div>
{{- end}}
{{- if .LoggedUser.HasPermission "manage_roles"}}
<div class="menu-item">
<a class="menu-link {{- if eq .CurrentURL .RolesURL}} active{{- end}}" href="{{.RolesURL}}">
<span class="menu-icon">
<i class="ki-duotone ki-user-tick fs-1">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
</i>
</span>
<span data-i18n="title.roles" class="menu-title">Roles</span>
</a>
</div>
{{- end}}
{{- if or (.LoggedUser.HasPermission "manage_system") (.LoggedUser.HasPermission "view_status") (and .HasSearcher (.LoggedUser.HasPermission "view_events"))}}
<div data-kt-menu-trigger="click" class="menu-item menu-accordion {{- if .IsServerManagerPage}} here show{{- end}}">
<span class="menu-link">
<span class="menu-icon">
<i class="ki-duotone ki-setting-3 fs-1">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
</span>
<span data-i18n="title.server_manager" class="menu-title">Server Manager</span>
<span class="menu-arrow"></span>
</span>
<div class="menu-sub menu-sub-accordion">
{{- if .LoggedUser.HasPermission "manage_system"}}
<div class="menu-item">
<a class="menu-link {{- if eq .CurrentURL .ConfigsURL}} active{{- end}}" href="{{.ConfigsURL}}">
<span class="menu-bullet">
<span class="bullet bullet-dot"></span>
</span>
<span data-i18n="title.configs" class="menu-title fs-5 fw-semibold">Configurations</span>
</a>
</div>
{{- end}}
{{ if and .HasSearcher (.LoggedUser.HasPermission "view_events")}}
<div class="menu-item">
<a class="menu-link {{- if eq .CurrentURL .EventsURL}} active{{- end}}" href="{{.EventsURL}}">
<span class="menu-bullet">
<span class="bullet bullet-dot"></span>
</span>
<span data-i18n="title.logs" class="menu-title fs-5 fw-semibold">Logs</span>
</a>
</div>
{{- end}}
{{- if .LoggedUser.HasPermission "manage_system"}}
<div class="menu-item">
<a class="menu-link {{- if eq .CurrentURL .MaintenanceURL}} active{{- end}}" href="{{.MaintenanceURL}}">
<span class="menu-bullet">
<span class="bullet bullet-dot"></span>
</span>
<span data-i18n="title.maintenance" class="menu-title fs-5 fw-semibold">Maintenance</span>
</a>
</div>
{{- end}}
{{- if .LoggedUser.HasPermission "view_status"}}
<div class="menu-item">
<a class="menu-link {{- if eq .CurrentURL .StatusURL}} active{{- end}}" href="{{.StatusURL}}">
<span class="menu-bullet">
<span class="bullet bullet-dot"></span>
</span>
<span data-i18n="title.status" class="menu-title fs-5 fw-semibold">Status</span>
</a>
</div>
{{- end}}
</div>
</div>
{{- end}}
{{block "dialog" .}}{{end}}
<!-- 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>
<script type="text/javascript">
function escapeHTML(str) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
}
function unescapeHTML(escapedStr) {
var div = document.createElement('div');
div.innerHTML = escapedStr;
var child = div.childNodes[0];
return child ? child.nodeValue : '';
}
function fixedEncodeURIComponent(str) {
return encodeURIComponent(unescapeHTML(str)).replace(/[!'()*]/g, function (c) {
return '%' + c.charCodeAt(0).toString(16);
});
}
</script>
<!-- Page level plugins -->
{{block "extra_js" .}}{{end}}
</body>
</html>
{{end}}
{{- end}}

View file

@ -1,386 +1,65 @@
<!--
Copyright (C) 2019 Nicola Murino
Copyright (C) 2024 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}}/assets/plugins/custom/datatables/datatables.bundle.css" rel="stylesheet" type="text/css"/>
{{- 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">
<link href="{{.StaticURL}}/vendor/datatables/colReorder.bootstrap4.min.css" rel="stylesheet">
{{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 id="successMsg" class="card mb-4 border-left-success" style="display: none;">
<div id="successTxt" class="card-body"></div>
</div>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">View and manage users</h6>
{{- define "page_body"}}
<div class="card shadow-sm">
<div class="card-header bg-light">
<h3 data-i18n="user.view_manage" class="card-title section-title">View and manage users</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">
<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 data-i18n="general.loading" class="text-gray-700">Loading...</span>
</div>
<div id="card_content" class="d-none">
<div class="d-flex flex-stack flex-wrap 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 name="search" data-i18n="[placeholder]general.search" type="text" data-table-filter="search"
class="form-control rounded-1 w-250px ps-15 me-5" placeholder="Search" />
</div>
<div class="d-flex justify-content-end" data-table-toolbar="base">
<a href="{{.UserURL}}" class="btn btn-primary">
<i class="ki-duotone ki-plus fs-2"></i>
<span data-i18n="general.add">Add</span>
</a>
</div>
</div>
<table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Status</th>
<th>Last login</th>
<th>Description</th>
<th>Email</th>
<th>Storage</th>
<th>Groups</th>
<th>MFA</th>
<th>Bandwidth</th>
<th>Quota</th>
<th>Role</th>
<th>Other</th>
<th></th>
<tr class="text-start text-muted fw-bold fs-6 gs-0">
<th data-i18n="general.name">Name</th>
<th data-i18n="share.scope">Scope</th>
<th data-i18n="general.info">Info</th>
<th class="min-w-100px"></th>
</tr>
</thead>
<tbody>
{{range .Users}}
<tr>
<td>{{.ID}}</td>
<td>{{.Username}}</td>
<td>{{.GetStatusAsString}}</td>
<td>{{.GetLastLoginAsString}}</td>
<td>{{.Description}}</td>
<td>{{.Email}}</td>
<td>{{.GetStorageDescrition}}</td>
<td>{{.GetGroupsAsString}}</td>
<td>{{.GetMFAStatusAsString}}</td>
<td>{{.GetBandwidthAsString}}</td>
<td>{{.GetQuotaSummary}}</td>
<td>{{.Role}}</td>
<td>{{.GetInfoString}}</td>
<td>{{.GetLastQuotaUpdateAsString}}</td>
</tr>
{{end}}
</tbody>
<tbody id="table_body" class="text-gray-800 fw-semibold"></tbody>
</table>
</div>
</div>
</div>
{{end}}
{{define "dialog"}}
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel"
aria-hidden="true">
<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 user?</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>
{{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/buttons.colVis.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}}/vendor/datatables/ellipsis.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.colReorder.min.js"></script>
<script type="text/javascript">
function deleteAction() {
let table = $('#dataTable').DataTable();
table.button('delete:name').enable(false);
let username = table.row({ selected: true }).data()[1];
let path = '{{.UserURL}}' + "/" + fixedEncodeURIComponent(username);
$('#deleteModal').modal('hide');
$('#errorMsg').hide();
$.ajax({
url: path,
type: 'DELETE',
dataType: 'json',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
timeout: 15000,
success: function (result) {
window.location.href = '{{.UsersURL}}';
},
error: function ($xhr, textStatus, errorThrown) {
var txt = "Unable to delete the selected user";
if ($xhr) {
var json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
}
}
}
$('#errorTxt').text(txt);
$('#errorMsg').show();
}
});
}
$(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 = '{{.UserURL}}';
}
};
$.fn.dataTable.ext.buttons.edit = {
text: '<i class="fas fa-pen"></i>',
name: 'edit',
titleAttr: "Edit",
action: function (e, dt, node, config) {
var username = dt.row({ selected: true }).data()[1];
var path = '{{.UserURL}}' + "/" + fixedEncodeURIComponent(username);
window.location.href = path;
},
enabled: false
};
$.fn.dataTable.ext.buttons.template = {
text: '<i class="fas fa-clone"></i>',
name: 'template',
titleAttr: "Template",
action: function (e, dt, node, config) {
var selectedRows = table.rows({ selected: true }).count();
if (selectedRows == 1){
var username = dt.row({ selected: true }).data()[1];
var path = '{{.UserTemplateURL}}' + "?from=" + encodeURIComponent(username);
window.location.href = path;
} else {
window.location.href = '{{.UserTemplateURL}}';
}
}
};
$.fn.dataTable.ext.buttons.delete = {
text: '<i class="fas fa-trash"></i>',
name: 'delete',
titleAttr: "Delete",
action: function (e, dt, node, config) {
/*console.log("delete clicked, num row selected: " + dt.rows({ selected: true }).count());
var data = dt.rows({ selected: true }).data();
for (var i = 0; i < data.length; i++) {
console.log("selected row data: " + JSON.stringify(data[i]));
}*/
$('#deleteModal').modal('show');
},
enabled: false
};
$.fn.dataTable.ext.buttons.quota_scan = {
text: '<i class="fas fa-redo-alt"></i>',
name: 'quota_scan',
titleAttr: 'Quota Scan',
action: function (e, dt, node, config) {
dt.button('quota_scan:name').enable(false);
var username = dt.row({ selected: true }).data()[1];
var path = '{{.QuotaScanURL}}'+ "/" + fixedEncodeURIComponent(username);
$.ajax({
url: path,
type: 'POST',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
timeout: 15000,
success: function (result) {
dt.button('quota_scan:name').enable(true);
$('#successTxt').text("Quota scan started for the selected user. Please reload the user's page to check when the scan ends");
$('#successMsg').show();
setTimeout(function () {
$('#successMsg').hide();
}, 15000);
},
error: function ($xhr, textStatus, errorThrown) {
dt.button('quota_scan:name').enable(true);
var txt = "Unable to update quota for the selected user";
if ($xhr) {
var json = $xhr.responseJSON;
if (json) {
if (json.message) {
txt += ": " + json.message;
} else if (json.error) {
txt += ": " + json.error;
}
}
}
$('#errorTxt').text(txt);
$('#errorMsg').show();
}
});
},
enabled: false
};
let dateFn = $.fn.dataTable.render.datetime();
let table = $('#dataTable').DataTable({
"select": {
"style": "single",
"blurable": true
},
"colReorder": {
"enable": true,
"fixedColumnsLeft": 2
},
"stateSave": true,
"stateDuration": 0,
"buttons": [
{
"text": "Column visibility",
"extend": "colvis",
"columns": ":not(.noVis)"
}
],
"columnDefs": [
{
"targets": [0,13],
"visible": false,
"searchable": false,
"className": "noVis"
},
{
"targets": [1],
"className": "noVis"
},
{
"targets": [3],
"render": $.fn.dataTable.render.datetime()
},
{
"targets": [4,5,8,11],
"visible": false
},
{
"targets": [6,7],
"visible": false,
"render": $.fn.dataTable.render.ellipsis(50, true)
},
{
"targets": [9],
"visible": false,
"render": $.fn.dataTable.render.ellipsis(40, true)
},
{
"targets": [10],
"visible": false,
"render": function ( data, type, row, meta ) {
if (type !== 'display') {
return data;
}
if (row[13] !== ""){
var formattedDate = dateFn(row[13], type);
data = `${data}. Updated at: ${formattedDate}`;
}
let ellipsisFn = $.fn.dataTable.render.ellipsis(70, true);
return ellipsisFn(data, type);
}
},
{
"targets": [12],
"visible": false,
"render": $.fn.dataTable.render.ellipsis(100, true)
}
],
"scrollX": false,
"scrollY": false,
"responsive": true,
"language": {
"emptyTable": "No user defined"
},
"order": [[1, 'asc']]
});
new $.fn.dataTable.FixedHeader( table );
{{if .LoggedAdmin.HasPermission "quota_scans"}}
table.button().add(0,'quota_scan');
{{end}}
{{if .LoggedAdmin.HasPermission "del_users"}}
table.button().add(0,'delete');
{{end}}
{{if .LoggedAdmin.HasPermission "manage_system"}}
table.button().add(0,'template');
{{end}}
{{if .LoggedAdmin.HasPermission "edit_users"}}
table.button().add(0,'edit');
{{end}}
{{if .LoggedAdmin.HasPermission "add_users"}}
table.button().add(0,'add');
{{end}}
table.buttons().container().appendTo('.col-md-6:eq(0)', table.table().container());
table.on('select deselect', function () {
var selectedRows = table.rows({ selected: true }).count();
{{if .LoggedAdmin.HasPermission "edit_users"}}
table.button('edit:name').enable(selectedRows == 1);
{{end}}
{{if .LoggedAdmin.HasPermission "del_users"}}
table.button('delete:name').enable(selectedRows == 1);
{{end}}
{{if .LoggedAdmin.HasPermission "quota_scans"}}
table.button('quota_scan:name').enable(selectedRows == 1);
{{end}}
});
});
</script>
{{end}}
{{- end}}

View file

@ -68,7 +68,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
placeholder="Paste your public key here">{{$val}}</textarea>
</div>
<div class="col-md-3">
<a data-i18n="general.delete" href="#" data-repeater-delete
<a href="#" 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>
@ -77,7 +77,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<span class="path4"></span>
<span class="path5"></span>
</i>
Delete
<span data-i18n="general.delete">Delete</span>
</a>
</div>
</div>
@ -90,7 +90,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
placeholder="Paste your public key here"></textarea>
</div>
<div class="col-md-3">
<a data-i18n="general.delete" href="#" data-repeater-delete
<a href="#" 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>
@ -99,7 +99,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<span class="path4"></span>
<span class="path5"></span>
</i>
Delete
<span data-i18n="general.delete">Delete</span>
</a>
</div>
</div>
@ -109,9 +109,9 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div>
<div class="form-group mt-5">
<a data-i18n="general.add" href="#" data-repeater-create class="btn btn-light-primary">
<a href="#" data-repeater-create class="btn btn-light-primary">
<i class="ki-duotone ki-plus fs-3"></i>
Add
<span data-i18n="general.add">Add</span>
</a>
</div>
</div>

View file

@ -61,7 +61,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
name="path" value="{{$val}}" />
</div>
<div class="col-md-3">
<a data-i18n="general.delete" href="#" data-repeater-delete
<a href="#" 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>
@ -70,7 +70,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<span class="path4"></span>
<span class="path5"></span>
</i>
Delete
<span data-i18n="general.delete">Delete</span>
</a>
</div>
</div>
@ -83,7 +83,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
name="path" value="" />
</div>
<div class="col-md-3">
<a data-i18n="general.delete" href="#" data-repeater-delete
<a href="#" 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>
@ -92,7 +92,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<span class="path4"></span>
<span class="path5"></span>
</i>
Delete
<span data-i18n="general.delete">Delete</span>
</a>
</div>
</div>
@ -102,9 +102,9 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div>
<div class="form-group mt-5">
<a data-i18n="general.add" href="#" data-repeater-create class="btn btn-light-primary">
<a href="#" data-repeater-create class="btn btn-light-primary">
<i class="ki-duotone ki-plus fs-3"></i>
Add
<span data-i18n="general.add">Add</span>
</a>
</div>
</div>

View file

@ -13,15 +13,13 @@ 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 "extra_css"}}
<link href="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css" rel="stylesheet" type="text/css"/>
{{- end}}
{{define "page_body"}}
{{- define "page_body"}}
<div class="card shadow-sm">
<div class="card-header bg-light">
<h3 data-i18n="share.view_manage" class="card-title section-title">View and manage shares</h3>
@ -29,23 +27,22 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div id="card_body" class="card-body">
<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 data-i18n="general.loading" class="text-gray-600">Loading...</span>
<span data-i18n="general.loading" class="text-gray-700">Loading...</span>
</div>
<div id="card_content" class="d-none">
<div class="d-flex flex-stack mb-5">
<div class="d-flex flex-stack flex-wrap 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 name="search" data-i18n="[placeholder]general.search" type="text" data-share-table-filter="search"
<input name="search" data-i18n="[placeholder]general.search" type="text" data-table-filter="search"
class="form-control rounded-1 w-250px ps-15 me-5" placeholder="Search" />
</div>
<div class="d-flex justify-content-end" data-share-table-toolbar="base">
<a data-i18n="general.add" href="{{.ShareURL}}" class="btn btn-primary">
<div class="d-flex justify-content-end" data-table-toolbar="base">
<a href="{{.ShareURL}}" class="btn btn-primary">
<i class="ki-duotone ki-plus fs-2"></i>
Add
<span data-i18n="general.add">Add</span>
</a>
</div>
</div>
@ -372,7 +369,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
stateDuration: 0,
stateLoadParams: function (settings, data) {
if (data.search.search){
const filterSearch = document.querySelector('[data-share-table-filter="search"]');
const filterSearch = document.querySelector('[data-table-filter="search"]');
filterSearch.value = data.search.search;
}
},
@ -405,7 +402,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}
var handleSearchDatatable = function () {
const filterSearch = document.querySelector('[data-share-table-filter="search"]');
const filterSearch = document.querySelector('[data-table-filter="search"]');
filterSearch.addEventListener('keyup', function (e) {
dt.rows().deselect();
dt.search(e.target.value, true, false).draw();