| 
					
				 | 
			
			
				@@ -13,6 +13,8 @@ import ( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	"strings" 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	"time" 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	"github.com/go-chi/render" 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	"github.com/drakkan/sftpgo/common" 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	"github.com/drakkan/sftpgo/dataprovider" 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	"github.com/drakkan/sftpgo/kms" 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -21,6 +23,14 @@ import ( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	"github.com/drakkan/sftpgo/vfs" 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 ) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+type userPageMode int 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+const ( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	userPageModeAdd userPageMode = iota + 1 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	userPageModeUpdate 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	userPageModeTemplate 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 const ( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	templateBase         = "base.html" 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	templateUsers        = "users.html" 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -62,6 +72,7 @@ type basePage struct { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	CurrentURL         string 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	UsersURL           string 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	UserURL            string 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	UserTemplateURL    string 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	AdminsURL          string 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	AdminURL           string 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	QuotaScanURL       string 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -110,7 +121,7 @@ type statusPage struct { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 type userPage struct { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	basePage 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-	User                 dataprovider.User 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	User                 *dataprovider.User 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	RootPerms            []string 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	Error                string 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	ValidPerms           []string 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -118,7 +129,7 @@ type userPage struct { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	ValidProtocols       []string 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	RootDirPerms         []string 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	RedactedSecret       string 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-	IsAdd                bool 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	Mode                 userPageMode 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 type adminPage struct { 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -158,6 +169,12 @@ type loginPage struct { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	Error      string 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+type userTemplateFields struct { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	Username  string 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	Password  string 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	PublicKey string 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 func loadTemplates(templatesPath string) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	usersPaths := []string{ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		filepath.Join(templatesPath, templateBase), 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -239,6 +256,7 @@ func getBasePageData(title, currentURL string, r *http.Request) basePage { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		CurrentURL:         currentURL, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		UsersURL:           webUsersPath, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		UserURL:            webUserPath, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		UserTemplateURL:    webTemplateUser, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		AdminsURL:          webAdminsPath, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		AdminURL:           webAdminPath, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		FoldersURL:         webFoldersPath, 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -337,27 +355,23 @@ func renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Request, admin *dat 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	renderTemplate(w, templateAdmin, data) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-func renderAddUserPage(w http.ResponseWriter, r *http.Request, user dataprovider.User, error string) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+func renderUserPage(w http.ResponseWriter, r *http.Request, user *dataprovider.User, mode userPageMode, error string) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	user.SetEmptySecretsIfNil() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-	data := userPage{ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-		basePage:             getBasePageData("Add a new user", webUserPath, r), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-		IsAdd:                true, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-		Error:                error, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-		User:                 user, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-		ValidPerms:           dataprovider.ValidPerms, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-		ValidSSHLoginMethods: dataprovider.ValidSSHLoginMethods, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-		ValidProtocols:       dataprovider.ValidProtocols, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-		RootDirPerms:         user.GetPermissionsForPath("/"), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-		RedactedSecret:       redactedSecret, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	var title, currentURL string 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	switch mode { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	case userPageModeAdd: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		title = "Add a new user" 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		currentURL = webUserPath 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	case userPageModeUpdate: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		title = "Update user" 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		currentURL = fmt.Sprintf("%v/%v", webUserPath, url.PathEscape(user.Username)) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	case userPageModeTemplate: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		title = "User template" 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		currentURL = webTemplateUser 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-	renderTemplate(w, templateUser, data) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-func renderUpdateUserPage(w http.ResponseWriter, r *http.Request, user dataprovider.User, error string) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-	user.SetEmptySecretsIfNil() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	data := userPage{ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-		basePage:             getBasePageData("Update user", fmt.Sprintf("%v/%v", webUserPath, url.PathEscape(user.Username)), r), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-		IsAdd:                false, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		basePage:             getBasePageData(title, currentURL, r), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		Mode:                 mode, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		Error:                error, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		User:                 user, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		ValidPerms:           dataprovider.ValidPerms, 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -378,6 +392,33 @@ func renderAddFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.Base 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	renderTemplate(w, templateFolder, data) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+func getUsersForTemplate(r *http.Request) []userTemplateFields { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	var res []userTemplateFields 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	formValue := r.Form.Get("users") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	for _, cleaned := range getSliceFromDelimitedValues(formValue, "\n") { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		if strings.Contains(cleaned, "::") { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			mapping := strings.Split(cleaned, "::") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			if len(mapping) > 1 { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+				username := strings.TrimSpace(mapping[0]) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+				password := strings.TrimSpace(mapping[1]) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+				var publicKey string 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+				if len(mapping) > 2 { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+					publicKey = strings.TrimSpace(mapping[2]) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+				} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+				if username == "" || (password == "" && publicKey == "") { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+					continue 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+				} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+				res = append(res, userTemplateFields{ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+					Username:  username, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+					Password:  password, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+					PublicKey: publicKey, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+				}) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	return res 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 func getVirtualFoldersFromPostFields(r *http.Request) []vfs.VirtualFolder { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	var virtualFolders []vfs.VirtualFolder 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	formValue := r.Form.Get("virtual_folders") 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -429,7 +470,7 @@ func getUserPermissionsFromPostFields(r *http.Request) map[string][]string { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 						perms = append(perms, cleanedPerm) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 					} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 				} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-				if len(dir) > 0 { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+				if dir != "" { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 					permissions[dir] = perms 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 				} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 			} 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -442,7 +483,7 @@ func getSliceFromDelimitedValues(values, delimiter string) []string { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	result := []string{} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	for _, v := range strings.Split(values, delimiter) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		cleaned := strings.TrimSpace(v) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-		if len(cleaned) > 0 { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		if cleaned != "" { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 			result = append(result, cleaned) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	} 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -708,6 +749,128 @@ func getAdminFromPostFields(r *http.Request) (dataprovider.Admin, error) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	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 getCryptFsFromTemplate(fsConfig vfs.CryptFsConfig, replacements map[string]string) vfs.CryptFsConfig { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	if fsConfig.Passphrase != nil { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		if fsConfig.Passphrase.IsPlain() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			payload := replacePlaceholders(fsConfig.Passphrase.GetPayload(), replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			fsConfig.Passphrase = kms.NewPlainSecret(payload) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	return fsConfig 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+func getS3FsFromTemplate(fsConfig vfs.S3FsConfig, replacements map[string]string) vfs.S3FsConfig { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	fsConfig.KeyPrefix = replacePlaceholders(fsConfig.KeyPrefix, replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	fsConfig.AccessKey = replacePlaceholders(fsConfig.AccessKey, replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	if fsConfig.AccessSecret != nil && fsConfig.AccessSecret.IsPlain() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		payload := replacePlaceholders(fsConfig.AccessSecret.GetPayload(), replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		fsConfig.AccessSecret = kms.NewPlainSecret(payload) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	return fsConfig 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+func getGCSFsFromTemplate(fsConfig vfs.GCSFsConfig, replacements map[string]string) vfs.GCSFsConfig { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	fsConfig.KeyPrefix = replacePlaceholders(fsConfig.KeyPrefix, replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	return fsConfig 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+func getAzBlobFsFromTemplate(fsConfig vfs.AzBlobFsConfig, replacements map[string]string) vfs.AzBlobFsConfig { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	fsConfig.KeyPrefix = replacePlaceholders(fsConfig.KeyPrefix, replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	fsConfig.AccountName = replacePlaceholders(fsConfig.AccountName, replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	if fsConfig.AccountKey != nil && fsConfig.AccountKey.IsPlain() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		payload := replacePlaceholders(fsConfig.AccountKey.GetPayload(), replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		fsConfig.AccountKey = kms.NewPlainSecret(payload) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	return fsConfig 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+func getSFTPFsFromTemplate(fsConfig vfs.SFTPFsConfig, replacements map[string]string) vfs.SFTPFsConfig { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	fsConfig.Prefix = replacePlaceholders(fsConfig.Prefix, replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	fsConfig.Username = replacePlaceholders(fsConfig.Username, replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	if fsConfig.Password != nil && fsConfig.Password.IsPlain() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		payload := replacePlaceholders(fsConfig.Password.GetPayload(), replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		fsConfig.Password = kms.NewPlainSecret(payload) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	return fsConfig 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+func getUserFromTemplate(user dataprovider.User, template userTemplateFields) dataprovider.User { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	user.Username = template.Username 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	user.Password = template.Password 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	user.PublicKeys = nil 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	if template.PublicKey != "" { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		user.PublicKeys = append(user.PublicKeys, template.PublicKey) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	replacements := make(map[string]string) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	replacements["%username%"] = user.Username 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	user.Password = replacePlaceholders(user.Password, replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	replacements["%password%"] = user.Password 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	user.HomeDir = replacePlaceholders(user.HomeDir, replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	var vfolders []vfs.VirtualFolder 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	for _, vfolder := range user.VirtualFolders { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		vfolder.MappedPath = replacePlaceholders(vfolder.MappedPath, replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		vfolders = append(vfolders, vfolder) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	user.VirtualFolders = vfolders 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	user.AdditionalInfo = replacePlaceholders(user.AdditionalInfo, replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	switch user.FsConfig.Provider { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	case dataprovider.CryptedFilesystemProvider: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		user.FsConfig.CryptConfig = getCryptFsFromTemplate(user.FsConfig.CryptConfig, replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	case dataprovider.S3FilesystemProvider: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		user.FsConfig.S3Config = getS3FsFromTemplate(user.FsConfig.S3Config, replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	case dataprovider.GCSFilesystemProvider: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		user.FsConfig.GCSConfig = getGCSFsFromTemplate(user.FsConfig.GCSConfig, replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	case dataprovider.AzureBlobFilesystemProvider: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		user.FsConfig.AzBlobConfig = getAzBlobFsFromTemplate(user.FsConfig.AzBlobConfig, replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	case dataprovider.SFTPFilesystemProvider: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		user.FsConfig.SFTPConfig = getSFTPFsFromTemplate(user.FsConfig.SFTPConfig, replacements) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	return user 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+/*func decryptSecretsForTemplateUser(user *dataprovider.User) error { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	user.SetEmptySecretsIfNil() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	switch user.FsConfig.Provider { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	case dataprovider.CryptedFilesystemProvider: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		if user.FsConfig.CryptConfig.Passphrase.IsEncrypted() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			return user.FsConfig.CryptConfig.Passphrase.Decrypt() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	case dataprovider.S3FilesystemProvider: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		if user.FsConfig.S3Config.AccessSecret.IsEncrypted() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			return user.FsConfig.S3Config.AccessSecret.Decrypt() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	case dataprovider.GCSFilesystemProvider: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		if user.FsConfig.GCSConfig.Credentials.IsEncrypted() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			return user.FsConfig.GCSConfig.Credentials.Decrypt() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	case dataprovider.AzureBlobFilesystemProvider: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		if user.FsConfig.AzBlobConfig.AccountKey.IsEncrypted() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			return user.FsConfig.AzBlobConfig.AccountKey.Decrypt() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	case dataprovider.SFTPFilesystemProvider: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		if user.FsConfig.SFTPConfig.Password.IsEncrypted() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			if err := user.FsConfig.SFTPConfig.Password.Decrypt(); err != nil { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+				return err 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		if user.FsConfig.SFTPConfig.PrivateKey.IsEncrypted() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			return user.FsConfig.SFTPConfig.PrivateKey.Decrypt() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	return nil 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+}*/ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 func getUserFromPostFields(r *http.Request) (dataprovider.User, error) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	var user dataprovider.User 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	err := r.ParseMultipartForm(maxRequestSize) 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -1004,18 +1167,63 @@ func handleGetWebUsers(w http.ResponseWriter, r *http.Request) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	renderTemplate(w, templateUsers, data) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+func handleWebTemplateUserGet(w http.ResponseWriter, r *http.Request) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	if r.URL.Query().Get("from") != "" { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		username := r.URL.Query().Get("from") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		user, err := dataprovider.UserExists(username) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		if err == nil { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			user.SetEmptySecrets() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			renderUserPage(w, r, &user, userPageModeTemplate, "") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			renderNotFoundPage(w, r, err) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		} else { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			renderInternalServerErrorPage(w, r, err) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	} else { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		user := dataprovider.User{Status: 1} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		renderUserPage(w, r, &user, userPageModeTemplate, "") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+func handleWebTemplateUserPost(w http.ResponseWriter, r *http.Request) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	templateUser, err := getUserFromPostFields(r) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	if err != nil { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		renderMessagePage(w, r, "Error parsing user fields", "", http.StatusBadRequest, err, "") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		return 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	var dump dataprovider.BackupData 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	dump.Version = dataprovider.DumpVersion 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	userTmplFields := getUsersForTemplate(r) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	for _, tmpl := range userTmplFields { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		u := getUserFromTemplate(templateUser, tmpl) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		if err := dataprovider.ValidateUser(&u); err != nil { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			renderMessagePage(w, r, fmt.Sprintf("Error validating user %#v", u.Username), "", http.StatusBadRequest, err, "") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			return 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		dump.Users = append(dump.Users, u) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	if len(dump.Users) == 0 { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		renderMessagePage(w, r, "No users to export", "No valid users found, export is not possible", http.StatusBadRequest, nil, "") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		return 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"sftpgo-%v-users-from-template.json\"", len(dump.Users))) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	render.JSON(w, r, dump) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-	if r.URL.Query().Get("cloneFrom") != "" { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-		username := r.URL.Query().Get("cloneFrom") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+	if r.URL.Query().Get("clone-from") != "" { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		username := r.URL.Query().Get("clone-from") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		user, err := dataprovider.UserExists(username) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		if err == nil { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 			user.ID = 0 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 			user.Username = "" 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-			if err := user.DecryptSecrets(); err != nil { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-				renderInternalServerErrorPage(w, r, err) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-				return 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-			} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-			renderAddUserPage(w, r, user, "") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			user.SetEmptySecrets() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+			renderUserPage(w, r, &user, userPageModeAdd, "") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 			renderNotFoundPage(w, r, err) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		} else { 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -1023,7 +1231,7 @@ func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	} else { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		user := dataprovider.User{Status: 1} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-		renderAddUserPage(w, r, user, "") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		renderUserPage(w, r, &user, userPageModeAdd, "") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -1031,7 +1239,7 @@ func handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	username := getURLParam(r, "username") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	user, err := dataprovider.UserExists(username) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	if err == nil { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-		renderUpdateUserPage(w, r, user, "") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		renderUserPage(w, r, &user, userPageModeUpdate, "") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		renderNotFoundPage(w, r, err) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	} else { 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -1043,14 +1251,14 @@ func handleWebAddUserPost(w http.ResponseWriter, r *http.Request) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	user, err := getUserFromPostFields(r) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	if err != nil { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-		renderAddUserPage(w, r, user, err.Error()) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		renderUserPage(w, r, &user, userPageModeAdd, err.Error()) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		return 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	err = dataprovider.AddUser(&user) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	if err == nil { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		http.Redirect(w, r, webUsersPath, http.StatusSeeOther) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	} else { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-		renderAddUserPage(w, r, user, err.Error()) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		renderUserPage(w, r, &user, userPageModeAdd, err.Error()) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -1067,7 +1275,7 @@ func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	updatedUser, err := getUserFromPostFields(r) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	if err != nil { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-		renderUpdateUserPage(w, r, user, err.Error()) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		renderUserPage(w, r, &user, userPageModeUpdate, err.Error()) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		return 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	updatedUser.ID = user.ID 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -1087,7 +1295,7 @@ func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 		http.Redirect(w, r, webUsersPath, http.StatusSeeOther) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	} else { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-		renderUpdateUserPage(w, r, user, err.Error()) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+		renderUserPage(w, r, &user, userPageModeUpdate, err.Error()) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 	} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 |