diff --git a/build.gradle b/build.gradle index 1c04334bb5c..90e130f6c4a 100644 --- a/build.gradle +++ b/build.gradle @@ -36,7 +36,9 @@ sourceSets { if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") { exclude "stirling/software/SPDF/config/security/**" exclude "stirling/software/SPDF/controller/api/UserController.java" + exclude "stirling/software/SPDF/controller/api/DatabaseController.java" exclude "stirling/software/SPDF/controller/web/AccountWebController.java" + exclude "stirling/software/SPDF/controller/web/DatabaseWebController.java" exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java" exclude "stirling/software/SPDF/model/Authority.java" exclude "stirling/software/SPDF/model/PersistentLogin.java" @@ -120,6 +122,7 @@ dependencies { //2.2.x requires rebuild of DB file.. need migration path implementation "com.h2database:h2:2.1.214" + // implementation "com.h2database:h2:2.2.224" } testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion" diff --git a/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java b/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java index e568b327c12..8c1ed05f2a3 100644 --- a/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java +++ b/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java @@ -22,7 +22,8 @@ public class CleanUrlInterceptor implements HandlerInterceptor { "error", "erroroauth", "file", - "messageType"); + "messageType", + "infoMessage"); @Override public boolean preHandle( diff --git a/src/main/java/stirling/software/SPDF/config/DatabaseBackupInterface.java b/src/main/java/stirling/software/SPDF/config/DatabaseBackupInterface.java new file mode 100644 index 00000000000..267981d1888 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/DatabaseBackupInterface.java @@ -0,0 +1,16 @@ +package stirling.software.SPDF.config; + +import java.io.IOException; +import java.util.List; + +import stirling.software.SPDF.utils.FileInfo; + +public interface DatabaseBackupInterface { + void exportDatabase() throws IOException; + + boolean importDatabase(); + + boolean hasBackup(); + + List getBackupList(); +} diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java index e696c6bc19a..689b5df03fe 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -6,28 +6,33 @@ import java.util.UUID; import org.simpleyaml.configuration.file.YamlFile; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.config.DatabaseBackupInterface; import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.Role; @Component +@Slf4j public class InitialSecuritySetup { @Autowired private UserService userService; @Autowired private ApplicationProperties applicationProperties; - private static final Logger logger = LoggerFactory.getLogger(InitialSecuritySetup.class); + @Autowired private DatabaseBackupInterface databaseBackupHelper; @PostConstruct - public void init() { - if (!userService.hasUsers()) { + public void init() throws IllegalArgumentException, IOException { + if (databaseBackupHelper.hasBackup() && !userService.hasUsers()) { + databaseBackupHelper.importDatabase(); + } else if (!userService.hasUsers()) { initializeAdminUser(); + } else { + databaseBackupHelper.exportDatabase(); } initializeInternalApiUser(); } @@ -41,12 +46,11 @@ public void initSecretKey() throws IOException { } } - private void initializeAdminUser() { + private void initializeAdminUser() throws IOException { String initialUsername = applicationProperties.getSecurity().getInitialLogin().getUsername(); String initialPassword = applicationProperties.getSecurity().getInitialLogin().getPassword(); - if (initialUsername != null && !initialUsername.isEmpty() && initialPassword != null @@ -54,9 +58,9 @@ private void initializeAdminUser() { && !userService.findByUsernameIgnoreCase(initialUsername).isPresent()) { try { userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); - logger.info("Admin user created: " + initialUsername); + log.info("Admin user created: " + initialUsername); } catch (IllegalArgumentException e) { - logger.error("Failed to initialize security setup", e); + log.error("Failed to initialize security setup", e); System.exit(1); } } else { @@ -64,23 +68,23 @@ private void initializeAdminUser() { } } - private void createDefaultAdminUser() { + private void createDefaultAdminUser() throws IllegalArgumentException, IOException { String defaultUsername = "admin"; String defaultPassword = "stirling"; if (!userService.findByUsernameIgnoreCase(defaultUsername).isPresent()) { userService.saveUser(defaultUsername, defaultPassword, Role.ADMIN.getRoleId(), true); - logger.info("Default admin user created: " + defaultUsername); + log.info("Default admin user created: " + defaultUsername); } } - private void initializeInternalApiUser() { + private void initializeInternalApiUser() throws IllegalArgumentException, IOException { if (!userService.usernameExistsIgnoreCase(Role.INTERNAL_API_USER.getRoleId())) { userService.saveUser( Role.INTERNAL_API_USER.getRoleId(), UUID.randomUUID().toString(), Role.INTERNAL_API_USER.getRoleId()); userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId()); - logger.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId()); + log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId()); } } diff --git a/src/main/java/stirling/software/SPDF/config/security/UserService.java b/src/main/java/stirling/software/SPDF/config/security/UserService.java index 0a6898f8ae6..f6cdfb91a36 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserService.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -1,5 +1,6 @@ package stirling.software.SPDF.config.security; +import java.io.IOException; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -19,6 +20,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import stirling.software.SPDF.config.DatabaseBackupInterface; import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface; import stirling.software.SPDF.model.AuthenticationType; import stirling.software.SPDF.model.Authority; @@ -38,8 +40,11 @@ public class UserService implements UserServiceInterface { @Autowired private MessageSource messageSource; + @Autowired DatabaseBackupInterface databaseBackupHelper; + // Handle OAUTH2 login and user auto creation. - public boolean processOAuth2PostLogin(String username, boolean autoCreateUser) { + public boolean processOAuth2PostLogin(String username, boolean autoCreateUser) + throws IllegalArgumentException, IOException { if (!isUsernameValid(username)) { return false; } @@ -131,7 +136,7 @@ public boolean validateApiKeyForUser(String username, String apiKey) { } public void saveUser(String username, AuthenticationType authenticationType) - throws IllegalArgumentException { + throws IllegalArgumentException, IOException { if (!isUsernameValid(username)) { throw new IllegalArgumentException(getInvalidUsernameMessage()); } @@ -142,9 +147,11 @@ public void saveUser(String username, AuthenticationType authenticationType) user.addAuthority(new Authority(Role.USER.getRoleId(), user)); user.setAuthenticationType(authenticationType); userRepository.save(user); + databaseBackupHelper.exportDatabase(); } - public void saveUser(String username, String password) throws IllegalArgumentException { + public void saveUser(String username, String password) + throws IllegalArgumentException, IOException { if (!isUsernameValid(username)) { throw new IllegalArgumentException(getInvalidUsernameMessage()); } @@ -154,10 +161,11 @@ public void saveUser(String username, String password) throws IllegalArgumentExc user.setEnabled(true); user.setAuthenticationType(AuthenticationType.WEB); userRepository.save(user); + databaseBackupHelper.exportDatabase(); } public void saveUser(String username, String password, String role, boolean firstLogin) - throws IllegalArgumentException { + throws IllegalArgumentException, IOException { if (!isUsernameValid(username)) { throw new IllegalArgumentException(getInvalidUsernameMessage()); } @@ -169,10 +177,11 @@ public void saveUser(String username, String password, String role, boolean firs user.setAuthenticationType(AuthenticationType.WEB); user.setFirstLogin(firstLogin); userRepository.save(user); + databaseBackupHelper.exportDatabase(); } public void saveUser(String username, String password, String role) - throws IllegalArgumentException { + throws IllegalArgumentException, IOException { saveUser(username, password, role, false); } @@ -206,7 +215,8 @@ public boolean hasUsers() { return userCount > 0; } - public void updateUserSettings(String username, Map updates) { + public void updateUserSettings(String username, Map updates) + throws IOException { Optional userOpt = userRepository.findByUsernameIgnoreCase(username); if (userOpt.isPresent()) { User user = userOpt.get(); @@ -220,6 +230,7 @@ public void updateUserSettings(String username, Map updates) { user.setSettings(settingsMap); userRepository.save(user); + databaseBackupHelper.exportDatabase(); } } @@ -235,22 +246,26 @@ public Authority findRole(User user) { return authorityRepository.findByUserId(user.getId()); } - public void changeUsername(User user, String newUsername) throws IllegalArgumentException { + public void changeUsername(User user, String newUsername) + throws IllegalArgumentException, IOException { if (!isUsernameValid(newUsername)) { throw new IllegalArgumentException(getInvalidUsernameMessage()); } user.setUsername(newUsername); userRepository.save(user); + databaseBackupHelper.exportDatabase(); } - public void changePassword(User user, String newPassword) { + public void changePassword(User user, String newPassword) throws IOException { user.setPassword(passwordEncoder.encode(newPassword)); userRepository.save(user); + databaseBackupHelper.exportDatabase(); } - public void changeFirstUse(User user, boolean firstUse) { + public void changeFirstUse(User user, boolean firstUse) throws IOException { user.setFirstLogin(firstUse); userRepository.save(user); + databaseBackupHelper.exportDatabase(); } public void changeRole(User user, String newRole) { diff --git a/src/main/java/stirling/software/SPDF/config/security/database/DatabaseBackupHelper.java b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseBackupHelper.java new file mode 100644 index 00000000000..026a9684357 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseBackupHelper.java @@ -0,0 +1,202 @@ +package stirling.software.SPDF.config.security.database; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.config.DatabaseBackupInterface; +import stirling.software.SPDF.utils.FileInfo; + +@Slf4j +@Configuration +public class DatabaseBackupHelper implements DatabaseBackupInterface { + + @Value("${spring.datasource.url}") + private String url; + + private Path backupPath = Paths.get("configs/db/backup/"); + + @Override + public boolean hasBackup() { + // Check if there is at least one backup + return !getBackupList().isEmpty(); + } + + @Override + public List getBackupList() { + // Check if the backup directory exists, and create it if it does not + ensureBackupDirectoryExists(); + + List backupFiles = new ArrayList<>(); + + // Read the backup directory and filter for files with the prefix "backup_" and suffix + // ".sql" + try (DirectoryStream stream = + Files.newDirectoryStream( + backupPath, + path -> + path.getFileName().toString().startsWith("backup_") + && path.getFileName().toString().endsWith(".sql"))) { + for (Path entry : stream) { + BasicFileAttributes attrs = Files.readAttributes(entry, BasicFileAttributes.class); + LocalDateTime modificationDate = + LocalDateTime.ofInstant( + attrs.lastModifiedTime().toInstant(), ZoneId.systemDefault()); + LocalDateTime creationDate = + LocalDateTime.ofInstant( + attrs.creationTime().toInstant(), ZoneId.systemDefault()); + long fileSize = attrs.size(); + backupFiles.add( + new FileInfo( + entry.getFileName().toString(), + entry.toString(), + modificationDate, + fileSize, + creationDate)); + } + } catch (IOException e) { + log.error("Error reading backup directory: {}", e.getMessage(), e); + } + return backupFiles; + } + + // Imports a database backup from the specified file. + public boolean importDatabaseFromUI(String fileName) throws IOException { + return this.importDatabaseFromUI(getBackupFilePath(fileName)); + } + + // Imports a database backup from the specified path. + public boolean importDatabaseFromUI(Path tempTemplatePath) throws IOException { + boolean success = executeDatabaseScript(tempTemplatePath); + if (success) { + LocalDateTime dateNow = LocalDateTime.now(); + DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm"); + Path insertOutputFilePath = + this.getBackupFilePath("backup_user_" + dateNow.format(myFormatObj) + ".sql"); + Files.copy(tempTemplatePath, insertOutputFilePath); + Files.deleteIfExists(tempTemplatePath); + } + return success; + } + + @Override + public boolean importDatabase() { + if (!this.hasBackup()) return false; + + List backupList = this.getBackupList(); + backupList.sort(Comparator.comparing(FileInfo::getModificationDate).reversed()); + + return executeDatabaseScript(Paths.get(backupList.get(0).getFilePath())); + } + + @Override + public void exportDatabase() throws IOException { + // Check if the backup directory exists, and create it if it does not + ensureBackupDirectoryExists(); + + // Filter and delete old backups if there are more than 5 + List filteredBackupList = + this.getBackupList().stream() + .filter(backup -> !backup.getFileName().startsWith("backup_user_")) + .collect(Collectors.toList()); + + if (filteredBackupList.size() > 5) { + filteredBackupList.sort( + Comparator.comparing( + p -> p.getFileName().substring(7, p.getFileName().length() - 4))); + Files.deleteIfExists(Paths.get(filteredBackupList.get(0).getFilePath())); + log.info("Deleted oldest backup: {}", filteredBackupList.get(0).getFileName()); + } + + LocalDateTime dateNow = LocalDateTime.now(); + DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm"); + Path insertOutputFilePath = + this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql"); + String query = "SCRIPT SIMPLE COLUMNS DROP to '" + insertOutputFilePath.toString() + "';"; + + try (Connection conn = DriverManager.getConnection(url, "sa", ""); + Statement stmt = conn.createStatement()) { + stmt.execute(query); + log.info("Database export completed: {}", insertOutputFilePath); + } catch (SQLException e) { + log.error("Error during database export: {}", e.getMessage(), e); + } + } + + // Retrieves the H2 database version. + public String getH2Version() { + String version = "Unknown"; + try (Connection conn = DriverManager.getConnection(url, "sa", "")) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) { + if (rs.next()) { + version = rs.getString("version"); + log.info("H2 Database Version: {}", version); + } + } + } catch (SQLException e) { + log.error("Error retrieving H2 version: {}", e.getMessage(), e); + } + return version; + } + + // Deletes a backup file. + public boolean deleteBackupFile(String fileName) throws IOException { + Path filePath = this.getBackupFilePath(fileName); + if (Files.deleteIfExists(filePath)) { + log.info("Deleted backup file: {}", fileName); + return true; + } else { + log.error("File not found or could not be deleted: {}", fileName); + return false; + } + } + + // Gets the Path object for a given backup file name. + public Path getBackupFilePath(String fileName) { + return Paths.get(backupPath.toString(), fileName); + } + + private boolean executeDatabaseScript(Path scriptPath) { + try (Connection conn = DriverManager.getConnection(url, "sa", ""); + Statement stmt = conn.createStatement()) { + + String query = "RUNSCRIPT from '" + scriptPath.toString() + "';"; + stmt.execute(query); + log.info("Database import completed: {}", scriptPath); + return true; + } catch (SQLException e) { + log.error("Error during database import: {}", e.getMessage(), e); + return false; + } + } + + private void ensureBackupDirectoryExists() { + if (Files.notExists(backupPath)) { + try { + Files.createDirectories(backupPath); + } catch (IOException e) { + log.error("Error creating directories: {}", e.getMessage()); + } + } + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/database/ScheduledTasks.java b/src/main/java/stirling/software/SPDF/config/security/database/ScheduledTasks.java new file mode 100644 index 00000000000..2ddc47e12a3 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/database/ScheduledTasks.java @@ -0,0 +1,18 @@ +package stirling.software.SPDF.config.security.database; + +import java.io.IOException; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +public class ScheduledTasks { + + @Autowired private DatabaseBackupHelper databaseBackupService; + + @Scheduled(cron = "0 0 0 * * ?") + public void performBackup() throws IOException { + databaseBackupService.exportDatabase(); + } +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/DatabaseController.java b/src/main/java/stirling/software/SPDF/controller/api/DatabaseController.java new file mode 100644 index 00000000000..2f7e207bcfd --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/DatabaseController.java @@ -0,0 +1,144 @@ +package stirling.software.SPDF.controller.api; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +import org.eclipse.jetty.http.HttpStatus; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.config.security.database.DatabaseBackupHelper; + +@Slf4j +@Controller +@RequestMapping("/api/v1/database") +@PreAuthorize("hasRole('ROLE_ADMIN')") +@Tag(name = "Database", description = "Database APIs") +public class DatabaseController { + + @Autowired DatabaseBackupHelper databaseBackupHelper; + + @Hidden + @PostMapping(consumes = "multipart/form-data", value = "import-database") + @Operation( + summary = "Import database backup", + description = "This endpoint imports a database backup from a SQL file.") + public String importDatabase( + @RequestParam("fileInput") MultipartFile file, RedirectAttributes redirectAttributes) + throws IllegalArgumentException, IOException { + if (file == null || file.isEmpty()) { + redirectAttributes.addAttribute("error", "fileNullOrEmpty"); + return "redirect:/database"; + } + log.info("Received file: {}", file.getOriginalFilename()); + Path tempTemplatePath = Files.createTempFile("backup_", ".sql"); + try (InputStream in = file.getInputStream()) { + Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING); + boolean importSuccess = databaseBackupHelper.importDatabaseFromUI(tempTemplatePath); + if (importSuccess) { + redirectAttributes.addAttribute("infoMessage", "importIntoDatabaseSuccessed"); + } else { + redirectAttributes.addAttribute("error", "failedImportFile"); + } + } catch (Exception e) { + log.error("Error importing database: {}", e.getMessage()); + redirectAttributes.addAttribute("error", "failedImportFile"); + } + return "redirect:/database"; + } + + @Hidden + @GetMapping("/import-database-file/{fileName}") + public String importDatabaseFromBackupUI(@PathVariable String fileName) + throws IllegalArgumentException, IOException { + if (fileName == null || fileName.isEmpty()) { + return "redirect:/database?error=fileNullOrEmpty"; + } + + // Check if the file exists in the backup list + boolean fileExists = + databaseBackupHelper.getBackupList().stream() + .anyMatch(backup -> backup.getFileName().equals(fileName)); + if (!fileExists) { + log.error("File {} not found in backup list", fileName); + return "redirect:/database?error=fileNotFound"; + } + log.info("Received file: {}", fileName); + if (databaseBackupHelper.importDatabaseFromUI(fileName)) { + log.info("File {} imported to database", fileName); + return "redirect:/database?infoMessage=importIntoDatabaseSuccessed"; + } + return "redirect:/database?error=failedImportFile"; + } + + @Hidden + @GetMapping("/delete/{fileName}") + @Operation( + summary = "Delete a database backup file", + description = + "This endpoint deletes a database backup file with the specified file name.") + public String deleteFile(@PathVariable String fileName) { + if (fileName == null || fileName.isEmpty()) { + throw new IllegalArgumentException("File must not be null or empty"); + } + try { + if (databaseBackupHelper.deleteBackupFile(fileName)) { + log.info("Deleted file: {}", fileName); + } else { + log.error("Failed to delete file: {}", fileName); + return "redirect:/database?error=failedToDeleteFile"; + } + } catch (IOException e) { + log.error("Error deleting file: {}", e.getMessage()); + return "redirect:/database?error=" + e.getMessage(); + } + return "redirect:/database"; + } + + @Hidden + @GetMapping("/download/{fileName}") + @Operation( + summary = "Download a database backup file", + description = + "This endpoint downloads a database backup file with the specified file name.") + public ResponseEntity downloadFile(@PathVariable String fileName) { + if (fileName == null || fileName.isEmpty()) { + throw new IllegalArgumentException("File must not be null or empty"); + } + try { + Path filePath = databaseBackupHelper.getBackupFilePath(fileName); + InputStreamResource resource = new InputStreamResource(Files.newInputStream(filePath)); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .contentLength(Files.size(filePath)) + .body(resource); + } catch (IOException e) { + log.error("Error downloading file: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.SEE_OTHER_303) + .location(URI.create("/database?error=downloadFailed")) + .build(); + } + } +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/UserController.java b/src/main/java/stirling/software/SPDF/controller/api/UserController.java index ec316fbc4fb..b0deefa0685 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/UserController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/UserController.java @@ -1,5 +1,6 @@ package stirling.software.SPDF.controller.api; +import java.io.IOException; import java.security.Principal; import java.util.HashMap; import java.util.Map; @@ -42,7 +43,8 @@ public class UserController { @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/register") - public String register(@ModelAttribute UsernameAndPass requestModel, Model model) { + public String register(@ModelAttribute UsernameAndPass requestModel, Model model) + throws IOException { if (userService.usernameExistsIgnoreCase(requestModel.getUsername())) { model.addAttribute("error", "Username already exists"); return "register"; @@ -63,7 +65,8 @@ public RedirectView changeUsername( @RequestParam(name = "newUsername") String newUsername, HttpServletRequest request, HttpServletResponse response, - RedirectAttributes redirectAttributes) { + RedirectAttributes redirectAttributes) + throws IOException { if (!userService.isUsernameValid(newUsername)) { return new RedirectView("/account?messageType=invalidUsername", true); @@ -116,7 +119,8 @@ public RedirectView changePasswordOnLogin( @RequestParam(name = "newPassword") String newPassword, HttpServletRequest request, HttpServletResponse response, - RedirectAttributes redirectAttributes) { + RedirectAttributes redirectAttributes) + throws IOException { if (principal == null) { return new RedirectView("/change-creds?messageType=notAuthenticated", true); } @@ -149,7 +153,8 @@ public RedirectView changePassword( @RequestParam(name = "newPassword") String newPassword, HttpServletRequest request, HttpServletResponse response, - RedirectAttributes redirectAttributes) { + RedirectAttributes redirectAttributes) + throws IOException { if (principal == null) { return new RedirectView("/account?messageType=notAuthenticated", true); } @@ -176,7 +181,8 @@ public RedirectView changePassword( @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/updateUserSettings") - public String updateUserSettings(HttpServletRequest request, Principal principal) { + public String updateUserSettings(HttpServletRequest request, Principal principal) + throws IOException { Map paramMap = request.getParameterMap(); Map updates = new HashMap<>(); @@ -201,7 +207,8 @@ public RedirectView saveUser( @RequestParam(name = "password") String password, @RequestParam(name = "role") String role, @RequestParam(name = "forceChange", required = false, defaultValue = "false") - boolean forceChange) { + boolean forceChange) + throws IllegalArgumentException, IOException { if (!userService.isUsernameValid(username)) { return new RedirectView("/addUsers?messageType=invalidUsername", true); diff --git a/src/main/java/stirling/software/SPDF/controller/web/DatabaseWebController.java b/src/main/java/stirling/software/SPDF/controller/web/DatabaseWebController.java new file mode 100644 index 00000000000..3fd68ad5960 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/web/DatabaseWebController.java @@ -0,0 +1,41 @@ +package stirling.software.SPDF.controller.web; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +import io.swagger.v3.oas.annotations.tags.Tag; + +import jakarta.servlet.http.HttpServletRequest; +import stirling.software.SPDF.config.security.database.DatabaseBackupHelper; +import stirling.software.SPDF.utils.FileInfo; + +@Controller +@Tag(name = "Database Management", description = "Database management and security APIs") +public class DatabaseWebController { + + @Autowired private DatabaseBackupHelper databaseBackupHelper; + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/database") + public String database(HttpServletRequest request, Model model, Authentication authentication) { + String error = request.getParameter("error"); + String confirmed = request.getParameter("infoMessage"); + + if (error != null) { + model.addAttribute("error", error); + } else if (confirmed != null) { + model.addAttribute("infoMessage", confirmed); + } + + List backupList = databaseBackupHelper.getBackupList(); + model.addAttribute("systemUpdate", backupList); + + return "database"; + } +} diff --git a/src/main/java/stirling/software/SPDF/utils/FileInfo.java b/src/main/java/stirling/software/SPDF/utils/FileInfo.java new file mode 100644 index 00000000000..4e236756a80 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/utils/FileInfo.java @@ -0,0 +1,50 @@ +package stirling.software.SPDF.utils; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@AllArgsConstructor +@Data +public class FileInfo { + private String fileName; + private String filePath; + private LocalDateTime modificationDate; + private long fileSize; + private LocalDateTime creationDate; + + private static final DateTimeFormatter DATE_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + // Converts the file path string to a Path object. + public Path getFilePathAsPath() { + return Paths.get(filePath); + } + + // Formats the file size into a human-readable string. + public String getFormattedFileSize() { + if (fileSize >= 1024 * 1024 * 1024) { + return String.format("%.2f GB", fileSize / (1024.0 * 1024 * 1024)); + } else if (fileSize >= 1024 * 1024) { + return String.format("%.2f MB", fileSize / (1024.0 * 1024)); + } else if (fileSize >= 1024) { + return String.format("%.2f KB", fileSize / 1024.0); + } else { + return String.format("%d Bytes", fileSize); + } + } + + // Formats the modification date to a string. + public String getFormattedModificationDate() { + return modificationDate.format(DATE_FORMATTER); + } + + // Formats the creation date to a string. + public String getFormattedCreationDate() { + return creationDate.format(DATE_FORMATTER); + } +} diff --git a/src/main/resources/messages_ar_AR.properties b/src/main/resources/messages_ar_AR.properties index 66902b18a12..4633c9f64ba 100644 --- a/src/main/resources/messages_ar_AR.properties +++ b/src/main/resources/messages_ar_AR.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Save User adminUserSettings.changeUserRole=تغيير دور المستخدم adminUserSettings.authenticated=Authenticated + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_bg_BG.properties b/src/main/resources/messages_bg_BG.properties index ac4971deb5f..3ab9cb477f2 100644 --- a/src/main/resources/messages_bg_BG.properties +++ b/src/main/resources/messages_bg_BG.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Съхранете потребителя adminUserSettings.changeUserRole=Промяна на ролята на потребителя adminUserSettings.authenticated=Удостоверен + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_ca_CA.properties b/src/main/resources/messages_ca_CA.properties index f6003fbaf0b..ce4b3c964ef 100644 --- a/src/main/resources/messages_ca_CA.properties +++ b/src/main/resources/messages_ca_CA.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Desar Usuari adminUserSettings.changeUserRole=Canvia el rol de l'usuari adminUserSettings.authenticated=Authenticated + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_cs_CZ.properties b/src/main/resources/messages_cs_CZ.properties index 9df08b56919..64774be0f01 100644 --- a/src/main/resources/messages_cs_CZ.properties +++ b/src/main/resources/messages_cs_CZ.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Uložit Uživatele adminUserSettings.changeUserRole=Zmenit Roli Uživatele adminUserSettings.authenticated=Ověřeno + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_de_DE.properties b/src/main/resources/messages_de_DE.properties index e685e7f5cd8..2082599211c 100644 --- a/src/main/resources/messages_de_DE.properties +++ b/src/main/resources/messages_de_DE.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Benutzer speichern adminUserSettings.changeUserRole=Benutzerrolle ändern adminUserSettings.authenticated=Authentifiziert + +database.title=Datenbank Import/Export +database.header=Datenbank Import/Export +database.fileName=Dateiname +database.creationDate=Erstellungsdatum +database.fileSize=Dateigröße +database.deleteBackupFile=Sicherungsdatei löschen +database.importBackupFile=Sicherungsdatei importieren +database.downloadBackupFile=Sicherungsdatei herunterladen +database.info_1=Beim Importieren der Daten ist es von größter Bedeutung, die korrekte Struktur zu gewährleisten. Wenn Sie nicht sicher sind, was Sie tun, suchen Sie Rat und Unterstützung von einem Fachmann. Ein Fehler in der Struktur kann zu Fehlfunktionen der Anwendung führen, bis hin zur vollständigen Nicht-Lauffähigkeit der Anwendung. +database.info_2=Der Dateiname spielt beim Hochladen keine Rolle. Dieser wird nachträglich in das Format backup_user_yyyyMMddHHmm.sql geändert, um eine einheitliche Benennung zu gewährleisten. +database.submit=Sicherungsdatei importieren +database.importIntoDatabaseSuccessed=Import in die Datenbank erfolgreich +database.fileNotFound=Datei nicht gefunden +database.fileNullOrEmpty=Datei darf nicht null oder leer sein +database.failedImportFile=Dateiimport fehlgeschlagen + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_el_GR.properties b/src/main/resources/messages_el_GR.properties index c140c12c5a6..b7bfabdb4f4 100644 --- a/src/main/resources/messages_el_GR.properties +++ b/src/main/resources/messages_el_GR.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Αποθήκευση Χρήστη adminUserSettings.changeUserRole=Αλλαγή ρόλου χρήστη adminUserSettings.authenticated=Authenticated + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 744cbbd7608..7add06f62aa 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Save User adminUserSettings.changeUserRole=Change User's Role adminUserSettings.authenticated=Authenticated + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed to import file + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_en_US.properties b/src/main/resources/messages_en_US.properties index 6717272be0d..5f20e450c60 100644 --- a/src/main/resources/messages_en_US.properties +++ b/src/main/resources/messages_en_US.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Save User adminUserSettings.changeUserRole=Change User's Role adminUserSettings.authenticated=Authenticated + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_es_ES.properties b/src/main/resources/messages_es_ES.properties index 0c0504602b9..94e668777fb 100644 --- a/src/main/resources/messages_es_ES.properties +++ b/src/main/resources/messages_es_ES.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Guardar Usuario adminUserSettings.changeUserRole=Cambiar rol de usuario adminUserSettings.authenticated=Authenticated + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_eu_ES.properties b/src/main/resources/messages_eu_ES.properties index 45bdb032c76..3cd8fbb456d 100644 --- a/src/main/resources/messages_eu_ES.properties +++ b/src/main/resources/messages_eu_ES.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Gorde Erabiltzailea adminUserSettings.changeUserRole=Erabiltzailearen rola aldatu adminUserSettings.authenticated=Authenticated + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_fr_FR.properties b/src/main/resources/messages_fr_FR.properties index 7ff1c61b7c9..a1c44958758 100644 --- a/src/main/resources/messages_fr_FR.properties +++ b/src/main/resources/messages_fr_FR.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Ajouter adminUserSettings.changeUserRole=Changer le rôle de l'utilisateur adminUserSettings.authenticated=Authentifié + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_hi_IN.properties b/src/main/resources/messages_hi_IN.properties index 080d9bb3792..e8dea135417 100644 --- a/src/main/resources/messages_hi_IN.properties +++ b/src/main/resources/messages_hi_IN.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=उपयोगकर्ता को सहेजे adminUserSettings.changeUserRole=यूज़र की भूमिका बदलें adminUserSettings.authenticated=Authenticated + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_hr_HR.properties b/src/main/resources/messages_hr_HR.properties index a98e8ef7340..fc26615b051 100644 --- a/src/main/resources/messages_hr_HR.properties +++ b/src/main/resources/messages_hr_HR.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Spremi korisnika adminUserSettings.changeUserRole=Promijenite korisničku ulogu adminUserSettings.authenticated=Autentificirano + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_hu_HU.properties b/src/main/resources/messages_hu_HU.properties index 759fae669a5..895ef840017 100644 --- a/src/main/resources/messages_hu_HU.properties +++ b/src/main/resources/messages_hu_HU.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Felhasználó mentése adminUserSettings.changeUserRole=Felhasználó szerepkörének módosítása adminUserSettings.authenticated=Authenticated + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_id_ID.properties b/src/main/resources/messages_id_ID.properties index 4ffaec2f789..e8f4f3c1002 100644 --- a/src/main/resources/messages_id_ID.properties +++ b/src/main/resources/messages_id_ID.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Simpan Pengguna adminUserSettings.changeUserRole=Ubah Peran Pengguna adminUserSettings.authenticated=Authenticated + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_it_IT.properties b/src/main/resources/messages_it_IT.properties index 7589e4c5224..b9117019d23 100644 --- a/src/main/resources/messages_it_IT.properties +++ b/src/main/resources/messages_it_IT.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Salva utente adminUserSettings.changeUserRole=Cambia il ruolo dell'utente adminUserSettings.authenticated=Autenticato + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_ja_JP.properties b/src/main/resources/messages_ja_JP.properties index bab67998bb1..648f16336da 100644 --- a/src/main/resources/messages_ja_JP.properties +++ b/src/main/resources/messages_ja_JP.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=ユーザーの保存 adminUserSettings.changeUserRole=ユーザーの役割を変更する adminUserSettings.authenticated=認証済 + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_ko_KR.properties b/src/main/resources/messages_ko_KR.properties index 2a9d3461dd4..40c48a21b9c 100644 --- a/src/main/resources/messages_ko_KR.properties +++ b/src/main/resources/messages_ko_KR.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=사용자 저장 adminUserSettings.changeUserRole=사용자의 역할 변경 adminUserSettings.authenticated=Authenticated + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_nl_NL.properties b/src/main/resources/messages_nl_NL.properties index 02718c3a003..e0b8aa2f7d3 100644 --- a/src/main/resources/messages_nl_NL.properties +++ b/src/main/resources/messages_nl_NL.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Gebruiker opslaan adminUserSettings.changeUserRole=De rol van de gebruiker wijzigen adminUserSettings.authenticated=Geauthenticeerd + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_no_NB.properties b/src/main/resources/messages_no_NB.properties index 8597ebd81d3..84ed25f4f23 100644 --- a/src/main/resources/messages_no_NB.properties +++ b/src/main/resources/messages_no_NB.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Lagre Bruker adminUserSettings.changeUserRole=Endre Brukerens Rolle adminUserSettings.authenticated=Autentisert + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_pl_PL.properties b/src/main/resources/messages_pl_PL.properties index 519cc59fd4b..18017dba5bd 100755 --- a/src/main/resources/messages_pl_PL.properties +++ b/src/main/resources/messages_pl_PL.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Zapisz użytkownika adminUserSettings.changeUserRole=Zmień rolę użytkownika adminUserSettings.authenticated=Zalogowany + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_pt_BR.properties b/src/main/resources/messages_pt_BR.properties index cc7fe4e89fa..c1a41ff2320 100644 --- a/src/main/resources/messages_pt_BR.properties +++ b/src/main/resources/messages_pt_BR.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Save User adminUserSettings.changeUserRole=Alterar Função de Usuário adminUserSettings.authenticated=Authenticated + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_pt_PT.properties b/src/main/resources/messages_pt_PT.properties index 74246eb6ba8..f7905ea01e1 100644 --- a/src/main/resources/messages_pt_PT.properties +++ b/src/main/resources/messages_pt_PT.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Save User adminUserSettings.changeUserRole=Alterar usuário adminUserSettings.authenticated=Authenticated + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_ro_RO.properties b/src/main/resources/messages_ro_RO.properties index 836bfc430d0..b186c4c7ae6 100644 --- a/src/main/resources/messages_ro_RO.properties +++ b/src/main/resources/messages_ro_RO.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Save User adminUserSettings.changeUserRole=Schimbați rolul utilizatorului adminUserSettings.authenticated=Authenticated + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_ru_RU.properties b/src/main/resources/messages_ru_RU.properties index ab7a1ac9dc6..cbd5c285ecb 100644 --- a/src/main/resources/messages_ru_RU.properties +++ b/src/main/resources/messages_ru_RU.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Сохранить пользователя adminUserSettings.changeUserRole=Изменить роль пользователя adminUserSettings.authenticated=Authenticated + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_sk_SK.properties b/src/main/resources/messages_sk_SK.properties index 6ca3173da90..599ef7bae9a 100644 --- a/src/main/resources/messages_sk_SK.properties +++ b/src/main/resources/messages_sk_SK.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Uložiť používateľa adminUserSettings.changeUserRole=Zmeniť rolu používateľa adminUserSettings.authenticated=Authenticated + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_sr_LATN_RS.properties b/src/main/resources/messages_sr_LATN_RS.properties index 5e0e7ec3b5b..102fee73fd1 100644 --- a/src/main/resources/messages_sr_LATN_RS.properties +++ b/src/main/resources/messages_sr_LATN_RS.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Sačuvaj korisnika adminUserSettings.changeUserRole=Promenite ulogu korisnika adminUserSettings.authenticated=Authenticated + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_sv_SE.properties b/src/main/resources/messages_sv_SE.properties index 00381ee8b11..ecbd13bacaf 100644 --- a/src/main/resources/messages_sv_SE.properties +++ b/src/main/resources/messages_sv_SE.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Save User adminUserSettings.changeUserRole=Ändra användarens roll adminUserSettings.authenticated=Authenticated + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_tr_TR.properties b/src/main/resources/messages_tr_TR.properties index 4dfe0e19029..16e4b5aeaa8 100644 --- a/src/main/resources/messages_tr_TR.properties +++ b/src/main/resources/messages_tr_TR.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Kullanıcıyı Kaydet adminUserSettings.changeUserRole=Kullanıcı rolünü değiştir adminUserSettings.authenticated=Onaylandı + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_uk_UA.properties b/src/main/resources/messages_uk_UA.properties index 2f9356c7021..ebe2d989731 100644 --- a/src/main/resources/messages_uk_UA.properties +++ b/src/main/resources/messages_uk_UA.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=Зберегти користувача adminUserSettings.changeUserRole=Змінити роль користувача adminUserSettings.authenticated=Автентифіковано + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_zh_CN.properties b/src/main/resources/messages_zh_CN.properties index ce70cb86d4d..46e46fbc33b 100644 --- a/src/main/resources/messages_zh_CN.properties +++ b/src/main/resources/messages_zh_CN.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=保存用户 adminUserSettings.changeUserRole=更改用户角色 adminUserSettings.authenticated=已验证 + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_zh_TW.properties b/src/main/resources/messages_zh_TW.properties index 8bcc59eed16..ead961d01ec 100644 --- a/src/main/resources/messages_zh_TW.properties +++ b/src/main/resources/messages_zh_TW.properties @@ -191,6 +191,23 @@ adminUserSettings.submit=儲存 adminUserSettings.changeUserRole=更改使用者身份 adminUserSettings.authenticated=已驗證 + +database.title=Database Import/Export +database.header=Database Import/Export +database.fileName=File Name +database.creationDate=Creation Date +database.fileSize=File Size +database.deleteBackupFile=Delete Backup File +database.importBackupFile=Import Backup File +database.downloadBackupFile=Download Backup File +database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. +database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. +database.submit=Import Backup +database.importIntoDatabaseSuccessed=Import into database successed +database.fileNotFound=File not Found +database.fileNullOrEmpty=File must not be null or empty +database.failedImportFile=Failed Import File + ############# # HOME-PAGE # ############# diff --git a/src/main/resources/templates/database.html b/src/main/resources/templates/database.html new file mode 100644 index 00000000000..4f6f8a86413 --- /dev/null +++ b/src/main/resources/templates/database.html @@ -0,0 +1,71 @@ + + + + + + + +
+
+ +

+
+
+
+
+ database + Database Im-/Export +
+

+

+
+ + + + + + + + + + + + + + + + + + + + + +
File NameCreation DateFile Size
deletebackupdownload
+
+
+
+
+

+

+
+
+
+ +
+
+ +
+
+
+
+
+ + + +
+ + \ No newline at end of file