From ffa453fea13f3e3e508b25c3cef30d22f28338b6 Mon Sep 17 00:00:00 2001
From: Robert Goldmann <deadlocker@gmx.de>
Date: Tue, 14 Jun 2022 23:07:18 +0200
Subject: [PATCH] #696 - save backup settings

---
 ... => BackupSettingsContainerValidator.java} |  11 +-
 .../settings/SettingsController.java          | 160 +++++++--------
 .../containers/BackupSettingsContainer.java   | 138 +++++++++++++
 .../resources/languages/base_de.properties    |   2 +
 .../resources/languages/base_en.properties    |   2 +
 .../src/main/resources/static/js/settings.js  |  93 ---------
 .../settings/containers/settingsBackup.ftl    | 194 ++++++++++++++++++
 .../settings/containers/settingsContainer.ftl |   4 +-
 .../resources/templates/settings/settings.ftl |  40 +---
 .../templates/settings/settingsMacros.ftl     |  14 +-
 10 files changed, 427 insertions(+), 231 deletions(-)
 rename BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/{SettingsValidator.java => BackupSettingsContainerValidator.java} (70%)
 create mode 100644 BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/containers/BackupSettingsContainer.java
 create mode 100644 BudgetMasterServer/src/main/resources/templates/settings/containers/settingsBackup.ftl

diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsValidator.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/BackupSettingsContainerValidator.java
similarity index 70%
rename from BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsValidator.java
rename to BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/BackupSettingsContainerValidator.java
index c85738326..1994ec8a1 100644
--- a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsValidator.java
+++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/BackupSettingsContainerValidator.java
@@ -1,31 +1,32 @@
 package de.deadlocker8.budgetmaster.settings;
 
 import de.deadlocker8.budgetmaster.backup.AutoBackupStrategy;
+import de.deadlocker8.budgetmaster.settings.containers.BackupSettingsContainer;
 import de.deadlocker8.budgetmaster.utils.Strings;
 import org.springframework.validation.Errors;
 import org.springframework.validation.ValidationUtils;
 import org.springframework.validation.Validator;
 
 
-public class SettingsValidator implements Validator
+public class BackupSettingsContainerValidator implements Validator
 {
 	public boolean supports(Class clazz)
 	{
-		return Settings.class.equals(clazz);
+		return BackupSettingsContainer.class.equals(clazz);
 	}
 
 	public void validate(Object obj, Errors errors)
 	{
-		final Settings settings = (Settings) obj;
+		final BackupSettingsContainer backupSettingsContainer = (BackupSettingsContainer) obj;
 
 		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "autoBackupDays", Strings.WARNING_EMPTY_NUMBER);
 
-		if(settings.getAutoBackupStrategy() == AutoBackupStrategy.LOCAL)
+		if(backupSettingsContainer.getAutoBackupStrategy() == AutoBackupStrategy.LOCAL)
 		{
 			ValidationUtils.rejectIfEmptyOrWhitespace(errors, "autoBackupFilesToKeep", Strings.WARNING_EMPTY_NUMBER_ZERO_ALLOWED);
 		}
 
-		if(settings.getAutoBackupStrategy() == AutoBackupStrategy.GIT_REMOTE)
+		if(backupSettingsContainer.getAutoBackupStrategy() == AutoBackupStrategy.GIT_REMOTE)
 		{
 			ValidationUtils.rejectIfEmptyOrWhitespace(errors, "autoBackupGitUrl", Strings.WARNING_EMPTY_GIT_URL);
 			ValidationUtils.rejectIfEmptyOrWhitespace(errors, "autoBackupGitBranchName", Strings.WARNING_EMPTY_GIT_BRANCH_NAME);
diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java
index cb2297b24..c7c1ae9af 100644
--- a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java
+++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java
@@ -12,10 +12,7 @@ import de.deadlocker8.budgetmaster.database.model.BackupDatabase;
 import de.deadlocker8.budgetmaster.hints.HintService;
 import de.deadlocker8.budgetmaster.services.ImportResultItem;
 import de.deadlocker8.budgetmaster.services.ImportService;
-import de.deadlocker8.budgetmaster.settings.containers.PersonalizationSettingsContainer;
-import de.deadlocker8.budgetmaster.settings.containers.SecuritySettingsContainer;
-import de.deadlocker8.budgetmaster.settings.containers.TransactionsSettingsContainer;
-import de.deadlocker8.budgetmaster.settings.containers.UpdateSettingsContainer;
+import de.deadlocker8.budgetmaster.settings.containers.*;
 import de.deadlocker8.budgetmaster.update.BudgetMasterUpdateService;
 import de.deadlocker8.budgetmaster.utils.Mappings;
 import de.deadlocker8.budgetmaster.utils.WebRequestUtils;
@@ -83,6 +80,7 @@ public class SettingsController extends BaseController
 		public static final String CONTAINER_SECURITY = "settings/containers/settingsSecurity";
 		public static final String CONTAINER_PERSONALIZATION = "settings/containers/settingsPersonalization";
 		public static final String CONTAINER_TRANSACTIONS = "settings/containers/settingsTransactions";
+		public static final String CONTAINER_BACKUP = "settings/containers/settingsBackup";
 		public static final String CONTAINER_UPDATE = "settings/containers/settingsUpdate";
 		public static final String CONTAINER_MISC = "settings/containers/settingsMisc";
 	}
@@ -95,7 +93,7 @@ public class SettingsController extends BaseController
 		public static final String IMPORT_CHARTS = "importCharts";
 	}
 
