Improve SFTP reliability and credential hygiene with regression tests
This commit is contained in:
@@ -24,7 +24,7 @@ cd backend
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
后端运行在 http://localhost:8080
|
||||
后端运行在 http://localhost:48080
|
||||
|
||||
默认登录:`admin` / `admin123`
|
||||
|
||||
|
||||
@@ -9,12 +9,13 @@ import com.sshmanager.service.ConnectionService;
|
||||
import com.sshmanager.service.SftpService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -57,12 +58,27 @@ public class SftpController {
|
||||
return userId + ":" + connectionId;
|
||||
}
|
||||
|
||||
private <T> T withSessionLock(String key, Supplier<T> action) {
|
||||
Object lock = sessionLocks.computeIfAbsent(key, k -> new Object());
|
||||
synchronized (lock) {
|
||||
return action.get();
|
||||
}
|
||||
}
|
||||
private <T> T withSessionLock(String key, Supplier<T> action) {
|
||||
Object lock = sessionLocks.computeIfAbsent(key, k -> new Object());
|
||||
synchronized (lock) {
|
||||
return action.get();
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T withTwoSessionLocks(String keyA, String keyB, Supplier<T> action) {
|
||||
if (keyA.equals(keyB)) {
|
||||
return withSessionLock(keyA, action);
|
||||
}
|
||||
String first = keyA.compareTo(keyB) < 0 ? keyA : keyB;
|
||||
String second = keyA.compareTo(keyB) < 0 ? keyB : keyA;
|
||||
Object firstLock = sessionLocks.computeIfAbsent(first, k -> new Object());
|
||||
Object secondLock = sessionLocks.computeIfAbsent(second, k -> new Object());
|
||||
synchronized (firstLock) {
|
||||
synchronized (secondLock) {
|
||||
return action.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SftpService.SftpSession getOrCreateSession(Long connectionId, Long userId) throws Exception {
|
||||
String key = sessionKey(userId, connectionId);
|
||||
@@ -160,35 +176,38 @@ public class SftpController {
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/download")
|
||||
public ResponseEntity<byte[]> download(
|
||||
@RequestParam Long connectionId,
|
||||
@RequestParam String path,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(authentication);
|
||||
String key = sessionKey(userId, connectionId);
|
||||
return withSessionLock(key, () -> {
|
||||
try {
|
||||
SftpService.SftpSession session = getOrCreateSession(connectionId, userId);
|
||||
byte[] data = sftpService.download(session, path);
|
||||
String filename = path.contains("/") ? path.substring(path.lastIndexOf('/') + 1) : path;
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.body(data);
|
||||
} catch (Exception e) {
|
||||
SftpService.SftpSession existing = sessions.remove(key);
|
||||
if (existing != null) {
|
||||
existing.disconnect();
|
||||
}
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.status(500).build();
|
||||
}
|
||||
}
|
||||
@GetMapping("/download")
|
||||
public ResponseEntity<StreamingResponseBody> download(
|
||||
@RequestParam Long connectionId,
|
||||
@RequestParam String path,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(authentication);
|
||||
String key = sessionKey(userId, connectionId);
|
||||
String filename = path.contains("/") ? path.substring(path.lastIndexOf('/') + 1) : path;
|
||||
StreamingResponseBody stream = outputStream -> withSessionLock(key, () -> {
|
||||
try {
|
||||
SftpService.SftpSession session = getOrCreateSession(connectionId, userId);
|
||||
sftpService.download(session, path, outputStream);
|
||||
outputStream.flush();
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
SftpService.SftpSession existing = sessions.remove(key);
|
||||
if (existing != null) {
|
||||
existing.disconnect();
|
||||
}
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.body(stream);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.status(500).build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/upload")
|
||||
public ResponseEntity<Map<String, String>> upload(
|
||||
@@ -202,13 +221,15 @@ public class SftpController {
|
||||
return withSessionLock(key, () -> {
|
||||
try {
|
||||
SftpService.SftpSession session = getOrCreateSession(connectionId, userId);
|
||||
String remotePath = (path == null || path.isEmpty() || path.equals("/"))
|
||||
? "/" + file.getOriginalFilename()
|
||||
: (path.endsWith("/") ? path + file.getOriginalFilename() : path + "/" + file.getOriginalFilename());
|
||||
sftpService.upload(session, remotePath, file.getBytes());
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("message", "Uploaded");
|
||||
return ResponseEntity.ok(result);
|
||||
String remotePath = (path == null || path.isEmpty() || path.equals("/"))
|
||||
? "/" + file.getOriginalFilename()
|
||||
: (path.endsWith("/") ? path + file.getOriginalFilename() : path + "/" + file.getOriginalFilename());
|
||||
try (java.io.InputStream in = file.getInputStream()) {
|
||||
sftpService.upload(session, remotePath, in);
|
||||
}
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("message", "Uploaded");
|
||||
return ResponseEntity.ok(result);
|
||||
} catch (Exception e) {
|
||||
SftpService.SftpSession existing = sessions.remove(key);
|
||||
if (existing != null) {
|
||||
@@ -317,36 +338,55 @@ public class SftpController {
|
||||
}
|
||||
|
||||
@PostMapping("/transfer-remote")
|
||||
public ResponseEntity<Map<String, String>> transferRemote(
|
||||
@RequestParam Long sourceConnectionId,
|
||||
@RequestParam String sourcePath,
|
||||
@RequestParam Long targetConnectionId,
|
||||
@RequestParam String targetPath,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(authentication);
|
||||
public ResponseEntity<Map<String, String>> transferRemote(
|
||||
@RequestParam Long sourceConnectionId,
|
||||
@RequestParam String sourcePath,
|
||||
@RequestParam Long targetConnectionId,
|
||||
@RequestParam String targetPath,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(authentication);
|
||||
if (sourcePath == null || sourcePath.trim().isEmpty()) {
|
||||
Map<String, String> err = new HashMap<>();
|
||||
err.put("error", "sourcePath is required");
|
||||
return ResponseEntity.badRequest().body(err);
|
||||
}
|
||||
if (targetPath == null || targetPath.trim().isEmpty()) {
|
||||
Map<String, String> err = new HashMap<>();
|
||||
err.put("error", "targetPath is required");
|
||||
return ResponseEntity.badRequest().body(err);
|
||||
}
|
||||
SftpService.SftpSession sourceSession = getOrCreateSession(sourceConnectionId, userId);
|
||||
SftpService.SftpSession targetSession = getOrCreateSession(targetConnectionId, userId);
|
||||
if (sourceConnectionId.equals(targetConnectionId)) {
|
||||
sftpService.rename(sourceSession, sourcePath.trim(), targetPath.trim());
|
||||
} else {
|
||||
sftpService.transferRemote(sourceSession, sourcePath.trim(), targetSession, targetPath.trim());
|
||||
}
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("message", "Transferred");
|
||||
return ResponseEntity.ok(result);
|
||||
} catch (Exception e) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
if (targetPath == null || targetPath.trim().isEmpty()) {
|
||||
Map<String, String> err = new HashMap<>();
|
||||
err.put("error", "targetPath is required");
|
||||
return ResponseEntity.badRequest().body(err);
|
||||
}
|
||||
String sourceKey = sessionKey(userId, sourceConnectionId);
|
||||
String targetKey = sessionKey(userId, targetConnectionId);
|
||||
withTwoSessionLocks(sourceKey, targetKey, () -> {
|
||||
try {
|
||||
SftpService.SftpSession sourceSession = getOrCreateSession(sourceConnectionId, userId);
|
||||
SftpService.SftpSession targetSession = getOrCreateSession(targetConnectionId, userId);
|
||||
if (sourceConnectionId.equals(targetConnectionId)) {
|
||||
sftpService.rename(sourceSession, sourcePath.trim(), targetPath.trim());
|
||||
} else {
|
||||
sftpService.transferRemote(sourceSession, sourcePath.trim(), targetSession, targetPath.trim());
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
SftpService.SftpSession source = sessions.remove(sourceKey);
|
||||
if (source != null) {
|
||||
source.disconnect();
|
||||
}
|
||||
if (!sourceKey.equals(targetKey)) {
|
||||
SftpService.SftpSession target = sessions.remove(targetKey);
|
||||
if (target != null) {
|
||||
target.disconnect();
|
||||
}
|
||||
}
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("message", "Transferred");
|
||||
return ResponseEntity.ok(result);
|
||||
} catch (Exception e) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("error", e.getMessage() != null ? e.getMessage() : "Transfer failed");
|
||||
return ResponseEntity.status(500).body(error);
|
||||
}
|
||||
|
||||
@@ -45,17 +45,18 @@ public class ConnectionService {
|
||||
conn.setName(request.getName());
|
||||
conn.setHost(request.getHost());
|
||||
conn.setPort(request.getPort() != null ? request.getPort() : 22);
|
||||
conn.setUsername(request.getUsername());
|
||||
conn.setAuthType(request.getAuthType() != null ? request.getAuthType() : Connection.AuthType.PASSWORD);
|
||||
|
||||
if (conn.getAuthType() == Connection.AuthType.PASSWORD && request.getPassword() != null) {
|
||||
conn.setEncryptedPassword(encryptionService.encrypt(request.getPassword()));
|
||||
} else if (conn.getAuthType() == Connection.AuthType.PRIVATE_KEY && request.getPrivateKey() != null) {
|
||||
conn.setEncryptedPrivateKey(encryptionService.encrypt(request.getPrivateKey()));
|
||||
if (request.getPassphrase() != null && !request.getPassphrase().isEmpty()) {
|
||||
conn.setPassphrase(encryptionService.encrypt(request.getPassphrase()));
|
||||
}
|
||||
}
|
||||
conn.setUsername(request.getUsername());
|
||||
conn.setAuthType(request.getAuthType() != null ? request.getAuthType() : Connection.AuthType.PASSWORD);
|
||||
|
||||
if (conn.getAuthType() == Connection.AuthType.PASSWORD) {
|
||||
conn.setEncryptedPassword(encryptionService.encrypt(request.getPassword()));
|
||||
conn.setEncryptedPrivateKey(null);
|
||||
conn.setPassphrase(null);
|
||||
} else {
|
||||
conn.setEncryptedPassword(null);
|
||||
conn.setEncryptedPrivateKey(encryptionService.encrypt(request.getPrivateKey()));
|
||||
conn.setPassphrase(encryptionService.encrypt(request.getPassphrase()));
|
||||
}
|
||||
|
||||
conn = connectionRepository.save(conn);
|
||||
return ConnectionDto.fromEntity(conn);
|
||||
@@ -69,22 +70,27 @@ public class ConnectionService {
|
||||
throw new RuntimeException("Access denied");
|
||||
}
|
||||
|
||||
if (request.getName() != null) conn.setName(request.getName());
|
||||
if (request.getHost() != null) conn.setHost(request.getHost());
|
||||
if (request.getPort() != null) conn.setPort(request.getPort());
|
||||
if (request.getUsername() != null) conn.setUsername(request.getUsername());
|
||||
if (request.getAuthType() != null) conn.setAuthType(request.getAuthType());
|
||||
|
||||
if (request.getPassword() != null) {
|
||||
conn.setEncryptedPassword(encryptionService.encrypt(request.getPassword()));
|
||||
}
|
||||
if (request.getPrivateKey() != null) {
|
||||
conn.setEncryptedPrivateKey(encryptionService.encrypt(request.getPrivateKey()));
|
||||
}
|
||||
if (request.getPassphrase() != null) {
|
||||
conn.setPassphrase(request.getPassphrase().isEmpty() ? null :
|
||||
encryptionService.encrypt(request.getPassphrase()));
|
||||
}
|
||||
if (request.getName() != null) conn.setName(request.getName());
|
||||
if (request.getHost() != null) conn.setHost(request.getHost());
|
||||
if (request.getPort() != null) conn.setPort(request.getPort());
|
||||
if (request.getUsername() != null) conn.setUsername(request.getUsername());
|
||||
if (request.getAuthType() != null) conn.setAuthType(request.getAuthType());
|
||||
|
||||
if (conn.getAuthType() == Connection.AuthType.PASSWORD) {
|
||||
if (request.getPassword() != null) {
|
||||
conn.setEncryptedPassword(encryptionService.encrypt(request.getPassword()));
|
||||
}
|
||||
conn.setEncryptedPrivateKey(null);
|
||||
conn.setPassphrase(null);
|
||||
} else {
|
||||
if (request.getPrivateKey() != null) {
|
||||
conn.setEncryptedPrivateKey(encryptionService.encrypt(request.getPrivateKey()));
|
||||
}
|
||||
if (request.getPassphrase() != null) {
|
||||
conn.setPassphrase(encryptionService.encrypt(request.getPassphrase()));
|
||||
}
|
||||
conn.setEncryptedPassword(null);
|
||||
}
|
||||
|
||||
conn.setUpdatedAt(Instant.now());
|
||||
conn = connectionRepository.save(conn);
|
||||
|
||||
@@ -4,15 +4,14 @@ import com.jcraft.jsch.ChannelSftp;
|
||||
import com.jcraft.jsch.JSch;
|
||||
import com.jcraft.jsch.Session;
|
||||
import com.jcraft.jsch.SftpException;
|
||||
import com.sshmanager.entity.Connection;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.PipedInputStream;
|
||||
import java.io.PipedOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import com.sshmanager.entity.Connection;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PipedInputStream;
|
||||
import java.io.PipedOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
@@ -149,15 +148,13 @@ public class SftpService {
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] download(SftpSession sftpSession, String remotePath) throws Exception {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
sftpSession.getChannel().get(remotePath, out);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
public void upload(SftpSession sftpSession, String remotePath, byte[] data) throws Exception {
|
||||
sftpSession.getChannel().put(new ByteArrayInputStream(data), remotePath);
|
||||
}
|
||||
public void download(SftpSession sftpSession, String remotePath, OutputStream out) throws Exception {
|
||||
sftpSession.getChannel().get(remotePath, out);
|
||||
}
|
||||
|
||||
public void upload(SftpSession sftpSession, String remotePath, InputStream in) throws Exception {
|
||||
sftpSession.getChannel().put(in, remotePath);
|
||||
}
|
||||
|
||||
public void delete(SftpSession sftpSession, String remotePath, boolean isDir) throws Exception {
|
||||
if (isDir) {
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.sshmanager.controller;
|
||||
|
||||
import com.jcraft.jsch.ChannelSftp;
|
||||
import com.jcraft.jsch.Session;
|
||||
import com.sshmanager.entity.Connection;
|
||||
import com.sshmanager.entity.User;
|
||||
import com.sshmanager.repository.UserRepository;
|
||||
import com.sshmanager.service.ConnectionService;
|
||||
import com.sshmanager.service.SftpService;
|
||||
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.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SftpControllerTest {
|
||||
|
||||
@Mock
|
||||
private ConnectionService connectionService;
|
||||
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Mock
|
||||
private SftpService sftpService;
|
||||
|
||||
@InjectMocks
|
||||
private SftpController sftpController;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
User user = new User();
|
||||
user.setId(1L);
|
||||
user.setUsername("alice");
|
||||
when(userRepository.findByUsername("alice")).thenReturn(Optional.of(user));
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferRemoteReturnsBadRequestWhenSourcePathMissing() {
|
||||
ResponseEntity<Map<String, String>> response = sftpController.transferRemote(
|
||||
1L,
|
||||
" ",
|
||||
2L,
|
||||
"/tmp/target.txt",
|
||||
authentication()
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
assertEquals("sourcePath is required", response.getBody().get("error"));
|
||||
verifyNoInteractions(sftpService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferRemoteUsesRenameWhenSourceAndTargetConnectionAreSame() throws Exception {
|
||||
when(connectionService.getConnectionForSsh(anyLong(), eq(1L))).thenReturn(new Connection());
|
||||
SftpService.SftpSession session = connectedSession(true);
|
||||
when(sftpService.connect(any(Connection.class), any(), any(), any())).thenReturn(session);
|
||||
|
||||
ResponseEntity<Map<String, String>> response = sftpController.transferRemote(
|
||||
3L,
|
||||
"/src/file.txt",
|
||||
3L,
|
||||
"/dst/file.txt",
|
||||
authentication()
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertEquals("Transferred", response.getBody().get("message"));
|
||||
verify(sftpService).rename(session, "/src/file.txt", "/dst/file.txt");
|
||||
verify(sftpService, never()).transferRemote(any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferRemoteUsesCrossSessionTransferWhenConnectionsDiffer() throws Exception {
|
||||
when(connectionService.getConnectionForSsh(anyLong(), eq(1L))).thenReturn(new Connection());
|
||||
SftpService.SftpSession sourceSession = connectedSession(false);
|
||||
SftpService.SftpSession targetSession = connectedSession(false);
|
||||
when(sftpService.connect(any(Connection.class), any(), any(), any()))
|
||||
.thenReturn(sourceSession)
|
||||
.thenReturn(targetSession);
|
||||
|
||||
ResponseEntity<Map<String, String>> response = sftpController.transferRemote(
|
||||
10L,
|
||||
"/src/file.txt",
|
||||
20L,
|
||||
"/dst/file.txt",
|
||||
authentication()
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertEquals("Transferred", response.getBody().get("message"));
|
||||
verify(sftpService).transferRemote(sourceSession, "/src/file.txt", targetSession, "/dst/file.txt");
|
||||
verify(sftpService, never()).rename(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferRemoteReturnsServerErrorWhenTransferFails() throws Exception {
|
||||
when(connectionService.getConnectionForSsh(anyLong(), eq(1L))).thenReturn(new Connection());
|
||||
SftpService.SftpSession session = connectedSession(true);
|
||||
when(sftpService.connect(any(Connection.class), any(), any(), any())).thenReturn(session);
|
||||
doThrow(new RuntimeException("boom")).when(sftpService).rename(any(), any(), any());
|
||||
|
||||
ResponseEntity<Map<String, String>> response = sftpController.transferRemote(
|
||||
3L,
|
||||
"/src/file.txt",
|
||||
3L,
|
||||
"/dst/file.txt",
|
||||
authentication()
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());
|
||||
assertTrue(response.getBody().get("error").contains("boom"));
|
||||
}
|
||||
|
||||
private Authentication authentication() {
|
||||
Authentication authentication = mock(Authentication.class);
|
||||
when(authentication.getName()).thenReturn("alice");
|
||||
return authentication;
|
||||
}
|
||||
|
||||
private SftpService.SftpSession connectedSession(boolean connected) {
|
||||
Session session = mock(Session.class);
|
||||
ChannelSftp channel = mock(ChannelSftp.class);
|
||||
if (connected) {
|
||||
when(channel.isConnected()).thenReturn(true);
|
||||
}
|
||||
return new SftpService.SftpSession(session, channel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.sshmanager.service;
|
||||
|
||||
import com.sshmanager.dto.ConnectionCreateRequest;
|
||||
import com.sshmanager.dto.ConnectionDto;
|
||||
import com.sshmanager.entity.Connection;
|
||||
import com.sshmanager.repository.ConnectionRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ConnectionServiceTest {
|
||||
|
||||
@Mock
|
||||
private ConnectionRepository connectionRepository;
|
||||
|
||||
@Mock
|
||||
private EncryptionService encryptionService;
|
||||
|
||||
@InjectMocks
|
||||
private ConnectionService connectionService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(connectionRepository.save(any(Connection.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createPasswordConnectionClearsPrivateKeyCredentials() {
|
||||
ConnectionCreateRequest request = new ConnectionCreateRequest();
|
||||
request.setName("prod");
|
||||
request.setHost("127.0.0.1");
|
||||
request.setUsername("root");
|
||||
request.setAuthType(Connection.AuthType.PASSWORD);
|
||||
request.setPassword("secret");
|
||||
request.setPrivateKey("unused-key");
|
||||
request.setPassphrase("unused-passphrase");
|
||||
|
||||
when(encryptionService.encrypt("secret")).thenReturn("enc-secret");
|
||||
|
||||
ConnectionDto result = connectionService.create(request, 1L);
|
||||
|
||||
assertNotNull(result);
|
||||
ArgumentCaptor<Connection> captor = ArgumentCaptor.forClass(Connection.class);
|
||||
verify(connectionRepository).save(captor.capture());
|
||||
Connection saved = captor.getValue();
|
||||
|
||||
assertEquals(Connection.AuthType.PASSWORD, saved.getAuthType());
|
||||
assertEquals("enc-secret", saved.getEncryptedPassword());
|
||||
assertNull(saved.getEncryptedPrivateKey());
|
||||
assertNull(saved.getPassphrase());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createPrivateKeyConnectionClearsPasswordCredential() {
|
||||
ConnectionCreateRequest request = new ConnectionCreateRequest();
|
||||
request.setName("prod");
|
||||
request.setHost("127.0.0.1");
|
||||
request.setUsername("root");
|
||||
request.setAuthType(Connection.AuthType.PRIVATE_KEY);
|
||||
request.setPassword("unused-password");
|
||||
request.setPrivateKey("private-key");
|
||||
request.setPassphrase("passphrase");
|
||||
|
||||
when(encryptionService.encrypt("private-key")).thenReturn("enc-key");
|
||||
when(encryptionService.encrypt("passphrase")).thenReturn("enc-passphrase");
|
||||
|
||||
ConnectionDto result = connectionService.create(request, 1L);
|
||||
|
||||
assertNotNull(result);
|
||||
ArgumentCaptor<Connection> captor = ArgumentCaptor.forClass(Connection.class);
|
||||
verify(connectionRepository).save(captor.capture());
|
||||
Connection saved = captor.getValue();
|
||||
|
||||
assertEquals(Connection.AuthType.PRIVATE_KEY, saved.getAuthType());
|
||||
assertEquals("enc-key", saved.getEncryptedPrivateKey());
|
||||
assertEquals("enc-passphrase", saved.getPassphrase());
|
||||
assertNull(saved.getEncryptedPassword());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateSwitchToPrivateKeyClearsPasswordCredential() {
|
||||
Connection existing = new Connection();
|
||||
existing.setId(10L);
|
||||
existing.setUserId(1L);
|
||||
existing.setAuthType(Connection.AuthType.PASSWORD);
|
||||
existing.setEncryptedPassword("old-password");
|
||||
|
||||
ConnectionCreateRequest request = new ConnectionCreateRequest();
|
||||
request.setAuthType(Connection.AuthType.PRIVATE_KEY);
|
||||
request.setPrivateKey("new-key");
|
||||
request.setPassphrase("new-passphrase");
|
||||
|
||||
when(connectionRepository.findById(10L)).thenReturn(Optional.of(existing));
|
||||
when(encryptionService.encrypt("new-key")).thenReturn("enc-new-key");
|
||||
when(encryptionService.encrypt("new-passphrase")).thenReturn("enc-new-passphrase");
|
||||
|
||||
ConnectionDto result = connectionService.update(10L, request, 1L);
|
||||
|
||||
assertNotNull(result);
|
||||
ArgumentCaptor<Connection> captor = ArgumentCaptor.forClass(Connection.class);
|
||||
verify(connectionRepository).save(captor.capture());
|
||||
Connection saved = captor.getValue();
|
||||
|
||||
assertEquals(Connection.AuthType.PRIVATE_KEY, saved.getAuthType());
|
||||
assertEquals("enc-new-key", saved.getEncryptedPrivateKey());
|
||||
assertEquals("enc-new-passphrase", saved.getPassphrase());
|
||||
assertNull(saved.getEncryptedPassword());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateSwitchToPasswordClearsPrivateKeyCredentials() {
|
||||
Connection existing = new Connection();
|
||||
existing.setId(20L);
|
||||
existing.setUserId(1L);
|
||||
existing.setAuthType(Connection.AuthType.PRIVATE_KEY);
|
||||
existing.setEncryptedPrivateKey("old-key");
|
||||
existing.setPassphrase("old-passphrase");
|
||||
|
||||
ConnectionCreateRequest request = new ConnectionCreateRequest();
|
||||
request.setAuthType(Connection.AuthType.PASSWORD);
|
||||
request.setPassword("new-password");
|
||||
|
||||
when(connectionRepository.findById(20L)).thenReturn(Optional.of(existing));
|
||||
when(encryptionService.encrypt("new-password")).thenReturn("enc-new-password");
|
||||
|
||||
ConnectionDto result = connectionService.update(20L, request, 1L);
|
||||
|
||||
assertNotNull(result);
|
||||
ArgumentCaptor<Connection> captor = ArgumentCaptor.forClass(Connection.class);
|
||||
verify(connectionRepository).save(captor.capture());
|
||||
Connection saved = captor.getValue();
|
||||
|
||||
assertEquals(Connection.AuthType.PASSWORD, saved.getAuthType());
|
||||
assertEquals("enc-new-password", saved.getEncryptedPassword());
|
||||
assertNull(saved.getEncryptedPrivateKey());
|
||||
assertNull(saved.getPassphrase());
|
||||
}
|
||||
}
|
||||
216
frontend/package-lock.json
generated
216
frontend/package-lock.json
generated
@@ -637,9 +637,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
|
||||
"integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
||||
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -651,9 +651,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -665,9 +665,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -679,9 +679,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
|
||||
"integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -693,9 +693,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -707,9 +707,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
|
||||
"integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -721,9 +721,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
|
||||
"integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -735,9 +735,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
|
||||
"integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
|
||||
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -749,9 +749,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -763,9 +763,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -777,9 +777,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -791,9 +791,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -805,9 +805,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -819,9 +819,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -833,9 +833,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -847,9 +847,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -861,9 +861,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -875,9 +875,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -889,9 +889,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
|
||||
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -903,9 +903,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
|
||||
"integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -917,9 +917,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
|
||||
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -931,9 +931,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
|
||||
"integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -945,9 +945,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
|
||||
"integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -959,9 +959,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
|
||||
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -973,9 +973,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
|
||||
"integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
|
||||
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1402,13 +1402,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
|
||||
"integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==",
|
||||
"version": "1.13.6",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
|
||||
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
@@ -2744,9 +2744,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
|
||||
"integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2760,31 +2760,31 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.57.1",
|
||||
"@rollup/rollup-android-arm64": "4.57.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.57.1",
|
||||
"@rollup/rollup-darwin-x64": "4.57.1",
|
||||
"@rollup/rollup-freebsd-arm64": "4.57.1",
|
||||
"@rollup/rollup-freebsd-x64": "4.57.1",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.57.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-x64-musl": "4.57.1",
|
||||
"@rollup/rollup-openbsd-x64": "4.57.1",
|
||||
"@rollup/rollup-openharmony-arm64": "4.57.1",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.57.1",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.57.1",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.57.1",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.57.1",
|
||||
"@rollup/rollup-android-arm-eabi": "4.59.0",
|
||||
"@rollup/rollup-android-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-x64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.59.0",
|
||||
"@rollup/rollup-openbsd-x64": "4.59.0",
|
||||
"@rollup/rollup-openharmony-arm64": "4.59.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.59.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user