// Copyright (C) 2019 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 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.
//
// 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/>.

package httpd

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"html/template"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/go-chi/render"
	"github.com/sftpgo/sdk"
	sdkkms "github.com/sftpgo/sdk/kms"

	"github.com/drakkan/sftpgo/v2/internal/acme"
	"github.com/drakkan/sftpgo/v2/internal/common"
	"github.com/drakkan/sftpgo/v2/internal/dataprovider"
	"github.com/drakkan/sftpgo/v2/internal/kms"
	"github.com/drakkan/sftpgo/v2/internal/logger"
	"github.com/drakkan/sftpgo/v2/internal/mfa"
	"github.com/drakkan/sftpgo/v2/internal/plugin"
	"github.com/drakkan/sftpgo/v2/internal/smtp"
	"github.com/drakkan/sftpgo/v2/internal/util"
	"github.com/drakkan/sftpgo/v2/internal/vfs"
)

type userPageMode int

const (
	userPageModeAdd userPageMode = iota + 1
	userPageModeUpdate
	userPageModeTemplate
)

type folderPageMode int

const (
	folderPageModeAdd folderPageMode = iota + 1
	folderPageModeUpdate
	folderPageModeTemplate
)

type genericPageMode int

const (
	genericPageModeAdd genericPageMode = iota + 1
	genericPageModeUpdate
)

const (
	templateAdminDir         = "webadmin"
	templateBase             = "base.html"
	templateFsConfig         = "fsconfig.html"
	templateSharedComponents = "sharedcomponents.html"
	templateUsers            = "users.html"
	templateUser             = "user.html"
	templateAdmins           = "admins.html"
	templateAdmin            = "admin.html"
	templateConnections      = "connections.html"
	templateGroups           = "groups.html"
	templateGroup            = "group.html"
	templateFolders          = "folders.html"
	templateFolder           = "folder.html"
	templateEventRules       = "eventrules.html"
	templateEventRule        = "eventrule.html"
	templateEventActions     = "eventactions.html"
	templateEventAction      = "eventaction.html"
	templateRoles            = "roles.html"
	templateRole             = "role.html"
	templateEvents           = "events.html"
	templateStatus           = "status.html"
	templateDefender         = "defender.html"
	templateIPLists          = "iplists.html"
	templateIPList           = "iplist.html"
	templateConfigs          = "configs.html"
	templateProfile          = "profile.html"
	templateMaintenance      = "maintenance.html"
	templateMFA              = "mfa.html"
	templateSetup            = "adminsetup.html"
	defaultQueryLimit        = 1000
	inversePatternType       = "inverse"
)

var (
	adminTemplates = make(map[string]*template.Template)
)

type basePage struct {
	commonBasePage
	Title               string
	CurrentURL          string
	UsersURL            string
	UserURL             string
	UserTemplateURL     string
	AdminsURL           string
	AdminURL            string
	QuotaScanURL        string
	ConnectionsURL      string
	GroupsURL           string
	GroupURL            string
	FoldersURL          string
	FolderURL           string
	FolderTemplateURL   string
	DefenderURL         string
	IPListsURL          string
	IPListURL           string
	EventsURL           string
	ConfigsURL          string
	LogoutURL           string
	LoginURL            string
	ProfileURL          string
	ChangePwdURL        string
	MFAURL              string
	EventRulesURL       string
	EventRuleURL        string
	EventActionsURL     string
	EventActionURL      string
	RolesURL            string
	RoleURL             string
	FolderQuotaScanURL  string
	StatusURL           string
	MaintenanceURL      string
	CSRFToken           string
	IsEventManagerPage  bool
	IsIPManagerPage     bool
	IsServerManagerPage bool
	HasDefender         bool
	HasSearcher         bool
	HasExternalLogin    bool
	LoggedUser          *dataprovider.Admin
	Branding            UIBranding
}

type statusPage struct {
	basePage
	Status *ServicesStatus
}

type fsWrapper struct {
	vfs.Filesystem
	IsUserPage      bool
	IsGroupPage     bool
	IsHidden        bool
	HasUsersBaseDir bool
	DirPath         string
}

type userPage struct {
	basePage
	User               *dataprovider.User
	RootPerms          []string
	Error              *util.I18nError
	ValidPerms         []string
	ValidLoginMethods  []string
	ValidProtocols     []string
	TwoFactorProtocols []string
	WebClientOptions   []string
	RootDirPerms       []string
	Mode               userPageMode
	VirtualFolders     []vfs.BaseVirtualFolder
	Groups             []dataprovider.Group
	Roles              []dataprovider.Role
	CanImpersonate     bool
	FsWrapper          fsWrapper
}

type adminPage struct {
	basePage
	Admin  *dataprovider.Admin
	Groups []dataprovider.Group
	Roles  []dataprovider.Role
	Error  *util.I18nError
	IsAdd  bool
}

type profilePage struct {
	basePage
	Error           *util.I18nError
	AllowAPIKeyAuth bool
	Email           string
	Description     string
}

type changePasswordPage struct {
	basePage
	Error *util.I18nError
}

type mfaPage struct {
	basePage
	TOTPConfigs      []string
	TOTPConfig       dataprovider.AdminTOTPConfig
	GenerateTOTPURL  string
	ValidateTOTPURL  string
	SaveTOTPURL      string
	RecCodesURL      string
	RequireTwoFactor bool
}

type maintenancePage struct {
	basePage
	BackupPath  string
	RestorePath string
	Error       *util.I18nError
}

type defenderHostsPage struct {
	basePage
	DefenderHostsURL string
}

type ipListsPage struct {
	basePage
	IPListsSearchURL      string
	RateLimitersStatus    bool
	RateLimitersProtocols string
	IsAllowListEnabled    bool
}

type ipListPage struct {
	basePage
	Entry *dataprovider.IPListEntry
	Error *util.I18nError
	Mode  genericPageMode
}

type setupPage struct {
	commonBasePage
	CurrentURL           string
	Error                *util.I18nError
	CSRFToken            string
	Username             string
	HasInstallationCode  bool
	InstallationCodeHint string
	HideSupportLink      bool
	Title                string
	Branding             UIBranding
}

type folderPage struct {
	basePage
	Folder    vfs.BaseVirtualFolder
	Error     *util.I18nError
	Mode      folderPageMode
	FsWrapper fsWrapper
}

type groupPage struct {
	basePage
	Group              *dataprovider.Group
	Error              *util.I18nError
	Mode               genericPageMode
	ValidPerms         []string
	ValidLoginMethods  []string
	ValidProtocols     []string
	TwoFactorProtocols []string
	WebClientOptions   []string
	VirtualFolders     []vfs.BaseVirtualFolder
	FsWrapper          fsWrapper
}

type rolePage struct {
	basePage
	Role  *dataprovider.Role
	Error *util.I18nError
	Mode  genericPageMode
}

type eventActionPage struct {
	basePage
	Action         dataprovider.BaseEventAction
	ActionTypes    []dataprovider.EnumMapping
	FsActions      []dataprovider.EnumMapping
	HTTPMethods    []string
	RedactedSecret string
	Error          *util.I18nError
	Mode           genericPageMode
}

type eventRulePage struct {
	basePage
	Rule            dataprovider.EventRule
	TriggerTypes    []dataprovider.EnumMapping
	Actions         []dataprovider.BaseEventAction
	FsEvents        []string
	Protocols       []string
	ProviderEvents  []string
	ProviderObjects []string
	Error           *util.I18nError
	Mode            genericPageMode
	IsShared        bool
}

type eventsPage struct {
	basePage
	FsEventsSearchURL       string
	ProviderEventsSearchURL string
	LogEventsSearchURL      string
}

type configsPage struct {
	basePage
	Configs           dataprovider.Configs
	ConfigSection     int
	RedactedSecret    string
	OAuth2TokenURL    string
	OAuth2RedirectURL string
	Error             *util.I18nError
}

type messagePage struct {
	basePage
	Error   *util.I18nError
	Success string
	Text    string
}

type userTemplateFields struct {
	Username   string
	Password   string
	PublicKeys []string
}

func loadAdminTemplates(templatesPath string) {
	usersPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateUsers),
	}
	userPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateFsConfig),
		filepath.Join(templatesPath, templateAdminDir, templateUser),
	}
	adminsPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateAdmins),
	}
	adminPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateAdmin),
	}
	profilePaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateProfile),
	}
	changePwdPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateCommonDir, templateChangePwd),
	}
	connectionsPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateConnections),
	}
	messagePaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateCommonDir, templateMessage),
	}
	foldersPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateFolders),
	}
	folderPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateFsConfig),
		filepath.Join(templatesPath, templateAdminDir, templateFolder),
	}
	groupsPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateGroups),
	}
	groupPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateFsConfig),
		filepath.Join(templatesPath, templateAdminDir, templateGroup),
	}
	eventRulesPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateEventRules),
	}
	eventRulePaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateEventRule),
	}
	eventActionsPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateEventActions),
	}
	eventActionPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateEventAction),
	}
	statusPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateStatus),
	}
	loginPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),
		filepath.Join(templatesPath, templateCommonDir, templateCommonLogin),
	}
	maintenancePaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateMaintenance),
	}
	defenderPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateDefender),
	}
	ipListsPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateIPLists),
	}
	ipListPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateIPList),
	}
	mfaPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateMFA),
	}
	twoFactorPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),
		filepath.Join(templatesPath, templateCommonDir, templateTwoFactor),
	}
	twoFactorRecoveryPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),
		filepath.Join(templatesPath, templateCommonDir, templateTwoFactorRecovery),
	}
	setupPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),
		filepath.Join(templatesPath, templateAdminDir, templateSetup),
	}
	forgotPwdPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),
		filepath.Join(templatesPath, templateCommonDir, templateForgotPassword),
	}
	resetPwdPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),
		filepath.Join(templatesPath, templateCommonDir, templateResetPassword),
	}
	rolesPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateRoles),
	}
	rolePaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateRole),
	}
	eventsPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateEvents),
	}
	configsPaths := []string{
		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
		filepath.Join(templatesPath, templateAdminDir, templateBase),
		filepath.Join(templatesPath, templateAdminDir, templateConfigs),
	}

	fsBaseTpl := template.New("fsBaseTemplate").Funcs(template.FuncMap{
		"ListFSProviders": func() []dataprovider.FilesystemProvider {
			return []dataprovider.FilesystemProvider{
				{FilesystemProvider: sdk.LocalFilesystemProvider},
				{FilesystemProvider: sdk.CryptedFilesystemProvider},
				{FilesystemProvider: sdk.S3FilesystemProvider},
				{FilesystemProvider: sdk.GCSFilesystemProvider},
				{FilesystemProvider: sdk.AzureBlobFilesystemProvider},
				{FilesystemProvider: sdk.SFTPFilesystemProvider},
				{FilesystemProvider: sdk.HTTPFilesystemProvider},
			}
		},
		"HumanizeBytes": util.ByteCountSI,
	})
	usersTmpl := util.LoadTemplate(nil, usersPaths...)
	userTmpl := util.LoadTemplate(fsBaseTpl, userPaths...)
	adminsTmpl := util.LoadTemplate(nil, adminsPaths...)
	adminTmpl := util.LoadTemplate(nil, adminPaths...)
	connectionsTmpl := util.LoadTemplate(nil, connectionsPaths...)
	messageTmpl := util.LoadTemplate(nil, messagePaths...)
	groupsTmpl := util.LoadTemplate(nil, groupsPaths...)
	groupTmpl := util.LoadTemplate(fsBaseTpl, groupPaths...)
	foldersTmpl := util.LoadTemplate(nil, foldersPaths...)
	folderTmpl := util.LoadTemplate(fsBaseTpl, folderPaths...)
	eventRulesTmpl := util.LoadTemplate(nil, eventRulesPaths...)
	eventRuleTmpl := util.LoadTemplate(fsBaseTpl, eventRulePaths...)
	eventActionsTmpl := util.LoadTemplate(nil, eventActionsPaths...)
	eventActionTmpl := util.LoadTemplate(nil, eventActionPaths...)
	statusTmpl := util.LoadTemplate(nil, statusPaths...)
	loginTmpl := util.LoadTemplate(nil, loginPaths...)
	profileTmpl := util.LoadTemplate(nil, profilePaths...)
	changePwdTmpl := util.LoadTemplate(nil, changePwdPaths...)
	maintenanceTmpl := util.LoadTemplate(nil, maintenancePaths...)
	defenderTmpl := util.LoadTemplate(nil, defenderPaths...)
	ipListsTmpl := util.LoadTemplate(nil, ipListsPaths...)
	ipListTmpl := util.LoadTemplate(nil, ipListPaths...)
	mfaTmpl := util.LoadTemplate(nil, mfaPaths...)
	twoFactorTmpl := util.LoadTemplate(nil, twoFactorPaths...)
	twoFactorRecoveryTmpl := util.LoadTemplate(nil, twoFactorRecoveryPaths...)
	setupTmpl := util.LoadTemplate(nil, setupPaths...)
	forgotPwdTmpl := util.LoadTemplate(nil, forgotPwdPaths...)
	resetPwdTmpl := util.LoadTemplate(nil, resetPwdPaths...)
	rolesTmpl := util.LoadTemplate(nil, rolesPaths...)
	roleTmpl := util.LoadTemplate(nil, rolePaths...)
	eventsTmpl := util.LoadTemplate(nil, eventsPaths...)
	configsTmpl := util.LoadTemplate(nil, configsPaths...)

	adminTemplates[templateUsers] = usersTmpl
	adminTemplates[templateUser] = userTmpl
	adminTemplates[templateAdmins] = adminsTmpl
	adminTemplates[templateAdmin] = adminTmpl
	adminTemplates[templateConnections] = connectionsTmpl
	adminTemplates[templateMessage] = messageTmpl
	adminTemplates[templateGroups] = groupsTmpl
	adminTemplates[templateGroup] = groupTmpl
	adminTemplates[templateFolders] = foldersTmpl
	adminTemplates[templateFolder] = folderTmpl
	adminTemplates[templateEventRules] = eventRulesTmpl
	adminTemplates[templateEventRule] = eventRuleTmpl
	adminTemplates[templateEventActions] = eventActionsTmpl
	adminTemplates[templateEventAction] = eventActionTmpl
	adminTemplates[templateStatus] = statusTmpl
	adminTemplates[templateCommonLogin] = loginTmpl
	adminTemplates[templateProfile] = profileTmpl
	adminTemplates[templateChangePwd] = changePwdTmpl
	adminTemplates[templateMaintenance] = maintenanceTmpl
	adminTemplates[templateDefender] = defenderTmpl
	adminTemplates[templateIPLists] = ipListsTmpl
	adminTemplates[templateIPList] = ipListTmpl
	adminTemplates[templateMFA] = mfaTmpl
	adminTemplates[templateTwoFactor] = twoFactorTmpl
	adminTemplates[templateTwoFactorRecovery] = twoFactorRecoveryTmpl
	adminTemplates[templateSetup] = setupTmpl
	adminTemplates[templateForgotPassword] = forgotPwdTmpl
	adminTemplates[templateResetPassword] = resetPwdTmpl
	adminTemplates[templateRoles] = rolesTmpl
	adminTemplates[templateRole] = roleTmpl
	adminTemplates[templateEvents] = eventsTmpl
	adminTemplates[templateConfigs] = configsTmpl
}

func isEventManagerResource(currentURL string) bool {
	if currentURL == webAdminEventRulesPath {
		return true
	}
	if currentURL == webAdminEventActionsPath {
		return true
	}
	if currentURL == webAdminEventRulePath || strings.HasPrefix(currentURL, webAdminEventRulePath+"/") {
		return true
	}
	if currentURL == webAdminEventActionPath || strings.HasPrefix(currentURL, webAdminEventActionPath+"/") {
		return true
	}
	return false
}

func isIPListsResource(currentURL string) bool {
	if currentURL == webDefenderPath {
		return true
	}
	if currentURL == webIPListsPath {
		return true
	}
	if strings.HasPrefix(currentURL, webIPListPath+"/") {
		return true
	}
	return false
}

func isServerManagerResource(currentURL string) bool {
	return currentURL == webEventsPath || currentURL == webStatusPath || currentURL == webMaintenancePath ||
		currentURL == webConfigsPath
}

func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request) basePage {
	var csrfToken string
	if currentURL != "" {
		csrfToken = createCSRFToken(util.GetIPFromRemoteAddress(r.RemoteAddr))
	}
	return basePage{
		commonBasePage:      getCommonBasePage(r),
		Title:               title,
		CurrentURL:          currentURL,
		UsersURL:            webUsersPath,
		UserURL:             webUserPath,
		UserTemplateURL:     webTemplateUser,
		AdminsURL:           webAdminsPath,
		AdminURL:            webAdminPath,
		GroupsURL:           webGroupsPath,
		GroupURL:            webGroupPath,
		FoldersURL:          webFoldersPath,
		FolderURL:           webFolderPath,
		FolderTemplateURL:   webTemplateFolder,
		DefenderURL:         webDefenderPath,
		IPListsURL:          webIPListsPath,
		IPListURL:           webIPListPath,
		EventsURL:           webEventsPath,
		ConfigsURL:          webConfigsPath,
		LogoutURL:           webLogoutPath,
		LoginURL:            webAdminLoginPath,
		ProfileURL:          webAdminProfilePath,
		ChangePwdURL:        webChangeAdminPwdPath,
		MFAURL:              webAdminMFAPath,
		EventRulesURL:       webAdminEventRulesPath,
		EventRuleURL:        webAdminEventRulePath,
		EventActionsURL:     webAdminEventActionsPath,
		EventActionURL:      webAdminEventActionPath,
		RolesURL:            webAdminRolesPath,
		RoleURL:             webAdminRolePath,
		QuotaScanURL:        webQuotaScanPath,
		ConnectionsURL:      webConnectionsPath,
		StatusURL:           webStatusPath,
		FolderQuotaScanURL:  webScanVFolderPath,
		MaintenanceURL:      webMaintenancePath,
		LoggedUser:          getAdminFromToken(r),
		IsEventManagerPage:  isEventManagerResource(currentURL),
		IsIPManagerPage:     isIPListsResource(currentURL),
		IsServerManagerPage: isServerManagerResource(currentURL),
		HasDefender:         common.Config.DefenderConfig.Enabled,
		HasSearcher:         plugin.Handler.HasSearcher(),
		HasExternalLogin:    isLoggedInWithOIDC(r),
		CSRFToken:           csrfToken,
		Branding:            s.binding.Branding.WebAdmin,
	}
}

