diff --git a/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminSettingsController.java b/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminSettingsController.java new file mode 100644 index 000000000..496820fac --- /dev/null +++ b/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminSettingsController.java @@ -0,0 +1,332 @@ +package stirling.software.proprietary.security.controller.api; + +import java.io.IOException; +import java.util.Map; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.configuration.InstallationPathConfig; +import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.util.GeneralUtils; +import stirling.software.proprietary.security.model.api.admin.SettingValueResponse; +import stirling.software.proprietary.security.model.api.admin.UpdateSettingValueRequest; +import stirling.software.proprietary.security.model.api.admin.UpdateSettingsRequest; + +@Controller +@Tag(name = "Admin Settings", description = "Admin-only Settings Management APIs") +@RequestMapping("/api/v1/admin/settings") +@RequiredArgsConstructor +@PreAuthorize("hasRole('ROLE_ADMIN')") +@Slf4j +public class AdminSettingsController { + + private final ApplicationProperties applicationProperties; + + @GetMapping + @Operation( + summary = "Get all application settings", + description = "Retrieve all current application settings. Admin access required.") + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "Settings retrieved successfully"), + @ApiResponse( + responseCode = "403", + description = "Access denied - Admin role required") + }) + public ResponseEntity getSettings() { + log.debug("Admin requested all application settings"); + return ResponseEntity.ok(applicationProperties); + } + + @PutMapping + @Operation( + summary = "Update application settings (delta updates)", + description = + "Update specific application settings using dot notation keys. Only sends changed values. Changes take effect on restart. Admin access required.") + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "Settings updated successfully"), + @ApiResponse(responseCode = "400", description = "Invalid setting key or value"), + @ApiResponse( + responseCode = "403", + description = "Access denied - Admin role required"), + @ApiResponse( + responseCode = "500", + description = "Failed to save settings to configuration file") + }) + public ResponseEntity updateSettings(@RequestBody UpdateSettingsRequest request) { + try { + Map settings = request.getSettings(); + if (settings == null || settings.isEmpty()) { + return ResponseEntity.badRequest().body("No settings provided to update"); + } + + int updatedCount = 0; + for (Map.Entry entry : settings.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + log.info("Admin updating setting: {} = {}", key, value); + GeneralUtils.saveKeyToSettings(key, value); + updatedCount++; + } + + return ResponseEntity.ok( + String.format( + "Successfully updated %d setting(s). Changes will take effect on application restart.", + updatedCount)); + + } catch (IOException e) { + log.error("Failed to save settings to file: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body( + "Failed to save settings to configuration file at: " + + InstallationPathConfig.getSettingsPath() + + ". Error: " + + e.getMessage()); + + } catch (Exception e) { + log.error("Unexpected error while updating settings: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body("Invalid setting key or value. Error: " + e.getMessage()); + } + } + + @GetMapping("/section/{sectionName}") + @Operation( + summary = "Get specific settings section", + description = + "Retrieve settings for a specific section (e.g., security, system, ui). Admin access required.") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Section settings retrieved successfully"), + @ApiResponse(responseCode = "400", description = "Invalid section name"), + @ApiResponse( + responseCode = "403", + description = "Access denied - Admin role required") + }) + public ResponseEntity getSettingsSection(@PathVariable String sectionName) { + try { + Object sectionData = getSectionData(sectionName); + if (sectionData == null) { + return ResponseEntity.badRequest() + .body( + "Invalid section name: " + + sectionName + + ". Valid sections: security, system, ui, endpoints, metrics, mail, premium, processExecutor, autoPipeline"); + } + log.debug("Admin requested settings section: {}", sectionName); + return ResponseEntity.ok(sectionData); + } catch (Exception e) { + log.error("Error retrieving section {}: {}", sectionName, e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to retrieve section: " + e.getMessage()); + } + } + + @PutMapping("/section/{sectionName}") + @Operation( + summary = "Update specific settings section", + description = "Update all settings within a specific section. Admin access required.") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Section settings updated successfully"), + @ApiResponse(responseCode = "400", description = "Invalid section name or data"), + @ApiResponse( + responseCode = "403", + description = "Access denied - Admin role required"), + @ApiResponse(responseCode = "500", description = "Failed to save settings") + }) + public ResponseEntity updateSettingsSection( + @PathVariable String sectionName, @RequestBody Map sectionData) { + try { + if (sectionData == null || sectionData.isEmpty()) { + return ResponseEntity.badRequest().body("No section data provided to update"); + } + + if (!isValidSectionName(sectionName)) { + return ResponseEntity.badRequest() + .body( + "Invalid section name: " + + sectionName + + ". Valid sections: security, system, ui, endpoints, metrics, mail, premium, processExecutor, autoPipeline"); + } + + int updatedCount = 0; + for (Map.Entry entry : sectionData.entrySet()) { + String key = sectionName + "." + entry.getKey(); + Object value = entry.getValue(); + + log.info("Admin updating section setting: {} = {}", key, value); + GeneralUtils.saveKeyToSettings(key, value); + updatedCount++; + } + + return ResponseEntity.ok( + String.format( + "Successfully updated %d setting(s) in section '%s'. Changes will take effect on application restart.", + updatedCount, sectionName)); + + } catch (IOException e) { + log.error("Failed to save section settings to file: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to save settings to configuration file: " + e.getMessage()); + } catch (Exception e) { + log.error("Unexpected error while updating section settings: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body("Invalid section data. Error: " + e.getMessage()); + } + } + + @GetMapping("/key/{key}") + @Operation( + summary = "Get specific setting value", + description = + "Retrieve value for a specific setting key using dot notation. Admin access required.") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Setting value retrieved successfully"), + @ApiResponse(responseCode = "400", description = "Invalid setting key"), + @ApiResponse( + responseCode = "403", + description = "Access denied - Admin role required") + }) + public ResponseEntity getSettingValue(@PathVariable String key) { + try { + Object value = getSettingByKey(key); + if (value == null) { + return ResponseEntity.badRequest().body("Setting key not found: " + key); + } + log.debug("Admin requested setting: {}", key); + return ResponseEntity.ok(new SettingValueResponse(key, value)); + } catch (Exception e) { + log.error("Error retrieving setting {}: {}", key, e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to retrieve setting: " + e.getMessage()); + } + } + + @PutMapping("/key/{key}") + @Operation( + summary = "Update specific setting value", + description = + "Update value for a specific setting key using dot notation. Admin access required.") + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "Setting updated successfully"), + @ApiResponse(responseCode = "400", description = "Invalid setting key or value"), + @ApiResponse( + responseCode = "403", + description = "Access denied - Admin role required"), + @ApiResponse(responseCode = "500", description = "Failed to save setting") + }) + public ResponseEntity updateSettingValue( + @PathVariable String key, @RequestBody UpdateSettingValueRequest request) { + try { + if (request.getValue() == null) { + return ResponseEntity.badRequest().body("Request body must contain 'value' field"); + } + + Object value = request.getValue(); + log.info("Admin updating single setting: {} = {}", key, value); + GeneralUtils.saveKeyToSettings(key, value); + + return ResponseEntity.ok( + String.format( + "Successfully updated setting '%s'. Changes will take effect on application restart.", + key)); + + } catch (IOException e) { + log.error("Failed to save setting to file: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to save setting to configuration file: " + e.getMessage()); + } catch (Exception e) { + log.error("Unexpected error while updating setting: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body("Invalid setting key or value. Error: " + e.getMessage()); + } + } + + private Object getSectionData(String sectionName) { + return switch (sectionName.toLowerCase()) { + case "security" -> applicationProperties.getSecurity(); + case "system" -> applicationProperties.getSystem(); + case "ui" -> applicationProperties.getUi(); + case "endpoints" -> applicationProperties.getEndpoints(); + case "metrics" -> applicationProperties.getMetrics(); + case "mail" -> applicationProperties.getMail(); + case "premium" -> applicationProperties.getPremium(); + case "processexecutor" -> applicationProperties.getProcessExecutor(); + case "autopipeline" -> applicationProperties.getAutoPipeline(); + case "legal" -> applicationProperties.getLegal(); + default -> null; + }; + } + + private boolean isValidSectionName(String sectionName) { + return getSectionData(sectionName) != null; + } + + private Object getSettingByKey(String key) { + String[] parts = key.split("\\.", 2); + if (parts.length < 2) { + return null; + } + + String sectionName = parts[0]; + String propertyPath = parts[1]; + Object section = getSectionData(sectionName); + + if (section == null) { + return null; + } + + try { + return getNestedProperty(section, propertyPath); + } catch (Exception e) { + log.warn("Failed to get nested property {}: {}", key, e.getMessage()); + return null; + } + } + + private Object getNestedProperty(Object obj, String propertyPath) throws Exception { + if (obj == null) { + return null; + } + + String[] parts = propertyPath.split("\\.", 2); + String currentProperty = parts[0]; + + java.lang.reflect.Field field = obj.getClass().getDeclaredField(currentProperty); + field.setAccessible(true); + Object value = field.get(obj); + + if (parts.length == 1) { + return value; + } else { + return getNestedProperty(value, parts[1]); + } + } +} diff --git a/proprietary/src/main/java/stirling/software/proprietary/security/model/api/admin/SettingValueResponse.java b/proprietary/src/main/java/stirling/software/proprietary/security/model/api/admin/SettingValueResponse.java new file mode 100644 index 000000000..f13c5ffba --- /dev/null +++ b/proprietary/src/main/java/stirling/software/proprietary/security/model/api/admin/SettingValueResponse.java @@ -0,0 +1,20 @@ +package stirling.software.proprietary.security.model.api.admin; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Response object containing a setting key and its value") +public class SettingValueResponse { + + @Schema(description = "The setting key in dot notation", example = "system.enableAnalytics") + private String key; + + @Schema(description = "The current value of the setting", example = "true") + private Object value; +} diff --git a/proprietary/src/main/java/stirling/software/proprietary/security/model/api/admin/UpdateSettingValueRequest.java b/proprietary/src/main/java/stirling/software/proprietary/security/model/api/admin/UpdateSettingValueRequest.java new file mode 100644 index 000000000..a49171989 --- /dev/null +++ b/proprietary/src/main/java/stirling/software/proprietary/security/model/api/admin/UpdateSettingValueRequest.java @@ -0,0 +1,13 @@ +package stirling.software.proprietary.security.model.api.admin; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +@Data +@Schema(description = "Request object for updating a single setting value") +public class UpdateSettingValueRequest { + + @Schema(description = "The new value for the setting", example = "true") + private Object value; +} diff --git a/proprietary/src/main/java/stirling/software/proprietary/security/model/api/admin/UpdateSettingsRequest.java b/proprietary/src/main/java/stirling/software/proprietary/security/model/api/admin/UpdateSettingsRequest.java new file mode 100644 index 000000000..85616f0b3 --- /dev/null +++ b/proprietary/src/main/java/stirling/software/proprietary/security/model/api/admin/UpdateSettingsRequest.java @@ -0,0 +1,25 @@ +package stirling.software.proprietary.security.model.api.admin; + +import java.util.Map; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +@Data +@Schema( + description = + "Request object for delta updates to application settings. Only include the settings you want to change. Uses dot notation for nested properties (e.g., 'system.enableAnalytics', 'ui.appName')") +public class UpdateSettingsRequest { + + @Schema( + description = + "Map of setting keys to their new values. Only include changed settings (delta updates). Keys use dot notation for nested properties.", + example = + "{\n" + + " \"system.enableAnalytics\": true,\n" + + " \"ui.appName\": \"My Custom PDF Tool\",\n" + + " \"security.enableLogin\": false\n" + + "}") + private Map settings; +}