| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557 | 
							- package httpd
 
- import (
 
- 	"errors"
 
- 	"fmt"
 
- 	"html/template"
 
- 	"io"
 
- 	"net/http"
 
- 	"net/url"
 
- 	"path/filepath"
 
- 	"strconv"
 
- 	"strings"
 
- 	"time"
 
- 	"github.com/go-chi/render"
 
- 	"github.com/drakkan/sftpgo/common"
 
- 	"github.com/drakkan/sftpgo/dataprovider"
 
- 	"github.com/drakkan/sftpgo/kms"
 
- 	"github.com/drakkan/sftpgo/utils"
 
- 	"github.com/drakkan/sftpgo/version"
 
- 	"github.com/drakkan/sftpgo/vfs"
 
- )
 
- type userPageMode int
 
- const (
 
- 	userPageModeAdd userPageMode = iota + 1
 
- 	userPageModeUpdate
 
- 	userPageModeTemplate
 
- )
 
- type folderPageMode int
 
- const (
 
- 	folderPageModeAdd folderPageMode = iota + 1
 
- 	folderPageModeUpdate
 
- 	folderPageModeTemplate
 
- )
 
- const (
 
- 	templateAdminDir     = "webadmin"
 
- 	templateBase         = "base.html"
 
- 	templateFsConfig     = "fsconfig.html"
 
- 	templateUsers        = "users.html"
 
- 	templateUser         = "user.html"
 
- 	templateAdmins       = "admins.html"
 
- 	templateAdmin        = "admin.html"
 
- 	templateConnections  = "connections.html"
 
- 	templateFolders      = "folders.html"
 
- 	templateFolder       = "folder.html"
 
- 	templateMessage      = "message.html"
 
- 	templateStatus       = "status.html"
 
- 	templateLogin        = "login.html"
 
- 	templateChangePwd    = "changepwd.html"
 
- 	templateMaintenance  = "maintenance.html"
 
- 	templateSetup        = "adminsetup.html"
 
- 	pageUsersTitle       = "Users"
 
- 	pageAdminsTitle      = "Admins"
 
- 	pageConnectionsTitle = "Connections"
 
- 	pageStatusTitle      = "Status"
 
- 	pageFoldersTitle     = "Folders"
 
- 	pageChangePwdTitle   = "Change password"
 
- 	pageMaintenanceTitle = "Maintenance"
 
- 	pageSetupTitle       = "Create first admin user"
 
- 	defaultQueryLimit    = 500
 
- )
 
- var (
 
- 	adminTemplates = make(map[string]*template.Template)
 
- )
 
- type basePage struct {
 
- 	Title              string
 
- 	CurrentURL         string
 
- 	UsersURL           string
 
- 	UserURL            string
 
- 	UserTemplateURL    string
 
- 	AdminsURL          string
 
- 	AdminURL           string
 
- 	QuotaScanURL       string
 
- 	ConnectionsURL     string
 
- 	FoldersURL         string
 
- 	FolderURL          string
 
- 	FolderTemplateURL  string
 
- 	LogoutURL          string
 
- 	ChangeAdminPwdURL  string
 
- 	FolderQuotaScanURL string
 
- 	StatusURL          string
 
- 	MaintenanceURL     string
 
- 	StaticURL          string
 
- 	UsersTitle         string
 
- 	AdminsTitle        string
 
- 	ConnectionsTitle   string
 
- 	FoldersTitle       string
 
- 	StatusTitle        string
 
- 	MaintenanceTitle   string
 
- 	Version            string
 
- 	CSRFToken          string
 
- 	LoggedAdmin        *dataprovider.Admin
 
- }
 
- type usersPage struct {
 
- 	basePage
 
- 	Users []dataprovider.User
 
- }
 
- type adminsPage struct {
 
- 	basePage
 
- 	Admins []dataprovider.Admin
 
- }
 
- type foldersPage struct {
 
- 	basePage
 
- 	Folders []vfs.BaseVirtualFolder
 
- }
 
- type connectionsPage struct {
 
- 	basePage
 
- 	Connections []*common.ConnectionStatus
 
- }
 
- type statusPage struct {
 
- 	basePage
 
- 	Status ServicesStatus
 
- }
 
- type userPage struct {
 
- 	basePage
 
- 	User              *dataprovider.User
 
- 	RootPerms         []string
 
- 	Error             string
 
- 	ValidPerms        []string
 
- 	ValidLoginMethods []string
 
- 	ValidProtocols    []string
 
- 	WebClientOptions  []string
 
- 	RootDirPerms      []string
 
- 	RedactedSecret    string
 
- 	Mode              userPageMode
 
- }
 
- type adminPage struct {
 
- 	basePage
 
- 	Admin *dataprovider.Admin
 
- 	Error string
 
- 	IsAdd bool
 
- }
 
- type changePwdPage struct {
 
- 	basePage
 
- 	Error string
 
- }
 
- type maintenancePage struct {
 
- 	basePage
 
- 	BackupPath  string
 
- 	RestorePath string
 
- 	Error       string
 
- }
 
- type setupPage struct {
 
- 	basePage
 
- 	Username string
 
- 	Error    string
 
- }
 
- type folderPage struct {
 
- 	basePage
 
- 	Folder vfs.BaseVirtualFolder
 
- 	Error  string
 
- 	Mode   folderPageMode
 
- }
 
- type messagePage struct {
 
- 	basePage
 
- 	Error   string
 
- 	Success string
 
- }
 
- type userTemplateFields struct {
 
- 	Username  string
 
- 	Password  string
 
- 	PublicKey string
 
- }
 
- func loadAdminTemplates(templatesPath string) {
 
- 	usersPaths := []string{
 
- 		filepath.Join(templatesPath, templateAdminDir, templateBase),
 
- 		filepath.Join(templatesPath, templateAdminDir, templateUsers),
 
- 	}
 
- 	userPaths := []string{
 
- 		filepath.Join(templatesPath, templateAdminDir, templateBase),
 
- 		filepath.Join(templatesPath, templateAdminDir, templateFsConfig),
 
- 		filepath.Join(templatesPath, templateAdminDir, templateUser),
 
- 	}
 
- 	adminsPaths := []string{
 
- 		filepath.Join(templatesPath, templateAdminDir, templateBase),
 
- 		filepath.Join(templatesPath, templateAdminDir, templateAdmins),
 
- 	}
 
- 	adminPaths := []string{
 
- 		filepath.Join(templatesPath, templateAdminDir, templateBase),
 
- 		filepath.Join(templatesPath, templateAdminDir, templateAdmin),
 
- 	}
 
- 	changePwdPaths := []string{
 
- 		filepath.Join(templatesPath, templateAdminDir, templateBase),
 
- 		filepath.Join(templatesPath, templateAdminDir, templateChangePwd),
 
- 	}
 
- 	connectionsPaths := []string{
 
- 		filepath.Join(templatesPath, templateAdminDir, templateBase),
 
- 		filepath.Join(templatesPath, templateAdminDir, templateConnections),
 
- 	}
 
- 	messagePath := []string{
 
- 		filepath.Join(templatesPath, templateAdminDir, templateBase),
 
- 		filepath.Join(templatesPath, templateAdminDir, templateMessage),
 
- 	}
 
- 	foldersPath := []string{
 
- 		filepath.Join(templatesPath, templateAdminDir, templateBase),
 
- 		filepath.Join(templatesPath, templateAdminDir, templateFolders),
 
- 	}
 
- 	folderPath := []string{
 
- 		filepath.Join(templatesPath, templateAdminDir, templateBase),
 
- 		filepath.Join(templatesPath, templateAdminDir, templateFsConfig),
 
- 		filepath.Join(templatesPath, templateAdminDir, templateFolder),
 
- 	}
 
- 	statusPath := []string{
 
- 		filepath.Join(templatesPath, templateAdminDir, templateBase),
 
- 		filepath.Join(templatesPath, templateAdminDir, templateStatus),
 
- 	}
 
- 	loginPath := []string{
 
- 		filepath.Join(templatesPath, templateAdminDir, templateLogin),
 
- 	}
 
- 	maintenancePath := []string{
 
- 		filepath.Join(templatesPath, templateAdminDir, templateBase),
 
- 		filepath.Join(templatesPath, templateAdminDir, templateMaintenance),
 
- 	}
 
- 	setupPath := []string{
 
- 		filepath.Join(templatesPath, templateAdminDir, templateSetup),
 
- 	}
 
- 	usersTmpl := utils.LoadTemplate(template.ParseFiles(usersPaths...))
 
- 	userTmpl := utils.LoadTemplate(template.ParseFiles(userPaths...))
 
- 	adminsTmpl := utils.LoadTemplate(template.ParseFiles(adminsPaths...))
 
- 	adminTmpl := utils.LoadTemplate(template.ParseFiles(adminPaths...))
 
- 	connectionsTmpl := utils.LoadTemplate(template.ParseFiles(connectionsPaths...))
 
- 	messageTmpl := utils.LoadTemplate(template.ParseFiles(messagePath...))
 
- 	foldersTmpl := utils.LoadTemplate(template.ParseFiles(foldersPath...))
 
- 	folderTmpl := utils.LoadTemplate(template.ParseFiles(folderPath...))
 
- 	statusTmpl := utils.LoadTemplate(template.ParseFiles(statusPath...))
 
- 	loginTmpl := utils.LoadTemplate(template.ParseFiles(loginPath...))
 
- 	changePwdTmpl := utils.LoadTemplate(template.ParseFiles(changePwdPaths...))
 
- 	maintenanceTmpl := utils.LoadTemplate(template.ParseFiles(maintenancePath...))
 
- 	setupTmpl := utils.LoadTemplate(template.ParseFiles(setupPath...))
 
- 	adminTemplates[templateUsers] = usersTmpl
 
- 	adminTemplates[templateUser] = userTmpl
 
- 	adminTemplates[templateAdmins] = adminsTmpl
 
- 	adminTemplates[templateAdmin] = adminTmpl
 
- 	adminTemplates[templateConnections] = connectionsTmpl
 
- 	adminTemplates[templateMessage] = messageTmpl
 
- 	adminTemplates[templateFolders] = foldersTmpl
 
- 	adminTemplates[templateFolder] = folderTmpl
 
- 	adminTemplates[templateStatus] = statusTmpl
 
- 	adminTemplates[templateLogin] = loginTmpl
 
- 	adminTemplates[templateChangePwd] = changePwdTmpl
 
- 	adminTemplates[templateMaintenance] = maintenanceTmpl
 
- 	adminTemplates[templateSetup] = setupTmpl
 
- }
 