func renderAdminTemplate(w http.ResponseWriter, tmplName string, data any) {
	err := adminTemplates[tmplName].ExecuteTemplate(w, tmplName, data)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func (s *httpdServer) renderMessagePageWithString(w http.ResponseWriter, r *http.Request, title string, statusCode int,
	err error, message, text string,
) {
	data := messagePage{
		basePage: s.getBasePageData(title, "", r),
		Error:    getI18nError(err),
		Success:  message,
		Text:     text,
	}
	w.WriteHeader(statusCode)
	renderAdminTemplate(w, templateMessage, data)
}

func (s *httpdServer) renderMessagePage(w http.ResponseWriter, r *http.Request, title string, statusCode int,
	err error, message string,
) {
	s.renderMessagePageWithString(w, r, title, statusCode, err, message, "")
}

func (s *httpdServer) renderInternalServerErrorPage(w http.ResponseWriter, r *http.Request, err error) {
	s.renderMessagePage(w, r, util.I18nError500Title, http.StatusInternalServerError,
		util.NewI18nError(err, util.I18nError500Message), "")
}

func (s *httpdServer) renderBadRequestPage(w http.ResponseWriter, r *http.Request, err error) {
	s.renderMessagePage(w, r, util.I18nError400Title, http.StatusBadRequest,
		util.NewI18nError(err, util.I18nError400Message), "")
}

func (s *httpdServer) renderForbiddenPage(w http.ResponseWriter, r *http.Request, err error) {
	s.renderMessagePage(w, r, util.I18nError403Title, http.StatusForbidden,
		util.NewI18nError(err, util.I18nError403Message), "")
}

func (s *httpdServer) renderNotFoundPage(w http.ResponseWriter, r *http.Request, err error) {
	s.renderMessagePage(w, r, util.I18nError404Title, http.StatusNotFound,
		util.NewI18nError(err, util.I18nError404Message), "")
}

func (s *httpdServer) renderForgotPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
	data := forgotPwdPage{
		commonBasePage: getCommonBasePage(r),
		CurrentURL:     webAdminForgotPwdPath,
		Error:          err,
		CSRFToken:      createCSRFToken(ip),
		LoginURL:       webAdminLoginPath,
		Title:          util.I18nForgotPwdTitle,
		Branding:       s.binding.Branding.WebAdmin,
	}
	renderAdminTemplate(w, templateForgotPassword, data)
}

func (s *httpdServer) renderResetPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
	data := resetPwdPage{
		commonBasePage: getCommonBasePage(r),
		CurrentURL:     webAdminResetPwdPath,
		Error:          err,
		CSRFToken:      createCSRFToken(ip),
		LoginURL:       webAdminLoginPath,
		Title:          util.I18nResetPwdTitle,
		Branding:       s.binding.Branding.WebAdmin,
	}
	renderAdminTemplate(w, templateResetPassword, data)
}

func (s *httpdServer) renderTwoFactorPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
	data := twoFactorPage{
		commonBasePage: getCommonBasePage(r),
		Title:          pageTwoFactorTitle,
		CurrentURL:     webAdminTwoFactorPath,
		Error:          err,
		CSRFToken:      createCSRFToken(ip),
		RecoveryURL:    webAdminTwoFactorRecoveryPath,
		Branding:       s.binding.Branding.WebAdmin,
	}
	renderAdminTemplate(w, templateTwoFactor, data)
}

func (s *httpdServer) renderTwoFactorRecoveryPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
	data := twoFactorPage{
		commonBasePage: getCommonBasePage(r),
		Title:          pageTwoFactorRecoveryTitle,
		CurrentURL:     webAdminTwoFactorRecoveryPath,
		Error:          err,
		CSRFToken:      createCSRFToken(ip),
		Branding:       s.binding.Branding.WebAdmin,
	}
	renderAdminTemplate(w, templateTwoFactorRecovery, data)
}

func (s *httpdServer) renderMFAPage(w http.ResponseWriter, r *http.Request) {
	data := mfaPage{
		basePage:        s.getBasePageData(pageMFATitle, webAdminMFAPath, r),
		TOTPConfigs:     mfa.GetAvailableTOTPConfigNames(),
		GenerateTOTPURL: webAdminTOTPGeneratePath,
		ValidateTOTPURL: webAdminTOTPValidatePath,
		SaveTOTPURL:     webAdminTOTPSavePath,
		RecCodesURL:     webAdminRecoveryCodesPath,
	}
	admin, err := dataprovider.AdminExists(data.LoggedUser.Username)
	if err != nil {
		s.renderInternalServerErrorPage(w, r, err)
		return
	}
	data.TOTPConfig = admin.Filters.TOTPConfig
	data.RequireTwoFactor = admin.Filters.RequireTwoFactor
	renderAdminTemplate(w, templateMFA, data)
}

func (s *httpdServer) renderProfilePage(w http.ResponseWriter, r *http.Request, err error) {
	data := profilePage{
		basePage: s.getBasePageData(util.I18nProfileTitle, webAdminProfilePath, r),
		Error:    getI18nError(err),
	}
	admin, err := dataprovider.AdminExists(data.LoggedUser.Username)
	if err != nil {
		s.renderInternalServerErrorPage(w, r, err)
		return
	}
	data.AllowAPIKeyAuth = admin.Filters.AllowAPIKeyAuth
	data.Email = admin.Email
	data.Description = admin.Description

	renderAdminTemplate(w, templateProfile, data)
}

func (s *httpdServer) renderChangePasswordPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {
	data := changePasswordPage{
		basePage: s.getBasePageData(util.I18nChangePwdTitle, webChangeAdminPwdPath, r),
		Error:    err,
	}

	renderAdminTemplate(w, templateChangePwd, data)
}

func (s *httpdServer) renderMaintenancePage(w http.ResponseWriter, r *http.Request, err error) {
	data := maintenancePage{
		basePage:    s.getBasePageData(util.I18nMaintenanceTitle, webMaintenancePath, r),
		BackupPath:  webBackupPath,
		RestorePath: webRestorePath,
		Error:       getI18nError(err),
	}

	renderAdminTemplate(w, templateMaintenance, data)
}

func (s *httpdServer) renderConfigsPage(w http.ResponseWriter, r *http.Request, configs dataprovider.Configs,
	err error, section int,
) {
	configs.SetNilsToEmpty()
	if configs.SMTP.Port == 0 {
		configs.SMTP.Port = 587
		configs.SMTP.AuthType = 1
		configs.SMTP.Encryption = 2
	}
	if configs.ACME.HTTP01Challenge.Port == 0 {
		configs.ACME.HTTP01Challenge.Port = 80
	}
	data := configsPage{
		basePage:          s.getBasePageData(util.I18nConfigsTitle, webConfigsPath, r),
		Configs:           configs,
		ConfigSection:     section,
		RedactedSecret:    redactedSecret,
		OAuth2TokenURL:    webOAuth2TokenPath,
		OAuth2RedirectURL: webOAuth2RedirectPath,
		Error:             getI18nError(err),
	}

	renderAdminTemplate(w, templateConfigs, data)
}

func (s *httpdServer) renderAdminSetupPage(w http.ResponseWriter, r *http.Request, username, ip string, err *util.I18nError) {
	data := setupPage{
		commonBasePage:       getCommonBasePage(r),
		Title:                util.I18nSetupTitle,
		CurrentURL:           webAdminSetupPath,
		CSRFToken:            createCSRFToken(ip),
		Username:             username,
		HasInstallationCode:  installationCode != "",
		InstallationCodeHint: installationCodeHint,
		HideSupportLink:      hideSupportLink,
		Error:                err,
		Branding:             s.binding.Branding.WebAdmin,
	}

	renderAdminTemplate(w, templateSetup, data)
}

func (s *httpdServer) renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Request, admin *dataprovider.Admin,
	err error, isAdd bool) {
	groups, errGroups := s.getWebGroups(w, r, defaultQueryLimit, true)
	if errGroups != nil {
		return
	}
	roles, errRoles := s.getWebRoles(w, r, 10, true)
	if errRoles != nil {
		return
	}
	currentURL := webAdminPath
	title := util.I18nAddAdminTitle
	if !isAdd {
		currentURL = fmt.Sprintf("%v/%v", webAdminPath, url.PathEscape(admin.Username))
		title = util.I18nUpdateAdminTitle
	}
	data := adminPage{
		basePage: s.getBasePageData(title, currentURL, r),
		Admin:    admin,
		Groups:   groups,
		Roles:    roles,
		Error:    getI18nError(err),
		IsAdd:    isAdd,
	}

	renderAdminTemplate(w, templateAdmin, data)
}

func (s *httpdServer) getUserPageTitleAndURL(mode userPageMode, username string) (string, string) {
	var title, currentURL string
	switch mode {
	case userPageModeAdd:
		title = util.I18nAddUserTitle
		currentURL = webUserPath
	case userPageModeUpdate:
		title = util.I18nUpdateUserTitle
		currentURL = fmt.Sprintf("%v/%v", webUserPath, url.PathEscape(username))
	case userPageModeTemplate:
		title = util.I18nTemplateUserTitle
		currentURL = webTemplateUser
	}
	return title, currentURL
}

func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, user *dataprovider.User,
	mode userPageMode, err error, admin *dataprovider.Admin,
) {
	user.SetEmptySecretsIfNil()
	title, currentURL := s.getUserPageTitleAndURL(mode, user.Username)
	if user.Password != "" && user.IsPasswordHashed() {
		switch mode {
		case userPageModeUpdate:
			user.Password = redactedSecret
		default:
			user.Password = ""
		}
	}
	user.FsConfig.RedactedSecret = redactedSecret
	basePage := s.getBasePageData(title, currentURL, r)
	if (mode == userPageModeAdd || mode == userPageModeTemplate) && len(user.Groups) == 0 && admin != nil {
		for _, group := range admin.Groups {
			user.Groups = append(user.Groups, sdk.GroupMapping{
				Name: group.Name,
				Type: group.Options.GetUserGroupType(),
			})
		}
	}
	var roles []dataprovider.Role
	if basePage.LoggedUser.Role == "" {
		var errRoles error
		roles, errRoles = s.getWebRoles(w, r, 10, true)
		if errRoles != nil {
			return
		}
	}
	folders, errFolders := s.getWebVirtualFolders(w, r, defaultQueryLimit, true)
	if errFolders != nil {
		return
	}
	groups, errGroups := s.getWebGroups(w, r, defaultQueryLimit, true)
	if errGroups != nil {
		return
	}
	data := userPage{
		basePage:           basePage,
		Mode:               mode,
		Error:              getI18nError(err),
		User:               user,
		ValidPerms:         dataprovider.ValidPerms,
		ValidLoginMethods:  dataprovider.ValidLoginMethods,
		ValidProtocols:     dataprovider.ValidProtocols,
		TwoFactorProtocols: dataprovider.MFAProtocols,
		WebClientOptions:   sdk.WebClientOptions,
		RootDirPerms:       user.GetPermissionsForPath("/"),
		VirtualFolders:     folders,
		Groups:             groups,
		Roles:              roles,
		CanImpersonate:     os.Getuid() == 0,
		FsWrapper: fsWrapper{
			Filesystem:      user.FsConfig,
			IsUserPage:      true,
			IsGroupPage:     false,
			IsHidden:        basePage.LoggedUser.Filters.Preferences.HideFilesystem(),
			HasUsersBaseDir: dataprovider.HasUsersBaseDir(),
			DirPath:         user.HomeDir,
		},
	}
	renderAdminTemplate(w, templateUser, data)
}

func (s *httpdServer) renderIPListPage(w http.ResponseWriter, r *http.Request, entry dataprovider.IPListEntry,
	mode genericPageMode, err error,
) {
	var title, currentURL string
	switch mode {
	case genericPageModeAdd:
		title = util.I18nAddIPListTitle
		currentURL = fmt.Sprintf("%s/%d", webIPListPath, entry.Type)
	case genericPageModeUpdate:
		title = util.I18nUpdateIPListTitle
		currentURL = fmt.Sprintf("%s/%d/%s", webIPListPath, entry.Type, url.PathEscape(entry.IPOrNet))
	}
	data := ipListPage{
		basePage: s.getBasePageData(title, currentURL, r),
		Error:    getI18nError(err),
		Entry:    &entry,
		Mode:     mode,
	}
	renderAdminTemplate(w, templateIPList, data)
}

func (s *httpdServer) renderRolePage(w http.ResponseWriter, r *http.Request, role dataprovider.Role,
	mode genericPageMode, err error,
) {
	var title, currentURL string
	switch mode {
	case genericPageModeAdd:
		title = util.I18nRoleAddTitle
		currentURL = webAdminRolePath
	case genericPageModeUpdate:
		title = util.I18nRoleUpdateTitle
		currentURL = fmt.Sprintf("%s/%s", webAdminRolePath, url.PathEscape(role.Name))
	}
	data := rolePage{
		basePage: s.getBasePageData(title, currentURL, r),
		Error:    getI18nError(err),
		Role:     &role,
		Mode:     mode,
	}
	renderAdminTemplate(w, templateRole, data)
}

func (s *httpdServer) renderGroupPage(w http.ResponseWriter, r *http.Request, group dataprovider.Group,
	mode genericPageMode, err error,
) {
	folders, errFolders := s.getWebVirtualFolders(w, r, defaultQueryLimit, true)
	if errFolders != nil {
		return
	}
	group.SetEmptySecretsIfNil()
	group.UserSettings.FsConfig.RedactedSecret = redactedSecret
	var title, currentURL string
	switch mode {
	case genericPageModeAdd:
		title = util.I18nAddGroupTitle
		currentURL = webGroupPath
	case genericPageModeUpdate:
		title = util.I18nUpdateGroupTitle
		currentURL = fmt.Sprintf("%v/%v", webGroupPath, url.PathEscape(group.Name))
	}
	group.UserSettings.FsConfig.RedactedSecret = redactedSecret
	group.UserSettings.FsConfig.SetEmptySecretsIfNil()

	data := groupPage{
		basePage:           s.getBasePageData(title, currentURL, r),
		Error:              getI18nError(err),
		Group:              &group,
		Mode:               mode,
		ValidPerms:         dataprovider.ValidPerms,
		ValidLoginMethods:  dataprovider.ValidLoginMethods,
		ValidProtocols:     dataprovider.ValidProtocols,
		TwoFactorProtocols: dataprovider.MFAProtocols,
		WebClientOptions:   sdk.WebClientOptions,
		VirtualFolders:     folders,
		FsWrapper: fsWrapper{
			Filesystem:      group.UserSettings.FsConfig,
			IsUserPage:      false,
			IsGroupPage:     true,
			HasUsersBaseDir: false,
			DirPath:         group.UserSettings.HomeDir,
		},
	}
	renderAdminTemplate(w, templateGroup, data)
}

func (s *httpdServer) renderEventActionPage(w http.ResponseWriter, r *http.Request, action dataprovider.BaseEventAction,
	mode genericPageMode, err error,
) {
	action.Options.SetEmptySecretsIfNil()
	var title, currentURL string
	switch mode {
	case genericPageModeAdd:
		title = util.I18nAddActionTitle
		currentURL = webAdminEventActionPath
	case genericPageModeUpdate:
		title = util.I18nUpdateActionTitle
		currentURL = fmt.Sprintf("%s/%s", webAdminEventActionPath, url.PathEscape(action.Name))
	}
	if action.Options.HTTPConfig.Timeout == 0 {
		action.Options.HTTPConfig.Timeout = 20
	}
	if action.Options.CmdConfig.Timeout == 0 {
		action.Options.CmdConfig.Timeout = 20
	}
	if action.Options.PwdExpirationConfig.Threshold == 0 {
		action.Options.PwdExpirationConfig.Threshold = 10
	}

	data := eventActionPage{
		basePage:       s.getBasePageData(title, currentURL, r),
		Action:         action,
		ActionTypes:    dataprovider.EventActionTypes,
		FsActions:      dataprovider.FsActionTypes,
		HTTPMethods:    dataprovider.SupportedHTTPActionMethods,
		RedactedSecret: redactedSecret,
		Error:          getI18nError(err),
		Mode:           mode,
	}
	renderAdminTemplate(w, templateEventAction, data)
}

func (s *httpdServer) renderEventRulePage(w http.ResponseWriter, r *http.Request, rule dataprovider.EventRule,
	mode genericPageMode, err error,
) {
	actions, errActions := s.getWebEventActions(w, r, defaultQueryLimit, true)
	if errActions != nil {
		return
	}
	var title, currentURL string
	switch mode {
	case genericPageModeAdd:
		title = util.I18nAddRuleTitle
		currentURL = webAdminEventRulePath
	case genericPageModeUpdate:
		title = util.I18nUpdateRuleTitle
		currentURL = fmt.Sprintf("%v/%v", webAdminEventRulePath, url.PathEscape(rule.Name))
	}

	data := eventRulePage{
		basePage:        s.getBasePageData(title, currentURL, r),
		Rule:            rule,
		TriggerTypes:    dataprovider.EventTriggerTypes,
		Actions:         actions,
		FsEvents:        dataprovider.SupportedFsEvents,
		Protocols:       dataprovider.SupportedRuleConditionProtocols,
		ProviderEvents:  dataprovider.SupportedProviderEvents,
		ProviderObjects: dataprovider.SupporteRuleConditionProviderObjects,
		Error:           getI18nError(err),
		Mode:            mode,
		IsShared:        s.isShared > 0,
	}
	renderAdminTemplate(w, templateEventRule, data)
}

