WIP new WebAdmin: roles page

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2024-01-21 16:49:04 +01:00
parent 0d387d9799
commit 3f479c5537
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
11 changed files with 405 additions and 274 deletions

View file

@ -1787,6 +1787,8 @@ func (s *httpdServer) setupWebAdminRoutes() {
Post(webAdminEventRulePath+"/run/{name}", runOnDemandRule) Post(webAdminEventRulePath+"/run/{name}", runOnDemandRule)
router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie). router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie).
Get(webAdminRolesPath, s.handleWebGetRoles) Get(webAdminRolesPath, s.handleWebGetRoles)
router.With(s.checkPerm(dataprovider.PermAdminManageRoles), compressor.Handler, s.refreshCookie).
Get(webAdminRolesPath+jsonAPISuffix, getAllRoles)
router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie). router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie).
Get(webAdminRolePath, s.handleWebAddRoleGet) Get(webAdminRolePath, s.handleWebAddRoleGet)
router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Post(webAdminRolePath, s.handleWebAddRolePost) router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Post(webAdminRolePath, s.handleWebAddRolePost)

View file

@ -102,8 +102,6 @@ const (
pageStatusTitle = "Status" pageStatusTitle = "Status"
pageEventRulesTitle = "Event rules" pageEventRulesTitle = "Event rules"
pageEventActionsTitle = "Event actions" pageEventActionsTitle = "Event actions"
pageRolesTitle = "Roles"
pageChangePwdTitle = "Change password"
pageMaintenanceTitle = "Maintenance" pageMaintenanceTitle = "Maintenance"
pageDefenderTitle = "Auto Blocklist" pageDefenderTitle = "Auto Blocklist"
pageIPListsTitle = "IP Lists" pageIPListsTitle = "IP Lists"
@ -169,11 +167,6 @@ type adminsPage struct {
Admins []dataprovider.Admin Admins []dataprovider.Admin
} }
type rolesPage struct {
basePage
Roles []dataprovider.Role
}
type eventRulesPage struct { type eventRulesPage struct {
basePage basePage
Rules []dataprovider.EventRule Rules []dataprovider.EventRule
@ -518,7 +511,7 @@ func loadAdminTemplates(templatesPath string) {
filepath.Join(templatesPath, templateCommonDir, templateResetPassword), filepath.Join(templatesPath, templateCommonDir, templateResetPassword),
} }
rolesPaths := []string{ rolesPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS), filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
filepath.Join(templatesPath, templateAdminDir, templateBase), filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateRoles), filepath.Join(templatesPath, templateAdminDir, templateRoles),
} }
@ -839,7 +832,7 @@ func (s *httpdServer) renderProfilePage(w http.ResponseWriter, r *http.Request,
func (s *httpdServer) renderChangePasswordPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) { func (s *httpdServer) renderChangePasswordPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {
data := changePasswordPage{ data := changePasswordPage{
basePage: s.getBasePageData(pageChangePwdTitle, webChangeAdminPwdPath, r), basePage: s.getBasePageData(util.I18nChangePwdTitle, webChangeAdminPwdPath, r),
Error: err, Error: err,
} }
@ -3856,7 +3849,7 @@ func (s *httpdServer) handleWebUpdateEventRulePost(w http.ResponseWriter, r *htt
} }
func (s *httpdServer) getWebRoles(w http.ResponseWriter, r *http.Request, limit int, minimal bool) ([]dataprovider.Role, error) { func (s *httpdServer) getWebRoles(w http.ResponseWriter, r *http.Request, limit int, minimal bool) ([]dataprovider.Role, error) {
roles := make([]dataprovider.Role, 0, limit) roles := make([]dataprovider.Role, 0, 10)
for { for {
res, err := dataprovider.GetRoles(limit, len(roles), dataprovider.OrderASC, minimal) res, err := dataprovider.GetRoles(limit, len(roles), dataprovider.OrderASC, minimal)
if err != nil { if err != nil {
@ -3871,16 +3864,27 @@ func (s *httpdServer) getWebRoles(w http.ResponseWriter, r *http.Request, limit
return roles, nil return roles, nil
} }
func getAllRoles(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
roles := make([]dataprovider.Role, 0, 10)
for {
res, err := dataprovider.GetRoles(defaultQueryLimit, len(roles), dataprovider.OrderASC, false)
if err != nil {
sendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nError500Message), http.StatusInternalServerError)
return
}
roles = append(roles, res...)
if len(res) < defaultQueryLimit {
break
}
}
render.JSON(w, r, roles)
}
func (s *httpdServer) handleWebGetRoles(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleWebGetRoles(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
roles, err := s.getWebRoles(w, r, 10, false) data := s.getBasePageData(util.I18nRolesTitle, webAdminRolesPath, r)
if err != nil {
return
}
data := rolesPage{
basePage: s.getBasePageData(pageRolesTitle, webAdminRolesPath, r),
Roles: roles,
}
renderAdminTemplate(w, templateRoles, data) renderAdminTemplate(w, templateRoles, data)
} }

View file

@ -59,6 +59,7 @@ const (
I18nOAuth2Title = "title.oauth2_success" I18nOAuth2Title = "title.oauth2_success"
I18nOAuth2ErrorTitle = "title.oauth2_error" I18nOAuth2ErrorTitle = "title.oauth2_error"
I18nSessionsTitle = "title.connections" I18nSessionsTitle = "title.connections"
I18nRolesTitle = "title.roles"
I18nErrorSetupInstallCode = "setup.install_code_mismatch" I18nErrorSetupInstallCode = "setup.install_code_mismatch"
I18nInvalidAuth = "general.invalid_auth_request" I18nInvalidAuth = "general.invalid_auth_request"
I18nError429Message = "general.error429" I18nError429Message = "general.error429"

View file

@ -221,7 +221,9 @@
"backup_ok": "Backup successfully restored", "backup_ok": "Backup successfully restored",
"configs_saved": "Configurations has been successfully updated", "configs_saved": "Configurations has been successfully updated",
"protocol": "Protocol", "protocol": "Protocol",
"refresh": "Refresh" "refresh": "Refresh",
"members": "Members",
"members_summary": "Users: {{users}}. Admins: {{admins}}"
}, },
"fs": { "fs": {
"view_file": "View file \"{{- path}}\"", "view_file": "View file \"{{- path}}\"",
@ -493,9 +495,7 @@
"template_no_user": "No valid user defined, unable to complete the requested action" "template_no_user": "No valid user defined, unable to complete the requested action"
}, },
"group": { "group": {
"view_manage": "View and manage groups", "view_manage": "View and manage groups"
"members": "Members",
"members_summary": "Users: {{users}}. Admins: {{admins}}"
}, },
"virtual_folders": { "virtual_folders": {
"view_manage": "View and manage virtual folders", "view_manage": "View and manage virtual folders",
@ -686,5 +686,8 @@
"upload_info": "$t(connections.upload). Size: {{- size}}. Speed: {{- speed}}", "upload_info": "$t(connections.upload). Size: {{- size}}. Speed: {{- speed}}",
"download_info": "$t(connections.download). Size: {{- size}}. Speed: {{- speed}}", "download_info": "$t(connections.download). Size: {{- size}}. Speed: {{- speed}}",
"client": "Client: {{- val}}" "client": "Client: {{- val}}"
},
"role": {
"view_manage": "View and manage roles"
} }
} }

View file

@ -221,7 +221,9 @@
"backup_ok": "Backup ripristinato correttamente", "backup_ok": "Backup ripristinato correttamente",
"configs_saved": "Configurazioni aggiornate", "configs_saved": "Configurazioni aggiornate",
"protocol": "Protocollo", "protocol": "Protocollo",
"refresh": "Aggiorna" "refresh": "Aggiorna",
"members": "Membri",
"members_summary": "Utenti: {{users}}. Amministratori: {{admins}}"
}, },
"fs": { "fs": {
"view_file": "Visualizza file \"{{- path}}\"", "view_file": "Visualizza file \"{{- path}}\"",
@ -493,9 +495,7 @@
"template_no_user": "Nessun utente valido definito. Impossibile completare l'azione richiesta" "template_no_user": "Nessun utente valido definito. Impossibile completare l'azione richiesta"
}, },
"group": { "group": {
"view_manage": "Visualizza e gestisci gruppi", "view_manage": "Visualizza e gestisci gruppi"
"members": "Membri",
"members_summary": "Utenti: {{users}}. Amministratori: {{admins}}"
}, },
"virtual_folders": { "virtual_folders": {
"view_manage": "Visualizza e gestisci cartelle virtuali", "view_manage": "Visualizza e gestisci cartelle virtuali",
@ -686,5 +686,8 @@
"upload_info": "$t(connections.upload). Dimensione: {{- size}}. Velocità: {{- speed}}", "upload_info": "$t(connections.upload). Dimensione: {{- size}}. Velocità: {{- speed}}",
"download_info": "$t(connections.download). Dimensione: {{- size}}. Velocità: {{- speed}}", "download_info": "$t(connections.download). Dimensione: {{- size}}. Velocità: {{- speed}}",
"client": "Client: {{- val}}" "client": "Client: {{- val}}"
},
"role": {
"view_manage": "Visualizza e gestisci ruoli"
} }
} }

