2270: cleaning up

This commit is contained in:
DarioGii 2024-12-20 20:04:56 +00:00
parent 6b07439798
commit 3584156293
32 changed files with 753 additions and 418 deletions

View File

@ -297,10 +297,14 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
implementation "org.springframework.session:spring-session-core:$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' implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
// Don't upgrade h2database // Don't upgrade h2database
runtimeOnly "com.h2database:h2:2.3.232"
runtimeOnly "org.postgresql:postgresql:42.7.4" runtimeOnly "org.postgresql:postgresql:42.7.4"
implementation "com.unboundid.product.scim2:scim2-sdk-client:2.3.5"
constraints { constraints {
implementation "org.opensaml:opensaml-core:$openSamlVersion" implementation "org.opensaml:opensaml-core:$openSamlVersion"
implementation "org.opensaml:opensaml-saml-api:$openSamlVersion" implementation "org.opensaml:opensaml-saml-api:$openSamlVersion"
@ -388,6 +392,7 @@ tasks.withType(JavaCompile).configureEach {
options.encoding = "UTF-8" options.encoding = "UTF-8"
dependsOn "spotlessApply" dependsOn "spotlessApply"
} }
compileJava { compileJava {
options.compilerArgs << "-parameters" options.compilerArgs << "-parameters"
} }

View File

@ -6,6 +6,8 @@ services:
resources: resources:
limits: limits:
memory: 4G memory: 4G
depends_on:
- db
healthcheck: healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"] test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"]
interval: 5s interval: 5s
@ -31,3 +33,14 @@ services:
METRICS_ENABLED: "true" METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true" SYSTEM_GOOGLEVISIBILITY: "true"
restart: on-failure:5 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"

View File

@ -6,6 +6,8 @@ services:
resources: resources:
limits: limits:
memory: 4G memory: 4G
depends_on:
- db
healthcheck: 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'"] 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 interval: 5s
@ -39,3 +41,14 @@ services:
METRICS_ENABLED: "true" METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true" SYSTEM_GOOGLEVISIBILITY: "true"
restart: on-failure:5 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"

View File

@ -6,6 +6,8 @@ services:
resources: resources:
limits: limits:
memory: 4G memory: 4G
depends_on:
- db
healthcheck: 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'"] 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 interval: 5s
@ -30,4 +32,24 @@ services:
SYSTEM_MAXFILESIZE: "100" SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true" METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "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 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"

View File

@ -6,6 +6,8 @@ services:
resources: resources:
limits: limits:
memory: 1G memory: 1G
depends_on:
- db
healthcheck: 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'"] 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 interval: 5s
@ -28,3 +30,15 @@ services:
METRICS_ENABLED: "true" METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true" SYSTEM_GOOGLEVISIBILITY: "true"
restart: on-failure:5 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"

View File