- func getBasePageData(title, currentURL string, r *http.Request) basePage {
 
- 	var csrfToken string
 
- 	if currentURL != "" {
 
- 		csrfToken = createCSRFToken()
 
- 	}
 
- 	return basePage{
 
- 		Title:              title,
 
- 		CurrentURL:         currentURL,
 
- 		UsersURL:           webUsersPath,
 
- 		UserURL:            webUserPath,
 
- 		UserTemplateURL:    webTemplateUser,
 
- 		AdminsURL:          webAdminsPath,
 
- 		AdminURL:           webAdminPath,
 
- 		FoldersURL:         webFoldersPath,
 
- 		FolderURL:          webFolderPath,
 
- 		FolderTemplateURL:  webTemplateFolder,
 
- 		LogoutURL:          webLogoutPath,
 
- 		ChangeAdminPwdURL:  webChangeAdminPwdPath,
 
- 		QuotaScanURL:       webQuotaScanPath,
 
- 		ConnectionsURL:     webConnectionsPath,
 
- 		StatusURL:          webStatusPath,
 
- 		FolderQuotaScanURL: webScanVFolderPath,
 
- 		MaintenanceURL:     webMaintenancePath,
 
- 		StaticURL:          webStaticFilesPath,
 
- 		UsersTitle:         pageUsersTitle,
 
- 		AdminsTitle:        pageAdminsTitle,
 
- 		ConnectionsTitle:   pageConnectionsTitle,
 
- 		FoldersTitle:       pageFoldersTitle,
 
- 		StatusTitle:        pageStatusTitle,
 
- 		MaintenanceTitle:   pageMaintenanceTitle,
 
- 		Version:            version.GetAsString(),
 
- 		LoggedAdmin:        getAdminFromToken(r),
 
- 		CSRFToken:          csrfToken,
 
- 	}
 
- }
 
- func renderAdminTemplate(w http.ResponseWriter, tmplName string, data interface{}) {
 
- 	err := adminTemplates[tmplName].ExecuteTemplate(w, tmplName, data)
 
- 	if err != nil {
 
- 		http.Error(w, err.Error(), http.StatusInternalServerError)
 
- 	}
 
- }
 
- func renderMessagePage(w http.ResponseWriter, r *http.Request, title, body string, statusCode int, err error, message string) {
 
- 	var errorString string
 
- 	if body != "" {
 
- 		errorString = body + " "
 
- 	}
 
- 	if err != nil {
 
- 		errorString += err.Error()
 
- 	}
 
- 	data := messagePage{
 
- 		basePage: getBasePageData(title, "", r),
 
- 		Error:    errorString,
 
- 		Success:  message,
 
- 	}
 
- 	w.WriteHeader(statusCode)
 
- 	renderAdminTemplate(w, templateMessage, data)
 
- }
 
- func renderInternalServerErrorPage(w http.ResponseWriter, r *http.Request, err error) {
 
- 	renderMessagePage(w, r, page500Title, page500Body, http.StatusInternalServerError, err, "")
 
- }
 
- func renderBadRequestPage(w http.ResponseWriter, r *http.Request, err error) {
 
- 	renderMessagePage(w, r, page400Title, "", http.StatusBadRequest, err, "")
 
- }
 
- func renderForbiddenPage(w http.ResponseWriter, r *http.Request, body string) {
 
- 	renderMessagePage(w, r, page403Title, "", http.StatusForbidden, nil, body)
 
- }
 
- func renderNotFoundPage(w http.ResponseWriter, r *http.Request, err error) {
 
- 	renderMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "")
 
- }
 
- func renderChangePwdPage(w http.ResponseWriter, r *http.Request, error string) {
 
- 	data := changePwdPage{
 
- 		basePage: getBasePageData(pageChangePwdTitle, webChangeAdminPwdPath, r),
 
- 		Error:    error,
 
- 	}
 
- 	renderAdminTemplate(w, templateChangePwd, data)
 
- }
 
- func renderMaintenancePage(w http.ResponseWriter, r *http.Request, error string) {
 
- 	data := maintenancePage{
 
- 		basePage:    getBasePageData(pageMaintenanceTitle, webMaintenancePath, r),
 
- 		BackupPath:  webBackupPath,
 
- 		RestorePath: webRestorePath,
 
- 		Error:       error,
 
- 	}
 
- 	renderAdminTemplate(w, templateMaintenance, data)
 
- }
 
- func renderAdminSetupPage(w http.ResponseWriter, r *http.Request, username, error string) {
 
- 	data := setupPage{
 
- 		basePage: getBasePageData(pageSetupTitle, webAdminSetupPath, r),
 
- 		Username: username,
 
- 		Error:    error,
 
- 	}
 
- 	renderAdminTemplate(w, templateSetup, data)
 
- }
 
- func renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Request, admin *dataprovider.Admin,
 
- 	error string, isAdd bool) {
 
- 	currentURL := webAdminPath
 
- 	if !isAdd {
 
- 		currentURL = fmt.Sprintf("%v/%v", webAdminPath, url.PathEscape(admin.Username))
 
- 	}
 
- 	data := adminPage{
 
- 		basePage: getBasePageData("Add a new user", currentURL, r),
 
- 		Admin:    admin,
 
- 		Error:    error,
 
- 		IsAdd:    isAdd,
 
- 	}
 
- 	renderAdminTemplate(w, templateAdmin, data)
 
- }
 
- func renderUserPage(w http.ResponseWriter, r *http.Request, user *dataprovider.User, mode userPageMode, error string) {
 
- 	user.SetEmptySecretsIfNil()
 
- 	var title, currentURL string
 
- 	switch mode {
 
- 	case userPageModeAdd:
 
- 		title = "Add a new user"
 
- 		currentURL = webUserPath
 
- 	case userPageModeUpdate:
 
- 		title = "Update user"
 
- 		currentURL = fmt.Sprintf("%v/%v", webUserPath, url.PathEscape(user.Username))
 
- 	case userPageModeTemplate:
 
- 		title = "User template"
 
- 		currentURL = webTemplateUser
 
- 	}
 
- 	if user.Password != "" && user.IsPasswordHashed() && mode == userPageModeUpdate {
 
- 		user.Password = redactedSecret
 
- 	}
 
- 	user.FsConfig.RedactedSecret = redactedSecret
 
- 	data := userPage{
 
- 		basePage:          getBasePageData(title, currentURL, r),
 
- 		Mode:              mode,
 
- 		Error:             error,
 
- 		User:              user,
 
- 		ValidPerms:        dataprovider.ValidPerms,
 
- 		ValidLoginMethods: dataprovider.ValidLoginMethods,
 
- 		ValidProtocols:    dataprovider.ValidProtocols,
 
- 		WebClientOptions:  dataprovider.WebClientOptions,
 
- 		RootDirPerms:      user.GetPermissionsForPath("/"),
 
- 	}
 
- 	renderAdminTemplate(w, templateUser, data)
 
- }
 
