diff --git a/build.gradle b/build.gradle index 8a80900f4..a0547ee4d 100644 --- a/build.gradle +++ b/build.gradle @@ -297,10 +297,14 @@ dependencies { implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion" implementation "org.springframework.session:spring-session-core:$springBootVersion" + implementation "org.springframework:spring-jdbc" implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5' // Don't upgrade h2database + runtimeOnly "com.h2database:h2:2.3.232" runtimeOnly "org.postgresql:postgresql:42.7.4" + + implementation "com.unboundid.product.scim2:scim2-sdk-client:2.3.5" constraints { implementation "org.opensaml:opensaml-core:$openSamlVersion" implementation "org.opensaml:opensaml-saml-api:$openSamlVersion" @@ -388,6 +392,7 @@ tasks.withType(JavaCompile).configureEach { options.encoding = "UTF-8" dependsOn "spotlessApply" } + compileJava { options.compilerArgs << "-parameters" } diff --git a/exampleYmlFiles/docker-compose-latest-fat-security.yml b/exampleYmlFiles/docker-compose-latest-fat-security.yml index e46bd0e9f..ba7dbfad9 100644 --- a/exampleYmlFiles/docker-compose-latest-fat-security.yml +++ b/exampleYmlFiles/docker-compose-latest-fat-security.yml @@ -6,6 +6,8 @@ services: resources: limits: memory: 4G + depends_on: + - db healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"] interval: 5s @@ -31,3 +33,14 @@ services: METRICS_ENABLED: "true" SYSTEM_GOOGLEVISIBILITY: "true" restart: on-failure:5 + + db: + image: 'postgres:17.2-alpine' + restart: on-failure:5 + container_name: db + ports: + - "5432:5432" + environment: + POSTGRES_DB: "stirling_pdf" + POSTGRES_USER: "admin" + POSTGRES_PASSWORD: "stirling" \ No newline at end of file diff --git a/exampleYmlFiles/docker-compose-latest-security-with-sso.yml b/exampleYmlFiles/docker-compose-latest-security-with-sso.yml index 9d30986c6..7f7ee2bda 100644 --- a/exampleYmlFiles/docker-compose-latest-security-with-sso.yml +++ b/exampleYmlFiles/docker-compose-latest-security-with-sso.yml @@ -6,6 +6,8 @@ services: resources: limits: memory: 4G + depends_on: + - db healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"] interval: 5s @@ -39,3 +41,14 @@ services: METRICS_ENABLED: "true" SYSTEM_GOOGLEVISIBILITY: "true" restart: on-failure:5 + + db: + image: 'postgres:17.2-alpine' + restart: on-failure:5 + container_name: db + ports: + - "5432:5432" + environment: + POSTGRES_DB: "stirling_pdf" + POSTGRES_USER: "admin" + POSTGRES_PASSWORD: "stirling" \ No newline at end of file diff --git a/exampleYmlFiles/docker-compose-latest-security.yml b/exampleYmlFiles/docker-compose-latest-security.yml index d29c185d9..ec9707089 100644 --- a/exampleYmlFiles/docker-compose-latest-security.yml +++ b/exampleYmlFiles/docker-compose-latest-security.yml @@ -6,6 +6,8 @@ services: resources: limits: memory: 4G + depends_on: + - db healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"] interval: 5s @@ -30,4 +32,24 @@ services: SYSTEM_MAXFILESIZE: "100" METRICS_ENABLED: "true" SYSTEM_GOOGLEVISIBILITY: "true" + SYSTEM_DATASOURCE_TYPE: "postgresql" + SYSTEM_DATASOURCE_HOSTNAME: "db" + SYSTEM_DATASOURCE_PORT: "5432" + SYSTEM_DATASOURCE_NAME: "stirling_pdf" + SYSTEM_DATASOURCE_USEDEFAULT: "false" + SYSTEM_DATASOURCE_USERNAME: "admin" + SYSTEM_DATASOURCE_PASSWORD: "stirling" +# SPRING_JPA_HIBERNATE_DDL_AUTO: "update" +# SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT: "org.hibernate.dialect.PostgreSQLDialect" restart: on-failure:5 + + db: + image: 'postgres:17.2-alpine' + restart: on-failure:5 + container_name: db + ports: + - "5432:5432" + environment: + POSTGRES_DB: "stirling_pdf" + POSTGRES_USER: "admin" + POSTGRES_PASSWORD: "stirling" \ No newline at end of file diff --git a/exampleYmlFiles/docker-compose-latest-ultra-lite-security.yml b/exampleYmlFiles/docker-compose-latest-ultra-lite-security.yml index f357e0b96..fba3bcd9c 100644 --- a/exampleYmlFiles/docker-compose-latest-ultra-lite-security.yml +++ b/exampleYmlFiles/docker-compose-latest-ultra-lite-security.yml @@ -6,6 +6,8 @@ services: resources: limits: memory: 1G + depends_on: + - db healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"] interval: 5s @@ -28,3 +30,15 @@ services: METRICS_ENABLED: "true" SYSTEM_GOOGLEVISIBILITY: "true" restart: on-failure:5 + + db: + image: 'postgres:17.2-alpine' + restart: on-failure:5 + shm_size: 32mb + container_name: db + ports: + - "5432:5432" + environment: + POSTGRES_DB: "stirling_pdf" + POSTGRES_USER: "admin" + POSTGRES_PASSWORD: "stirling" \ No newline at end of file diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPDFApplication.java similarity index 90% rename from src/main/java/stirling/software/SPDF/SPdfApplication.java rename to src/main/java/stirling/software/SPDF/SPDFApplication.java index e26fb1117..30eeeb908 100644 --- a/src/main/java/stirling/software/SPDF/SPdfApplication.java +++ b/src/main/java/stirling/software/SPDF/SPDFApplication.java @@ -1,6 +1,5 @@ package stirling.software.SPDF; -import java.awt.*; import java.io.IOException; import java.net.ServerSocket; import java.nio.file.Files; @@ -11,8 +10,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Properties; -import javax.swing.*; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; @@ -29,51 +26,25 @@ import stirling.software.SPDF.UI.WebBrowser; import stirling.software.SPDF.config.ConfigInitializer; import stirling.software.SPDF.model.ApplicationProperties; -@SpringBootApplication -@EnableScheduling @Slf4j -public class SPdfApplication { +@EnableScheduling +@SpringBootApplication +public class SPDFApplication { + + private static String serverPortStatic; + private static String baseUrlStatic; @Autowired private Environment env; - @Autowired ApplicationProperties applicationProperties; + @Autowired private ApplicationProperties applicationProperties; - private static String baseUrlStatic; - private static String serverPortStatic; + @Autowired(required = false) + private WebBrowser webBrowser; @Value("${baseUrl:http://localhost}") private String baseUrl; - @Value("${server.port:8080}") - public void setServerPortStatic(String port) { - if ("auto".equalsIgnoreCase(port)) { - // Use Spring Boot's automatic port assignment (server.port=0) - SPdfApplication.serverPortStatic = - "0"; // This will let Spring Boot assign an available port - } else { - SPdfApplication.serverPortStatic = port; - } - } - - // Optionally keep this method if you want to provide a manual port-incrementation fallback. - private static String findAvailablePort(int startPort) { - int port = startPort; - while (!isPortAvailable(port)) { - port++; - } - return String.valueOf(port); - } - - private static boolean isPortAvailable(int port) { - try (ServerSocket socket = new ServerSocket(port)) { - return true; - } catch (IOException e) { - return false; - } - } - public static void main(String[] args) throws IOException, InterruptedException { - - SpringApplication app = new SpringApplication(SPdfApplication.class); + SpringApplication app = new SpringApplication(SPDFApplication.class); Properties props = new Properties(); @@ -84,7 +55,7 @@ public class SPdfApplication { props.put("spring.main.web-application-type", "servlet"); } - app.setAdditionalProfiles("default"); + app.setAdditionalProfiles(getActiveProfile(args)); app.addInitializers(new ConfigInitializer()); Map propertyFiles = new HashMap<>(); @@ -134,15 +105,6 @@ public class SPdfApplication { printStartupLogs(); } - private static void printStartupLogs() { - log.info("Stirling-PDF Started."); - String url = baseUrlStatic + ":" + getStaticPort(); - log.info("Navigate to {}", url); - } - - @Autowired(required = false) - private WebBrowser webBrowser; - @PostConstruct public void init() { baseUrlStatic = this.baseUrl; @@ -173,6 +135,17 @@ public class SPdfApplication { log.info("Running configs {}", applicationProperties.toString()); } + @Value("${server.port:8080}") + public void setServerPortStatic(String port) { + if ("auto".equalsIgnoreCase(port)) { + // Use Spring Boot's automatic port assignment (server.port=0) + SPDFApplication.serverPortStatic = + "0"; // This will let Spring Boot assign an available port + } else { + SPDFApplication.serverPortStatic = port; + } + } + @PreDestroy public void cleanup() { if (webBrowser != null) { @@ -180,6 +153,43 @@ public class SPdfApplication { } } + private static void printStartupLogs() { + log.info("Stirling-PDF Started."); + String url = baseUrlStatic + ":" + getStaticPort(); + log.info("Navigate to {}", url); + } + + private static String[] getActiveProfile(String[] args) { + if (args == null) { + return new String[] {"default"}; + } + + for (String arg : args) { + if (arg.contains("spring.profiles.active")) { + return arg.substring(args[0].indexOf('=') + 1).split(","); + } + } + + return new String[] {"default"}; + } + + private static boolean isPortAvailable(int port) { + try (ServerSocket socket = new ServerSocket(port)) { + return true; + } catch (IOException e) { + return false; + } + } + + // Optionally keep this method if you want to provide a manual port-incrementation fallback. + private static String findAvailablePort(int startPort) { + int port = startPort; + while (!isPortAvailable(port)) { + port++; + } + return String.valueOf(port); + } + public static String getStaticBaseUrl() { return baseUrlStatic; } diff --git a/src/main/java/stirling/software/SPDF/config/interfaces/DatabaseBackupInterface.java b/src/main/java/stirling/software/SPDF/config/interfaces/DatabaseBackupInterface.java deleted file mode 100644 index 9d0e094ac..000000000 --- a/src/main/java/stirling/software/SPDF/config/interfaces/DatabaseBackupInterface.java +++ /dev/null @@ -1,17 +0,0 @@ -package stirling.software.SPDF.config.interfaces; - -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/interfaces/DatabaseInterface.java b/src/main/java/stirling/software/SPDF/config/interfaces/DatabaseInterface.java new file mode 100644 index 000000000..55107123e --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/interfaces/DatabaseInterface.java @@ -0,0 +1,13 @@ +package stirling.software.SPDF.config.interfaces; + +import java.sql.SQLException; +import java.util.List; + +import stirling.software.SPDF.model.provider.UnsupportedProviderException; +import stirling.software.SPDF.utils.FileInfo; + +public interface DatabaseInterface { + void exportDatabase() throws SQLException, UnsupportedProviderException; + + List getBackupList(); +} diff --git a/src/main/java/stirling/software/SPDF/config/security/CustomLogoutSuccessHandler.java b/src/main/java/stirling/software/SPDF/config/security/CustomLogoutSuccessHandler.java index 8d5aa76d0..ffeb8d08c 100644 --- a/src/main/java/stirling/software/SPDF/config/security/CustomLogoutSuccessHandler.java +++ b/src/main/java/stirling/software/SPDF/config/security/CustomLogoutSuccessHandler.java @@ -20,7 +20,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import stirling.software.SPDF.SPdfApplication; +import stirling.software.SPDF.SPDFApplication; import stirling.software.SPDF.config.security.saml2.CertificateUtils; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal; import stirling.software.SPDF.model.ApplicationProperties; @@ -110,7 +110,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { // Construct URLs required for SAML configuration String serverUrl = - SPdfApplication.getStaticBaseUrl() + ":" + SPdfApplication.getStaticPort(); + SPDFApplication.getStaticBaseUrl() + ":" + SPDFApplication.getStaticPort(); String relyingPartyIdentifier = serverUrl + "/saml2/service-provider-metadata/" + registrationId; @@ -219,9 +219,9 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { // "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue=" // + response.encodeRedirectURL(redirect_url); log.info("Google does not have a specific logout URL"); - // log.info("Redirecting to Google logout URL: " + googleLogoutUrl); - // response.sendRedirect(googleLogoutUrl); - // break; + // log.info("Redirecting to Google logout URL: " + googleLogoutUrl); + // response.sendRedirect(googleLogoutUrl); + // break; default: String defaultRedirectUrl = request.getContextPath() + "/login?" + param; log.info("Redirecting to default logout URL: " + defaultRedirectUrl); 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 08356a83d..103181fc4 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -1,6 +1,6 @@ package stirling.software.SPDF.config.security; -import java.io.IOException; +import java.sql.SQLException; import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; @@ -8,35 +8,37 @@ import org.springframework.stereotype.Component; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; -import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface; +import stirling.software.SPDF.config.interfaces.DatabaseInterface; import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.Role; +import stirling.software.SPDF.model.provider.UnsupportedProviderException; -@Component @Slf4j +@Component public class InitialSecuritySetup { @Autowired private UserService userService; @Autowired private ApplicationProperties applicationProperties; - // todo: wip add Postgres here - @Autowired private DatabaseBackupInterface databaseBackupHelper; + @Autowired private DatabaseInterface databaseService; @PostConstruct - public void init() throws IllegalArgumentException, IOException { - if (databaseBackupHelper.hasBackup() && !userService.hasUsers()) { - databaseBackupHelper.importDatabase(); - } else if (!userService.hasUsers()) { - initializeAdminUser(); - } else { - databaseBackupHelper.exportDatabase(); + public void init() { + try { + if (!userService.hasUsers()) { + initializeAdminUser(); + } + userService.migrateOauth2ToSSO(); + initializeInternalApiUser(); + } catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) { + log.error("Failed to initialize security setup.", e); + System.exit(1); } - initializeInternalApiUser(); } - private void initializeAdminUser() throws IOException { + private void initializeAdminUser() throws SQLException, UnsupportedProviderException { String initialUsername = applicationProperties.getSecurity().getInitialLogin().getUsername(); String initialPassword = @@ -45,36 +47,34 @@ public class InitialSecuritySetup { && !initialUsername.isEmpty() && initialPassword != null && !initialPassword.isEmpty() - && !userService.findByUsernameIgnoreCase(initialUsername).isPresent()) { - try { - userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); - log.info("Admin user created: " + initialUsername); - } catch (IllegalArgumentException e) { - log.error("Failed to initialize security setup", e); - System.exit(1); - } + && userService.findByUsernameIgnoreCase(initialUsername).isEmpty()) { + + userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); + log.info("Admin user created: {}", initialUsername); } else { createDefaultAdminUser(); } } - private void createDefaultAdminUser() throws IllegalArgumentException, IOException { + private void createDefaultAdminUser() throws SQLException, UnsupportedProviderException { String defaultUsername = "admin"; String defaultPassword = "stirling"; - if (!userService.findByUsernameIgnoreCase(defaultUsername).isPresent()) { + + if (userService.findByUsernameIgnoreCase(defaultUsername).isEmpty()) { userService.saveUser(defaultUsername, defaultPassword, Role.ADMIN.getRoleId(), true); - log.info("Default admin user created: " + defaultUsername); + log.info("Default admin user created: {}", defaultUsername); } } - private void initializeInternalApiUser() throws IllegalArgumentException, IOException { + private void initializeInternalApiUser() + throws IllegalArgumentException, SQLException, UnsupportedProviderException { 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()); - log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId()); + log.info("Internal API user created: {}", Role.INTERNAL_API_USER.getRoleId()); } userService.syncCustomApiUser(applicationProperties.getSecurity().getCustomGlobalAPIKey()); } 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 d5fea5942..57b34746f 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserService.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -1,6 +1,6 @@ package stirling.software.SPDF.config.security; -import java.io.IOException; +import java.sql.SQLException; import java.util.*; import java.util.stream.Collectors; @@ -21,7 +21,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import lombok.extern.slf4j.Slf4j; -import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface; +import stirling.software.SPDF.config.interfaces.DatabaseInterface; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface; @@ -30,6 +30,7 @@ import stirling.software.SPDF.model.AuthenticationType; import stirling.software.SPDF.model.Authority; import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.User; +import stirling.software.SPDF.model.provider.UnsupportedProviderException; import stirling.software.SPDF.repository.AuthorityRepository; import stirling.software.SPDF.repository.UserRepository; @@ -47,7 +48,7 @@ public class UserService implements UserServiceInterface { @Autowired private SessionPersistentRegistry sessionRegistry; - @Autowired DatabaseBackupInterface databaseBackupHelper; + @Autowired DatabaseInterface databaseService; @Autowired ApplicationProperties applicationProperties; @@ -64,7 +65,7 @@ public class UserService implements UserServiceInterface { // Handle OAUTH2 login and user auto creation. public boolean processSSOPostLogin(String username, boolean autoCreateUser) - throws IllegalArgumentException, IOException { + throws IllegalArgumentException, SQLException, UnsupportedProviderException { if (!isUsernameValid(username)) { return false; } @@ -151,12 +152,12 @@ public class UserService implements UserServiceInterface { } public void saveUser(String username, AuthenticationType authenticationType) - throws IllegalArgumentException, IOException { + throws IllegalArgumentException, SQLException, UnsupportedProviderException { saveUser(username, authenticationType, Role.USER.getRoleId()); } public void saveUser(String username, AuthenticationType authenticationType, String role) - throws IllegalArgumentException, IOException { + throws IllegalArgumentException, SQLException, UnsupportedProviderException { if (!isUsernameValid(username)) { throw new IllegalArgumentException(getInvalidUsernameMessage()); } @@ -167,11 +168,11 @@ public class UserService implements UserServiceInterface { user.addAuthority(new Authority(role, user)); user.setAuthenticationType(authenticationType); userRepository.save(user); - databaseBackupHelper.exportDatabase(); + databaseService.exportDatabase(); } public void saveUser(String username, String password) - throws IllegalArgumentException, IOException { + throws IllegalArgumentException, SQLException, UnsupportedProviderException { if (!isUsernameValid(username)) { throw new IllegalArgumentException(getInvalidUsernameMessage()); } @@ -181,11 +182,11 @@ public class UserService implements UserServiceInterface { user.setEnabled(true); user.setAuthenticationType(AuthenticationType.WEB); userRepository.save(user); - databaseBackupHelper.exportDatabase(); + databaseService.exportDatabase(); } public void saveUser(String username, String password, String role, boolean firstLogin) - throws IllegalArgumentException, IOException { + throws IllegalArgumentException, SQLException, UnsupportedProviderException { if (!isUsernameValid(username)) { throw new IllegalArgumentException(getInvalidUsernameMessage()); } @@ -197,11 +198,11 @@ public class UserService implements UserServiceInterface { user.setAuthenticationType(AuthenticationType.WEB); user.setFirstLogin(firstLogin); userRepository.save(user); - databaseBackupHelper.exportDatabase(); + databaseService.exportDatabase(); } public void saveUser(String username, String password, String role) - throws IllegalArgumentException, IOException { + throws IllegalArgumentException, SQLException, UnsupportedProviderException { saveUser(username, password, role, false); } @@ -235,7 +236,7 @@ public class UserService implements UserServiceInterface { } public void updateUserSettings(String username, Map updates) - throws IOException { + throws SQLException, UnsupportedProviderException { Optional userOpt = findByUsernameIgnoreCaseWithSettings(username); if (userOpt.isPresent()) { User user = userOpt.get(); @@ -249,7 +250,7 @@ public class UserService implements UserServiceInterface { user.setSettings(settingsMap); userRepository.save(user); - databaseBackupHelper.exportDatabase(); + databaseService.exportDatabase(); } } @@ -270,38 +271,42 @@ public class UserService implements UserServiceInterface { } public void changeUsername(User user, String newUsername) - throws IllegalArgumentException, IOException { + throws IllegalArgumentException, SQLException, UnsupportedProviderException { if (!isUsernameValid(newUsername)) { throw new IllegalArgumentException(getInvalidUsernameMessage()); } user.setUsername(newUsername); userRepository.save(user); - databaseBackupHelper.exportDatabase(); + databaseService.exportDatabase(); } - public void changePassword(User user, String newPassword) throws IOException { + public void changePassword(User user, String newPassword) + throws SQLException, UnsupportedProviderException { user.setPassword(passwordEncoder.encode(newPassword)); userRepository.save(user); - databaseBackupHelper.exportDatabase(); + databaseService.exportDatabase(); } - public void changeFirstUse(User user, boolean firstUse) throws IOException { + public void changeFirstUse(User user, boolean firstUse) + throws SQLException, UnsupportedProviderException { user.setFirstLogin(firstUse); userRepository.save(user); - databaseBackupHelper.exportDatabase(); + databaseService.exportDatabase(); } - public void changeRole(User user, String newRole) throws IOException { + public void changeRole(User user, String newRole) + throws SQLException, UnsupportedProviderException { Authority userAuthority = this.findRole(user); userAuthority.setAuthority(newRole); authorityRepository.save(userAuthority); - databaseBackupHelper.exportDatabase(); + databaseService.exportDatabase(); } - public void changeUserEnabled(User user, Boolean enbeled) throws IOException { + public void changeUserEnabled(User user, Boolean enbeled) + throws SQLException, UnsupportedProviderException { user.setEnabled(enbeled); userRepository.save(user); - databaseBackupHelper.exportDatabase(); + databaseService.exportDatabase(); } public boolean isPasswordCorrect(User user, String currentPassword) { @@ -391,7 +396,8 @@ public class UserService implements UserServiceInterface { } @Transactional - public void syncCustomApiUser(String customApiKey) throws IOException { + public void syncCustomApiUser(String customApiKey) + throws SQLException, UnsupportedProviderException { if (customApiKey == null || customApiKey.trim().length() == 0) { return; } @@ -409,14 +415,14 @@ public class UserService implements UserServiceInterface { user.setApiKey(customApiKey); user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user)); userRepository.save(user); - databaseBackupHelper.exportDatabase(); + databaseService.exportDatabase(); } else { // Update API key if it has changed User user = existingUser.get(); if (!customApiKey.equals(user.getApiKey())) { user.setApiKey(customApiKey); userRepository.save(user); - databaseBackupHelper.exportDatabase(); + databaseService.exportDatabase(); } } } diff --git a/src/main/java/stirling/software/SPDF/config/security/database/DatabaseConfig.java b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseConfig.java new file mode 100644 index 000000000..eab29f3e3 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseConfig.java @@ -0,0 +1,123 @@ +package stirling.software.SPDF.config.security.database; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.model.ApplicationProperties; +import stirling.software.SPDF.model.provider.UnsupportedProviderException; + +@Getter +@Slf4j +@Configuration +public class DatabaseConfig { + + public static final String DATASOURCE_URL_TEMPLATE = "jdbc:%s://%s:%4d/%s"; + + private final ApplicationProperties applicationProperties; + + public DatabaseConfig(@Autowired ApplicationProperties applicationProperties) { + this.applicationProperties = applicationProperties; + } + + /** + * Creates the DataSource for the connection to the DB. If useDefault + * is set to true, it will use the default H2 DB. If it is set to false + * , it will use the user's custom configuration set in the settings.yml. + * + * @return a DataSource using the configuration settings in the settings.yml + * @throws UnsupportedProviderException if the type of database selected is not supported + */ + @Bean + public DataSource dataSource() throws UnsupportedProviderException { + ApplicationProperties.System system = applicationProperties.getSystem(); + ApplicationProperties.Datasource datasource = system.getDatasource(); + DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create(); + + if (datasource.isUseDefault()) { + log.debug("Using default H2 database"); + + dataSourceBuilder.driverClassName("org.h2.Driver"); + dataSourceBuilder.url(datasource.getDefaultUrl()); + dataSourceBuilder.username("sa"); + + return dataSourceBuilder.build(); + } + + dataSourceBuilder.driverClassName(getDriverClassName(datasource.getType())); + dataSourceBuilder.url( + getDataSourceUrl( + datasource.getType(), + datasource.getHostName(), + datasource.getPort(), + datasource.getName())); + dataSourceBuilder.username(datasource.getUsername()); + dataSourceBuilder.password(datasource.getPassword()); + + return dataSourceBuilder.build(); + } + + /** + * Generate the URL the DataSource will use to connect to the database + * + * @param dataSourceType the type of the database + * @param hostname the host name + * @param port the port number to use for the database + * @param dataSourceName the name the database to connect to + * @return the DataSource URL + */ + private String getDataSourceUrl( + String dataSourceType, String hostname, Integer port, String dataSourceName) { + return DATASOURCE_URL_TEMPLATE.formatted(dataSourceType, hostname, port, dataSourceName); + } + + /** + * @return a Connection using the configured DataSource + * @throws SQLException if a database access error occurs + * @throws UnsupportedProviderException when an unsupported database is selected + */ + public Connection connection() throws SQLException, UnsupportedProviderException { + return dataSource().getConnection(); + } + + /** + * Selects the database driver based on the type of database chosen. + * + * @param driverName the type of the driver (e.g. 'h2', 'postgresql') + * @return the fully qualified driver for the database chosen + * @throws UnsupportedProviderException when an unsupported database is selected + */ + private String getDriverClassName(String driverName) throws UnsupportedProviderException { + try { + ApplicationProperties.Driver driver = + ApplicationProperties.Driver.valueOf(driverName.toUpperCase()); + + switch (driver) { + case H2 -> { + log.debug("H2 driver selected"); + return "org.h2.Driver"; + } + case POSTGRESQL -> { + log.debug("Postgres driver selected"); + return "org.postgresql.Driver"; + } + default -> { + log.warn("{} driver selected", driverName); + throw new UnsupportedProviderException( + driverName + " is not currently supported"); + } + } + } catch (IllegalArgumentException e) { + log.warn("Unknown driver: {}", driverName); + throw new UnsupportedProviderException(driverName + " is not currently supported"); + } + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/database/DatabaseBackupHelper.java b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseService.java similarity index 53% rename from src/main/java/stirling/software/SPDF/config/security/database/DatabaseBackupHelper.java rename to src/main/java/stirling/software/SPDF/config/security/database/DatabaseService.java index 1da9b108b..3a0b01d2c 100644 --- a/src/main/java/stirling/software/SPDF/config/security/database/DatabaseBackupHelper.java +++ b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseService.java @@ -7,8 +7,6 @@ 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.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; @@ -20,49 +18,41 @@ 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 org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.PathResource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.jdbc.datasource.init.ScriptException; +import org.springframework.jdbc.datasource.init.ScriptUtils; +import org.springframework.stereotype.Service; import lombok.extern.slf4j.Slf4j; -import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface; +import stirling.software.SPDF.config.interfaces.DatabaseInterface; +import stirling.software.SPDF.model.ApplicationProperties; +import stirling.software.SPDF.model.provider.UnsupportedProviderException; import stirling.software.SPDF.utils.FileInfo; @Slf4j -@Configuration -public class DatabaseBackupHelper implements DatabaseBackupInterface { +@Service +public class DatabaseService implements DatabaseInterface { - @Value("${spring.datasource.url}") - private String url; + public static final String BACKUP_PREFIX = "backup_"; + public static final String SQL_SUFFIX = ".sql"; + private static final Path BACKUP_PATH = Paths.get("configs/db/backup/"); - @Value("${spring.datasource.username}") - private String databaseUsername; - - @Value("${spring.datasource.password}") - private String databasePassword; - - private Path backupPath = Paths.get("configs/db/backup/"); - - @Override - public boolean hasBackup() { - // Check if there is at least one backup - return !getBackupList().isEmpty(); - } + @Autowired private DatabaseConfig databaseConfig; @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, + BACKUP_PATH, path -> - path.getFileName().toString().startsWith("backup_") - && path.getFileName().toString().endsWith(".sql"))) { + path.getFileName().toString().startsWith(BACKUP_PREFIX) + && path.getFileName().toString().endsWith(SQL_SUFFIX))) { for (Path entry : stream) { BasicFileAttributes attrs = Files.readAttributes(entry, BasicFileAttributes.class); LocalDateTime modificationDate = @@ -83,90 +73,105 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface { } 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)); + public boolean importDatabaseFromUI(String fileName) { + try { + importDatabaseFromUI(getBackupFilePath(fileName)); + return true; + } catch (IOException e) { + log.error( + "Error importing database from file: {}, message: {}", + fileName, + e.getMessage(), + e.getCause()); + return false; + } } // 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; + private void importDatabaseFromUI(Path tempTemplatePath) throws IOException { + executeDatabaseScript(tempTemplatePath); + LocalDateTime dateNow = LocalDateTime.now(); + DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm"); + Path insertOutputFilePath = + this.getBackupFilePath( + BACKUP_PREFIX + "user_" + dateNow.format(myFormatObj) + SQL_SUFFIX); + Files.copy(tempTemplatePath, insertOutputFilePath); + Files.deleteIfExists(tempTemplatePath); } @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())); - } - - // fixMe: Needs to check the type of DB before executing script - @Override - public void exportDatabase() throws IOException { - // Check if the backup directory exists, and create it if it does not - ensureBackupDirectoryExists(); - + public void exportDatabase() throws SQLException, UnsupportedProviderException { // Filter and delete old backups if there are more than 5 List filteredBackupList = this.getBackupList().stream() - .filter(backup -> !backup.getFileName().startsWith("backup_user_")) + .filter(backup -> !backup.getFileName().startsWith(BACKUP_PREFIX + "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()); + deleteOldestBackup(filteredBackupList); } 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 ?;"; + this.getBackupFilePath(BACKUP_PREFIX + dateNow.format(myFormatObj) + SQL_SUFFIX); + + try (Connection conn = databaseConfig.connection()) { + ScriptUtils.executeSqlScript( + conn, new EncodedResource(new PathResource(insertOutputFilePath))); - try (Connection conn = - DriverManager.getConnection(url, databaseUsername, databasePassword); - PreparedStatement stmt = conn.prepareStatement(query)) { - stmt.setString(1, insertOutputFilePath.toString()); - stmt.execute(); log.info("Database export completed: {}", insertOutputFilePath); - } catch (SQLException e) { + } catch (SQLException | UnsupportedProviderException e) { log.error("Error during database export: {}", e.getMessage(), e); + throw e; + } catch (ScriptException e) { + log.error("Error during database export: File {} not found", insertOutputFilePath); + throw e; + } + } + + private static void deleteOldestBackup(List filteredBackupList) { + try { + filteredBackupList.sort( + Comparator.comparing( + p -> p.getFileName().substring(7, p.getFileName().length() - 4))); + + FileInfo oldestFile = filteredBackupList.get(0); + Files.deleteIfExists(Paths.get(oldestFile.getFilePath())); + log.info("Deleted oldest backup: {}", oldestFile.getFileName()); + } catch (IOException e) { + log.error("Unable to delete oldest backup, message: {}", e.getMessage(), e); } } // Retrieves the H2 database version. public String getH2Version() { String version = "Unknown"; - try (Connection conn = - DriverManager.getConnection(url, databaseUsername, databasePassword)) { - 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); + + if (databaseConfig + .getApplicationProperties() + .getSystem() + .getDatasource() + .getType() + .equals(ApplicationProperties.Driver.H2.name())) { + try (Connection conn = databaseConfig.connection()) { + 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 | UnsupportedProviderException e) { + log.error("Error retrieving H2 version: {}", e.getMessage(), e); } - } catch (SQLException e) { - log.error("Error retrieving H2 version: {}", e.getMessage(), e); } + return version; } @@ -188,36 +193,22 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface { // Gets the Path object for a given backup file name. public Path getBackupFilePath(String fileName) { - Path filePath = Paths.get(backupPath.toString(), fileName).normalize(); - if (!filePath.startsWith(backupPath)) { + Path filePath = Paths.get(BACKUP_PATH.toString(), fileName).normalize(); + if (!filePath.startsWith(BACKUP_PATH)) { throw new SecurityException("Path traversal detected"); } return filePath; } - private boolean executeDatabaseScript(Path scriptPath) { - String query = "RUNSCRIPT from ?;"; + private void executeDatabaseScript(Path scriptPath) { + try (Connection conn = databaseConfig.connection()) { + ScriptUtils.executeSqlScript(conn, new EncodedResource(new PathResource(scriptPath))); - try (Connection conn = - DriverManager.getConnection(url, databaseUsername, databasePassword); - PreparedStatement stmt = conn.prepareStatement(query)) { - stmt.setString(1, scriptPath.toString()); - stmt.execute(); log.info("Database import completed: {}", scriptPath); - return true; - } catch (SQLException e) { + } catch (SQLException | UnsupportedProviderException 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()); - } + } catch (ScriptException e) { + log.error("Error: File {} not found", scriptPath.toString(), e); } } 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 index 2ddc47e12..5d5248ae7 100644 --- a/src/main/java/stirling/software/SPDF/config/security/database/ScheduledTasks.java +++ b/src/main/java/stirling/software/SPDF/config/security/database/ScheduledTasks.java @@ -1,18 +1,20 @@ package stirling.software.SPDF.config.security.database; -import java.io.IOException; +import java.sql.SQLException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +import stirling.software.SPDF.model.provider.UnsupportedProviderException; + @Component public class ScheduledTasks { - @Autowired private DatabaseBackupHelper databaseBackupService; + @Autowired private DatabaseService databaseService; @Scheduled(cron = "0 0 0 * * ?") - public void performBackup() throws IOException { - databaseBackupService.exportDatabase(); + public void performBackup() throws SQLException, UnsupportedProviderException { + databaseService.exportDatabase(); } } diff --git a/src/main/java/stirling/software/SPDF/config/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java b/src/main/java/stirling/software/SPDF/config/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java index 9f3f6e359..ef4ac3247 100644 --- a/src/main/java/stirling/software/SPDF/config/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java +++ b/src/main/java/stirling/software/SPDF/config/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java @@ -1,6 +1,7 @@ package stirling.software.SPDF.config.security.oauth2; import java.io.IOException; +import java.sql.SQLException; import org.springframework.security.authentication.LockedException; import org.springframework.security.core.Authentication; @@ -18,6 +19,7 @@ import stirling.software.SPDF.config.security.UserService; import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; import stirling.software.SPDF.model.AuthenticationType; +import stirling.software.SPDF.model.provider.UnsupportedProviderException; import stirling.software.SPDF.utils.RequestUriUtils; public class CustomOAuth2AuthenticationSuccessHandler @@ -97,10 +99,8 @@ public class CustomOAuth2AuthenticationSuccessHandler userService.processSSOPostLogin(username, oAuth.getAutoCreateUser()); } response.sendRedirect(contextPath + "/"); - return; - } catch (IllegalArgumentException e) { + } catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) { response.sendRedirect(contextPath + "/logout?invalidUsername=true"); - return; } } } diff --git a/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticationSuccessHandler.java b/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticationSuccessHandler.java index faa5e67ee..8324f70d0 100644 --- a/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticationSuccessHandler.java +++ b/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticationSuccessHandler.java @@ -1,6 +1,7 @@ package stirling.software.SPDF.config.security.saml2; import java.io.IOException; +import java.sql.SQLException; import org.springframework.security.authentication.LockedException; import org.springframework.security.core.Authentication; @@ -18,6 +19,7 @@ import stirling.software.SPDF.config.security.UserService; import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2; import stirling.software.SPDF.model.AuthenticationType; +import stirling.software.SPDF.model.provider.UnsupportedProviderException; import stirling.software.SPDF.utils.RequestUriUtils; @AllArgsConstructor @@ -108,13 +110,14 @@ public class CustomSaml2AuthenticationSuccessHandler userService.processSSOPostLogin(username, saml2.getAutoCreateUser()); log.debug("Successfully processed authentication for user: {}", username); response.sendRedirect(contextPath + "/"); - return; } catch (IllegalArgumentException e) { log.debug( "Invalid username detected for user: {}, redirecting to logout", username); response.sendRedirect(contextPath + "/logout?invalidUsername=true"); - return; + } catch (SQLException | UnsupportedProviderException e) { + log.error("Error, redirecting to logout", e); + response.sendRedirect(contextPath + "/logout?error=true"); } } } else { diff --git a/src/main/java/stirling/software/SPDF/controller/api/DatabaseController.java b/src/main/java/stirling/software/SPDF/controller/api/DatabaseController.java index efff6e92c..84c36c95d 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/DatabaseController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/DatabaseController.java @@ -29,7 +29,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; -import stirling.software.SPDF.config.security.database.DatabaseBackupHelper; +import stirling.software.SPDF.config.security.database.DatabaseService; @Slf4j @Controller @@ -38,19 +38,16 @@ import stirling.software.SPDF.config.security.database.DatabaseBackupHelper; @Tag(name = "Database", description = "Database APIs for backup, import, and management") public class DatabaseController { - @Autowired DatabaseBackupHelper databaseBackupHelper; + @Autowired DatabaseService databaseService; - @Operation( - summary = "Import a database backup file", - description = "Uploads and imports a database backup SQL file.") + @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( - @Parameter(description = "SQL file to import", required = true) - @RequestParam("fileInput") - MultipartFile file, - RedirectAttributes redirectAttributes) - throws IOException { - + @RequestParam("fileInput") MultipartFile file, RedirectAttributes redirectAttributes) + throws IllegalArgumentException, IOException { if (file == null || file.isEmpty()) { redirectAttributes.addAttribute("error", "fileNullOrEmpty"); return "redirect:/database"; @@ -60,7 +57,8 @@ public class DatabaseController { Path tempTemplatePath = Files.createTempFile("backup_", ".sql"); try (InputStream in = file.getInputStream()) { Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING); - boolean importSuccess = databaseBackupHelper.importDatabaseFromUI(tempTemplatePath); + boolean importSuccess = + databaseService.importDatabaseFromUI(tempTemplatePath.toString()); if (importSuccess) { redirectAttributes.addAttribute("infoMessage", "importIntoDatabaseSuccessed"); } else { @@ -80,8 +78,7 @@ public class DatabaseController { @GetMapping("/import-database-file/{fileName}") public String importDatabaseFromBackupUI( @Parameter(description = "Name of the file to import", required = true) @PathVariable - String fileName) - throws IOException { + String fileName) { if (fileName == null || fileName.isEmpty()) { return "redirect:/database?error=fileNullOrEmpty"; @@ -89,15 +86,14 @@ public class DatabaseController { // Check if the file exists in the backup list boolean fileExists = - databaseBackupHelper.getBackupList().stream() + databaseService.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)) { + if (databaseService.importDatabaseFromUI(fileName)) { log.info("File {} imported to database", fileName); return "redirect:/database?infoMessage=importIntoDatabaseSuccessed"; } @@ -105,19 +101,17 @@ public class DatabaseController { } @Hidden + @GetMapping("/delete/{fileName}") @Operation( summary = "Delete a database backup file", - description = "Deletes a specified database backup file from the server.") - @GetMapping("/delete/{fileName}") - public String deleteFile( - @Parameter(description = "Name of the file to delete", required = true) @PathVariable - String fileName) { - + 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)) { + if (databaseService.deleteBackupFile(fileName)) { log.info("Deleted file: {}", fileName); } else { log.error("Failed to delete file: {}", fileName); @@ -131,18 +125,17 @@ public class DatabaseController { } @Hidden + @GetMapping("/download/{fileName}") @Operation( summary = "Download a database backup file", - description = "Downloads the specified database backup file from the server.") - @GetMapping("/download/{fileName}") - public ResponseEntity downloadFile( - @Parameter(description = "Name of the file to download", required = true) @PathVariable - String fileName) { + 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); + Path filePath = databaseService.getBackupFilePath(fileName); InputStreamResource resource = new InputStreamResource(Files.newInputStream(filePath)); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName) @@ -156,22 +149,4 @@ public class DatabaseController { .build(); } } - - @Operation( - summary = "Create a database backup", - description = - "This endpoint triggers the creation of a database backup and redirects to the" - + " database management page.") - @GetMapping("/createDatabaseBackup") - public String createDatabaseBackup() { - try { - log.info("Starting database backup creation..."); - databaseBackupHelper.exportDatabase(); - log.info("Database backup successfully created."); - } catch (IOException e) { - log.error("Error creating database backup: {}", e.getMessage(), e); - return "redirect:/database?error=" + e.getMessage(); - } - return "redirect:/database?infoMessage=backupCreated"; - } } 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 c7d19f518..2613cfac0 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/UserController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/UserController.java @@ -2,6 +2,7 @@ package stirling.software.SPDF.controller.api; import java.io.IOException; import java.security.Principal; +import java.sql.SQLException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -38,6 +39,7 @@ import stirling.software.SPDF.model.AuthenticationType; import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.api.user.UsernameAndPass; +import stirling.software.SPDF.model.provider.UnsupportedProviderException; @Controller @Tag(name = "User", description = "User APIs") @@ -51,17 +53,17 @@ public class UserController { @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/register") - public String register(@ModelAttribute UsernameAndPass requestModel, Model model) - throws IOException { + public String register(@ModelAttribute UsernameAndPass requestModel, Model model) { if (userService.usernameExistsIgnoreCase(requestModel.getUsername())) { model.addAttribute("error", "Username already exists"); return "register"; } try { userService.saveUser(requestModel.getUsername(), requestModel.getPassword()); - } catch (IllegalArgumentException e) { + } catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) { return "redirect:/login?messageType=invalidUsername"; } + return "redirect:/login?registered=true"; } @@ -73,8 +75,7 @@ public class UserController { @RequestParam(name = "newUsername") String newUsername, HttpServletRequest request, HttpServletResponse response, - RedirectAttributes redirectAttributes) - throws IOException { + RedirectAttributes redirectAttributes) { if (!userService.isUsernameValid(newUsername)) { return new RedirectView("/account?messageType=invalidUsername", true); @@ -108,7 +109,7 @@ public class UserController { if (newUsername != null && newUsername.length() > 0) { try { userService.changeUsername(user, newUsername); - } catch (IllegalArgumentException e) { + } catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) { return new RedirectView("/account?messageType=invalidUsername", true); } } @@ -128,7 +129,7 @@ public class UserController { HttpServletRequest request, HttpServletResponse response, RedirectAttributes redirectAttributes) - throws IOException { + throws SQLException, UnsupportedProviderException { if (principal == null) { return new RedirectView("/change-creds?messageType=notAuthenticated", true); } @@ -162,7 +163,7 @@ public class UserController { HttpServletRequest request, HttpServletResponse response, RedirectAttributes redirectAttributes) - throws IOException { + throws SQLException, UnsupportedProviderException { if (principal == null) { return new RedirectView("/account?messageType=notAuthenticated", true); } @@ -190,7 +191,7 @@ public class UserController { @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/updateUserSettings") public String updateUserSettings(HttpServletRequest request, Principal principal) - throws IOException { + throws SQLException, UnsupportedProviderException { Map paramMap = request.getParameterMap(); Map updates = new HashMap<>(); @@ -215,7 +216,7 @@ public class UserController { @RequestParam(name = "authType") String authType, @RequestParam(name = "forceChange", required = false, defaultValue = "false") boolean forceChange) - throws IllegalArgumentException, IOException { + throws IllegalArgumentException, SQLException, UnsupportedProviderException { if (!userService.isUsernameValid(username)) { return new RedirectView("/addUsers?messageType=invalidUsername", true); @@ -263,7 +264,7 @@ public class UserController { @RequestParam(name = "username") String username, @RequestParam(name = "role") String role, Authentication authentication) - throws IOException { + throws IOException, SQLException, UnsupportedProviderException { Optional userOpt = userService.findByUsernameIgnoreCase(username); @@ -280,6 +281,7 @@ public class UserController { if (currentUsername.equalsIgnoreCase(username)) { return new RedirectView("/addUsers?messageType=downgradeCurrentUser", true); } + try { // Validate the role Role roleEnum = Role.fromString(role); @@ -305,7 +307,7 @@ public class UserController { @PathVariable("username") String username, @RequestParam("enabled") boolean enabled, Authentication authentication) - throws IOException { + throws IOException, SQLException, UnsupportedProviderException { Optional userOpt = userService.findByUsernameIgnoreCase(username); diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/ApiDocService.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/ApiDocService.java index 2c5b9fac2..c68dfcb97 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/ApiDocService.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/ApiDocService.java @@ -20,12 +20,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.ServletContext; import lombok.extern.slf4j.Slf4j; -import stirling.software.SPDF.SPdfApplication; +import stirling.software.SPDF.SPDFApplication; import stirling.software.SPDF.model.ApiEndpoint; import stirling.software.SPDF.model.Role; -@Service @Slf4j +@Service public class ApiDocService { private final Map apiDocumentation = new HashMap<>(); @@ -34,7 +34,7 @@ public class ApiDocService { private String getApiDocsUrl() { String contextPath = servletContext.getContextPath(); - String port = SPdfApplication.getStaticPort(); + String port = SPDFApplication.getStaticPort(); return "http://localhost:" + port + contextPath + "/v1/api-docs"; } diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java index cfa6fbec8..d4c51d21d 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java @@ -39,7 +39,7 @@ import io.github.pixee.security.ZipSecurity; import jakarta.servlet.ServletContext; import lombok.extern.slf4j.Slf4j; -import stirling.software.SPDF.SPdfApplication; +import stirling.software.SPDF.SPDFApplication; import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.Role; @@ -62,7 +62,7 @@ public class PipelineProcessor { private String getBaseUrl() { String contextPath = servletContext.getContextPath(); - String port = SPdfApplication.getStaticPort(); + String port = SPDFApplication.getStaticPort(); return "http://localhost:" + port + contextPath + "/"; } diff --git a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java index b46290201..3e478af25 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java @@ -167,8 +167,8 @@ public class AccountWebController { case "invalid_destination": erroroauth = "login.invalid_destination"; break; - // Valid InResponseTo was not available from the validation context, unable to - // evaluate + // Valid InResponseTo was not available from the validation context, unable to + // evaluate case "invalid_in_response_to": erroroauth = "login.invalid_in_response_to"; break; diff --git a/src/main/java/stirling/software/SPDF/controller/web/DatabaseWebController.java b/src/main/java/stirling/software/SPDF/controller/web/DatabaseWebController.java index 5f521b50e..628d95e5d 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/DatabaseWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/DatabaseWebController.java @@ -12,14 +12,14 @@ 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.config.security.database.DatabaseService; import stirling.software.SPDF.utils.FileInfo; @Controller @Tag(name = "Database Management", description = "Database management and security APIs") public class DatabaseWebController { - @Autowired private DatabaseBackupHelper databaseBackupHelper; + @Autowired private DatabaseService databaseService; @PreAuthorize("hasRole('ROLE_ADMIN')") @GetMapping("/database") @@ -33,10 +33,10 @@ public class DatabaseWebController { model.addAttribute("infoMessage", confirmed); } - List backupList = databaseBackupHelper.getBackupList(); + List backupList = databaseService.getBackupList(); model.addAttribute("backupFiles", backupList); - model.addAttribute("databaseVersion", databaseBackupHelper.getH2Version()); + model.addAttribute("databaseVersion", databaseService.getH2Version()); return "database"; } diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java index 7d66574b5..ab1c51dfc 100644 --- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java +++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java @@ -6,7 +6,6 @@ import java.net.HttpURLConnection; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.sql.Driver; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -253,23 +252,37 @@ public class ApplicationProperties { @Data public static class Datasource { - private String url; - private Driver driver; + private String type; + private String hostName; + private Integer port; + private String name; private String username; - private String password; + @ToString.Exclude private String password; + private boolean useDefault; + private final String defaultUrl; } public enum Driver { H2("h2"), POSTGRESQL("postgresql"), ORACLE("oracle"), - MY_SQL("mysql"); + MYSQL("mysql"); private final String driverName; Driver(String driverName) { this.driverName = driverName; } + + @Override + public String toString() { + return """ + Driver { + driverName='%s' + } + """ + .formatted(driverName); + } } @Data diff --git a/src/main/java/stirling/software/SPDF/model/SessionEntity.java b/src/main/java/stirling/software/SPDF/model/SessionEntity.java index 3b4989d5a..fcdb8777b 100644 --- a/src/main/java/stirling/software/SPDF/model/SessionEntity.java +++ b/src/main/java/stirling/software/SPDF/model/SessionEntity.java @@ -5,7 +5,6 @@ import java.util.Date; import jakarta.persistence.Entity; import jakarta.persistence.Id; -import jakarta.persistence.Lob; import jakarta.persistence.Table; import lombok.Data; @@ -15,7 +14,7 @@ import lombok.Data; public class SessionEntity implements Serializable { @Id private String sessionId; - @Lob private String principalName; + private String principalName; private Date lastRequest; diff --git a/src/main/java/stirling/software/SPDF/model/User.java b/src/main/java/stirling/software/SPDF/model/User.java index ddfb71353..b3cfa129d 100644 --- a/src/main/java/stirling/software/SPDF/model/User.java +++ b/src/main/java/stirling/software/SPDF/model/User.java @@ -47,7 +47,7 @@ public class User implements Serializable { @ElementCollection @MapKeyColumn(name = "setting_key") @Lob - @Column(name = "setting_value", columnDefinition = "CLOB") + @Column(name = "setting_value", columnDefinition = "text") @CollectionTable(name = "user_settings", joinColumns = @JoinColumn(name = "user_id")) private Map settings = new HashMap<>(); // Key-value pairs of settings. diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b789d7681..372a297cf 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -41,13 +41,12 @@ spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000} #spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/ #spring.thymeleaf.cache=false -spring.datasource.url=jdbc:postgresql://localhost:5432/stirling-pdf-DB -spring.datasource.driver-class-name=org.postgresql.Driver -spring.datasource.username=postgres +spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa spring.datasource.password= -spring.jpa.generate-ddl=true +spring.h2.console.enabled=false spring.jpa.hibernate.ddl-auto=update -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect server.servlet.session.timeout=30m # Change the default URL path for OpenAPI JSON springdoc.api-docs.path=/v1/api-docs @@ -55,6 +54,5 @@ springdoc.api-docs.path=/v1/api-docs # Set the URL of the OpenAPI JSON for the Swagger UI springdoc.swagger-ui.url=/v1/api-docs - posthog.api.key=phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq posthog.host=https://eu.i.posthog.com diff --git a/src/main/resources/settings.yml.template b/src/main/resources/settings.yml.template index dcd940802..0ed84dc0d 100644 --- a/src/main/resources/settings.yml.template +++ b/src/main/resources/settings.yml.template @@ -86,10 +86,14 @@ system: tessdataDir: /usr/share/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored. enableAnalytics: undefined # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true datasource: - url: jdbc:postgresql://localhost:5432/stirling-pdf-DB - driver: postgresql - username: postgres - password: + type: postgresql # the type of the database to set (e.g. 'h2', 'postgresql') + hostName: localhost # the host name to use for the database url. Set to 'localhost' when running the app locally. Set to match the name of the container name of your database container when running the app on a server (Docker configuration) + port: 5432 # set the port number of the database. Ensure this matches the port the database is listening to + name: postgres # set the name of your database. Should match the name of the database you create + username: postgres # set the database username + password: postgres # set the database password + useDefault: 'true' # set this property to 'true' if you would like to use the default database configuration + defaultUrl: jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE # the default database url for the application ui: appName: '' # application's visible name diff --git a/src/test/java/stirling/software/SPDF/SPDFApplicationTest.java b/src/test/java/stirling/software/SPDF/SPDFApplicationTest.java new file mode 100644 index 000000000..8b8e7a077 --- /dev/null +++ b/src/test/java/stirling/software/SPDF/SPDFApplicationTest.java @@ -0,0 +1,47 @@ +package stirling.software.SPDF; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.env.Environment; +import stirling.software.SPDF.model.ApplicationProperties; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@ExtendWith(MockitoExtension.class) +public class SPDFApplicationTest { + + @Mock + private Environment env; + + @Mock + private ApplicationProperties applicationProperties; + + @InjectMocks + private SPDFApplication sPdfApplication; + + @BeforeEach + public void setUp() { + sPdfApplication = new SPDFApplication(); + sPdfApplication.setServerPortStatic("8080"); + } + + @Test + public void testSetServerPortStatic() { + sPdfApplication.setServerPortStatic("9090"); + assertEquals("9090", SPDFApplication.getStaticPort()); + } + + @Test + public void testGetStaticPort() { + assertEquals("8080", SPDFApplication.getStaticPort()); + } + + @Test + public void testGetNonStaticPort() { + assertEquals("8080", sPdfApplication.getNonStaticPort()); + } +} diff --git a/src/test/java/stirling/software/SPDF/SPdfApplicationTest.java b/src/test/java/stirling/software/SPDF/SPdfApplicationTest.java deleted file mode 100644 index 350aef745..000000000 --- a/src/test/java/stirling/software/SPDF/SPdfApplicationTest.java +++ /dev/null @@ -1,94 +0,0 @@ -package stirling.software.SPDF; - -import static org.junit.jupiter.api.Assertions.*; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.core.env.Environment; - -import stirling.software.SPDF.model.ApplicationProperties; - -@ExtendWith(MockitoExtension.class) -public class SPdfApplicationTest { - - @Mock - private Environment env; - - @Mock - private ApplicationProperties applicationProperties; - - @InjectMocks - private SPdfApplication sPdfApplication; - - @BeforeEach - public void setUp() { - sPdfApplication = new SPdfApplication(); - sPdfApplication.setServerPortStatic("8080"); - } - - @Test - public void testSetServerPortStatic() { - sPdfApplication.setServerPortStatic("9090"); - assertEquals("9090", SPdfApplication.getStaticPort()); - } - - @Test - public void testMainApplicationStartup() throws IOException, InterruptedException { - // Setup mock environment for the main method - Path configPath = Path.of("test/configs"); - Path settingsPath = Paths.get("test/configs/settings.yml"); - Path customSettingsPath = Paths.get("test/configs/custom_settings.yml"); - Path staticPath = Path.of("test/customFiles/static/"); - Path templatesPath = Path.of("test/customFiles/templates/"); - - // Ensure the files do not exist for the test - if (Files.exists(settingsPath)) { - Files.delete(settingsPath); - } - if (Files.exists(customSettingsPath)) { - Files.delete(customSettingsPath); - } - if (Files.exists(staticPath)) { - Files.delete(staticPath); - } - if (Files.exists(templatesPath)) { - Files.delete(templatesPath); - } - - // Ensure the directories are created for testing - Files.createDirectories(configPath); - Files.createDirectories(staticPath); - Files.createDirectories(templatesPath); - - Files.createFile(settingsPath); - Files.createFile(customSettingsPath); - - // Run the main method - SPdfApplication.main(new String[]{}); - - // Verify that the directories were created - assertTrue(Files.exists(settingsPath)); - assertTrue(Files.exists(customSettingsPath)); - assertTrue(Files.exists(staticPath)); - assertTrue(Files.exists(templatesPath)); - } - - @Test - public void testGetStaticPort() { - assertEquals("8080", SPdfApplication.getStaticPort()); - } - - @Test - public void testGetNonStaticPort() { - assertEquals("8080", sPdfApplication.getNonStaticPort()); - } -} diff --git a/src/test/java/stirling/software/SPDF/config/security/InitialSecuritySetupTest.java b/src/test/java/stirling/software/SPDF/config/security/InitialSecuritySetupTest.java new file mode 100644 index 000000000..1d024b1e6 --- /dev/null +++ b/src/test/java/stirling/software/SPDF/config/security/InitialSecuritySetupTest.java @@ -0,0 +1,56 @@ +package stirling.software.SPDF.config.security; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import stirling.software.SPDF.config.security.database.DatabaseService; +import stirling.software.SPDF.model.ApplicationProperties; +import stirling.software.SPDF.model.User; +import stirling.software.SPDF.model.provider.UnsupportedProviderException; + +import java.sql.SQLException; +import java.util.Optional; + +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class InitialSecuritySetupTest { + + @Mock + private UserService userService; + + @Mock + private ApplicationProperties applicationProperties; + + @Mock + private DatabaseService databaseService; + + @InjectMocks + private InitialSecuritySetup initialSecuritySetup; + + @Test + void testInit() throws SQLException, UnsupportedProviderException { + String username = "admin"; + String password = "stirling"; + ApplicationProperties.Security security = mock(ApplicationProperties.Security.class); + ApplicationProperties.Security.InitialLogin initialLogin = mock(ApplicationProperties.Security.InitialLogin.class); + Optional user = Optional.of(mock(User.class)); + + when(userService.hasUsers()).thenReturn(false); + when(applicationProperties.getSecurity()).thenReturn(security); + when(security.getInitialLogin()).thenReturn(initialLogin); + when(initialLogin.getUsername()).thenReturn(username); + when(initialLogin.getPassword()).thenReturn(password); + when(userService.findByUsernameIgnoreCase(username)).thenReturn(user); + when(userService.usernameExistsIgnoreCase(anyString())).thenReturn(false); + + initialSecuritySetup.init(); + + verify(userService).saveUser(anyString(), anyString(), anyString()); + verify(userService).migrateOauth2ToSSO(); + verify(userService).addApiKeyToUser(anyString()); + } + +} \ No newline at end of file diff --git a/src/test/java/stirling/software/SPDF/config/security/database/DatabaseConfigTest.java b/src/test/java/stirling/software/SPDF/config/security/database/DatabaseConfigTest.java new file mode 100644 index 000000000..74ac58c0a --- /dev/null +++ b/src/test/java/stirling/software/SPDF/config/security/database/DatabaseConfigTest.java @@ -0,0 +1,77 @@ +package stirling.software.SPDF.config.security.database; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import stirling.software.SPDF.model.ApplicationProperties; +import stirling.software.SPDF.model.provider.UnsupportedProviderException; + +import javax.sql.DataSource; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DatabaseConfigTest { + + @Mock + private ApplicationProperties applicationProperties; + + @InjectMocks + private DatabaseConfig databaseConfig; + + @Test + void testDefaultConfigurationForDataSource() throws UnsupportedProviderException { + var system = mock(ApplicationProperties.System.class); + var datasource = mock(ApplicationProperties.Datasource.class); + var testUrl = "jdbc:h2:mem:test"; + + when(applicationProperties.getSystem()).thenReturn(system); + when(system.getDatasource()).thenReturn(datasource); + when(datasource.isUseDefault()).thenReturn(true); + when(datasource.getDefaultUrl()).thenReturn(testUrl); + + var result = databaseConfig.dataSource(); + + assertInstanceOf(DataSource.class, result); + } + + @Test + void testCustomConfigurationForDataSource() throws UnsupportedProviderException { + var system = mock(ApplicationProperties.System.class); + var datasource = mock(ApplicationProperties.Datasource.class); + + when(applicationProperties.getSystem()).thenReturn(system); + when(system.getDatasource()).thenReturn(datasource); + when(datasource.isUseDefault()).thenReturn(false); + when(datasource.getType()).thenReturn("postgresql"); + when(datasource.getHostName()).thenReturn("localhost"); + when(datasource.getPort()).thenReturn(5432); + when(datasource.getName()).thenReturn("postgres"); + when(datasource.getUsername()).thenReturn("postgres"); + when(datasource.getPassword()).thenReturn("postgres"); + + var result = databaseConfig.dataSource(); + + assertInstanceOf(DataSource.class, result); + } + + @ParameterizedTest(name = "Exception thrown when the DB type [{arguments}] is not supported") + @ValueSource(strings = {"oracle", "mysql", "mongoDb"}) + void exceptionThrownWhenDBTypeIsUnsupported(String datasourceType) { + var system = mock(ApplicationProperties.System.class); + var datasource = mock(ApplicationProperties.Datasource.class); + + when(applicationProperties.getSystem()).thenReturn(system); + when(system.getDatasource()).thenReturn(datasource); + when(datasource.isUseDefault()).thenReturn(false); + when(datasource.getType()).thenReturn(datasourceType); + + assertThrows(UnsupportedProviderException.class, () -> databaseConfig.dataSource()); + } +} \ No newline at end of file diff --git a/src/test/java/stirling/software/SPDF/integrationtests/SPDFApplicationIntegrationTest.java b/src/test/java/stirling/software/SPDF/integrationtests/SPDFApplicationIntegrationTest.java new file mode 100644 index 000000000..62f0a0765 --- /dev/null +++ b/src/test/java/stirling/software/SPDF/integrationtests/SPDFApplicationIntegrationTest.java @@ -0,0 +1,60 @@ +package stirling.software.SPDF.integrationtests; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import stirling.software.SPDF.SPDFApplication; +import static java.nio.file.Files.createDirectories; +import static java.nio.file.Files.createFile; +import static java.nio.file.Files.delete; +import static java.nio.file.Files.exists; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Disabled +@SpringBootTest +public class SPDFApplicationIntegrationTest { + + @Test + public void testMainApplicationStartup() throws IOException, InterruptedException { + // Setup mock environment for the main method + Path configPath = Path.of("test/configs"); + Path settingsPath = Paths.get("test/configs/settings.yml"); + Path customSettingsPath = Paths.get("test/configs/custom_settings.yml"); + Path staticPath = Path.of("test/customFiles/static/"); + Path templatesPath = Path.of("test/customFiles/templates/"); + + // Ensure the files do not exist for the test + if (exists(settingsPath)) { + delete(settingsPath); + } + if (exists(customSettingsPath)) { + delete(customSettingsPath); + } + if (exists(staticPath)) { + delete(staticPath); + } + if (exists(templatesPath)) { + delete(templatesPath); + } + + // Ensure the directories are created for testing + createDirectories(configPath); + createDirectories(staticPath); + createDirectories(templatesPath); + + createFile(settingsPath); + createFile(customSettingsPath); + + // Run the main method + SPDFApplication.main(new String[] {"-Dspring.profiles.active=default"}); + + // Verify that the directories were created + assertTrue(exists(settingsPath)); + assertTrue(exists(customSettingsPath)); + assertTrue(exists(staticPath)); + assertTrue(exists(templatesPath)); + } +}