func (s *httpdServer) renderFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.BaseVirtualFolder,
	mode folderPageMode, err error,
) {
	var title, currentURL string
	switch mode {
	case folderPageModeAdd:
		title = util.I18nAddFolderTitle
		currentURL = webFolderPath
	case folderPageModeUpdate:
		title = util.I18nUpdateFolderTitle
		currentURL = fmt.Sprintf("%v/%v", webFolderPath, url.PathEscape(folder.Name))
	case folderPageModeTemplate:
		title = util.I18nTemplateFolderTitle
		currentURL = webTemplateFolder
	}
	folder.FsConfig.RedactedSecret = redactedSecret
	folder.FsConfig.SetEmptySecretsIfNil()

	data := folderPage{
		basePage: s.getBasePageData(title, currentURL, r),
		Error:    getI18nError(err),
		Folder:   folder,
		Mode:     mode,
		FsWrapper: fsWrapper{
			Filesystem:      folder.FsConfig,
			IsUserPage:      false,
			IsGroupPage:     false,
			HasUsersBaseDir: false,
			DirPath:         folder.MappedPath,
		},
	}
	renderAdminTemplate(w, templateFolder, data)
}

func getFoldersForTemplate(r *http.Request) []string {
	var res []string
	for k := range r.Form {
		if hasPrefixAndSuffix(k, "template_folders[", "][tpl_foldername]") {
			r.Form.Add("tpl_foldername", r.Form.Get(k))
		}
	}
	folderNames := r.Form["tpl_foldername"]
	folders := make(map[string]bool)
	for _, name := range folderNames {
		name = strings.TrimSpace(name)
		if name == "" {
			continue
		}
		if _, ok := folders[name]; ok {
			continue
		}
		folders[name] = true
		res = append(res, name)
	}
	return res
}

func getUsersForTemplate(r *http.Request) []userTemplateFields {
	var res []userTemplateFields
	tplUsernames := r.Form["tpl_username"]
	tplPasswords := r.Form["tpl_password"]
	tplPublicKeys := r.Form["tpl_public_keys"]

	users := make(map[string]bool)
	for idx := range tplUsernames {
		username := tplUsernames[idx]
		password := ""
		publicKey := ""
		if len(tplPasswords) > idx {
			password = strings.TrimSpace(tplPasswords[idx])
		}
		if len(tplPublicKeys) > idx {
			publicKey = strings.TrimSpace(tplPublicKeys[idx])
		}
		if username == "" {
			continue
		}
		if _, ok := users[username]; ok {
			continue
		}

		users[username] = true
		res = append(res, userTemplateFields{
			Username:   username,
			Password:   password,
			PublicKeys: []string{publicKey},
		})
	}

	return res
}

func getVirtualFoldersFromPostFields(r *http.Request) []vfs.VirtualFolder {
	var virtualFolders []vfs.VirtualFolder
	folderPaths := r.Form["vfolder_path"]
	folderNames := r.Form["vfolder_name"]
	folderQuotaSizes := r.Form["vfolder_quota_size"]
	folderQuotaFiles := r.Form["vfolder_quota_files"]
	for idx, p := range folderPaths {
		name := ""
		if len(folderNames) > idx {
			name = folderNames[idx]
		}
		if p != "" && name != "" {
			vfolder := vfs.VirtualFolder{
				BaseVirtualFolder: vfs.BaseVirtualFolder{
					Name: name,
				},
				VirtualPath: p,
				QuotaFiles:  -1,
				QuotaSize:   -1,
			}
			if len(folderQuotaSizes) > idx {
				quotaSize, err := util.ParseBytes(folderQuotaSizes[idx])
				if err == nil {
					vfolder.QuotaSize = quotaSize
				}
			}
			if len(folderQuotaFiles) > idx {
				quotaFiles, err := strconv.Atoi(folderQuotaFiles[idx])
				if err == nil {
					vfolder.QuotaFiles = quotaFiles
				}
			}
			virtualFolders = append(virtualFolders, vfolder)
		}
	}

	return virtualFolders
}

func getSubDirPermissionsFromPostFields(r *http.Request) map[string][]string {
	permissions := make(map[string][]string)

	for idx, p := range r.Form["sub_perm_path"] {
		if p != "" {
			permissions[p] = r.Form["sub_perm_permissions"+strconv.Itoa(idx)]
		}
	}

	return permissions
}

func getUserPermissionsFromPostFields(r *http.Request) map[string][]string {
	permissions := getSubDirPermissionsFromPostFields(r)
	permissions["/"] = r.Form["permissions"]

	return permissions
}

func getAccessTimeRestrictionsFromPostFields(r *http.Request) []sdk.TimePeriod {
	var result []sdk.TimePeriod

	dayOfWeeks := r.Form["access_time_day_of_week"]
	starts := r.Form["access_time_start"]
	ends := r.Form["access_time_end"]

	for idx, dayOfWeek := range dayOfWeeks {
		dayOfWeek = strings.TrimSpace(dayOfWeek)
		start := ""
		if len(starts) > idx {
			start = strings.TrimSpace(starts[idx])
		}
		end := ""
		if len(ends) > idx {
			end = strings.TrimSpace(ends[idx])
		}
		dayNumber, err := strconv.Atoi(dayOfWeek)
		if err == nil && start != "" && end != "" {
			result = append(result, sdk.TimePeriod{
				DayOfWeek: dayNumber,
				From:      start,
				To:        end,
			})
		}
	}

	return result
}

func getBandwidthLimitsFromPostFields(r *http.Request) ([]sdk.BandwidthLimit, error) {
	var result []sdk.BandwidthLimit
	bwSources := r.Form["bandwidth_limit_sources"]
	uploadSources := r.Form["upload_bandwidth_source"]
	downloadSources := r.Form["download_bandwidth_source"]

	for idx, bwSource := range bwSources {
		sources := getSliceFromDelimitedValues(bwSource, ",")
		if len(sources) > 0 {
			bwLimit := sdk.BandwidthLimit{
				Sources: sources,
			}
			ul := ""
			dl := ""
			if len(uploadSources) > idx {
				ul = uploadSources[idx]
			}
			if len(downloadSources) > idx {
				dl = downloadSources[idx]
			}
			if ul != "" {
				bandwidthUL, err := strconv.ParseInt(ul, 10, 64)
				if err != nil {
					return result, fmt.Errorf("invalid upload_bandwidth_source%v %q: %w", idx, ul, err)
				}
				bwLimit.UploadBandwidth = bandwidthUL
			}
			if dl != "" {
				bandwidthDL, err := strconv.ParseInt(dl, 10, 64)
				if err != nil {
					return result, fmt.Errorf("invalid download_bandwidth_source%v %q: %w", idx, ul, err)
				}
				bwLimit.DownloadBandwidth = bandwidthDL
			}
			result = append(result, bwLimit)
		}
	}

	return result, nil
}

func getPatterDenyPolicyFromString(policy string) int {
	denyPolicy := sdk.DenyPolicyDefault
	if policy == "1" {
		denyPolicy = sdk.DenyPolicyHide
	}
	return denyPolicy
}

func getFilePatternsFromPostField(r *http.Request) []sdk.PatternsFilter {
	var result []sdk.PatternsFilter
	patternPaths := r.Form["pattern_path"]
	patterns := r.Form["patterns"]
	patternTypes := r.Form["pattern_type"]
	policies := r.Form["pattern_policy"]

	allowedPatterns := make(map[string][]string)
	deniedPatterns := make(map[string][]string)
	patternPolicies := make(map[string]string)

	for idx := range patternPaths {
		p := patternPaths[idx]
		filters := strings.ReplaceAll(patterns[idx], " ", "")
		patternType := patternTypes[idx]
		patternPolicy := policies[idx]
		if p != "" && filters != "" {
			if patternType == "allowed" {
				allowedPatterns[p] = append(allowedPatterns[p], strings.Split(filters, ",")...)
			} else {
				deniedPatterns[p] = append(deniedPatterns[p], strings.Split(filters, ",")...)
			}
			if patternPolicy != "" && patternPolicy != "0" {
				patternPolicies[p] = patternPolicy
			}
		}
	}

	for dirAllowed, allowPatterns := range allowedPatterns {
		filter := sdk.PatternsFilter{
			Path:            dirAllowed,
			AllowedPatterns: allowPatterns,
			DenyPolicy:      getPatterDenyPolicyFromString(patternPolicies[dirAllowed]),
		}
		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, sdk.PatternsFilter{
				Path:           dirDenied,
				DeniedPatterns: denPatterns,
				DenyPolicy:     getPatterDenyPolicyFromString(patternPolicies[dirDenied]),
			})
		}
	}
	return result
}

func getGroupsFromUserPostFields(r *http.Request) []sdk.GroupMapping {
	var groups []sdk.GroupMapping

	primaryGroup := strings.TrimSpace(r.Form.Get("primary_group"))
	if primaryGroup != "" {
		groups = append(groups, sdk.GroupMapping{
			Name: primaryGroup,
			Type: sdk.GroupTypePrimary,
		})
	}
	secondaryGroups := r.Form["secondary_groups"]
	for _, name := range secondaryGroups {
		groups = append(groups, sdk.GroupMapping{
			Name: strings.TrimSpace(name),
			Type: sdk.GroupTypeSecondary,
		})
	}
	membershipGroups := r.Form["membership_groups"]
	for _, name := range membershipGroups {
		groups = append(groups, sdk.GroupMapping{
			Name: strings.TrimSpace(name),
			Type: sdk.GroupTypeMembership,
		})
	}
	return groups
}

func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error) {
	var filters sdk.BaseUserFilters
	bwLimits, err := getBandwidthLimitsFromPostFields(r)
	if err != nil {
		return filters, err
	}
	maxFileSize, err := util.ParseBytes(r.Form.Get("max_upload_file_size"))
	if err != nil {
		return filters, util.NewI18nError(fmt.Errorf("invalid max upload file size: %w", err), util.I18nErrorInvalidMaxFilesize)
	}
	defaultSharesExpiration, err := strconv.Atoi(r.Form.Get("default_shares_expiration"))
	if err != nil {
		return filters, fmt.Errorf("invalid default shares expiration: %w", err)
	}
	maxSharesExpiration, err := strconv.Atoi(r.Form.Get("max_shares_expiration"))
	if err != nil {
		return filters, fmt.Errorf("invalid max shares expiration: %w", err)
	}
	passwordExpiration, err := strconv.Atoi(r.Form.Get("password_expiration"))
	if err != nil {
		return filters, fmt.Errorf("invalid password expiration: %w", err)
	}
	passwordStrength, err := strconv.Atoi(r.Form.Get("password_strength"))
	if err != nil {
		return filters, fmt.Errorf("invalid password strength: %w", err)
	}
	if r.Form.Get("ftp_security") == "1" {
		filters.FTPSecurity = 1
	}
	filters.BandwidthLimits = bwLimits
	filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
	filters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get("denied_ip"), ",")
	filters.DeniedLoginMethods = r.Form["denied_login_methods"]
	filters.DeniedProtocols = r.Form["denied_protocols"]
	filters.TwoFactorAuthProtocols = r.Form["required_two_factor_protocols"]
	filters.FilePatterns = getFilePatternsFromPostField(r)
	filters.TLSUsername = sdk.TLSUsername(strings.TrimSpace(r.Form.Get("tls_username")))
	filters.WebClient = r.Form["web_client_options"]
	filters.DefaultSharesExpiration = defaultSharesExpiration
	filters.MaxSharesExpiration = maxSharesExpiration
	filters.PasswordExpiration = passwordExpiration
	filters.PasswordStrength = passwordStrength
	filters.AccessTime = getAccessTimeRestrictionsFromPostFields(r)
	hooks := r.Form["hooks"]
	if util.Contains(hooks, "external_auth_disabled") {
		filters.Hooks.ExternalAuthDisabled = true
	}
	if util.Contains(hooks, "pre_login_disabled") {
		filters.Hooks.PreLoginDisabled = true
	}
	if util.Contains(hooks, "check_password_disabled") {
		filters.Hooks.CheckPasswordDisabled = true
	}
	filters.IsAnonymous = r.Form.Get("is_anonymous") != ""
	filters.DisableFsChecks = r.Form.Get("disable_fs_checks") != ""
	filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != ""
	filters.StartDirectory = strings.TrimSpace(r.Form.Get("start_directory"))
	filters.MaxUploadFileSize = maxFileSize
	filters.ExternalAuthCacheTime, err = strconv.ParseInt(r.Form.Get("external_auth_cache_time"), 10, 64)
	if err != nil {
		return filters, fmt.Errorf("invalid external auth cache time: %w", err)
	}
	return filters, nil
}

