Merge remote-tracking branch 'origin/V2' into PaymentSelfhost

This commit is contained in:
Connor Yoh
2025-11-16 16:06:21 +00:00
203 changed files with 15828 additions and 2687 deletions

View File

@@ -505,10 +505,19 @@ public class ApplicationProperties {
public static class Ui {
private String appNameNavbar;
private List<String> languages;
private String logoStyle = "classic"; // Options: "classic" (default) or "modern"
public String getAppNameNavbar() {
return appNameNavbar != null && !appNameNavbar.trim().isEmpty() ? appNameNavbar : null;
}
public String getLogoStyle() {
// Validate and return either "modern" or "classic"
if ("modern".equalsIgnoreCase(logoStyle)) {
return "modern";
}
return "classic"; // default
}
}
@Data

View File

@@ -8,4 +8,6 @@ public interface UserServiceInterface {
long getTotalUsersCount();
boolean isCurrentUserAdmin();
boolean isCurrentUserFirstLogin();
}

View File

@@ -75,6 +75,9 @@ public class RequestUriUtils {
|| trimmedUri.startsWith("/api/v1/auth/login")
|| trimmedUri.startsWith("/api/v1/auth/refresh")
|| trimmedUri.startsWith("/api/v1/auth/logout")
|| trimmedUri.startsWith(
"/api/v1/proprietary/ui-data/login") // Login page config (SSO providers +
// enableLogin)
|| trimmedUri.startsWith("/v1/api-docs")
|| trimmedUri.startsWith("/api/v1/invite/validate")
|| trimmedUri.startsWith("/api/v1/invite/accept")

View File

@@ -28,6 +28,8 @@ public class InitialSetup {
private final ApplicationProperties applicationProperties;
private static boolean isNewServer = false;
@PostConstruct
public void init() throws IOException {
initUUIDKey();
@@ -88,6 +90,13 @@ public class InitialSetup {
}
public void initSetAppVersion() throws IOException {
// Check if this is a new server before setting the version
String existingVersion = applicationProperties.getAutomaticallyGenerated().getAppVersion();
isNewServer =
existingVersion == null
|| existingVersion.isEmpty()
|| existingVersion.equals("0.0.0");
String appVersion = "0.0.0";
Resource resource = new ClassPathResource("version.properties");
Properties props = new Properties();
@@ -99,4 +108,8 @@ public class InitialSetup {
GeneralUtils.saveKeyToSettings("AutomaticallyGenerated.appVersion", appVersion);
applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion);
}
public static boolean isNewServer() {
return isNewServer;
}
}

View File

@@ -10,7 +10,10 @@ import org.springframework.web.bind.annotation.RequestParam;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.config.InitialSetup;
import stirling.software.common.annotations.api.ConfigApi;
import stirling.software.common.configuration.AppConfig;
import stirling.software.common.model.ApplicationProperties;
@@ -19,6 +22,7 @@ import stirling.software.common.service.UserServiceInterface;
@ConfigApi
@Hidden
@Slf4j
public class ConfigController {
private final ApplicationProperties applicationProperties;
@@ -58,9 +62,15 @@ public class ConfigController {
// Extract values from ApplicationProperties
configData.put("appNameNavbar", applicationProperties.getUi().getAppNameNavbar());
configData.put("languages", applicationProperties.getUi().getLanguages());
configData.put("logoStyle", applicationProperties.getUi().getLogoStyle());
// Security settings
configData.put("enableLogin", applicationProperties.getSecurity().getEnableLogin());
// enableLogin requires both the config flag AND proprietary features to be loaded
// If userService is null, proprietary module isn't loaded
// (DISABLE_ADDITIONAL_FEATURES=true or DOCKER_ENABLE_SECURITY=false)
boolean enableLogin =
applicationProperties.getSecurity().getEnableLogin() && userService != null;
configData.put("enableLogin", enableLogin);
// Mail settings - check both SMTP enabled AND invites enabled
boolean smtpEnabled = applicationProperties.getMail().isEnabled();
@@ -78,6 +88,22 @@ public class ConfigController {
}
configData.put("isAdmin", isAdmin);
// Check if this is a new server (version was 0.0.0 before initialization)
configData.put("isNewServer", InitialSetup.isNewServer());
// Check if the current user is a first-time user
boolean isNewUser =
false; // Default to false when security is disabled or user not found
if (userService != null) {
try {
isNewUser = userService.isCurrentUserFirstLogin();
} catch (Exception e) {
// If there's an error, assume not new user for safety
isNewUser = false;
}
}
configData.put("isNewUser", isNewUser);
// System settings
configData.put(
"enableAlphaFunctionality",

View File

@@ -176,6 +176,7 @@ system:
ui:
appNameNavbar: '' # name displayed on the navigation bar
logoStyle: classic # Options: 'classic' (default - classic S icon) or 'modern' (minimalist logo)
languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled.
endpoints:

View File

@@ -7,6 +7,8 @@
--md-sys-color-surface-3: color-mix(in srgb, var(--md-sys-color-primary) 13%, rgba(0, 0, 255, 0.11) 5%);
--md-sys-color-surface-4: color-mix(in srgb, var(--md-sys-color-primary) 13%, rgba(0, 0, 255, 0.12) 5%);
--md-sys-color-surface-5: color-mix(in srgb, var(--md-sys-color-primary) 13%, rgba(0, 0, 255, 0.14) 5%);
/* Clear button disabled text color (default/light) */
--spdf-clear-disabled-text: var(--md-sys-color-primary);
/* Icon fill */
--md-sys-icon-fill-0: 'FILL' 0, 'wght' 500;
--md-sys-icon-fill-1: 'FILL' 1, 'wght' 500;
@@ -25,6 +27,12 @@
--md-sys-elevation-5: 0px 8px 10px -6px rgb(var(--md-elevation-shadow-color), 0.2), 0px 16px 24px 2px rgb(var(--md-elevation-shadow-color), 0.14), 0px 6px 30px 5px rgb(var(--md-elevation-shadow-color), 0.12);
}
/* Dark theme overrides */
.dark-theme {
/* In dark mode, use a neutral grey for disabled Clear button text */
--spdf-clear-disabled-text: var(--mantine-color-gray-5, #9e9e9e);
}
.fill {
font-variation-settings: var(--md-sys-icon-fill-1);
}

View File

@@ -116,6 +116,10 @@ public class ProprietaryUIDataController {
LoginData data = new LoginData();
Map<String, String> providerList = new HashMap<>();
Security securityProps = applicationProperties.getSecurity();
// Add enableLogin flag so frontend doesn't need to call /app-config
data.setEnableLogin(securityProps.getEnableLogin());
OAUTH2 oauth = securityProps.getOauth2();
if (oauth != null && oauth.getEnabled()) {
@@ -448,6 +452,7 @@ public class ProprietaryUIDataController {
@Data
public static class LoginData {
private Boolean enableLogin;
private Map<String, String> providerList;
private String loginMethod;
private boolean altLogin;

View File

@@ -742,4 +742,31 @@ public class UserController {
return errorMessage;
}
}
@PostMapping("/complete-initial-setup")
public ResponseEntity<?> completeInitialSetup() {
try {
String username = userService.getCurrentUsername();
if (username == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("User not authenticated");
}
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (userOpt.isEmpty()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found");
}
User user = userOpt.get();
user.setHasCompletedInitialSetup(true);
userRepository.save(user);
log.info("User {} completed initial setup", username);
return ResponseEntity.ok().body(Map.of("success", true));
} catch (Exception e) {
log.error("Error completing initial setup", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Failed to complete initial setup");
}
}
}

View File

@@ -223,7 +223,8 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|| trimmedUri.startsWith("/saml2")
|| trimmedUri.startsWith("/api/v1/auth/login")
|| trimmedUri.startsWith("/api/v1/auth/refresh")
|| trimmedUri.startsWith("/api/v1/auth/logout");
|| trimmedUri.startsWith("/api/v1/auth/logout")
|| trimmedUri.startsWith("/api/v1/proprietary/ui-data/login");
}
private enum UserLoginType {

View File

@@ -56,6 +56,9 @@ public class User implements UserDetails, Serializable {
@Column(name = "isFirstLogin")
private Boolean isFirstLogin = false;
@Column(name = "hasCompletedInitialSetup")
private Boolean hasCompletedInitialSetup = false;
@Column(name = "roleName")
private String roleName;
@@ -103,6 +106,14 @@ public class User implements UserDetails, Serializable {
this.isFirstLogin = isFirstLogin;
}
public boolean hasCompletedInitialSetup() {
return hasCompletedInitialSetup != null && hasCompletedInitialSetup;
}
public void setHasCompletedInitialSetup(boolean hasCompletedInitialSetup) {
this.hasCompletedInitialSetup = hasCompletedInitialSetup;
}
public void setAuthenticationType(AuthenticationType authenticationType) {
this.authenticationType = authenticationType.toString().toLowerCase();
}

View File

@@ -663,6 +663,21 @@ public class UserService implements UserServiceInterface {
return false;
}
public boolean isCurrentUserFirstLogin() {
try {
String username = getCurrentUsername();
if (username != null) {
Optional<User> userOpt = findByUsernameIgnoreCase(username);
if (userOpt.isPresent()) {
return !userOpt.get().hasCompletedInitialSetup();
}
}
} catch (Exception e) {
log.debug("Error checking first login status", e);
}
return false;
}
@Transactional
public void syncCustomApiUser(String customApiKey) {
if (customApiKey == null || customApiKey.trim().isBlank()) {