settings api

This commit is contained in:
Anthony Stirling 2025-07-02 21:23:56 +01:00
parent a95743286e
commit 5b4ae5590d
4 changed files with 390 additions and 0 deletions

View File

@ -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<ApplicationProperties> 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<String> updateSettings(@RequestBody UpdateSettingsRequest request) {
try {
Map<String, Object> settings = request.getSettings();
if (settings == null || settings.isEmpty()) {
return ResponseEntity.badRequest().body("No settings provided to update");
}
int updatedCount = 0;
for (Map.Entry<String, Object> 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<String> updateSettingsSection(
@PathVariable String sectionName, @RequestBody Map<String, Object> 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<String, Object> 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<String> 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]);
}
}
}

View File

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

View File

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

View File

@ -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<String, Object> settings;
}