WIP new WebAdmin: role page

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2024-01-21 17:19:25 +01:00
parent 3f479c5537
commit e38350e8b3
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
7 changed files with 82 additions and 90 deletions

View file

@ -237,15 +237,3 @@ func (g *Group) getACopy() Group {
VirtualFolders: virtualFolders, VirtualFolders: virtualFolders,
} }
} }
// GetMembersAsString returns a string representation for the group members
func (g *Group) GetMembersAsString() string {
var sb strings.Builder
if len(g.Users) > 0 {
sb.WriteString(fmt.Sprintf("Users: %d. ", len(g.Users)))
}
if len(g.Admins) > 0 {
sb.WriteString(fmt.Sprintf("Admins: %d. ", len(g.Admins)))
}
return sb.String()
}

View file

@ -17,7 +17,6 @@ package dataprovider
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
"github.com/drakkan/sftpgo/v2/internal/logger" "github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/util" "github.com/drakkan/sftpgo/v2/internal/util"
@ -56,13 +55,16 @@ func (r *Role) RenderAsJSON(reload bool) ([]byte, error) {
func (r *Role) validate() error { func (r *Role) validate() error {
if r.Name == "" { if r.Name == "" {
return util.NewValidationError("name is mandatory") return util.NewI18nError(util.NewValidationError("name is mandatory"), util.I18nErrorNameRequired)
} }
if len(r.Name) > 255 { if len(r.Name) > 255 {
return util.NewValidationError("name is too long, 255 is the maximum length allowed") return util.NewValidationError("name is too long, 255 is the maximum length allowed")
} }
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(r.Name) { if config.NamingRules&1 == 0 && !usernameRegex.MatchString(r.Name) {
return util.NewValidationError(fmt.Sprintf("name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", r.Name)) return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", r.Name)),
util.I18nErrorInvalidName,
)
} }
return nil return nil
} }
@ -83,15 +85,3 @@ func (r *Role) getACopy() Role {
Admins: admins, Admins: admins,
} }
} }
// GetMembersAsString returns a string representation for the role members
func (r *Role) GetMembersAsString() string {
var sb strings.Builder
if len(r.Users) > 0 {
sb.WriteString(fmt.Sprintf("Users: %d. ", len(r.Users)))
}
if len(r.Admins) > 0 {
sb.WriteString(fmt.Sprintf("Admins: %d. ", len(r.Admins)))
}
return sb.String()
}

View file