@ -1,6 +1,5 @@
package stirling.software.SPDF; package stirling.software.SPDF;
import java.awt.*;
import java.io.IOException; import java.io.IOException;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.nio.file.Files; import java.nio.file.Files;
@ -11,8 +10,6 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
import javax.swing.*;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication; 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.config.ConfigInitializer;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@SpringBootApplication
@EnableScheduling
@Slf4j @Slf4j
public class SPdfApplication { @EnableScheduling
@SpringBootApplication
public class SPDFApplication {
private static String serverPortStatic;
private static String baseUrlStatic;
@Autowired private Environment env; @Autowired private Environment env;
@Autowired ApplicationProperties applicationProperties; @Autowired private ApplicationProperties applicationProperties;
private static String baseUrlStatic; @Autowired(required = false)
private static String serverPortStatic; private WebBrowser webBrowser;
@Value("${baseUrl:http://localhost}") @Value("${baseUrl:http://localhost}")
private String baseUrl; 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 { 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(); Properties props = new Properties();
@ -84,7 +55,7 @@ public class SPdfApplication {
props.put("spring.main.web-application-type", "servlet"); props.put("spring.main.web-application-type", "servlet");
} }
app.setAdditionalProfiles("default"); app.setAdditionalProfiles(getActiveProfile(args));
app.addInitializers(new ConfigInitializer()); app.addInitializers(new ConfigInitializer());
Map<String, String> propertyFiles = new HashMap<>(); Map<String, String> propertyFiles = new HashMap<>();
@ -134,15 +105,6 @@ public class SPdfApplication {
printStartupLogs(); 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 @PostConstruct
public void init() { public void init() {
baseUrlStatic = this.baseUrl; baseUrlStatic = this.baseUrl;
@ -173,6 +135,17 @@ public class SPdfApplication {
log.info("Running configs {}", applicationProperties.toString()); 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 @PreDestroy
public void cleanup() { public void cleanup() {
if (webBrowser != null) { 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() { public static String getStaticBaseUrl() {
return baseUrlStatic; return baseUrlStatic;
} }

View File

@ -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<FileInfo> getBackupList();
}

View File

@ -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<FileInfo> getBackupList();
}

View File

@ -20,7 +20,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; 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.CertificateUtils;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@ -110,7 +110,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
// Construct URLs required for SAML configuration // Construct URLs required for SAML configuration
String serverUrl = String serverUrl =
SPdfApplication.getStaticBaseUrl() + ":" + SPdfApplication.getStaticPort(); SPDFApplication.getStaticBaseUrl() + ":" + SPDFApplication.getStaticPort();
String relyingPartyIdentifier = String relyingPartyIdentifier =
serverUrl + "/saml2/service-provider-metadata/" + registrationId; 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=" // "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
// + response.encodeRedirectURL(redirect_url); // + response.encodeRedirectURL(redirect_url);
log.info("Google does not have a specific logout URL"); log.info("Google does not have a specific logout URL");
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl); // log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
// response.sendRedirect(googleLogoutUrl); // response.sendRedirect(googleLogoutUrl);
// break; // break;
default: default:
String defaultRedirectUrl = request.getContextPath() + "/login?" + param; String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
log.info("Redirecting to default logout URL: " + defaultRedirectUrl); log.info("Redirecting to default logout URL: " + defaultRedirectUrl);

View File

@ -1,6 +1,6 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException; import java.sql.SQLException;
import java.util.UUID; import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -8,35 +8,37 @@ import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j; 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.ApplicationProperties;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
@Component
@Slf4j @Slf4j
@Component
public class InitialSecuritySetup { public class InitialSecuritySetup {
@Autowired private UserService userService; @Autowired private UserService userService;
@Autowired private ApplicationProperties applicationProperties; @Autowired private ApplicationProperties applicationProperties;
// todo: wip add Postgres here @Autowired private DatabaseInterface databaseService;
@Autowired private DatabaseBackupInterface databaseBackupHelper;
@PostConstruct @PostConstruct
public void init() throws IllegalArgumentException, IOException { public void init() {
if (databaseBackupHelper.hasBackup() && !userService.hasUsers()) { try {
databaseBackupHelper.importDatabase(); if (!userService.hasUsers()) {
} else if (!userService.hasUsers()) { initializeAdminUser();
initializeAdminUser(); }
} else {
databaseBackupHelper.exportDatabase();
userService.migrateOauth2ToSSO(); 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 = String initialUsername =
applicationProperties.getSecurity().getInitialLogin().getUsername(); applicationProperties.getSecurity().getInitialLogin().getUsername();
String initialPassword = String initialPassword =
@ -45,36 +47,34 @@ public class InitialSecuritySetup {
&& !initialUsername.isEmpty() && !initialUsername.isEmpty()
&& initialPassword != null && initialPassword != null
&& !initialPassword.isEmpty() && !initialPassword.isEmpty()
&& !userService.findByUsernameIgnoreCase(initialUsername).isPresent()) { && userService.findByUsernameIgnoreCase(initialUsername).isEmpty()) {
try {
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
log.info("Admin user created: " + initialUsername); log.info("Admin user created: {}", initialUsername);
} catch (IllegalArgumentException e) {
log.error("Failed to initialize security setup", e);
System.exit(1);
}
} else { } else {
createDefaultAdminUser(); createDefaultAdminUser();
} }
} }
private void createDefaultAdminUser() throws IllegalArgumentException, IOException { private void createDefaultAdminUser() throws SQLException, UnsupportedProviderException {
String defaultUsername = "admin"; String defaultUsername = "admin";
String defaultPassword = "stirling"; String defaultPassword = "stirling";
if (!userService.findByUsernameIgnoreCase(defaultUsername).isPresent()) {
if (userService.findByUsernameIgnoreCase(defaultUsername).isEmpty()) {
userService.saveUser(defaultUsername, defaultPassword, Role.ADMIN.getRoleId(), true); 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())) { if (!userService.usernameExistsIgnoreCase(Role.INTERNAL_API_USER.getRoleId())) {
userService.saveUser( userService.saveUser(
Role.INTERNAL_API_USER.getRoleId(), Role.INTERNAL_API_USER.getRoleId(),
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
Role.INTERNAL_API_USER.getRoleId()); Role.INTERNAL_API_USER.getRoleId());
userService.addApiKeyToUser(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()); userService.syncCustomApiUser(applicationProperties.getSecurity().getCustomGlobalAPIKey());
} }

View File

@ -1,6 +1,6 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException; import java.sql.SQLException;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -21,7 +21,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import lombok.extern.slf4j.Slf4j; 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.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface; 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.Authority;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.repository.AuthorityRepository; import stirling.software.SPDF.repository.AuthorityRepository;
import stirling.software.SPDF.repository.UserRepository; import stirling.software.SPDF.repository.UserRepository;
@ -47,7 +48,7 @@ public class UserService implements UserServiceInterface {
@Autowired private SessionPersistentRegistry sessionRegistry; @Autowired private SessionPersistentRegistry sessionRegistry;
@Autowired DatabaseBackupInterface databaseBackupHelper; @Autowired DatabaseInterface databaseService;
@Autowired ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
@ -64,7 +65,7 @@ public class UserService implements UserServiceInterface {
// Handle OAUTH2 login and user auto creation. // Handle OAUTH2 login and user auto creation.
public boolean processSSOPostLogin(String username, boolean autoCreateUser) public boolean processSSOPostLogin(String username, boolean autoCreateUser)
throws IllegalArgumentException, IOException { throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
return false; return false;
} }
@ -151,12 +152,12 @@ public class UserService implements UserServiceInterface {
} }
public void saveUser(String username, AuthenticationType authenticationType) public void saveUser(String username, AuthenticationType authenticationType)
throws IllegalArgumentException, IOException { throws IllegalArgumentException, SQLException, UnsupportedProviderException {
saveUser(username, authenticationType, Role.USER.getRoleId()); saveUser(username, authenticationType, Role.USER.getRoleId());
} }
public void saveUser(String username, AuthenticationType authenticationType, String role) public void saveUser(String username, AuthenticationType authenticationType, String role)
throws IllegalArgumentException, IOException { throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage()); throw new IllegalArgumentException(getInvalidUsernameMessage());
} }
@ -167,11 +168,11 @@ public class UserService implements UserServiceInterface {
user.addAuthority(new Authority(role, user)); user.addAuthority(new Authority(role, user));
user.setAuthenticationType(authenticationType); user.setAuthenticationType(authenticationType);
userRepository.save(user); userRepository.save(user);
databaseBackupHelper.exportDatabase(); databaseService.exportDatabase();
} }
public void saveUser(String username, String password) public void saveUser(String username, String password)
throws IllegalArgumentException, IOException { throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage()); throw new IllegalArgumentException(getInvalidUsernameMessage());
} }
@ -181,11 +182,11 @@ public class UserService implements UserServiceInterface {
user.setEnabled(true); user.setEnabled(true);
user.setAuthenticationType(AuthenticationType.WEB); user.setAuthenticationType(AuthenticationType.WEB);
userRepository.save(user); userRepository.save(user);
databaseBackupHelper.exportDatabase(); databaseService.exportDatabase();
} }
public void saveUser(String username, String password, String role, boolean firstLogin) public void saveUser(String username, String password, String role, boolean firstLogin)
throws IllegalArgumentException, IOException { throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage()); throw new IllegalArgumentException(getInvalidUsernameMessage());
} }
@ -197,11 +198,11 @@ public class UserService implements UserServiceInterface {
user.setAuthenticationType(AuthenticationType.WEB); user.setAuthenticationType(AuthenticationType.WEB);
user.setFirstLogin(firstLogin); user.setFirstLogin(firstLogin);
userRepository.save(user); userRepository.save(user);
databaseBackupHelper.exportDatabase(); databaseService.exportDatabase();
} }
public void saveUser(String username, String password, String role) public void saveUser(String username, String password, String role)
throws IllegalArgumentException, IOException { throws IllegalArgumentException, SQLException, UnsupportedProviderException {
saveUser(username, password, role, false); saveUser(username, password, role, false);
} }
@ -235,7 +236,7 @@ public class UserService implements UserServiceInterface {
} }
public void updateUserSettings(String username, Map<String, String> updates) public void updateUserSettings(String username, Map<String, String> updates)
throws IOException { throws SQLException, UnsupportedProviderException {
Optional<User> userOpt = findByUsernameIgnoreCaseWithSettings(username); Optional<User> userOpt = findByUsernameIgnoreCaseWithSettings(username);
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
User user = userOpt.get(); User user = userOpt.get();
@ -249,7 +250,7 @@ public class UserService implements UserServiceInterface {
user.setSettings(settingsMap); user.setSettings(settingsMap);
userRepository.save(user); userRepository.save(user);
databaseBackupHelper.exportDatabase(); databaseService.exportDatabase();
} }
} }
@ -270,38 +271,42 @@ public class UserService implements UserServiceInterface {
} }
public void changeUsername(User user, String newUsername) public void changeUsername(User user, String newUsername)
throws IllegalArgumentException, IOException { throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!isUsernameValid(newUsername)) { if (!isUsernameValid(newUsername)) {
throw new IllegalArgumentException(getInvalidUsernameMessage()); throw new IllegalArgumentException(getInvalidUsernameMessage());
} }
user.setUsername(newUsername); user.setUsername(newUsername);
userRepository.save(user); 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)); user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user); 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); user.setFirstLogin(firstUse);
userRepository.save(user); 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); Authority userAuthority = this.findRole(user);
userAuthority.setAuthority(newRole); userAuthority.setAuthority(newRole);
authorityRepository.save(userAuthority); 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); user.setEnabled(enbeled);
userRepository.save(user); userRepository.save(user);
databaseBackupHelper.exportDatabase(); databaseService.exportDatabase();
} }
public boolean isPasswordCorrect(User user, String currentPassword) { public boolean isPasswordCorrect(User user, String currentPassword) {
@ -391,7 +396,8 @@ public class UserService implements UserServiceInterface {
} }
@Transactional @Transactional
public void syncCustomApiUser(String customApiKey) throws IOException { public void syncCustomApiUser(String customApiKey)
throws SQLException, UnsupportedProviderException {
if (customApiKey == null || customApiKey.trim().length() == 0) { if (customApiKey == null || customApiKey.trim().length() == 0) {
return; return;
} }
@ -409,14 +415,14 @@ public class UserService implements UserServiceInterface {
user.setApiKey(customApiKey); user.setApiKey(customApiKey);
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user)); user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
userRepository.save(user); userRepository.save(user);
databaseBackupHelper.exportDatabase(); databaseService.exportDatabase();
} else { } else {
// Update API key if it has changed // Update API key if it has changed
User user = existingUser.get(); User user = existingUser.get();
if (!customApiKey.equals(user.getApiKey())) { if (!customApiKey.equals(user.getApiKey())) {
user.setApiKey(customApiKey); user.setApiKey(customApiKey);
userRepository.save(user); userRepository.save(user);
databaseBackupHelper.exportDatabase(); databaseService.exportDatabase();
} }
} }
} }