- func renderFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.BaseVirtualFolder, mode folderPageMode, error string) {
 
- 	var title, currentURL string
 
- 	switch mode {
 
- 	case folderPageModeAdd:
 
- 		title = "Add a new folder"
 
- 		currentURL = webFolderPath
 
- 	case folderPageModeUpdate:
 
- 		title = "Update folder"
 
- 		currentURL = fmt.Sprintf("%v/%v", webFolderPath, url.PathEscape(folder.Name))
 
- 	case folderPageModeTemplate:
 
- 		title = "Folder template"
 
- 		currentURL = webTemplateFolder
 
- 	}
 
- 	folder.FsConfig.RedactedSecret = redactedSecret
 
- 	folder.FsConfig.SetEmptySecretsIfNil()
 
- 	data := folderPage{
 
- 		basePage: getBasePageData(title, currentURL, r),
 
- 		Error:    error,
 
- 		Folder:   folder,
 
- 		Mode:     mode,
 
- 	}
 
- 	renderAdminTemplate(w, templateFolder, data)
 
- }
 
- func getFoldersForTemplate(r *http.Request) []string {
 
- 	var res []string
 
- 	formValue := r.Form.Get("folders")
 
- 	folders := make(map[string]bool)
 
- 	for _, name := range getSliceFromDelimitedValues(formValue, "\n") {
 
- 		if _, ok := folders[name]; ok {
 
- 			continue
 
- 		}
 
- 		folders[name] = true
 
- 		res = append(res, name)
 
- 	}
 
- 	return res
 
- }
 
- func getUsersForTemplate(r *http.Request) []userTemplateFields {
 
- 	var res []userTemplateFields
 
- 	formValue := r.Form.Get("users")
 
- 	users := make(map[string]bool)
 
- 	for _, cleaned := range getSliceFromDelimitedValues(formValue, "\n") {
 
- 		if strings.Contains(cleaned, "::") {
 
- 			mapping := strings.Split(cleaned, "::")
 
- 			if len(mapping) > 1 {
 
- 				username := strings.TrimSpace(mapping[0])
 
- 				password := strings.TrimSpace(mapping[1])
 
- 				var publicKey string
 
- 				if len(mapping) > 2 {
 
- 					publicKey = strings.TrimSpace(mapping[2])
 
- 				}
 
- 				if username == "" || (password == "" && publicKey == "") {
 
- 					continue
 
- 				}
 
- 				if _, ok := users[username]; ok {
 
- 					continue
 
- 				}
 
- 				users[username] = true
 
- 				res = append(res, userTemplateFields{
 
- 					Username:  username,
 
- 					Password:  password,
 
- 					PublicKey: publicKey,
 
- 				})
 
- 			}
 
- 		}
 
- 	}
 
- 	return res
 
- }
 
- func getVirtualFoldersFromPostFields(r *http.Request) []vfs.VirtualFolder {
 
- 	var virtualFolders []vfs.VirtualFolder
 
- 	formValue := r.Form.Get("virtual_folders")
 
- 	for _, cleaned := range getSliceFromDelimitedValues(formValue, "\n") {
 
- 		if strings.Contains(cleaned, "::") {
 
- 			mapping := strings.Split(cleaned, "::")
 
- 			if len(mapping) > 1 {
 
- 				vfolder := vfs.VirtualFolder{
 
- 					BaseVirtualFolder: vfs.BaseVirtualFolder{
 
- 						Name: strings.TrimSpace(mapping[1]),
 
- 					},
 
- 					VirtualPath: strings.TrimSpace(mapping[0]),
 
- 					QuotaFiles:  -1,
 
- 					QuotaSize:   -1,
 
- 				}
 
- 				if len(mapping) > 2 {
 
- 					quotaFiles, err := strconv.Atoi(strings.TrimSpace(mapping[2]))
 
- 					if err == nil {
 
- 						vfolder.QuotaFiles = quotaFiles
 
- 					}
 
- 				}
 
- 				if len(mapping) > 3 {
 
- 					quotaSize, err := strconv.ParseInt(strings.TrimSpace(mapping[3]), 10, 64)
 
- 					if err == nil {
 
- 						vfolder.QuotaSize = quotaSize
 
- 					}
 
- 				}
 
- 				virtualFolders = append(virtualFolders, vfolder)
 
- 			}
 
- 		}
 
- 	}
 
- 	return virtualFolders
 
- }
 
- func getUserPermissionsFromPostFields(r *http.Request) map[string][]string {
 
- 	permissions := make(map[string][]string)
 
- 	permissions["/"] = r.Form["permissions"]
 
- 	subDirsPermsValue := r.Form.Get("sub_dirs_permissions")
 
- 	for _, cleaned := range getSliceFromDelimitedValues(subDirsPermsValue, "\n") {
 
- 		if strings.Contains(cleaned, "::") {
 
- 			dirPerms := strings.Split(cleaned, "::")
 
- 			if len(dirPerms) > 1 {
 
- 				dir := dirPerms[0]
 
- 				dir = strings.TrimSpace(dir)
 
- 				perms := []string{}
 
- 				for _, p := range strings.Split(dirPerms[1], ",") {
 
- 					cleanedPerm := strings.TrimSpace(p)
 
- 					if cleanedPerm != "" {
 
- 						perms = append(perms, cleanedPerm)
 
- 					}
 
- 				}
 
- 				if dir != "" {
 
- 					permissions[dir] = perms
 
- 				}
 
- 			}
 
- 		}
 
- 	}
 
- 	return permissions
 
- }
 
- func getFilePatternsFromPostField(valueAllowed, valuesDenied string) []dataprovider.PatternsFilter {
 
- 	var result []dataprovider.PatternsFilter
 
- 	allowedPatterns := getListFromPostFields(valueAllowed)
 
- 	deniedPatterns := getListFromPostFields(valuesDenied)
 
- 	for dirAllowed, allowPatterns := range allowedPatterns {
 
- 		filter := dataprovider.PatternsFilter{
 
- 			Path:            dirAllowed,
 
- 			AllowedPatterns: allowPatterns,
 
- 		}
 
- 		for dirDenied, denPatterns := range deniedPatterns {
 
- 			if dirAllowed == dirDenied {
 
- 				filter.DeniedPatterns = denPatterns
 
- 				break
 
- 			}
 
- 		}
 
- 		result = append(result, filter)
 
- 	}
 
- 	for dirDenied, denPatterns := range deniedPatterns {
 
- 		found := false
 
- 		for _, res := range result {
 
- 			if res.Path == dirDenied {
 
- 				found = true
 
- 				break
 
- 			}
 
- 		}
 
- 		if !found {
 
- 			result = append(result, dataprovider.PatternsFilter{
 
- 				Path:           dirDenied,
 
- 				DeniedPatterns: denPatterns,
 
- 			})
 
- 		}
 
- 	}
 
- 	return result
 
- }
 
- func getFiltersFromUserPostFields(r *http.Request) dataprovider.UserFilters {
 
- 	var filters dataprovider.UserFilters
 
- 	filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
 
- 	filters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get("denied_ip"), ",")
 
- 	filters.DeniedLoginMethods = r.Form["ssh_login_methods"]
 
- 	filters.DeniedProtocols = r.Form["denied_protocols"]
 
- 	filters.FilePatterns = getFilePatternsFromPostField(r.Form.Get("allowed_patterns"), r.Form.Get("denied_patterns"))
 
- 	filters.TLSUsername = dataprovider.TLSUsername(r.Form.Get("tls_username"))
 
- 	filters.WebClient = r.Form["web_client_options"]
 
- 	hooks := r.Form["hooks"]
 
- 	if utils.IsStringInSlice("external_auth_disabled", hooks) {
 
- 		filters.Hooks.ExternalAuthDisabled = true
 
- 	}
 
- 	if utils.IsStringInSlice("pre_login_disabled", hooks) {
 
- 		filters.Hooks.PreLoginDisabled = true
 
- 	}
 