func getSecretFromFormField(r *http.Request, field string) *kms.Secret {
	secret := kms.NewPlainSecret(r.Form.Get(field))
	if strings.TrimSpace(secret.GetPayload()) == redactedSecret {
		secret.SetStatus(sdkkms.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 = strings.TrimSpace(r.Form.Get("s3_bucket"))
	config.Region = strings.TrimSpace(r.Form.Get("s3_region"))
	config.AccessKey = strings.TrimSpace(r.Form.Get("s3_access_key"))
	config.RoleARN = strings.TrimSpace(r.Form.Get("s3_role_arn"))
	config.AccessSecret = getSecretFromFormField(r, "s3_access_secret")
	config.Endpoint = strings.TrimSpace(r.Form.Get("s3_endpoint"))
	config.StorageClass = strings.TrimSpace(r.Form.Get("s3_storage_class"))
	config.ACL = strings.TrimSpace(r.Form.Get("s3_acl"))
	config.KeyPrefix = strings.TrimSpace(strings.TrimPrefix(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, fmt.Errorf("invalid s3 upload part size: %w", err)
	}
	config.UploadConcurrency, err = strconv.Atoi(r.Form.Get("s3_upload_concurrency"))
	if err != nil {
		return config, fmt.Errorf("invalid s3 upload concurrency: %w", err)
	}
	config.DownloadPartSize, err = strconv.ParseInt(r.Form.Get("s3_download_part_size"), 10, 64)
	if err != nil {
		return config, fmt.Errorf("invalid s3 download part size: %w", err)
	}
	config.DownloadConcurrency, err = strconv.Atoi(r.Form.Get("s3_download_concurrency"))
	if err != nil {
		return config, fmt.Errorf("invalid s3 download concurrency: %w", err)
	}
	config.ForcePathStyle = r.Form.Get("s3_force_path_style") != ""
	config.SkipTLSVerify = r.Form.Get("s3_skip_tls_verify") != ""
	config.DownloadPartMaxTime, err = strconv.Atoi(r.Form.Get("s3_download_part_max_time"))
	if err != nil {
		return config, fmt.Errorf("invalid s3 download part max time: %w", err)
	}
	config.UploadPartMaxTime, err = strconv.Atoi(r.Form.Get("s3_upload_part_max_time"))
	if err != nil {
		return config, fmt.Errorf("invalid s3 upload part max time: %w", err)
	}
	return config, nil
}

func getGCSConfig(r *http.Request) (vfs.GCSFsConfig, error) {
	var err error
	config := vfs.GCSFsConfig{}

	config.Bucket = strings.TrimSpace(r.Form.Get("gcs_bucket"))
	config.StorageClass = strings.TrimSpace(r.Form.Get("gcs_storage_class"))
	config.ACL = strings.TrimSpace(r.Form.Get("gcs_acl"))
	config.KeyPrefix = strings.TrimSpace(strings.TrimPrefix(r.Form.Get("gcs_key_prefix"), "/"))
	uploadPartSize, err := strconv.ParseInt(r.Form.Get("gcs_upload_part_size"), 10, 64)
	if err == nil {
		config.UploadPartSize = uploadPartSize
	}
	uploadPartMaxTime, err := strconv.Atoi(r.Form.Get("gcs_upload_part_max_time"))
	if err == nil {
		config.UploadPartMaxTime = uploadPartMaxTime
	}
	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 = strings.TrimSpace(r.Form.Get("sftp_endpoint"))
	config.Username = strings.TrimSpace(r.Form.Get("sftp_username"))
	config.Password = getSecretFromFormField(r, "sftp_password")
	config.PrivateKey = getSecretFromFormField(r, "sftp_private_key")
	config.KeyPassphrase = getSecretFromFormField(r, "sftp_key_passphrase")
	fingerprintsFormValue := r.Form.Get("sftp_fingerprints")
	config.Fingerprints = getSliceFromDelimitedValues(fingerprintsFormValue, "\n")
	config.Prefix = strings.TrimSpace(r.Form.Get("sftp_prefix"))
	config.DisableCouncurrentReads = r.Form.Get("sftp_disable_concurrent_reads") != ""
	config.BufferSize, err = strconv.ParseInt(r.Form.Get("sftp_buffer_size"), 10, 64)
	if r.Form.Get("sftp_equality_check_mode") != "" {
		config.EqualityCheckMode = 1
	} else {
		config.EqualityCheckMode = 0
	}
	if err != nil {
		return config, fmt.Errorf("invalid SFTP buffer size: %w", err)
	}
	return config, nil
}

func getHTTPFsConfig(r *http.Request) vfs.HTTPFsConfig {
	config := vfs.HTTPFsConfig{}
	config.Endpoint = strings.TrimSpace(r.Form.Get("http_endpoint"))
	config.Username = strings.TrimSpace(r.Form.Get("http_username"))
	config.SkipTLSVerify = r.Form.Get("http_skip_tls_verify") != ""
	config.Password = getSecretFromFormField(r, "http_password")
	config.APIKey = getSecretFromFormField(r, "http_api_key")
	if r.Form.Get("http_equality_check_mode") != "" {
		config.EqualityCheckMode = 1
	} else {
		config.EqualityCheckMode = 0
	}
	return config
}

func getAzureConfig(r *http.Request) (vfs.AzBlobFsConfig, error) {
	var err error
	config := vfs.AzBlobFsConfig{}
	config.Container = strings.TrimSpace(r.Form.Get("az_container"))
	config.AccountName = strings.TrimSpace(r.Form.Get("az_account_name"))
	config.AccountKey = getSecretFromFormField(r, "az_account_key")
	config.SASURL = getSecretFromFormField(r, "az_sas_url")
	config.Endpoint = strings.TrimSpace(r.Form.Get("az_endpoint"))
	config.KeyPrefix = strings.TrimSpace(strings.TrimPrefix(r.Form.Get("az_key_prefix"), "/"))
	config.AccessTier = strings.TrimSpace(r.Form.Get("az_access_tier"))
	config.UseEmulator = r.Form.Get("az_use_emulator") != ""
	config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("az_upload_part_size"), 10, 64)
	if err != nil {
		return config, fmt.Errorf("invalid azure upload part size: %w", err)
	}
	config.UploadConcurrency, err = strconv.Atoi(r.Form.Get("az_upload_concurrency"))
	if err != nil {
		return config, fmt.Errorf("invalid azure upload concurrency: %w", err)
	}
	config.DownloadPartSize, err = strconv.ParseInt(r.Form.Get("az_download_part_size"), 10, 64)
	if err != nil {
		return config, fmt.Errorf("invalid azure download part size: %w", err)
	}
	config.DownloadConcurrency, err = strconv.Atoi(r.Form.Get("az_download_concurrency"))
	if err != nil {
		return config, fmt.Errorf("invalid azure download concurrency: %w", err)
	}
	return config, nil
}

func getOsConfigFromPostFields(r *http.Request, readBufferField, writeBufferField string) sdk.OSFsConfig {
	config := sdk.OSFsConfig{}
	readBuffer, err := strconv.Atoi(r.Form.Get(readBufferField))
	if err == nil {
		config.ReadBufferSize = readBuffer
	}
	writeBuffer, err := strconv.Atoi(r.Form.Get(writeBufferField))
	if err == nil {
		config.WriteBufferSize = writeBuffer
	}
	return config
}

func getFsConfigFromPostFields(r *http.Request) (vfs.Filesystem, error) {
	var fs vfs.Filesystem
	fs.Provider = sdk.GetProviderByName(r.Form.Get("fs_provider"))
	switch fs.Provider {
	case sdk.LocalFilesystemProvider:
		fs.OSConfig = getOsConfigFromPostFields(r, "osfs_read_buffer_size", "osfs_write_buffer_size")
	case sdk.S3FilesystemProvider:
		config, err := getS3Config(r)
		if err != nil {
			return fs, err
		}
		fs.S3Config = config
	case sdk.AzureBlobFilesystemProvider:
		config, err := getAzureConfig(r)
		if err != nil {
			return fs, err
		}
		fs.AzBlobConfig = config
	case sdk.GCSFilesystemProvider:
		config, err := getGCSConfig(r)
		if err != nil {
			return fs, err
		}
		fs.GCSConfig = config
	case sdk.CryptedFilesystemProvider:
		fs.CryptConfig.Passphrase = getSecretFromFormField(r, "crypt_passphrase")
		fs.CryptConfig.OSFsConfig = getOsConfigFromPostFields(r, "cryptfs_read_buffer_size", "cryptfs_write_buffer_size")
	case sdk.SFTPFilesystemProvider:
		config, err := getSFTPConfig(r)
		if err != nil {
			return fs, err
		}
		fs.SFTPConfig = config
	case sdk.HTTPFilesystemProvider:
		fs.HTTPConfig = getHTTPFsConfig(r)
	}
	return fs, nil
}

func getAdminHiddenUserPageSections(r *http.Request) int {
	var result int

	for _, val := range r.Form["user_page_hidden_sections"] {
		switch val {
		case "1":
			result++
		case "2":
			result += 2
		case "3":
			result += 4
		case "4":
			result += 8
		case "5":
			result += 16
		case "6":
			result += 32
		case "7":
			result += 64
		}
	}

	return result
}

func getAdminFromPostFields(r *http.Request) (dataprovider.Admin, error) {
	var admin dataprovider.Admin
	err := r.ParseForm()
	if err != nil {
		return admin, util.NewI18nError(err, util.I18nErrorInvalidForm)
	}
	status, err := strconv.Atoi(r.Form.Get("status"))
	if err != nil {
		return admin, fmt.Errorf("invalid status: %w", err)
	}
	admin.Username = strings.TrimSpace(r.Form.Get("username"))
	admin.Password = strings.TrimSpace(r.Form.Get("password"))
	admin.Permissions = r.Form["permissions"]
	admin.Email = strings.TrimSpace(r.Form.Get("email"))
	admin.Status = status
	admin.Role = strings.TrimSpace(r.Form.Get("role"))
	admin.Filters.AllowList = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
	admin.Filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != ""
	admin.Filters.RequireTwoFactor = r.Form.Get("require_two_factor") != ""
	admin.Filters.RequirePasswordChange = r.Form.Get("require_password_change") != ""
	admin.AdditionalInfo = r.Form.Get("additional_info")
	admin.Description = r.Form.Get("description")
	admin.Filters.Preferences.HideUserPageSections = getAdminHiddenUserPageSections(r)
	admin.Filters.Preferences.DefaultUsersExpiration = 0
	if val := r.Form.Get("default_users_expiration"); val != "" {
		defaultUsersExpiration, err := strconv.Atoi(r.Form.Get("default_users_expiration"))
		if err != nil {
			return admin, fmt.Errorf("invalid default users expiration: %w", err)
		}
		admin.Filters.Preferences.DefaultUsersExpiration = defaultUsersExpiration
	}
	for k := range r.Form {
		if hasPrefixAndSuffix(k, "groups[", "][group]") {
			groupName := strings.TrimSpace(r.Form.Get(k))
			if groupName != "" {
				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
				case "2":
					group.Options.AddToUsersAs = dataprovider.GroupAddToUsersAsSecondary
				default:
					group.Options.AddToUsersAs = dataprovider.GroupAddToUsersAsMembership
				}
				admin.Groups = append(admin.Groups, group)
			}
		}
	}
	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 sdk.CryptedFilesystemProvider:
		folder.FsConfig.CryptConfig = getCryptFsFromTemplate(folder.FsConfig.CryptConfig, replacements)
	case sdk.S3FilesystemProvider:
		folder.FsConfig.S3Config = getS3FsFromTemplate(folder.FsConfig.S3Config, replacements)
	case sdk.GCSFilesystemProvider:
		folder.FsConfig.GCSConfig = getGCSFsFromTemplate(folder.FsConfig.GCSConfig, replacements)
	case sdk.AzureBlobFilesystemProvider:
		folder.FsConfig.AzBlobConfig = getAzBlobFsFromTemplate(folder.FsConfig.AzBlobConfig, replacements)
	case sdk.SFTPFilesystemProvider:
		folder.FsConfig.SFTPConfig = getSFTPFsFromTemplate(folder.FsConfig.SFTPConfig, replacements)
	case sdk.HTTPFilesystemProvider:
		folder.FsConfig.HTTPConfig = getHTTPFsFromTemplate(folder.FsConfig.HTTPConfig, 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 getHTTPFsFromTemplate(fsConfig vfs.HTTPFsConfig, replacements map[string]string) vfs.HTTPFsConfig {
	fsConfig.Username = replacePlaceholders(fsConfig.Username, replacements)
	return fsConfig
}

func getUserFromTemplate(user dataprovider.User, template userTemplateFields) dataprovider.User {
	user.Username = template.Username
	user.Password = template.Password
	user.PublicKeys = template.PublicKeys
	replacements := make(map[string]string)
	replacements["%username%"] = user.Username
	if user.Password != "" && !user.IsPasswordHashed() {
		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)
	user.Filters.StartDirectory = replacePlaceholders(user.Filters.StartDirectory, replacements)

	switch user.FsConfig.Provider {
	case sdk.CryptedFilesystemProvider:
		user.FsConfig.CryptConfig = getCryptFsFromTemplate(user.FsConfig.CryptConfig, replacements)
	case sdk.S3FilesystemProvider:
		user.FsConfig.S3Config = getS3FsFromTemplate(user.FsConfig.S3Config, replacements)
	case sdk.GCSFilesystemProvider:
		user.FsConfig.GCSConfig = getGCSFsFromTemplate(user.FsConfig.GCSConfig, replacements)
	case sdk.AzureBlobFilesystemProvider:
		user.FsConfig.AzBlobConfig = getAzBlobFsFromTemplate(user.FsConfig.AzBlobConfig, replacements)
	case sdk.SFTPFilesystemProvider:
		user.FsConfig.SFTPConfig = getSFTPFsFromTemplate(user.FsConfig.SFTPConfig, replacements)
	case sdk.HTTPFilesystemProvider:
		user.FsConfig.HTTPConfig = getHTTPFsFromTemplate(user.FsConfig.HTTPConfig, replacements)
	}

	return user
}

func getTransferLimits(r *http.Request) (int64, int64, int64, error) {
	dataTransferUL, err := strconv.ParseInt(r.Form.Get("upload_data_transfer"), 10, 64)
	if err != nil {
		return 0, 0, 0, fmt.Errorf("invalid upload data transfer: %w", err)
	}
	dataTransferDL, err := strconv.ParseInt(r.Form.Get("download_data_transfer"), 10, 64)
	if err != nil {
		return 0, 0, 0, fmt.Errorf("invalid download data transfer: %w", err)
	}
	dataTransferTotal, err := strconv.ParseInt(r.Form.Get("total_data_transfer"), 10, 64)
	if err != nil {
		return 0, 0, 0, fmt.Errorf("invalid total data transfer: %w", err)
	}
	return dataTransferUL, dataTransferDL, dataTransferTotal, nil
}

func getQuotaLimits(r *http.Request) (int64, int, error) {
	quotaSize, err := util.ParseBytes(r.Form.Get("quota_size"))
	if err != nil {
		return 0, 0, util.NewI18nError(fmt.Errorf("invalid quota size: %w", err), util.I18nErrorInvalidQuotaSize)
	}
	quotaFiles, err := strconv.Atoi(r.Form.Get("quota_files"))
	if err != nil {
		return 0, 0, fmt.Errorf("invalid quota files: %w", err)
	}
	return quotaSize, quotaFiles, nil
}

func updateRepeaterFormFields(r *http.Request) {
	for k := range r.Form {
		if hasPrefixAndSuffix(k, "public_keys[", "][public_key]") {
			key := r.Form.Get(k)
			if strings.TrimSpace(key) != "" {
				r.Form.Add("public_keys", key)
			}
			continue
		}
		if hasPrefixAndSuffix(k, "tls_certs[", "][tls_cert]") {
			cert := strings.TrimSpace(r.Form.Get(k))
			if cert != "" {
				r.Form.Add("tls_certs", cert)
			}
			continue
		}
		if hasPrefixAndSuffix(k, "virtual_folders[", "][vfolder_path]") {
			base, _ := strings.CutSuffix(k, "[vfolder_path]")
			r.Form.Add("vfolder_path", strings.TrimSpace(r.Form.Get(k)))
			r.Form.Add("vfolder_name", strings.TrimSpace(r.Form.Get(base+"[vfolder_name]")))
			r.Form.Add("vfolder_quota_files", strings.TrimSpace(r.Form.Get(base+"[vfolder_quota_files]")))
			r.Form.Add("vfolder_quota_size", strings.TrimSpace(r.Form.Get(base+"[vfolder_quota_size]")))
			continue
		}
		if hasPrefixAndSuffix(k, "directory_permissions[", "][sub_perm_path]") {
			base, _ := strings.CutSuffix(k, "[sub_perm_path]")
			r.Form.Add("sub_perm_path", strings.TrimSpace(r.Form.Get(k)))
			r.Form["sub_perm_permissions"+strconv.Itoa(len(r.Form["sub_perm_path"])-1)] = r.Form[base+"[sub_perm_permissions][]"]
			continue
		}
		if hasPrefixAndSuffix(k, "directory_patterns[", "][pattern_path]") {
			base, _ := strings.CutSuffix(k, "[pattern_path]")
			r.Form.Add("pattern_path", strings.TrimSpace(r.Form.Get(k)))
			r.Form.Add("patterns", strings.TrimSpace(r.Form.Get(base+"[patterns]")))
			r.Form.Add("pattern_type", strings.TrimSpace(r.Form.Get(base+"[pattern_type]")))
			r.Form.Add("pattern_policy", strings.TrimSpace(r.Form.Get(base+"[pattern_policy]")))
			continue
		}
		if hasPrefixAndSuffix(k, "access_time_restrictions[", "][access_time_day_of_week]") {
			base, _ := strings.CutSuffix(k, "[access_time_day_of_week]")
			r.Form.Add("access_time_day_of_week", strings.TrimSpace(r.Form.Get(k)))
			r.Form.Add("access_time_start", strings.TrimSpace(r.Form.Get(base+"[access_time_start]")))
			r.Form.Add("access_time_end", strings.TrimSpace(r.Form.Get(base+"[access_time_end]")))
			continue
		}
		if hasPrefixAndSuffix(k, "src_bandwidth_limits[", "][bandwidth_limit_sources]") {
			base, _ := strings.CutSuffix(k, "[bandwidth_limit_sources]")
			r.Form.Add("bandwidth_limit_sources", r.Form.Get(k))
			r.Form.Add("upload_bandwidth_source", strings.TrimSpace(r.Form.Get(base+"[upload_bandwidth_source]")))
			r.Form.Add("download_bandwidth_source", strings.TrimSpace(r.Form.Get(base+"[download_bandwidth_source]")))
			continue
		}
		if hasPrefixAndSuffix(k, "template_users[", "][tpl_username]") {
			base, _ := strings.CutSuffix(k, "[tpl_username]")
			r.Form.Add("tpl_username", strings.TrimSpace(r.Form.Get(k)))
			r.Form.Add("tpl_password", strings.TrimSpace(r.Form.Get(base+"[tpl_password]")))
			r.Form.Add("tpl_public_keys", strings.TrimSpace(r.Form.Get(base+"[tpl_public_keys]")))
			continue
		}
	}
}

func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
	user := dataprovider.User{}
	err := r.ParseMultipartForm(maxRequestSize)
	if err != nil {
		return user, util.NewI18nError(err, util.I18nErrorInvalidForm)
	}
	defer r.MultipartForm.RemoveAll() //nolint:errcheck

	updateRepeaterFormFields(r)

	uid, err := strconv.Atoi(r.Form.Get("uid"))
	if err != nil {
		return user, fmt.Errorf("invalid uid: %w", err)
	}
	gid, err := strconv.Atoi(r.Form.Get("gid"))
	if err != nil {
		return user, fmt.Errorf("invalid uid: %w", err)
	}
	maxSessions, err := strconv.Atoi(r.Form.Get("max_sessions"))
	if err != nil {
		return user, fmt.Errorf("invalid max sessions: %w", err)
	}
	quotaSize, quotaFiles, err := getQuotaLimits(r)
	if err != nil {
		return user, err
	}
	bandwidthUL, err := strconv.ParseInt(r.Form.Get("upload_bandwidth"), 10, 64)
	if err != nil {
		return user, fmt.Errorf("invalid upload bandwidth: %w", err)
	}
	bandwidthDL, err := strconv.ParseInt(r.Form.Get("download_bandwidth"), 10, 64)
	if err != nil {
		return user, fmt.Errorf("invalid download bandwidth: %w", err)
	}
	dataTransferUL, dataTransferDL, dataTransferTotal, err := getTransferLimits(r)
	if err != nil {
		return user, err
	}
	status, err := strconv.Atoi(r.Form.Get("status"))
	if err != nil {
		return user, fmt.Errorf("invalid status: %w", err)
	}
	expirationDateMillis := int64(0)
	expirationDateString := r.Form.Get("expiration_date")
	if strings.TrimSpace(expirationDateString) != "" {
		expirationDate, err := time.Parse(webDateTimeFormat, expirationDateString)
		if err != nil {
			return user, err
		}
		expirationDateMillis = util.GetTimeAsMsSinceEpoch(expirationDate)
	}
	fsConfig, err := getFsConfigFromPostFields(r)
	if err != nil {
		return user, err
	}
	filters, err := getFiltersFromUserPostFields(r)
	if err != nil {
		return user, err
	}
	filters.TLSCerts = r.Form["tls_certs"]
	user = dataprovider.User{
		BaseUser: sdk.BaseUser{
			Username:             strings.TrimSpace(r.Form.Get("username")),
			Email:                strings.TrimSpace(r.Form.Get("email")),
			Password:             strings.TrimSpace(r.Form.Get("password")),
			PublicKeys:           r.Form["public_keys"],
			HomeDir:              strings.TrimSpace(r.Form.Get("home_dir")),
			UID:                  uid,
			GID:                  gid,
			Permissions:          getUserPermissionsFromPostFields(r),
			MaxSessions:          maxSessions,
			QuotaSize:            quotaSize,
			QuotaFiles:           quotaFiles,
			UploadBandwidth:      bandwidthUL,
			DownloadBandwidth:    bandwidthDL,
			UploadDataTransfer:   dataTransferUL,
			DownloadDataTransfer: dataTransferDL,
			TotalDataTransfer:    dataTransferTotal,
			Status:               status,
			ExpirationDate:       expirationDateMillis,
			AdditionalInfo:       r.Form.Get("additional_info"),
			Description:          r.Form.Get("description"),
			Role:                 strings.TrimSpace(r.Form.Get("role")),
		},
		Filters: dataprovider.UserFilters{
			BaseUserFilters:       filters,
			RequirePasswordChange: r.Form.Get("require_password_change") != "",
		},
		VirtualFolders: getVirtualFoldersFromPostFields(r),
		FsConfig:       fsConfig,
		Groups:         getGroupsFromUserPostFields(r),
	}
	return user, nil
}

func getGroupFromPostFields(r *http.Request) (dataprovider.Group, error) {
	group := dataprovider.Group{}
	err := r.ParseMultipartForm(maxRequestSize)
	if err != nil {
		return group, util.NewI18nError(err, util.I18nErrorInvalidForm)
	}
	defer r.MultipartForm.RemoveAll() //nolint:errcheck

	updateRepeaterFormFields(r)

	maxSessions, err := strconv.Atoi(r.Form.Get("max_sessions"))
	if err != nil {
		return group, fmt.Errorf("invalid max sessions: %w", err)
	}
	quotaSize, quotaFiles, err := getQuotaLimits(r)
	if err != nil {
		return group, err
	}
	bandwidthUL, err := strconv.ParseInt(r.Form.Get("upload_bandwidth"), 10, 64)
	if err != nil {
		return group, fmt.Errorf("invalid upload bandwidth: %w", err)
	}
	bandwidthDL, err := strconv.ParseInt(r.Form.Get("download_bandwidth"), 10, 64)
	if err != nil {
		return group, fmt.Errorf("invalid download bandwidth: %w", err)
	}
	dataTransferUL, dataTransferDL, dataTransferTotal, err := getTransferLimits(r)
	if err != nil {
		return group, err
	}
	expiresIn, err := strconv.Atoi(r.Form.Get("expires_in"))
	if err != nil {
		return group, fmt.Errorf("invalid expires in: %w", err)
	}
	fsConfig, err := getFsConfigFromPostFields(r)
	if err != nil {
		return group, err
	}
	filters, err := getFiltersFromUserPostFields(r)
	if err != nil {
		return group, err
	}
	group = dataprovider.Group{
		BaseGroup: sdk.BaseGroup{
			Name:        strings.TrimSpace(r.Form.Get("name")),
			Description: r.Form.Get("description"),
		},
		UserSettings: dataprovider.GroupUserSettings{
			BaseGroupUserSettings: sdk.BaseGroupUserSettings{
				HomeDir:              strings.TrimSpace(r.Form.Get("home_dir")),
				MaxSessions:          maxSessions,
				QuotaSize:            quotaSize,
				QuotaFiles:           quotaFiles,
				Permissions:          getSubDirPermissionsFromPostFields(r),
				UploadBandwidth:      bandwidthUL,
				DownloadBandwidth:    bandwidthDL,
				UploadDataTransfer:   dataTransferUL,
				DownloadDataTransfer: dataTransferDL,
				TotalDataTransfer:    dataTransferTotal,
				ExpiresIn:            expiresIn,
				Filters:              filters,
			},
			FsConfig: fsConfig,
		},
		VirtualFolders: getVirtualFoldersFromPostFields(r),
	}
	return group, nil
}

func getKeyValsFromPostFields(r *http.Request, key, val string) []dataprovider.KeyValue {
	var res []dataprovider.KeyValue

	keys := r.Form[key]
	values := r.Form[val]

	for idx, k := range keys {
		v := values[idx]
		if k != "" && v != "" {
			res = append(res, dataprovider.KeyValue{
				Key:   k,
				Value: v,
			})
		}
	}

	return res
}

func getFoldersRetentionFromPostFields(r *http.Request) ([]dataprovider.FolderRetention, error) {
	var res []dataprovider.FolderRetention
	paths := r.Form["folder_retention_path"]
	values := r.Form["folder_retention_val"]

	for idx, p := range paths {
		if p != "" {
			retention, err := strconv.Atoi(values[idx])
			if err != nil {
				return nil, fmt.Errorf("invalid retention for path %q: %w", p, err)
			}
			opts := r.Form["folder_retention_options"+strconv.Itoa(idx)]
			res = append(res, dataprovider.FolderRetention{
				Path:            p,
				Retention:       retention,
				DeleteEmptyDirs: util.Contains(opts, "1"),
			})
		}
	}

	return res, nil
}

func getHTTPPartsFromPostFields(r *http.Request) []dataprovider.HTTPPart {
	var result []dataprovider.HTTPPart

	names := r.Form["http_part_name"]
	files := r.Form["http_part_file"]
	headers := r.Form["http_part_headers"]
	bodies := r.Form["http_part_body"]
	orders := r.Form["http_part_order"]

	for idx, partName := range names {
		if partName != "" {
			order, err := strconv.Atoi(orders[idx])
			if err == nil {
				filePath := files[idx]
				body := bodies[idx]
				concatHeaders := getSliceFromDelimitedValues(headers[idx], "\n")
				var headers []dataprovider.KeyValue
				for _, h := range concatHeaders {
					values := strings.SplitN(h, ":", 2)
					if len(values) > 1 {
						headers = append(headers, dataprovider.KeyValue{
							Key:   strings.TrimSpace(values[0]),
							Value: strings.TrimSpace(values[1]),
						})
					}
				}
				result = append(result, dataprovider.HTTPPart{
					Name:     partName,
					Filepath: filePath,
					Headers:  headers,
					Body:     body,
					Order:    order,
				})
			}
		}
	}

	sort.Slice(result, func(i, j int) bool {
		return result[i].Order < result[j].Order
	})
	return result
}

func updateRepeaterFormActionFields(r *http.Request) {
	for k := range r.Form {
		if hasPrefixAndSuffix(k, "http_headers[", "][http_header_key]") {
			base, _ := strings.CutSuffix(k, "[http_header_key]")
			r.Form.Add("http_header_key", strings.TrimSpace(r.Form.Get(k)))
			r.Form.Add("http_header_value", strings.TrimSpace(r.Form.Get(base+"[http_header_value]")))
			continue
		}
		if hasPrefixAndSuffix(k, "query_parameters[", "][http_query_key]") {
			base, _ := strings.CutSuffix(k, "[http_query_key]")
			r.Form.Add("http_query_key", strings.TrimSpace(r.Form.Get(k)))
			r.Form.Add("http_query_value", strings.TrimSpace(r.Form.Get(base+"[http_query_value]")))
			continue
		}
		if hasPrefixAndSuffix(k, "multipart_body[", "][http_part_name]") {
			base, _ := strings.CutSuffix(k, "[http_part_name]")
			order, _ := strings.CutPrefix(k, "multipart_body[")
			order, _ = strings.CutSuffix(order, "][http_part_name]")
			r.Form.Add("http_part_name", strings.TrimSpace(r.Form.Get(k)))
			r.Form.Add("http_part_file", strings.TrimSpace(r.Form.Get(base+"[http_part_file]")))
			r.Form.Add("http_part_headers", strings.TrimSpace(r.Form.Get(base+"[http_part_headers]")))
			r.Form.Add("http_part_body", strings.TrimSpace(r.Form.Get(base+"[http_part_body]")))
			r.Form.Add("http_part_order", order)
			continue
		}
		if hasPrefixAndSuffix(k, "env_vars[", "][cmd_env_key]") {
			base, _ := strings.CutSuffix(k, "[cmd_env_key]")
			r.Form.Add("cmd_env_key", strings.TrimSpace(r.Form.Get(k)))
			r.Form.Add("cmd_env_value", strings.TrimSpace(r.Form.Get(base+"[cmd_env_value]")))
			continue
		}
		if hasPrefixAndSuffix(k, "data_retention[", "][folder_retention_path]") {
			base, _ := strings.CutSuffix(k, "[folder_retention_path]")
			r.Form.Add("folder_retention_path", strings.TrimSpace(r.Form.Get(k)))
			r.Form.Add("folder_retention_val", strings.TrimSpace(r.Form.Get(base+"[folder_retention_val]")))
			r.Form["folder_retention_options"+strconv.Itoa(len(r.Form["folder_retention_path"])-1)] =
				r.Form[base+"[folder_retention_options][]"]
			continue
		}
		if hasPrefixAndSuffix(k, "fs_rename[", "][fs_rename_source]") {
			base, _ := strings.CutSuffix(k, "[fs_rename_source]")
			r.Form.Add("fs_rename_source", strings.TrimSpace(r.Form.Get(k)))
			r.Form.Add("fs_rename_target", strings.TrimSpace(r.Form.Get(base+"[fs_rename_target]")))
			continue
		}
		if hasPrefixAndSuffix(k, "fs_copy[", "][fs_copy_source]") {
			base, _ := strings.CutSuffix(k, "[fs_copy_source]")
			r.Form.Add("fs_copy_source", strings.TrimSpace(r.Form.Get(k)))
			r.Form.Add("fs_copy_target", strings.TrimSpace(r.Form.Get(base+"[fs_copy_target]")))
			continue
		}
	}
}

func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEventActionOptions, error) {
	updateRepeaterFormActionFields(r)
	httpTimeout, err := strconv.Atoi(r.Form.Get("http_timeout"))
	if err != nil {
		return dataprovider.BaseEventActionOptions{}, fmt.Errorf("invalid http timeout: %w", err)
	}
	cmdTimeout, err := strconv.Atoi(r.Form.Get("cmd_timeout"))
	if err != nil {
		return dataprovider.BaseEventActionOptions{}, fmt.Errorf("invalid command timeout: %w", err)
	}
	foldersRetention, err := getFoldersRetentionFromPostFields(r)
	if err != nil {
		return dataprovider.BaseEventActionOptions{}, err
	}
	fsActionType, err := strconv.Atoi(r.Form.Get("fs_action_type"))
	if err != nil {
		return dataprovider.BaseEventActionOptions{}, fmt.Errorf("invalid fs action type: %w", err)
	}
	pwdExpirationThreshold, err := strconv.Atoi(r.Form.Get("pwd_expiration_threshold"))
	if err != nil {
		return dataprovider.BaseEventActionOptions{}, fmt.Errorf("invalid password expiration threshold: %w", err)
	}
	var disableThreshold, deleteThreshold int
	if val, err := strconv.Atoi(r.Form.Get("inactivity_disable_threshold")); err == nil {
		disableThreshold = val
	}
	if val, err := strconv.Atoi(r.Form.Get("inactivity_delete_threshold")); err == nil {
		deleteThreshold = val
	}
	var emailAttachments []string
	if r.Form.Get("email_attachments") != "" {
		emailAttachments = getSliceFromDelimitedValues(r.Form.Get("email_attachments"), ",")
	}
	var cmdArgs []string
	if r.Form.Get("cmd_arguments") != "" {
		cmdArgs = getSliceFromDelimitedValues(r.Form.Get("cmd_arguments"), ",")
	}
	idpMode := 0
	if r.Form.Get("idp_mode") == "1" {
		idpMode = 1
	}
	emailContentType := 0
	if r.Form.Get("email_content_type") == "1" {
		emailContentType = 1
	}
	options := dataprovider.BaseEventActionOptions{
		HTTPConfig: dataprovider.EventActionHTTPConfig{
			Endpoint:        strings.TrimSpace(r.Form.Get("http_endpoint")),
			Username:        strings.TrimSpace(r.Form.Get("http_username")),
			Password:        getSecretFromFormField(r, "http_password"),
			Headers:         getKeyValsFromPostFields(r, "http_header_key", "http_header_value"),
			Timeout:         httpTimeout,
			SkipTLSVerify:   r.Form.Get("http_skip_tls_verify") != "",
			Method:          r.Form.Get("http_method"),
			QueryParameters: getKeyValsFromPostFields(r, "http_query_key", "http_query_value"),
			Body:            r.Form.Get("http_body"),
			Parts:           getHTTPPartsFromPostFields(r),
		},
		CmdConfig: dataprovider.EventActionCommandConfig{
			Cmd:     strings.TrimSpace(r.Form.Get("cmd_path")),
			Args:    cmdArgs,
			Timeout: cmdTimeout,
			EnvVars: getKeyValsFromPostFields(r, "cmd_env_key", "cmd_env_value"),
		},
		EmailConfig: dataprovider.EventActionEmailConfig{
			Recipients:  getSliceFromDelimitedValues(r.Form.Get("email_recipients"), ","),
			Bcc:         getSliceFromDelimitedValues(r.Form.Get("email_bcc"), ","),
			Subject:     r.Form.Get("email_subject"),
			ContentType: emailContentType,
			Body:        r.Form.Get("email_body"),
			Attachments: emailAttachments,
		},
		RetentionConfig: dataprovider.EventActionDataRetentionConfig{
			Folders: foldersRetention,
		},
		FsConfig: dataprovider.EventActionFilesystemConfig{
			Type:    fsActionType,
			Renames: getKeyValsFromPostFields(r, "fs_rename_source", "fs_rename_target"),
			Deletes: getSliceFromDelimitedValues(r.Form.Get("fs_delete_paths"), ","),
			MkDirs:  getSliceFromDelimitedValues(r.Form.Get("fs_mkdir_paths"), ","),
			Exist:   getSliceFromDelimitedValues(r.Form.Get("fs_exist_paths"), ","),
			Copy:    getKeyValsFromPostFields(r, "fs_copy_source", "fs_copy_target"),
			Compress: dataprovider.EventActionFsCompress{
				Name:  strings.TrimSpace(r.Form.Get("fs_compress_name")),
				Paths: getSliceFromDelimitedValues(r.Form.Get("fs_compress_paths"), ","),
			},
		},
		PwdExpirationConfig: dataprovider.EventActionPasswordExpiration{
			Threshold: pwdExpirationThreshold,
		},
		UserInactivityConfig: dataprovider.EventActionUserInactivity{
			DisableThreshold: disableThreshold,
			DeleteThreshold:  deleteThreshold,
		},
		IDPConfig: dataprovider.EventActionIDPAccountCheck{
			Mode:          idpMode,
			TemplateUser:  strings.TrimSpace(r.Form.Get("idp_user")),
			TemplateAdmin: strings.TrimSpace(r.Form.Get("idp_admin")),
		},
	}
	return options, nil
}

func getEventActionFromPostFields(r *http.Request) (dataprovider.BaseEventAction, error) {
	err := r.ParseForm()
	if err != nil {
		return dataprovider.BaseEventAction{}, util.NewI18nError(err, util.I18nErrorInvalidForm)
	}
	actionType, err := strconv.Atoi(r.Form.Get("type"))
	if err != nil {
		return dataprovider.BaseEventAction{}, fmt.Errorf("invalid action type: %w", err)
	}
	options, err := getEventActionOptionsFromPostFields(r)
	if err != nil {
		return dataprovider.BaseEventAction{}, err
	}
	action := dataprovider.BaseEventAction{
		Name:        strings.TrimSpace(r.Form.Get("name")),
		Description: r.Form.Get("description"),
		Type:        actionType,
		Options:     options,
	}
	return action, nil
}

func getIDPLoginEventFromPostField(r *http.Request) int {
	switch r.Form.Get("idp_login_event") {
	case "1":
		return 1
	case "2":
		return 2
	default:
		return 0
	}
}

func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventConditions, error) {
	var schedules []dataprovider.Schedule
	var names, groupNames, roleNames, fsPaths []dataprovider.ConditionPattern

	scheduleHours := r.Form["schedule_hour"]
	scheduleDayOfWeeks := r.Form["schedule_day_of_week"]
	scheduleDayOfMonths := r.Form["schedule_day_of_month"]
	scheduleMonths := r.Form["schedule_month"]

	for idx, hour := range scheduleHours {
		if hour != "" {
			schedules = append(schedules, dataprovider.Schedule{
				Hours:      hour,
				DayOfWeek:  scheduleDayOfWeeks[idx],
				DayOfMonth: scheduleDayOfMonths[idx],
				Month:      scheduleMonths[idx],
			})
		}
	}

	for idx, name := range r.Form["name_pattern"] {
		if name != "" {
			names = append(names, dataprovider.ConditionPattern{
				Pattern:      name,
				InverseMatch: r.Form["type_name_pattern"][idx] == inversePatternType,
			})
		}
	}

	for idx, name := range r.Form["group_name_pattern"] {
		if name != "" {
			groupNames = append(groupNames, dataprovider.ConditionPattern{
				Pattern:      name,
				InverseMatch: r.Form["type_group_name_pattern"][idx] == inversePatternType,
			})
		}
	}

	for idx, name := range r.Form["role_name_pattern"] {
		if name != "" {
			roleNames = append(roleNames, dataprovider.ConditionPattern{
				Pattern:      name,
				InverseMatch: r.Form["type_role_name_pattern"][idx] == inversePatternType,
			})
		}
	}

	for idx, name := range r.Form["fs_path_pattern"] {
		if name != "" {
			fsPaths = append(fsPaths, dataprovider.ConditionPattern{
				Pattern:      name,
				InverseMatch: r.Form["type_fs_path_pattern"][idx] == inversePatternType,
			})
		}
	}

	minFileSize, err := util.ParseBytes(r.Form.Get("fs_min_size"))
	if err != nil {
		return dataprovider.EventConditions{}, util.NewI18nError(fmt.Errorf("invalid min file size: %w", err), util.I18nErrorInvalidMinSize)
	}
	maxFileSize, err := util.ParseBytes(r.Form.Get("fs_max_size"))
	if err != nil {
		return dataprovider.EventConditions{}, util.NewI18nError(fmt.Errorf("invalid max file size: %w", err), util.I18nErrorInvalidMaxSize)
	}
	conditions := dataprovider.EventConditions{
		FsEvents:       r.Form["fs_events"],
		ProviderEvents: r.Form["provider_events"],
		IDPLoginEvent:  getIDPLoginEventFromPostField(r),
		Schedules:      schedules,
		Options: dataprovider.ConditionOptions{
			Names:               names,
			GroupNames:          groupNames,
			RoleNames:           roleNames,
			FsPaths:             fsPaths,
			Protocols:           r.Form["fs_protocols"],
			ProviderObjects:     r.Form["provider_objects"],
			MinFileSize:         minFileSize,
			MaxFileSize:         maxFileSize,
			ConcurrentExecution: r.Form.Get("concurrent_execution") != "",
		},
	}
	return conditions, nil
}

