diff --git a/backend/src/main/java/com/sshmanager/config/SecurityConfig.java b/backend/src/main/java/com/sshmanager/config/SecurityConfig.java index b4e8e3f..7923510 100644 --- a/backend/src/main/java/com/sshmanager/config/SecurityConfig.java +++ b/backend/src/main/java/com/sshmanager/config/SecurityConfig.java @@ -1,73 +1,71 @@ -package com.sshmanager.config; - -import com.sshmanager.security.JwtAuthenticationFilter; -import com.sshmanager.security.PasswordExpirationFilter; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - private final JwtAuthenticationFilter jwtAuthenticationFilter; - - @Autowired(required = false) - private SecurityExceptionHandler securityExceptionHandler; - - public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, - PasswordExpirationFilter passwordExpirationFilter) { - this.jwtAuthenticationFilter = jwtAuthenticationFilter; - this.passwordExpirationFilter = passwordExpirationFilter; - } - - private final PasswordExpirationFilter passwordExpirationFilter; - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .cors().and() - .csrf().disable() - .sessionManagement() - .sessionCreationPolicy(SessionCreationPolicy.STATELESS) - .and() - .authorizeRequests() - .antMatchers("/api/auth/**").permitAll() - .antMatchers("/ws/**").authenticated() - .antMatchers("/api/**").authenticated() - .anyRequest().permitAll() - .and() - .exceptionHandling(e -> { - if (securityExceptionHandler != null) { - e.authenticationEntryPoint(securityExceptionHandler); - } - }) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .addFilterBefore(passwordExpirationFilter, UsernamePasswordAuthenticationFilter.class); - - return http.build(); - } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean +package com.sshmanager.config; + +import com.sshmanager.security.JwtAuthenticationFilter; +import com.sshmanager.security.PasswordExpirationFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + @Autowired(required = false) + private SecurityExceptionHandler securityExceptionHandler; + + public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, + PasswordExpirationFilter passwordExpirationFilter) { + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + this.passwordExpirationFilter = passwordExpirationFilter; + } + + private final PasswordExpirationFilter passwordExpirationFilter; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .cors().and() + .csrf().disable() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/api/auth/**").permitAll() + .antMatchers("/ws/**").authenticated() + .antMatchers("/api/**").authenticated() + .anyRequest().permitAll() + .and() + .exceptionHandling(e -> { + if (securityExceptionHandler != null) { + e.authenticationEntryPoint(securityExceptionHandler); + } + }) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(passwordExpirationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); // Docker/remote deployments may be accessed via IP/hostname. @@ -76,9 +74,9 @@ public class SecurityConfig { config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); config.setAllowedHeaders(Arrays.asList("*")); config.setAllowCredentials(true); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); - return source; - } -} + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } +} diff --git a/backend/src/main/java/com/sshmanager/config/SpaForwardConfig.java b/backend/src/main/java/com/sshmanager/config/SpaForwardConfig.java index 18fedbf..70b42eb 100644 --- a/backend/src/main/java/com/sshmanager/config/SpaForwardConfig.java +++ b/backend/src/main/java/com/sshmanager/config/SpaForwardConfig.java @@ -2,6 +2,7 @@ package com.sshmanager.config; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; +import org.springframework.lang.NonNull; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.resource.PathResourceResolver; @@ -15,13 +16,13 @@ import java.io.IOException; public class SpaForwardConfig implements WebMvcConfigurer { @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { + public void addResourceHandlers(@NonNull ResourceHandlerRegistry registry) { registry.addResourceHandler("/**") .addResourceLocations("classpath:/static/") .resourceChain(true) .addResolver(new PathResourceResolver() { @Override - protected Resource getResource(String path, Resource location) throws IOException { + protected Resource getResource(@NonNull String path, @NonNull Resource location) throws IOException { Resource resource = location.createRelative(path); if (resource.exists() && resource.isReadable()) { return resource; diff --git a/backend/src/main/java/com/sshmanager/config/TerminalHandshakeInterceptor.java b/backend/src/main/java/com/sshmanager/config/TerminalHandshakeInterceptor.java index fdf9363..e12e402 100644 --- a/backend/src/main/java/com/sshmanager/config/TerminalHandshakeInterceptor.java +++ b/backend/src/main/java/com/sshmanager/config/TerminalHandshakeInterceptor.java @@ -9,6 +9,8 @@ import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeInterceptor; import javax.servlet.http.HttpServletRequest; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; import java.util.Map; @Component @@ -21,8 +23,8 @@ public class TerminalHandshakeInterceptor implements HandshakeInterceptor { } @Override - public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, - WebSocketHandler wsHandler, Map attributes) throws Exception { + public boolean beforeHandshake(@NonNull ServerHttpRequest request, @NonNull ServerHttpResponse response, + @NonNull WebSocketHandler wsHandler, @NonNull Map attributes) throws Exception { if (!(request instanceof ServletServerHttpRequest)) { return false; } @@ -50,7 +52,7 @@ public class TerminalHandshakeInterceptor implements HandshakeInterceptor { } @Override - public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, - WebSocketHandler wsHandler, Exception exception) { + public void afterHandshake(@NonNull ServerHttpRequest request, @NonNull ServerHttpResponse response, + @NonNull WebSocketHandler wsHandler, @Nullable Exception exception) { } } diff --git a/backend/src/main/java/com/sshmanager/config/WebSocketConfig.java b/backend/src/main/java/com/sshmanager/config/WebSocketConfig.java index 7285eb9..2acf3cd 100644 --- a/backend/src/main/java/com/sshmanager/config/WebSocketConfig.java +++ b/backend/src/main/java/com/sshmanager/config/WebSocketConfig.java @@ -1,13 +1,14 @@ -package com.sshmanager.config; - +package com.sshmanager.config; + import com.sshmanager.controller.TerminalWebSocketHandler; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; - -@Configuration -@EnableWebSocket +import org.springframework.lang.NonNull; + +@Configuration +@EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { private final TerminalWebSocketHandler terminalWebSocketHandler; @@ -18,9 +19,10 @@ public class WebSocketConfig implements WebSocketConfigurer { this.terminalWebSocketHandler = terminalWebSocketHandler; this.terminalHandshakeInterceptor = terminalHandshakeInterceptor; } - + @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + @SuppressWarnings("null") + public void registerWebSocketHandlers(@NonNull WebSocketHandlerRegistry registry) { registry.addHandler(terminalWebSocketHandler, "/ws/terminal") .addInterceptors(terminalHandshakeInterceptor) // Docker/remote deployments often use non-localhost origins. diff --git a/backend/src/main/java/com/sshmanager/controller/SftpController.java b/backend/src/main/java/com/sshmanager/controller/SftpController.java index 9d70778..eb254bf 100644 --- a/backend/src/main/java/com/sshmanager/controller/SftpController.java +++ b/backend/src/main/java/com/sshmanager/controller/SftpController.java @@ -14,7 +14,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; -import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; @@ -33,7 +32,6 @@ import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; import java.util.stream.Collectors; -import java.util.stream.Stream; @RestController @RequestMapping("/api/sftp") @@ -282,7 +280,7 @@ public class SftpController { return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"") - .contentType(MediaType.APPLICATION_OCTET_STREAM) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE) .body(stream); } catch (Exception e) { return ResponseEntity.status(500).build(); @@ -331,12 +329,11 @@ public class SftpController { file.transferTo(tempFile); final java.io.File savedFile = tempFile; - UploadTaskStatus status = new UploadTaskStatus(taskId, userId, connectionId, - path, filename, file.getSize()); + UploadTaskStatus status = new UploadTaskStatus(taskId, userId, filename, file.getSize()); status.setController(this); uploadTasks.put(taskKey, status); - Future future = transferTaskExecutor.submit(() -> { + transferTaskExecutor.submit(() -> { status.setStatus("running"); try { withSessionLock(key, () -> { @@ -387,8 +384,6 @@ public class SftpController { } } }); - status.setFuture(future); - Map result = new HashMap<>(); result.put("taskId", taskId); result.put("message", "Upload started"); @@ -654,6 +649,7 @@ public class SftpController { } @GetMapping("/transfer-remote/tasks/{taskId}/progress") + @SuppressWarnings("null") public SseEmitter streamTransferProgress( @PathVariable String taskId, Authentication authentication) { @@ -692,6 +688,7 @@ public class SftpController { } @GetMapping("/upload/tasks/{taskId}/progress") + @SuppressWarnings("null") public SseEmitter streamUploadProgress( @PathVariable String taskId, Authentication authentication) { @@ -739,6 +736,7 @@ public class SftpController { } } + @SuppressWarnings("null") private void broadcastProgress(String taskKey, Map data) { CopyOnWriteArrayList emitters = taskEmitters.get(taskKey); if (emitters == null || emitters.isEmpty()) { @@ -976,10 +974,7 @@ public class SftpController { public static class UploadTaskStatus { private final String taskId; private final Long userId; - private final Long connectionId; - private final String path; private final String filename; - private final long fileSize; private final long createdAt; private volatile String status; private volatile String error; @@ -987,17 +982,12 @@ public class SftpController { private volatile long finishedAt; private final AtomicLong totalBytes; private final AtomicLong transferredBytes; - private volatile Future future; private volatile SftpController controller; - public UploadTaskStatus(String taskId, Long userId, Long connectionId, - String path, String filename, long fileSize) { + public UploadTaskStatus(String taskId, Long userId, String filename, long fileSize) { this.taskId = taskId; this.userId = userId; - this.connectionId = connectionId; - this.path = path; this.filename = filename; - this.fileSize = fileSize; this.createdAt = System.currentTimeMillis(); this.status = "queued"; this.totalBytes = new AtomicLong(fileSize); @@ -1012,9 +1002,7 @@ public class SftpController { this.controller = controller; } - public void setFuture(Future future) { - this.future = future; - } + public void setStatus(String status) { this.status = status; diff --git a/backend/src/main/java/com/sshmanager/controller/TerminalWebSocketHandler.java b/backend/src/main/java/com/sshmanager/controller/TerminalWebSocketHandler.java index 97a5c8b..4f57093 100644 --- a/backend/src/main/java/com/sshmanager/controller/TerminalWebSocketHandler.java +++ b/backend/src/main/java/com/sshmanager/controller/TerminalWebSocketHandler.java @@ -1,107 +1,109 @@ -package com.sshmanager.controller; - -import com.sshmanager.entity.Connection; -import com.sshmanager.entity.User; -import com.sshmanager.repository.ConnectionRepository; -import com.sshmanager.repository.UserRepository; -import com.sshmanager.service.ConnectionService; -import com.sshmanager.service.SshService; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.stereotype.Component; -import org.springframework.web.socket.CloseStatus; -import org.springframework.web.socket.TextMessage; -import org.springframework.web.socket.WebSocketSession; -import org.springframework.web.socket.handler.TextWebSocketHandler; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicInteger; - -@Component -public class TerminalWebSocketHandler extends TextWebSocketHandler { - - private final ConnectionRepository connectionRepository; - private final UserRepository userRepository; - private final ConnectionService connectionService; - private final SshService sshService; - private final ExecutorService executor; - - private final AtomicInteger sessionCount = new AtomicInteger(0); - private final Map sessions = new ConcurrentHashMap<>(); - private final Map lastActivity = new ConcurrentHashMap<>(); - - public TerminalWebSocketHandler(ConnectionRepository connectionRepository, - UserRepository userRepository, - ConnectionService connectionService, - SshService sshService, - @Qualifier("terminalWebSocketExecutor") ExecutorService executor) { - this.connectionRepository = connectionRepository; - this.userRepository = userRepository; - this.connectionService = connectionService; - this.sshService = sshService; - this.executor = executor; +package com.sshmanager.controller; + +import com.sshmanager.entity.Connection; +import com.sshmanager.entity.User; +import com.sshmanager.repository.ConnectionRepository; +import com.sshmanager.repository.UserRepository; +import com.sshmanager.service.ConnectionService; +import com.sshmanager.service.SshService; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; +import org.springframework.lang.NonNull; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicInteger; + +@Component +public class TerminalWebSocketHandler extends TextWebSocketHandler { + + private final ConnectionRepository connectionRepository; + private final UserRepository userRepository; + private final ConnectionService connectionService; + private final SshService sshService; + private final ExecutorService executor; + + private final AtomicInteger sessionCount = new AtomicInteger(0); + private final Map sessions = new ConcurrentHashMap<>(); + private final Map lastActivity = new ConcurrentHashMap<>(); + + public TerminalWebSocketHandler(ConnectionRepository connectionRepository, + UserRepository userRepository, + ConnectionService connectionService, + SshService sshService, + @Qualifier("terminalWebSocketExecutor") ExecutorService executor) { + this.connectionRepository = connectionRepository; + this.userRepository = userRepository; + this.connectionService = connectionService; + this.sshService = sshService; + this.executor = executor; } - - @Override - public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception { - Long connectionId = (Long) webSocketSession.getAttributes().get("connectionId"); - String username = (String) webSocketSession.getAttributes().get("username"); - - if (connectionId == null || username == null) { - webSocketSession.close(CloseStatus.BAD_DATA); - return; - } - - User user = userRepository.findByUsername(username).orElse(null); - if (user == null) { - webSocketSession.close(CloseStatus.BAD_DATA); - return; - } - - Connection conn = connectionRepository.findById(connectionId).orElse(null); - if (conn == null || !conn.getUserId().equals(user.getId())) { - webSocketSession.close(CloseStatus.BAD_DATA); - return; - } - - String password = connectionService.getDecryptedPassword(conn); - String privateKey = connectionService.getDecryptedPrivateKey(conn); - String passphrase = connectionService.getDecryptedPassphrase(conn); - - try { - SshService.SshSession sshSession = sshService.createShellSession(conn, password, privateKey, passphrase); - sessions.put(webSocketSession.getId(), sshSession); - lastActivity.put(webSocketSession.getId(), System.currentTimeMillis()); - sessionCount.incrementAndGet(); - - executor.submit(() -> { - try { - InputStream in = sshSession.getOutputStream(); - byte[] buf = new byte[4096]; - int n; - while (webSocketSession.isOpen() && sshSession.isConnected() && (n = in.read(buf)) >= 0) { - String text = new String(buf, 0, n, "UTF-8"); - webSocketSession.sendMessage(new TextMessage(text)); - } - } catch (Exception e) { - if (webSocketSession.isOpen()) { - try { - webSocketSession.sendMessage(new TextMessage("\r\n[Connection closed]\r\n")); - } catch (IOException ignored) { - } - } - } - }); - } catch (Exception e) { - webSocketSession.sendMessage(new TextMessage("\r\n[SSH Error: " + e.getMessage() + "]\r\n")); - } - } - + @Override - protected void handleTextMessage(WebSocketSession webSocketSession, TextMessage message) throws Exception { + @SuppressWarnings("null") + public void afterConnectionEstablished(@NonNull WebSocketSession webSocketSession) throws Exception { + Long connectionId = (Long) webSocketSession.getAttributes().get("connectionId"); + String username = (String) webSocketSession.getAttributes().get("username"); + + if (connectionId == null || username == null) { + webSocketSession.close(CloseStatus.BAD_DATA); + return; + } + + User user = userRepository.findByUsername(username).orElse(null); + if (user == null) { + webSocketSession.close(CloseStatus.BAD_DATA); + return; + } + + Connection conn = connectionRepository.findById(connectionId).orElse(null); + if (conn == null || !conn.getUserId().equals(user.getId())) { + webSocketSession.close(CloseStatus.BAD_DATA); + return; + } + + String password = connectionService.getDecryptedPassword(conn); + String privateKey = connectionService.getDecryptedPrivateKey(conn); + String passphrase = connectionService.getDecryptedPassphrase(conn); + + try { + SshService.SshSession sshSession = sshService.createShellSession(conn, password, privateKey, passphrase); + sessions.put(webSocketSession.getId(), sshSession); + lastActivity.put(webSocketSession.getId(), System.currentTimeMillis()); + sessionCount.incrementAndGet(); + + executor.submit(() -> { + try { + InputStream in = sshSession.getOutputStream(); + byte[] buf = new byte[4096]; + int n; + while (webSocketSession.isOpen() && sshSession.isConnected() && (n = in.read(buf)) >= 0) { + String text = new String(buf, 0, n, "UTF-8"); + webSocketSession.sendMessage(new TextMessage(text)); + } + } catch (Exception e) { + if (webSocketSession.isOpen()) { + try { + webSocketSession.sendMessage(new TextMessage("\r\n[Connection closed]\r\n")); + } catch (IOException ignored) { + } + } + } + }); + } catch (Exception e) { + webSocketSession.sendMessage(new TextMessage("\r\n[SSH Error: " + e.getMessage() + "]\r\n")); + } + } + + @Override + protected void handleTextMessage(@NonNull WebSocketSession webSocketSession, @NonNull TextMessage message) throws Exception { SshService.SshSession sshSession = sessions.get(webSocketSession.getId()); if (sshSession != null && sshSession.isConnected()) { lastActivity.put(webSocketSession.getId(), System.currentTimeMillis()); @@ -120,14 +122,14 @@ public class TerminalWebSocketHandler extends TextWebSocketHandler { sshSession.getInputStream().flush(); } } - - @Override - public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus status) throws Exception { - SshService.SshSession sshSession = sessions.remove(webSocketSession.getId()); - lastActivity.remove(webSocketSession.getId()); - if (sshSession != null) { - sshSession.disconnect(); - sessionCount.decrementAndGet(); - } - } -} + + @Override + public void afterConnectionClosed(@NonNull WebSocketSession webSocketSession, @NonNull CloseStatus status) throws Exception { + SshService.SshSession sshSession = sessions.remove(webSocketSession.getId()); + lastActivity.remove(webSocketSession.getId()); + if (sshSession != null) { + sshSession.disconnect(); + sessionCount.decrementAndGet(); + } + } +} diff --git a/backend/src/main/java/com/sshmanager/entity/User.java b/backend/src/main/java/com/sshmanager/entity/User.java index a800d45..cd533a6 100644 --- a/backend/src/main/java/com/sshmanager/entity/User.java +++ b/backend/src/main/java/com/sshmanager/entity/User.java @@ -1,39 +1,38 @@ -package com.sshmanager.entity; - -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.AllArgsConstructor; - -import javax.persistence.*; -import java.time.Instant; -import java.time.temporal.ChronoUnit; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Entity -@Table(name = "users") -public class User { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, unique = true, length = 50) - private String username; - - @Column(nullable = false, length = 255) - private String passwordHash; - - @Column(length = 100) - private String displayName; - - @Column(nullable = false) - private Instant createdAt = Instant.now(); - - @Column(nullable = false) - private Instant updatedAt = Instant.now(); - - @Column(nullable = false) - private Instant passwordChangedAt; -} +package com.sshmanager.entity; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import javax.persistence.*; +import java.time.Instant; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true, length = 50) + private String username; + + @Column(nullable = false, length = 255) + private String passwordHash; + + @Column(length = 100) + private String displayName; + + @Column(nullable = false) + private Instant createdAt = Instant.now(); + + @Column(nullable = false) + private Instant updatedAt = Instant.now(); + + @Column(nullable = false) + private Instant passwordChangedAt; +} diff --git a/backend/src/main/java/com/sshmanager/security/JwtAuthenticationFilter.java b/backend/src/main/java/com/sshmanager/security/JwtAuthenticationFilter.java index fc4d531..0b5b366 100644 --- a/backend/src/main/java/com/sshmanager/security/JwtAuthenticationFilter.java +++ b/backend/src/main/java/com/sshmanager/security/JwtAuthenticationFilter.java @@ -1,65 +1,66 @@ -package com.sshmanager.security; - -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; -import org.springframework.web.filter.OncePerRequestFilter; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -@Component -public class JwtAuthenticationFilter extends OncePerRequestFilter { - - private final JwtTokenProvider tokenProvider; - private final UserDetailsService userDetailsService; - - public JwtAuthenticationFilter(JwtTokenProvider tokenProvider, UserDetailsService userDetailsService) { - this.tokenProvider = tokenProvider; - this.userDetailsService = userDetailsService; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - try { - String jwt = getJwtFromRequest(request); - if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { - String username = tokenProvider.getUsernameFromToken(jwt); - UserDetails userDetails = userDetailsService.loadUserByUsername(username); - - UsernamePasswordAuthenticationToken authentication = - new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - - SecurityContextHolder.getContext().setAuthentication(authentication); - } - } catch (Exception ex) { - // Log but don't fail - let controller handle 401 - } - filterChain.doFilter(request, response); - } - - private String getJwtFromRequest(HttpServletRequest request) { - String bearerToken = request.getHeader("Authorization"); - if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(7); - } - // WebSocket handshake and SSE endpoints send token as query param - String uri = request.getRequestURI(); - if (uri != null && (uri.startsWith("/ws/") || uri.contains("/progress"))) { - String token = request.getParameter("token"); - if (StringUtils.hasText(token)) { - return token; - } - } - return null; - } -} +package com.sshmanager.security; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.lang.NonNull; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider tokenProvider; + private final UserDetailsService userDetailsService; + + public JwtAuthenticationFilter(JwtTokenProvider tokenProvider, UserDetailsService userDetailsService) { + this.tokenProvider = tokenProvider; + this.userDetailsService = userDetailsService; + } + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + try { + String jwt = getJwtFromRequest(request); + if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { + String username = tokenProvider.getUsernameFromToken(jwt); + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception ex) { + // Log but don't fail - let controller handle 401 + } + filterChain.doFilter(request, response); + } + + private String getJwtFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + // WebSocket handshake and SSE endpoints send token as query param + String uri = request.getRequestURI(); + if (uri != null && (uri.startsWith("/ws/") || uri.contains("/progress"))) { + String token = request.getParameter("token"); + if (StringUtils.hasText(token)) { + return token; + } + } + return null; + } +} diff --git a/backend/src/main/java/com/sshmanager/security/PasswordExpirationFilter.java b/backend/src/main/java/com/sshmanager/security/PasswordExpirationFilter.java index 20c5e8c..5113930 100644 --- a/backend/src/main/java/com/sshmanager/security/PasswordExpirationFilter.java +++ b/backend/src/main/java/com/sshmanager/security/PasswordExpirationFilter.java @@ -7,6 +7,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.lang.NonNull; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -29,7 +30,7 @@ public class PasswordExpirationFilter extends OncePerRequestFilter { } @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.isAuthenticated()) { diff --git a/backend/src/main/java/com/sshmanager/service/BackupService.java b/backend/src/main/java/com/sshmanager/service/BackupService.java index 32bf64c..e2aea0f 100644 --- a/backend/src/main/java/com/sshmanager/service/BackupService.java +++ b/backend/src/main/java/com/sshmanager/service/BackupService.java @@ -59,6 +59,7 @@ public class BackupService { } @Transactional + @SuppressWarnings("null") public BackupImportResponseDto importBackup(Long userId, BackupPackageDto backupPackage) { if (backupPackage == null) { throw new IllegalArgumentException("Backup package is required"); diff --git a/backend/src/main/java/com/sshmanager/service/ConnectionService.java b/backend/src/main/java/com/sshmanager/service/ConnectionService.java index e3bfd6b..18e40c6 100644 --- a/backend/src/main/java/com/sshmanager/service/ConnectionService.java +++ b/backend/src/main/java/com/sshmanager/service/ConnectionService.java @@ -1,5 +1,5 @@ -package com.sshmanager.service; - +package com.sshmanager.service; + import com.sshmanager.dto.ConnectionCreateRequest; import com.sshmanager.dto.ConnectionDto; import com.sshmanager.entity.Connection; @@ -9,14 +9,15 @@ import com.sshmanager.exception.NotFoundException; import com.sshmanager.repository.ConnectionRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; - -import java.time.Instant; -import java.util.List; -import java.util.stream.Collectors; - -@Service -public class ConnectionService { - + +import java.time.Instant; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@SuppressWarnings("null") +public class ConnectionService { + private final ConnectionRepository connectionRepository; private final EncryptionService encryptionService; private final SshService sshService; @@ -31,13 +32,13 @@ public class ConnectionService { this.sshService = sshService; this.sshBootstrapService = sshBootstrapService; } - - public List listByUserId(Long userId) { - return connectionRepository.findByUserIdOrderByUpdatedAtDesc(userId).stream() - .map(ConnectionDto::fromEntity) - .collect(Collectors.toList()); - } - + + public List listByUserId(Long userId) { + return connectionRepository.findByUserIdOrderByUpdatedAtDesc(userId).stream() + .map(ConnectionDto::fromEntity) + .collect(Collectors.toList()); + } + public ConnectionDto getById(Long id, Long userId) { Connection conn = connectionRepository.findById(id).orElseThrow( () -> new NotFoundException("Connection not found: " + id)); @@ -101,7 +102,7 @@ public class ConnectionService { conn = connectionRepository.save(conn); return ConnectionDto.fromEntity(conn); } - + @Transactional public void delete(Long id, Long userId) { Connection conn = connectionRepository.findById(id).orElse(null); @@ -122,30 +123,30 @@ public class ConnectionService { } return conn; } - - public String getDecryptedPassword(Connection conn) { - return conn.getEncryptedPassword() != null ? - encryptionService.decrypt(conn.getEncryptedPassword()) : null; - } - - public String getDecryptedPrivateKey(Connection conn) { - return conn.getEncryptedPrivateKey() != null ? - encryptionService.decrypt(conn.getEncryptedPrivateKey()) : null; - } - - public String getDecryptedPassphrase(Connection conn) { - return conn.getPassphrase() != null ? - encryptionService.decrypt(conn.getPassphrase()) : null; - } - - public Connection testConnection(Connection conn, String password, String privateKey, String passphrase) { - SshService.SshSession session = null; - try { - session = sshService.createShellSession(conn, password, privateKey, passphrase); - return conn; - } catch (Exception e) { - throw new RuntimeException("Connection test failed: " + e.getMessage(), e); - } finally { + + public String getDecryptedPassword(Connection conn) { + return conn.getEncryptedPassword() != null ? + encryptionService.decrypt(conn.getEncryptedPassword()) : null; + } + + public String getDecryptedPrivateKey(Connection conn) { + return conn.getEncryptedPrivateKey() != null ? + encryptionService.decrypt(conn.getEncryptedPrivateKey()) : null; + } + + public String getDecryptedPassphrase(Connection conn) { + return conn.getPassphrase() != null ? + encryptionService.decrypt(conn.getPassphrase()) : null; + } + + public Connection testConnection(Connection conn, String password, String privateKey, String passphrase) { + SshService.SshSession session = null; + try { + session = sshService.createShellSession(conn, password, privateKey, passphrase); + return conn; + } catch (Exception e) { + throw new RuntimeException("Connection test failed: " + e.getMessage(), e); + } finally { if (session != null) { session.disconnect(); } diff --git a/backend/src/test/java/com/sshmanager/controller/ConnectionControllerTest.java b/backend/src/test/java/com/sshmanager/controller/ConnectionControllerTest.java index 8982c37..5f52c61 100644 --- a/backend/src/test/java/com/sshmanager/controller/ConnectionControllerTest.java +++ b/backend/src/test/java/com/sshmanager/controller/ConnectionControllerTest.java @@ -28,17 +28,13 @@ 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.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; -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) +@SuppressWarnings("null") class ConnectionControllerTest { @Mock diff --git a/backend/src/test/java/com/sshmanager/controller/SftpControllerTest.java b/backend/src/test/java/com/sshmanager/controller/SftpControllerTest.java index a4d6c88..6dd4045 100644 --- a/backend/src/test/java/com/sshmanager/controller/SftpControllerTest.java +++ b/backend/src/test/java/com/sshmanager/controller/SftpControllerTest.java @@ -45,6 +45,7 @@ import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) +@SuppressWarnings("null") class SftpControllerTest { @Mock diff --git a/backend/src/test/java/com/sshmanager/service/BackupServiceTest.java b/backend/src/test/java/com/sshmanager/service/BackupServiceTest.java index 16f0063..b200291 100644 --- a/backend/src/test/java/com/sshmanager/service/BackupServiceTest.java +++ b/backend/src/test/java/com/sshmanager/service/BackupServiceTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) +@SuppressWarnings("null") class BackupServiceTest { @Mock diff --git a/backend/src/test/java/com/sshmanager/service/ConnectionServiceTest.java b/backend/src/test/java/com/sshmanager/service/ConnectionServiceTest.java index cf92037..c6f77e1 100644 --- a/backend/src/test/java/com/sshmanager/service/ConnectionServiceTest.java +++ b/backend/src/test/java/com/sshmanager/service/ConnectionServiceTest.java @@ -27,6 +27,7 @@ import static org.mockito.Mockito.when; import static org.mockito.Mockito.lenient; @ExtendWith(MockitoExtension.class) +@SuppressWarnings("null") class ConnectionServiceTest { @Mock diff --git a/backend/src/test/java/com/sshmanager/service/SessionTreeLayoutServiceTest.java b/backend/src/test/java/com/sshmanager/service/SessionTreeLayoutServiceTest.java index 3d9d610..6aa2a0a 100644 --- a/backend/src/test/java/com/sshmanager/service/SessionTreeLayoutServiceTest.java +++ b/backend/src/test/java/com/sshmanager/service/SessionTreeLayoutServiceTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) +@SuppressWarnings("null") class SessionTreeLayoutServiceTest { @Mock diff --git a/frontend/src/components/TransferCenterModal.tsx b/frontend/src/components/TransferCenterModal.tsx index 3484d04..712b534 100644 --- a/frontend/src/components/TransferCenterModal.tsx +++ b/frontend/src/components/TransferCenterModal.tsx @@ -110,12 +110,12 @@ export default function TransferCenterModal({ const unsubscribe = subscribeUploadProgress(taskId, (task) => { updateTaskGroup(groupId, (current) => { const nextItems = current.items.map((item) => - item.taskId === task.taskId + item.taskId === task.taskId ? { ...item, progress: task.progress, status: task.status, - message: task.error || (task.status === 'success' ? '上传完成' : task.status === 'error' ? '上传失败' : task.status === 'cancelled' ? '已取消' : '正在传输...'), + message: task.error || (task.status === 'success' ? '上传完成' : task.status === 'error' ? '上传失败' : '正在传输...'), } : item, ) @@ -231,9 +231,9 @@ export default function TransferCenterModal({
按浏览器任务并行执行
-
- 3. 目标服务器 -
+
+ 3. 目标服务器 +
{connections.map((server) => { const st = connectionStatuses[server.id] const isOnline = st === 'online'