- 	if utils.IsStringInSlice("check_password_disabled", hooks) {
 
- 		filters.Hooks.CheckPasswordDisabled = true
 
- 	}
 
- 	filters.DisableFsChecks = len(r.Form.Get("disable_fs_checks")) > 0
 
- 	return filters
 
- }
 
- func getSecretFromFormField(r *http.Request, field string) *kms.Secret {
 
- 	secret := kms.NewPlainSecret(r.Form.Get(field))
 
- 	if strings.TrimSpace(secret.GetPayload()) == redactedSecret {
 
- 		secret.SetStatus(kms.SecretStatusRedacted)
 
- 	}
 
- 	if strings.TrimSpace(secret.GetPayload()) == "" {
 
- 		secret.SetStatus("")
 
- 	}
 
- 	return secret
 
- }
 
- func getS3Config(r *http.Request) (vfs.S3FsConfig, error) {
 
- 	var err error
 
- 	config := vfs.S3FsConfig{}
 
- 	config.Bucket = r.Form.Get("s3_bucket")
 
- 	config.Region = r.Form.Get("s3_region")
 
- 	config.AccessKey = r.Form.Get("s3_access_key")
 
- 	config.AccessSecret = getSecretFromFormField(r, "s3_access_secret")
 
- 	config.Endpoint = r.Form.Get("s3_endpoint")
 
- 	config.StorageClass = r.Form.Get("s3_storage_class")
 
- 	config.KeyPrefix = r.Form.Get("s3_key_prefix")
 
- 	config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("s3_upload_part_size"), 10, 64)
 
- 	if err != nil {
 
- 		return config, err
 
- 	}
 
- 	config.UploadConcurrency, err = strconv.Atoi(r.Form.Get("s3_upload_concurrency"))
 
- 	return config, err
 
- }
 
- func getGCSConfig(r *http.Request) (vfs.GCSFsConfig, error) {
 
- 	var err error
 
- 	config := vfs.GCSFsConfig{}
 
- 	config.Bucket = r.Form.Get("gcs_bucket")
 
- 	config.StorageClass = r.Form.Get("gcs_storage_class")
 
- 	config.KeyPrefix = r.Form.Get("gcs_key_prefix")
 
- 	autoCredentials := r.Form.Get("gcs_auto_credentials")
 
- 	if autoCredentials != "" {
 
- 		config.AutomaticCredentials = 1
 
- 	} else {
 
- 		config.AutomaticCredentials = 0
 
- 	}
 
- 	credentials, _, err := r.FormFile("gcs_credential_file")
 
- 	if err == http.ErrMissingFile {
 
- 		return config, nil
 
- 	}
 
- 	if err != nil {
 
- 		return config, err
 
- 	}
 
- 	defer credentials.Close()
 
- 	fileBytes, err := io.ReadAll(credentials)
 
- 	if err != nil || len(fileBytes) == 0 {
 
- 		if len(fileBytes) == 0 {
 
- 			err = errors.New("credentials file size must be greater than 0")
 
- 		}
 
- 		return config, err
 
- 	}
 
- 	config.Credentials = kms.NewPlainSecret(string(fileBytes))
 
- 	config.AutomaticCredentials = 0
 
- 	return config, err
 
- }
 
- func getSFTPConfig(r *http.Request) (vfs.SFTPFsConfig, error) {
 
- 	var err error
 
- 	config := vfs.SFTPFsConfig{}
 
- 	config.Endpoint = r.Form.Get("sftp_endpoint")
 
- 	config.Username = r.Form.Get("sftp_username")
 
- 	config.Password = getSecretFromFormField(r, "sftp_password")
 
- 	config.PrivateKey = getSecretFromFormField(r, "sftp_private_key")
 
- 	fingerprintsFormValue := r.Form.Get("sftp_fingerprints")
 
- 	config.Fingerprints = getSliceFromDelimitedValues(fingerprintsFormValue, "\n")
 
- 	config.Prefix = r.Form.Get("sftp_prefix")
 
- 	config.DisableCouncurrentReads = len(r.Form.Get("sftp_disable_concurrent_reads")) > 0
 
- 	config.BufferSize, err = strconv.ParseInt(r.Form.Get("sftp_buffer_size"), 10, 64)
 
- 	return config, err
 
- }
 
- func getAzureConfig(r *http.Request) (vfs.AzBlobFsConfig, error) {
 
- 	var err error
 
- 	config := vfs.AzBlobFsConfig{}
 
- 	config.Container = r.Form.Get("az_container")
 
- 	config.AccountName = r.Form.Get("az_account_name")
 
- 	config.AccountKey = getSecretFromFormField(r, "az_account_key")
 
- 	config.SASURL = r.Form.Get("az_sas_url")
 
- 	config.Endpoint = r.Form.Get("az_endpoint")
 
- 	config.KeyPrefix = r.Form.Get("az_key_prefix")
 
- 	config.AccessTier = r.Form.Get("az_access_tier")
 
- 	config.UseEmulator = len(r.Form.Get("az_use_emulator")) > 0
 
- 	config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("az_upload_part_size"), 10, 64)
 
- 	if err != nil {
 
- 		return config, err
 
- 	}
 
- 	config.UploadConcurrency, err = strconv.Atoi(r.Form.Get("az_upload_concurrency"))
 
- 	return config, err
 
- }
 
- func getFsConfigFromPostFields(r *http.Request) (vfs.Filesystem, error) {
 
- 	var fs vfs.Filesystem
 
- 	provider, err := strconv.Atoi(r.Form.Get("fs_provider"))
 
- 	if err != nil {
 
- 		provider = int(vfs.LocalFilesystemProvider)
 
- 	}
 
- 	fs.Provider = vfs.FilesystemProvider(provider)
 
- 	switch fs.Provider {
 
- 	case vfs.S3FilesystemProvider:
 
- 		config, err := getS3Config(r)
 
- 		if err != nil {
 
- 			return fs, err
 
- 		}
 
- 		fs.S3Config = config
 
- 	case vfs.AzureBlobFilesystemProvider:
 
- 		config, err := getAzureConfig(r)
 
- 		if err != nil {
 
- 			return fs, err
 
- 		}
 
- 		fs.AzBlobConfig = config
 
- 	case vfs.GCSFilesystemProvider:
 
- 		config, err := getGCSConfig(r)
 
- 		if err != nil {
 
- 			return fs, err
 
- 		}
 
- 		fs.GCSConfig = config
 
- 	case vfs.CryptedFilesystemProvider:
 
- 		fs.CryptConfig.Passphrase = getSecretFromFormField(r, "crypt_passphrase")
 
- 	case vfs.SFTPFilesystemProvider:
 
- 		config, err := getSFTPConfig(r)
 
- 		if err != nil {
 
- 			return fs, err
 
- 		}
 
- 		fs.SFTPConfig = config
 
- 	}
 
- 	return fs, nil
 
- }
 
- func getAdminFromPostFields(r *http.Request) (dataprovider.Admin, error) {
 
- 	var admin dataprovider.Admin
 
- 	err := r.ParseForm()
 
- 	if err != nil {
 
- 		return admin, err
 
- 	}
 
- 	status, err := strconv.Atoi(r.Form.Get("status"))
 
- 	if err != nil {
 
- 		return admin, err
 
- 	}
 
- 	admin.Username = r.Form.Get("username")
 
- 	admin.Password = r.Form.Get("password")
 
- 	admin.Permissions = r.Form["permissions"]
 
- 	admin.Email = r.Form.Get("email")
 
- 	admin.Status = status
 
- 	admin.Filters.AllowList = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
 
- 	admin.AdditionalInfo = r.Form.Get("additional_info")
 
- 	admin.Description = r.Form.Get("description")
 
- 	return admin, nil
 
- }
 
- func replacePlaceholders(field string, replacements map[string]string) string {
 
- 	for k, v := range replacements {
 
- 		field = strings.ReplaceAll(field, k, v)
 
- 	}
 
- 	return field
 
- }
 