-	private static final String PASSWORD_PLACEHOLDER = "•••••";
+	public static final String PASSWORD_PLACEHOLDER = "•••••";
 	private final SettingsService settingsService;
 	private final DatabaseService databaseService;
 	private final CategoryService categoryService;
@@ -230,6 +228,63 @@ public class SettingsController extends BaseController
 		return ReturnValues.CONTAINER_TRANSACTIONS;
 	}
 
+	@PostMapping(value = "/save/backup")
+	public String saveContainerBackup( Model model,
+									  @ModelAttribute("BackupSettingsContainer") BackupSettingsContainer backupSettingsContainer,
+									  @RequestParam(value = "runBackup", required = false) Boolean runBackup,
+									  BindingResult bindingResult)
+	{
+		BackupSettingsContainerValidator backupSettingsContainerValidator = new BackupSettingsContainerValidator();
+		backupSettingsContainerValidator.validate(backupSettingsContainer, bindingResult);
+
+		final Settings settings = settingsService.getSettings();
+		backupSettingsContainer.fillMissingFieldsWithDefaults(settings);
+
+		final Settings previousSettings = settingsService.getSettings();
+
+		// update settings here to hand them over to ftl to allow validation to show in case of binding errors
+		settings.setBackupReminderActivated(backupSettingsContainer.getBackupReminderActivated());
+		settings.setAutoBackupStrategy(backupSettingsContainer.getAutoBackupStrategy());
+		settings.setAutoBackupDays(backupSettingsContainer.getAutoBackupDays());
+		settings.setAutoBackupTime(backupSettingsContainer.getAutoBackupTime());
+		settings.setAutoBackupFilesToKeep(backupSettingsContainer.getAutoBackupFilesToKeep());
+		settings.setAutoBackupGitUrl(backupSettingsContainer.getAutoBackupGitUrl());
+		settings.setAutoBackupGitBranchName(backupSettingsContainer.getAutoBackupGitBranchName());
+		settings.setAutoBackupGitUserName(backupSettingsContainer.getAutoBackupGitUserName());
+		settings.setAutoBackupGitToken(backupSettingsContainer.getAutoBackupGitToken());
+
+		if(bindingResult.hasErrors())
+		{
+			model.addAttribute(ModelAttributes.ERROR, bindingResult);
+
+			final JsonObject toastContent = getToastContent("notification.settings.backup.error", NotificationType.ERROR);
+			model.addAttribute(ModelAttributes.TOAST_CONTENT, toastContent);
+			prepareModelBackup(model, settings);
+			return ReturnValues.CONTAINER_BACKUP;
+		}
+
+		settingsService.updateSettings(settings);
+
+		updateBackupTask(previousSettings, settings);
+
+		// run backup now if requested
+		JsonObject toastContent = runBackupIfRequested(runBackup);
+
+		model.addAttribute(ModelAttributes.TOAST_CONTENT, toastContent);
+		prepareModelBackup(model, settings);
+		return ReturnValues.CONTAINER_BACKUP;
+	}
+
+	private void prepareModelBackup(Model model, Settings settings)
+	{
+		model.addAttribute(ModelAttributes.SETTINGS, settings);
+		model.addAttribute(ModelAttributes.AUTO_BACKUP_TIME, AutoBackupTime.values());
+
+		final Optional<LocalDateTime> nextBackupTimeOptional = backupService.getNextRun();
+		nextBackupTimeOptional.ifPresent(date -> model.addAttribute(ModelAttributes.NEXT_BACKUP_TIME, date));
+		model.addAttribute(ModelAttributes.AUTO_BACKUP_STATUS, backupService.getBackupStatus());
+	}
+
 	@PostMapping(value = "/save/update")
 	public String saveContainerUpdate(Model model,
 									  @ModelAttribute("UpdateSettingsContainer") UpdateSettingsContainer updateSettingsContainer,
@@ -282,97 +337,28 @@ public class SettingsController extends BaseController
 		return MessageFormat.format("{0} {1}", notificationType.getBackgroundColor(), notificationType.getTextColor());
 	}
 
