add session-based connection management and React dashboard
Backend: adds JDBC session support, login/status/logout endpoints, and new DTOs (AuthResponse, ConnectionStatusResponse, LoginResult). Frontend replaces the Vite boilerplate with a Dashboard, ServiceCard, and ConnectModal backed by a typed API client. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,26 @@
|
||||
package com.vaessl.app.connection;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.vaessl.app.dto.AuthResponse;
|
||||
import com.vaessl.app.dto.ConnectionRequest;
|
||||
import com.vaessl.app.dto.ConnectionResponse;
|
||||
import com.vaessl.app.dto.ConnectionStatusResponse;
|
||||
import com.vaessl.app.dto.LoginResult;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@@ -15,10 +28,63 @@ import lombok.RequiredArgsConstructor;
|
||||
@RequiredArgsConstructor
|
||||
public class ConnectionController {
|
||||
|
||||
private static final String SUFFIX = "_CONNECTION_ID";
|
||||
|
||||
private final ConnectionService connectionService;
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<ConnectionResponse> loginResponse(@Valid @RequestBody ConnectionRequest request) {
|
||||
return ResponseEntity.ok(connectionService.login(request));
|
||||
public ResponseEntity<AuthResponse> login(
|
||||
@Valid @RequestBody ConnectionRequest request,
|
||||
HttpServletRequest httpReq) {
|
||||
|
||||
LoginResult result = connectionService.login(request);
|
||||
|
||||
HttpSession session = httpReq.getSession(true);
|
||||
session.setAttribute(request.serviceType() + SUFFIX, result.connectionId());
|
||||
|
||||
if (result.expiresAt() != null) {
|
||||
long secs = Instant.now().until(result.expiresAt(), ChronoUnit.SECONDS);
|
||||
session.setMaxInactiveInterval((int) Math.max(secs, 300));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(new AuthResponse(request.serviceType(), result.expiresAt()));
|
||||
}
|
||||
|
||||
@GetMapping("/connections/status")
|
||||
public ResponseEntity<List<ConnectionStatusResponse>> getStatus(HttpServletRequest httpReq) {
|
||||
HttpSession session = httpReq.getSession(false);
|
||||
if (session == null) {
|
||||
return ResponseEntity.ok(List.of());
|
||||
}
|
||||
|
||||
List<ConnectionStatusResponse> statuses = new ArrayList<>();
|
||||
Collections.list(session.getAttributeNames()).stream()
|
||||
.filter(k -> k.endsWith(SUFFIX))
|
||||
.forEach(k -> {
|
||||
String serviceType = k.replace(SUFFIX, "");
|
||||
Long id = (Long) session.getAttribute(k);
|
||||
ConnectionStatusResponse status = connectionService.getConnectionStatus(serviceType, id);
|
||||
if (status != null) statuses.add(status);
|
||||
});
|
||||
|
||||
return ResponseEntity.ok(statuses);
|
||||
}
|
||||
|
||||
@DeleteMapping("/connections/{serviceType}")
|
||||
public ResponseEntity<Void> logout(
|
||||
@PathVariable("serviceType") String serviceType,
|
||||
HttpServletRequest httpReq) {
|
||||
|
||||
HttpSession session = httpReq.getSession(false);
|
||||
if (session != null) {
|
||||
session.removeAttribute(serviceType + SUFFIX);
|
||||
boolean hasMore = Collections.list(session.getAttributeNames()).stream()
|
||||
.anyMatch(k -> k.endsWith(SUFFIX));
|
||||
if (!hasMore) {
|
||||
session.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.vaessl.app.connection;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import com.vaessl.app.dto.ConnectionRequest;
|
||||
import com.vaessl.app.dto.ConnectionResponse;
|
||||
|
||||
@@ -16,4 +18,8 @@ public interface ConnectionProvider {
|
||||
ConnectionEntity connectionToEntity(ConnectionRequest request, ConnectionResponse response);
|
||||
|
||||
void updateToRepository(ConnectionEntity existing, ConnectionResponse response);
|
||||
|
||||
default Instant getTokenExpiry(ConnectionEntity entity) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.vaessl.app.connection;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -8,6 +9,8 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import com.vaessl.app.dto.ConnectionRequest;
|
||||
import com.vaessl.app.dto.ConnectionResponse;
|
||||
import com.vaessl.app.dto.ConnectionStatusResponse;
|
||||
import com.vaessl.app.dto.LoginResult;
|
||||
import com.vaessl.app.exception.WrongServiceTypeException;
|
||||
|
||||
@Service
|
||||
@@ -23,7 +26,7 @@ public class ConnectionService {
|
||||
this.cRepository = cRepository;
|
||||
}
|
||||
|
||||
public ConnectionResponse login(ConnectionRequest request) {
|
||||
public LoginResult login(ConnectionRequest request) {
|
||||
|
||||
ConnectionProvider provider = providerRegistry.get(request.serviceType());
|
||||
|
||||
@@ -37,13 +40,27 @@ public class ConnectionService {
|
||||
|
||||
ConnectionEntity existing = provider.findUniqueConnectionEntry(request);
|
||||
|
||||
ConnectionEntity saved;
|
||||
if (existing != null) {
|
||||
provider.updateToRepository(existing, response);
|
||||
saved = existing;
|
||||
} else {
|
||||
ConnectionEntity newEntity = provider.connectionToEntity(request, response);
|
||||
cRepository.save(newEntity);
|
||||
saved = cRepository.save(newEntity);
|
||||
}
|
||||
|
||||
return response;
|
||||
return new LoginResult(saved.getId(), response.expiresAt());
|
||||
}
|
||||
}
|
||||
|
||||
public ConnectionStatusResponse getConnectionStatus(String serviceType, Long connectionId) {
|
||||
ConnectionEntity entity = cRepository.findById(connectionId).orElse(null);
|
||||
if (entity == null) return null;
|
||||
|
||||
ConnectionProvider provider = providerRegistry.get(serviceType);
|
||||
Instant expiresAt = (provider != null) ? provider.getTokenExpiry(entity) : null;
|
||||
boolean connected = expiresAt == null || expiresAt.isAfter(Instant.now());
|
||||
|
||||
return new ConnectionStatusResponse(serviceType, entity.getAppUrl(),
|
||||
entity.getUsername(), expiresAt, connected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.vaessl.app.connection;
|
||||
|
||||
public enum Endpoint {
|
||||
HOMEBOX_LOGIN("/api/v1/users/login"), LOGIN("/login");
|
||||
HOMEBOX_LOGIN("/api/v1/users/login"),
|
||||
LOGIN("/login"),
|
||||
CONNECTION_STATUS("/connections/status");
|
||||
|
||||
private final String value;
|
||||
|
||||
|
||||
@@ -97,6 +97,11 @@ public class HomeBoxConnectionProvider implements ConnectionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getTokenExpiry(ConnectionEntity entity) {
|
||||
return (entity instanceof HomeboxEntity he) ? he.getExpiresAt() : null;
|
||||
}
|
||||
|
||||
private record HomeboxLoginResponse(String token, String attachmentToken, Instant expiresAt) {
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user