- func getFolderFromTemplate(folder vfs.BaseVirtualFolder, name string) vfs.BaseVirtualFolder {
 
- 	folder.Name = name
 
- 	replacements := make(map[string]string)
 
- 	replacements["%name%"] = folder.Name
 
- 	folder.MappedPath = replacePlaceholders(folder.MappedPath, replacements)
 
- 	folder.Description = replacePlaceholders(folder.Description, replacements)
 
- 	switch folder.FsConfig.Provider {
 
- 	case vfs.CryptedFilesystemProvider:
 
- 		folder.FsConfig.CryptConfig = getCryptFsFromTemplate(folder.FsConfig.CryptConfig, replacements)
 
- 	case vfs.S3FilesystemProvider:
 
- 		folder.FsConfig.S3Config = getS3FsFromTemplate(folder.FsConfig.S3Config, replacements)
 
- 	case vfs.GCSFilesystemProvider:
 
- 		folder.FsConfig.GCSConfig = getGCSFsFromTemplate(folder.FsConfig.GCSConfig, replacements)
 
- 	case vfs.AzureBlobFilesystemProvider:
 
- 		folder.FsConfig.AzBlobConfig = getAzBlobFsFromTemplate(folder.FsConfig.AzBlobConfig, replacements)
 
- 	case vfs.SFTPFilesystemProvider:
 
- 		folder.FsConfig.SFTPConfig = getSFTPFsFromTemplate(folder.FsConfig.SFTPConfig, replacements)
 
- 	}
 
- 	return folder
 
- }
 
- func getCryptFsFromTemplate(fsConfig vfs.CryptFsConfig, replacements map[string]string) vfs.CryptFsConfig {
 
- 	if fsConfig.Passphrase != nil {
 
- 		if fsConfig.Passphrase.IsPlain() {
 
- 			payload := replacePlaceholders(fsConfig.Passphrase.GetPayload(), replacements)
 
- 			fsConfig.Passphrase = kms.NewPlainSecret(payload)
 
- 		}
 
- 	}
 
- 	return fsConfig
 
- }
 
- func getS3FsFromTemplate(fsConfig vfs.S3FsConfig, replacements map[string]string) vfs.S3FsConfig {
 
- 	fsConfig.KeyPrefix = replacePlaceholders(fsConfig.KeyPrefix, replacements)
 
- 	fsConfig.AccessKey = replacePlaceholders(fsConfig.AccessKey, replacements)
 
- 	if fsConfig.AccessSecret != nil && fsConfig.AccessSecret.IsPlain() {
 
- 		payload := replacePlaceholders(fsConfig.AccessSecret.GetPayload(), replacements)
 
- 		fsConfig.AccessSecret = kms.NewPlainSecret(payload)
 
- 	}
 
- 	return fsConfig
 
- }
 
- func getGCSFsFromTemplate(fsConfig vfs.GCSFsConfig, replacements map[string]string) vfs.GCSFsConfig {
 
- 	fsConfig.KeyPrefix = replacePlaceholders(fsConfig.KeyPrefix, replacements)
 
- 	return fsConfig
 
- }
 
- func getAzBlobFsFromTemplate(fsConfig vfs.AzBlobFsConfig, replacements map[string]string) vfs.AzBlobFsConfig {
 
- 	fsConfig.KeyPrefix = replacePlaceholders(fsConfig.KeyPrefix, replacements)
 
- 	fsConfig.AccountName = replacePlaceholders(fsConfig.AccountName, replacements)
 
- 	if fsConfig.AccountKey != nil && fsConfig.AccountKey.IsPlain() {
 
- 		payload := replacePlaceholders(fsConfig.AccountKey.GetPayload(), replacements)
 
- 		fsConfig.AccountKey = kms.NewPlainSecret(payload)
 
- 	}
 
- 	return fsConfig
 
- }
 
- func getSFTPFsFromTemplate(fsConfig vfs.SFTPFsConfig, replacements map[string]string) vfs.SFTPFsConfig {
 
- 	fsConfig.Prefix = replacePlaceholders(fsConfig.Prefix, replacements)
 
- 	fsConfig.Username = replacePlaceholders(fsConfig.Username, replacements)
 
- 	if fsConfig.Password != nil && fsConfig.Password.IsPlain() {
 
- 		payload := replacePlaceholders(fsConfig.Password.GetPayload(), replacements)
 
- 		fsConfig.Password = kms.NewPlainSecret(payload)
 
- 	}
 
- 	return fsConfig
 
- }
 
- func getUserFromTemplate(user dataprovider.User, template userTemplateFields) dataprovider.User {
 
- 	user.Username = template.Username
 
- 	user.Password = template.Password
 
- 	user.PublicKeys = nil
 
- 	if template.PublicKey != "" {
 
- 		user.PublicKeys = append(user.PublicKeys, template.PublicKey)
 
- 	}
 
- 	replacements := make(map[string]string)
 
- 	replacements["%username%"] = user.Username
 
- 	user.Password = replacePlaceholders(user.Password, replacements)
 
- 	replacements["%password%"] = user.Password
 
- 	user.HomeDir = replacePlaceholders(user.HomeDir, replacements)
 
- 	var vfolders []vfs.VirtualFolder
 
- 	for _, vfolder := range user.VirtualFolders {
 
- 		vfolder.Name = replacePlaceholders(vfolder.Name, replacements)
 
- 		vfolder.VirtualPath = replacePlaceholders(vfolder.VirtualPath, replacements)
 
- 		vfolders = append(vfolders, vfolder)
 
- 	}
 
- 	user.VirtualFolders = vfolders
 
- 	user.Description = replacePlaceholders(user.Description, replacements)
 
- 	user.AdditionalInfo = replacePlaceholders(user.AdditionalInfo, replacements)
 
- 	switch user.FsConfig.Provider {
 
- 	case vfs.CryptedFilesystemProvider:
 
- 		user.FsConfig.CryptConfig = getCryptFsFromTemplate(user.FsConfig.CryptConfig, replacements)
 
- 	case vfs.S3FilesystemProvider:
 
- 		user.FsConfig.S3Config = getS3FsFromTemplate(user.FsConfig.S3Config, replacements)
 
- 	case vfs.GCSFilesystemProvider:
 
- 		user.FsConfig.GCSConfig = getGCSFsFromTemplate(user.FsConfig.GCSConfig, replacements)
 
- 	case vfs.AzureBlobFilesystemProvider:
 
- 		user.FsConfig.AzBlobConfig = getAzBlobFsFromTemplate(user.FsConfig.AzBlobConfig, replacements)
 
- 	case vfs.SFTPFilesystemProvider:
 
- 		user.FsConfig.SFTPConfig = getSFTPFsFromTemplate(user.FsConfig.SFTPConfig, replacements)
 
- 	}
 
- 	return user
 
- }
 
- func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
 
- 	var user dataprovider.User
 
- 	err := r.ParseMultipartForm(maxRequestSize)
 
- 	if err != nil {
 
- 		return user, err
 
- 	}
 
- 	publicKeysFormValue := r.Form.Get("public_keys")
 
- 	publicKeys := getSliceFromDelimitedValues(publicKeysFormValue, "\n")
 
- 	uid, err := strconv.Atoi(r.Form.Get("uid"))
 
- 	if err != nil {
 
- 		return user, err
 
- 	}
 
- 	gid, err := strconv.Atoi(r.Form.Get("gid"))
 
- 	if err != nil {
 
- 		return user, err
 
- 	}
 
- 	maxSessions, err := strconv.Atoi(r.Form.Get("max_sessions"))
 
- 	if err != nil {
 
- 		return user, err
 
- 	}
 
- 	quotaSize, err := strconv.ParseInt(r.Form.Get("quota_size"), 10, 64)
 
- 	if err != nil {
 
- 		return user, err
 
- 	}
 
- 	quotaFiles, err := strconv.Atoi(r.Form.Get("quota_files"))
 
- 	if err != nil {
 
- 		return user, err
 
- 	}
 
- 	bandwidthUL, err := strconv.ParseInt(r.Form.Get("upload_bandwidth"), 10, 64)
 
- 	if err != nil {
 
- 		return user, err
 
- 	}
 
- 	bandwidthDL, err := strconv.ParseInt(r.Form.Get("download_bandwidth"), 10, 64)
 
- 	if err != nil {
 
- 		return user, err
 
- 	}
 
- 	status, err := strconv.Atoi(r.Form.Get("status"))
 
- 	if err != nil {
 
- 		return user, err
 
- 	}
 
- 	expirationDateMillis := int64(0)
 
- 	expirationDateString := r.Form.Get("expiration_date")
 
- 	if len(strings.TrimSpace(expirationDateString)) > 0 {
 
- 		expirationDate, err := time.Parse(webDateTimeFormat, expirationDateString)
 
- 		if err != nil {
 
- 			return user, err
 
- 		}
 
- 		expirationDateMillis = utils.GetTimeAsMsSinceEpoch(expirationDate)
 
- 	}
 
