mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
WIP new WebAdmin: admin/admins pages
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
d67f00546a
commit
d381304136
10 changed files with 743 additions and 545 deletions
|
@ -286,7 +286,7 @@ func (a *Admin) hashPassword() error {
|
|||
if a.Password != "" && !util.IsStringPrefixInSlice(a.Password, internalHashPwdPrefixes) {
|
||||
if config.PasswordValidation.Admins.MinEntropy > 0 {
|
||||
if err := passwordvalidator.Validate(a.Password, config.PasswordValidation.Admins.MinEntropy); err != nil {
|
||||
return util.NewValidationError(err.Error())
|
||||
return util.NewI18nError(util.NewValidationError(err.Error()), util.I18nErrorPasswordComplexity)
|
||||
}
|
||||
}
|
||||
if config.PasswordHashing.Algo == HashingAlgoBcrypt {
|
||||
|
@ -397,7 +397,7 @@ func (a *Admin) validate() error {
|
|||
return util.NewI18nError(err, util.I18nError2FAInvalid)
|
||||
}
|
||||
if err := a.validateRecoveryCodes(); err != nil {
|
||||
return err
|
||||
return util.NewI18nError(err, util.I18nErrorRecoveryCodesInvalid)
|
||||
}
|
||||
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(a.Username) {
|
||||
return util.NewI18nError(
|
||||
|
|
|
@ -1723,6 +1723,8 @@ func (s *httpdServer) setupWebAdminRoutes() {
|
|||
Get(webStatusPath, s.handleWebGetStatus)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
|
||||
Get(webAdminsPath, s.handleGetWebAdmins)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), compressor.Handler, s.refreshCookie).
|
||||
Get(webAdminsPath+jsonAPISuffix, getAllAdmins)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
|
||||
Get(webAdminPath, s.handleWebAddAdminGet)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
|
||||
|
|
|
@ -98,7 +98,6 @@ const (
|
|||
templateMaintenance = "maintenance.html"
|
||||
templateMFA = "mfa.html"
|
||||
templateSetup = "adminsetup.html"
|
||||
pageAdminsTitle = "Admins"
|
||||
pageStatusTitle = "Status"
|
||||
pageEventRulesTitle = "Event rules"
|
||||
pageEventActionsTitle = "Event actions"
|
||||
|
@ -162,11 +161,6 @@ type basePage struct {
|
|||
Branding UIBranding
|
||||
}
|
||||
|
||||
type adminsPage struct {
|
||||
basePage
|
||||
Admins []dataprovider.Admin
|
||||
}
|
||||
|
||||
type eventRulesPage struct {
|
||||
basePage
|
||||
Rules []dataprovider.EventRule
|
||||
|
@ -215,7 +209,7 @@ type adminPage struct {
|
|||
Admin *dataprovider.Admin
|
||||
Groups []dataprovider.Group
|
||||
Roles []dataprovider.Role
|
||||
Error string
|
||||
Error *util.I18nError
|
||||
IsAdd bool
|
||||
}
|
||||
|
||||
|
@ -379,12 +373,12 @@ func loadAdminTemplates(templatesPath string) {
|
|||
filepath.Join(templatesPath, templateAdminDir, templateUser),
|
||||
}
|
||||
adminsPaths := []string{
|
||||
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
|
||||
filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateAdmins),
|
||||
}
|
||||
adminPaths := []string{
|
||||
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
|
||||
filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateAdmin),
|
||||
}
|
||||
|
@ -893,27 +887,27 @@ func (s *httpdServer) renderAdminSetupPage(w http.ResponseWriter, r *http.Reques
|
|||
}
|
||||
|
||||
func (s *httpdServer) renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Request, admin *dataprovider.Admin,
|
||||
error string, isAdd bool) {
|
||||
groups, err := s.getWebGroups(w, r, defaultQueryLimit, true)
|
||||
if err != nil {
|
||||
err error, isAdd bool) {
|
||||
groups, errGroups := s.getWebGroups(w, r, defaultQueryLimit, true)
|
||||
if errGroups != nil {
|
||||
return
|
||||
}
|
||||
roles, err := s.getWebRoles(w, r, 10, true)
|
||||
if err != nil {
|
||||
roles, errRoles := s.getWebRoles(w, r, 10, true)
|
||||
if errRoles != nil {
|
||||
return
|
||||
}
|
||||
currentURL := webAdminPath
|
||||
title := "Add a new admin"
|
||||
title := util.I18nAddAdminTitle
|
||||
if !isAdd {
|
||||
currentURL = fmt.Sprintf("%v/%v", webAdminPath, url.PathEscape(admin.Username))
|
||||
title = "Update admin"
|
||||
title = util.I18nUpdateAdminTitle
|
||||
}
|
||||
data := adminPage{
|
||||
basePage: s.getBasePageData(title, currentURL, r),
|
||||
Admin: admin,
|
||||
Groups: groups,
|
||||
Roles: roles,
|
||||
Error: error,
|
||||
Error: getI18nError(err),
|
||||
IsAdd: isAdd,
|
||||
}
|
||||
|
||||
|
@ -1777,14 +1771,14 @@ func getAdminFromPostFields(r *http.Request) (dataprovider.Admin, error) {
|
|||
admin.Filters.Preferences.DefaultUsersExpiration = defaultUsersExpiration
|
||||
}
|
||||
for k := range r.Form {
|
||||
if strings.HasPrefix(k, "group") {
|
||||
if hasPrefixAndSuffix(k, "groups[", "][group]") {
|
||||
groupName := strings.TrimSpace(r.Form.Get(k))
|
||||
if groupName != "" {
|
||||
idx := strings.TrimPrefix(k, "group")
|
||||
addAsGroupType := r.Form.Get(fmt.Sprintf("add_as_group_type%s", idx))
|
||||
group := dataprovider.AdminGroupMapping{
|
||||
Name: groupName,
|
||||
}
|
||||
base, _ := strings.CutSuffix(k, "[group]")
|
||||
addAsGroupType := strings.TrimSpace(r.Form.Get(base + "[group_type]"))
|
||||
switch addAsGroupType {
|
||||
case "1":
|
||||
group.Options.AddToUsersAs = dataprovider.GroupAddToUsersAsPrimary
|
||||
|
@ -2803,32 +2797,32 @@ func (s *httpdServer) handleWebRestore(w http.ResponseWriter, r *http.Request) {
|
|||
s.renderMessagePage(w, r, util.I18nMaintenanceTitle, http.StatusOK, nil, util.I18nBackupOK)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleGetWebAdmins(w http.ResponseWriter, r *http.Request) {
|
||||
func getAllAdmins(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
limit := defaultQueryLimit
|
||||
if _, ok := r.URL.Query()["qlimit"]; ok {
|
||||
var err error
|
||||
limit, err = strconv.Atoi(r.URL.Query().Get("qlimit"))
|
||||
if err != nil {
|
||||
limit = defaultQueryLimit
|
||||
}
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
sendAPIResponse(w, r, nil, util.I18nErrorDirList403, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
admins := make([]dataprovider.Admin, 0, limit)
|
||||
admins := make([]dataprovider.Admin, 0, 50)
|
||||
for {
|
||||
a, err := dataprovider.GetAdmins(limit, len(admins), dataprovider.OrderASC)
|
||||
a, err := dataprovider.GetAdmins(defaultQueryLimit, len(admins), dataprovider.OrderASC)
|
||||
if err != nil {
|
||||
s.renderInternalServerErrorPage(w, r, err)
|
||||
sendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nError500Message), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
admins = append(admins, a...)
|
||||
if len(a) < limit {
|
||||
if len(a) < defaultQueryLimit {
|
||||
break
|
||||
}
|
||||
}
|
||||
data := adminsPage{
|
||||
basePage: s.getBasePageData(pageAdminsTitle, webAdminsPath, r),
|
||||
Admins: admins,
|
||||
}
|
||||
render.JSON(w, r, admins)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleGetWebAdmins(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
data := s.getBasePageData(util.I18nAdminsTitle, webAdminsPath, r)
|
||||
renderAdminTemplate(w, templateAdmins, data)
|
||||
}
|
||||
|
||||
|
@ -2847,7 +2841,7 @@ func (s *httpdServer) handleWebAddAdminGet(w http.ResponseWriter, r *http.Reques
|
|||
Status: 1,
|
||||
Permissions: []string{dataprovider.PermAdminAny},
|
||||
}
|
||||
s.renderAddUpdateAdminPage(w, r, admin, "", true)
|
||||
s.renderAddUpdateAdminPage(w, r, admin, nil, true)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebUpdateAdminGet(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -2855,7 +2849,7 @@ func (s *httpdServer) handleWebUpdateAdminGet(w http.ResponseWriter, r *http.Req
|
|||
username := getURLParam(r, "username")
|
||||
admin, err := dataprovider.AdminExists(username)
|
||||
if err == nil {
|
||||
s.renderAddUpdateAdminPage(w, r, &admin, "", false)
|
||||
s.renderAddUpdateAdminPage(w, r, &admin, nil, false)
|
||||
} else if errors.Is(err, util.ErrNotFound) {
|
||||
s.renderNotFoundPage(w, r, err)
|
||||
} else {
|
||||
|
@ -2872,7 +2866,7 @@ func (s *httpdServer) handleWebAddAdminPost(w http.ResponseWriter, r *http.Reque
|
|||
}
|
||||
admin, err := getAdminFromPostFields(r)
|
||||
if err != nil {
|
||||
s.renderAddUpdateAdminPage(w, r, &admin, err.Error(), true)
|
||||
s.renderAddUpdateAdminPage(w, r, &admin, err, true)
|
||||
return
|
||||
}
|
||||
if admin.Password == "" && s.binding.isWebAdminLoginFormDisabled() {
|
||||
|
@ -2885,7 +2879,7 @@ func (s *httpdServer) handleWebAddAdminPost(w http.ResponseWriter, r *http.Reque
|
|||
}
|
||||
err = dataprovider.AddAdmin(&admin, claims.Username, ipAddr, claims.Role)
|
||||
if err != nil {
|
||||
s.renderAddUpdateAdminPage(w, r, &admin, err.Error(), true)
|
||||
s.renderAddUpdateAdminPage(w, r, &admin, err, true)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, webAdminsPath, http.StatusSeeOther)
|
||||
|
@ -2906,7 +2900,7 @@ func (s *httpdServer) handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Re
|
|||
|
||||
updatedAdmin, err := getAdminFromPostFields(r)
|
||||
if err != nil {
|
||||
s.renderAddUpdateAdminPage(w, r, &updatedAdmin, err.Error(), false)
|
||||
s.renderAddUpdateAdminPage(w, r, &updatedAdmin, err, false)
|
||||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
|
@ -2923,26 +2917,36 @@ func (s *httpdServer) handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Re
|
|||
updatedAdmin.Filters.RecoveryCodes = admin.Filters.RecoveryCodes
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
s.renderAddUpdateAdminPage(w, r, &updatedAdmin, "Invalid token claims", false)
|
||||
s.renderAddUpdateAdminPage(w, r, &updatedAdmin, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken), false)
|
||||
return
|
||||
}
|
||||
if username == claims.Username {
|
||||
if claims.isCriticalPermRemoved(updatedAdmin.Permissions) {
|
||||
s.renderAddUpdateAdminPage(w, r, &updatedAdmin, "You cannot remove these permissions to yourself", false)
|
||||
s.renderAddUpdateAdminPage(w, r, &updatedAdmin,
|
||||
util.NewI18nError(errors.New("you cannot remove these permissions to yourself"),
|
||||
util.I18nErrorAdminSelfPerms,
|
||||
), false)
|
||||
return
|
||||
}
|
||||
if updatedAdmin.Status == 0 {
|
||||
s.renderAddUpdateAdminPage(w, r, &updatedAdmin, "You cannot disable yourself", false)
|
||||
s.renderAddUpdateAdminPage(w, r, &updatedAdmin,
|
||||
util.NewI18nError(errors.New("you cannot disable yourself"),
|
||||
util.I18nErrorAdminSelfDisable,
|
||||
), false)
|
||||
return
|
||||
}
|
||||
if updatedAdmin.Role != claims.Role {
|
||||
s.renderAddUpdateAdminPage(w, r, &updatedAdmin, "You cannot add/change your role", false)
|
||||
s.renderAddUpdateAdminPage(w, r, &updatedAdmin,
|
||||
util.NewI18nError(
|
||||
errors.New("you cannot add/change your role"),
|
||||
util.I18nErrorAdminSelfRole,
|
||||
), false)
|
||||
return
|
||||
}
|
||||
}
|
||||
err = dataprovider.UpdateAdmin(&updatedAdmin, claims.Username, ipAddr, claims.Role)
|
||||
if err != nil {
|
||||
s.renderAddUpdateAdminPage(w, r, &updatedAdmin, err.Error(), false)
|
||||
s.renderAddUpdateAdminPage(w, r, &updatedAdmin, err, false)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, webAdminsPath, http.StatusSeeOther)
|
||||
|
|
|
@ -53,6 +53,8 @@ const (
|
|||
I18nErrorEditorTitle = "title.error_editor"
|
||||
I18nAddUserTitle = "title.add_user"
|
||||
I18nUpdateUserTitle = "title.update_user"
|
||||
I18nAddAdminTitle = "title.add_admin"
|
||||
I18nUpdateAdminTitle = "title.update_admin"
|
||||
I18nTemplateUserTitle = "title.template_user"
|
||||
I18nMaintenanceTitle = "title.maintenance"
|
||||
I18nConfigsTitle = "title.configs"
|
||||
|
@ -60,6 +62,7 @@ const (
|
|||
I18nOAuth2ErrorTitle = "title.oauth2_error"
|
||||
I18nSessionsTitle = "title.connections"
|
||||
I18nRolesTitle = "title.roles"
|
||||
I18nAdminsTitle = "title.admins"
|
||||
I18nErrorSetupInstallCode = "setup.install_code_mismatch"
|
||||
I18nInvalidAuth = "general.invalid_auth_request"
|
||||
I18nError429Message = "general.error429"
|
||||
|
@ -212,6 +215,9 @@ const (
|
|||
I18nOAuth2ErrTokenExchange = "oauth2.token_exchange_err"
|
||||
I18nOAuth2ErrNoRefreshToken = "oauth2.no_refresh_token"
|
||||
I18nOAuth2OK = "oauth2.success"
|
||||
I18nErrorAdminSelfPerms = "admin.self_permissions"
|
||||
I18nErrorAdminSelfDisable = "admin.self_disable"
|
||||
I18nErrorAdminSelfRole = "admin.self_role"
|
||||
)
|
||||
|
||||
// NewI18nError returns a I18nError wrappring the provided error
|
||||
|
|
|
@ -56,7 +56,9 @@
|
|||
"oauth2_error": "Unable to complete OAuth2 flow",
|
||||
"oauth2_success": "OAuth2 flow completed",
|
||||
"add_role": "Add role",
|
||||
"update_role": "Update role"
|
||||
"update_role": "Update role",
|
||||
"add_admin": "Add admin",
|
||||
"update_admin": "Update admin"
|
||||
},
|
||||
"setup": {
|
||||
"desc": "To start using SFTPGo you need to create an administrator user",
|
||||
|
@ -225,7 +227,9 @@
|
|||
"protocol": "Protocol",
|
||||
"refresh": "Refresh",
|
||||
"members": "Members",
|
||||
"members_summary": "Users: {{users}}. Admins: {{admins}}"
|
||||
"members_summary": "Users: {{users}}. Admins: {{admins}}",
|
||||
"status": "Status",
|
||||
"last_login": "Last login"
|
||||
},
|
||||
"fs": {
|
||||
"view_file": "View file \"{{- path}}\"",
|
||||
|
@ -468,8 +472,6 @@
|
|||
"file_pattern_invalid": "Invalid file name pattern filters",
|
||||
"disable_active_2fa": "Two-factor authentication cannot be disabled for a user with an active configuration",
|
||||
"pwd_change_conflict": "It is not possible to request a password change and at the same time prevent the password from being changed",
|
||||
"status": "Status",
|
||||
"last_login": "Last login",
|
||||
"role_help": "Users with a role can be managed by global administrators and administrators with the same role",
|
||||
"require_pwd_change": "Require password change",
|
||||
"require_pwd_change_help": "The user will need to change the password from WebClient to activate the account",
|
||||
|
@ -673,7 +675,24 @@
|
|||
"external_auth_cache_time_help": "Cache time, in seconds, for users authenticated using an external auth hook. 0 means no cache"
|
||||
},
|
||||
"admin": {
|
||||
"role_permissions": "A role admin cannot have the following permissions: {{val}}"
|
||||
"role_permissions": "A role admin cannot have the following permissions: {{val}}",
|
||||
"view_manage": "View and manage admins",
|
||||
"self_delete": "You cannot delete yourself",
|
||||
"self_permissions": "You cannot remove these permissions to yourself",
|
||||
"self_disable": "You cannot disable yourself",
|
||||
"self_role": "You cannot add/change your role",
|
||||
"password_help": "If blank the current password will not be changed",
|
||||
"role_help": "Setting a role limit the administrator to only manage users with the same role. Administrators with a role cannot have the following permissions: \"manage_admins\", \"manage_roles\", \"manage_event_rules\", \"manage_apikeys\", \"manage_system\", \"manage_ip_lists\"",
|
||||
"users_groups": "Groups for users",
|
||||
"users_groups_help": "Groups automatically selected for new users created by this admin. The admin will still be able to choose different groups. These settings are only used for this admin UI and they will be ignored in REST API/hooks",
|
||||
"group_membership": "Add as membership",
|
||||
"group_primary": "Add as primary",
|
||||
"group_secondary": "Add as secondary",
|
||||
"user_page_pref": "User page preferences",
|
||||
"user_page_pref_help": "You can hide some sections from the user page. These are not security settings and are not enforced server side in any way. They are only intended to simplify the add/update user page",
|
||||
"hide_sections": "Hide sections",
|
||||
"default_users_expiration": "Default users expiration",
|
||||
"default_users_expiration_help": "Default expiration for new users as number of days"
|
||||
},
|
||||
"connections": {
|
||||
"view_manage": "View and manage connections",
|
||||
|
|
|
@ -56,7 +56,9 @@
|
|||
"oauth2_error": "Impossibile completare il flusso OAuth2",
|
||||
"oauth2_success": "OAuth2 completato",
|
||||
"add_role": "Aggiungi ruolo",
|
||||
"update_role": "Aggiorna ruolo"
|
||||
"update_role": "Aggiorna ruolo",
|
||||
"add_admin": "Aggiungi amministratore",
|
||||
"update_admin": "Aggiorna amministratore"
|
||||
},
|
||||
"setup": {
|
||||
"desc": "Per iniziare a utilizzare SFTPGo devi creare un utente amministratore",
|
||||
|
@ -225,7 +227,9 @@
|
|||
"protocol": "Protocollo",
|
||||
"refresh": "Aggiorna",
|
||||
"members": "Membri",
|
||||
"members_summary": "Utenti: {{users}}. Amministratori: {{admins}}"
|
||||
"members_summary": "Utenti: {{users}}. Amministratori: {{admins}}",
|
||||
"status": "Stato",
|
||||
"last_login": "Ultimo accesso"
|
||||
},
|
||||
"fs": {
|
||||
"view_file": "Visualizza file \"{{- path}}\"",
|
||||
|
@ -468,8 +472,6 @@
|
|||
"file_pattern_invalid": "Filtri su modelli di nome file non validi",
|
||||
"disable_active_2fa": "L'autenticazione a due fattori non può essere disabilitata per un utente con una configurazione attiva",
|
||||
"pwd_change_conflict": "Non è possibile richiedere la modifica della password e allo stesso tempo impedire la modifica della password",
|
||||
"status": "Stato",
|
||||
"last_login": "Ultimo accesso",
|
||||
"role_help": "Gli utenti con un ruolo possono essere gestiti da amministratori globali e amministratori con lo stesso ruolo",
|
||||
"require_pwd_change": "Richiedi modifica password",
|
||||
"require_pwd_change_help": "L'utente dovrà modificare la password dal WebClient per attivare l'account",
|
||||
|
@ -673,7 +675,24 @@
|
|||
"external_auth_cache_time_help": "Tempo di memorizzazione nella cache, in secondi, per gli utenti autenticati utilizzando un hook di autenticazione esterno. 0 significa nessuna cache"
|
||||
},
|
||||
"admin": {
|
||||
"role_permissions": "Un amministratore di ruolo non può avere le seguenti autorizzazioni: {{val}}"
|
||||
"role_permissions": "Un amministratore di ruolo non può avere le seguenti autorizzazioni: {{val}}",
|
||||
"view_manage": "Visualizza e gestisci amministratori",
|
||||
"self_delete": "Non puoi eliminare te stesso",
|
||||
"self_permissions": "Non puoi rimuovere questi permessi a te stesso",
|
||||
"self_disable": "Non puoi disabilitare te stesso",
|
||||
"self_role": "Non puoi aggiungere/modificare il tuo ruolo",
|
||||
"password_help": "Se vuoto la password corrente non verrà modificata",
|
||||
"role_help": "L'impostazione di un ruolo limita l'amministratore a gestire solo gli utenti con lo stesso ruolo. Gli amministratori con un ruolo non possono avere le seguenti autorizzazioni: \"manage_admins\", \"manage_roles\", \"manage_event_rules\", \"manage_apikeys\", \"manage_system\", \"manage_ip_lists\"",
|
||||
"users_groups": "Gruppi per gli utenti",
|
||||
"users_groups_help": "Gruppi selezionati automaticamente per i nuovi utenti creati da questo amministratore. L'amministratore potrà comunque scegliere gruppi differenti. Queste impostazioni vengono utilizzate solo per questa interfaccia utente di amministrazione e verranno ignorate negli hook/API REST",
|
||||
"group_membership": "Aggiungi come di appartenenza",
|
||||
"group_primary": "Aggiungi come primario",
|
||||
"group_secondary": "Aggiungi come secondario",
|
||||
"user_page_pref": "Preferenze della pagina utente",
|
||||
"user_page_pref_help": "Puoi nascondere alcune sezioni dalla pagina utente. Queste non sono impostazioni di sicurezza e non vengono verificate lato server. Hanno il solo scopo di semplificare la pagina di creazione/modifica utenti",
|
||||
"hide_sections": "Nascondi sezioni",
|
||||
"default_users_expiration": "Scadenza predefinita utenti",
|
||||
"default_users_expiration_help": "Scadenza predefinita per i nuovi utenti espressa in numero di giorni"
|
||||
},
|
||||
"connections": {
|
||||
"view_manage": "Visualizza e gestisci connessioni attive",
|
||||
|
|
|
@ -1,114 +1,79 @@
|
|||
<!--
|
||||
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}}/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 admin{{else}}Edit admin{{end}}</h6>
|
||||
{{- define "page_body"}}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h3 data-i18n="{{.Title}}" class="card-title section-title"></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">×</span>
|
||||
</button>
|
||||
</div>
|
||||
{{end}}
|
||||
{{- template "errmsg" .Error}}
|
||||
<form id="admin_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
|
||||
<div class="form-group row">
|
||||
<label for="idUsername" class="col-sm-2 col-form-label">Username</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idUsername" name="username" placeholder=""
|
||||
value="{{.Admin.Username}}" maxlength="255" autocomplete="nope" spellcheck="false" required {{if not .IsAdd}}readonly{{end}}>
|
||||
<label for="idUsername" data-i18n="login.username" class="col-md-3 col-form-label">Username</label>
|
||||
<div class="col-md-9">
|
||||
<input id="idUsername" type="text" placeholder="" name="username" value="{{.Admin.Username}}" maxlength="255" autocomplete="off"
|
||||
spellcheck="false" required {{if .IsAdd}}class="form-control"{{else}}class="form-control-plaintext readonly-input" readonly{{end}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idStatus" class="col-sm-2 col-form-label">Status</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control selectpicker" id="idStatus" name="status">
|
||||
<option value="1" {{if eq .Admin.Status 1 }}selected{{end}}>Active</option>
|
||||
<option value="0" {{if eq .Admin.Status 0 }}selected{{end}}>Inactive</option>
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idPassword" data-i18n="login.password" class="col-md-3 col-form-label">Password</label>
|
||||
<div class="col-md-9">
|
||||
<input id="idPassword" type="password" class="form-control" name="password" autocomplete="new-password"
|
||||
spellcheck="false" value="" {{if not .IsAdd}}aria-describedby="idPasswordHelp"{{end}} />
|
||||
{{- if not .IsAdd}}
|
||||
<div id="idPasswordHelp" class="form-text" data-i18n="admin.password_help"></div>
|
||||
{{- end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idStatus" data-i18n="general.status" class="col-md-3 col-form-label">Status</label>
|
||||
<div class="col-md-9">
|
||||
<select id="idStatus" name="status" class="form-select" data-control="i18n-select2" data-hide-search="true">
|
||||
<option data-i18n="general.active" value="1" {{- if eq .Admin.Status 1 }} selected{{- end}}>Active</option>
|
||||
<option data-i18n="general.inactive" value="0" {{- if eq .Admin.Status 0 }} selected{{- end}}>Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idEmail" class="col-sm-2 col-form-label">Email</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idEmail" name="email" placeholder=""
|
||||
value="{{.Admin.Email}}" maxlength="255" spellcheck="false">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idDescription" name="description" placeholder=""
|
||||
value="{{.Admin.Description}}" maxlength="255" aria-describedby="descriptionHelpBlock">
|
||||
<small id="descriptionHelpBlock" class="form-text text-muted">
|
||||
Optional description, for example the admin full name
|
||||
</small>
|
||||
</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" placeholder="" autocomplete="new-password" spellcheck="false"
|
||||
{{if not .IsAdd}}aria-describedby="pwdHelpBlock" {{end}}>
|
||||
{{if not .IsAdd}}
|
||||
<small id="pwdHelpBlock" class="form-text text-muted">
|
||||
If empty the current password will not be changed
|
||||
</small>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idPermissions" class="col-sm-2 col-form-label">Permissions</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control selectpicker" id="idPermissions" name="permissions" required multiple>
|
||||
{{range $validPerm := .Admin.GetValidPerms}}
|
||||
<option value="{{$validPerm}}" {{range $perm :=$.Admin.Permissions }}
|
||||
{{if eq $perm $validPerm}}selected{{end}}{{end}}>{{$validPerm}}
|
||||
</option>
|
||||
{{end}}
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idPermissions" data-i18n="general.permissions" class="col-md-3 col-form-label">Permissions</label>
|
||||
<div class="col-md-9">
|
||||
<select id="idPermissions" name="permissions" class="form-select" data-control="i18n-select2" data-hide-search="true" data-close-on-select="false" multiple>
|
||||
{{- range $validPerm := .Admin.GetValidPerms}}
|
||||
<option value="{{$validPerm}}" {{- range $perm :=$.Admin.Permissions }}{{- if eq $perm $validPerm}} selected{{- end}}{{- end}}>{{$validPerm}}</option>
|
||||
{{- end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header">
|
||||
<b>Role</b>
|
||||
{{- if .Roles}}
|
||||
<div class="card mt-10">
|
||||
<div class="card-header bg-light">
|
||||
<h3 data-i18n="general.role" class="card-title section-title-inner">Role</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-4">Setting a role limit the administrator to only manage users with the same role. Role administrators cannot have the following permissions: "manage_admins", "manage_roles", "manage_event_rules", "manage_apikeys", "manage_system", "manage_ip_lists"</h6>
|
||||
<div class="form-group row">
|
||||
<label for="idRole" class="col-sm-2 col-form-label">Role</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control selectpicker" data-live-search="true" id="idRole" name="role">
|
||||
<p data-i18n="admin.role_help" class="fs-5 fw-semibold mb-4"></p>
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idRole" data-i18n="general.role" class="col-md-3 col-form-label">Role</label>
|
||||
<div class="col-md-9">
|
||||
<select id="idRole" name="role" data-i18n="[data-placeholder]general.role_placeholder" class="form-select" data-control="i18n-select2" data-placeholder="Select a role" data-allow-clear="true">
|
||||
<option value=""></option>
|
||||
{{- range .Roles}}
|
||||
<option value="{{.Name}}" {{if eq $.Admin.Role .Name}}selected{{end}}>{{.Name}}</option>
|
||||
|
@ -118,185 +83,207 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{- end}}
|
||||
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header">
|
||||
<b>Groups for users</b>
|
||||
{{- if .Groups}}
|
||||
<div class="card mt-10">
|
||||
<div class="card-header bg-light">
|
||||
<h3 data-i18n="admin.users_groups" class="card-title section-title-inner">Groups for users</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-4">Groups automatically selected for new users created by this admin. The admin will still be able to choose different groups. These settings are only used for this admin UI and they will be ignored in REST API/hooks.</h6>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-12 form_field_groups_outer">
|
||||
{{range $idx, $val := .Admin.Groups}}
|
||||
<div class="row form_field_groups_outer_row">
|
||||
<div class="form-group col-md-7">
|
||||
<select class="form-control selectpicker" data-live-search="true" id="idGroup{{$idx}}" name="group{{$idx}}">
|
||||
<option value=""></option>
|
||||
{{- range $.Groups}}
|
||||
<option value="{{.Name}}" {{if eq $val.Name .Name}}selected{{end}}>{{.Name}}</option>
|
||||
{{- end}}
|
||||
</select>
|
||||
<div id="groups">
|
||||
<p class="fs-5 fw-semibold mb-4" data-i18n="admin.users_groups_help"></p>
|
||||
<div class="form-group">
|
||||
<div data-repeater-list="groups">
|
||||
{{range $idx, $val := .Admin.Groups}}
|
||||
<div data-repeater-item>
|
||||
<div data-repeater-item>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-6 mt-3 mt-md-8">
|
||||
<select name="group" data-i18n="[data-placeholder]general.group_placeholder" class="form-select select-repetear" data-allow-clear="true">
|
||||
<option value=""></option>
|
||||
{{- range $.Groups}}
|
||||
<option value="{{.Name}}" {{- if eq $val.Name .Name}} selected{{- end}}>{{.Name}}</option>
|
||||
{{- end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-5 mt-3 mt-md-8">
|
||||
<select name="group_type" class="form-select select-repetear">
|
||||
<option value="0" data-i18n="admin.group_membership" {{if eq $val.Options.AddToUsersAs 0}}selected{{end}}>Add as membership</option>
|
||||
<option value="1" data-i18n="admin.group_primary" {{if eq $val.Options.AddToUsersAs 1}}selected{{end}}>Add as primary</option>
|
||||
<option value="2" data-i18n="admin.group_secondary" {{if eq $val.Options.AddToUsersAs 2}}selected{{end}}>Add as secondary</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1 mt-3 mt-md-8">
|
||||
<a href="#" data-repeater-delete
|
||||
class="btn btn-light-danger ps-5 pe-4">
|
||||
<i class="ki-duotone ki-trash fs-2">
|
||||
<span class="path1"></span>
|
||||
<span class="path2"></span>
|
||||
<span class="path3"></span>
|
||||
<span class="path4"></span>
|
||||
<span class="path5"></span>
|
||||
</i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<select class="form-control selectpicker" id="idAddAsGroupType{{$idx}}" name="add_as_group_type{{$idx}}">
|
||||
<option value="0" {{if eq $val.Options.AddToUsersAs 0}}selected{{end}}>Add as membership</option>
|
||||
<option value="1" {{if eq $val.Options.AddToUsersAs 1}}selected{{end}}>Add as primary</option>
|
||||
<option value="2" {{if eq $val.Options.AddToUsersAs 2}}selected{{end}}>Add as secondary</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_group_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
{{- else}}
|
||||
<div data-repeater-item>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-6 mt-3 mt-md-8">
|
||||
<select name="group" data-i18n="[data-placeholder]general.group_placeholder" class="form-select select-repetear" data-allow-clear="true">
|
||||
<option value=""></option>
|
||||
{{- range $.Groups}}
|
||||
<option value="{{.Name}}">{{.Name}}</option>
|
||||
{{- end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-5 mt-3 mt-md-8">
|
||||
<select name="group_type" class="form-select select-repetear select-first">
|
||||
<option value="0" data-i18n="admin.group_membership">Add as membership</option>
|
||||
<option value="1" data-i18n="admin.group_primary">Add as primary</option>
|
||||
<option value="2" data-i18n="admin.group_secondary">Add as secondary</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1 mt-3 mt-md-8">
|
||||
<a href="#" data-repeater-delete
|
||||
class="btn btn-light-danger ps-5 pe-4">
|
||||
<i class="ki-duotone ki-trash fs-2">
|
||||
<span class="path1"></span>
|
||||
<span class="path2"></span>
|
||||
<span class="path3"></span>
|
||||
<span class="path4"></span>
|
||||
<span class="path5"></span>
|
||||
</i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{- end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="row form_field_groups_outer_row">
|
||||
<div class="form-group col-md-7">
|
||||
<select class="form-control selectpicker" data-live-search="true" id="idGroup0" name="group0">
|
||||
<option value=""></option>
|
||||
{{- range .Groups}}
|
||||
<option value="{{.Name}}">{{.Name}}</option>
|
||||
{{- end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<select class="form-control selectpicker" id="idAddAsGroupType0" name="add_as_group_type0">
|
||||
<option value="0">Add as membership</option>
|
||||
<option value="1">Add as primary</option>
|
||||
<option value="2">Add as secondary</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_group_btn_frm_field">
|
||||
<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_group_field_btn">
|
||||
<i class="fas fa-plus"></i> Add group
|
||||
</button>
|
||||
<div class="form-group mt-5">
|
||||
<a href="#" data-repeater-create class="btn btn-light-primary">
|
||||
<i class="ki-duotone ki-plus fs-3"></i>
|
||||
<span data-i18n="general.add">Add</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{- end}}
|
||||
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header">
|
||||
<b>User page preferences</b>
|
||||
<div class="card mt-10">
|
||||
<div class="card-header bg-light">
|
||||
<h3 data-i18n="admin.user_page_pref" class="card-title section-title-inner">User page preferences</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-4">You can hide some sections from the user page. These are not security settings and are not enforced server side in any way. They are only intended to simplify the user page in the WebAdmin UI.</h6>
|
||||
<div class="form-group row">
|
||||
<label for="idUserPageHiddenSections" class="col-sm-2 col-form-label">Hide sections</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control selectpicker" id="idUserPageHiddenSections" name="user_page_hidden_sections" multiple>
|
||||
<option value="1" {{if .Admin.Filters.Preferences.HideGroups}}selected{{end}}>Groups</option>
|
||||
<option value="2" {{if .Admin.Filters.Preferences.HideFilesystem}}selected{{end}}>Filesystem</option>
|
||||
<option value="3" {{if .Admin.Filters.Preferences.HideVirtualFolders}}selected{{end}}>Virtual Folders</option>
|
||||
<option value="4" {{if .Admin.Filters.Preferences.HideProfile}}selected{{end}}>Profile</option>
|
||||
<option value="5" {{if .Admin.Filters.Preferences.HideACLs}}selected{{end}}>ACLs</option>
|
||||
<option value="6" {{if .Admin.Filters.Preferences.HideDiskQuotaAndBandwidthLimits}}selected{{end}}>Disk quota and bandwidth limits</option>
|
||||
<option value="7" {{if .Admin.Filters.Preferences.HideAdvancedSettings}}selected{{end}}>Advanced settings</option>
|
||||
<p data-i18n="admin.user_page_pref_help" class="fs-5 fw-semibold mb-4"></p>
|
||||
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idUserPageHiddenSections" data-i18n="admin.hide_sections" class="col-md-3 col-form-label">Hide sections</label>
|
||||
<div class="col-md-9">
|
||||
<select id="idUserPageHiddenSections" name="user_page_hidden_sections" class="form-select" data-control="i18n-select2" data-hide-search="true" data-close-on-select="false" multiple>
|
||||
<option value="1" data-i18n="title.groups" {{if .Admin.Filters.Preferences.HideGroups}}selected{{end}}>Groups</option>
|
||||
<option value="2" data-i18n="storage.title" {{if .Admin.Filters.Preferences.HideFilesystem}}selected{{end}}>Filesystem</option>
|
||||
<option value="3" data-i18n="title.folders" {{if .Admin.Filters.Preferences.HideVirtualFolders}}selected{{end}}>Virtual Folders</option>
|
||||
<option value="4" data-i18n="title.profile" {{if .Admin.Filters.Preferences.HideProfile}}selected{{end}}>Profile</option>
|
||||
<option value="5" data-i18n="general.acls" {{if .Admin.Filters.Preferences.HideACLs}}selected{{end}}>ACLs</option>
|
||||
<option value="6" data-i18n="general.quota_limits" {{if .Admin.Filters.Preferences.HideDiskQuotaAndBandwidthLimits}}selected{{end}}>Disk quota and bandwidth limits</option>
|
||||
<option value="7" data-i18n="general.advanced_settings" {{if .Admin.Filters.Preferences.HideAdvancedSettings}}selected{{end}}>Advanced settings</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="idDefaultUsersExpiration" class="col-sm-2 col-form-label">Default users expiration</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" class="form-control" id="idDefaultUsersExpiration" name="default_users_expiration"
|
||||
value="{{.Admin.Filters.Preferences.DefaultUsersExpiration}}" min="0" aria-describedby="defaultUsersExpirationHelpBlock">
|
||||
<small id="defaultUsersExpirationHelpBlock" class="form-text text-muted">
|
||||
Default expiration for newly created users as number of days
|
||||
</small>
|
||||
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idDefaultUsersExpiration" data-i18n="admin.default_users_expiration" class="col-md-3 col-form-label">Default users expiration</label>
|
||||
<div class="col-md-9">
|
||||
<input id="idDefaultUsersExpiration" type="number" min="0" class="form-control" name="default_users_expiration" value="{{.Admin.Filters.Preferences.DefaultUsersExpiration}}" aria-describedby="idDefaultUsersExpirationHelp"/>
|
||||
<div id="idDefaultUsersExpirationHelp" class="form-text" data-i18n="admin.default_users_expiration_help"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idEmail" data-i18n="general.email" class="col-md-3 col-form-label">Email</label>
|
||||
<div class="col-md-9">
|
||||
<input id="idEmail" type="text" class="form-control" placeholder="" name="email" value="{{.Admin.Email}}"
|
||||
maxlength="255" autocomplete="off" spellcheck="false" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idDescription" data-i18n="general.description" class="col-md-3 col-form-label">Description</label>
|
||||
<div class="col-md-9">
|
||||
<input id="idDescription" type="text" class="form-control" name="description" value="{{.Admin.Description}}" maxlength="255">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idAllowedIP" data-i18n="general.allowed_ip_mask" class="col-md-3 col-form-label">Allowed IP/Mask</label>
|
||||
<div class="col-md-9">
|
||||
<textarea class="form-control" id="idAllowedIP" name="allowed_ip" aria-describedby="idAllowedIPHelp"
|
||||
rows="3">{{.Admin.GetAllowedIPAsString}}</textarea>
|
||||
<div id="idAllowedIPHelp" class="form-text" data-i18n="general.ip_mask_help"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row align-items-center mt-10">
|
||||
<label data-i18n="general.api_key_auth" class="col-md-3 col-form-label" for="idAllowAPIKeyAuth">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 .Admin.Filters.AllowAPIKeyAuth}}checked{{end}}/>
|
||||
<label data-i18n="filters.api_key_auth_help" class="form-check-label fw-semibold text-gray-800" for="idAllowAPIKeyAuth">
|
||||
Allow to impersonate yourself, in REST API, with an API key
|
||||
</label>
|
||||
</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">{{.Admin.GetAllowedIPAsString}}</textarea>
|
||||
<small id="allowedIPHelpBlock" class="form-text text-muted">
|
||||
Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32"
|
||||
</small>
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idAdditionalInfo" data-i18n="general.additional_info" class="col-md-3 col-form-label">Additional info</label>
|
||||
<div class="col-md-9">
|
||||
<textarea id="idAdditionalInfo" class="form-control" name="additional_info" rows="3">{{.Admin.AdditionalInfo}}</textarea>
|
||||
</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 .Admin.Filters.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 this admin, in REST API, with an API key
|
||||
</small>
|
||||
</div>
|
||||
<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" name="form_action" value="submit">
|
||||
<span data-i18n="general.submit" class="indicator-label">
|
||||
Submit
|
||||
</span>
|
||||
<span data-i18n="general.wait" class="indicator-progress">
|
||||
Please wait...
|
||||
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idAdditionalInfo" class="col-sm-2 col-form-label">Additional info</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="idAdditionalInfo" name="additional_info" rows="3"
|
||||
aria-describedby="additionalInfoHelpBlock">{{.Admin.AdditionalInfo}}</textarea>
|
||||
<small id="additionalInfoHelpBlock" class="form-text text-muted">
|
||||
Free form text field
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="btn btn-primary float-right mt-3 px-5">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{- end}}
|
||||
|
||||
{{define "extra_js"}}
|
||||
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$("body").on("click", ".add_new_group_field_btn", function () {
|
||||
let index = $(".form_field_groups_outer").find(".form_field_groups_outer_row").length;
|
||||
while (document.getElementById("idGroup"+index) != null){
|
||||
index++;
|
||||
}
|
||||
$(".form_field_groups_outer").append(`
|
||||
<div class="row form_field_groups_outer_row">
|
||||
<div class="form-group col-md-7">
|
||||
<select class="form-control" id="idGroup${index}" name="group${index}">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<select class="form-control" id="idAddAsGroupType${index}" name="add_as_group_type${index}">
|
||||
<option value="0">Add as membership</option>
|
||||
<option value="1">Add as primary</option>
|
||||
<option value="2">Add as secondary</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-1">
|
||||
<button class="btn btn-circle btn-danger remove_group_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
{{- range .Groups}}
|
||||
$("#idGroup"+index).append($('<option>').val('{{.Name}}').text('{{.Name}}'));
|
||||
{{- end}}
|
||||
$("#idGroup"+index).selectpicker({'liveSearch': true});
|
||||
$("#idAddAsGroupType"+index).selectpicker();
|
||||
{{- define "extra_js"}}
|
||||
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/formrepeater/formrepeater.bundle.js"></script>
|
||||
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
|
||||
|
||||
$(document).on("i18nload", function(){
|
||||
initRepeater('#groups');
|
||||
initRepeaterItems();
|
||||
});
|
||||
|
||||
$("body").on("click", ".remove_group_btn_frm_field", function () {
|
||||
$(this).closest(".form_field_groups_outer_row").remove();
|
||||
$(document).on("i18nshow", function(){
|
||||
$('#admin_form').submit(function (event) {
|
||||
let submitButton = document.querySelector('#form_submit');
|
||||
submitButton.setAttribute('data-kt-indicator', 'on');
|
||||
submitButton.disabled = true;
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
{{- end}}
|
|
@ -1,272 +1,433 @@
|
|||
<!--
|
||||
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">×</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 admins</h6>
|
||||
{{- define "page_body"}}
|
||||
{{- template "errmsg" ""}}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h3 data-i18n="admin.view_manage" class="card-title section-title">View and manage admins</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-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="checkColStatus" />
|
||||
<label class="form-check-label" for="checkColStatus">
|
||||
<span data-i18n="general.status" class="text-gray-800 fs-6">Status</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="checkColLastLogin" />
|
||||
<label class="form-check-label" for="checkColLastLogin">
|
||||
<span data-i18n="general.last_login" class="text-gray-800 fs-6">Last login</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="checkColEmail" />
|
||||
<label class="form-check-label" for="checkColEmail">
|
||||
<span data-i18n="general.email" class="text-gray-800 fs-6">Email</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="checkColRole" />
|
||||
<label class="form-check-label" for="checkColRole">
|
||||
<span data-i18n="general.role" class="text-gray-800 fs-6">Role</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="checkCol2FA" />
|
||||
<label class="form-check-label" for="checkCol2FA">
|
||||
<span data-i18n="title.two_factor_auth_short" class="text-gray-800 fs-6">2FA</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_admins"}}
|
||||
<a href="{{.AdminURL}}" 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>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Username</th>
|
||||
<th>Status</th>
|
||||
<th>Permissions</th>
|
||||
<th>Last login</th>
|
||||
<th>MFA</th>
|
||||
<th>Allow list</th>
|
||||
<th>Email</th>
|
||||
<th>Description</th>
|
||||
<th>Groups for users</th>
|
||||
<th>Role</th>
|
||||
<tr class="text-start text-muted fw-bold fs-6 gs-0">
|
||||
<th data-i18n="login.username">Username</th>
|
||||
<th data-i18n="general.status">Status</th>
|
||||
<th data-i18n="general.last_login">Last login</th>
|
||||
<th data-i18n="general.email">Email</th>
|
||||
<th data-i18n="general.role">Role</th>
|
||||
<th data-i18n="title.two_factor_auth_short">2FA</th>
|
||||
<th data-i18n="general.description">Description</th>
|
||||
<th class="min-w-100px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Admins}}
|
||||
<tr>
|
||||
<td>{{.ID}}</td>
|
||||
<td>{{.Username}}</td>
|
||||
<td>{{if eq .Status 1 }}Active{{else}}Inactive{{end}}</td>
|
||||
<td>{{.GetPermissionsAsString}}</td>
|
||||
<td>{{.GetLastLoginAsString}}</td>
|
||||
<td>{{if .Filters.TOTPConfig.Enabled }}Enabled{{else}}Disabled{{end}}</td>
|
||||
<td>{{.GetAllowedIPAsString}}</td>
|
||||
<td>{{.Email}}</td>
|
||||
<td>{{.Description}}</td>
|
||||
<td>{{.GetGroupsAsString}}</td>
|
||||
<td>{{.Role}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
|
||||
</tbody>
|
||||
<tbody id="table_body" class="text-gray-800 fw-semibold"></tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{- end}}
|
||||
{{- define "extra_js"}}
|
||||
<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}}>
|
||||
function deleteAction(username) {
|
||||
ModalAlert.fire({
|
||||
text: $.t('general.delete_confirm_generic'),
|
||||
icon: "warning",
|
||||
confirmButtonText: $.t('general.delete_confirm_btn'),
|
||||
cancelButtonText: $.t('general.cancel'),
|
||||
customClass: {
|
||||
confirmButton: "btn btn-danger",
|
||||
cancelButton: 'btn btn-secondary'
|
||||
}
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed){
|
||||
$('#loading_message').text("");
|
||||
KTApp.showPageLoading();
|
||||
let path = '{{.AdminURL}}' + "/" + encodeURIComponent(username);
|
||||
|
||||
{{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">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">Do you want to delete the selected admin?</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() {
|
||||
var table = $('#dataTable').DataTable();
|
||||
table.button('delete:name').enable(false);
|
||||
var username = table.row({ selected: true }).data()[1];
|
||||
var path = '{{.AdminURL}}' + "/" + 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 = '{{.AdminsURL}}';
|
||||
},
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
var txt = "Unable to delete the selected admin";
|
||||
if ($xhr) {
|
||||
var json = $xhr.responseJSON;
|
||||
if (json) {
|
||||
if (json.message){
|
||||
txt += ": " + json.message;
|
||||
} else {
|
||||
txt += ": " + json.error;
|
||||
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;
|
||||
if (error && error.response) {
|
||||
switch (error.response.status) {
|
||||
case 400:
|
||||
if (error.response.data && error.response.data.error){
|
||||
if (error.response.data.error.includes("delete yourself")){
|
||||
errorMessage = "admin.self_delete";
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 403:
|
||||
errorMessage = "general.delete_error_403";
|
||||
break;
|
||||
case 404:
|
||||
errorMessage = "general.delete_error_404";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$('#errorTxt').text(txt);
|
||||
$('#errorMsg').show();
|
||||
if (!errorMessage){
|
||||
errorMessage = "general.delete_error_generic";
|
||||
}
|
||||
ModalAlert.fire({
|
||||
text: $.t(errorMessage),
|
||||
icon: "warning",
|
||||
confirmButtonText: $.t('general.ok'),
|
||||
customClass: {
|
||||
confirmButton: "btn btn-primary"
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(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 = '{{.AdminURL}}';
|
||||
var datatable = function(){
|
||||
var dt;
|
||||
|
||||
var initDatatable = function () {
|
||||
$('#errorMsg').addClass("d-none");
|
||||
dt = $('#dataTable').DataTable({
|
||||
ajax: {
|
||||
url: "{{.AdminsURL}}/json",
|
||||
dataSrc: "",
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
$(".dataTables_processing").hide();
|
||||
let txt = "";
|
||||
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: "username",
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
return escapeHTML(data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "status",
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
switch (data){
|
||||
case 1:
|
||||
return $.t('general.active');
|
||||
default:
|
||||
return $.t('general.inactive');
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "last_login",
|
||||
defaultContent: "",
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
if (data > 0){
|
||||
return $.t('general.datetime', {
|
||||
val: parseInt(data, 10),
|
||||
formatParams: {
|
||||
val: { year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric' },
|
||||
}
|
||||
});
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "email",
|
||||
visible: false,
|
||||
defaultContent: "",
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
if (data){
|
||||
return escapeHTML(data);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "role",
|
||||
visible: false,
|
||||
defaultContent: "",
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
if (data){
|
||||
return escapeHTML(data);
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "filters.totp_config.enabled",
|
||||
visible: false,
|
||||
defaultContent: false,
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
if (data){
|
||||
return $.t('general.active');
|
||||
}
|
||||
return $.t('general.inactive')
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
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') {
|
||||
//{{- if .LoggedUser.HasPermission "manage_admins"}}
|
||||
return `<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">
|
||||
<div class="menu-item px-3">
|
||||
<a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
|
||||
</div>
|
||||
<div class="menu-item px-3">
|
||||
<a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
|
||||
</div>
|
||||
</div>`;
|
||||
//{{- end}}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
},
|
||||
],
|
||||
deferRender: true,
|
||||
stateSave: true,
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
dt.on('draw', drawAction);
|
||||
dt.on('column-reorder', function(e, settings, details){
|
||||
drawAction();
|
||||
});
|
||||
}
|
||||
|
||||
function drawAction() {
|
||||
KTMenu.createInstances();
|
||||
handleRowActions();
|
||||
$('#table_body').localize();
|
||||
}
|
||||
|
||||
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');
|
||||
});
|
||||
}
|
||||
|
||||
var handleDatatableActions = function () {
|
||||
const filterSearch = $(document.querySelector('[data-table-filter="search"]'));
|
||||
filterSearch.off("keyup");
|
||||
filterSearch.on('keyup', function (e) {
|
||||
dt.rows().deselect();
|
||||
dt.search(e.target.value, true, false).draw();
|
||||
});
|
||||
handleColVisibilityCheckbox($('#checkColStatus'), 1);
|
||||
handleColVisibilityCheckbox($('#checkColLastLogin'), 2);
|
||||
handleColVisibilityCheckbox($('#checkColEmail'), 3);
|
||||
handleColVisibilityCheckbox($('#checkColRole'), 4);
|
||||
handleColVisibilityCheckbox($('#checkCol2FA'), 5);
|
||||
handleColVisibilityCheckbox($('#checkColDesc'), 6);
|
||||
}
|
||||
|
||||
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('{{.AdminURL}}' + "/" + encodeURIComponent(rowData['username']));
|
||||
});
|
||||
});
|
||||
|
||||
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()['username']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
init: function () {
|
||||
initDatatable();
|
||||
handleDatatableActions();
|
||||
}
|
||||
};
|
||||
}
|
||||
}();
|
||||
|
||||
$.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 = '{{.AdminURL}}' + "/" + fixedEncodeURIComponent(username);
|
||||
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
|
||||
};
|
||||
|
||||
var 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],
|
||||
"visible": false,
|
||||
"searchable": false,
|
||||
"className": "noVis"
|
||||
},
|
||||
{
|
||||
"targets": [1],
|
||||
"className": "noVis"
|
||||
},
|
||||
{
|
||||
"targets": [3],
|
||||
"render": $.fn.dataTable.render.ellipsis(70, true)
|
||||
},
|
||||
{
|
||||
"targets": [4],
|
||||
"render": $.fn.dataTable.render.datetime()
|
||||
},
|
||||
{
|
||||
"targets": [6],
|
||||
"render": $.fn.dataTable.render.ellipsis(60, true),
|
||||
"visible": false,
|
||||
},
|
||||
{
|
||||
"targets": [5,7,8,10],
|
||||
"visible": false,
|
||||
},
|
||||
{
|
||||
"targets": [9],
|
||||
"render": $.fn.dataTable.render.ellipsis(50, true),
|
||||
"visible": false,
|
||||
}
|
||||
],
|
||||
"scrollX": false,
|
||||
"scrollY": false,
|
||||
"responsive": true,
|
||||
"order": [[1, 'asc']]
|
||||
});
|
||||
|
||||
new $.fn.dataTable.FixedHeader( table );
|
||||
|
||||
{{if .LoggedAdmin.HasPermission "manage_admins"}}
|
||||
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('delete:name').enable(selectedRows == 1);
|
||||
});
|
||||
{{end}}
|
||||
$(document).on("i18nshow", function(){
|
||||
datatable.init();
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
{{- end}}
|
|
@ -370,7 +370,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
<div class="accordion-body">
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idStatus" data-i18n="user.status" class="col-md-3 col-form-label">Status</label>
|
||||
<label for="idStatus" data-i18n="general.status" class="col-md-3 col-form-label">Status</label>
|
||||
<div class="col-md-9">
|
||||
<select id="idStatus" name="status" class="form-select" data-control="i18n-select2" data-hide-search="true">
|
||||
<option data-i18n="general.active" value="1" {{- if eq .User.Status 1 }} selected{{- end}}>Active</option>
|
||||
|
|
|
@ -45,15 +45,15 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
</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="user.status" class="text-gray-800 fs-6">Status</span>
|
||||
<input type="checkbox" class="form-check-input" value="" id="checkColStatus" />
|
||||
<label class="form-check-label" for="checkColStatus">
|
||||
<span data-i18n="general.status" class="text-gray-800 fs-6">Status</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="checkColLastLogin" />
|
||||
<label class="form-check-label" for="checkColLastLogin">
|
||||
<span data-i18n="user.last_login" class="text-gray-800 fs-6">Last login</span>
|
||||
<span data-i18n="general.last_login" class="text-gray-800 fs-6">Last login</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid">
|
||||
|
@ -112,8 +112,8 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
<thead>
|
||||
<tr class="text-start text-muted fw-bold fs-6 gs-0">
|
||||
<th data-i18n="login.username">Username</th>
|
||||
<th data-i18n="user.status">Status</th>
|
||||
<th data-i18n="user.last_login">Last login</th>
|
||||
<th data-i18n="general.status">Status</th>
|
||||
<th data-i18n="general.last_login">Last login</th>
|
||||
<th data-i18n="general.email">Email</th>
|
||||
<th data-i18n="storage.label">Storage</th>
|
||||
<th data-i18n="general.role">Role</th>
|
||||
|
|
Loading…
Reference in a new issue