-	@PostMapping(value = "/save")
-	public String post(WebRequest request, Model model,
-					   @ModelAttribute("Settings") Settings settings, BindingResult bindingResult,
-					   @RequestParam(value = "autoBackupStrategyType", required = false) String autoBackupStrategyType,
-					   @RequestParam(value = "runBackup", required = false) Boolean runBackup)
-	{
-		if(autoBackupStrategyType == null)
-		{
-			settings.setAutoBackupStrategy(AutoBackupStrategy.NONE);
-		}
-		else
-		{
-			settings.setAutoBackupStrategy(AutoBackupStrategy.fromName(autoBackupStrategyType));
-		}
-
-		SettingsValidator settingsValidator = new SettingsValidator();
-		settingsValidator.validate(settings, bindingResult);
-
-		fillMissingFieldsWithDefaults(settings);
-
-		if(bindingResult.hasErrors())
-		{
-			model.addAttribute(ModelAttributes.ERROR, bindingResult);
-			prepareBasicModel(model, settings);
-			return ReturnValues.ALL_ENTITIES;
-		}
-
-		updateSettings(settings);
-
-		runBackup(request, runBackup);
-
-		WebRequestUtils.putNotification(request, new Notification(Localization.getString("notification.settings.saved"), NotificationType.SUCCESS));
-		return ReturnValues.REDIRECT_ALL_ENTITIES;
-	}
-
-	private void runBackup(WebRequest request, Boolean runBackup)
+	private JsonObject runBackupIfRequested(Boolean runBackup)
 	{
-		if(runBackup == null)
+		if(runBackup == null || !runBackup)
 		{
-			return;
+			return getToastContent("notification.settings.backup.saved", NotificationType.SUCCESS);
 		}
 
-		if(runBackup)
-		{
-			backupService.runNow();
-
-			BackupStatus backupStatus = backupService.getBackupStatus();
-			if(backupStatus == BackupStatus.OK)
-			{
-				WebRequestUtils.putNotification(request, new Notification(Localization.getString("notification.settings.backup.run.success"), NotificationType.SUCCESS));
-			}
-			else
-			{
-				WebRequestUtils.putNotification(request, new Notification(Localization.getString("notification.settings.backup.run.error"), NotificationType.ERROR));
-			}
-		}
-	}
+		backupService.runNow();
 
-	private void fillMissingFieldsWithDefaults(Settings settings)
-	{
-		if(settings.getBackupReminderActivated() == null)
+		BackupStatus backupStatus = backupService.getBackupStatus();
+		if(backupStatus == BackupStatus.OK)
 		{
-			settings.setBackupReminderActivated(false);
+			return getToastContent("notification.settings.backup.run.success", NotificationType.SUCCESS);
 		}
-
-		if(settings.getAutoBackupStrategy() == null)
-		{
-			settings.setAutoBackupStrategy(AutoBackupStrategy.NONE);
-		}
-
-		if(settings.getAutoBackupGitToken().equals(PASSWORD_PLACEHOLDER))
-		{
-			settings.setAutoBackupGitToken(settingsService.getSettings().getAutoBackupGitToken());
-		}
-
-		if(settings.getAutoBackupStrategy() == AutoBackupStrategy.NONE)
+		else
 		{
-			final Settings defaultSettings = Settings.getDefault();
-			settings.setAutoBackupDays(defaultSettings.getAutoBackupDays());
-			settings.setAutoBackupTime(defaultSettings.getAutoBackupTime());
-			settings.setAutoBackupFilesToKeep(defaultSettings.getAutoBackupFilesToKeep());
-			settings.setAutoBackupGitUserName(defaultSettings.getAutoBackupGitUserName());
-			settings.setAutoBackupGitToken(defaultSettings.getAutoBackupGitToken());
+			return getToastContent("notification.settings.backup.run.error", NotificationType.ERROR);
 		}
 	}
 