- 	fsConfig, err := getFsConfigFromPostFields(r)
 
- 	if err != nil {
 
- 		return user, err
 
- 	}
 
- 	user = dataprovider.User{
 
- 		Username:          r.Form.Get("username"),
 
- 		Password:          r.Form.Get("password"),
 
- 		PublicKeys:        publicKeys,
 
- 		HomeDir:           r.Form.Get("home_dir"),
 
- 		VirtualFolders:    getVirtualFoldersFromPostFields(r),
 
- 		UID:               uid,
 
- 		GID:               gid,
 
- 		Permissions:       getUserPermissionsFromPostFields(r),
 
- 		MaxSessions:       maxSessions,
 
- 		QuotaSize:         quotaSize,
 
- 		QuotaFiles:        quotaFiles,
 
- 		UploadBandwidth:   bandwidthUL,
 
- 		DownloadBandwidth: bandwidthDL,
 
- 		Status:            status,
 
- 		ExpirationDate:    expirationDateMillis,
 
- 		Filters:           getFiltersFromUserPostFields(r),
 
- 		FsConfig:          fsConfig,
 
- 		AdditionalInfo:    r.Form.Get("additional_info"),
 
- 		Description:       r.Form.Get("description"),
 
- 	}
 
- 	maxFileSize, err := strconv.ParseInt(r.Form.Get("max_upload_file_size"), 10, 64)
 
- 	user.Filters.MaxUploadFileSize = maxFileSize
 
- 	return user, err
 
- }
 
- func renderLoginPage(w http.ResponseWriter, error string) {
 
- 	data := loginPage{
 
- 		CurrentURL: webLoginPath,
 
- 		Version:    version.Get().Version,
 
- 		Error:      error,
 
- 		CSRFToken:  createCSRFToken(),
 
- 		StaticURL:  webStaticFilesPath,
 
- 	}
 
- 	renderAdminTemplate(w, templateLogin, data)
 
- }
 
- func handleWebAdminChangePwd(w http.ResponseWriter, r *http.Request) {
 
- 	renderChangePwdPage(w, r, "")
 
- }
 
- func handleWebAdminChangePwdPost(w http.ResponseWriter, r *http.Request) {
 
- 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 
- 	err := r.ParseForm()
 
- 	if err != nil {
 
- 		renderChangePwdPage(w, r, err.Error())
 
- 		return
 
- 	}
 
- 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 
- 		renderForbiddenPage(w, r, err.Error())
 
- 		return
 
- 	}
 
- 	err = doChangeAdminPassword(r, r.Form.Get("current_password"), r.Form.Get("new_password1"),
 
- 		r.Form.Get("new_password2"))
 
- 	if err != nil {
 
- 		renderChangePwdPage(w, r, err.Error())
 
- 		return
 
- 	}
 
- 	handleWebLogout(w, r)
 
- }
 
- func handleWebLogout(w http.ResponseWriter, r *http.Request) {
 
- 	c := jwtTokenClaims{}
 
- 	c.removeCookie(w, r, webBaseAdminPath)
 
- 	http.Redirect(w, r, webLoginPath, http.StatusFound)
 
- }
 
- func handleWebLogin(w http.ResponseWriter, r *http.Request) {
 
- 	if !dataprovider.HasAdmin() {
 
- 		http.Redirect(w, r, webAdminSetupPath, http.StatusFound)
 
- 		return
 
- 	}
 
- 	renderLoginPage(w, "")
 
- }
 
- func handleWebMaintenance(w http.ResponseWriter, r *http.Request) {
 
- 	renderMaintenancePage(w, r, "")
 
- }
 
- func handleWebRestore(w http.ResponseWriter, r *http.Request) {
 
- 	err := r.ParseMultipartForm(MaxRestoreSize)
 
- 	if err != nil {
 
- 		renderMaintenancePage(w, r, err.Error())
 
- 		return
 
- 	}
 
- 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 
- 		renderForbiddenPage(w, r, err.Error())
 
- 		return
 
- 	}
 
- 	restoreMode, err := strconv.Atoi(r.Form.Get("mode"))
 
- 	if err != nil {
 
- 		renderMaintenancePage(w, r, err.Error())
 
- 		return
 
- 	}
 
- 	scanQuota, err := strconv.Atoi(r.Form.Get("quota"))
 
- 	if err != nil {
 
- 		renderMaintenancePage(w, r, err.Error())
 
- 		return
 
- 	}
 
- 	backupFile, _, err := r.FormFile("backup_file")
 
- 	if err != nil {
 
- 		renderMaintenancePage(w, r, err.Error())
 
- 		return
 
- 	}
 
- 	defer backupFile.Close()
 
- 	backupContent, err := io.ReadAll(backupFile)
 
- 	if err != nil || len(backupContent) == 0 {
 
- 		if len(backupContent) == 0 {
 
- 			err = errors.New("backup file size must be greater than 0")
 
- 		}
 
- 		renderMaintenancePage(w, r, err.Error())
 
- 		return
 
- 	}
 
- 	if err := restoreBackup(backupContent, "", scanQuota, restoreMode); err != nil {
 
- 		renderMaintenancePage(w, r, err.Error())
 
- 		return
 
- 	}
 
- 	renderMessagePage(w, r, "Data restored", "", http.StatusOK, nil, "Your backup was successfully restored")
 
- }
 
- func handleGetWebAdmins(w http.ResponseWriter, r *http.Request) {
 
- 	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
 
- 		}
 
- 	}
 
- 	admins := make([]dataprovider.Admin, 0, limit)
 
- 	for {
 
- 		a, err := dataprovider.GetAdmins(limit, len(admins), dataprovider.OrderASC)
 
- 		if err != nil {
 
- 			renderInternalServerErrorPage(w, r, err)
 
- 			return
 
- 		}
 
- 		admins = append(admins, a...)
 
- 		if len(a) < limit {
 
- 			break
 
- 		}
 
- 	}
 
- 	data := adminsPage{
 
- 		basePage: getBasePageData(pageAdminsTitle, webAdminsPath, r),
 
- 		Admins:   admins,
 
- 	}
 
- 	renderAdminTemplate(w, templateAdmins, data)
 
- }
 
- func handleWebAdminSetupGet(w http.ResponseWriter, r *http.Request) {
 
- 	if dataprovider.HasAdmin() {
 
- 		http.Redirect(w, r, webLoginPath, http.StatusFound)
 
- 		return
 
- 	}
 
- 	renderAdminSetupPage(w, r, "", "")
 
- }
 
- func handleWebAddAdminGet(w http.ResponseWriter, r *http.Request) {
 
- 	admin := &dataprovider.Admin{Status: 1}
 
- 	renderAddUpdateAdminPage(w, r, admin, "", true)
 
- }
 
- func handleWebUpdateAdminGet(w http.ResponseWriter, r *http.Request) {
 
- 	username := getURLParam(r, "username")
 
- 	admin, err := dataprovider.AdminExists(username)
 
- 	if err == nil {
 
- 		renderAddUpdateAdminPage(w, r, &admin, "", false)
 
- 	} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
 
- 		renderNotFoundPage(w, r, err)
 
- 	} else {
 
- 		renderInternalServerErrorPage(w, r, err)
 
- 	}
 
- }
 
- func handleWebAddAdminPost(w http.ResponseWriter, r *http.Request) {
 
- 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 
- 	admin, err := getAdminFromPostFields(r)
 
- 	if err != nil {
 
- 		renderAddUpdateAdminPage(w, r, &admin, err.Error(), true)
 
- 		return
 
- 	}
 
- 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 
- 		renderForbiddenPage(w, r, err.Error())
 
- 		return
 
- 	}
 
- 	err = dataprovider.AddAdmin(&admin)
 
- 	if err != nil {
 
- 		renderAddUpdateAdminPage(w, r, &admin, err.Error(), true)
 
- 		return
 
- 	}
 
- 	http.Redirect(w, r, webAdminsPath, http.StatusSeeOther)
 
- }
 
- func handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Request) {
 
- 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 
- 	username := getURLParam(r, "username")
 
- 	admin, err := dataprovider.AdminExists(username)
 
- 	if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
 
- 		renderNotFoundPage(w, r, err)
 
- 		return
 
- 	} else if err != nil {
 
- 		renderInternalServerErrorPage(w, r, err)
 
- 		return
 
- 	}
 
- 	updatedAdmin, err := getAdminFromPostFields(r)
 