func getEventRuleActionsFromPostFields(r *http.Request) []dataprovider.EventAction {
	var actions []dataprovider.EventAction

	names := r.Form["action_name"]
	orders := r.Form["action_order"]

	for idx, name := range names {
		if name != "" {
			order, err := strconv.Atoi(orders[idx])
			if err == nil {
				options := r.Form["action_options"+strconv.Itoa(idx)]
				actions = append(actions, dataprovider.EventAction{
					BaseEventAction: dataprovider.BaseEventAction{
						Name: name,
					},
					Order: order + 1,
					Options: dataprovider.EventActionOptions{
						IsFailureAction: util.Contains(options, "1"),
						StopOnFailure:   util.Contains(options, "2"),
						ExecuteSync:     util.Contains(options, "3"),
					},
				})
			}
		}
	}

	return actions
}

func updateRepeaterFormRuleFields(r *http.Request) {
	for k := range r.Form {
		if hasPrefixAndSuffix(k, "schedules[", "][schedule_hour]") {
			base, _ := strings.CutSuffix(k, "[schedule_hour]")
			r.Form.Add("schedule_hour", strings.TrimSpace(r.Form.Get(k)))
			r.Form.Add("schedule_day_of_week", strings.TrimSpace(r.Form.Get(base+"[schedule_day_of_week]")))
			r.Form.Add("schedule_day_of_month", strings.TrimSpace(r.Form.Get(base+"[schedule_day_of_month]")))
			r.Form.Add("schedule_month", strings.TrimSpace(r.Form.Get(base+"[schedule_month]")))
			continue
		}
		if hasPrefixAndSuffix(k, "name_filters[", "][name_pattern]") {
			base, _ := strings.CutSuffix(k, "[name_pattern]")
			r.Form.Add("name_pattern", strings.TrimSpace(r.Form.Get(k)))
			r.Form.Add("type_name_pattern", strings.TrimSpace(r.Form.Get(base+"[type_name_pattern]")))
			continue
		}
		if hasPrefixAndSuffix(k, "group_name_filters[", "][group_name_pattern]") {
			base, _ := strings.CutSuffix(k, "[group_name_pattern]")
			r.Form.Add("group_name_pattern", strings.TrimSpace(r.Form.Get(k)))
			r.Form.Add("type_group_name_pattern", strings.TrimSpace(r.Form.Get(base+"[type_group_name_pattern]")))
			continue
		}
		if hasPrefixAndSuffix(k, "role_name_filters[", "][role_name_pattern]") {
			base, _ := strings.CutSuffix(k, "[role_name_pattern]")
			r.Form.Add("role_name_pattern", strings.TrimSpace(r.Form.Get(k)))
			r.Form.Add("type_role_name_pattern", strings.TrimSpace(r.Form.Get(base+"[type_role_name_pattern]")))
			continue
		}
		if hasPrefixAndSuffix(k, "path_filters[", "][fs_path_pattern]") {
			base, _ := strings.CutSuffix(k, "[fs_path_pattern]")
			r.Form.Add("fs_path_pattern", strings.TrimSpace(r.Form.Get(k)))
			r.Form.Add("type_fs_path_pattern", strings.TrimSpace(r.Form.Get(base+"[type_fs_path_pattern]")))
			continue
		}
		if hasPrefixAndSuffix(k, "actions[", "][action_name]") {
			base, _ := strings.CutSuffix(k, "[action_name]")
			order, _ := strings.CutPrefix(k, "actions[")
			order, _ = strings.CutSuffix(order, "][action_name]")
			r.Form.Add("action_name", strings.TrimSpace(r.Form.Get(k)))
			r.Form["action_options"+strconv.Itoa(len(r.Form["action_name"])-1)] = r.Form[base+"[action_options][]"]
			r.Form.Add("action_order", order)
			continue
		}
	}
}