-	public void updateSettings(Settings settings)
+	public void updateBackupTask(Settings previousSettings, Settings settings)
 	{
-		final Settings previousSettings = settingsService.getSettings();
-		settingsService.updateSettings(settings);
-
 		backupService.stopBackupCron();
 		if(settings.getAutoBackupStrategy() != AutoBackupStrategy.NONE)
 		{
@@ -570,10 +556,6 @@ public class SettingsController extends BaseController
 	{
 		model.addAttribute(ModelAttributes.SETTINGS, settings);
 		model.addAttribute(ModelAttributes.SEARCH_RESULTS_PER_PAGE, SEARCH_RESULTS_PER_PAGE_OPTIONS);
-		model.addAttribute(ModelAttributes.AUTO_BACKUP_TIME, AutoBackupTime.values());
-
-		final Optional<LocalDateTime> nextBackupTimeOptional = backupService.getNextRun();
-		nextBackupTimeOptional.ifPresent(date -> model.addAttribute(ModelAttributes.NEXT_BACKUP_TIME, date));
-		model.addAttribute(ModelAttributes.AUTO_BACKUP_STATUS, backupService.getBackupStatus());
+		prepareModelBackup(model, settings);
 	}
 }
\ No newline at end of file
diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/containers/BackupSettingsContainer.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/containers/BackupSettingsContainer.java
new file mode 100644
index 000000000..f295b1934
--- /dev/null
+++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/containers/BackupSettingsContainer.java
@@ -0,0 +1,138 @@
+package de.deadlocker8.budgetmaster.settings.containers;
+
+import de.deadlocker8.budgetmaster.backup.AutoBackupStrategy;
+import de.deadlocker8.budgetmaster.backup.AutoBackupTime;
+import de.deadlocker8.budgetmaster.settings.Settings;
+import de.deadlocker8.budgetmaster.utils.Strings;
+import org.springframework.validation.Errors;
+import org.springframework.validation.ValidationUtils;
+
+import static de.deadlocker8.budgetmaster.settings.SettingsController.PASSWORD_PLACEHOLDER;
+
+public final class BackupSettingsContainer
+{
+	private Boolean backupReminderActivated;
+
+	private String autoBackupStrategyType;
+	private Integer autoBackupDays;
+	private String autoBackupTimeType;
+
+	private Integer autoBackupFilesToKeep;
+	private final String autoBackupGitUrl;
+	private final String autoBackupGitBranchName;
+	private String autoBackupGitUserName;
+	private String autoBackupGitToken;
+
+	public BackupSettingsContainer(Boolean backupReminderActivated, String autoBackupStrategyType, Integer autoBackupDays, String autoBackupTimeType, Integer autoBackupFilesToKeep, String autoBackupGitUrl, String autoBackupGitBranchName, String autoBackupGitUserName, String autoBackupGitToken)
+	{
+		this.backupReminderActivated = backupReminderActivated;
+		this.autoBackupStrategyType = autoBackupStrategyType;
+		this.autoBackupDays = autoBackupDays;
+		this.autoBackupTimeType = autoBackupTimeType;
+		this.autoBackupFilesToKeep = autoBackupFilesToKeep;
+		this.autoBackupGitUrl = autoBackupGitUrl;
+		this.autoBackupGitBranchName = autoBackupGitBranchName;
+		this.autoBackupGitUserName = autoBackupGitUserName;
+		this.autoBackupGitToken = autoBackupGitToken;
+	}
+
+	public Boolean getBackupReminderActivated()
+	{
+		return backupReminderActivated;
+	}
+
+	public AutoBackupStrategy getAutoBackupStrategy()
+	{
+		if(autoBackupStrategyType == null)
+		{
+			return AutoBackupStrategy.NONE;
+		}
+		else
+		{
+			return AutoBackupStrategy.fromName(autoBackupStrategyType);
+		}
+	}
+
+	public AutoBackupTime getAutoBackupTime()
+	{
+		return AutoBackupTime.valueOf(autoBackupTimeType);
+	}
+
+	public Integer getAutoBackupDays()
+	{
+		return autoBackupDays;
+	}
+
+	public Integer getAutoBackupFilesToKeep()
+	{
+		return autoBackupFilesToKeep;
+	}
+
+	public String getAutoBackupGitUrl()
+	{
+		return autoBackupGitUrl;
+	}
+
+	public String getAutoBackupGitBranchName()
+	{
+		return autoBackupGitBranchName;
+	}
+
+	public String getAutoBackupGitUserName()
+	{
+		return autoBackupGitUserName;
+	}
+
+	public String getAutoBackupGitToken()
+	{
+		return autoBackupGitToken;
+	}
+
+	public void validate(Object obj, Errors errors)
+	{
+		final Settings settings = (Settings) obj;
+
+		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "autoBackupDays", Strings.WARNING_EMPTY_NUMBER);
+
+		if(settings.getAutoBackupStrategy() == AutoBackupStrategy.LOCAL)
+		{
+			ValidationUtils.rejectIfEmptyOrWhitespace(errors, "autoBackupFilesToKeep", Strings.WARNING_EMPTY_NUMBER_ZERO_ALLOWED);
+		}
+
+		if(settings.getAutoBackupStrategy() == AutoBackupStrategy.GIT_REMOTE)
+		{
+			ValidationUtils.rejectIfEmptyOrWhitespace(errors, "autoBackupGitUrl", Strings.WARNING_EMPTY_GIT_URL);
+			ValidationUtils.rejectIfEmptyOrWhitespace(errors, "autoBackupGitBranchName", Strings.WARNING_EMPTY_GIT_BRANCH_NAME);
+			ValidationUtils.rejectIfEmptyOrWhitespace(errors, "autoBackupGitUserName", Strings.WARNING_EMPTY_GIT_USER_NAME);
+			ValidationUtils.rejectIfEmptyOrWhitespace(errors, "autoBackupGitToken", Strings.WARNING_EMPTY_GIT_TOKEN);
+		}
+	}
+
+	public void fillMissingFieldsWithDefaults(Settings settings)
+	{
+		if(backupReminderActivated == null)
+		{
+			backupReminderActivated = false;
+		}
+
+		if(autoBackupStrategyType == null)
+		{
+			autoBackupStrategyType = AutoBackupStrategy.NONE.getName();
+		}
+
+		if(autoBackupGitToken.equals(PASSWORD_PLACEHOLDER))
+		{
+			autoBackupGitToken = settings.getAutoBackupGitToken();
+		}
+
+		if(getAutoBackupStrategy() == AutoBackupStrategy.NONE)
+		{
+			final Settings defaultSettings = Settings.getDefault();
+			autoBackupDays = defaultSettings.getAutoBackupDays();
+			autoBackupTimeType = defaultSettings.getAutoBackupTime().name();
+			autoBackupFilesToKeep = defaultSettings.getAutoBackupFilesToKeep();
+			autoBackupGitUserName = defaultSettings.getAutoBackupGitUserName();
+			autoBackupGitToken = defaultSettings.getAutoBackupGitToken();
+		}
+	}
+}
diff --git a/BudgetMasterServer/src/main/resources/languages/base_de.properties b/BudgetMasterServer/src/main/resources/languages/base_de.properties
index b5073104b..64a24ebe1 100644
--- a/BudgetMasterServer/src/main/resources/languages/base_de.properties
+++ b/BudgetMasterServer/src/main/resources/languages/base_de.properties
@@ -177,6 +177,8 @@ notification.settings.transactions.saved=Buchungseinstellungen gespeichert
 notification.settings.transactions.error=Fehler beim Speichern der Buchungseinstellungen
 notification.settings.update.saved=Updateeinstellungen gespeichert
 notification.settings.update.error=Fehler beim Speichern der Updateeinstellungen