- 	if err != nil {
 
- 		renderAddUpdateAdminPage(w, r, &updatedAdmin, err.Error(), false)
 
- 		return
 
- 	}
 
- 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 
- 		renderForbiddenPage(w, r, err.Error())
 
- 		return
 
- 	}
 
- 	updatedAdmin.ID = admin.ID
 
- 	updatedAdmin.Username = admin.Username
 
- 	if updatedAdmin.Password == "" {
 
- 		updatedAdmin.Password = admin.Password
 
- 	}
 
- 	claims, err := getTokenClaims(r)
 
- 	if err != nil || claims.Username == "" {
 
- 		renderAddUpdateAdminPage(w, r, &updatedAdmin, fmt.Sprintf("Invalid token claims: %v", err), false)
 
- 		return
 
- 	}
 
- 	if username == claims.Username {
 
- 		if claims.isCriticalPermRemoved(updatedAdmin.Permissions) {
 
- 			renderAddUpdateAdminPage(w, r, &updatedAdmin, "You cannot remove these permissions to yourself", false)
 
- 			return
 
- 		}
 
- 		if updatedAdmin.Status == 0 {
 
- 			renderAddUpdateAdminPage(w, r, &updatedAdmin, "You cannot disable yourself", false)
 
- 			return
 
- 		}
 
- 	}
 
- 	err = dataprovider.UpdateAdmin(&updatedAdmin)
 
- 	if err != nil {
 
- 		renderAddUpdateAdminPage(w, r, &admin, err.Error(), false)
 
- 		return
 
- 	}
 
- 	http.Redirect(w, r, webAdminsPath, http.StatusSeeOther)
 
- }
 
- func handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
 
- 	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
 
- 		}
 
- 	}
 
- 	users := make([]dataprovider.User, 0, limit)
 
- 	for {
 
- 		u, err := dataprovider.GetUsers(limit, len(users), dataprovider.OrderASC)
 
- 		if err != nil {
 
- 			renderInternalServerErrorPage(w, r, err)
 
- 			return
 
- 		}
 
- 		users = append(users, u...)
 
- 		if len(u) < limit {
 
- 			break
 
- 		}
 
- 	}
 
- 	data := usersPage{
 
- 		basePage: getBasePageData(pageUsersTitle, webUsersPath, r),
 
- 		Users:    users,
 
- 	}
 
- 	renderAdminTemplate(w, templateUsers, data)
 
- }
 
- func handleWebTemplateFolderGet(w http.ResponseWriter, r *http.Request) {
 
- 	if r.URL.Query().Get("from") != "" {
 
- 		name := r.URL.Query().Get("from")
 
- 		folder, err := dataprovider.GetFolderByName(name)
 
- 		if err == nil {
 
- 			renderFolderPage(w, r, folder, folderPageModeTemplate, "")
 
- 		} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
 
- 			renderNotFoundPage(w, r, err)
 
- 		} else {
 
- 			renderInternalServerErrorPage(w, r, err)
 
- 		}
 
- 	} else {
 
- 		folder := vfs.BaseVirtualFolder{}
 
- 		renderFolderPage(w, r, folder, folderPageModeTemplate, "")
 
- 	}
 
- }
 
- func handleWebTemplateFolderPost(w http.ResponseWriter, r *http.Request) {
 
- 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 
- 	templateFolder := vfs.BaseVirtualFolder{}
 
- 	err := r.ParseForm()
 
- 	if err != nil {
 
- 		renderMessagePage(w, r, "Error parsing folders fields", "", http.StatusBadRequest, err, "")
 
- 		return
 
- 	}
 
- 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 
- 		renderForbiddenPage(w, r, err.Error())
 
- 		return
 
- 	}
 
- 	templateFolder.MappedPath = r.Form.Get("mapped_path")
 
- 	templateFolder.Description = r.Form.Get("description")
 
- 	fsConfig, err := getFsConfigFromPostFields(r)
 
- 	if err != nil {
 
- 		renderMessagePage(w, r, "Error parsing folders fields", "", http.StatusBadRequest, err, "")
 
- 		return
 
- 	}
 
- 	templateFolder.FsConfig = fsConfig
 
- 	var dump dataprovider.BackupData
 
- 	dump.Version = dataprovider.DumpVersion
 
- 	foldersFields := getFoldersForTemplate(r)
 
- 	for _, tmpl := range foldersFields {
 
- 		f := getFolderFromTemplate(templateFolder, tmpl)
 
- 		if err := dataprovider.ValidateFolder(&f); err != nil {
 
- 			renderMessagePage(w, r, fmt.Sprintf("Error validating folder %#v", f.Name), "", http.StatusBadRequest, err, "")
 
- 			return
 
- 		}
 
- 		dump.Folders = append(dump.Folders, f)
 
- 	}
 
- 	if len(dump.Folders) == 0 {
 
- 		renderMessagePage(w, r, "No folders to export", "No valid folders found, export is not possible", http.StatusBadRequest, nil, "")
 
- 		return
 
- 	}
 
- 	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"sftpgo-%v-folders-from-template.json\"", len(dump.Folders)))
 
- 	render.JSON(w, r, dump)
 
- }
 
- func handleWebTemplateUserGet(w http.ResponseWriter, r *http.Request) {
 
- 	if r.URL.Query().Get("from") != "" {
 
- 		username := r.URL.Query().Get("from")
 
- 		user, err := dataprovider.UserExists(username)
 
- 		if err == nil {
 
- 			user.SetEmptySecrets()
 
- 			renderUserPage(w, r, &user, userPageModeTemplate, "")
 
- 		} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
 
- 			renderNotFoundPage(w, r, err)
 
- 		} else {
 
- 			renderInternalServerErrorPage(w, r, err)
 
- 		}
 
- 	} else {
 
- 		user := dataprovider.User{Status: 1}
 
- 		renderUserPage(w, r, &user, userPageModeTemplate, "")
 
- 	}
 
- }
 
- func handleWebTemplateUserPost(w http.ResponseWriter, r *http.Request) {
 
- 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 
- 	templateUser, err := getUserFromPostFields(r)
 
- 	if err != nil {
 
- 		renderMessagePage(w, r, "Error parsing user fields", "", http.StatusBadRequest, err, "")
 
- 		return
 
- 	}
 
- 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 
- 		renderForbiddenPage(w, r, err.Error())
 
- 		return
 
- 	}
 
- 	var dump dataprovider.BackupData
 
- 	dump.Version = dataprovider.DumpVersion
 
- 	userTmplFields := getUsersForTemplate(r)
 
- 	for _, tmpl := range userTmplFields {
 
- 		u := getUserFromTemplate(templateUser, tmpl)
 
- 		if err := dataprovider.ValidateUser(&u); err != nil {
 
- 			renderMessagePage(w, r, fmt.Sprintf("Error validating user %#v", u.Username), "", http.StatusBadRequest, err, "")
 
- 			return
 
- 		}
 
- 		dump.Users = append(dump.Users, u)
 
- 		for _, folder := range u.VirtualFolders {
 
- 			if !dump.HasFolder(folder.Name) {
 
- 				dump.Folders = append(dump.Folders, folder.BaseVirtualFolder)
 
- 			}
 
- 		}
 
- 	}
 
- 	if len(dump.Users) == 0 {
 
- 		renderMessagePage(w, r, "No users to export", "No valid users found, export is not possible", http.StatusBadRequest, nil, "")
 
- 		return
 
- 	}
 
- 	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"sftpgo-%v-users-from-template.json\"", len(dump.Users)))
 
- 	render.JSON(w, r, dump)
 
- }
 
- func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
 
- 	if r.URL.Query().Get("clone-from") != "" {
 
- 		username := r.URL.Query().Get("clone-from")
 
- 		user, err := dataprovider.UserExists(username)
 
- 		if err == nil {
 
- 			user.ID = 0
 
- 			user.Username = ""
 
- 			user.Password = ""
 
- 			user.SetEmptySecrets()
 
- 			renderUserPage(w, r, &user, userPageModeAdd, "")
 
- 		} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
 
- 			renderNotFoundPage(w, r, err)
 
- 		} else {
 
- 			renderInternalServerErrorPage(w, r, err)
 
- 		}
 
- 	} else {
 
- 		user := dataprovider.User{Status: 1}
 
- 		renderUserPage(w, r, &user, userPageModeAdd, "")
 
- 	}
 
- }
 