func getEventRuleFromPostFields(r *http.Request) (dataprovider.EventRule, error) {
	err := r.ParseForm()
	if err != nil {
		return dataprovider.EventRule{}, util.NewI18nError(err, util.I18nErrorInvalidForm)
	}
	updateRepeaterFormRuleFields(r)
	status, err := strconv.Atoi(r.Form.Get("status"))
	if err != nil {
		return dataprovider.EventRule{}, fmt.Errorf("invalid status: %w", err)
	}
	trigger, err := strconv.Atoi(r.Form.Get("trigger"))
	if err != nil {
		return dataprovider.EventRule{}, fmt.Errorf("invalid trigger: %w", err)
	}
	conditions, err := getEventRuleConditionsFromPostFields(r)
	if err != nil {
		return dataprovider.EventRule{}, err
	}
	rule := dataprovider.EventRule{
		Name:        strings.TrimSpace(r.Form.Get("name")),
		Status:      status,
		Description: r.Form.Get("description"),
		Trigger:     trigger,
		Conditions:  conditions,
		Actions:     getEventRuleActionsFromPostFields(r),
	}
	return rule, nil
}

func getRoleFromPostFields(r *http.Request) (dataprovider.Role, error) {
	err := r.ParseForm()
	if err != nil {
		return dataprovider.Role{}, util.NewI18nError(err, util.I18nErrorInvalidForm)
	}

	return dataprovider.Role{
		Name:        strings.TrimSpace(r.Form.Get("name")),
		Description: r.Form.Get("description"),
	}, nil
}

func getIPListEntryFromPostFields(r *http.Request, listType dataprovider.IPListType) (dataprovider.IPListEntry, error) {
	err := r.ParseForm()
	if err != nil {
		return dataprovider.IPListEntry{}, util.NewI18nError(err, util.I18nErrorInvalidForm)
	}
	var mode int
	if listType == dataprovider.IPListTypeDefender {
		mode, err = strconv.Atoi(r.Form.Get("mode"))
		if err != nil {
			return dataprovider.IPListEntry{}, fmt.Errorf("invalid mode: %w", err)
		}
	} else {
		mode = 1
	}
	protocols := 0
	for _, proto := range r.Form["protocols"] {
		p, err := strconv.Atoi(proto)
		if err == nil {
			protocols += p
		}
	}

	return dataprovider.IPListEntry{
		IPOrNet:     strings.TrimSpace(r.Form.Get("ipornet")),
		Mode:        mode,
		Protocols:   protocols,
		Description: r.Form.Get("description"),
	}, nil
}

func getSFTPConfigsFromPostFields(r *http.Request) *dataprovider.SFTPDConfigs {
	return &dataprovider.SFTPDConfigs{
		HostKeyAlgos:   r.Form["sftp_host_key_algos"],
		PublicKeyAlgos: r.Form["sftp_pub_key_algos"],
		KexAlgorithms:  r.Form["sftp_kex_algos"],
		Ciphers:        r.Form["sftp_ciphers"],
		MACs:           r.Form["sftp_macs"],
	}
}

func getACMEConfigsFromPostFields(r *http.Request) *dataprovider.ACMEConfigs {
	port, err := strconv.Atoi(r.Form.Get("acme_port"))
	if err != nil {
		port = 80
	}
	var protocols int
	for _, val := range r.Form["acme_protocols"] {
		switch val {
		case "1":
			protocols++
		case "2":
			protocols += 2
		case "3":
			protocols += 4
		}
	}

	return &dataprovider.ACMEConfigs{
		Domain:          strings.TrimSpace(r.Form.Get("acme_domain")),
		Email:           strings.TrimSpace(r.Form.Get("acme_email")),
		HTTP01Challenge: dataprovider.ACMEHTTP01Challenge{Port: port},
		Protocols:       protocols,
	}
}

func getSMTPConfigsFromPostFields(r *http.Request) *dataprovider.SMTPConfigs {
	port, err := strconv.Atoi(r.Form.Get("smtp_port"))
	if err != nil {
		port = 587
	}
	authType, err := strconv.Atoi(r.Form.Get("smtp_auth"))
	if err != nil {
		authType = 0
	}
	encryption, err := strconv.Atoi(r.Form.Get("smtp_encryption"))
	if err != nil {
		encryption = 0
	}
	debug := 0
	if r.Form.Get("smtp_debug") != "" {
		debug = 1
	}
	oauth2Provider := 0
	if r.Form.Get("smtp_oauth2_provider") == "1" {
		oauth2Provider = 1
	}
	return &dataprovider.SMTPConfigs{
		Host:       strings.TrimSpace(r.Form.Get("smtp_host")),
		Port:       port,
		From:       strings.TrimSpace(r.Form.Get("smtp_from")),
		User:       strings.TrimSpace(r.Form.Get("smtp_username")),
		Password:   getSecretFromFormField(r, "smtp_password"),
		AuthType:   authType,
		Encryption: encryption,
		Domain:     strings.TrimSpace(r.Form.Get("smtp_domain")),
		Debug:      debug,
		OAuth2: dataprovider.SMTPOAuth2{
			Provider:     oauth2Provider,
			Tenant:       strings.TrimSpace(r.Form.Get("smtp_oauth2_tenant")),
			ClientID:     strings.TrimSpace(r.Form.Get("smtp_oauth2_client_id")),
			ClientSecret: getSecretFromFormField(r, "smtp_oauth2_client_secret"),
			RefreshToken: getSecretFromFormField(r, "smtp_oauth2_refresh_token"),
		},
	}
}

func (s *httpdServer) handleWebAdminForgotPwd(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	if !smtp.IsEnabled() {
		s.renderNotFoundPage(w, r, errors.New("this page does not exist"))
		return
	}
	s.renderForgotPwdPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
}

func (s *httpdServer) handleWebAdminForgotPwdPost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)

	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	err := r.ParseForm()
	if err != nil {
		s.renderForgotPwdPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
		return
	}
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	err = handleForgotPassword(r, r.Form.Get("username"), true)
	if err != nil {
		s.renderForgotPwdPage(w, r, util.NewI18nError(err, util.I18nErrorPwdResetGeneric), ipAddr)
		return
	}
	http.Redirect(w, r, webAdminResetPwdPath, http.StatusFound)
}

func (s *httpdServer) handleWebAdminPasswordReset(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
	if !smtp.IsEnabled() {
		s.renderNotFoundPage(w, r, errors.New("this page does not exist"))
		return
	}
	s.renderResetPwdPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
}

func (s *httpdServer) handleWebAdminTwoFactor(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	s.renderTwoFactorPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
}

func (s *httpdServer) handleWebAdminTwoFactorRecovery(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	s.renderTwoFactorRecoveryPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
}

func (s *httpdServer) handleWebAdminMFA(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	s.renderMFAPage(w, r)
}

func (s *httpdServer) handleWebAdminProfile(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	s.renderProfilePage(w, r, nil)
}

func (s *httpdServer) handleWebAdminChangePwd(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	s.renderChangePasswordPage(w, r, nil)
}

func (s *httpdServer) handleWebAdminProfilePost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	err := r.ParseForm()
	if err != nil {
		s.renderProfilePage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
		return
	}
	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderProfilePage(w, r, util.NewI18nError(err, util.I18nErrorInvalidToken))
		return
	}
	admin, err := dataprovider.AdminExists(claims.Username)
	if err != nil {
		s.renderProfilePage(w, r, err)
		return
	}
	admin.Filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != ""
	admin.Email = r.Form.Get("email")
	admin.Description = r.Form.Get("description")
	err = dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, ipAddr, admin.Role)
	if err != nil {
		s.renderProfilePage(w, r, err)
		return
	}
	s.renderMessagePage(w, r, util.I18nProfileTitle, http.StatusOK, nil, util.I18nProfileUpdated)
}

func (s *httpdServer) handleWebMaintenance(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	s.renderMaintenancePage(w, r, nil)
}

func (s *httpdServer) handleWebRestore(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, MaxRestoreSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	err = r.ParseMultipartForm(MaxRestoreSize)
	if err != nil {
		s.renderMaintenancePage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
		return
	}
	defer r.MultipartForm.RemoveAll() //nolint:errcheck

	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	restoreMode, err := strconv.Atoi(r.Form.Get("mode"))
	if err != nil {
		s.renderMaintenancePage(w, r, err)
		return
	}
	scanQuota, err := strconv.Atoi(r.Form.Get("quota"))
	if err != nil {
		s.renderMaintenancePage(w, r, err)
		return
	}
	backupFile, _, err := r.FormFile("backup_file")
	if err != nil {
		s.renderMaintenancePage(w, r, util.NewI18nError(err, util.I18nErrorBackupFile))
		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")
		}
		s.renderMaintenancePage(w, r, util.NewI18nError(err, util.I18nErrorBackupFile))
		return
	}

	if err := restoreBackup(backupContent, "", scanQuota, restoreMode, claims.Username, ipAddr, claims.Role); err != nil {
		s.renderMaintenancePage(w, r, util.NewI18nError(err, util.I18nErrorRestore))
		return
	}

	s.renderMessagePage(w, r, util.I18nMaintenanceTitle, http.StatusOK, nil, util.I18nBackupOK)
}

func getAllAdmins(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		sendAPIResponse(w, r, nil, util.I18nErrorInvalidToken, http.StatusForbidden)
		return
	}

	dataGetter := func(limit, offset int) ([]byte, int, error) {
		results, err := dataprovider.GetAdmins(limit, offset, dataprovider.OrderASC)
		if err != nil {
			return nil, 0, err
		}
		data, err := json.Marshal(results)
		return data, len(results), err
	}

	streamJSONArray(w, defaultQueryLimit, dataGetter)
}

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)
}

func (s *httpdServer) handleWebAdminSetupGet(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
	if dataprovider.HasAdmin() {
		http.Redirect(w, r, webAdminLoginPath, http.StatusFound)
		return
	}
	s.renderAdminSetupPage(w, r, "", util.GetIPFromRemoteAddress(r.RemoteAddr), nil)
}

func (s *httpdServer) handleWebAddAdminGet(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	admin := &dataprovider.Admin{
		Status:      1,
		Permissions: []string{dataprovider.PermAdminAny},
	}
	s.renderAddUpdateAdminPage(w, r, admin, nil, true)
}

func (s *httpdServer) handleWebUpdateAdminGet(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	username := getURLParam(r, "username")
	admin, err := dataprovider.AdminExists(username)
	if err == nil {
		s.renderAddUpdateAdminPage(w, r, &admin, nil, false)
	} else if errors.Is(err, util.ErrNotFound) {
		s.renderNotFoundPage(w, r, err)
	} else {
		s.renderInternalServerErrorPage(w, r, err)
	}
}

func (s *httpdServer) handleWebAddAdminPost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	admin, err := getAdminFromPostFields(r)
	if err != nil {
		s.renderAddUpdateAdminPage(w, r, &admin, err, true)
		return
	}
	if admin.Password == "" && s.binding.isWebAdminLoginFormDisabled() {
		admin.Password = util.GenerateUniqueID()
	}
	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	err = dataprovider.AddAdmin(&admin, claims.Username, ipAddr, claims.Role)
	if err != nil {
		s.renderAddUpdateAdminPage(w, r, &admin, err, true)
		return
	}
	http.Redirect(w, r, webAdminsPath, http.StatusSeeOther)
}

func (s *httpdServer) 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 errors.Is(err, util.ErrNotFound) {
		s.renderNotFoundPage(w, r, err)
		return
	} else if err != nil {
		s.renderInternalServerErrorPage(w, r, err)
		return
	}

	updatedAdmin, err := getAdminFromPostFields(r)
	if err != nil {
		s.renderAddUpdateAdminPage(w, r, &updatedAdmin, err, false)
		return
	}
	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	updatedAdmin.ID = admin.ID
	updatedAdmin.Username = admin.Username
	if updatedAdmin.Password == "" {
		updatedAdmin.Password = admin.Password
	}
	updatedAdmin.Filters.TOTPConfig = admin.Filters.TOTPConfig
	updatedAdmin.Filters.RecoveryCodes = admin.Filters.RecoveryCodes
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		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,
				util.NewI18nError(errors.New("you cannot remove these permissions to yourself"),
					util.I18nErrorAdminSelfPerms,
				), false)
			return
		}
		if updatedAdmin.Status == 0 {
			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,
				util.NewI18nError(
					errors.New("you cannot add/change your role"),
					util.I18nErrorAdminSelfRole,
				), false)
			return
		}
		updatedAdmin.Filters.RequirePasswordChange = admin.Filters.RequirePasswordChange
		updatedAdmin.Filters.RequireTwoFactor = admin.Filters.RequireTwoFactor
	}
	err = dataprovider.UpdateAdmin(&updatedAdmin, claims.Username, ipAddr, claims.Role)
	if err != nil {
		s.renderAddUpdateAdminPage(w, r, &updatedAdmin, err, false)
		return
	}
	http.Redirect(w, r, webAdminsPath, http.StatusSeeOther)
}

func (s *httpdServer) handleWebDefenderPage(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	data := defenderHostsPage{
		basePage:         s.getBasePageData(util.I18nDefenderTitle, webDefenderPath, r),
		DefenderHostsURL: webDefenderHostsPath,
	}

	renderAdminTemplate(w, templateDefender, data)
}

func getAllUsers(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		sendAPIResponse(w, r, nil, util.I18nErrorInvalidToken, http.StatusForbidden)
		return
	}

	dataGetter := func(limit, offset int) ([]byte, int, error) {
		results, err := dataprovider.GetUsers(limit, offset, dataprovider.OrderASC, claims.Role)
		if err != nil {
			return nil, 0, err
		}
		data, err := json.Marshal(results)
		return data, len(results), err
	}

	streamJSONArray(w, defaultQueryLimit, dataGetter)
}

func (s *httpdServer) handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	data := s.getBasePageData(util.I18nUsersTitle, webUsersPath, r)
	renderAdminTemplate(w, templateUsers, data)
}

func (s *httpdServer) handleWebTemplateFolderGet(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	if r.URL.Query().Get("from") != "" {
		name := r.URL.Query().Get("from")
		folder, err := dataprovider.GetFolderByName(name)
		if err == nil {
			folder.FsConfig.SetEmptySecrets()
			s.renderFolderPage(w, r, folder, folderPageModeTemplate, nil)
		} else if errors.Is(err, util.ErrNotFound) {
			s.renderNotFoundPage(w, r, err)
		} else {
			s.renderInternalServerErrorPage(w, r, err)
		}
	} else {
		folder := vfs.BaseVirtualFolder{}
		s.renderFolderPage(w, r, folder, folderPageModeTemplate, nil)
	}
}