View File

@ -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 <code>DataSource</code> for the connection to the DB. If <code>useDefault</code>
* is set to <code>true</code>, it will use the default H2 DB. If it is set to <code>false
* </code>, it will use the user's custom configuration set in the settings.yml.
*
* @return a <code>DataSource</code> 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 <code>DataSource</code> 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 <code>DataSource</code> URL
*/
private String getDataSourceUrl(
String dataSourceType, String hostname, Integer port, String dataSourceName) {
return DATASOURCE_URL_TEMPLATE.formatted(dataSourceType, hostname, port, dataSourceName);
}
/**
* @return a <code>Connection</code> using the configured <code>DataSource</code>
* @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");
}
}
}

View File

@ -7,8 +7,6 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.sql.Statement;
@ -20,49 +18,41 @@ import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; 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 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; import stirling.software.SPDF.utils.FileInfo;
@Slf4j @Slf4j
@Configuration @Service
public class DatabaseBackupHelper implements DatabaseBackupInterface { public class DatabaseService implements DatabaseInterface {
@Value("${spring.datasource.url}") public static final String BACKUP_PREFIX = "backup_";
private String url; public static final String SQL_SUFFIX = ".sql";
private static final Path BACKUP_PATH = Paths.get("configs/db/backup/");
@Value("${spring.datasource.username}") @Autowired private DatabaseConfig databaseConfig;
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();
}
@Override @Override
public List<FileInfo> getBackupList() { public List<FileInfo> getBackupList() {
// Check if the backup directory exists, and create it if it does not
ensureBackupDirectoryExists();
List<FileInfo> backupFiles = new ArrayList<>(); List<FileInfo> backupFiles = new ArrayList<>();
// Read the backup directory and filter for files with the prefix "backup_" and suffix // Read the backup directory and filter for files with the prefix "backup_" and suffix
// ".sql" // ".sql"
try (DirectoryStream<Path> stream = try (DirectoryStream<Path> stream =
Files.newDirectoryStream( Files.newDirectoryStream(
backupPath, BACKUP_PATH,
path -> path ->
path.getFileName().toString().startsWith("backup_") path.getFileName().toString().startsWith(BACKUP_PREFIX)
&& path.getFileName().toString().endsWith(".sql"))) { && path.getFileName().toString().endsWith(SQL_SUFFIX))) {
for (Path entry : stream) { for (Path entry : stream) {
BasicFileAttributes attrs = Files.readAttributes(entry, BasicFileAttributes.class); BasicFileAttributes attrs = Files.readAttributes(entry, BasicFileAttributes.class);
LocalDateTime modificationDate = LocalDateTime modificationDate =
@ -83,90 +73,105 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
} catch (IOException e) { } catch (IOException e) {
log.error("Error reading backup directory: {}", e.getMessage(), e); log.error("Error reading backup directory: {}", e.getMessage(), e);
} }
return backupFiles; return backupFiles;
} }
// Imports a database backup from the specified file. // Imports a database backup from the specified file.
public boolean importDatabaseFromUI(String fileName) throws IOException { public boolean importDatabaseFromUI(String fileName) {
return this.importDatabaseFromUI(getBackupFilePath(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. // Imports a database backup from the specified path.
public boolean importDatabaseFromUI(Path tempTemplatePath) throws IOException { private void importDatabaseFromUI(Path tempTemplatePath) throws IOException {
boolean success = executeDatabaseScript(tempTemplatePath); executeDatabaseScript(tempTemplatePath);
if (success) { LocalDateTime dateNow = LocalDateTime.now();
LocalDateTime dateNow = LocalDateTime.now(); DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm"); Path insertOutputFilePath =
Path insertOutputFilePath = this.getBackupFilePath(
this.getBackupFilePath("backup_user_" + dateNow.format(myFormatObj) + ".sql"); BACKUP_PREFIX + "user_" + dateNow.format(myFormatObj) + SQL_SUFFIX);
Files.copy(tempTemplatePath, insertOutputFilePath); Files.copy(tempTemplatePath, insertOutputFilePath);
Files.deleteIfExists(tempTemplatePath); Files.deleteIfExists(tempTemplatePath);
}
return success;
} }
@Override @Override
public boolean importDatabase() { public void exportDatabase() throws SQLException, UnsupportedProviderException {
if (!this.hasBackup()) return false;
List<FileInfo> 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();
// Filter and delete old backups if there are more than 5 // Filter and delete old backups if there are more than 5
List<FileInfo> filteredBackupList = List<FileInfo> filteredBackupList =
this.getBackupList().stream() this.getBackupList().stream()
.filter(backup -> !backup.getFileName().startsWith("backup_user_")) .filter(backup -> !backup.getFileName().startsWith(BACKUP_PREFIX + "user_"))
.collect(Collectors.toList()); .collect(Collectors.toList());
if (filteredBackupList.size() > 5) { if (filteredBackupList.size() > 5) {
filteredBackupList.sort( deleteOldestBackup(filteredBackupList);
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(); LocalDateTime dateNow = LocalDateTime.now();
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm"); DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
Path insertOutputFilePath = Path insertOutputFilePath =
this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql"); this.getBackupFilePath(BACKUP_PREFIX + dateNow.format(myFormatObj) + SQL_SUFFIX);
String query = "SCRIPT SIMPLE COLUMNS DROP to ?;";
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); log.info("Database export completed: {}", insertOutputFilePath);
} catch (SQLException e) { } catch (SQLException | UnsupportedProviderException e) {
log.error("Error during database export: {}", e.getMessage(), 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<FileInfo> 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. // Retrieves the H2 database version.
public String getH2Version() { public String getH2Version() {
String version = "Unknown"; String version = "Unknown";
try (Connection conn =
DriverManager.getConnection(url, databaseUsername, databasePassword)) { if (databaseConfig
try (Statement stmt = conn.createStatement(); .getApplicationProperties()
ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) { .getSystem()
if (rs.next()) { .getDatasource()
version = rs.getString("version"); .getType()
log.info("H2 Database Version: {}", version); .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; return version;
} }
@ -188,36 +193,22 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
// Gets the Path object for a given backup file name. // Gets the Path object for a given backup file name.
public Path getBackupFilePath(String fileName) { public Path getBackupFilePath(String fileName) {
Path filePath = Paths.get(backupPath.toString(), fileName).normalize(); Path filePath = Paths.get(BACKUP_PATH.toString(), fileName).normalize();
if (!filePath.startsWith(backupPath)) { if (!filePath.startsWith(BACKUP_PATH)) {
throw new SecurityException("Path traversal detected"); throw new SecurityException("Path traversal detected");
} }
return filePath; return filePath;
} }
private boolean executeDatabaseScript(Path scriptPath) { private void executeDatabaseScript(Path scriptPath) {
String query = "RUNSCRIPT from ?;"; 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); log.info("Database import completed: {}", scriptPath);
return true; } catch (SQLException | UnsupportedProviderException e) {
} catch (SQLException e) {
log.error("Error during database import: {}", e.getMessage(), e); log.error("Error during database import: {}", e.getMessage(), e);
return false; } catch (ScriptException e) {
} log.error("Error: File {} not found", scriptPath.toString(), e);
}
private void ensureBackupDirectoryExists() {
if (Files.notExists(backupPath)) {
try {
Files.createDirectories(backupPath);
} catch (IOException e) {
log.error("Error creating directories: {}", e.getMessage());
}
} }
} }

View File

@ -1,18 +1,20 @@
package stirling.software.SPDF.config.security.database; 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.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
@Component @Component
public class ScheduledTasks { public class ScheduledTasks {
@Autowired private DatabaseBackupHelper databaseBackupService; @Autowired private DatabaseService databaseService;
@Scheduled(cron = "0 0 0 * * ?") @Scheduled(cron = "0 0 0 * * ?")
public void performBackup() throws IOException { public void performBackup() throws SQLException, UnsupportedProviderException {
databaseBackupService.exportDatabase(); databaseService.exportDatabase();
} }
} }

View File

@ -1,6 +1,7 @@
package stirling.software.SPDF.config.security.oauth2; package stirling.software.SPDF.config.security.oauth2;
import java.io.IOException; import java.io.IOException;
import java.sql.SQLException;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication; 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;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.AuthenticationType; import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.RequestUriUtils; import stirling.software.SPDF.utils.RequestUriUtils;
public class CustomOAuth2AuthenticationSuccessHandler public class CustomOAuth2AuthenticationSuccessHandler
@ -97,10 +99,8 @@ public class CustomOAuth2AuthenticationSuccessHandler
userService.processSSOPostLogin(username, oAuth.getAutoCreateUser()); userService.processSSOPostLogin(username, oAuth.getAutoCreateUser());
} }
response.sendRedirect(contextPath + "/"); response.sendRedirect(contextPath + "/");
return; } catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
} catch (IllegalArgumentException e) {
response.sendRedirect(contextPath + "/logout?invalidUsername=true"); response.sendRedirect(contextPath + "/logout?invalidUsername=true");
return;
} }
} }
} }

View File

@ -1,6 +1,7 @@
package stirling.software.SPDF.config.security.saml2; package stirling.software.SPDF.config.security.saml2;
import java.io.IOException; import java.io.IOException;
import java.sql.SQLException;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication; 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;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2; import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.AuthenticationType; import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.RequestUriUtils; import stirling.software.SPDF.utils.RequestUriUtils;
@AllArgsConstructor @AllArgsConstructor
@ -108,13 +110,14 @@ public class CustomSaml2AuthenticationSuccessHandler
userService.processSSOPostLogin(username, saml2.getAutoCreateUser()); userService.processSSOPostLogin(username, saml2.getAutoCreateUser());
log.debug("Successfully processed authentication for user: {}", username); log.debug("Successfully processed authentication for user: {}", username);
response.sendRedirect(contextPath + "/"); response.sendRedirect(contextPath + "/");
return;
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
log.debug( log.debug(
"Invalid username detected for user: {}, redirecting to logout", "Invalid username detected for user: {}, redirecting to logout",
username); username);
response.sendRedirect(contextPath + "/logout?invalidUsername=true"); 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 { } else {

View File

@ -29,7 +29,7 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.database.DatabaseBackupHelper; import stirling.software.SPDF.config.security.database.DatabaseService;
@Slf4j @Slf4j
@Controller @Controller
@ -38,19 +38,16 @@ import stirling.software.SPDF.config.security.database.DatabaseBackupHelper;
@Tag(name = "Database", description = "Database APIs for backup, import, and management") @Tag(name = "Database", description = "Database APIs for backup, import, and management")
public class DatabaseController { public class DatabaseController {
@Autowired DatabaseBackupHelper databaseBackupHelper; @Autowired DatabaseService databaseService;
@Operation( @Hidden
summary = "Import a database backup file",
description = "Uploads and imports a database backup SQL file.")
@PostMapping(consumes = "multipart/form-data", value = "import-database") @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( public String importDatabase(
@Parameter(description = "SQL file to import", required = true) @RequestParam("fileInput") MultipartFile file, RedirectAttributes redirectAttributes)
@RequestParam("fileInput") throws IllegalArgumentException, IOException {
MultipartFile file,
RedirectAttributes redirectAttributes)
throws IOException {
if (file == null || file.isEmpty()) { if (file == null || file.isEmpty()) {
redirectAttributes.addAttribute("error", "fileNullOrEmpty"); redirectAttributes.addAttribute("error", "fileNullOrEmpty");
return "redirect:/database"; return "redirect:/database";
@ -60,7 +57,8 @@ public class DatabaseController {
Path tempTemplatePath = Files.createTempFile("backup_", ".sql"); Path tempTemplatePath = Files.createTempFile("backup_", ".sql");
try (InputStream in = file.getInputStream()) { try (InputStream in = file.getInputStream()) {
Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING); Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING);
boolean importSuccess = databaseBackupHelper.importDatabaseFromUI(tempTemplatePath); boolean importSuccess =
databaseService.importDatabaseFromUI(tempTemplatePath.toString());
if (importSuccess) { if (importSuccess) {
redirectAttributes.addAttribute("infoMessage", "importIntoDatabaseSuccessed"); redirectAttributes.addAttribute("infoMessage", "importIntoDatabaseSuccessed");
} else { } else {
@ -80,8 +78,7 @@ public class DatabaseController {
@GetMapping("/import-database-file/{fileName}") @GetMapping("/import-database-file/{fileName}")
public String importDatabaseFromBackupUI( public String importDatabaseFromBackupUI(
@Parameter(description = "Name of the file to import", required = true) @PathVariable @Parameter(description = "Name of the file to import", required = true) @PathVariable
String fileName) String fileName) {
throws IOException {
if (fileName == null || fileName.isEmpty()) { if (fileName == null || fileName.isEmpty()) {
return "redirect:/database?error=fileNullOrEmpty"; return "redirect:/database?error=fileNullOrEmpty";
@ -89,15 +86,14 @@ public class DatabaseController {
// Check if the file exists in the backup list // Check if the file exists in the backup list
boolean fileExists = boolean fileExists =
databaseBackupHelper.getBackupList().stream() databaseService.getBackupList().stream()
.anyMatch(backup -> backup.getFileName().equals(fileName)); .anyMatch(backup -> backup.getFileName().equals(fileName));
if (!fileExists) { if (!fileExists) {
log.error("File {} not found in backup list", fileName); log.error("File {} not found in backup list", fileName);
return "redirect:/database?error=fileNotFound"; return "redirect:/database?error=fileNotFound";
} }
log.info("Received file: {}", fileName); log.info("Received file: {}", fileName);
if (databaseService.importDatabaseFromUI(fileName)) {
if (databaseBackupHelper.importDatabaseFromUI(fileName)) {
log.info("File {} imported to database", fileName); log.info("File {} imported to database", fileName);
return "redirect:/database?infoMessage=importIntoDatabaseSuccessed"; return "redirect:/database?infoMessage=importIntoDatabaseSuccessed";
} }
@ -105,19 +101,17 @@ public class DatabaseController {
} }
@Hidden @Hidden
@GetMapping("/delete/{fileName}")
@Operation( @Operation(
summary = "Delete a database backup file", summary = "Delete a database backup file",
description = "Deletes a specified database backup file from the server.") description =
@GetMapping("/delete/{fileName}") "This endpoint deletes a database backup file with the specified file name.")
public String deleteFile( public String deleteFile(@PathVariable String fileName) {
@Parameter(description = "Name of the file to delete", required = true) @PathVariable
String fileName) {
if (fileName == null || fileName.isEmpty()) { if (fileName == null || fileName.isEmpty()) {
throw new IllegalArgumentException("File must not be null or empty"); throw new IllegalArgumentException("File must not be null or empty");
} }
try { try {
if (databaseBackupHelper.deleteBackupFile(fileName)) { if (databaseService.deleteBackupFile(fileName)) {
log.info("Deleted file: {}", fileName); log.info("Deleted file: {}", fileName);
} else { } else {
log.error("Failed to delete file: {}", fileName); log.error("Failed to delete file: {}", fileName);
@ -131,18 +125,17 @@ public class DatabaseController {
} }
@Hidden @Hidden
@GetMapping("/download/{fileName}")
@Operation( @Operation(
summary = "Download a database backup file", summary = "Download a database backup file",
description = "Downloads the specified database backup file from the server.") description =
@GetMapping("/download/{fileName}") "This endpoint downloads a database backup file with the specified file name.")
public ResponseEntity<?> downloadFile( public ResponseEntity<?> downloadFile(@PathVariable String fileName) {
@Parameter(description = "Name of the file to download", required = true) @PathVariable
String fileName) {
if (fileName == null || fileName.isEmpty()) { if (fileName == null || fileName.isEmpty()) {
throw new IllegalArgumentException("File must not be null or empty"); throw new IllegalArgumentException("File must not be null or empty");
} }
try { try {
Path filePath = databaseBackupHelper.getBackupFilePath(fileName); Path filePath = databaseService.getBackupFilePath(fileName);
InputStreamResource resource = new InputStreamResource(Files.newInputStream(filePath)); InputStreamResource resource = new InputStreamResource(Files.newInputStream(filePath));
return ResponseEntity.ok() return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName)
@ -156,22 +149,4 @@ public class DatabaseController {
.build(); .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";
}
} }

View File

@ -2,6 +2,7 @@ package stirling.software.SPDF.controller.api;
import java.io.IOException; import java.io.IOException;
import java.security.Principal; import java.security.Principal;
import java.sql.SQLException;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; 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.Role;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.api.user.UsernameAndPass; import stirling.software.SPDF.model.api.user.UsernameAndPass;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
@Controller @Controller
@Tag(name = "User", description = "User APIs") @Tag(name = "User", description = "User APIs")
@ -51,17 +53,17 @@ public class UserController {
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/register") @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())) { if (userService.usernameExistsIgnoreCase(requestModel.getUsername())) {
model.addAttribute("error", "Username already exists"); model.addAttribute("error", "Username already exists");
return "register"; return "register";
} }
try { try {
userService.saveUser(requestModel.getUsername(), requestModel.getPassword()); userService.saveUser(requestModel.getUsername(), requestModel.getPassword());
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
return "redirect:/login?messageType=invalidUsername"; return "redirect:/login?messageType=invalidUsername";
} }
return "redirect:/login?registered=true"; return "redirect:/login?registered=true";
} }
@ -73,8 +75,7 @@ public class UserController {
@RequestParam(name = "newUsername") String newUsername, @RequestParam(name = "newUsername") String newUsername,
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
RedirectAttributes redirectAttributes) RedirectAttributes redirectAttributes) {
throws IOException {
if (!userService.isUsernameValid(newUsername)) { if (!userService.isUsernameValid(newUsername)) {
return new RedirectView("/account?messageType=invalidUsername", true); return new RedirectView("/account?messageType=invalidUsername", true);
@ -108,7 +109,7 @@ public class UserController {
if (newUsername != null && newUsername.length() > 0) { if (newUsername != null && newUsername.length() > 0) {
try { try {
userService.changeUsername(user, newUsername); userService.changeUsername(user, newUsername);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
return new RedirectView("/account?messageType=invalidUsername", true); return new RedirectView("/account?messageType=invalidUsername", true);
} }
} }
@ -128,7 +129,7 @@ public class UserController {
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
RedirectAttributes redirectAttributes) RedirectAttributes redirectAttributes)
throws IOException { throws SQLException, UnsupportedProviderException {
if (principal == null) { if (principal == null) {
return new RedirectView("/change-creds?messageType=notAuthenticated", true); return new RedirectView("/change-creds?messageType=notAuthenticated", true);
} }
@ -162,7 +163,7 @@ public class UserController {
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
RedirectAttributes redirectAttributes) RedirectAttributes redirectAttributes)
throws IOException { throws SQLException, UnsupportedProviderException {
if (principal == null) { if (principal == null) {
return new RedirectView("/account?messageType=notAuthenticated", true); return new RedirectView("/account?messageType=notAuthenticated", true);
} }
@ -190,7 +191,7 @@ public class UserController {
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/updateUserSettings") @PostMapping("/updateUserSettings")
public String updateUserSettings(HttpServletRequest request, Principal principal) public String updateUserSettings(HttpServletRequest request, Principal principal)
throws IOException { throws SQLException, UnsupportedProviderException {
Map<String, String[]> paramMap = request.getParameterMap(); Map<String, String[]> paramMap = request.getParameterMap();
Map<String, String> updates = new HashMap<>(); Map<String, String> updates = new HashMap<>();
@ -215,7 +216,7 @@ public class UserController {
@RequestParam(name = "authType") String authType, @RequestParam(name = "authType") String authType,
@RequestParam(name = "forceChange", required = false, defaultValue = "false") @RequestParam(name = "forceChange", required = false, defaultValue = "false")
boolean forceChange) boolean forceChange)
throws IllegalArgumentException, IOException { throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!userService.isUsernameValid(username)) { if (!userService.isUsernameValid(username)) {
return new RedirectView("/addUsers?messageType=invalidUsername", true); return new RedirectView("/addUsers?messageType=invalidUsername", true);
@ -263,7 +264,7 @@ public class UserController {
@RequestParam(name = "username") String username, @RequestParam(name = "username") String username,
@RequestParam(name = "role") String role, @RequestParam(name = "role") String role,
Authentication authentication) Authentication authentication)
throws IOException { throws IOException, SQLException, UnsupportedProviderException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username); Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
@ -280,6 +281,7 @@ public class UserController {
if (currentUsername.equalsIgnoreCase(username)) { if (currentUsername.equalsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=downgradeCurrentUser", true); return new RedirectView("/addUsers?messageType=downgradeCurrentUser", true);
} }
try { try {
// Validate the role // Validate the role
Role roleEnum = Role.fromString(role); Role roleEnum = Role.fromString(role);
@ -305,7 +307,7 @@ public class UserController {
@PathVariable("username") String username, @PathVariable("username") String username,
@RequestParam("enabled") boolean enabled, @RequestParam("enabled") boolean enabled,
Authentication authentication) Authentication authentication)
throws IOException { throws IOException, SQLException, UnsupportedProviderException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username); Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);

View File

@ -20,12 +20,12 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContext;
import lombok.extern.slf4j.Slf4j; 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.ApiEndpoint;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
@Service
@Slf4j @Slf4j
@Service
public class ApiDocService { public class ApiDocService {
private final Map<String, ApiEndpoint> apiDocumentation = new HashMap<>(); private final Map<String, ApiEndpoint> apiDocumentation = new HashMap<>();
@ -34,7 +34,7 @@ public class ApiDocService {
private String getApiDocsUrl() { private String getApiDocsUrl() {
String contextPath = servletContext.getContextPath(); String contextPath = servletContext.getContextPath();
String port = SPdfApplication.getStaticPort(); String port = SPDFApplication.getStaticPort();
return "http://localhost:" + port + contextPath + "/v1/api-docs"; return "http://localhost:" + port + contextPath + "/v1/api-docs";
} }

View File

@ -39,7 +39,7 @@ import io.github.pixee.security.ZipSecurity;
import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContext;
import lombok.extern.slf4j.Slf4j; 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.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineOperation;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
@ -62,7 +62,7 @@ public class PipelineProcessor {
private String getBaseUrl() { private String getBaseUrl() {
String contextPath = servletContext.getContextPath(); String contextPath = servletContext.getContextPath();
String port = SPdfApplication.getStaticPort(); String port = SPDFApplication.getStaticPort();
return "http://localhost:" + port + contextPath + "/"; return "http://localhost:" + port + contextPath + "/";
} }

View File

@ -167,8 +167,8 @@ public class AccountWebController {
case "invalid_destination": case "invalid_destination":
erroroauth = "login.invalid_destination"; erroroauth = "login.invalid_destination";
break; break;
// Valid InResponseTo was not available from the validation context, unable to // Valid InResponseTo was not available from the validation context, unable to
// evaluate // evaluate
case "invalid_in_response_to": case "invalid_in_response_to":
erroroauth = "login.invalid_in_response_to"; erroroauth = "login.invalid_in_response_to";
break; break;

View File

@ -12,14 +12,14 @@ import org.springframework.web.bind.annotation.GetMapping;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; 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; import stirling.software.SPDF.utils.FileInfo;
@Controller @Controller
@Tag(name = "Database Management", description = "Database management and security APIs") @Tag(name = "Database Management", description = "Database management and security APIs")
public class DatabaseWebController { public class DatabaseWebController {
@Autowired private DatabaseBackupHelper databaseBackupHelper; @Autowired private DatabaseService databaseService;
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/database") @GetMapping("/database")
@ -33,10 +33,10 @@ public class DatabaseWebController {
model.addAttribute("infoMessage", confirmed); model.addAttribute("infoMessage", confirmed);
} }
List<FileInfo> backupList = databaseBackupHelper.getBackupList(); List<FileInfo> backupList = databaseService.getBackupList();
model.addAttribute("backupFiles", backupList); model.addAttribute("backupFiles", backupList);
model.addAttribute("databaseVersion", databaseBackupHelper.getH2Version()); model.addAttribute("databaseVersion", databaseService.getH2Version());
return "database"; return "database";
} }

View File

@ -6,7 +6,6 @@ import java.net.HttpURLConnection;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.sql.Driver;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@ -253,23 +252,37 @@ public class ApplicationProperties {
@Data @Data
public static class Datasource { public static class Datasource {
private String url; private String type;
private Driver driver; private String hostName;
private Integer port;
private String name;
private String username; private String username;
private String password; @ToString.Exclude private String password;
private boolean useDefault;
private final String defaultUrl;
} }
public enum Driver { public enum Driver {
H2("h2"), H2("h2"),
POSTGRESQL("postgresql"), POSTGRESQL("postgresql"),
ORACLE("oracle"), ORACLE("oracle"),
MY_SQL("mysql"); MYSQL("mysql");
private final String driverName; private final String driverName;
Driver(String driverName) { Driver(String driverName) {
this.driverName = driverName; this.driverName = driverName;
} }
@Override
public String toString() {
return """
Driver {
driverName='%s'
}
"""
.formatted(driverName);
}
} }
@Data @Data

View File

@ -5,7 +5,6 @@ import java.util.Date;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.Lob;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import lombok.Data; import lombok.Data;
@ -15,7 +14,7 @@ import lombok.Data;
public class SessionEntity implements Serializable { public class SessionEntity implements Serializable {
@Id private String sessionId; @Id private String sessionId;
@Lob private String principalName; private String principalName;
private Date lastRequest; private Date lastRequest;

View File

@ -47,7 +47,7 @@ public class User implements Serializable {
@ElementCollection @ElementCollection
@MapKeyColumn(name = "setting_key") @MapKeyColumn(name = "setting_key")
@Lob @Lob
@Column(name = "setting_value", columnDefinition = "CLOB") @Column(name = "setting_value", columnDefinition = "text")
@CollectionTable(name = "user_settings", joinColumns = @JoinColumn(name = "user_id")) @CollectionTable(name = "user_settings", joinColumns = @JoinColumn(name = "user_id"))
private Map<String, String> settings = new HashMap<>(); // Key-value pairs of settings. private Map<String, String> settings = new HashMap<>(); // Key-value pairs of settings.

View File

@ -41,13 +41,12 @@ spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000}
#spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/ #spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/
#spring.thymeleaf.cache=false #spring.thymeleaf.cache=false
spring.datasource.url=jdbc:postgresql://localhost:5432/stirling-pdf-DB 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.postgresql.Driver spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=postgres spring.datasource.username=sa
spring.datasource.password= spring.datasource.password=
spring.jpa.generate-ddl=true spring.h2.console.enabled=false
spring.jpa.hibernate.ddl-auto=update spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
server.servlet.session.timeout=30m server.servlet.session.timeout=30m
# Change the default URL path for OpenAPI JSON # Change the default URL path for OpenAPI JSON
springdoc.api-docs.path=/v1/api-docs 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 # Set the URL of the OpenAPI JSON for the Swagger UI
springdoc.swagger-ui.url=/v1/api-docs springdoc.swagger-ui.url=/v1/api-docs
posthog.api.key=phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq posthog.api.key=phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq
posthog.host=https://eu.i.posthog.com posthog.host=https://eu.i.posthog.com

View File

@ -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. 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 enableAnalytics: undefined # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true
datasource: datasource:
url: jdbc:postgresql://localhost:5432/stirling-pdf-DB type: postgresql # the type of the database to set (e.g. 'h2', 'postgresql')
driver: 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)
username: postgres port: 5432 # set the port number of the database. Ensure this matches the port the database is listening to
password: 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: ui:
appName: '' # application's visible name appName: '' # application's visible name

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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> 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());
}
}

View File

@ -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());
}
}

View File

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