+notification.settings.backup.saved=Backupeinstellungen gespeichert
+notification.settings.backup.error=Fehler beim Speichern der Backupeinstellungen
 notification.settings.update.available=BudgetMaster Update "{0}" verfügbar
 notification.settings.update.not.available=Kein Update verfügbar
 notification.settings.hints.reset=Alle Tipps zurückgesetzt
diff --git a/BudgetMasterServer/src/main/resources/languages/base_en.properties b/BudgetMasterServer/src/main/resources/languages/base_en.properties
index 77a27e38c..83ebe7d75 100644
--- a/BudgetMasterServer/src/main/resources/languages/base_en.properties
+++ b/BudgetMasterServer/src/main/resources/languages/base_en.properties
@@ -176,6 +176,8 @@ notification.settings.personalization.saved=Personalization settings saved
 notification.settings.personalization.error=Error saving personalization settings
 notification.settings.transactions.saved=Transactions settings saved
 notification.settings.transactions.error=Error saving transactions settings
+notification.settings.backup.saved=Backup settings saved
+notification.settings.backup.error=Error saving backup settings
 notification.settings.update.saved=Update settings saved
 notification.settings.update.error=Error saving update settings
 notification.settings.hints.reset=All hints reset
diff --git a/BudgetMasterServer/src/main/resources/static/js/settings.js b/BudgetMasterServer/src/main/resources/static/js/settings.js
index 4e35537c0..98aaa3f54 100644
--- a/BudgetMasterServer/src/main/resources/static/js/settings.js
+++ b/BudgetMasterServer/src/main/resources/static/js/settings.js
@@ -14,70 +14,6 @@ $(document).ready(function()
         document.getElementById("form-database-import").submit();
     });
 
-    $('input[name="autoBackupActivated"]').click(function()
-    {
-        $('#settings-auto-backup').toggle($(this).prop("checked"));
-    });
-
-    $('#settings-backup-auto-strategy').change(function()
-    {
-        onAutoBackupStrategyChange(this.selectedIndex);
-    });
-
-    $('#settings-backup-auto-git-test').click(function()
-    {
-        $.ajax({
-            type: 'POST',
-            url: $('#settings-backup-auto-git-test').attr('data-url'),
-            data: {
-                '_csrf': document.getElementById('token').value,
-                'autoBackupGitUrl': document.getElementById('settings-backup-auto-git-url').value,
-                'autoBackupGitBranchName': document.getElementById('settings-backup-auto-git-branch-name').value,
-                'autoBackupGitUserName': document.getElementById('settings-backup-auto-git-user-name').value,
-                'autoBackupGitToken': document.getElementById('settings-backup-auto-git-token').value,
-            },
-            success: function(data)
-            {
-                let parsedData = JSON.parse(data);
-                let isValidConnection = parsedData['isValidConnection']
-                M.toast({
-                    html: parsedData['localizedMessage'],
-                    classes: isValidConnection ? 'green': 'red'
-                });
-            },
-            error: function(data)
-            {
-                M.toast({
-                    html: 'Error: ' + data,
-                    classes: 'red'
-                });
-            }
-        });
-    });
-
-    let autoBackupDays = $('#settings-backup-auto-days');
-    if(autoBackupDays.length)
-    {
-        autoBackupDays.on('change keydown paste input', function()
-        {
-            validateNumber(autoBackupDays.val(), 'settings-backup-auto-days', "hidden-settings-backup-auto-days", numberValidationMessage, REGEX_NUMBER_GREATER_ZERO);
-        });
-    }
-
-    let autoBackupFilesToKeep = $('#settings-backup-auto-files-to-keep');
-    if(autoBackupFilesToKeep.length)
-    {
-        autoBackupFilesToKeep.on('change keydown paste input', function()
-        {
-            validateNumber(autoBackupFilesToKeep.val(), "settings-backup-auto-files-to-keep", "hidden-settings-backup-auto-files-to-keep", numberValidationMessageZeroAllowed, REGEX_NUMBER);
-        });
-    }
-
-    $('#settings-backup-run-now').click(function()
-    {
-        document.getElementById('runBackupInput').value = 1;
-    });
-
     $('#verificationCode').click(function()
     {
         let verificationCodeElement = document.getElementsByName('verificationCode')[0];
@@ -88,37 +24,8 @@ $(document).ready(function()
 
         M.toast({html: copiedToClipboard, classes: 'green'});
     });
-
-    // on initial page load
-    let autoBackupCheckbox = document.getElementsByName("autoBackupActivated")[0];
-    $('#settings-auto-backup').toggle(autoBackupCheckbox.checked);
-    onAutoBackupStrategyChange(document.getElementById('settings-backup-auto-strategy').selectedIndex);
 });
 