func (s *httpdServer) handleWebTemplateFolderPost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	templateFolder := vfs.BaseVirtualFolder{}
	err = r.ParseMultipartForm(maxRequestSize)
	if err != nil {
		s.renderMessagePage(w, r, util.I18nTemplateFolderTitle, http.StatusBadRequest, util.NewI18nError(err, util.I18nErrorInvalidForm), "")
		return
	}
	defer r.MultipartForm.RemoveAll() //nolint:errcheck

	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}

	templateFolder.MappedPath = r.Form.Get("mapped_path")
	templateFolder.Description = r.Form.Get("description")
	fsConfig, err := getFsConfigFromPostFields(r)
	if err != nil {
		s.renderMessagePage(w, r, util.I18nTemplateFolderTitle, 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 {
			s.renderMessagePage(w, r, util.I18nTemplateFolderTitle, http.StatusBadRequest, err, "")
			return
		}
		dump.Folders = append(dump.Folders, f)
	}

	if len(dump.Folders) == 0 {
		s.renderMessagePage(w, r, util.I18nTemplateFolderTitle, http.StatusBadRequest,
			util.NewI18nError(
				errors.New("no valid folder defined, unable to complete the requested action"),
				util.I18nErrorFolderTemplate,
			), "")
		return
	}
	if r.Form.Get("form_action") == "export_from_template" {
		w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"sftpgo-%v-folders-from-template.json\"",
			len(dump.Folders)))
		render.JSON(w, r, dump)
		return
	}
	if err = RestoreFolders(dump.Folders, "", 1, 0, claims.Username, ipAddr, claims.Role); err != nil {
		s.renderMessagePage(w, r, util.I18nTemplateFolderTitle, getRespStatus(err), err, "")
		return
	}
	http.Redirect(w, r, webFoldersPath, http.StatusSeeOther)
}

func (s *httpdServer) handleWebTemplateUserGet(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	tokenAdmin := getAdminFromToken(r)
	admin, err := dataprovider.AdminExists(tokenAdmin.Username)
	if err != nil {
		s.renderInternalServerErrorPage(w, r, fmt.Errorf("unable to get the admin %q: %w", tokenAdmin.Username, err))
		return
	}
	if r.URL.Query().Get("from") != "" {
		username := r.URL.Query().Get("from")
		user, err := dataprovider.UserExists(username, admin.Role)
		if err == nil {
			user.SetEmptySecrets()
			user.PublicKeys = nil
			user.Email = ""
			user.Description = ""
			if user.ExpirationDate == 0 && admin.Filters.Preferences.DefaultUsersExpiration > 0 {
				user.ExpirationDate = util.GetTimeAsMsSinceEpoch(time.Now().Add(24 * time.Hour * time.Duration(admin.Filters.Preferences.DefaultUsersExpiration)))
			}
			s.renderUserPage(w, r, &user, userPageModeTemplate, nil, &admin)
		} else if errors.Is(err, util.ErrNotFound) {
			s.renderNotFoundPage(w, r, err)
		} else {
			s.renderInternalServerErrorPage(w, r, err)
		}
	} else {
		user := dataprovider.User{BaseUser: sdk.BaseUser{
			Status: 1,
			Permissions: map[string][]string{
				"/": {dataprovider.PermAny},
			},
		}}
		if admin.Filters.Preferences.DefaultUsersExpiration > 0 {
			user.ExpirationDate = util.GetTimeAsMsSinceEpoch(time.Now().Add(24 * time.Hour * time.Duration(admin.Filters.Preferences.DefaultUsersExpiration)))
		}
		s.renderUserPage(w, r, &user, userPageModeTemplate, nil, &admin)
	}
}

func (s *httpdServer) handleWebTemplateUserPost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	templateUser, err := getUserFromPostFields(r)
	if err != nil {
		s.renderMessagePage(w, r, util.I18nTemplateUserTitle, http.StatusBadRequest, err, "")
		return
	}
	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		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 {
			s.renderMessagePage(w, r, util.I18nTemplateUserTitle, http.StatusBadRequest, err, "")
			return
		}
		// to create a template the "manage_system" permission is required, so role admins cannot use
		// this method, we don't need to force the role
		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 {
		s.renderMessagePage(w, r, util.I18nTemplateUserTitle,
			http.StatusBadRequest, util.NewI18nError(
				errors.New("no valid user defined, unable to complete the requested action"),
				util.I18nErrorUserTemplate,
			), "")
		return
	}
	if r.Form.Get("form_action") == "export_from_template" {
		w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"sftpgo-%v-users-from-template.json\"",
			len(dump.Users)))
		render.JSON(w, r, dump)
		return
	}
	if err = RestoreUsers(dump.Users, "", 1, 0, claims.Username, ipAddr, claims.Role); err != nil {
		s.renderMessagePage(w, r, util.I18nTemplateUserTitle, getRespStatus(err), err, "")
		return
	}
	http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
}

func (s *httpdServer) handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	tokenAdmin := getAdminFromToken(r)
	admin, err := dataprovider.AdminExists(tokenAdmin.Username)
	if err != nil {
		s.renderInternalServerErrorPage(w, r, fmt.Errorf("unable to get the admin %q: %w", tokenAdmin.Username, err))
		return
	}
	user := dataprovider.User{BaseUser: sdk.BaseUser{
		Status: 1,
		Permissions: map[string][]string{
			"/": {dataprovider.PermAny},
		}},
	}
	if admin.Filters.Preferences.DefaultUsersExpiration > 0 {
		user.ExpirationDate = util.GetTimeAsMsSinceEpoch(time.Now().Add(24 * time.Hour * time.Duration(admin.Filters.Preferences.DefaultUsersExpiration)))
	}
	s.renderUserPage(w, r, &user, userPageModeAdd, nil, &admin)
}

func (s *httpdServer) handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	username := getURLParam(r, "username")
	user, err := dataprovider.UserExists(username, claims.Role)
	if err == nil {
		s.renderUserPage(w, r, &user, userPageModeUpdate, nil, nil)
	} else if errors.Is(err, util.ErrNotFound) {
		s.renderNotFoundPage(w, r, err)
	} else {
		s.renderInternalServerErrorPage(w, r, err)
	}
}

func (s *httpdServer) handleWebAddUserPost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	user, err := getUserFromPostFields(r)
	if err != nil {
		s.renderUserPage(w, r, &user, userPageModeAdd, err, nil)
		return
	}
	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	user = getUserFromTemplate(user, userTemplateFields{
		Username:   user.Username,
		Password:   user.Password,
		PublicKeys: user.PublicKeys,
	})
	if claims.Role != "" {
		user.Role = claims.Role
	}
	user.Filters.RecoveryCodes = nil
	user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
		Enabled: false,
	}
	err = dataprovider.AddUser(&user, claims.Username, ipAddr, claims.Role)
	if err != nil {
		s.renderUserPage(w, r, &user, userPageModeAdd, err, nil)
		return
	}
	http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
}

func (s *httpdServer) handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	username := getURLParam(r, "username")
	user, err := dataprovider.UserExists(username, claims.Role)
	if errors.Is(err, util.ErrNotFound) {
		s.renderNotFoundPage(w, r, err)
		return
	} else if err != nil {
		s.renderInternalServerErrorPage(w, r, err)
		return
	}
	updatedUser, err := getUserFromPostFields(r)
	if err != nil {
		s.renderUserPage(w, r, &user, userPageModeUpdate, err, nil)
		return
	}
	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	updatedUser.ID = user.ID
	updatedUser.Username = user.Username
	updatedUser.Filters.RecoveryCodes = user.Filters.RecoveryCodes
	updatedUser.Filters.TOTPConfig = user.Filters.TOTPConfig
	updatedUser.LastPasswordChange = user.LastPasswordChange
	updatedUser.SetEmptySecretsIfNil()
	if updatedUser.Password == redactedSecret {
		updatedUser.Password = user.Password
	}
	updateEncryptedSecrets(&updatedUser.FsConfig, user.FsConfig.S3Config.AccessSecret, user.FsConfig.AzBlobConfig.AccountKey,
		user.FsConfig.AzBlobConfig.SASURL, user.FsConfig.GCSConfig.Credentials, user.FsConfig.CryptConfig.Passphrase,
		user.FsConfig.SFTPConfig.Password, user.FsConfig.SFTPConfig.PrivateKey, user.FsConfig.SFTPConfig.KeyPassphrase,
		user.FsConfig.HTTPConfig.Password, user.FsConfig.HTTPConfig.APIKey)

	updatedUser = getUserFromTemplate(updatedUser, userTemplateFields{
		Username:   updatedUser.Username,
		Password:   updatedUser.Password,
		PublicKeys: updatedUser.PublicKeys,
	})
	if claims.Role != "" {
		updatedUser.Role = claims.Role
	}

	err = dataprovider.UpdateUser(&updatedUser, claims.Username, ipAddr, claims.Role)
	if err != nil {
		s.renderUserPage(w, r, &updatedUser, userPageModeUpdate, err, nil)
		return
	}
	if r.Form.Get("disconnect") != "" {
		disconnectUser(user.Username, claims.Username, claims.Role)
	}
	http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
}

func (s *httpdServer) handleWebGetStatus(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	data := statusPage{
		basePage: s.getBasePageData(util.I18nStatusTitle, webStatusPath, r),
		Status:   getServicesStatus(),
	}
	renderAdminTemplate(w, templateStatus, data)
}

func (s *httpdServer) handleWebGetConnections(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}

	data := s.getBasePageData(util.I18nSessionsTitle, webConnectionsPath, r)
	renderAdminTemplate(w, templateConnections, data)
}

func (s *httpdServer) handleWebAddFolderGet(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	s.renderFolderPage(w, r, vfs.BaseVirtualFolder{}, folderPageModeAdd, nil)
}

func (s *httpdServer) handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	folder := vfs.BaseVirtualFolder{}
	err = r.ParseMultipartForm(maxRequestSize)
	if err != nil {
		s.renderFolderPage(w, r, folder, folderPageModeAdd, util.NewI18nError(err, util.I18nErrorInvalidForm))
		return
	}
	defer r.MultipartForm.RemoveAll() //nolint:errcheck

	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	folder.MappedPath = strings.TrimSpace(r.Form.Get("mapped_path"))
	folder.Name = strings.TrimSpace(r.Form.Get("name"))
	folder.Description = r.Form.Get("description")
	fsConfig, err := getFsConfigFromPostFields(r)
	if err != nil {
		s.renderFolderPage(w, r, folder, folderPageModeAdd, err)
		return
	}
	folder.FsConfig = fsConfig
	folder = getFolderFromTemplate(folder, folder.Name)

	err = dataprovider.AddFolder(&folder, claims.Username, ipAddr, claims.Role)
	if err == nil {
		http.Redirect(w, r, webFoldersPath, http.StatusSeeOther)
	} else {
		s.renderFolderPage(w, r, folder, folderPageModeAdd, err)
	}
}

func (s *httpdServer) handleWebUpdateFolderGet(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	name := getURLParam(r, "name")
	folder, err := dataprovider.GetFolderByName(name)
	if err == nil {
		s.renderFolderPage(w, r, folder, folderPageModeUpdate, nil)
	} else if errors.Is(err, util.ErrNotFound) {
		s.renderNotFoundPage(w, r, err)
	} else {
		s.renderInternalServerErrorPage(w, r, err)
	}
}

func (s *httpdServer) handleWebUpdateFolderPost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	name := getURLParam(r, "name")
	folder, err := dataprovider.GetFolderByName(name)
	if errors.Is(err, util.ErrNotFound) {
		s.renderNotFoundPage(w, r, err)
		return
	} else if err != nil {
		s.renderInternalServerErrorPage(w, r, err)
		return
	}

	err = r.ParseMultipartForm(maxRequestSize)
	if err != nil {
		s.renderFolderPage(w, r, folder, folderPageModeUpdate, util.NewI18nError(err, util.I18nErrorInvalidForm))
		return
	}
	defer r.MultipartForm.RemoveAll() //nolint:errcheck

	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	fsConfig, err := getFsConfigFromPostFields(r)
	if err != nil {
		s.renderFolderPage(w, r, folder, folderPageModeUpdate, err)
		return
	}
	updatedFolder := vfs.BaseVirtualFolder{
		MappedPath:  strings.TrimSpace(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.AzBlobConfig.SASURL, folder.FsConfig.GCSConfig.Credentials, folder.FsConfig.CryptConfig.Passphrase,
		folder.FsConfig.SFTPConfig.Password, folder.FsConfig.SFTPConfig.PrivateKey, folder.FsConfig.SFTPConfig.KeyPassphrase,
		folder.FsConfig.HTTPConfig.Password, folder.FsConfig.HTTPConfig.APIKey)

	updatedFolder = getFolderFromTemplate(updatedFolder, updatedFolder.Name)

	err = dataprovider.UpdateFolder(&updatedFolder, folder.Users, folder.Groups, claims.Username, ipAddr, claims.Role)
	if err != nil {
		s.renderFolderPage(w, r, updatedFolder, folderPageModeUpdate, err)
		return
	}
	http.Redirect(w, r, webFoldersPath, http.StatusSeeOther)
}

func (s *httpdServer) getWebVirtualFolders(w http.ResponseWriter, r *http.Request, limit int, minimal bool) ([]vfs.BaseVirtualFolder, error) {
	folders := make([]vfs.BaseVirtualFolder, 0, 50)
	for {
		f, err := dataprovider.GetFolders(limit, len(folders), dataprovider.OrderASC, minimal)
		if err != nil {
			s.renderInternalServerErrorPage(w, r, err)
			return folders, err
		}
		folders = append(folders, f...)
		if len(f) < limit {
			break
		}
	}
	return folders, nil
}

func getAllFolders(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)

	dataGetter := func(limit, offset int) ([]byte, int, error) {
		results, err := dataprovider.GetFolders(limit, offset, dataprovider.OrderASC, false)
		if err != nil {
			return nil, 0, err
		}
		data, err := json.Marshal(results)
		return data, len(results), err
	}

	streamJSONArray(w, defaultQueryLimit, dataGetter)
}

func (s *httpdServer) handleWebGetFolders(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)

	data := s.getBasePageData(util.I18nFoldersTitle, webFoldersPath, r)
	renderAdminTemplate(w, templateFolders, data)
}

func (s *httpdServer) getWebGroups(w http.ResponseWriter, r *http.Request, limit int, minimal bool) ([]dataprovider.Group, error) {
	groups := make([]dataprovider.Group, 0, 50)
	for {
		f, err := dataprovider.GetGroups(limit, len(groups), dataprovider.OrderASC, minimal)
		if err != nil {
			s.renderInternalServerErrorPage(w, r, err)
			return groups, err
		}
		groups = append(groups, f...)
		if len(f) < limit {
			break
		}
	}
	return groups, nil
}

func getAllGroups(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)

	dataGetter := func(limit, offset int) ([]byte, int, error) {
		results, err := dataprovider.GetGroups(limit, offset, dataprovider.OrderASC, false)
		if err != nil {
			return nil, 0, err
		}
		data, err := json.Marshal(results)
		return data, len(results), err
	}

	streamJSONArray(w, defaultQueryLimit, dataGetter)
}

func (s *httpdServer) handleWebGetGroups(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)

	data := s.getBasePageData(util.I18nGroupsTitle, webGroupsPath, r)
	renderAdminTemplate(w, templateGroups, data)
}

func (s *httpdServer) handleWebAddGroupGet(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	s.renderGroupPage(w, r, dataprovider.Group{}, genericPageModeAdd, nil)
}

func (s *httpdServer) handleWebAddGroupPost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	group, err := getGroupFromPostFields(r)
	if err != nil {
		s.renderGroupPage(w, r, group, genericPageModeAdd, err)
		return
	}
	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	err = dataprovider.AddGroup(&group, claims.Username, ipAddr, claims.Role)
	if err != nil {
		s.renderGroupPage(w, r, group, genericPageModeAdd, err)
		return
	}
	http.Redirect(w, r, webGroupsPath, http.StatusSeeOther)
}

func (s *httpdServer) handleWebUpdateGroupGet(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	name := getURLParam(r, "name")
	group, err := dataprovider.GroupExists(name)
	if err == nil {
		s.renderGroupPage(w, r, group, genericPageModeUpdate, nil)
	} else if errors.Is(err, util.ErrNotFound) {
		s.renderNotFoundPage(w, r, err)
	} else {
		s.renderInternalServerErrorPage(w, r, err)
	}
}

func (s *httpdServer) handleWebUpdateGroupPost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	name := getURLParam(r, "name")
	group, err := dataprovider.GroupExists(name)
	if errors.Is(err, util.ErrNotFound) {
		s.renderNotFoundPage(w, r, err)
		return
	} else if err != nil {
		s.renderInternalServerErrorPage(w, r, err)
		return
	}
	updatedGroup, err := getGroupFromPostFields(r)
	if err != nil {
		s.renderGroupPage(w, r, group, genericPageModeUpdate, err)
		return
	}
	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	updatedGroup.ID = group.ID
	updatedGroup.Name = group.Name
	updatedGroup.SetEmptySecretsIfNil()

	updateEncryptedSecrets(&updatedGroup.UserSettings.FsConfig, group.UserSettings.FsConfig.S3Config.AccessSecret,
		group.UserSettings.FsConfig.AzBlobConfig.AccountKey, group.UserSettings.FsConfig.AzBlobConfig.SASURL,
		group.UserSettings.FsConfig.GCSConfig.Credentials, group.UserSettings.FsConfig.CryptConfig.Passphrase,
		group.UserSettings.FsConfig.SFTPConfig.Password, group.UserSettings.FsConfig.SFTPConfig.PrivateKey,
		group.UserSettings.FsConfig.SFTPConfig.KeyPassphrase, group.UserSettings.FsConfig.HTTPConfig.Password,
		group.UserSettings.FsConfig.HTTPConfig.APIKey)

	err = dataprovider.UpdateGroup(&updatedGroup, group.Users, claims.Username, ipAddr, claims.Role)
	if err != nil {
		s.renderGroupPage(w, r, updatedGroup, genericPageModeUpdate, err)
		return
	}
	http.Redirect(w, r, webGroupsPath, http.StatusSeeOther)
}

