Adapt FileStorage tests to Instance<JobOwnershipService> constructor
This commit is contained in:
+10
-2
@@ -2,26 +2,34 @@ package stirling.software.common.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import jakarta.enterprise.inject.Instance;
|
||||
|
||||
import stirling.software.common.cluster.inprocess.LocalDiskFileStore;
|
||||
|
||||
class FileStorageDelegationTest {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void storeBytesThenRetrieveBytesRoundTripsThroughFileStore(@TempDir Path tempDir)
|
||||
throws IOException {
|
||||
// FileStorage now takes a CDI Instance<JobOwnershipService> (was Optional). Mock one that
|
||||
// is
|
||||
// not resolvable, mirroring the previous Optional.empty() (desktop / no-security mode).
|
||||
Instance<JobOwnershipService> noOwnership = mock(Instance.class);
|
||||
when(noOwnership.isResolvable()).thenReturn(false);
|
||||
FileStorage fs =
|
||||
new FileStorage(
|
||||
mock(FileOrUploadService.class),
|
||||
new LocalDiskFileStore(tempDir.toString()),
|
||||
Optional.empty());
|
||||
noOwnership);
|
||||
byte[] payload = "round-trip".getBytes();
|
||||
String id = fs.storeBytes(payload, "x.bin");
|
||||
assertArrayEquals(payload, fs.retrieveBytes(id));
|
||||
|
||||
+15
-2
@@ -13,16 +13,29 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import jakarta.enterprise.inject.Instance;
|
||||
|
||||
import stirling.software.common.cluster.inprocess.LocalDiskFileStore;
|
||||
import stirling.software.common.util.JobContext;
|
||||
|
||||
class FileStorageOwnershipTest {
|
||||
|
||||
// FileStorage now takes a CDI Instance<JobOwnershipService> (was Optional). Build a mock
|
||||
// Instance
|
||||
// whose isResolvable()/get() mirror the previous Optional.empty()/Optional.of(svc) contract.
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Instance<JobOwnershipService> instanceOf(JobOwnershipService svc) {
|
||||
Instance<JobOwnershipService> instance = mock(Instance.class);
|
||||
when(instance.isResolvable()).thenReturn(svc != null);
|
||||
when(instance.get()).thenReturn(svc);
|
||||
return instance;
|
||||
}
|
||||
|
||||
private FileStorage newStorageWithoutSecurity(Path tempDir) {
|
||||
return new FileStorage(
|
||||
mock(FileOrUploadService.class),
|
||||
new LocalDiskFileStore(tempDir.toString()),
|
||||
Optional.empty());
|
||||
instanceOf(null));
|
||||
}
|
||||
|
||||
private FileStorage newStorageWithCurrentUser(Path tempDir, AtomicReference<String> userRef) {
|
||||
@@ -31,7 +44,7 @@ class FileStorageOwnershipTest {
|
||||
return new FileStorage(
|
||||
mock(FileOrUploadService.class),
|
||||
new LocalDiskFileStore(tempDir.toString()),
|
||||
Optional.of(svc));
|
||||
instanceOf(svc));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -11,7 +11,6 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@@ -108,8 +107,6 @@ public class AiCreateController {
|
||||
|
||||
// TODO: Migration required - @Transactional(readOnly = true): jakarta.transaction.Transactional
|
||||
// has no readOnly attribute; using a plain transaction.
|
||||
// TODO: Migration required - org.springframework.data.domain.PageRequest is Spring Data; replace
|
||||
// with Hibernate ORM Panache paging when the repository layer is migrated.
|
||||
@GET
|
||||
@Path("/sessions")
|
||||
@Transactional
|
||||
@@ -121,8 +118,7 @@ public class AiCreateController {
|
||||
int safeSize = Math.max(1, Math.min(size, 50));
|
||||
List<AiCreateSessionRepository.AiCreateSessionSummaryProjection> sessions =
|
||||
sessionService.listSessionSummariesForCurrentUser(
|
||||
org.springframework.data.domain.PageRequest.of(safePage, safeSize),
|
||||
includeDrafts);
|
||||
safePage, safeSize, includeDrafts);
|
||||
return Response.ok(sessions.stream().map(this::toSummary).toList()).build();
|
||||
}
|
||||
|
||||
|
||||
+7
-8
@@ -3,6 +3,13 @@ package stirling.software.saas.ai.controller;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
@@ -13,14 +20,6 @@ import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ import java.io.InputStream;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.Optional;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.ws.rs.GET;
|
||||
@@ -15,11 +19,6 @@ import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.StreamingOutput;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.security.Authentication;
|
||||
@@ -109,8 +108,7 @@ public class AiProxyController {
|
||||
|
||||
@GET
|
||||
@Path("/versions/{userId}")
|
||||
public Response versions(
|
||||
@PathParam("userId") String userId, HttpServletRequest request) {
|
||||
public Response versions(@PathParam("userId") String userId, HttpServletRequest request) {
|
||||
return proxy("GET", "/api/versions/" + userId, request, false, false);
|
||||
}
|
||||
|
||||
@@ -122,8 +120,7 @@ public class AiProxyController {
|
||||
|
||||
@POST
|
||||
@Path("/style/{userId}")
|
||||
public Response updateStyle(
|
||||
@PathParam("userId") String userId, HttpServletRequest request) {
|
||||
public Response updateStyle(@PathParam("userId") String userId, HttpServletRequest request) {
|
||||
return proxy("POST", "/api/style/" + userId, request, false, false);
|
||||
}
|
||||
|
||||
@@ -221,8 +218,7 @@ public class AiProxyController {
|
||||
};
|
||||
|
||||
Response.ResponseBuilder builder = Response.status(statusCode).entity(body);
|
||||
boolean hasContentType =
|
||||
copyHeader(aiResponse, builder, "Content-Type");
|
||||
boolean hasContentType = copyHeader(aiResponse, builder, "Content-Type");
|
||||
copyHeader(aiResponse, builder, "Cache-Control");
|
||||
copyHeader(aiResponse, builder, "X-Accel-Buffering");
|
||||
copyHeader(aiResponse, builder, "Content-Disposition");
|
||||
|
||||
+107
@@ -1,5 +1,6 @@
|
||||
package stirling.software.saas.ai.repository;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
@@ -7,10 +8,23 @@ import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import stirling.software.saas.ai.model.AiCreateSession;
|
||||
import stirling.software.saas.ai.model.AiCreateSessionStatus;
|
||||
|
||||
@ApplicationScoped
|
||||
public class AiCreateSessionRepository implements PanacheRepositoryBase<AiCreateSession, String> {
|
||||
|
||||
/**
|
||||
* Persist-or-update an entity and return the managed instance. Replaces the Spring Data {@code
|
||||
* save} convenience: for a managed/updated entity, mutations are flushed by the active
|
||||
* transaction; for a new entity, {@code persist} attaches it.
|
||||
*/
|
||||
public AiCreateSession save(AiCreateSession entity) {
|
||||
if (entity != null && !isPersistent(entity)) {
|
||||
persist(entity);
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
public List<AiCreateSession> findByUserIdOrderByUpdatedAtDesc(String userId) {
|
||||
return find("userId = ?1 ORDER BY updatedAt DESC", userId).list();
|
||||
}
|
||||
@@ -25,4 +39,97 @@ public class AiCreateSessionRepository implements PanacheRepositoryBase<AiCreate
|
||||
.page(0, maxResults)
|
||||
.list();
|
||||
}
|
||||
|
||||
public List<AiCreateSession> findByUserIdOrderByUpdatedAtDesc(
|
||||
String userId, int page, int size) {
|
||||
return find("userId = ?1 ORDER BY updatedAt DESC", userId).page(page, size).list();
|
||||
}
|
||||
|
||||
public List<AiCreateSession> findByUserIdAndPdfUrlIsNotNullOrderByUpdatedAtDesc(
|
||||
String userId, int page, int size) {
|
||||
return find("userId = ?1 AND pdfUrl IS NOT NULL ORDER BY updatedAt DESC", userId)
|
||||
.page(page, size)
|
||||
.list();
|
||||
}
|
||||
|
||||
public List<AiCreateSessionSummaryProjection> findSummariesByUserIdOrderByUpdatedAtDesc(
|
||||
String userId, int page, int size) {
|
||||
return findByUserIdOrderByUpdatedAtDesc(userId, page, size).stream()
|
||||
.map(AiCreateSessionSummaryProjection::of)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<AiCreateSessionSummaryProjection>
|
||||
findSummariesByUserIdAndPdfUrlIsNotNullOrderByUpdatedAtDesc(
|
||||
String userId, int page, int size) {
|
||||
return findByUserIdAndPdfUrlIsNotNullOrderByUpdatedAtDesc(userId, page, size).stream()
|
||||
.map(AiCreateSessionSummaryProjection::of)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight summary view of an {@link AiCreateSession}. Was a Spring Data interface
|
||||
* projection; under Panache it is materialised from the entity. Getter names are preserved so
|
||||
* callers are unchanged.
|
||||
*/
|
||||
public record AiCreateSessionSummaryProjection(
|
||||
String sessionId,
|
||||
String docType,
|
||||
String templateId,
|
||||
String promptLatest,
|
||||
String promptInitial,
|
||||
AiCreateSessionStatus status,
|
||||
String pdfUrl,
|
||||
Instant createdAt,
|
||||
Instant updatedAt) {
|
||||
|
||||
static AiCreateSessionSummaryProjection of(AiCreateSession session) {
|
||||
return new AiCreateSessionSummaryProjection(
|
||||
session.getSessionId(),
|
||||
session.getDocType(),
|
||||
session.getTemplateId(),
|
||||
session.getPromptLatest(),
|
||||
session.getPromptInitial(),
|
||||
session.getStatus(),
|
||||
session.getPdfUrl(),
|
||||
session.getCreatedAt(),
|
||||
session.getUpdatedAt());
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public String getDocType() {
|
||||
return docType;
|
||||
}
|
||||
|
||||
public String getTemplateId() {
|
||||
return templateId;
|
||||
}
|
||||
|
||||
public String getPromptLatest() {
|
||||
return promptLatest;
|
||||
}
|
||||
|
||||
public String getPromptInitial() {
|
||||
return promptInitial;
|
||||
}
|
||||
|
||||
public AiCreateSessionStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public String getPdfUrl() {
|
||||
return pdfUrl;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public Instant getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@ import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
+25
-23
@@ -4,23 +4,15 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
// TODO: Migration required - org.springframework.data.domain.Pageable is Spring Data; replace with
|
||||
// Hibernate ORM Panache paging (Page / range) when the repository layer is migrated.
|
||||
import org.springframework.data.domain.Pageable;
|
||||
// TODO: Migration required - org.springframework.web.context.request.RequestContextHolder /
|
||||
// ServletRequestAttributes are Spring MVC; replace with a JAX-RS/Quarkus request-scoped lookup
|
||||
// (e.g. injected jakarta.ws.rs.core.HttpHeaders / RoutingContext) for session-scoped id resolution.
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.inject.Instance;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -43,6 +35,12 @@ public class AiCreateSessionService {
|
||||
|
||||
private final Optional<UserServiceInterface> userService;
|
||||
|
||||
// TODO: Migration required - Spring MVC RequestContextHolder/ServletRequestAttributes replaced
|
||||
// with a CDI-injected request-scoped HttpServletRequest (quarkus-undertow). Wrapped in Instance
|
||||
// so resolution outside an active HTTP request (e.g. scheduled/startup contexts) is a safe
|
||||
// no-op.
|
||||
@jakarta.inject.Inject Instance<HttpServletRequest> currentRequest;
|
||||
|
||||
public AiCreateSession createSession(
|
||||
String prompt,
|
||||
String docType,
|
||||
@@ -66,7 +64,7 @@ public class AiCreateSessionService {
|
||||
|
||||
public AiCreateSession getSession(String sessionId) {
|
||||
return repository
|
||||
.findById(sessionId)
|
||||
.findByIdOptional(sessionId)
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new WebApplicationException(
|
||||
@@ -188,28 +186,28 @@ public class AiCreateSessionService {
|
||||
return repository.findByUserIdOrderByUpdatedAtDesc(userId);
|
||||
}
|
||||
|
||||
public List<AiCreateSession> listSessionsForCurrentUser(Pageable pageable) {
|
||||
public List<AiCreateSession> listSessionsForCurrentUser(int page, int size) {
|
||||
String userId = resolveUserId();
|
||||
return repository.findByUserIdOrderByUpdatedAtDesc(userId, pageable);
|
||||
return repository.findByUserIdOrderByUpdatedAtDesc(userId, page, size);
|
||||
}
|
||||
|
||||
public List<AiCreateSession> listSessionsForCurrentUser(
|
||||
Pageable pageable, boolean includeDrafts) {
|
||||
int page, int size, boolean includeDrafts) {
|
||||
String userId = resolveUserId();
|
||||
if (includeDrafts) {
|
||||
return repository.findByUserIdOrderByUpdatedAtDesc(userId, pageable);
|
||||
return repository.findByUserIdOrderByUpdatedAtDesc(userId, page, size);
|
||||
}
|
||||
return repository.findByUserIdAndPdfUrlIsNotNullOrderByUpdatedAtDesc(userId, pageable);
|
||||
return repository.findByUserIdAndPdfUrlIsNotNullOrderByUpdatedAtDesc(userId, page, size);
|
||||
}
|
||||
|
||||
public List<AiCreateSessionRepository.AiCreateSessionSummaryProjection>
|
||||
listSessionSummariesForCurrentUser(Pageable pageable, boolean includeDrafts) {
|
||||
listSessionSummariesForCurrentUser(int page, int size, boolean includeDrafts) {
|
||||
String userId = resolveUserId();
|
||||
if (includeDrafts) {
|
||||
return repository.findSummariesByUserIdOrderByUpdatedAtDesc(userId, pageable);
|
||||
return repository.findSummariesByUserIdOrderByUpdatedAtDesc(userId, page, size);
|
||||
}
|
||||
return repository.findSummariesByUserIdAndPdfUrlIsNotNullOrderByUpdatedAtDesc(
|
||||
userId, pageable);
|
||||
userId, page, size);
|
||||
}
|
||||
|
||||
public String resolveUserId() {
|
||||
@@ -250,12 +248,16 @@ public class AiCreateSessionService {
|
||||
}
|
||||
|
||||
private String resolveSessionScopedId() {
|
||||
ServletRequestAttributes attributes =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
if (currentRequest == null || !currentRequest.isResolvable()) {
|
||||
return null;
|
||||
}
|
||||
HttpServletRequest request;
|
||||
try {
|
||||
request = currentRequest.get();
|
||||
} catch (RuntimeException exc) {
|
||||
// No active request context (e.g. invoked outside an HTTP request).
|
||||
return null;
|
||||
}
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -11,13 +11,13 @@ import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.Part;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.Part;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package stirling.software.saas.config;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.saas.interceptor.UnifiedCreditInterceptor;
|
||||
|
||||
@@ -2,13 +2,14 @@ package stirling.software.saas.config;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
// TODO: Migration required - @ConfigurationProperties(prefix="credits"); bind via @ConfigProperty or @ConfigMapping
|
||||
// TODO: Migration required - @ConfigurationProperties(prefix="credits"); bind via @ConfigProperty
|
||||
// or @ConfigMapping
|
||||
@Data
|
||||
@ApplicationScoped
|
||||
@IfBuildProfile("saas")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package stirling.software.saas.config;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package stirling.software.saas.config;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
/**
|
||||
* Previously registered the {@code :saas} module's entities and repositories with Spring Data JPA.
|
||||
*
|
||||
* <p>TODO: Migration required - datasource/JPA now configured via quarkus.datasource.* /
|
||||
* quarkus.hibernate-orm.* in application.properties. Entity scanning and repository discovery are
|
||||
* automatic in Quarkus (Panache/Hibernate ORM), so the former @EnableJpaRepositories basePackages
|
||||
* (stirling.software.saas.repository, .billing.repository, .ai.repository, .payg.repository) and
|
||||
* @EntityScan packages (.model, .billing.model, .ai.model, .payg) are no longer needed.
|
||||
* (stirling.software.saas.repository, .billing.repository, .ai.repository, .payg.repository)
|
||||
* and @EntityScan packages (.model, .billing.model, .ai.model, .payg) are no longer needed.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@IfBuildProfile("saas")
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package stirling.software.saas.config;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.inject.Produces;
|
||||
import jakarta.inject.Named;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
/** Saas mode is unconditionally ENTERPRISE (every tenant is a paying Stripe customer). */
|
||||
@ApplicationScoped
|
||||
@IfBuildProfile("saas")
|
||||
|
||||
@@ -3,18 +3,19 @@ package stirling.software.saas.config;
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.inject.Produces;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
/**
|
||||
* HTTP client for talking to Supabase Edge Functions, with a bounded connect timeout.
|
||||
*
|
||||
* <p>TODO: Migration required - replaced Spring RestTemplate with java.net.http.HttpClient. Consider
|
||||
* a typed {@code @RegisterRestClient} client instead. Note: the per-request read timeout previously
|
||||
* set on RestTemplate must now be applied per HttpRequest via {@code HttpRequest.Builder#timeout}.
|
||||
* <p>TODO: Migration required - replaced Spring RestTemplate with java.net.http.HttpClient.
|
||||
* Consider a typed {@code @RegisterRestClient} client instead. Note: the per-request read timeout
|
||||
* previously set on RestTemplate must now be applied per HttpRequest via {@code
|
||||
* HttpRequest.Builder#timeout}.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@IfBuildProfile("saas")
|
||||
|
||||
+4
-2
@@ -1,13 +1,15 @@
|
||||
package stirling.software.saas.config;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/** Supabase configuration ({@code app.supabase.*}) for saas mode. */
|
||||
// TODO: Migration required - @ConfigurationProperties(prefix="app.supabase"); bind via @ConfigProperty or @ConfigMapping
|
||||
// TODO: Migration required - @ConfigurationProperties(prefix="app.supabase"); bind via
|
||||
// @ConfigProperty or @ConfigMapping
|
||||
@Slf4j
|
||||
@Data
|
||||
@ApplicationScoped
|
||||
|
||||
@@ -2,6 +2,14 @@ package stirling.software.saas.controller;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.ws.rs.GET;
|
||||
@@ -11,15 +19,6 @@ import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
@@ -16,8 +18,6 @@ import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -166,7 +166,7 @@ public class SaasTeamController {
|
||||
|
||||
invitation.setStatus(
|
||||
stirling.software.common.model.enumeration.InvitationStatus.REJECTED);
|
||||
invitationRepository.save(invitation);
|
||||
invitationRepository.persist(invitation);
|
||||
|
||||
return Response.ok(Map.of("message", "Invitation rejected")).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
@@ -195,7 +195,7 @@ public class SaasTeamController {
|
||||
User currentUser = getCurrentUser();
|
||||
TeamInvitation invitation =
|
||||
invitationRepository
|
||||
.findById(invitationId)
|
||||
.findByIdOptional(invitationId)
|
||||
.orElseThrow(
|
||||
() -> new IllegalArgumentException("Invitation not found"));
|
||||
|
||||
@@ -221,7 +221,7 @@ public class SaasTeamController {
|
||||
|
||||
invitation.setStatus(
|
||||
stirling.software.common.model.enumeration.InvitationStatus.CANCELLED);
|
||||
invitationRepository.save(invitation);
|
||||
invitationRepository.persist(invitation);
|
||||
|
||||
return Response.ok(Map.of("message", "Invitation cancelled")).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
@@ -392,9 +392,9 @@ public class SaasTeamController {
|
||||
// Get the user being removed before removing them
|
||||
User userToRemove =
|
||||
userRepository
|
||||
.findById(memberId)
|
||||
.findByIdOptional(memberId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Member not found"));
|
||||
Team oldTeam = teamRepository.findById(teamId).orElseThrow();
|
||||
Team oldTeam = teamRepository.findByIdOptional(teamId).orElseThrow();
|
||||
|
||||
// Remove the user from the team
|
||||
saasTeamService.removeTeamMember(teamId, memberId, currentUser);
|
||||
@@ -443,7 +443,7 @@ public class SaasTeamController {
|
||||
User currentUser = getCurrentUser();
|
||||
|
||||
// Get the team before leaving
|
||||
Team oldTeam = teamRepository.findById(teamId).orElseThrow();
|
||||
Team oldTeam = teamRepository.findByIdOptional(teamId).orElseThrow();
|
||||
|
||||
// Leave the team
|
||||
saasTeamService.leaveTeam(teamId, currentUser);
|
||||
@@ -498,7 +498,7 @@ public class SaasTeamController {
|
||||
|
||||
Team team =
|
||||
teamRepository
|
||||
.findById(teamId)
|
||||
.findByIdOptional(teamId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Team not found"));
|
||||
|
||||
// Prevent renaming personal teams
|
||||
@@ -516,7 +516,7 @@ public class SaasTeamController {
|
||||
}
|
||||
|
||||
team.setName(request.newName.trim());
|
||||
teamRepository.save(team);
|
||||
teamRepository.persist(team);
|
||||
|
||||
log.info(
|
||||
"Team {} renamed to {} by leader {}",
|
||||
@@ -641,12 +641,11 @@ public class SaasTeamController {
|
||||
@Path("/{teamId}/seats")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@RolesAllowed("ADMIN")
|
||||
public Response updateTeamSeats(
|
||||
@PathParam("teamId") Long teamId, UpdateSeatsRequest request) {
|
||||
public Response updateTeamSeats(@PathParam("teamId") Long teamId, UpdateSeatsRequest request) {
|
||||
try {
|
||||
saasTeamService.updateTeamSeats(teamId, request.maxSeats);
|
||||
|
||||
Team team = teamRepository.findById(teamId).orElseThrow();
|
||||
Team team = teamRepository.findByIdOptional(teamId).orElseThrow();
|
||||
int maxSeats = saasTeamExtensionService.getMaxSeats(team);
|
||||
int seatsUsed = saasTeamExtensionService.getSeatsUsed(team);
|
||||
return Response.ok(
|
||||
@@ -734,7 +733,7 @@ public class SaasTeamController {
|
||||
try {
|
||||
Team team =
|
||||
teamRepository
|
||||
.findById(teamId)
|
||||
.findByIdOptional(teamId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Team not found"));
|
||||
|
||||
List<TeamMembership> memberships = membershipRepository.findByTeamId(teamId);
|
||||
|
||||
+3
-4
@@ -6,6 +6,9 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.ws.rs.POST;
|
||||
@@ -13,10 +16,6 @@ import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
@@ -37,10 +37,10 @@ import stirling.software.saas.util.CreditHeaderUtils;
|
||||
* <p>// TODO: Migration required - was a Spring {@code @RestControllerAdvice(annotations =
|
||||
* AutoJobPostMapping.class)} with {@code @Profile("saas")} and {@code @Order(1)}, handling
|
||||
* exceptions via {@code @ExceptionHandler(Throwable.class)}. Re-express as a JAX-RS
|
||||
* {@code @jakarta.ws.rs.ext.Provider ExceptionMapper<Throwable>} (scoped/gated to AutoJobPostMapping
|
||||
* endpoints), wiring the saas profile gating via build profile. The {@code handleThrowable} body has
|
||||
* been translated from Spring {@code ResponseEntity} to JAX-RS {@code Response} but the
|
||||
* registration/scoping as an ExceptionMapper still needs wiring.
|
||||
* {@code @jakarta.ws.rs.ext.Provider ExceptionMapper<Throwable>} (scoped/gated to
|
||||
* AutoJobPostMapping endpoints), wiring the saas profile gating via build profile. The {@code
|
||||
* handleThrowable} body has been translated from Spring {@code ResponseEntity} to JAX-RS {@code
|
||||
* Response} but the registration/scoping as an ExceptionMapper still needs wiring.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
@@ -84,7 +84,8 @@ public class CreditErrorAdvice {
|
||||
}
|
||||
|
||||
// TODO: Migration required - was @ExceptionHandler(Throwable.class) on a @RestControllerAdvice.
|
||||
// Convert to ExceptionMapper<Throwable>.toResponse(Throwable); HttpServletRequest access must be
|
||||
// Convert to ExceptionMapper<Throwable>.toResponse(Throwable); HttpServletRequest access must
|
||||
// be
|
||||
// replaced by an injected JAX-RS request context (e.g. @Context ContainerRequestContext) since
|
||||
// ExceptionMapper does not receive the servlet request as a parameter.
|
||||
public Response handleThrowable(HttpServletRequest request, Throwable ex) {
|
||||
|
||||
@@ -28,11 +28,12 @@ import stirling.software.saas.util.CreditHeaderUtils;
|
||||
* beforeBodyWrite(...)}. Spring's {@code ResponseBodyAdvice}, {@code MethodParameter}, {@code
|
||||
* HttpMessageConverter}, {@code ServerHttpRequest/Response} and {@code
|
||||
* ServletServerHttpRequest/Response} have no Quarkus equivalent. Re-express the body-write
|
||||
* interception as a JAX-RS {@code @jakarta.ws.rs.ext.Provider ContainerResponseFilter}. The original
|
||||
* {@code supports(...)} returned true for all REST bodies. The credit-consumption logic from {@code
|
||||
* beforeBodyWrite} is preserved verbatim in {@link #onBeforeBodyWrite(HttpServletRequest,
|
||||
* HttpServletResponse)} below, operating on the servlet request/response that the original obtained
|
||||
* via {@code ServletServerHttpRequest.getServletRequest()} / {@code
|
||||
* interception as a JAX-RS {@code @jakarta.ws.rs.ext.Provider ContainerResponseFilter}. The
|
||||
* original {@code supports(...)} returned true for all REST bodies. The credit-consumption logic
|
||||
* from {@code beforeBodyWrite} is preserved verbatim in {@link
|
||||
* #onBeforeBodyWrite(HttpServletRequest, HttpServletResponse)} below, operating on the servlet
|
||||
* request/response that the original obtained via {@code
|
||||
* ServletServerHttpRequest.getServletRequest()} / {@code
|
||||
* ServletServerHttpResponse.getServletResponse()}.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@@ -70,7 +71,8 @@ public class CreditSuccessAdvice {
|
||||
.register(meterRegistry);
|
||||
}
|
||||
|
||||
// TODO: Migration required - was ResponseBodyAdvice#beforeBodyWrite(Object body, MethodParameter,
|
||||
// TODO: Migration required - was ResponseBodyAdvice#beforeBodyWrite(Object body,
|
||||
// MethodParameter,
|
||||
// MediaType, Class<? extends HttpMessageConverter<?>>, ServerHttpRequest, ServerHttpResponse).
|
||||
// Re-wire as ContainerResponseFilter.filter(ContainerRequestContext, ContainerResponseContext).
|
||||
// The original returned `body` unchanged; this side-effects credit consumption + response
|
||||
|
||||
+15
-11
@@ -38,10 +38,10 @@ import stirling.software.saas.util.AuthenticationUtils;
|
||||
* HandlerInterceptor}, {@code HandlerMethod} and {@code ModelAndView} have no Quarkus equivalent.
|
||||
* Convert this to a JAX-RS {@code @jakarta.ws.rs.ext.Provider ContainerRequestFilter} (preHandle ->
|
||||
* filter, with abort responses replacing the {@code return false} short-circuits). The handler
|
||||
* introspection that read {@code @AutoJobPostMapping} off the resolved {@code HandlerMethod} must be
|
||||
* replaced by JAX-RS {@code ResourceInfo#getResourceMethod()} (injected via {@code @Context}). The
|
||||
* method bodies are preserved; {@code handler} is now an opaque {@code Object} and the
|
||||
* {@code HandlerMethod} cast has been replaced by a reflective {@link Method} fallback (see TODOs).
|
||||
* introspection that read {@code @AutoJobPostMapping} off the resolved {@code HandlerMethod} must
|
||||
* be replaced by JAX-RS {@code ResourceInfo#getResourceMethod()} (injected via {@code @Context}).
|
||||
* The method bodies are preserved; {@code handler} is now an opaque {@code Object} and the {@code
|
||||
* HandlerMethod} cast has been replaced by a reflective {@link Method} fallback (see TODOs).
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
@@ -368,8 +368,10 @@ public class UnifiedCreditInterceptor {
|
||||
log.debug("[CREDIT-DEBUG] postHandle: Success path will be handled by CreditSuccessAdvice");
|
||||
}
|
||||
|
||||
// TODO: Migration required - was @Override HandlerInterceptor#afterCompletion(request, response,
|
||||
// handler, Exception). Re-wire via a JAX-RS ContainerResponseFilter if afterCompletion semantics
|
||||
// TODO: Migration required - was @Override HandlerInterceptor#afterCompletion(request,
|
||||
// response,
|
||||
// handler, Exception). Re-wire via a JAX-RS ContainerResponseFilter if afterCompletion
|
||||
// semantics
|
||||
// are needed; the error path is handled by CreditErrorAdvice.
|
||||
public void afterCompletion(
|
||||
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
|
||||
@@ -385,7 +387,8 @@ public class UnifiedCreditInterceptor {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Migration required - was @Override AsyncHandlerInterceptor#afterConcurrentHandlingStarted.
|
||||
// TODO: Migration required - was @Override
|
||||
// AsyncHandlerInterceptor#afterConcurrentHandlingStarted.
|
||||
// JAX-RS handles async dispatch differently; no direct equivalent required.
|
||||
public void afterConcurrentHandlingStarted(
|
||||
HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||
@@ -397,10 +400,11 @@ public class UnifiedCreditInterceptor {
|
||||
}
|
||||
|
||||
/**
|
||||
* // TODO: Migration required - resolves the resource {@link Method} that the original code read
|
||||
* from Spring's {@code HandlerMethod}. Until this is wired to JAX-RS {@code ResourceInfo}, it
|
||||
* supports a handler that is already a {@link Method} or exposes a no-arg {@code getMethod()}
|
||||
* returning one (reflective best-effort), so the {@code @AutoJobPostMapping} gating still works.
|
||||
* // TODO: Migration required - resolves the resource {@link Method} that the original code
|
||||
* read from Spring's {@code HandlerMethod}. Until this is wired to JAX-RS {@code ResourceInfo},
|
||||
* it supports a handler that is already a {@link Method} or exposes a no-arg {@code
|
||||
* getMethod()} returning one (reflective best-effort), so the {@code @AutoJobPostMapping}
|
||||
* gating still works.
|
||||
*/
|
||||
private Method resolveResourceMethod(Object handler) {
|
||||
if (handler instanceof Method m) {
|
||||
|
||||
@@ -8,15 +8,14 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.MultipartFile;
|
||||
|
||||
import stirling.software.saas.payg.docs.DocumentClassifier;
|
||||
import stirling.software.saas.payg.docs.DocumentMetrics;
|
||||
import stirling.software.saas.payg.job.JobContext;
|
||||
|
||||
+2
-2
@@ -8,10 +8,10 @@ import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
+13
-12
@@ -25,6 +25,8 @@ import jakarta.servlet.http.Part;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.security.Authentication;
|
||||
import stirling.software.common.security.SecurityContextHolder;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
@@ -38,8 +40,6 @@ import stirling.software.saas.payg.job.JobService;
|
||||
import stirling.software.saas.payg.model.JobSource;
|
||||
import stirling.software.saas.payg.model.JobStepStatus;
|
||||
import stirling.software.saas.payg.model.ProcessType;
|
||||
import stirling.software.common.security.Authentication;
|
||||
import stirling.software.common.security.SecurityContextHolder;
|
||||
import stirling.software.saas.util.AuthenticationUtils;
|
||||
|
||||
/**
|
||||
@@ -64,8 +64,8 @@ import stirling.software.saas.util.AuthenticationUtils;
|
||||
* {@code @jakarta.ws.rs.ext.Provider} request/response filter pair. The Spring MVC types {@code
|
||||
* HandlerMethod}, {@code HandlerMapping}, {@code MultiValueMap}, {@code MultipartFile} and {@code
|
||||
* MultipartHttpServletRequest} have been removed: handler-annotation introspection now uses a
|
||||
* reflective {@link Method} fallback (see {@link #resolveResourceMethod}); multipart access now uses
|
||||
* the servlet-native {@link Part} API ({@code request.getParts()}); the best-matching-pattern
|
||||
* reflective {@link Method} fallback (see {@link #resolveResourceMethod}); multipart access now
|
||||
* uses the servlet-native {@link Part} API ({@code request.getParts()}); the best-matching-pattern
|
||||
* attribute now uses a literal key constant ({@link #BEST_MATCHING_PATTERN_ATTRIBUTE}).
|
||||
*
|
||||
* <p>// TODO: Migration required - {@link JobInput}'s {@code multipart} component is still typed as
|
||||
@@ -212,7 +212,8 @@ public class PaygChargeInterceptor {
|
||||
}
|
||||
|
||||
// TODO: Migration required - was `request instanceof MultipartHttpServletRequest mreq` +
|
||||
// mreq.getMultiFileMap(). Now uses servlet-native request.getParts(). A non-multipart request
|
||||
// mreq.getMultiFileMap(). Now uses servlet-native request.getParts(). A non-multipart
|
||||
// request
|
||||
// yields no file parts and short-circuits, preserving the original behavior.
|
||||
List<Part> nonEmpty = new ArrayList<>();
|
||||
try {
|
||||
@@ -291,9 +292,9 @@ public class PaygChargeInterceptor {
|
||||
* // TODO: Migration required - the {@link JobInput} record's first component is still Spring's
|
||||
* {@code MultipartFile} (owned by another module). This interceptor now sources inputs from the
|
||||
* servlet {@link Part} API. Once {@code JobInput} is migrated to carry a {@link Part} (or a
|
||||
* neutral size+content-type holder), construct it directly here:
|
||||
* {@code return new JobInput(part, path);}. Kept as a single adaptation seam so the rest of the
|
||||
* charge flow is untouched.
|
||||
* neutral size+content-type holder), construct it directly here: {@code return new
|
||||
* JobInput(part, path);}. Kept as a single adaptation seam so the rest of the charge flow is
|
||||
* untouched.
|
||||
*/
|
||||
private JobInput buildJobInput(Part part, Path path) {
|
||||
return new JobInput(part, path);
|
||||
@@ -482,10 +483,10 @@ public class PaygChargeInterceptor {
|
||||
}
|
||||
|
||||
/**
|
||||
* // TODO: Migration required - resolves the resource {@link Method} the original code read from
|
||||
* Spring's {@code HandlerMethod} (via {@code hm.getMethodAnnotation(...)}). Until wired to JAX-RS
|
||||
* {@code ResourceInfo}, supports a handler that is already a {@link Method} or exposes a no-arg
|
||||
* {@code getMethod()} returning one, preserving the {@code @AutoJobPostMapping} gating.
|
||||
* // TODO: Migration required - resolves the resource {@link Method} the original code read
|
||||
* from Spring's {@code HandlerMethod} (via {@code hm.getMethodAnnotation(...)}). Until wired to
|
||||
* JAX-RS {@code ResourceInfo}, supports a handler that is already a {@link Method} or exposes a
|
||||
* no-arg {@code getMethod()} returning one, preserving the {@code @AutoJobPostMapping} gating.
|
||||
*/
|
||||
private Method resolveResourceMethod(Object handler) {
|
||||
if (handler instanceof Method m) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package stirling.software.saas.payg.filter;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@@ -20,7 +21,8 @@ import lombok.Setter;
|
||||
* OUTPUT recording in {@code afterCompletion}. {@code null} = unbounded.
|
||||
* </ul>
|
||||
*/
|
||||
// TODO: Migration required - @ConfigurationProperties(prefix="payg.filter"); bind via @ConfigProperty or @ConfigMapping
|
||||
// TODO: Migration required - @ConfigurationProperties(prefix="payg.filter"); bind via
|
||||
// @ConfigProperty or @ConfigMapping
|
||||
@ApplicationScoped
|
||||
@IfBuildProfile("saas")
|
||||
@Getter
|
||||
|
||||
@@ -12,8 +12,9 @@ import java.util.Objects;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Component;
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -42,8 +43,8 @@ import stirling.software.common.util.TempFileManager;
|
||||
* by the wrapper and is NOT in the returned list when extraction takes the direct-PDF path.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Profile("saas")
|
||||
@ApplicationScoped
|
||||
@IfBuildProfile("saas")
|
||||
public class PaygOutputExtractor {
|
||||
|
||||
/** {@code %PDF-} in ASCII. */
|
||||
|
||||
+17
-7
@@ -2,10 +2,9 @@ package stirling.software.saas.payg.filter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.servlet.AsyncEvent;
|
||||
import jakarta.servlet.AsyncListener;
|
||||
import jakarta.servlet.FilterChain;
|
||||
@@ -31,10 +30,20 @@ import stirling.software.common.util.TempFileManager;
|
||||
* deferred to an {@link AsyncListener} so the wrapper survives the async window. Close is
|
||||
* idempotent so a defensive call by the interceptor's {@code afterCompletion} is harmless.
|
||||
*/
|
||||
// TODO: Migration required - this was a Spring {@code OncePerRequestFilter}
|
||||
// ({@code @Component @Profile("saas")}). It must be re-registered as a {@code
|
||||
// jakarta.servlet.Filter}
|
||||
// (or a JAX-RS {@code @jakarta.ws.rs.ext.Provider} ContainerResponse filter pair) and ordered ahead
|
||||
// of the PAYG interceptor so the response wrapper is available in afterCompletion. The Spring base
|
||||
// class provided once-per-request dispatch and the {@code doFilterInternal} hook; that hook's
|
||||
// servlet signature is retained below and must be wired into the chosen registration mechanism
|
||||
// (e.g. a {@code doFilter(ServletRequest, ServletResponse, FilterChain)} delegating here) during
|
||||
// the
|
||||
// conversion. The Servlet-native request/response/async handling itself is unchanged.
|
||||
@Slf4j
|
||||
@Component
|
||||
@Profile("saas")
|
||||
public class PaygResponseBodyWrapperFilter extends OncePerRequestFilter {
|
||||
@ApplicationScoped
|
||||
@IfBuildProfile("saas")
|
||||
public class PaygResponseBodyWrapperFilter {
|
||||
|
||||
/** Request-attribute key under which the wrapper is exposed to the interceptor. */
|
||||
public static final String REQUEST_ATTRIBUTE =
|
||||
@@ -49,7 +58,8 @@ public class PaygResponseBodyWrapperFilter extends OncePerRequestFilter {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
// TODO: Migration required - was @Override of Spring OncePerRequestFilter#doFilterInternal.
|
||||
// Retains the servlet signature; invoke from the filter registration's doFilter once converted.
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package stirling.software.saas.payg.filter;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
@@ -19,7 +20,8 @@ import lombok.RequiredArgsConstructor;
|
||||
* </ul>
|
||||
*/
|
||||
// TODO: Migration required - interceptor registration moved to @Provider JAX-RS filters; filter
|
||||
// registration (PaygResponseBodyWrapperFilter on /api/*) now via @WebFilter or quarkus filter config
|
||||
// registration (PaygResponseBodyWrapperFilter on /api/*) now via @WebFilter or quarkus filter
|
||||
// config
|
||||
@ApplicationScoped
|
||||
@IfBuildProfile("saas")
|
||||
@RequiredArgsConstructor
|
||||
|
||||
@@ -14,12 +14,12 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package stirling.software.saas.payg.job;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
import io.quarkus.scheduler.Scheduled;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
+2
-2
@@ -10,10 +10,10 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
/**
|
||||
* SHA-256 of the file's bytes. The simplest universally-applicable signature — works for every
|
||||
* content type, doesn't parse, doesn't allocate proportional to file size (fixed 64 KiB read
|
||||
|
||||
+2
-2
@@ -10,12 +10,12 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import io.quarkus.arc.All;
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.saas.payg.job.JobArtifactHash;
|
||||
|
||||
+4
-3
@@ -4,12 +4,12 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
import io.quarkus.scheduler.Scheduled;
|
||||
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -44,7 +44,8 @@ public class LineagePruneScheduler {
|
||||
this.retention = retention;
|
||||
}
|
||||
|
||||
// TODO: Migration required - Spring 6-field cron "0 0 * * * *" (top of every hour) translated to
|
||||
// TODO: Migration required - Spring 6-field cron "0 0 * * * *" (top of every hour) translated
|
||||
// to
|
||||
// Quartz cron "0 0 * ? * *" (day-of-month set to ? per Quartz day-of-week/day-of-month
|
||||
// mutual-exclusion). Configurability is preserved via the {payg.lineage.prune-cron} config
|
||||
// expression; set that property to a Quartz-syntax cron (default below) to override.
|
||||
|
||||
+45
-47
@@ -5,16 +5,15 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.event.Event;
|
||||
import jakarta.enterprise.event.Observes;
|
||||
import jakarta.enterprise.event.TransactionPhase;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.saas.payg.repository.PaygTeamExtensionsRepository;
|
||||
@@ -41,7 +40,7 @@ import stirling.software.saas.payg.repository.PricingPolicyRepository;
|
||||
* read-only. We accept this rather than wrapping in a DTO to keep the PR small; if mutation becomes
|
||||
* a footgun, swap the cache value type for an immutable snapshot.
|
||||
*/
|
||||
@Service
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class PricingPolicyService {
|
||||
|
||||
@@ -50,7 +49,7 @@ public class PricingPolicyService {
|
||||
|
||||
private final PricingPolicyRepository policyRepository;
|
||||
private final PaygTeamExtensionsRepository teamExtensionsRepository;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
private final Event<PolicyChangedEvent> eventPublisher;
|
||||
|
||||
/**
|
||||
* Cache keyed by {@code teamId}. Null teamId not supported (caller's bug). Value is the
|
||||
@@ -61,7 +60,7 @@ public class PricingPolicyService {
|
||||
public PricingPolicyService(
|
||||
PricingPolicyRepository policyRepository,
|
||||
PaygTeamExtensionsRepository teamExtensionsRepository,
|
||||
ApplicationEventPublisher eventPublisher) {
|
||||
Event<PolicyChangedEvent> eventPublisher) {
|
||||
this.policyRepository = Objects.requireNonNull(policyRepository, "policyRepository");
|
||||
this.teamExtensionsRepository =
|
||||
Objects.requireNonNull(teamExtensionsRepository, "teamExtensionsRepository");
|
||||
@@ -87,7 +86,7 @@ public class PricingPolicyService {
|
||||
* <p>{@link Transactional}({@code readOnly = true}) so the eager-loaded {@code stepLimits} and
|
||||
* {@code stripePriceIds} collections initialize inside the same session.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
@Transactional
|
||||
public PricingPolicy getEffectivePolicy(Long teamId) {
|
||||
if (teamId == null) {
|
||||
return loadDefaultPolicy();
|
||||
@@ -96,7 +95,7 @@ public class PricingPolicyService {
|
||||
}
|
||||
|
||||
/** Bypasses the cache. Useful for admin endpoints that want a fresh read after a mutation. */
|
||||
@Transactional(readOnly = true)
|
||||
@Transactional
|
||||
public PricingPolicy getEffectivePolicyUncached(Long teamId) {
|
||||
if (teamId == null) {
|
||||
return loadDefaultPolicy();
|
||||
@@ -105,19 +104,19 @@ public class PricingPolicyService {
|
||||
}
|
||||
|
||||
/** Lists every policy (admin read). Not cached — admin pages should always see fresh state. */
|
||||
@Transactional(readOnly = true)
|
||||
@Transactional
|
||||
public List<PricingPolicy> listAll() {
|
||||
return policyRepository.findAll();
|
||||
return policyRepository.listAll();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Transactional
|
||||
public Optional<PricingPolicy> findByVersion(String version) {
|
||||
return policyRepository.findByVersion(version);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Transactional
|
||||
public Optional<PricingPolicy> findById(Long policyId) {
|
||||
return policyRepository.findById(policyId);
|
||||
return policyRepository.findByIdOptional(policyId);
|
||||
}
|
||||
|
||||
/** Creates a new policy row. Publishes {@link PolicyChangedEvent} after commit. */
|
||||
@@ -136,9 +135,9 @@ public class PricingPolicyService {
|
||||
"Create with is_default=true is not allowed; create the row then call"
|
||||
+ " setDefault(id).");
|
||||
}
|
||||
PricingPolicy saved = policyRepository.save(draft);
|
||||
publishOnCommit("create:" + saved.getId());
|
||||
return saved;
|
||||
policyRepository.persist(draft);
|
||||
publishOnCommit("create:" + draft.getId());
|
||||
return draft;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,7 +150,7 @@ public class PricingPolicyService {
|
||||
Objects.requireNonNull(newDefaultId, "newDefaultId");
|
||||
PricingPolicy target =
|
||||
policyRepository
|
||||
.findById(newDefaultId)
|
||||
.findByIdOptional(newDefaultId)
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new IllegalArgumentException(
|
||||
@@ -161,9 +160,9 @@ public class PricingPolicyService {
|
||||
}
|
||||
policyRepository.clearDefaultFlag();
|
||||
target.setIsDefault(true);
|
||||
PricingPolicy saved = policyRepository.save(target);
|
||||
publishOnCommit("setDefault:" + saved.getId());
|
||||
return saved;
|
||||
policyRepository.persist(target);
|
||||
publishOnCommit("setDefault:" + target.getId());
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,12 +172,12 @@ public class PricingPolicyService {
|
||||
@Transactional
|
||||
public void setTeamOverride(Long teamId, Long policyId) {
|
||||
Objects.requireNonNull(teamId, "teamId");
|
||||
if (policyId != null && !policyRepository.existsById(policyId)) {
|
||||
if (policyId != null && policyRepository.findByIdOptional(policyId).isEmpty()) {
|
||||
throw new IllegalArgumentException("No pricing_policy with id " + policyId);
|
||||
}
|
||||
PaygTeamExtensions extensions =
|
||||
teamExtensionsRepository
|
||||
.findById(teamId)
|
||||
.findByIdOptional(teamId)
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new IllegalStateException(
|
||||
@@ -187,7 +186,7 @@ public class PricingPolicyService {
|
||||
+ " — should have been created on first"
|
||||
+ " PAYG access."));
|
||||
extensions.setPricingPolicyId(policyId);
|
||||
teamExtensionsRepository.save(extensions);
|
||||
teamExtensionsRepository.persist(extensions);
|
||||
publishOnCommit("teamOverride:" + teamId);
|
||||
}
|
||||
|
||||
@@ -196,8 +195,13 @@ public class PricingPolicyService {
|
||||
* changed — cache hit rate is already team-scoped so the cost of a clear is bounded by how many
|
||||
* active teams there are.
|
||||
*/
|
||||
@EventListener
|
||||
public void onPolicyChanged(PolicyChangedEvent event) {
|
||||
// TODO: Migration required - after-commit delivery is now expressed on the observer side via
|
||||
// CDI's TransactionPhase.AFTER_SUCCESS (replacing the firing-side
|
||||
// TransactionSynchronizationManager.registerSynchronization afterCommit hook). When no
|
||||
// transaction is active (e.g. test paths calling write methods without a tx), CDI delivers the
|
||||
// event immediately, matching the former else-branch behavior.
|
||||
public void onPolicyChanged(
|
||||
@Observes(during = TransactionPhase.AFTER_SUCCESS) PolicyChangedEvent event) {
|
||||
long evicted = byTeamCache.estimatedSize();
|
||||
byTeamCache.invalidateAll();
|
||||
log.debug(
|
||||
@@ -212,34 +216,28 @@ public class PricingPolicyService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a {@link PolicyChangedEvent} to fire after the current transaction commits, or
|
||||
* fires immediately if no transaction is active (e.g. test paths calling write methods without
|
||||
* a tx). Inside-transaction firing would have listeners clearing caches before the row change
|
||||
* is visible to other connections — racing them into re-reading stale state.
|
||||
* Fires a {@link PolicyChangedEvent}. The after-commit timing that the Spring implementation
|
||||
* achieved with {@code TransactionSynchronizationManager.registerSynchronization(afterCommit)}
|
||||
* is now expressed on the observer side: {@link #onPolicyChanged} observes {@code during =
|
||||
* TransactionPhase.AFTER_SUCCESS}, so CDI defers delivery until the active transaction commits
|
||||
* (and delivers immediately when no transaction is active). Firing inside the tx is therefore
|
||||
* safe — the listener still only clears caches once the row change is committed and visible.
|
||||
*/
|
||||
private void publishOnCommit(String payload) {
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.registerSynchronization(
|
||||
new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
eventPublisher.publishEvent(
|
||||
new PolicyChangedEvent(PricingPolicyService.this, payload));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
eventPublisher.publishEvent(new PolicyChangedEvent(this, payload));
|
||||
}
|
||||
// TODO: Migration required - was TransactionSynchronizationManager-driven after-commit
|
||||
// dispatch; now a plain Event.fire() whose after-commit timing is enforced by the
|
||||
// AFTER_SUCCESS observer phase on onPolicyChanged.
|
||||
eventPublisher.fire(new PolicyChangedEvent(this, payload));
|
||||
}
|
||||
|
||||
private PricingPolicy loadEffectivePolicy(Long teamId) {
|
||||
Optional<Long> overrideId =
|
||||
teamExtensionsRepository
|
||||
.findById(teamId)
|
||||
.findByIdOptional(teamId)
|
||||
.map(PaygTeamExtensions::getPricingPolicyId);
|
||||
if (overrideId.isPresent()) {
|
||||
Long id = overrideId.get();
|
||||
Optional<PricingPolicy> override = policyRepository.findById(id);
|
||||
Optional<PricingPolicy> override = policyRepository.findByIdOptional(id);
|
||||
if (override.isPresent()) {
|
||||
return override.get();
|
||||
}
|
||||
|
||||
+9
-10
@@ -3,7 +3,11 @@ package stirling.software.saas.payg.policy.admin;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
@@ -16,12 +20,6 @@ import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -88,7 +86,9 @@ public class PricingPolicyAdminController {
|
||||
.entity(PolicyResponse.from(saved))
|
||||
.build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity(error(e.getMessage())).build();
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(error(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,8 +118,7 @@ public class PricingPolicyAdminController {
|
||||
description =
|
||||
"Payload {policyId: <id>} sets the override; {policyId: null} clears it"
|
||||
+ " (team falls back to default).")
|
||||
public Response setTeamOverride(
|
||||
@PathParam("teamId") Long teamId, TeamOverrideRequest req) {
|
||||
public Response setTeamOverride(@PathParam("teamId") Long teamId, TeamOverrideRequest req) {
|
||||
try {
|
||||
policyService.setTeamOverride(teamId, req == null ? null : req.policyId());
|
||||
return Response.noContent().build();
|
||||
|
||||
+6
-7
@@ -1,5 +1,11 @@
|
||||
package stirling.software.saas.payg.test;
|
||||
|
||||
import org.jboss.resteasy.reactive.RestForm;
|
||||
import org.jboss.resteasy.reactive.multipart.FileUpload;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.POST;
|
||||
@@ -7,13 +13,6 @@ import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
import org.jboss.resteasy.reactive.RestForm;
|
||||
import org.jboss.resteasy.reactive.multipart.FileUpload;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
|
||||
+38
-12
@@ -56,9 +56,9 @@ import stirling.software.saas.util.LogRedactionUtils;
|
||||
* Quarkus OIDC/auth processing. The {@code doFilterInternal}/{@code shouldNotFilter} servlet
|
||||
* signatures are retained here; the request/response handling and entry-point error path
|
||||
* (previously Spring's {@code BearerTokenAuthenticationEntryPoint}) must be reattached during that
|
||||
* conversion. JWT decoding/validation (previously Spring {@code JwtDecoder}/{@code NimbusJwtDecoder})
|
||||
* must move to Quarkus OIDC; the {@link JwtDecoder} functional interface below is a placeholder so
|
||||
* the decode call site keeps compiling.
|
||||
* conversion. JWT decoding/validation (previously Spring {@code JwtDecoder}/{@code
|
||||
* NimbusJwtDecoder}) must move to Quarkus OIDC; the {@link JwtDecoder} functional interface below
|
||||
* is a placeholder so the decode call site keeps compiling.
|
||||
*/
|
||||
@Slf4j
|
||||
public class SupabaseAuthenticationFilter {
|
||||
@@ -135,8 +135,10 @@ public class SupabaseAuthenticationFilter {
|
||||
processJwtAuthentication(request);
|
||||
} catch (AuthenticationException e) {
|
||||
SecurityContextHolder.clearContext();
|
||||
// TODO: Migration required - was authenticationEntryPoint.commence(request, response, e)
|
||||
// (Spring BearerTokenAuthenticationEntryPoint). Emit the 401 challenge response here when
|
||||
// TODO: Migration required - was authenticationEntryPoint.commence(request, response,
|
||||
// e)
|
||||
// (Spring BearerTokenAuthenticationEntryPoint). Emit the 401 challenge response here
|
||||
// when
|
||||
// converting to a JAX-RS @Provider filter.
|
||||
log.debug("JWT authentication failed: {}", e.getMessage());
|
||||
return;
|
||||
@@ -181,7 +183,16 @@ public class SupabaseAuthenticationFilter {
|
||||
|
||||
EnhancedJwtAuthenticationToken authToken =
|
||||
new EnhancedJwtAuthenticationToken(
|
||||
jwt, user.getAuthorities(), user.getUsername(), supabaseId);
|
||||
jwt,
|
||||
user.getAuthorities().stream()
|
||||
.map(
|
||||
a ->
|
||||
new stirling.software.common.security
|
||||
.SimpleGrantedAuthority(
|
||||
a.getAuthority()))
|
||||
.collect(java.util.stream.Collectors.toSet()),
|
||||
user.getUsername(),
|
||||
supabaseId);
|
||||
SecurityContextHolder.getContext().setAuthentication(authToken);
|
||||
|
||||
// Hot path: runs on every authenticated request (>10 per page on a typical SPA),
|
||||
@@ -290,7 +301,8 @@ public class SupabaseAuthenticationFilter {
|
||||
try {
|
||||
return userService.saveUser(user);
|
||||
} catch (PersistenceException e) {
|
||||
// TODO: Migration required - was Spring's DataIntegrityViolationException (email-collision
|
||||
// TODO: Migration required - was Spring's DataIntegrityViolationException
|
||||
// (email-collision
|
||||
// race). jakarta.persistence.PersistenceException is broader; narrow to the
|
||||
// Hibernate/JPA constraint-violation type once the persistence layer is finalized.
|
||||
log.warn(
|
||||
@@ -384,7 +396,8 @@ public class SupabaseAuthenticationFilter {
|
||||
supabaseUserService.createSupabaseUser(supabaseId, isAnon ? null : email, isAnon);
|
||||
} catch (PersistenceException ignored) {
|
||||
// Concurrent creation; fall through, the row exists.
|
||||
// TODO: Migration required - was Spring's DataIntegrityViolationException. Narrow to the
|
||||
// TODO: Migration required - was Spring's DataIntegrityViolationException. Narrow to
|
||||
// the
|
||||
// Hibernate/JPA constraint-violation type once the persistence layer is finalized.
|
||||
} catch (Exception e) {
|
||||
throw new AuthenticationFailureException("Failed to create SupabaseUser", e);
|
||||
@@ -396,7 +409,8 @@ public class SupabaseAuthenticationFilter {
|
||||
savedUser = userService.saveUser(newUser);
|
||||
} catch (PersistenceException dup) {
|
||||
// Parallel filter won the race; fetch the winning row.
|
||||
// TODO: Migration required - was Spring's DataIntegrityViolationException. Narrow to the
|
||||
// TODO: Migration required - was Spring's DataIntegrityViolationException. Narrow to
|
||||
// the
|
||||
// Hibernate/JPA constraint-violation type once the persistence layer is finalized.
|
||||
weCreatedThisUser = false;
|
||||
savedUser =
|
||||
@@ -453,8 +467,19 @@ public class SupabaseAuthenticationFilter {
|
||||
|
||||
userService.trackApiKeyFirstUse(user.get());
|
||||
|
||||
ApiKeyAuthenticationToken authToken =
|
||||
new ApiKeyAuthenticationToken(user.get(), apiKey, user.get().getAuthorities());
|
||||
// TODO: Migration required - ApiKeyAuthenticationToken is a plain POJO that does not
|
||||
// implement the Authentication shim. Wrap the principal/credentials/authorities in a
|
||||
// UsernamePasswordAuthenticationToken (which does) so it can be set on the SecurityContext.
|
||||
// Re-wire to a Quarkus SecurityIdentity when the API-key auth path is migrated.
|
||||
java.util.Collection<stirling.software.common.security.GrantedAuthority> mappedAuthorities =
|
||||
user.get().getAuthorities().stream()
|
||||
.map(
|
||||
a ->
|
||||
new stirling.software.common.security.SimpleGrantedAuthority(
|
||||
a.getAuthority()))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
UsernamePasswordAuthenticationToken authToken =
|
||||
new UsernamePasswordAuthenticationToken(user.get(), apiKey, mappedAuthorities);
|
||||
SecurityContextHolder.getContext().setAuthentication(authToken);
|
||||
return true;
|
||||
}
|
||||
@@ -466,7 +491,8 @@ public class SupabaseAuthenticationFilter {
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// TODO: Migration required - claim accessor adapters. Spring's Jwt exposed typed claim getters
|
||||
// (getClaimAsString/getClaimAsStringList/getClaimAsInstant/getClaimAsBoolean). MicroProfile
|
||||
// JsonWebToken only exposes a generic getClaim(name); these helpers reproduce the original typed
|
||||
// JsonWebToken only exposes a generic getClaim(name); these helpers reproduce the original
|
||||
// typed
|
||||
// semantics so the validation/user-creation logic is preserved unchanged.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -34,9 +34,9 @@ import stirling.software.saas.service.SupabaseUserService;
|
||||
* <p>// TODO: Migration required - this class was a Spring {@code @Configuration} with
|
||||
* {@code @EnableWebSecurity}, {@code @EnableMethodSecurity}, {@code @Profile("saas")} and
|
||||
* {@code @Order(1)}. The {@code SecurityFilterChain} bean (CSRF/CORS/session/oauth2ResourceServer
|
||||
* wiring) has no Quarkus equivalent and must be re-expressed declaratively via
|
||||
* {@code quarkus.http.auth.*} config plus Quarkus OIDC/SmallRye-JWT. The {@code SecurityFilterChain}
|
||||
* bean method has been removed; the JWKS issuer/audience/clock-skew validation logic and the
|
||||
* wiring) has no Quarkus equivalent and must be re-expressed declaratively via {@code
|
||||
* quarkus.http.auth.*} config plus Quarkus OIDC/SmallRye-JWT. The {@code SecurityFilterChain} bean
|
||||
* method has been removed; the JWKS issuer/audience/clock-skew validation logic and the
|
||||
* CORS/authority-mapping helpers are retained below so the policy can be re-applied during native
|
||||
* security wiring. The {@code JwtDecoder}/{@code NimbusJwtDecoder}/{@code OAuth2TokenValidator}
|
||||
* Spring OAuth2 resource-server types are not available; the token decoding/validation must move to
|
||||
@@ -65,16 +65,20 @@ public class SupabaseSecurityConfig {
|
||||
@ConfigProperty(name = "app.supabase.clock-skew-seconds", defaultValue = "120")
|
||||
long clockSkewSeconds;
|
||||
|
||||
// TODO: Migration required - the original @Bean SecurityFilterChain saasSecurityFilterChain(...)
|
||||
// TODO: Migration required - the original @Bean SecurityFilterChain
|
||||
// saasSecurityFilterChain(...)
|
||||
// configured CSRF-disabled, CORS, STATELESS sessions, permitAll matchers for
|
||||
// OPTIONS/actuator-health/config/static/public-auth/frontend routes, anyRequest().authenticated(),
|
||||
// OPTIONS/actuator-health/config/static/public-auth/frontend routes,
|
||||
// anyRequest().authenticated(),
|
||||
// registered SupabaseAuthenticationFilter before BearerTokenAuthenticationFilter, set a
|
||||
// BearerTokenAuthenticationEntryPoint + BearerTokenAccessDeniedHandler, and wired
|
||||
// oauth2ResourceServer().jwt() with this JwtDecoder and SupabaseSecurityConfig::toAuthentication.
|
||||
// oauth2ResourceServer().jwt() with this JwtDecoder and
|
||||
// SupabaseSecurityConfig::toAuthentication.
|
||||
// Re-express this via quarkus.http.auth.permission.* + quarkus.http.cors.* config and Quarkus
|
||||
// OIDC (mp.jwt). SupabaseAuthenticationFilter must be registered as a JAX-RS @Provider filter.
|
||||
|
||||
// TODO: Migration required - original @Bean JwtDecoder jwtDecoder() built a NimbusJwtDecoder from
|
||||
// TODO: Migration required - original @Bean JwtDecoder jwtDecoder() built a NimbusJwtDecoder
|
||||
// from
|
||||
// the Supabase JWKS endpoint (issuer + "/.well-known/jwks.json") and attached a
|
||||
// SupabaseTokenValidator (iss/exp/aud enforcement with clock skew), failing closed when the
|
||||
// issuer was unusable. NimbusJwtDecoder / JwtDecoder are Spring OAuth2 types with no Quarkus
|
||||
|
||||
@@ -8,10 +8,9 @@ import jakarta.inject.Named;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.model.enumeration.TeamRole;
|
||||
import stirling.software.common.security.Authentication;
|
||||
import stirling.software.common.security.SecurityContextHolder;
|
||||
|
||||
import stirling.software.common.model.enumeration.TeamRole;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
import stirling.software.saas.repository.TeamMembershipRepository;
|
||||
|
||||
+2
-2
@@ -69,7 +69,7 @@ public class AnonymousUserCleanupService {
|
||||
|
||||
idStream.collect(Collectors.groupingBy(id -> counter.getAndIncrement() / batchSize))
|
||||
.values()
|
||||
.forEach(supabaseUserRepository::deleteAllByIdInBatch);
|
||||
.forEach(batch -> supabaseUserRepository.delete("id in ?1", batch));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ public class AnonymousUserCleanupService {
|
||||
|
||||
idStream.collect(Collectors.groupingBy(id -> counter.getAndIncrement() / batchSize))
|
||||
.values()
|
||||
.forEach(userRepository::deleteAllByIdInBatch);
|
||||
.forEach(batch -> userRepository.delete("id in ?1", batch));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ import stirling.software.proprietary.security.model.User;
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@IfBuildProfile("saas")
|
||||
// TODO: Migration required - @ConditionalOnProperty(credits.enabled, matchIfMissing=true) gating not
|
||||
// TODO: Migration required - @ConditionalOnProperty(credits.enabled, matchIfMissing=true) gating
|
||||
// not
|
||||
// translated; backfill always runs under the saas profile
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
|
||||
@@ -17,15 +17,14 @@ import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.transaction.TransactionSynchronizationRegistry;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.security.Authentication;
|
||||
import stirling.software.common.security.SecurityContextHolder;
|
||||
import stirling.software.common.security.UsernamePasswordAuthenticationToken;
|
||||
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
import stirling.software.proprietary.security.model.ApiKeyAuthenticationToken;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
|
||||
@@ -4,15 +4,15 @@ import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
import io.quarkus.scheduler.Scheduled;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
|
||||
@@ -611,9 +611,7 @@ public class SaasTeamService {
|
||||
.uri(java.net.URI.create(url))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", "Bearer " + edgeFunctionSecret)
|
||||
.POST(
|
||||
java.net.http.HttpRequest.BodyPublishers.ofString(
|
||||
requestJson))
|
||||
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(requestJson))
|
||||
.build();
|
||||
|
||||
java.net.http.HttpClient.newHttpClient()
|
||||
|
||||
Reference in New Issue
Block a user