-function validateForm()
-{
-    let autoBackupCheckbox = document.getElementsByName("autoBackupActivated")[0];
-    if(autoBackupCheckbox.checked)
-    {
-        let autoBackupDaysValid = validateNumber($('#settings-backup-auto-days').val(), "settings-backup-auto-days", "hidden-settings-backup-auto-days", numberValidationMessage, REGEX_NUMBER_GREATER_ZERO);
-        let autoBackupFilesToKeepValid = validateNumber($('#settings-backup-auto-files-to-keep').val(), "settings-backup-auto-files-to-keep", "hidden-settings-backup-auto-files-to-keep", numberValidationMessageZeroAllowed, REGEX_NUMBER);
-        return autoBackupDaysValid && autoBackupFilesToKeepValid;
-    }
-    else
-    {
-        document.getElementById('settings-backup-auto-strategy').name = '';
-    }
-
-    return true;
-}
-
-function onAutoBackupStrategyChange(newSelectedIndex)
-{
-    $('#settings-auto-backup-local').toggle(newSelectedIndex === 0);  // local backup with file system copies
-    // index 1 --> git local doesn't have any settings
-    $('#settings-auto-backup-git-remote').toggle(newSelectedIndex === 2);  // git remote
-}
-
 function toggleSettingsContainerHeader(id, hide)
 {
     document.querySelector('#' + id + ' .collapsible-header-button').classList.toggle('hidden', hide);
diff --git a/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsBackup.ftl b/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsBackup.ftl
new file mode 100644
index 000000000..320e6f4bf
--- /dev/null
+++ b/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsBackup.ftl
@@ -0,0 +1,194 @@
+<#import "/spring.ftl" as s>
+<#import "../../helpers/validation.ftl" as validation>
+<#import "../../helpers/header.ftl" as header>
+<@header.globals/>
+
+<#import "settingsContainer.ftl" as settingsContainerMacros>
+<#import "../settingsMacros.ftl" as settingsMacros>
+
+<#macro backupSettingsContainer importScripts settings>
+    <@settingsContainerMacros.settingsContainer 'BackupSettingsContainer' 'backupSettingsContainer' importScripts true>
+        <div class="row">
+            <div class="col s12">
+                <div class="table-container">
+                    <div class="table-cell">
+                        <div class="switch-cell-margin">${locale.getString("settings.backupReminder")}</div>
+                        <div class="switch-cell-margin">${locale.getString("settings.backup.auto")}</div>
+                    </div>
+                    <div class="table-cell table-cell-spacer"></div>
+                    <div class="table-cell">
+                        <@settingsMacros.switch "backupReminder" "backupReminderActivated" settings.getBackupReminderActivated()/>
+                        <@settingsMacros.switch "backup.auto" "autoBackupActivated" settings.isAutoBackupActive()/>
+                    </div>
+                    <div class="table-cell table-cell-spacer"></div>
+                    <div class="table-cell">
+                        <div class="switch-cell-margin">
+                            <a class="btn btn-flat tooltipped text-default" data-position="bottom" data-tooltip="${locale.getString("settings.backupReminder.description")}"><i class="material-icons">help_outline</i></a>
+                        </div>
+                        <div class="switch-cell-margin">
+                            <a class="btn btn-flat tooltipped text-default" data-position="bottom" data-tooltip="${locale.getString("settings.backup.auto.description")}"><i class="material-icons">help_outline</i></a>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <#-- auto backup -->
+        <@settingsMacros.autoBackup/>
+
+        <div class="row">
+            <div class="col s12 center-align">
+                <@header.buttonSubmit name='action' icon='save' localizationKey='save' color='background-green' formaction='/settings/save/backup'/>
+            </div>
+        </div>
+
+        <script>
+            function onAutoBackupStrategyChange(newSelectedIndex)
+            {
+                $('#settings-auto-backup-local').toggle(newSelectedIndex === 0);  // local backup with file system copies
+                // index 1 --> git local doesn't have any settings
+                $('#settings-auto-backup-git-remote').toggle(newSelectedIndex === 2);  // git remote
+            }
+
+            function validateForm()
+            {
+                let autoBackupCheckbox = document.getElementsByName("autoBackupActivated")[0];
+                if(autoBackupCheckbox.checked)
+                {
+                    let autoBackupDaysValid = validateNumber($('#settings-backup-auto-days').val(), "settings-backup-auto-days", "hidden-settings-backup-auto-days", numberValidationMessage, REGEX_NUMBER_GREATER_ZERO);
+                    let autoBackupFilesToKeepValid = validateNumber($('#settings-backup-auto-files-to-keep').val(), "settings-backup-auto-files-to-keep", "hidden-settings-backup-auto-files-to-keep", numberValidationMessageZeroAllowed, REGEX_NUMBER);
+                    return autoBackupDaysValid && autoBackupFilesToKeepValid;
+                }
+                else
+                {
+                    document.getElementById('settings-backup-auto-strategy').name = '';
+                }
+
+                return true;
+            }
+
+            $('input[name="autoBackupActivated"]').click(function()
+            {
+                $('#settings-auto-backup').toggle($(this).prop("checked"));
+            });
+
+            $('#settings-backup-auto-strategy').change(function()
+            {
+                onAutoBackupStrategyChange(this.selectedIndex);
+            });
+
+            $('#settings-backup-auto-git-test').click(function()
+            {
+                $.ajax({
+                    type: 'POST',
+                    url: $('#settings-backup-auto-git-test').attr('data-url'),
+                    data: {
+                        '_csrf': document.getElementById('token').value,
+                        'autoBackupGitUrl': document.getElementById('settings-backup-auto-git-url').value,
+                        'autoBackupGitBranchName': document.getElementById('settings-backup-auto-git-branch-name').value,
+                        'autoBackupGitUserName': document.getElementById('settings-backup-auto-git-user-name').value,
+                        'autoBackupGitToken': document.getElementById('settings-backup-auto-git-token').value,
+                    },
+                    success: function(data)
+                    {
+                        let parsedData = JSON.parse(data);
+                        let isValidConnection = parsedData['isValidConnection']
+                        M.toast({
+                            html: parsedData['localizedMessage'],
+                            classes: isValidConnection ? 'green': 'red'
+                        });
+                    },
+                    error: function(data)
+                    {
+                        M.toast({
+                            html: 'Error: ' + data,
+                            classes: 'red'
+                        });
+                    }
+                });
+            });
+
+            var autoBackupDays = $('#settings-backup-auto-days');
+            if(autoBackupDays.length)
+            {
+                autoBackupDays.on('change keydown paste input', function()
+                {
+                    validateNumber(autoBackupDays.val(), 'settings-backup-auto-days', "hidden-settings-backup-auto-days", numberValidationMessage, REGEX_NUMBER_GREATER_ZERO);
+                });
+            }
+
+            var autoBackupFilesToKeep = $('#settings-backup-auto-files-to-keep');
+            if(autoBackupFilesToKeep.length)
+            {
+                autoBackupFilesToKeep.on('change keydown paste input', function()
+                {
+                    validateNumber(autoBackupFilesToKeep.val(), "settings-backup-auto-files-to-keep", "hidden-settings-backup-auto-files-to-keep", numberValidationMessageZeroAllowed, REGEX_NUMBER);
+                });
+            }
+
+            $('#settings-backup-run-now').click(function()
+            {
+                document.getElementById('runBackupInput').value = 1;
+            });
+
+            var autoBackupCheckbox = document.getElementsByName("autoBackupActivated")[0];
+            $('#settings-auto-backup').toggle(autoBackupCheckbox.checked);
+            onAutoBackupStrategyChange(document.getElementById('settings-backup-auto-strategy').selectedIndex);
+
+
+            // toggle unsaved changes warning
+
+            $('input[name="backupReminderActivated"]').change(function()
+            {
+                toggleSettingsContainerHeader('backupSettingsContainerHeader', false);
+            });
+
+            $('input[name="autoBackupActivated"]').change(function()
+            {
+                toggleSettingsContainerHeader('backupSettingsContainerHeader', false);
+            });
+
+            $('#settings-backup-auto-days').on('change keydown paste input', function()
+            {
+                toggleSettingsContainerHeader('backupSettingsContainerHeader', false);
+            });
+
+            $('#settings-backup-auto-time').change(function()
+            {
+                toggleSettingsContainerHeader('backupSettingsContainerHeader', false);
+            });
+
+            $('#settings-backup-auto-strategy').change(function()
+            {
+                toggleSettingsContainerHeader('backupSettingsContainerHeader', false);
+            });
+
+            $('#settings-backup-auto-files-to-keep').on('change keydown paste input', function()
+            {
+                toggleSettingsContainerHeader('backupSettingsContainerHeader', false);
+            });
+
+            $('#settings-backup-auto-git-url').on('change keydown paste input', function()
+            {
+                toggleSettingsContainerHeader('backupSettingsContainerHeader', false);
+            });
+
+            $('#settings-backup-auto-git-branch-name').on('change keydown paste input', function()
+            {
+                toggleSettingsContainerHeader('backupSettingsContainerHeader', false);
+            });
+
+            $('#settings-backup-auto-git-user-name').on('change keydown paste input', function()
+            {
+                toggleSettingsContainerHeader('backupSettingsContainerHeader', false);
+            });
+
+            $('#settings-backup-auto-git-token').on('change keydown paste input', function()
+            {
+                toggleSettingsContainerHeader('backupSettingsContainerHeader', false);
+            });
+        </script>
+    </@settingsContainerMacros.settingsContainer>
+</#macro>
+
+<@backupSettingsContainer importScripts=true settings=settings/>
diff --git a/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsContainer.ftl b/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsContainer.ftl
index 41f9aa95d..282aaae51 100644
--- a/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsContainer.ftl
+++ b/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsContainer.ftl
@@ -1,7 +1,7 @@
 <#import "/spring.ftl" as s>
 
-<#macro settingsContainer formName containerId importScripts>
-    <form name="${formName}" method="post">
+<#macro settingsContainer formName containerId importScripts validateForm=false>
+    <form name="${formName}" method="post" <#if validateForm>onsubmit="return validateForm()"</#if>>
         <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" id="token"/>
 
         <#nested>
diff --git a/BudgetMasterServer/src/main/resources/templates/settings/settings.ftl b/BudgetMasterServer/src/main/resources/templates/settings/settings.ftl
index 8246e214d..6d9c42368 100644
--- a/BudgetMasterServer/src/main/resources/templates/settings/settings.ftl
+++ b/BudgetMasterServer/src/main/resources/templates/settings/settings.ftl
@@ -17,6 +17,7 @@
         <#import "containers/settingsSecurity.ftl" as settingsSecurityMacros>
         <#import "containers/settingsPersonalization.ftl" as settingsPersonalizationMacros>
         <#import "containers/settingsTransactions.ftl" as settingsTransactionsMacros>
+        <#import "containers/settingsBackup.ftl" as settingsBackupMacros>
         <#import "containers/settingsUpdate.ftl" as settingsUpdateMacros>
         <#import "containers/settingsMisc.ftl" as settingsMiscMacros>
 
@@ -50,40 +51,8 @@
                                         <@settingsTransactionsMacros.transactionsSettingsContainer importScripts=false settings=settings/>
                                     </@settingsMacros.settingsCollapsibleItem>
 
-                                    <@settingsMacros.settingsCollapsibleItem "" "cloud_download" locale.getString("settings.backup")>
-                                        <div class="row">
-                                            <div class="col s12">
-                                                <div class="table-container">
-                                                    <div class="table-cell">
-                                                        <div class="switch-cell-margin">${locale.getString("settings.backupReminder")}</div>
-                                                        <div class="switch-cell-margin">${locale.getString("settings.backup.auto")}</div>
-                                                    </div>
-                                                    <div class="table-cell table-cell-spacer"></div>
-                                                    <div class="table-cell">
-                                                        <@settingsMacros.switch "backupReminder" "backupReminderActivated" settings.getBackupReminderActivated()/>
-                                                        <@settingsMacros.switch "backup.auto" "autoBackupActivated" settings.isAutoBackupActive()/>
-                                                    </div>
-                                                    <div class="table-cell table-cell-spacer"></div>
-                                                    <div class="table-cell">
-                                                        <div class="switch-cell-margin">
-                                                            <a class="btn btn-flat tooltipped text-default" data-position="bottom" data-tooltip="${locale.getString("settings.backupReminder.description")}"><i class="material-icons">help_outline</i></a>
-                                                        </div>
-                                                        <div class="switch-cell-margin">
-                                                            <a class="btn btn-flat tooltipped text-default" data-position="bottom" data-tooltip="${locale.getString("settings.backup.auto.description")}"><i class="material-icons">help_outline</i></a>
-                                                        </div>
-                                                    </div>
-                                                </div>
-                                            </div>
-                                        </div>
-
-                                         <#-- auto backup -->
-                                        <@settingsMacros.autoBackup/>
-
-                                        <div class="row">
-                                            <div class="col s12 center-align">
-                                                <@header.buttonSubmit name='action' icon='save' localizationKey='save' color='background-green'/>
-                                            </div>
-                                        </div>
+                                    <@settingsMacros.settingsCollapsibleItem "backupSettingsContainer" "cloud_download" locale.getString("settings.backup")>
+                                        <@settingsBackupMacros.backupSettingsContainer importScripts=false settings=settings/>
                                     </@settingsMacros.settingsCollapsibleItem>
 
                                     <@settingsMacros.settingsCollapsibleItem "updateSettingsContainer" "system_update" locale.getString("settings.updates")>
@@ -144,8 +113,9 @@
             initSettingsContainer('SecuritySettingsContainer', 'securitySettingsContainer');
             initSettingsContainer('PersonalizationSettingsContainer', 'personalizationSettingsContainer');
             initSettingsContainer('TransactionsSettingsContainer', 'transactionsSettingsContainer');
-            initSettingsContainer('UpdateSettingsContainer', 'updateSettingsContainer');
+            initSettingsContainer('BackupSettingsContainer', 'backupSettingsContainer');
             initSettingsContainer('MiscSettingsContainer', 'miscSettingsContainer');
+            initSettingsContainer('UpdateSettingsContainer', 'updateSettingsContainer');
         </script>
     </@header.body>
 </html>
diff --git a/BudgetMasterServer/src/main/resources/templates/settings/settingsMacros.ftl b/BudgetMasterServer/src/main/resources/templates/settings/settingsMacros.ftl
index 79f231c53..efd2b3fa8 100644
--- a/BudgetMasterServer/src/main/resources/templates/settings/settingsMacros.ftl
+++ b/BudgetMasterServer/src/main/resources/templates/settings/settingsMacros.ftl
@@ -182,7 +182,7 @@
 
         <div class="input-field col s12 m12 l8 offset-l2">
             <i class="material-icons prefix">schedule</i>
-            <select id="settings-backup-auto-time" name="autoBackupTime" <@validation.validation "autoBackupTime"/>>
+            <select id="settings-backup-auto-time" name="autoBackupTimeType" <@validation.validation "autoBackupTime"/>>
                 <#list autoBackupTimes as time>
                     <#if settings.getAutoBackupTime() == time>
                         <option selected value="${time}">${time.getLocalized()}</option>
@@ -291,12 +291,12 @@
             <a target="_blank" href="${locale.getString("settings.backup.auto.strategy.git.remote.help.url")}" class="waves-effect waves-light btn btn-flat text-default"><i class="material-icons left">help_outline</i>${locale.getString("settings.backup.auto.strategy.git.remote.help")}</a>
         </div>
     </div>
-    <div class="row">
-        <div class="col s12 m12 l8 offset-l2 center-align">
-            <input id="runBackupInput" type="hidden" name="runBackup" value="0">
-            <@header.buttonSubmit id='settings-backup-run-now' name='action' icon='cloud_download' localizationKey='settings.backup.auto.run.now'/>
-        </div>
-    </div>
+<#--    <div class="row">-->
+<#--        <div class="col s12 m12 l8 offset-l2 center-align">-->
+<#--            <input id="runBackupInput" type="hidden" name="runBackup" value="0">-->
+<#--            <@header.buttonSubmit id='settings-backup-run-now' name='action' icon='cloud_download' localizationKey='settings.backup.auto.run.now' formaction='/settings/save/backup'/>-->
+<#--        </div>-->
+<#--    </div>-->
 </#macro>
 
 <#macro settingsCollapsibleItem id icon title isFontAwesomeIcon=false>
-- 
GitLab