View file

@ -191,30 +191,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
</div> </div>
{{- end}} {{- 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-solid ki-security-user fs-1"></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"))}} {{- 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}}"> <div data-kt-menu-trigger="click" class="menu-item menu-accordion {{- if .IsServerManagerPage}} here show{{- end}}">
<span class="menu-link"> <span class="menu-link">
@ -274,5 +250,29 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
</div> </div>
{{- end}} {{- 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-solid ki-security-user fs-1"></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}}
{{- end}} {{- end}}

View file

@ -286,19 +286,19 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
//{{- if .LoggedUser.HasPermission "manage_folders"}} //{{- if .LoggedUser.HasPermission "manage_folders"}}
numActions++; numActions++;
actions+=`<div class="menu-item px-3"> actions+=`<div class="menu-item px-3">
<a data-i18n="general.edit" href="#" class="menu-link px-3" data-share-table-action="edit_row">Edit</a> <a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
</div>` </div>`
//{{- end}} //{{- end}}
//{{- if .LoggedUser.HasPermission "manage_system"}} //{{- if .LoggedUser.HasPermission "manage_system"}}
numActions++; numActions++;
actions+=`<div class="menu-item px-3"> actions+=`<div class="menu-item px-3">
<a data-i18n="general.template" href="#" class="menu-link px-3" data-share-table-action="template_row">Template</a> <a data-i18n="general.template" href="#" class="menu-link px-3" data-table-action="template_row">Template</a>
</div>` </div>`
//{{- end}} //{{- end}}
//{{- if .LoggedUser.HasPermission "manage_folders"}} //{{- if .LoggedUser.HasPermission "manage_folders"}}
numActions++; numActions++;
actions+=`<div class="menu-item px-3"> actions+=`<div class="menu-item px-3">
<a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-share-table-action="delete_row">Delete</a> <a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
</div>` </div>`
//{{- end}} //{{- end}}
if (numActions > 0){ if (numActions > 0){
@ -377,7 +377,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
function handleRowActions() { function handleRowActions() {
const editButtons = document.querySelectorAll('[data-share-table-action="edit_row"]'); const editButtons = document.querySelectorAll('[data-table-action="edit_row"]');
editButtons.forEach(d => { editButtons.forEach(d => {
let el = $(d); let el = $(d);
el.off("click"); el.off("click");
@ -388,7 +388,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}); });
}); });
const templateButtons = document.querySelectorAll('[data-share-table-action="template_row"]'); const templateButtons = document.querySelectorAll('[data-table-action="template_row"]');
templateButtons.forEach(d => { templateButtons.forEach(d => {
let el = $(d); let el = $(d);
el.off("click"); el.off("click");
@ -399,7 +399,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}); });
}); });
const deleteButtons = document.querySelectorAll('[data-share-table-action="delete_row"]'); const deleteButtons = document.querySelectorAll('[data-table-action="delete_row"]');
deleteButtons.forEach(d => { deleteButtons.forEach(d => {
let el = $(d); let el = $(d);
el.off("click"); el.off("click");

View file

@ -47,7 +47,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid"> <div class="menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid">
<input type="checkbox" class="form-check-input" value="" id="checkColMembers" /> <input type="checkbox" class="form-check-input" value="" id="checkColMembers" />
<label class="form-check-label" for="checkColMembers"> <label class="form-check-label" for="checkColMembers">
<span data-i18n="group.members" class="text-gray-800 fs-6">Members</span> <span data-i18n="general.members" class="text-gray-800 fs-6">Members</span>
</label> </label>
</div> </div>
<div class="menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid"> <div class="menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid">
@ -70,7 +70,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<thead> <thead>
<tr class="text-start text-muted fw-bold fs-6 gs-0"> <tr class="text-start text-muted fw-bold fs-6 gs-0">
<th data-i18n="general.name">Name</th> <th data-i18n="general.name">Name</th>
<th data-i18n="group.members">Members</th> <th data-i18n="general.members">Members</th>
<th data-i18n="general.description">Description</th> <th data-i18n="general.description">Description</th>
<th class="min-w-100px"></th> <th class="min-w-100px"></th>
</tr> </tr>
@ -84,7 +84,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- define "extra_js"}} {{- define "extra_js"}}
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js"></script> <script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js"></script>
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}> <script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
function deleteAction(username) { function deleteAction(name) {
ModalAlert.fire({ ModalAlert.fire({
text: $.t('general.delete_confirm_generic'), text: $.t('general.delete_confirm_generic'),
icon: "warning", icon: "warning",
@ -98,7 +98,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
if (result.isConfirmed){ if (result.isConfirmed){
$('#loading_message').text(""); $('#loading_message').text("");
KTApp.showPageLoading(); KTApp.showPageLoading();
let path = '{{.GroupURL}}' + "/" + encodeURIComponent(username); let path = '{{.GroupURL}}' + "/" + encodeURIComponent(name);
axios.delete(path, { axios.delete(path, {
timeout: 15000, timeout: 15000,
@ -191,7 +191,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
if (row.admins){ if (row.admins){
admins = row.admins.length; admins = row.admins.length;
} }
return $.t('group.members_summary', {users: users, admins: admins}); return $.t('general.members_summary', {users: users, admins: admins});
} }
return ""; return "";
} }
@ -227,11 +227,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
//{{- if .LoggedUser.HasPermission "manage_groups"}} //{{- if .LoggedUser.HasPermission "manage_groups"}}
numActions++; numActions++;
actions+=`<div class="menu-item px-3"> actions+=`<div class="menu-item px-3">
<a data-i18n="general.edit" href="#" class="menu-link px-3" data-share-table-action="edit_row">Edit</a> <a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
</div>` </div>`
numActions++; numActions++;
actions+=`<div class="menu-item px-3"> actions+=`<div class="menu-item px-3">
<a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-share-table-action="delete_row">Delete</a> <a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
</div>` </div>`
//{{- end}} //{{- end}}
if (numActions > 0){ if (numActions > 0){
@ -308,7 +308,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
function handleRowActions() { function handleRowActions() {
const editButtons = document.querySelectorAll('[data-share-table-action="edit_row"]'); const editButtons = document.querySelectorAll('[data-table-action="edit_row"]');
editButtons.forEach(d => { editButtons.forEach(d => {
let el = $(d); let el = $(d);
el.off("click"); el.off("click");
@ -319,7 +319,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}); });
}); });
const deleteButtons = document.querySelectorAll('[data-share-table-action="delete_row"]'); const deleteButtons = document.querySelectorAll('[data-table-action="delete_row"]');
deleteButtons.forEach(d => { deleteButtons.forEach(d => {
let el = $(d); let el = $(d);
el.off("click"); el.off("click");

View file

@ -1,229 +1,347 @@
<!-- <!--
Copyright (C) 2019 Nicola Murino Copyright (C) 2024 Nicola Murino
This program is free software: you can redistribute it and/or modify This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, https://keenthemes.com/products/templates-mega-bundle
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License KeenThemes HTML/CSS/JS components are allowed for use only within the
along with this program. If not, see <https://www.gnu.org/licenses/>. SFTPGo product and restricted to be used in a resealable HTML template
that can compete with KeenThemes products anyhow.
This WebUI is allowed for use only within the SFTPGo product and
therefore cannot be used in derivative works/products without an
explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{template "base" .}} {{template "base" .}}
{{define "title"}}{{.Title}}{{end}} {{- define "extra_css"}}
<link href="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css" rel="stylesheet" type="text/css"/>
{{- end}}
{{define "extra_css"}} {{- define "page_body"}}
<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet"> {{- template "errmsg" ""}}
<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet"> <div class="card shadow-sm">
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet"> <div class="card-header bg-light">
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet"> <h3 data-i18n="role.view_manage" class="card-title section-title">View and manage roles</h3>
<link href="{{.StaticURL}}/vendor/datatables/select.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 class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">View and manage roles</h6>
</div> </div>
<div class="card-body"> <div id="card_body" class="card-body">
<div class="table-responsive"> <div id="loader" class="align-items-center text-center my-10">
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0"> <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-2">
<i class="ki-solid ki-magnifier fs-1 position-absolute ms-6"></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 my-2" data-table-toolbar="base">
<button type="button" class="btn btn-light-primary rotate" data-kt-menu-trigger="click" data-kt-menu-placement="bottom" data-kt-menu-permanent="true">
<span data-i18n="general.colvis">Column visibility</span>
<i class="ki-duotone ki-down fs-3 rotate-180 ms-3 me-0"></i>
</button>
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-800 menu-state-bg-light-primary fw-semibold w-auto min-w-200 mw-300px py-4" data-kt-menu="true">
<div class="menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid">
<input type="checkbox" class="form-check-input" value="" id="checkColMembers" />
<label class="form-check-label" for="checkColMembers">
<span data-i18n="general.members" class="text-gray-800 fs-6">Members</span>
</label>
</div>
<div class="menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid">
<input type="checkbox" class="form-check-input" value="" id="checkColDesc" />
<label class="form-check-label" for="checkColDesc">
<span data-i18n="general.description" class="text-gray-800 fs-6">Description</span>
</label>
</div>
</div>
{{- if .LoggedUser.HasPermission "manage_roles"}}
<a href="{{.RoleURL}}" class="btn btn-primary ms-5">
<i class="ki-duotone ki-plus fs-2"></i>
<span data-i18n="general.add">Add</span>
</a>
{{- end}}
</div>
</div>
<table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5">
<thead> <thead>
<tr> <tr class="text-start text-muted fw-bold fs-6 gs-0">
<th>Name</th> <th data-i18n="general.name">Name</th>
<th>Description</th> <th data-i18n="general.members">Members</th>
<th>Members</th> <th data-i18n="general.description">Description</th>
<th class="min-w-100px"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody id="table_body" class="text-gray-800 fw-semibold"></tbody>
{{range .Roles}}
<tr>
<td>{{.Name}}</td>
<td>{{.Description}}</td>
<td>{{.GetMembersAsString}}</td>
</tr>
{{end}}
</tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
{{end}} {{- end}}
{{define "dialog"}} {{- define "extra_js"}}
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" <script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js"></script>
aria-hidden="true"> <script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
<div class="modal-dialog" role="document"> function deleteAction(name) {
<div class="modal-content"> ModalAlert.fire({
<div class="modal-header"> text: $.t('general.delete_confirm_generic'),
<h5 class="modal-title" id="deleteModalLabel"> icon: "warning",
Confirmation required confirmButtonText: $.t('general.delete_confirm_btn'),
</h5> cancelButtonText: $.t('general.cancel'),
<button class="close" type="button" data-dismiss="modal" aria-label="Close"> customClass: {
<span aria-hidden="true">&times;</span> confirmButton: "btn btn-danger",
</button> cancelButton: 'btn btn-secondary'
</div> }
<div class="modal-body">Do you want to delete the selected role? It is not possible to remove a role that does contain administrators</div> }).then((result) => {
<div class="modal-footer"> if (result.isConfirmed){
<button class="btn btn-secondary" type="button" data-dismiss="modal"> $('#loading_message').text("");
Cancel KTApp.showPageLoading();
</button> let path = '{{.RoleURL}}' + "/" + encodeURIComponent(name);
<a class="btn btn-warning" href="#" onclick="deleteAction()">
Delete
</a>
</div>
</div>
</div>
</div>
{{end}}
{{define "extra_js"}} axios.delete(path, {
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script> timeout: 15000,
<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script> headers: {
<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script> 'X-CSRF-TOKEN': '{{.CSRFToken}}'
<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script> },
<script src="{{.StaticURL}}/vendor/datatables/buttons.colVis.min.js"></script> validateStatus: function (status) {
<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script> return status == 200;
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script> }
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script> }).then(function(response){
<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script> location.reload();
<script src="{{.StaticURL}}/vendor/datatables/ellipsis.js"></script> }).catch(function(error){
<script type="text/javascript"> KTApp.hidePageLoading();
let errorMessage;
function deleteAction() { if (error && error.response) {
let table = $('#dataTable').DataTable(); switch (error.response.status) {
table.button('delete:name').enable(false); case 403:
let roleName = table.row({ selected: true }).data()[0]; errorMessage = "general.delete_error_403";
let path = '{{.RoleURL}}' + "/" + fixedEncodeURIComponent(roleName); break;
$('#deleteModal').modal('hide'); case 404:
$('#errorMsg').hide(); errorMessage = "general.delete_error_404";
break;
$.ajax({
url: path,
type: 'DELETE',
dataType: 'json',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
timeout: 15000,
success: function (result) {
window.location.href = '{{.RolesURL}}';
},
error: function ($xhr, textStatus, errorThrown) {
var txt = "Unable to delete the selected role";
if ($xhr) {
var json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
} }
} }
} if (!errorMessage){
$('#errorTxt').text(txt); errorMessage = "general.delete_error_generic";
$('#errorMsg').show(); }
ModalAlert.fire({
text: $.t(errorMessage),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
});
} }
}); });
} }
$(document).ready(function () { var datatable = function(){
$.fn.dataTable.ext.buttons.add = { var dt;
text: '<i class="fas fa-plus"></i>',
name: 'add',
titleAttr: "Add",
action: function (e, dt, node, config) {
window.location.href = '{{.RoleURL}}';
}
};
$.fn.dataTable.ext.buttons.edit = { var initDatatable = function () {
text: '<i class="fas fa-pen"></i>', $('#errorMsg').addClass("d-none");
name: 'edit', dt = $('#dataTable').DataTable({
titleAttr: "Edit", ajax: {
action: function (e, dt, node, config) { url: "{{.RolesURL}}/json",
let roleName = table.row({ selected: true }).data()[0]; dataSrc: "",
let path = '{{.RoleURL}}' + "/" + fixedEncodeURIComponent(roleName); error: function ($xhr, textStatus, errorThrown) {
window.location.href = path; $(".dataTables_processing").hide();
}, let txt = "";
enabled: false if ($xhr) {
}; let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt = json.message;
}
}
}
if (!txt){
txt = "general.error500";
}
setI18NData($('#errorTxt'), txt);
$('#errorMsg').removeClass("d-none");
}
},
columns: [
{
data: "name",
render: function(data, type, row) {
if (type === 'display') {
return escapeHTML(data);
}
return data;
}
},
{
data: "users",
defaultContent: "",
searchable: false,
orderable: false,
render: function(data, type, row) {
if (type === 'display') {
let users = 0;
if (row.users){
users = row.users.length;
}
let admins = 0;
if (row.admins){
admins = row.admins.length;
}
return $.t('general.members_summary', {users: users, admins: admins});
}
return "";
}
},
{
data: "description",
visible: false,
defaultContent: "",
render: function(data, type, row) {
if (type === 'display') {
if (data){
return escapeHTML(data);
}
return ""
}
return data;
}
},
{
data: "id",
searchable: false,
orderable: false,
className: 'text-end',
render: function (data, type, row) {
if (type === 'display') {
let numActions = 0;
let actions = `<button class="btn btn-light btn-active-light-primary btn-flex btn-center btn-sm rotate" data-kt-menu-trigger="click" data-kt-menu-placement="bottom-end">
<span data-i18n="general.actions" class="fs-6">Actions</span>
<i class="ki-duotone ki-down fs-5 ms-1 rotate-180"></i>
</button>
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4" data-kt-menu="true">`;
$.fn.dataTable.ext.buttons.delete = { //{{- if .LoggedUser.HasPermission "manage_roles"}}
text: '<i class="fas fa-trash"></i>', numActions++;
name: 'delete', actions+=`<div class="menu-item px-3">
titleAttr: "Delete", <a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
action: function (e, dt, node, config) { </div>`
$('#deleteModal').modal('show'); numActions++;
}, actions+=`<div class="menu-item px-3">
enabled: false <a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
}; </div>`
//{{- end}}
var table = $('#dataTable').DataTable({ if (numActions > 0){
"select": { actions+=`</div>`;
"style": "single", return actions;
"blurable": true }
}, }
"stateSave": true, return "";
"stateDuration": 0, }
"buttons": [ },
{ ],
"text": "Column visibility", deferRender: true,
"extend": "colvis", stateSave: true,
"columns": ":not(.noVis)" stateDuration: 0,
colReorder: {
enable: true,
fixedColumnsLeft: 1
},
stateLoadParams: function (settings, data) {
if (data.search.search){
const filterSearch = document.querySelector('[data-table-filter="search"]');
filterSearch.value = data.search.search;
}
},
language: {
info: $.t('datatable.info'),
infoEmpty: $.t('datatable.info_empty'),
infoFiltered: $.t('datatable.info_filtered'),
loadingRecords: "",
processing: $.t('datatable.processing'),
zeroRecords: "",
emptyTable: $.t('datatable.no_records')
},
order: [[0, 'asc']],
initComplete: function(settings, json) {
$('#loader').addClass("d-none");
$('#card_content').removeClass("d-none");
let api = $.fn.dataTable.Api(settings);
api.columns.adjust().draw("page");
drawAction();
} }
], });
"columnDefs": [
{
"targets": [0],
"className": "noVis"
},
{
"targets": [2],
"render": $.fn.dataTable.render.ellipsis(100, true)
},
],
"scrollX": false,
"scrollY": false,
"responsive": true,
"language": {
"emptyTable": "No role defined"
},
"order": [[0, 'asc']]
});
new $.fn.dataTable.FixedHeader( table ); dt.on('draw', drawAction);
dt.on('column-reorder', function(e, settings, details){
drawAction();
});
}
{{if .LoggedAdmin.HasPermission "manage_roles"}} function drawAction() {
table.button().add(0,'delete'); KTMenu.createInstances();
table.button().add(0,'edit'); handleRowActions();
table.button().add(0,'add'); $('#table_body').localize();
}
table.buttons().container().appendTo('.col-md-6:eq(0)', table.table().container()); function handleColVisibilityCheckbox(el, index) {
el.off("change");
el.prop('checked', dt.column(index).visible());
el.on("change", function(e){
dt.column(index).visible($(this).is(':checked'));
dt.draw('page');
});
}
table.on('select deselect', function () { var handleDatatableActions = function () {
var selectedRows = table.rows({ selected: true }).count(); const filterSearch = $(document.querySelector('[data-table-filter="search"]'));
table.button('delete:name').enable(selectedRows == 1); filterSearch.off("keyup");
table.button('edit:name').enable(selectedRows == 1); filterSearch.on('keyup', function (e) {
}); dt.rows().deselect();
{{end}} dt.search(e.target.value, true, false).draw();
});
handleColVisibilityCheckbox($('#checkColMembers'), 1);
handleColVisibilityCheckbox($('#checkColDesc'), 2);
}
function handleRowActions() {
const editButtons = document.querySelectorAll('[data-table-action="edit_row"]');
editButtons.forEach(d => {
let el = $(d);
el.off("click");
el.on("click", function(e){
e.preventDefault();
let rowData = dt.row(e.target.closest('tr')).data();
window.location.replace('{{.RoleURL}}' + "/" + encodeURIComponent(rowData['name']));
});
});
const deleteButtons = document.querySelectorAll('[data-table-action="delete_row"]');
deleteButtons.forEach(d => {
let el = $(d);
el.off("click");
el.on("click", function(e){
e.preventDefault();
const parent = e.target.closest('tr');
deleteAction(dt.row(parent).data()['name']);
});
});
}
return {
init: function () {
initDatatable();
handleDatatableActions();
}
}
}();
$(document).on("i18nshow", function(){
datatable.init();
}); });
</script> </script>
{{end}} {{- end}}

View file

@ -471,25 +471,25 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
//{{- if .LoggedUser.HasPermission "edit_users"}} //{{- if .LoggedUser.HasPermission "edit_users"}}
numActions++; numActions++;
actions+=`<div class="menu-item px-3"> actions+=`<div class="menu-item px-3">
<a data-i18n="general.edit" href="#" class="menu-link px-3" data-share-table-action="edit_row">Edit</a> <a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
</div>` </div>`
//{{- end}} //{{- end}}
//{{- if .LoggedUser.HasPermission "manage_system"}} //{{- if .LoggedUser.HasPermission "manage_system"}}
numActions++; numActions++;
actions+=`<div class="menu-item px-3"> actions+=`<div class="menu-item px-3">
<a data-i18n="general.template" href="#" class="menu-link px-3" data-share-table-action="template_row">Template</a> <a data-i18n="general.template" href="#" class="menu-link px-3" data-table-action="template_row">Template</a>
</div>` </div>`
//{{- end}} //{{- end}}
//{{- if .LoggedUser.HasPermission "quota_scans"}} //{{- if .LoggedUser.HasPermission "quota_scans"}}
numActions++; numActions++;
actions+=`<div class="menu-item px-3"> actions+=`<div class="menu-item px-3">
<a data-i18n="general.quota_scan" href="#" class="menu-link px-3" data-share-table-action="quota_scan_row">Quota scan</a> <a data-i18n="general.quota_scan" href="#" class="menu-link px-3" data-table-action="quota_scan_row">Quota scan</a>
</div>` </div>`
//{{- end}} //{{- end}}
//{{- if .LoggedUser.HasPermission "del_users"}} //{{- if .LoggedUser.HasPermission "del_users"}}
numActions++; numActions++;
actions+=`<div class="menu-item px-3"> actions+=`<div class="menu-item px-3">
<a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-share-table-action="delete_row">Delete</a> <a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
</div>` </div>`
//{{- end}} //{{- end}}
if (numActions > 0){ if (numActions > 0){
@ -573,7 +573,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
function handleRowActions() { function handleRowActions() {
const editButtons = document.querySelectorAll('[data-share-table-action="edit_row"]'); const editButtons = document.querySelectorAll('[data-table-action="edit_row"]');
editButtons.forEach(d => { editButtons.forEach(d => {
let el = $(d); let el = $(d);
el.off("click"); el.off("click");
@ -584,7 +584,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}); });
}); });
const templateButtons = document.querySelectorAll('[data-share-table-action="template_row"]'); const templateButtons = document.querySelectorAll('[data-table-action="template_row"]');
templateButtons.forEach(d => { templateButtons.forEach(d => {
let el = $(d); let el = $(d);
el.off("click"); el.off("click");
@ -595,7 +595,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}); });
}); });
const quotaScanButtons = document.querySelectorAll('[data-share-table-action="quota_scan_row"]'); const quotaScanButtons = document.querySelectorAll('[data-table-action="quota_scan_row"]');
quotaScanButtons.forEach(d => { quotaScanButtons.forEach(d => {
let el = $(d); let el = $(d);
el.off("click"); el.off("click");
@ -606,7 +606,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}); });
}); });
const deleteButtons = document.querySelectorAll('[data-share-table-action="delete_row"]'); const deleteButtons = document.querySelectorAll('[data-table-action="delete_row"]');
deleteButtons.forEach(d => { deleteButtons.forEach(d => {
let el = $(d); let el = $(d);
el.off("click"); el.off("click");

View file

@ -336,7 +336,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
if (type === 'display') { if (type === 'display') {
return `<div class="d-flex justify-content-end"> return `<div class="d-flex justify-content-end">
<div class="ms-2"> <div class="ms-2">
<a href="#" class="btn btn-sm btn-icon btn-light btn-active-light-primary" data-share-table-action="show_link"> <a href="#" class="btn btn-sm btn-icon btn-light btn-active-light-primary" data-table-action="show_link">
<i class="ki-duotone ki-fasten fs-5 m-0"> <i class="ki-duotone ki-fasten fs-5 m-0">
<span class="path1"></span> <span class="path1"></span>
<span class="path2"></span> <span class="path2"></span>
@ -355,10 +355,10 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</button> </button>
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-150px py-4" data-kt-menu="true"> <div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-150px py-4" data-kt-menu="true">
<div class="menu-item px-3"> <div class="menu-item px-3">
<a data-i18n="general.edit" href="#" class="menu-link px-3" data-share-table-action="edit_row">Edit</a> <a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
</div> </div>
<div class="menu-item px-3"> <div class="menu-item px-3">
<a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-share-table-action="delete_row">Delete</a> <a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
</div> </div>
</div> </div>
</div> </div>
@ -415,7 +415,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
function handleRowActions() { function handleRowActions() {
const editButtons = document.querySelectorAll('[data-share-table-action="edit_row"]'); const editButtons = document.querySelectorAll('[data-table-action="edit_row"]');
editButtons.forEach(d => { editButtons.forEach(d => {
let el = $(d); let el = $(d);
@ -427,7 +427,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}); });
}); });
const deleteButtons = document.querySelectorAll('[data-share-table-action="delete_row"]'); const deleteButtons = document.querySelectorAll('[data-table-action="delete_row"]');
deleteButtons.forEach(d => { deleteButtons.forEach(d => {
let el = $(d); let el = $(d);
@ -439,7 +439,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}); });
}); });
const showLinkButtons = document.querySelectorAll('[data-share-table-action="show_link"]'); const showLinkButtons = document.querySelectorAll('[data-table-action="show_link"]');
showLinkButtons.forEach(d => { showLinkButtons.forEach(d => {
let el = $(d); let el = $(d);