@ -307,7 +307,7 @@ type groupPage struct {
type rolePage struct { type rolePage struct {
basePage basePage
Role *dataprovider.Role Role *dataprovider.Role
Error string Error *util.I18nError
Mode genericPageMode Mode genericPageMode
} }
@ -516,7 +516,7 @@ func loadAdminTemplates(templatesPath string) {
filepath.Join(templatesPath, templateAdminDir, templateRoles), filepath.Join(templatesPath, templateAdminDir, templateRoles),
} }
rolePaths := []string{ rolePaths := []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, templateRole), filepath.Join(templatesPath, templateAdminDir, templateRole),
} }
@ -1024,20 +1024,20 @@ func (s *httpdServer) renderIPListPage(w http.ResponseWriter, r *http.Request, e
} }
func (s *httpdServer) renderRolePage(w http.ResponseWriter, r *http.Request, role dataprovider.Role, func (s *httpdServer) renderRolePage(w http.ResponseWriter, r *http.Request, role dataprovider.Role,
mode genericPageMode, error string, mode genericPageMode, err error,
) { ) {
var title, currentURL string var title, currentURL string
switch mode { switch mode {
case genericPageModeAdd: case genericPageModeAdd:
title = "Add a new role" title = util.I18nRoleAddTitle
currentURL = webAdminRolePath currentURL = webAdminRolePath
case genericPageModeUpdate: case genericPageModeUpdate:
title = "Update role" title = util.I18nRoleUpdateTitle
currentURL = fmt.Sprintf("%s/%s", webAdminRolePath, url.PathEscape(role.Name)) currentURL = fmt.Sprintf("%s/%s", webAdminRolePath, url.PathEscape(role.Name))
} }
data := rolePage{ data := rolePage{
basePage: s.getBasePageData(title, currentURL, r), basePage: s.getBasePageData(title, currentURL, r),
Error: error, Error: getI18nError(err),
Role: &role, Role: &role,
Mode: mode, Mode: mode,
} }
@ -1751,7 +1751,7 @@ func getAdminFromPostFields(r *http.Request) (dataprovider.Admin, error) {
var admin dataprovider.Admin var admin dataprovider.Admin
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
return admin, err return admin, util.NewI18nError(err, util.I18nErrorInvalidForm)
} }
status, err := strconv.Atoi(r.Form.Get("status")) status, err := strconv.Atoi(r.Form.Get("status"))
if err != nil { if err != nil {
@ -2346,7 +2346,7 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
func getEventActionFromPostFields(r *http.Request) (dataprovider.BaseEventAction, error) { func getEventActionFromPostFields(r *http.Request) (dataprovider.BaseEventAction, error) {
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
return dataprovider.BaseEventAction{}, err return dataprovider.BaseEventAction{}, util.NewI18nError(err, util.I18nErrorInvalidForm)
} }
actionType, err := strconv.Atoi(r.Form.Get("type")) actionType, err := strconv.Atoi(r.Form.Get("type"))
if err != nil { if err != nil {
@ -2500,7 +2500,7 @@ func getEventRuleActionsFromPostFields(r *http.Request) ([]dataprovider.EventAct
func getEventRuleFromPostFields(r *http.Request) (dataprovider.EventRule, error) { func getEventRuleFromPostFields(r *http.Request) (dataprovider.EventRule, error) {
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
return dataprovider.EventRule{}, err return dataprovider.EventRule{}, util.NewI18nError(err, util.I18nErrorInvalidForm)
} }
status, err := strconv.Atoi(r.Form.Get("status")) status, err := strconv.Atoi(r.Form.Get("status"))
if err != nil { if err != nil {
@ -2532,7 +2532,7 @@ func getEventRuleFromPostFields(r *http.Request) (dataprovider.EventRule, error)
func getRoleFromPostFields(r *http.Request) (dataprovider.Role, error) { func getRoleFromPostFields(r *http.Request) (dataprovider.Role, error) {
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
return dataprovider.Role{}, err return dataprovider.Role{}, util.NewI18nError(err, util.I18nErrorInvalidForm)
} }
return dataprovider.Role{ return dataprovider.Role{
@ -2544,7 +2544,7 @@ func getRoleFromPostFields(r *http.Request) (dataprovider.Role, error) {
func getIPListEntryFromPostFields(r *http.Request, listType dataprovider.IPListType) (dataprovider.IPListEntry, error) { func getIPListEntryFromPostFields(r *http.Request, listType dataprovider.IPListType) (dataprovider.IPListEntry, error) {
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
return dataprovider.IPListEntry{}, err return dataprovider.IPListEntry{}, util.NewI18nError(err, util.I18nErrorInvalidForm)
} }
var mode int var mode int
if listType == dataprovider.IPListTypeDefender { if listType == dataprovider.IPListTypeDefender {
@ -3890,14 +3890,14 @@ func (s *httpdServer) handleWebGetRoles(w http.ResponseWriter, r *http.Request)
func (s *httpdServer) handleWebAddRoleGet(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleWebAddRoleGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
s.renderRolePage(w, r, dataprovider.Role{}, genericPageModeAdd, "") s.renderRolePage(w, r, dataprovider.Role{}, genericPageModeAdd, nil)
} }
func (s *httpdServer) handleWebAddRolePost(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleWebAddRolePost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
role, err := getRoleFromPostFields(r) role, err := getRoleFromPostFields(r)
if err != nil { if err != nil {
s.renderRolePage(w, r, role, genericPageModeAdd, err.Error()) s.renderRolePage(w, r, role, genericPageModeAdd, err)
return return
} }
claims, err := getTokenClaims(r) claims, err := getTokenClaims(r)
@ -3912,7 +3912,7 @@ func (s *httpdServer) handleWebAddRolePost(w http.ResponseWriter, r *http.Reques
} }
err = dataprovider.AddRole(&role, claims.Username, ipAddr, claims.Role) err = dataprovider.AddRole(&role, claims.Username, ipAddr, claims.Role)
if err != nil { if err != nil {
s.renderRolePage(w, r, role, genericPageModeAdd, err.Error()) s.renderRolePage(w, r, role, genericPageModeAdd, err)
return return
} }
http.Redirect(w, r, webAdminRolesPath, http.StatusSeeOther) http.Redirect(w, r, webAdminRolesPath, http.StatusSeeOther)
@ -3922,7 +3922,7 @@ func (s *httpdServer) handleWebUpdateRoleGet(w http.ResponseWriter, r *http.Requ
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
role, err := dataprovider.RoleExists(getURLParam(r, "name")) role, err := dataprovider.RoleExists(getURLParam(r, "name"))
if err == nil { if err == nil {
s.renderRolePage(w, r, role, genericPageModeUpdate, "") s.renderRolePage(w, r, role, genericPageModeUpdate, nil)
} else if errors.Is(err, util.ErrNotFound) { } else if errors.Is(err, util.ErrNotFound) {
s.renderNotFoundPage(w, r, err) s.renderNotFoundPage(w, r, err)
} else { } else {
@ -3948,7 +3948,7 @@ func (s *httpdServer) handleWebUpdateRolePost(w http.ResponseWriter, r *http.Req
updatedRole, err := getRoleFromPostFields(r) updatedRole, err := getRoleFromPostFields(r)
if err != nil { if err != nil {
s.renderRolePage(w, r, role, genericPageModeUpdate, err.Error()) s.renderRolePage(w, r, role, genericPageModeUpdate, err)
return return
} }
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
@ -3960,7 +3960,7 @@ func (s *httpdServer) handleWebUpdateRolePost(w http.ResponseWriter, r *http.Req
updatedRole.Name = role.Name updatedRole.Name = role.Name
err = dataprovider.UpdateRole(&updatedRole, claims.Username, ipAddr, claims.Role) err = dataprovider.UpdateRole(&updatedRole, claims.Username, ipAddr, claims.Role)
if err != nil { if err != nil {
s.renderRolePage(w, r, updatedRole, genericPageModeUpdate, err.Error()) s.renderRolePage(w, r, updatedRole, genericPageModeUpdate, err)
return return
} }
http.Redirect(w, r, webAdminRolesPath, http.StatusSeeOther) http.Redirect(w, r, webAdminRolesPath, http.StatusSeeOther)
@ -4113,7 +4113,7 @@ func (s *httpdServer) handleWebConfigsPost(w http.ResponseWriter, r *http.Reques
} }
err = r.ParseForm() err = r.ParseForm()
if err != nil { if err != nil {
s.renderBadRequestPage(w, r, err) s.renderBadRequestPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
return return
} }
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)

View file

@ -193,6 +193,8 @@ const (
I18nErrorFsUsernameRequired = "storage.username_required" I18nErrorFsUsernameRequired = "storage.username_required"
I18nAddGroupTitle = "title.add_group" I18nAddGroupTitle = "title.add_group"
I18nUpdateGroupTitle = "title.update_group" I18nUpdateGroupTitle = "title.update_group"
I18nRoleAddTitle = "title.add_role"
I18nRoleUpdateTitle = "title.update_role"
I18nErrorInvalidTLSCert = "user.tls_cert_invalid" I18nErrorInvalidTLSCert = "user.tls_cert_invalid"
I18nAddFolderTitle = "title.add_folder" I18nAddFolderTitle = "title.add_folder"
I18nUpdateFolderTitle = "title.update_folder" I18nUpdateFolderTitle = "title.update_folder"

View file

@ -54,7 +54,9 @@
"update_folder": "Update virtual folder", "update_folder": "Update virtual folder",
"template_folder": "Virtual folder template", "template_folder": "Virtual folder template",
"oauth2_error": "Unable to complete OAuth2 flow", "oauth2_error": "Unable to complete OAuth2 flow",
"oauth2_success": "OAuth2 flow completed" "oauth2_success": "OAuth2 flow completed",
"add_role": "Add role",
"update_role": "Update role"
}, },
"setup": { "setup": {
"desc": "To start using SFTPGo you need to create an administrator user", "desc": "To start using SFTPGo you need to create an administrator user",

View file

@ -54,7 +54,9 @@
"update_folder": "Aggiorna cartella virtuale", "update_folder": "Aggiorna cartella virtuale",
"template_folder": "Modello cartella virtuale", "template_folder": "Modello cartella virtuale",
"oauth2_error": "Impossibile completare il flusso OAuth2", "oauth2_error": "Impossibile completare il flusso OAuth2",
"oauth2_success": "OAuth2 completato" "oauth2_success": "OAuth2 completato",
"add_role": "Aggiungi ruolo",
"update_role": "Aggiorna ruolo"
}, },
"setup": { "setup": {
"desc": "Per iniziare a utilizzare SFTPGo devi creare un utente amministratore", "desc": "Per iniziare a utilizzare SFTPGo devi creare un utente amministratore",

View file

@ -1,61 +1,69 @@
<!-- <!--
Copyright (C) 2019 Nicola Murino Copyright (C) 2024 Nicola Murino
This program is free software: you can redistribute it and/or modify This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, https://keenthemes.com/products/templates-mega-bundle
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License KeenThemes HTML/CSS/JS components are allowed for use only within the
along with this program. If not, see <https://www.gnu.org/licenses/>. SFTPGo product and restricted to be used in a resealable HTML template
that can compete with KeenThemes products anyhow.
This WebUI is allowed for use only within the SFTPGo product and
therefore cannot be used in derivative works/products without an
explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{template "base" .}} {{template "base" .}}
{{define "title"}}{{.Title}}{{end}} {{- define "page_body"}}
<div class="card shadow-sm">
{{define "page_body"}} <div class="card-header bg-light">
<!-- Page Heading --> <h3 data-i18n="{{.Title}}" class="card-title section-title"></h3>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">{{.Title}}</h6>
</div> </div>
<div class="card-body"> <div class="card-body">
{{if .Error}} {{- template "errmsg" .Error}}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
{{.Error}}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{{end}}
<form id="role_form" action="{{.CurrentURL}}" method="POST" autocomplete="off"> <form id="role_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
<div class="form-group row"> <div class="form-group row">
<label for="idRoleName" class="col-sm-2 col-form-label">Name</label> <label for="idRoleName" data-i18n="general.name" class="col-md-3 col-form-label">Name</label>
<div class="col-sm-10"> <div class="col-md-9">
<input type="text" class="form-control" id="idRoleName" name="name" placeholder="" <input id="idRoleName" type="text" class="form-control" placeholder="" name="name" value="{{.Role.Name}}"
value="{{.Role.Name}}" maxlength="255" autocomplete="nope" required {{if eq .Mode 2}}readonly{{end}}> maxlength="255" autocomplete="nope" spellcheck="false" required {{if eq .Mode 2}}readonly{{end}} />
</div>
</div>
<div class="form-group row">
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idDescription" name="description" placeholder=""
value="{{.Role.Description}}" maxlength="255" aria-describedby="descriptionHelpBlock">
<small id="descriptionHelpBlock" class="form-text text-muted">
Optional description
</small>
</div> </div>
</div> </div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <div class="form-group row mt-10">
<div class="col-sm-12 text-right px-0"> <label for="idDescription" data-i18n="general.description" class="col-md-3 col-form-label">Description</label>
<button type="submit" class="btn btn-primary mt-3 ml-3 px-5" name="form_action" value="submit">Submit</button> <div class="col-md-9">
<input id="idDescription" type="text" class="form-control" name="description" value="{{.Role.Description}}" maxlength="255">
</div>
</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>
</form> </form>
</div> </div>
</div> </div>
{{end}} {{- end}}
{{- define "extra_js"}}
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
$(document).on("i18nshow", function(){
$('#role_form').submit(function (event) {
let submitButton = document.querySelector('#form_submit');
submitButton.setAttribute('data-kt-indicator', 'on');
submitButton.disabled = true;
});
});
</script>
{{- end}}