feat: prepare sellable source delivery edition
This commit is contained in:
@@ -6,7 +6,6 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -15,34 +14,38 @@ public class ConfigurationValidator implements CommandLineRunner {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ConfigurationValidator.class);
|
||||
|
||||
@Value("${SSHMANAGER_ENCRYPTION_KEY:}")
|
||||
@Value("${sshmanager.encryption-key:}")
|
||||
private String encryptionKey;
|
||||
|
||||
@Value("${SSHMANAGER_JWT_SECRET:}")
|
||||
@Value("${sshmanager.jwt-secret:}")
|
||||
private String jwtSecret;
|
||||
|
||||
@Value("${DATA_DIR:/app/data}")
|
||||
private String dataDir;
|
||||
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
log.info("Data directory resolved to: {}", dataDir);
|
||||
Set<String> missingConfigs = new HashSet<>();
|
||||
|
||||
if (encryptionKey == null || encryptionKey.trim().isEmpty()) {
|
||||
missingConfigs.add("SSHMANAGER_ENCRYPTION_KEY");
|
||||
missingConfigs.add("sshmanager.encryption-key");
|
||||
}
|
||||
if (jwtSecret == null || jwtSecret.trim().isEmpty()) {
|
||||
missingConfigs.add("SSHMANAGER_JWT_SECRET");
|
||||
missingConfigs.add("sshmanager.jwt-secret");
|
||||
}
|
||||
|
||||
if (!missingConfigs.isEmpty()) {
|
||||
String missing = String.join(", ", missingConfigs);
|
||||
log.error("Missing required environment variables: {}", missing);
|
||||
log.error("Please set the following environment variables:");
|
||||
log.error("Missing required configuration values: {}", missing);
|
||||
log.error("Please provide them via Spring properties, environment variables or JVM -D arguments.");
|
||||
missingConfigs.forEach(key -> log.error(" - {} (required)", key));
|
||||
log.error("Application will not start without these configurations.");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
if ("ssh-manager-jwt-secret-change-in-production".equals(jwtSecret)) {
|
||||
log.error("Default JWT secret detected. Please set SSHMANAGER_JWT_SECRET to a secure random value.");
|
||||
log.error("Default JWT secret detected. Please set sshmanager.jwt-secret to a secure random value.");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,69 +1,140 @@
|
||||
package com.sshmanager.controller;
|
||||
|
||||
import com.sshmanager.dto.LoginRequest;
|
||||
import com.sshmanager.dto.LoginResponse;
|
||||
import com.sshmanager.entity.User;
|
||||
import com.sshmanager.repository.UserRepository;
|
||||
import com.sshmanager.security.JwtTokenProvider;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import com.sshmanager.dto.LoginRequest;
|
||||
import com.sshmanager.dto.LoginResponse;
|
||||
import com.sshmanager.dto.ChangePasswordRequest;
|
||||
import com.sshmanager.entity.User;
|
||||
import com.sshmanager.repository.UserRepository;
|
||||
import com.sshmanager.security.JwtTokenProvider;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
public class AuthController {
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final JwtTokenProvider tokenProvider;
|
||||
private final UserRepository userRepository;
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final JwtTokenProvider tokenProvider;
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public AuthController(AuthenticationManager authenticationManager,
|
||||
JwtTokenProvider tokenProvider,
|
||||
UserRepository userRepository,
|
||||
PasswordEncoder passwordEncoder) {
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.tokenProvider = tokenProvider;
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
public AuthController(AuthenticationManager authenticationManager,
|
||||
JwtTokenProvider tokenProvider,
|
||||
UserRepository userRepository) {
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.tokenProvider = tokenProvider;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
|
||||
try {
|
||||
Authentication authentication = authenticationManager.authenticate(
|
||||
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));
|
||||
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
String token = tokenProvider.generateToken(authentication);
|
||||
|
||||
User user = userRepository.findByUsername(request.getUsername()).orElseThrow(() -> new IllegalStateException("User not found"));
|
||||
LoginResponse response = new LoginResponse(token, user.getUsername(),
|
||||
user.getDisplayName() != null ? user.getDisplayName() : user.getUsername());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (BadCredentialsException e) {
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
String token = tokenProvider.generateToken(authentication);
|
||||
|
||||
User user = userRepository.findByUsername(request.getUsername()).orElseThrow(() -> new IllegalStateException("User not found"));
|
||||
LoginResponse response = new LoginResponse(token, user.getUsername(),
|
||||
user.getDisplayName() != null ? user.getDisplayName() : user.getUsername(),
|
||||
isPasswordChangeRequired(user));
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (BadCredentialsException e) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("message", "Invalid username or password");
|
||||
return ResponseEntity.status(401).body(error);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<?> me(Authentication authentication) {
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/health")
|
||||
public ResponseEntity<?> health() {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("app", "ssh-manager");
|
||||
data.put("status", "ok");
|
||||
data.put("timestamp", Instant.now().toEpochMilli());
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<?> me(Authentication authentication) {
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("error", "Unauthorized");
|
||||
return ResponseEntity.status(401).body(error);
|
||||
}
|
||||
User user = userRepository.findByUsername(authentication.getName()).orElseThrow(() -> new IllegalStateException("User not found"));
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("username", user.getUsername());
|
||||
data.put("displayName", user.getDisplayName());
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("username", user.getUsername());
|
||||
data.put("displayName", user.getDisplayName());
|
||||
data.put("passwordChangeRequired", isPasswordChangeRequired(user));
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@PostMapping("/change-password")
|
||||
public ResponseEntity<?> changePassword(@RequestBody ChangePasswordRequest request, Authentication authentication) {
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("error", "Unauthorized");
|
||||
return ResponseEntity.status(401).body(error);
|
||||
}
|
||||
|
||||
String currentPassword = request.getCurrentPassword() == null ? "" : request.getCurrentPassword().trim();
|
||||
String newPassword = request.getNewPassword() == null ? "" : request.getNewPassword().trim();
|
||||
|
||||
if (currentPassword.isEmpty() || newPassword.isEmpty()) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("message", "Current password and new password are required");
|
||||
return ResponseEntity.badRequest().body(error);
|
||||
}
|
||||
|
||||
if (newPassword.length() < 8) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("message", "New password must be at least 8 characters");
|
||||
return ResponseEntity.badRequest().body(error);
|
||||
}
|
||||
|
||||
User user = userRepository.findByUsername(authentication.getName()).orElseThrow(() -> new IllegalStateException("User not found"));
|
||||
if (!passwordEncoder.matches(currentPassword, user.getPasswordHash())) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("message", "Current password is incorrect");
|
||||
return ResponseEntity.badRequest().body(error);
|
||||
}
|
||||
|
||||
if (passwordEncoder.matches(newPassword, user.getPasswordHash())) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("message", "New password must be different from current password");
|
||||
return ResponseEntity.badRequest().body(error);
|
||||
}
|
||||
|
||||
user.setPasswordHash(passwordEncoder.encode(newPassword));
|
||||
user.setPasswordChangedAt(Instant.now());
|
||||
userRepository.save(user);
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("message", "Password updated");
|
||||
data.put("passwordChangeRequired", false);
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
private boolean isPasswordChangeRequired(User user) {
|
||||
return "admin".equals(user.getUsername()) && passwordEncoder.matches("admin123", user.getPasswordHash());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package com.sshmanager.controller;
|
||||
|
||||
import com.sshmanager.dto.ConnectionCreateRequest;
|
||||
import com.sshmanager.dto.ConnectionDto;
|
||||
import com.sshmanager.entity.Connection;
|
||||
import com.sshmanager.entity.User;
|
||||
import com.sshmanager.repository.UserRepository;
|
||||
import com.sshmanager.service.ConnectionService;
|
||||
import com.sshmanager.dto.ConnectionCreateRequest;
|
||||
import com.sshmanager.dto.ConnectionDto;
|
||||
import com.sshmanager.dto.BackupImportResponseDto;
|
||||
import com.sshmanager.dto.BackupPackageDto;
|
||||
import com.sshmanager.dto.BatchCommandRequest;
|
||||
import com.sshmanager.dto.BatchCommandResponseDto;
|
||||
import com.sshmanager.entity.Connection;
|
||||
import com.sshmanager.entity.User;
|
||||
import com.sshmanager.repository.UserRepository;
|
||||
import com.sshmanager.service.BackupService;
|
||||
import com.sshmanager.service.BatchCommandService;
|
||||
import com.sshmanager.service.ConnectionService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -18,14 +24,20 @@ import java.util.Map;
|
||||
@RequestMapping("/api/connections")
|
||||
public class ConnectionController {
|
||||
|
||||
private final ConnectionService connectionService;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public ConnectionController(ConnectionService connectionService,
|
||||
UserRepository userRepository) {
|
||||
this.connectionService = connectionService;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
private final ConnectionService connectionService;
|
||||
private final BackupService backupService;
|
||||
private final BatchCommandService batchCommandService;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public ConnectionController(ConnectionService connectionService,
|
||||
BackupService backupService,
|
||||
BatchCommandService batchCommandService,
|
||||
UserRepository userRepository) {
|
||||
this.connectionService = connectionService;
|
||||
this.backupService = backupService;
|
||||
this.batchCommandService = batchCommandService;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
private Long getCurrentUserId(Authentication auth) {
|
||||
User user = userRepository.findByUsername(auth.getName()).orElseThrow(() -> new IllegalStateException("User not found"));
|
||||
@@ -59,17 +71,37 @@ public class ConnectionController {
|
||||
return ResponseEntity.ok(connectionService.update(id, request, userId));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Map<String, String>> delete(@PathVariable Long id,
|
||||
Authentication authentication) {
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Map<String, String>> delete(@PathVariable Long id,
|
||||
Authentication authentication) {
|
||||
Long userId = getCurrentUserId(authentication);
|
||||
connectionService.delete(id, userId);
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("message", "Deleted");
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@PostMapping("/test")
|
||||
result.put("message", "Deleted");
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/backup/export")
|
||||
public ResponseEntity<BackupPackageDto> exportBackup(Authentication authentication) {
|
||||
Long userId = getCurrentUserId(authentication);
|
||||
return ResponseEntity.ok(backupService.exportBackup(userId));
|
||||
}
|
||||
|
||||
@PostMapping("/backup/import")
|
||||
public ResponseEntity<BackupImportResponseDto> importBackup(@RequestBody BackupPackageDto request,
|
||||
Authentication authentication) {
|
||||
Long userId = getCurrentUserId(authentication);
|
||||
return ResponseEntity.ok(backupService.importBackup(userId, request));
|
||||
}
|
||||
|
||||
@PostMapping("/batch-command")
|
||||
public ResponseEntity<BatchCommandResponseDto> executeBatchCommand(@RequestBody BatchCommandRequest request,
|
||||
Authentication authentication) {
|
||||
Long userId = getCurrentUserId(authentication);
|
||||
return ResponseEntity.ok(batchCommandService.execute(userId, request));
|
||||
}
|
||||
|
||||
@PostMapping("/test")
|
||||
public ResponseEntity<Map<String, Object>> connectivity(@RequestBody Connection connection,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.sshmanager.dto;
|
||||
|
||||
import com.sshmanager.entity.Connection;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class BackupConnectionDto {
|
||||
|
||||
private Long sourceId;
|
||||
private String name;
|
||||
private String host;
|
||||
private Integer port;
|
||||
private String username;
|
||||
private Connection.AuthType authType;
|
||||
private String password;
|
||||
private String privateKey;
|
||||
private String passphrase;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.sshmanager.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class BackupImportResponseDto {
|
||||
|
||||
private int importedConnections;
|
||||
private int importedTreeNodes;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.sshmanager.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class BackupPackageDto {
|
||||
|
||||
private Integer version = 1;
|
||||
private Instant exportedAt = Instant.now();
|
||||
private List<BackupConnectionDto> connections = new ArrayList<BackupConnectionDto>();
|
||||
private SessionTreeLayoutDto sessionTree = new SessionTreeLayoutDto();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sshmanager.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class BatchCommandRequest {
|
||||
|
||||
private List<Long> connectionIds = new ArrayList<Long>();
|
||||
private String command;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.sshmanager.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class BatchCommandResponseDto {
|
||||
|
||||
private String command;
|
||||
private int total;
|
||||
private int successCount;
|
||||
private int failureCount;
|
||||
private List<BatchCommandResultDto> results = new ArrayList<BatchCommandResultDto>();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.sshmanager.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class BatchCommandResultDto {
|
||||
|
||||
private Long connectionId;
|
||||
private String connectionName;
|
||||
private boolean success;
|
||||
private String output;
|
||||
private String error;
|
||||
private long durationMs;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sshmanager.dto;
|
||||
|
||||
public class ChangePasswordRequest {
|
||||
private String currentPassword;
|
||||
private String newPassword;
|
||||
|
||||
public String getCurrentPassword() {
|
||||
return currentPassword;
|
||||
}
|
||||
|
||||
public void setCurrentPassword(String currentPassword) {
|
||||
this.currentPassword = currentPassword;
|
||||
}
|
||||
|
||||
public String getNewPassword() {
|
||||
return newPassword;
|
||||
}
|
||||
|
||||
public void setNewPassword(String newPassword) {
|
||||
this.newPassword = newPassword;
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,9 @@ import lombok.NoArgsConstructor;
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoginResponse {
|
||||
private String token;
|
||||
private String username;
|
||||
private String displayName;
|
||||
}
|
||||
public class LoginResponse {
|
||||
private String token;
|
||||
private String username;
|
||||
private String displayName;
|
||||
private boolean passwordChangeRequired;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.sshmanager.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@@ -9,8 +8,13 @@ import java.util.List;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SessionTreeLayoutDto {
|
||||
|
||||
private List<SessionTreeNodeDto> nodes = new ArrayList<SessionTreeNodeDto>();
|
||||
private String sortMode = "manual";
|
||||
|
||||
public SessionTreeLayoutDto(List<SessionTreeNodeDto> nodes) {
|
||||
this.nodes = nodes;
|
||||
this.sortMode = "manual";
|
||||
}
|
||||
}
|
||||
|
||||
148
backend/src/main/java/com/sshmanager/service/BackupService.java
Normal file
148
backend/src/main/java/com/sshmanager/service/BackupService.java
Normal file
@@ -0,0 +1,148 @@
|
||||
package com.sshmanager.service;
|
||||
|
||||
import com.sshmanager.dto.BackupConnectionDto;
|
||||
import com.sshmanager.dto.BackupImportResponseDto;
|
||||
import com.sshmanager.dto.BackupPackageDto;
|
||||
import com.sshmanager.dto.ConnectionCreateRequest;
|
||||
import com.sshmanager.dto.ConnectionDto;
|
||||
import com.sshmanager.dto.SessionTreeLayoutDto;
|
||||
import com.sshmanager.dto.SessionTreeNodeDto;
|
||||
import com.sshmanager.entity.Connection;
|
||||
import com.sshmanager.repository.ConnectionRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class BackupService {
|
||||
|
||||
private final ConnectionRepository connectionRepository;
|
||||
private final ConnectionService connectionService;
|
||||
private final SessionTreeLayoutService sessionTreeLayoutService;
|
||||
|
||||
public BackupService(ConnectionRepository connectionRepository,
|
||||
ConnectionService connectionService,
|
||||
SessionTreeLayoutService sessionTreeLayoutService) {
|
||||
this.connectionRepository = connectionRepository;
|
||||
this.connectionService = connectionService;
|
||||
this.sessionTreeLayoutService = sessionTreeLayoutService;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public BackupPackageDto exportBackup(Long userId) {
|
||||
BackupPackageDto backup = new BackupPackageDto();
|
||||
backup.setExportedAt(Instant.now());
|
||||
|
||||
List<BackupConnectionDto> exportedConnections = new ArrayList<BackupConnectionDto>();
|
||||
for (Connection connection : connectionRepository.findByUserIdOrderByUpdatedAtDesc(userId)) {
|
||||
BackupConnectionDto item = new BackupConnectionDto();
|
||||
item.setSourceId(connection.getId());
|
||||
item.setName(connection.getName());
|
||||
item.setHost(connection.getHost());
|
||||
item.setPort(connection.getPort());
|
||||
item.setUsername(connection.getUsername());
|
||||
item.setAuthType(connection.getAuthType());
|
||||
item.setPassword(connectionService.getDecryptedPassword(connection));
|
||||
item.setPrivateKey(connectionService.getDecryptedPrivateKey(connection));
|
||||
item.setPassphrase(connectionService.getDecryptedPassphrase(connection));
|
||||
exportedConnections.add(item);
|
||||
}
|
||||
|
||||
backup.setConnections(exportedConnections);
|
||||
backup.setSessionTree(sessionTreeLayoutService.getLayout(userId));
|
||||
return backup;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public BackupImportResponseDto importBackup(Long userId, BackupPackageDto backupPackage) {
|
||||
if (backupPackage == null) {
|
||||
throw new IllegalArgumentException("Backup package is required");
|
||||
}
|
||||
|
||||
List<BackupConnectionDto> connections = backupPackage.getConnections() == null
|
||||
? new ArrayList<BackupConnectionDto>()
|
||||
: backupPackage.getConnections();
|
||||
SessionTreeLayoutDto sessionTree = backupPackage.getSessionTree() == null
|
||||
? new SessionTreeLayoutDto()
|
||||
: backupPackage.getSessionTree();
|
||||
|
||||
connectionRepository.deleteAll(connectionRepository.findByUserIdOrderByUpdatedAtDesc(userId));
|
||||
|
||||
Map<Long, Long> connectionIdMapping = new HashMap<Long, Long>();
|
||||
for (BackupConnectionDto item : connections) {
|
||||
ConnectionCreateRequest request = new ConnectionCreateRequest();
|
||||
request.setName(item.getName());
|
||||
request.setHost(item.getHost());
|
||||
request.setPort(item.getPort());
|
||||
request.setUsername(item.getUsername());
|
||||
request.setAuthType(item.getAuthType());
|
||||
request.setPassword(item.getPassword());
|
||||
request.setPrivateKey(item.getPrivateKey());
|
||||
request.setPassphrase(item.getPassphrase());
|
||||
|
||||
ConnectionDto created = connectionService.create(request, userId);
|
||||
if (item.getSourceId() != null) {
|
||||
connectionIdMapping.put(item.getSourceId(), created.getId());
|
||||
}
|
||||
}
|
||||
|
||||
SessionTreeLayoutDto remappedLayout = remapSessionTree(sessionTree, connectionIdMapping);
|
||||
sessionTreeLayoutService.saveLayout(userId, remappedLayout);
|
||||
|
||||
int nodeCount = remappedLayout.getNodes() == null ? 0 : remappedLayout.getNodes().size();
|
||||
return new BackupImportResponseDto(connections.size(), nodeCount);
|
||||
}
|
||||
|
||||
private SessionTreeLayoutDto remapSessionTree(SessionTreeLayoutDto source,
|
||||
Map<Long, Long> connectionIdMapping) {
|
||||
SessionTreeLayoutDto target = new SessionTreeLayoutDto();
|
||||
target.setSortMode(source.getSortMode());
|
||||
|
||||
List<SessionTreeNodeDto> remappedNodes = new ArrayList<SessionTreeNodeDto>();
|
||||
if (source.getNodes() != null) {
|
||||
for (SessionTreeNodeDto node : source.getNodes()) {
|
||||
if (node == null) {
|
||||
continue;
|
||||
}
|
||||
if ("connection".equals(node.getType())) {
|
||||
Long mappedConnectionId = connectionIdMapping.get(node.getConnectionId());
|
||||
if (mappedConnectionId == null) {
|
||||
continue;
|
||||
}
|
||||
remappedNodes.add(new SessionTreeNodeDto(
|
||||
node.getId(),
|
||||
node.getType(),
|
||||
node.getName(),
|
||||
node.getParentId(),
|
||||
node.getOrder(),
|
||||
mappedConnectionId,
|
||||
node.getExpanded(),
|
||||
node.getCreatedAt(),
|
||||
node.getUpdatedAt()
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
remappedNodes.add(new SessionTreeNodeDto(
|
||||
node.getId(),
|
||||
node.getType(),
|
||||
node.getName(),
|
||||
node.getParentId(),
|
||||
node.getOrder(),
|
||||
null,
|
||||
node.getExpanded(),
|
||||
node.getCreatedAt(),
|
||||
node.getUpdatedAt()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
target.setNodes(remappedNodes);
|
||||
return target;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.sshmanager.service;
|
||||
|
||||
import com.sshmanager.dto.BatchCommandRequest;
|
||||
import com.sshmanager.dto.BatchCommandResponseDto;
|
||||
import com.sshmanager.dto.BatchCommandResultDto;
|
||||
import com.sshmanager.entity.Connection;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class BatchCommandService {
|
||||
|
||||
private final ConnectionService connectionService;
|
||||
private final SshService sshService;
|
||||
|
||||
public BatchCommandService(ConnectionService connectionService,
|
||||
SshService sshService) {
|
||||
this.connectionService = connectionService;
|
||||
this.sshService = sshService;
|
||||
}
|
||||
|
||||
public BatchCommandResponseDto execute(Long userId, BatchCommandRequest request) {
|
||||
if (request == null || request.getConnectionIds() == null || request.getConnectionIds().isEmpty()) {
|
||||
throw new IllegalArgumentException("At least one connection is required");
|
||||
}
|
||||
|
||||
String command = request.getCommand() == null ? "" : request.getCommand().trim();
|
||||
if (command.isEmpty()) {
|
||||
throw new IllegalArgumentException("Command is required");
|
||||
}
|
||||
|
||||
List<BatchCommandResultDto> results = new ArrayList<BatchCommandResultDto>();
|
||||
int successCount = 0;
|
||||
int failureCount = 0;
|
||||
|
||||
for (Long connectionId : request.getConnectionIds()) {
|
||||
Connection connection = connectionService.getConnectionForSsh(connectionId, userId);
|
||||
long startedAt = System.currentTimeMillis();
|
||||
try {
|
||||
String output = sshService.executeCommand(
|
||||
connection,
|
||||
connectionService.getDecryptedPassword(connection),
|
||||
connectionService.getDecryptedPrivateKey(connection),
|
||||
connectionService.getDecryptedPassphrase(connection),
|
||||
command
|
||||
);
|
||||
long durationMs = System.currentTimeMillis() - startedAt;
|
||||
results.add(new BatchCommandResultDto(
|
||||
connection.getId(),
|
||||
connection.getName(),
|
||||
true,
|
||||
output,
|
||||
null,
|
||||
durationMs
|
||||
));
|
||||
successCount += 1;
|
||||
} catch (Exception error) {
|
||||
long durationMs = System.currentTimeMillis() - startedAt;
|
||||
results.add(new BatchCommandResultDto(
|
||||
connection.getId(),
|
||||
connection.getName(),
|
||||
false,
|
||||
null,
|
||||
error.getMessage(),
|
||||
durationMs
|
||||
));
|
||||
failureCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
BatchCommandResponseDto response = new BatchCommandResponseDto();
|
||||
response.setCommand(command);
|
||||
response.setTotal(results.size());
|
||||
response.setSuccessCount(successCount);
|
||||
response.setFailureCount(failureCount);
|
||||
response.setResults(results);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.sshmanager.dto.SessionTreeLayoutDto;
|
||||
import com.sshmanager.entity.SessionTreeLayout;
|
||||
import com.sshmanager.repository.SessionTreeLayoutRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -13,6 +15,8 @@ import java.util.ArrayList;
|
||||
@Service
|
||||
public class SessionTreeLayoutService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SessionTreeLayoutService.class);
|
||||
|
||||
private final SessionTreeLayoutRepository sessionTreeLayoutRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@@ -34,8 +38,10 @@ public class SessionTreeLayoutService {
|
||||
if (parsed.getNodes() == null) {
|
||||
parsed.setNodes(new ArrayList<>());
|
||||
}
|
||||
parsed.setSortMode(normalizeSortMode(parsed.getSortMode()));
|
||||
return parsed;
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to parse session tree layout for userId={}, returning empty layout", userId, e);
|
||||
return createEmptyLayout();
|
||||
}
|
||||
}
|
||||
@@ -46,11 +52,13 @@ public class SessionTreeLayoutService {
|
||||
if (payload.getNodes() == null) {
|
||||
payload.setNodes(new ArrayList<>());
|
||||
}
|
||||
payload.setSortMode(normalizeSortMode(payload.getSortMode()));
|
||||
|
||||
final String layoutJson;
|
||||
try {
|
||||
layoutJson = objectMapper.writeValueAsString(payload);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to serialize session tree layout for userId={}", userId, e);
|
||||
throw new RuntimeException("Failed to serialize session tree layout", e);
|
||||
}
|
||||
|
||||
@@ -66,4 +74,11 @@ public class SessionTreeLayoutService {
|
||||
private SessionTreeLayoutDto createEmptyLayout() {
|
||||
return new SessionTreeLayoutDto(new ArrayList<>());
|
||||
}
|
||||
|
||||
private String normalizeSortMode(String sortMode) {
|
||||
if ("nameAsc".equals(sortMode)) {
|
||||
return "nameAsc";
|
||||
}
|
||||
return "manual";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ spring:
|
||||
location: ${DATA_DIR:/app/data}/upload-temp # 使用容器数据目录,避免被解析为 Tomcat 工作目录
|
||||
file-size-threshold: 0 # 立即写入磁盘,不使用内存缓冲
|
||||
datasource:
|
||||
url: jdbc:h2:file:./data/sshmanager;DB_CLOSE_DELAY=-1
|
||||
url: jdbc:h2:file:${DATA_DIR:/app/data}/sshmanager;DB_CLOSE_DELAY=-1
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password:
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
package com.sshmanager.controller;
|
||||
|
||||
import com.sshmanager.dto.ChangePasswordRequest;
|
||||
import com.sshmanager.dto.LoginRequest;
|
||||
import com.sshmanager.dto.LoginResponse;
|
||||
import com.sshmanager.entity.User;
|
||||
import com.sshmanager.repository.UserRepository;
|
||||
import com.sshmanager.security.JwtTokenProvider;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AuthControllerTest {
|
||||
|
||||
@Mock
|
||||
private AuthenticationManager authenticationManager;
|
||||
|
||||
@Mock
|
||||
private JwtTokenProvider tokenProvider;
|
||||
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
|
||||
private PasswordEncoder passwordEncoder;
|
||||
private AuthController authController;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
passwordEncoder = new BCryptPasswordEncoder();
|
||||
authController = new AuthController(authenticationManager, tokenProvider, userRepository, passwordEncoder);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginMarksDefaultAdminPasswordAsRequiredToChange() {
|
||||
LoginRequest request = new LoginRequest();
|
||||
request.setUsername("admin");
|
||||
request.setPassword("admin123");
|
||||
|
||||
Authentication authentication = new UsernamePasswordAuthenticationToken("admin", "admin123");
|
||||
when(authenticationManager.authenticate(any(Authentication.class))).thenReturn(authentication);
|
||||
when(tokenProvider.generateToken(authentication)).thenReturn("token");
|
||||
|
||||
User user = new User();
|
||||
user.setUsername("admin");
|
||||
user.setPasswordHash(passwordEncoder.encode("admin123"));
|
||||
user.setDisplayName("Administrator");
|
||||
when(userRepository.findByUsername("admin")).thenReturn(Optional.of(user));
|
||||
|
||||
ResponseEntity<?> response = authController.login(request);
|
||||
assertEquals(200, response.getStatusCodeValue());
|
||||
LoginResponse body = (LoginResponse) response.getBody();
|
||||
assertNotNull(body);
|
||||
assertTrue(body.isPasswordChangeRequired());
|
||||
}
|
||||
|
||||
@Test
|
||||
void healthReturnsApplicationMarker() {
|
||||
ResponseEntity<?> response = authController.health();
|
||||
assertEquals(200, response.getStatusCodeValue());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> body = (Map<String, Object>) response.getBody();
|
||||
assertNotNull(body);
|
||||
assertEquals("ssh-manager", body.get("app"));
|
||||
assertEquals("ok", body.get("status"));
|
||||
assertTrue(((Number) body.get("timestamp")).longValue() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void changePasswordUpdatesHashAndTimestamp() {
|
||||
ChangePasswordRequest request = new ChangePasswordRequest();
|
||||
request.setCurrentPassword("admin123");
|
||||
request.setNewPassword("newPassword123");
|
||||
|
||||
Authentication authentication = mock(Authentication.class);
|
||||
when(authentication.isAuthenticated()).thenReturn(true);
|
||||
when(authentication.getName()).thenReturn("admin");
|
||||
|
||||
User user = new User();
|
||||
user.setUsername("admin");
|
||||
user.setPasswordHash(passwordEncoder.encode("admin123"));
|
||||
user.setPasswordChangedAt(Instant.now().minusSeconds(3600));
|
||||
when(userRepository.findByUsername("admin")).thenReturn(Optional.of(user));
|
||||
|
||||
ResponseEntity<?> response = authController.changePassword(request, authentication);
|
||||
assertEquals(200, response.getStatusCodeValue());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> body = (Map<String, Object>) response.getBody();
|
||||
assertNotNull(body);
|
||||
assertEquals("Password updated", body.get("message"));
|
||||
assertEquals(false, body.get("passwordChangeRequired"));
|
||||
assertTrue(passwordEncoder.matches("newPassword123", user.getPasswordHash()));
|
||||
verify(userRepository).save(user);
|
||||
}
|
||||
|
||||
@Test
|
||||
void changePasswordRejectsWrongCurrentPassword() {
|
||||
ChangePasswordRequest request = new ChangePasswordRequest();
|
||||
request.setCurrentPassword("wrong");
|
||||
request.setNewPassword("newPassword123");
|
||||
|
||||
Authentication authentication = mock(Authentication.class);
|
||||
when(authentication.isAuthenticated()).thenReturn(true);
|
||||
when(authentication.getName()).thenReturn("admin");
|
||||
|
||||
User user = new User();
|
||||
user.setUsername("admin");
|
||||
user.setPasswordHash(passwordEncoder.encode("admin123"));
|
||||
when(userRepository.findByUsername("admin")).thenReturn(Optional.of(user));
|
||||
|
||||
ResponseEntity<?> response = authController.changePassword(request, authentication);
|
||||
assertEquals(400, response.getStatusCodeValue());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> body = (Map<String, String>) response.getBody();
|
||||
assertNotNull(body);
|
||||
assertEquals("Current password is incorrect", body.get("message"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void meReturnsPasswordChangeRequiredFlag() {
|
||||
Authentication authentication = mock(Authentication.class);
|
||||
when(authentication.isAuthenticated()).thenReturn(true);
|
||||
when(authentication.getName()).thenReturn("admin");
|
||||
|
||||
User user = new User();
|
||||
user.setUsername("admin");
|
||||
user.setPasswordHash(passwordEncoder.encode("admin123"));
|
||||
user.setDisplayName("Administrator");
|
||||
when(userRepository.findByUsername("admin")).thenReturn(Optional.of(user));
|
||||
|
||||
ResponseEntity<?> response = authController.me(authentication);
|
||||
assertEquals(200, response.getStatusCodeValue());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> body = (Map<String, Object>) response.getBody();
|
||||
assertNotNull(body);
|
||||
assertTrue((Boolean) body.get("passwordChangeRequired"));
|
||||
assertFalse(((String) body.get("username")).isEmpty());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,13 @@ package com.sshmanager.controller;
|
||||
|
||||
import com.sshmanager.entity.Connection;
|
||||
import com.sshmanager.entity.User;
|
||||
import com.sshmanager.dto.BackupImportResponseDto;
|
||||
import com.sshmanager.dto.BackupPackageDto;
|
||||
import com.sshmanager.dto.BatchCommandRequest;
|
||||
import com.sshmanager.dto.BatchCommandResponseDto;
|
||||
import com.sshmanager.repository.UserRepository;
|
||||
import com.sshmanager.service.BackupService;
|
||||
import com.sshmanager.service.BatchCommandService;
|
||||
import com.sshmanager.service.ConnectionService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -35,6 +41,12 @@ class ConnectionControllerTest {
|
||||
@Mock
|
||||
private ConnectionService connectionService;
|
||||
|
||||
@Mock
|
||||
private BackupService backupService;
|
||||
|
||||
@Mock
|
||||
private BatchCommandService batchCommandService;
|
||||
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
|
||||
@@ -98,4 +110,43 @@ class ConnectionControllerTest {
|
||||
assertFalse((Boolean) body.get("success"));
|
||||
assertTrue(((String) body.get("message")).contains("Connection failed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportBackupUsesCurrentUserId() {
|
||||
BackupPackageDto expected = new BackupPackageDto();
|
||||
when(backupService.exportBackup(1L)).thenReturn(expected);
|
||||
|
||||
ResponseEntity<BackupPackageDto> response = connectionController.exportBackup(authentication);
|
||||
|
||||
assertEquals(200, response.getStatusCode().value());
|
||||
assertEquals(expected, response.getBody());
|
||||
verify(backupService).exportBackup(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void importBackupUsesCurrentUserId() {
|
||||
BackupPackageDto request = new BackupPackageDto();
|
||||
BackupImportResponseDto expected = new BackupImportResponseDto(2, 5);
|
||||
when(backupService.importBackup(1L, request)).thenReturn(expected);
|
||||
|
||||
ResponseEntity<BackupImportResponseDto> response = connectionController.importBackup(request, authentication);
|
||||
|
||||
assertEquals(200, response.getStatusCode().value());
|
||||
assertEquals(expected, response.getBody());
|
||||
verify(backupService).importBackup(1L, request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeBatchCommandUsesCurrentUserId() {
|
||||
BatchCommandRequest request = new BatchCommandRequest();
|
||||
BatchCommandResponseDto expected = new BatchCommandResponseDto();
|
||||
expected.setTotal(1);
|
||||
when(batchCommandService.execute(1L, request)).thenReturn(expected);
|
||||
|
||||
ResponseEntity<BatchCommandResponseDto> response = connectionController.executeBatchCommand(request, authentication);
|
||||
|
||||
assertEquals(200, response.getStatusCode().value());
|
||||
assertEquals(expected, response.getBody());
|
||||
verify(batchCommandService).execute(1L, request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.sshmanager.service;
|
||||
|
||||
import com.sshmanager.dto.BackupConnectionDto;
|
||||
import com.sshmanager.dto.BackupImportResponseDto;
|
||||
import com.sshmanager.dto.BackupPackageDto;
|
||||
import com.sshmanager.dto.ConnectionDto;
|
||||
import com.sshmanager.dto.SessionTreeLayoutDto;
|
||||
import com.sshmanager.dto.SessionTreeNodeDto;
|
||||
import com.sshmanager.entity.Connection;
|
||||
import com.sshmanager.repository.ConnectionRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class BackupServiceTest {
|
||||
|
||||
@Mock
|
||||
private ConnectionRepository connectionRepository;
|
||||
|
||||
@Mock
|
||||
private ConnectionService connectionService;
|
||||
|
||||
@Mock
|
||||
private SessionTreeLayoutService sessionTreeLayoutService;
|
||||
|
||||
@InjectMocks
|
||||
private BackupService backupService;
|
||||
|
||||
@Test
|
||||
void exportBackupIncludesConnectionsAndSessionTree() {
|
||||
Connection connection = new Connection();
|
||||
connection.setId(11L);
|
||||
connection.setName("prod");
|
||||
connection.setHost("127.0.0.1");
|
||||
connection.setPort(22);
|
||||
connection.setUsername("root");
|
||||
connection.setAuthType(Connection.AuthType.PASSWORD);
|
||||
|
||||
SessionTreeLayoutDto layout = new SessionTreeLayoutDto(Arrays.asList(
|
||||
new SessionTreeNodeDto("node-1", "connection", "prod", null, 0, 11L, null, 1L, 2L)
|
||||
));
|
||||
|
||||
when(connectionRepository.findByUserIdOrderByUpdatedAtDesc(1L)).thenReturn(Collections.singletonList(connection));
|
||||
when(connectionService.getDecryptedPassword(connection)).thenReturn("secret");
|
||||
when(sessionTreeLayoutService.getLayout(1L)).thenReturn(layout);
|
||||
|
||||
BackupPackageDto result = backupService.exportBackup(1L);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.getConnections().size());
|
||||
assertEquals(11L, result.getConnections().get(0).getSourceId().longValue());
|
||||
assertEquals("secret", result.getConnections().get(0).getPassword());
|
||||
assertEquals(layout, result.getSessionTree());
|
||||
}
|
||||
|
||||
@Test
|
||||
void importBackupReplacesConnectionsAndRemapsTreeIds() {
|
||||
BackupConnectionDto backupConnection = new BackupConnectionDto();
|
||||
backupConnection.setSourceId(7L);
|
||||
backupConnection.setName("prod");
|
||||
backupConnection.setHost("10.0.0.10");
|
||||
backupConnection.setPort(22);
|
||||
backupConnection.setUsername("root");
|
||||
backupConnection.setAuthType(Connection.AuthType.PASSWORD);
|
||||
backupConnection.setPassword("secret");
|
||||
|
||||
BackupPackageDto backupPackage = new BackupPackageDto();
|
||||
backupPackage.setConnections(Collections.singletonList(backupConnection));
|
||||
backupPackage.setSessionTree(new SessionTreeLayoutDto(Arrays.asList(
|
||||
new SessionTreeNodeDto("folder-1", "folder", "生产", null, 0, null, true, 1L, 2L),
|
||||
new SessionTreeNodeDto("conn-1", "connection", "prod", "folder-1", 1, 7L, null, 1L, 2L)
|
||||
)));
|
||||
|
||||
ConnectionDto created = new ConnectionDto();
|
||||
created.setId(101L);
|
||||
|
||||
when(connectionRepository.findByUserIdOrderByUpdatedAtDesc(1L)).thenReturn(Collections.emptyList());
|
||||
when(connectionService.create(any(), eq(1L))).thenReturn(created);
|
||||
|
||||
BackupImportResponseDto result = backupService.importBackup(1L, backupPackage);
|
||||
|
||||
assertEquals(1, result.getImportedConnections());
|
||||
assertEquals(2, result.getImportedTreeNodes());
|
||||
verify(connectionRepository).deleteAll(Collections.emptyList());
|
||||
|
||||
ArgumentCaptor<SessionTreeLayoutDto> captor = ArgumentCaptor.forClass(SessionTreeLayoutDto.class);
|
||||
verify(sessionTreeLayoutService).saveLayout(eq(1L), captor.capture());
|
||||
SessionTreeLayoutDto savedLayout = captor.getValue();
|
||||
assertEquals(2, savedLayout.getNodes().size());
|
||||
assertEquals(101L, savedLayout.getNodes().get(1).getConnectionId().longValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.sshmanager.service;
|
||||
|
||||
import com.sshmanager.dto.BatchCommandRequest;
|
||||
import com.sshmanager.dto.BatchCommandResponseDto;
|
||||
import com.sshmanager.entity.Connection;
|
||||
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 java.util.Arrays;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class BatchCommandServiceTest {
|
||||
|
||||
@Mock
|
||||
private ConnectionService connectionService;
|
||||
|
||||
@Mock
|
||||
private SshService sshService;
|
||||
|
||||
@InjectMocks
|
||||
private BatchCommandService batchCommandService;
|
||||
|
||||
@Test
|
||||
void executeAggregatesSuccessAndFailure() throws Exception {
|
||||
Connection successConnection = new Connection();
|
||||
successConnection.setId(1L);
|
||||
successConnection.setUserId(99L);
|
||||
successConnection.setName("prod");
|
||||
|
||||
Connection failedConnection = new Connection();
|
||||
failedConnection.setId(2L);
|
||||
failedConnection.setUserId(99L);
|
||||
failedConnection.setName("test");
|
||||
|
||||
BatchCommandRequest request = new BatchCommandRequest();
|
||||
request.setConnectionIds(Arrays.asList(1L, 2L));
|
||||
request.setCommand("uptime");
|
||||
|
||||
when(connectionService.getConnectionForSsh(1L, 99L)).thenReturn(successConnection);
|
||||
when(connectionService.getConnectionForSsh(2L, 99L)).thenReturn(failedConnection);
|
||||
when(sshService.executeCommand(eq(successConnection), eq(null), eq(null), eq(null), eq("uptime"))).thenReturn("ok");
|
||||
when(sshService.executeCommand(eq(failedConnection), eq(null), eq(null), eq(null), eq("uptime"))).thenThrow(new RuntimeException("boom"));
|
||||
|
||||
BatchCommandResponseDto response = batchCommandService.execute(99L, request);
|
||||
|
||||
assertEquals(2, response.getTotal());
|
||||
assertEquals(1, response.getSuccessCount());
|
||||
assertEquals(1, response.getFailureCount());
|
||||
assertEquals("prod", response.getResults().get(0).getConnectionName());
|
||||
assertEquals("ok", response.getResults().get(0).getOutput());
|
||||
assertEquals("boom", response.getResults().get(1).getError());
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeRejectsEmptyCommand() {
|
||||
BatchCommandRequest request = new BatchCommandRequest();
|
||||
request.setConnectionIds(Arrays.asList(1L));
|
||||
request.setCommand(" ");
|
||||
|
||||
IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> batchCommandService.execute(1L, request));
|
||||
assertEquals("Command is required", error.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ class SessionTreeLayoutServiceTest {
|
||||
SessionTreeLayoutDto result = sessionTreeLayoutService.getLayout(1L);
|
||||
|
||||
assertTrue(result.getNodes().isEmpty());
|
||||
assertEquals("manual", result.getSortMode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -52,6 +53,7 @@ class SessionTreeLayoutServiceTest {
|
||||
SessionTreeLayoutDto parsed = new SessionTreeLayoutDto(Arrays.asList(
|
||||
new SessionTreeNodeDto("n1", "folder", "我的连接", null, 0, null, true, 1L, 1L)
|
||||
));
|
||||
parsed.setSortMode(null);
|
||||
|
||||
when(sessionTreeLayoutRepository.findByUserId(1L)).thenReturn(Optional.of(saved));
|
||||
when(objectMapper.readValue(saved.getLayoutJson(), SessionTreeLayoutDto.class)).thenReturn(parsed);
|
||||
@@ -60,12 +62,14 @@ class SessionTreeLayoutServiceTest {
|
||||
|
||||
assertEquals(1, result.getNodes().size());
|
||||
assertEquals("n1", result.getNodes().get(0).getId());
|
||||
assertEquals("manual", result.getSortMode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveLayoutNormalizesNullNodes() throws Exception {
|
||||
SessionTreeLayoutDto request = new SessionTreeLayoutDto();
|
||||
request.setNodes(null);
|
||||
request.setSortMode("unknown");
|
||||
|
||||
when(sessionTreeLayoutRepository.findByUserId(1L)).thenReturn(Optional.empty());
|
||||
when(objectMapper.writeValueAsString(any(SessionTreeLayoutDto.class))).thenReturn("{\"nodes\":[]}");
|
||||
@@ -73,6 +77,7 @@ class SessionTreeLayoutServiceTest {
|
||||
SessionTreeLayoutDto result = sessionTreeLayoutService.saveLayout(1L, request);
|
||||
|
||||
assertTrue(result.getNodes().isEmpty());
|
||||
assertEquals("manual", result.getSortMode());
|
||||
ArgumentCaptor<SessionTreeLayout> captor = ArgumentCaptor.forClass(SessionTreeLayout.class);
|
||||
verify(sessionTreeLayoutRepository).save(captor.capture());
|
||||
assertEquals(1L, captor.getValue().getUserId().longValue());
|
||||
@@ -87,5 +92,6 @@ class SessionTreeLayoutServiceTest {
|
||||
SessionTreeLayoutDto result = sessionTreeLayoutService.saveLayout(1L, null);
|
||||
|
||||
assertEquals(Collections.emptyList(), result.getNodes());
|
||||
assertEquals("manual", result.getSortMode());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user