func (s *httpdServer) getWebEventActions(w http.ResponseWriter, r *http.Request, limit int, minimal bool,
) ([]dataprovider.BaseEventAction, error) {
	actions := make([]dataprovider.BaseEventAction, 0, limit)
	for {
		res, err := dataprovider.GetEventActions(limit, len(actions), dataprovider.OrderASC, minimal)
		if err != nil {
			s.renderInternalServerErrorPage(w, r, err)
			return actions, err
		}
		actions = append(actions, res...)
		if len(res) < limit {
			break
		}
	}
	return actions, nil
}

func getAllActions(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)

	dataGetter := func(limit, offset int) ([]byte, int, error) {
		results, err := dataprovider.GetEventActions(limit, offset, dataprovider.OrderASC, false)
		if err != nil {
			return nil, 0, err
		}
		data, err := json.Marshal(results)
		return data, len(results), err
	}

	streamJSONArray(w, defaultQueryLimit, dataGetter)
}

func (s *httpdServer) handleWebGetEventActions(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)

	data := s.getBasePageData(util.I18nActionsTitle, webAdminEventActionsPath, r)
	renderAdminTemplate(w, templateEventActions, data)
}

func (s *httpdServer) handleWebAddEventActionGet(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	action := dataprovider.BaseEventAction{
		Type: dataprovider.ActionTypeHTTP,
	}
	s.renderEventActionPage(w, r, action, genericPageModeAdd, nil)
}

func (s *httpdServer) handleWebAddEventActionPost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	action, err := getEventActionFromPostFields(r)
	if err != nil {
		s.renderEventActionPage(w, r, action, genericPageModeAdd, err)
		return
	}
	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	if err = dataprovider.AddEventAction(&action, claims.Username, ipAddr, claims.Role); err != nil {
		s.renderEventActionPage(w, r, action, genericPageModeAdd, err)
		return
	}
	http.Redirect(w, r, webAdminEventActionsPath, http.StatusSeeOther)
}

func (s *httpdServer) handleWebUpdateEventActionGet(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	name := getURLParam(r, "name")
	action, err := dataprovider.EventActionExists(name)
	if err == nil {
		s.renderEventActionPage(w, r, action, genericPageModeUpdate, nil)
	} else if errors.Is(err, util.ErrNotFound) {
		s.renderNotFoundPage(w, r, err)
	} else {
		s.renderInternalServerErrorPage(w, r, err)
	}
}

func (s *httpdServer) handleWebUpdateEventActionPost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	name := getURLParam(r, "name")
	action, err := dataprovider.EventActionExists(name)
	if errors.Is(err, util.ErrNotFound) {
		s.renderNotFoundPage(w, r, err)
		return
	} else if err != nil {
		s.renderInternalServerErrorPage(w, r, err)
		return
	}
	updatedAction, err := getEventActionFromPostFields(r)
	if err != nil {
		s.renderEventActionPage(w, r, updatedAction, genericPageModeUpdate, err)
		return
	}
	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	updatedAction.ID = action.ID
	updatedAction.Name = action.Name
	updatedAction.Options.SetEmptySecretsIfNil()
	switch updatedAction.Type {
	case dataprovider.ActionTypeHTTP:
		if updatedAction.Options.HTTPConfig.Password.IsNotPlainAndNotEmpty() {
			updatedAction.Options.HTTPConfig.Password = action.Options.HTTPConfig.Password
		}
	}
	err = dataprovider.UpdateEventAction(&updatedAction, claims.Username, ipAddr, claims.Role)
	if err != nil {
		s.renderEventActionPage(w, r, updatedAction, genericPageModeUpdate, err)
		return
	}
	http.Redirect(w, r, webAdminEventActionsPath, http.StatusSeeOther)
}

func getAllRules(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)

	dataGetter := func(limit, offset int) ([]byte, int, error) {
		results, err := dataprovider.GetEventRules(limit, offset, dataprovider.OrderASC)
		if err != nil {
			return nil, 0, err
		}
		data, err := json.Marshal(results)
		return data, len(results), err
	}

	streamJSONArray(w, defaultQueryLimit, dataGetter)
}

func (s *httpdServer) handleWebGetEventRules(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)

	data := s.getBasePageData(util.I18nRulesTitle, webAdminEventRulesPath, r)
	renderAdminTemplate(w, templateEventRules, data)
}

func (s *httpdServer) handleWebAddEventRuleGet(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	rule := dataprovider.EventRule{
		Status:  1,
		Trigger: dataprovider.EventTriggerFsEvent,
	}
	s.renderEventRulePage(w, r, rule, genericPageModeAdd, nil)
}

func (s *httpdServer) handleWebAddEventRulePost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	rule, err := getEventRuleFromPostFields(r)
	if err != nil {
		s.renderEventRulePage(w, r, rule, genericPageModeAdd, err)
		return
	}
	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	err = verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr)
	if err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	if err = dataprovider.AddEventRule(&rule, claims.Username, ipAddr, claims.Role); err != nil {
		s.renderEventRulePage(w, r, rule, genericPageModeAdd, err)
		return
	}
	http.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther)
}

func (s *httpdServer) handleWebUpdateEventRuleGet(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	name := getURLParam(r, "name")
	rule, err := dataprovider.EventRuleExists(name)
	if err == nil {
		s.renderEventRulePage(w, r, rule, genericPageModeUpdate, nil)
	} else if errors.Is(err, util.ErrNotFound) {
		s.renderNotFoundPage(w, r, err)
	} else {
		s.renderInternalServerErrorPage(w, r, err)
	}
}

func (s *httpdServer) handleWebUpdateEventRulePost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	name := getURLParam(r, "name")
	rule, err := dataprovider.EventRuleExists(name)
	if errors.Is(err, util.ErrNotFound) {
		s.renderNotFoundPage(w, r, err)
		return
	} else if err != nil {
		s.renderInternalServerErrorPage(w, r, err)
		return
	}
	updatedRule, err := getEventRuleFromPostFields(r)
	if err != nil {
		s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err)
		return
	}
	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	updatedRule.ID = rule.ID
	updatedRule.Name = rule.Name
	err = dataprovider.UpdateEventRule(&updatedRule, claims.Username, ipAddr, claims.Role)
	if err != nil {
		s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err)
		return
	}
	http.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther)
}

func (s *httpdServer) getWebRoles(w http.ResponseWriter, r *http.Request, limit int, minimal bool) ([]dataprovider.Role, error) {
	roles := make([]dataprovider.Role, 0, 10)
	for {
		res, err := dataprovider.GetRoles(limit, len(roles), dataprovider.OrderASC, minimal)
		if err != nil {
			s.renderInternalServerErrorPage(w, r, err)
			return roles, err
		}
		roles = append(roles, res...)
		if len(res) < limit {
			break
		}
	}
	return roles, nil
}

func getAllRoles(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)

	dataGetter := func(limit, offset int) ([]byte, int, error) {
		results, err := dataprovider.GetRoles(limit, offset, dataprovider.OrderASC, false)
		if err != nil {
			return nil, 0, err
		}
		data, err := json.Marshal(results)
		return data, len(results), err
	}

	streamJSONArray(w, defaultQueryLimit, dataGetter)
}

func (s *httpdServer) handleWebGetRoles(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	data := s.getBasePageData(util.I18nRolesTitle, webAdminRolesPath, r)

	renderAdminTemplate(w, templateRoles, data)
}

func (s *httpdServer) handleWebAddRoleGet(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	s.renderRolePage(w, r, dataprovider.Role{}, genericPageModeAdd, nil)
}

func (s *httpdServer) handleWebAddRolePost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	role, err := getRoleFromPostFields(r)
	if err != nil {
		s.renderRolePage(w, r, role, genericPageModeAdd, err)
		return
	}
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	err = dataprovider.AddRole(&role, claims.Username, ipAddr, claims.Role)
	if err != nil {
		s.renderRolePage(w, r, role, genericPageModeAdd, err)
		return
	}
	http.Redirect(w, r, webAdminRolesPath, http.StatusSeeOther)
}

func (s *httpdServer) handleWebUpdateRoleGet(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	role, err := dataprovider.RoleExists(getURLParam(r, "name"))
	if err == nil {
		s.renderRolePage(w, r, role, genericPageModeUpdate, nil)
	} else if errors.Is(err, util.ErrNotFound) {
		s.renderNotFoundPage(w, r, err)
	} else {
		s.renderInternalServerErrorPage(w, r, err)
	}
}

func (s *httpdServer) handleWebUpdateRolePost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	role, err := dataprovider.RoleExists(getURLParam(r, "name"))
	if errors.Is(err, util.ErrNotFound) {
		s.renderNotFoundPage(w, r, err)
		return
	} else if err != nil {
		s.renderInternalServerErrorPage(w, r, err)
		return
	}

	updatedRole, err := getRoleFromPostFields(r)
	if err != nil {
		s.renderRolePage(w, r, role, genericPageModeUpdate, err)
		return
	}
	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	updatedRole.ID = role.ID
	updatedRole.Name = role.Name
	err = dataprovider.UpdateRole(&updatedRole, claims.Username, ipAddr, claims.Role)
	if err != nil {
		s.renderRolePage(w, r, updatedRole, genericPageModeUpdate, err)
		return
	}
	http.Redirect(w, r, webAdminRolesPath, http.StatusSeeOther)
}

func (s *httpdServer) handleWebGetEvents(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)

	data := eventsPage{
		basePage:                s.getBasePageData(util.I18nEventsTitle, webEventsPath, r),
		FsEventsSearchURL:       webEventsFsSearchPath,
		ProviderEventsSearchURL: webEventsProviderSearchPath,
		LogEventsSearchURL:      webEventsLogSearchPath,
	}
	renderAdminTemplate(w, templateEvents, data)
}

func (s *httpdServer) handleWebIPListsPage(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	rtlStatus, rtlProtocols := common.Config.GetRateLimitersStatus()
	data := ipListsPage{
		basePage:              s.getBasePageData(util.I18nIPListsTitle, webIPListsPath, r),
		RateLimitersStatus:    rtlStatus,
		RateLimitersProtocols: strings.Join(rtlProtocols, ", "),
		IsAllowListEnabled:    common.Config.IsAllowListEnabled(),
	}

	renderAdminTemplate(w, templateIPLists, data)
}

func (s *httpdServer) handleWebAddIPListEntryGet(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	listType, _, err := getIPListPathParams(r)
	if err != nil {
		s.renderBadRequestPage(w, r, err)
		return
	}
	s.renderIPListPage(w, r, dataprovider.IPListEntry{Type: listType}, genericPageModeAdd, nil)
}

func (s *httpdServer) handleWebAddIPListEntryPost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	listType, _, err := getIPListPathParams(r)
	if err != nil {
		s.renderBadRequestPage(w, r, err)
		return
	}
	entry, err := getIPListEntryFromPostFields(r, listType)
	if err != nil {
		s.renderIPListPage(w, r, entry, genericPageModeAdd, err)
		return
	}
	entry.Type = listType
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	err = dataprovider.AddIPListEntry(&entry, claims.Username, ipAddr, claims.Role)
	if err != nil {
		s.renderIPListPage(w, r, entry, genericPageModeAdd, err)
		return
	}
	http.Redirect(w, r, webIPListsPath, http.StatusSeeOther)
}

func (s *httpdServer) handleWebUpdateIPListEntryGet(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	listType, ipOrNet, err := getIPListPathParams(r)
	if err != nil {
		s.renderBadRequestPage(w, r, err)
		return
	}
	entry, err := dataprovider.IPListEntryExists(ipOrNet, listType)
	if err == nil {
		s.renderIPListPage(w, r, entry, genericPageModeUpdate, nil)
	} else if errors.Is(err, util.ErrNotFound) {
		s.renderNotFoundPage(w, r, err)
	} else {
		s.renderInternalServerErrorPage(w, r, err)
	}
}

func (s *httpdServer) handleWebUpdateIPListEntryPost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	listType, ipOrNet, err := getIPListPathParams(r)
	if err != nil {
		s.renderBadRequestPage(w, r, err)
		return
	}
	entry, err := dataprovider.IPListEntryExists(ipOrNet, listType)
	if errors.Is(err, util.ErrNotFound) {
		s.renderNotFoundPage(w, r, err)
		return
	} else if err != nil {
		s.renderInternalServerErrorPage(w, r, err)
		return
	}
	updatedEntry, err := getIPListEntryFromPostFields(r, listType)
	if err != nil {
		s.renderIPListPage(w, r, entry, genericPageModeUpdate, err)
		return
	}
	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	updatedEntry.Type = listType
	updatedEntry.IPOrNet = ipOrNet
	err = dataprovider.UpdateIPListEntry(&updatedEntry, claims.Username, ipAddr, claims.Role)
	if err != nil {
		s.renderIPListPage(w, r, entry, genericPageModeUpdate, err)
		return
	}
	http.Redirect(w, r, webIPListsPath, http.StatusSeeOther)
}

func (s *httpdServer) handleWebConfigs(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	configs, err := dataprovider.GetConfigs()
	if err != nil {
		s.renderInternalServerErrorPage(w, r, err)
		return
	}
	s.renderConfigsPage(w, r, configs, nil, 0)
}

func (s *httpdServer) handleWebConfigsPost(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
	claims, err := getTokenClaims(r)
	if err != nil || claims.Username == "" {
		s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
		return
	}
	configs, err := dataprovider.GetConfigs()
	if err != nil {
		s.renderInternalServerErrorPage(w, r, err)
		return
	}
	err = r.ParseForm()
	if err != nil {
		s.renderBadRequestPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
		return
	}
	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
		s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
		return
	}
	var configSection int
	switch r.Form.Get("form_action") {
	case "sftp_submit":
		configSection = 1
		sftpConfigs := getSFTPConfigsFromPostFields(r)
		configs.SFTPD = sftpConfigs
	case "acme_submit":
		configSection = 2
		acmeConfigs := getACMEConfigsFromPostFields(r)
		configs.ACME = acmeConfigs
		if err := acme.GetCertificatesForConfig(acmeConfigs, configurationDir); err != nil {
			logger.Info(logSender, "", "unable to get ACME certificates: %v", err)
			s.renderConfigsPage(w, r, configs, util.NewI18nError(err, util.I18nErrorACMEGeneric), configSection)
			return
		}
	case "smtp_submit":
		configSection = 3
		smtpConfigs := getSMTPConfigsFromPostFields(r)
		updateSMTPSecrets(smtpConfigs, configs.SMTP)
		configs.SMTP = smtpConfigs
	default:
		s.renderBadRequestPage(w, r, errors.New("unsupported form action"))
		return
	}

	err = dataprovider.UpdateConfigs(&configs, claims.Username, ipAddr, claims.Role)
	if err != nil {
		s.renderConfigsPage(w, r, configs, err, configSection)
		return
	}
	if configSection == 3 {
		err := configs.SMTP.TryDecrypt()
		if err == nil {
			smtp.Activate(configs.SMTP)
		} else {
			logger.Error(logSender, "", "unable to decrypt SMTP configuration, cannot activate configuration: %v", err)
		}
	}
	s.renderMessagePage(w, r, util.I18nConfigsTitle, http.StatusOK, nil, util.I18nConfigsOK)
}

func (s *httpdServer) handleOAuth2TokenRedirect(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)

	stateToken := r.URL.Query().Get("state")

	state, err := verifyOAuth2Token(stateToken, util.GetIPFromRemoteAddress(r.RemoteAddr))
	if err != nil {
		s.renderMessagePage(w, r, util.I18nOAuth2ErrorTitle, http.StatusBadRequest, err, "")
		return
	}

	defer oauth2Mgr.removePendingAuth(state)

	pendingAuth, err := oauth2Mgr.getPendingAuth(state)
	if err != nil {
		s.renderMessagePage(w, r, util.I18nOAuth2ErrorTitle, http.StatusInternalServerError,
			util.NewI18nError(err, util.I18nOAuth2ErrorValidateState), "")
		return
	}
	oauth2Config := smtp.OAuth2Config{
		Provider:     pendingAuth.Provider,
		ClientID:     pendingAuth.ClientID,
		ClientSecret: pendingAuth.ClientSecret.GetPayload(),
	}
	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	cfg := oauth2Config.GetOAuth2()
	cfg.RedirectURL = pendingAuth.RedirectURL
	token, err := cfg.Exchange(ctx, r.URL.Query().Get("code"))
	if err != nil {
		s.renderMessagePage(w, r, util.I18nOAuth2ErrorTitle, http.StatusInternalServerError,
			util.NewI18nError(err, util.I18nOAuth2ErrTokenExchange), "")
		return
	}
	if token.RefreshToken == "" {
		errTxt := "the OAuth2 provider returned an empty token. " +
			"Some providers only return the token when the user first authorizes. " +
			"If you have already registered SFTPGo with this user in the past, revoke access and try again. " +
			"This way you will invalidate the previous token"
		s.renderMessagePage(w, r, util.I18nOAuth2ErrorTitle, http.StatusBadRequest,
			util.NewI18nError(errors.New(errTxt), util.I18nOAuth2ErrNoRefreshToken), "")
		return
	}
	s.renderMessagePageWithString(w, r, util.I18nOAuth2Title, http.StatusOK, nil, util.I18nOAuth2OK,
		fmt.Sprintf("%q", token.RefreshToken))
}

func updateSMTPSecrets(newConfigs, currentConfigs *dataprovider.SMTPConfigs) {
	if newConfigs.Password.IsNotPlainAndNotEmpty() {
		newConfigs.Password = currentConfigs.Password
	}
	if newConfigs.OAuth2.ClientSecret.IsNotPlainAndNotEmpty() {
		newConfigs.OAuth2.ClientSecret = currentConfigs.OAuth2.ClientSecret
	}
	if newConfigs.OAuth2.RefreshToken.IsNotPlainAndNotEmpty() {
		newConfigs.OAuth2.RefreshToken = currentConfigs.OAuth2.RefreshToken
	}
}