- func handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) {
 
- 	username := getURLParam(r, "username")
 
- 	user, err := dataprovider.UserExists(username)
 
- 	if err == nil {
 
- 		renderUserPage(w, r, &user, userPageModeUpdate, "")
 
- 	} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
 
- 		renderNotFoundPage(w, r, err)
 
- 	} else {
 
- 		renderInternalServerErrorPage(w, r, err)
 
- 	}
 
- }
 
- func handleWebAddUserPost(w http.ResponseWriter, r *http.Request) {
 
- 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 
- 	user, err := getUserFromPostFields(r)
 
- 	if err != nil {
 
- 		renderUserPage(w, r, &user, userPageModeAdd, err.Error())
 
- 		return
 
- 	}
 
- 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 
- 		renderForbiddenPage(w, r, err.Error())
 
- 		return
 
- 	}
 
- 	err = dataprovider.AddUser(&user)
 
- 	if err == nil {
 
- 		http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
 
- 	} else {
 
- 		renderUserPage(w, r, &user, userPageModeAdd, err.Error())
 
- 	}
 
- }
 
- func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
 
- 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 
- 	username := getURLParam(r, "username")
 
- 	user, err := dataprovider.UserExists(username)
 
- 	if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
 
- 		renderNotFoundPage(w, r, err)
 
- 		return
 
- 	} else if err != nil {
 
- 		renderInternalServerErrorPage(w, r, err)
 
- 		return
 
- 	}
 
- 	updatedUser, err := getUserFromPostFields(r)
 
- 	if err != nil {
 
- 		renderUserPage(w, r, &user, userPageModeUpdate, err.Error())
 
- 		return
 
- 	}
 
- 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 
- 		renderForbiddenPage(w, r, err.Error())
 
- 		return
 
- 	}
 
- 	updatedUser.ID = user.ID
 
- 	updatedUser.Username = user.Username
 
- 	updatedUser.SetEmptySecretsIfNil()
 
- 	if updatedUser.Password == redactedSecret {
 
- 		updatedUser.Password = user.Password
 
- 	}
 
- 	updateEncryptedSecrets(&updatedUser.FsConfig, user.FsConfig.S3Config.AccessSecret, user.FsConfig.AzBlobConfig.AccountKey,
 
- 		user.FsConfig.GCSConfig.Credentials, user.FsConfig.CryptConfig.Passphrase, user.FsConfig.SFTPConfig.Password,
 
- 		user.FsConfig.SFTPConfig.PrivateKey)
 
- 	err = dataprovider.UpdateUser(&updatedUser)
 
- 	if err == nil {
 
- 		if len(r.Form.Get("disconnect")) > 0 {
 
- 			disconnectUser(user.Username)
 
- 		}
 
- 		http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
 
- 	} else {
 
- 		renderUserPage(w, r, &user, userPageModeUpdate, err.Error())
 
- 	}
 
- }
 
- func handleWebGetStatus(w http.ResponseWriter, r *http.Request) {
 
- 	data := statusPage{
 
- 		basePage: getBasePageData(pageStatusTitle, webStatusPath, r),
 
- 		Status:   getServicesStatus(),
 
- 	}
 
- 	renderAdminTemplate(w, templateStatus, data)
 
- }
 
- func handleWebGetConnections(w http.ResponseWriter, r *http.Request) {
 
- 	connectionStats := common.Connections.GetStats()
 
- 	data := connectionsPage{
 
- 		basePage:    getBasePageData(pageConnectionsTitle, webConnectionsPath, r),
 
- 		Connections: connectionStats,
 
- 	}
 
- 	renderAdminTemplate(w, templateConnections, data)
 
- }
 
- func handleWebAddFolderGet(w http.ResponseWriter, r *http.Request) {
 
- 	renderFolderPage(w, r, vfs.BaseVirtualFolder{}, folderPageModeAdd, "")
 
- }
 
- func handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) {
 
- 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 
- 	folder := vfs.BaseVirtualFolder{}
 
- 	err := r.ParseMultipartForm(maxRequestSize)
 
- 	if err != nil {
 
- 		renderFolderPage(w, r, folder, folderPageModeAdd, err.Error())
 
- 		return
 
- 	}
 
- 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 
- 		renderForbiddenPage(w, r, err.Error())
 
- 		return
 
- 	}
 
- 	folder.MappedPath = r.Form.Get("mapped_path")
 
- 	folder.Name = r.Form.Get("name")
 
- 	folder.Description = r.Form.Get("description")
 
- 	fsConfig, err := getFsConfigFromPostFields(r)
 
- 	if err != nil {
 
- 		renderFolderPage(w, r, folder, folderPageModeAdd, err.Error())
 
- 		return
 
- 	}
 
- 	folder.FsConfig = fsConfig
 
- 	err = dataprovider.AddFolder(&folder)
 
- 	if err == nil {
 
- 		http.Redirect(w, r, webFoldersPath, http.StatusSeeOther)
 
- 	} else {
 
- 		renderFolderPage(w, r, folder, folderPageModeAdd, err.Error())
 
- 	}
 
- }
 
- func handleWebUpdateFolderGet(w http.ResponseWriter, r *http.Request) {
 
- 	name := getURLParam(r, "name")
 
- 	folder, err := dataprovider.GetFolderByName(name)
 
- 	if err == nil {
 
- 		renderFolderPage(w, r, folder, folderPageModeUpdate, "")
 
- 	} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
 
- 		renderNotFoundPage(w, r, err)
 
- 	} else {
 
- 		renderInternalServerErrorPage(w, r, err)
 
- 	}
 
- }
 
- func handleWebUpdateFolderPost(w http.ResponseWriter, r *http.Request) {
 
- 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 
- 	name := getURLParam(r, "name")
 
- 	folder, err := dataprovider.GetFolderByName(name)
 
- 	if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
 
- 		renderNotFoundPage(w, r, err)
 
- 		return
 
- 	} else if err != nil {
 
- 		renderInternalServerErrorPage(w, r, err)
 
- 		return
 
- 	}
 
- 	err = r.ParseMultipartForm(maxRequestSize)
 
- 	if err != nil {
 
- 		renderFolderPage(w, r, folder, folderPageModeUpdate, err.Error())
 
- 		return
 
- 	}
 
- 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 
- 		renderForbiddenPage(w, r, err.Error())
 
- 		return
 
- 	}
 
- 	fsConfig, err := getFsConfigFromPostFields(r)
 
- 	if err != nil {
 
- 		renderFolderPage(w, r, folder, folderPageModeUpdate, err.Error())
 
- 		return
 
- 	}
 
- 	updatedFolder := &vfs.BaseVirtualFolder{
 
- 		MappedPath:  r.Form.Get("mapped_path"),
 
- 		Description: r.Form.Get("description"),
 
- 	}
 
- 	updatedFolder.ID = folder.ID
 
- 	updatedFolder.Name = folder.Name
 
- 	updatedFolder.FsConfig = fsConfig
 
- 	updatedFolder.FsConfig.SetEmptySecretsIfNil()
 
- 	updateEncryptedSecrets(&updatedFolder.FsConfig, folder.FsConfig.S3Config.AccessSecret, folder.FsConfig.AzBlobConfig.AccountKey,
 
- 		folder.FsConfig.GCSConfig.Credentials, folder.FsConfig.CryptConfig.Passphrase, folder.FsConfig.SFTPConfig.Password,
 
- 		folder.FsConfig.SFTPConfig.PrivateKey)
 
- 	err = dataprovider.UpdateFolder(updatedFolder, folder.Users)
 
- 	if err != nil {
 
- 		renderFolderPage(w, r, folder, folderPageModeUpdate, err.Error())
 
- 		return
 
- 	}
 
- 	http.Redirect(w, r, webFoldersPath, http.StatusSeeOther)
 
- }
 
- func handleWebGetFolders(w http.ResponseWriter, r *http.Request) {
 
- 	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
 
- 		}
 
- 	}
 
- 	folders := make([]vfs.BaseVirtualFolder, 0, limit)
 
- 	for {
 
- 		f, err := dataprovider.GetFolders(limit, len(folders), dataprovider.OrderASC)
 
- 		if err != nil {
 
- 			renderInternalServerErrorPage(w, r, err)
 
- 			return
 
- 		}
 
- 		folders = append(folders, f...)
 
- 		if len(f) < limit {
 
- 			break
 
- 		}
 
- 	}
 
- 	data := foldersPage{
 
- 		basePage: getBasePageData(pageFoldersTitle, webFoldersPath, r),
 
- 		Folders:  folders,
 
- 	}
 
- 	renderAdminTemplate(w, templateFolders, data)
 
- }
 
 
  |