diff --git a/app/common/src/test/java/stirling/software/common/service/FileStorageDelegationTest.java b/app/common/src/test/java/stirling/software/common/service/FileStorageDelegationTest.java index b013a26927..ad6eb43285 100644 --- a/app/common/src/test/java/stirling/software/common/service/FileStorageDelegationTest.java +++ b/app/common/src/test/java/stirling/software/common/service/FileStorageDelegationTest.java @@ -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 (was Optional). Mock one that + // is + // not resolvable, mirroring the previous Optional.empty() (desktop / no-security mode). + Instance 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)); diff --git a/app/common/src/test/java/stirling/software/common/service/FileStorageOwnershipTest.java b/app/common/src/test/java/stirling/software/common/service/FileStorageOwnershipTest.java index 861efa8509..6c7c7b9b01 100644 --- a/app/common/src/test/java/stirling/software/common/service/FileStorageOwnershipTest.java +++ b/app/common/src/test/java/stirling/software/common/service/FileStorageOwnershipTest.java @@ -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 (was Optional). Build a mock + // Instance + // whose isResolvable()/get() mirror the previous Optional.empty()/Optional.of(svc) contract. + @SuppressWarnings("unchecked") + private static Instance instanceOf(JobOwnershipService svc) { + Instance 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 userRef) { @@ -31,7 +44,7 @@ class FileStorageOwnershipTest { return new FileStorage( mock(FileOrUploadService.class), new LocalDiskFileStore(tempDir.toString()), - Optional.of(svc)); + instanceOf(svc)); } @Test diff --git a/app/saas/src/main/java/stirling/software/saas/ai/controller/AiCreateController.java b/app/saas/src/main/java/stirling/software/saas/ai/controller/AiCreateController.java index b66c7526d1..ea47bd7493 100644 --- a/app/saas/src/main/java/stirling/software/saas/ai/controller/AiCreateController.java +++ b/app/saas/src/main/java/stirling/software/saas/ai/controller/AiCreateController.java @@ -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 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(); } diff --git a/app/saas/src/main/java/stirling/software/saas/ai/controller/AiCreateInternalController.java b/app/saas/src/main/java/stirling/software/saas/ai/controller/AiCreateInternalController.java index 2806b125e9..cd87987a89 100644 --- a/app/saas/src/main/java/stirling/software/saas/ai/controller/AiCreateInternalController.java +++ b/app/saas/src/main/java/stirling/software/saas/ai/controller/AiCreateInternalController.java @@ -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; diff --git a/app/saas/src/main/java/stirling/software/saas/ai/controller/AiProxyController.java b/app/saas/src/main/java/stirling/software/saas/ai/controller/AiProxyController.java index 59f205cab3..37a0c06ff4 100644 --- a/app/saas/src/main/java/stirling/software/saas/ai/controller/AiProxyController.java +++ b/app/saas/src/main/java/stirling/software/saas/ai/controller/AiProxyController.java @@ -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"); diff --git a/app/saas/src/main/java/stirling/software/saas/ai/repository/AiCreateSessionRepository.java b/app/saas/src/main/java/stirling/software/saas/ai/repository/AiCreateSessionRepository.java index 7c2d7b5942..f057284ae1 100644 --- a/app/saas/src/main/java/stirling/software/saas/ai/repository/AiCreateSessionRepository.java +++ b/app/saas/src/main/java/stirling/software/saas/ai/repository/AiCreateSessionRepository.java @@ -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 { + /** + * 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 findByUserIdOrderByUpdatedAtDesc(String userId) { return find("userId = ?1 ORDER BY updatedAt DESC", userId).list(); } @@ -25,4 +39,97 @@ public class AiCreateSessionRepository implements PanacheRepositoryBase findByUserIdOrderByUpdatedAtDesc( + String userId, int page, int size) { + return find("userId = ?1 ORDER BY updatedAt DESC", userId).page(page, size).list(); + } + + public List 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 findSummariesByUserIdOrderByUpdatedAtDesc( + String userId, int page, int size) { + return findByUserIdOrderByUpdatedAtDesc(userId, page, size).stream() + .map(AiCreateSessionSummaryProjection::of) + .toList(); + } + + public List + 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; + } + } } diff --git a/app/saas/src/main/java/stirling/software/saas/ai/service/AiCreateProxyService.java b/app/saas/src/main/java/stirling/software/saas/ai/service/AiCreateProxyService.java index eddd821fcd..a6f0c49337 100644 --- a/app/saas/src/main/java/stirling/software/saas/ai/service/AiCreateProxyService.java +++ b/app/saas/src/main/java/stirling/software/saas/ai/service/AiCreateProxyService.java @@ -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; diff --git a/app/saas/src/main/java/stirling/software/saas/ai/service/AiCreateSessionService.java b/app/saas/src/main/java/stirling/software/saas/ai/service/AiCreateSessionService.java index 06922d79d9..76f3591b05 100644 --- a/app/saas/src/main/java/stirling/software/saas/ai/service/AiCreateSessionService.java +++ b/app/saas/src/main/java/stirling/software/saas/ai/service/AiCreateSessionService.java @@ -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 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 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 listSessionsForCurrentUser(Pageable pageable) { + public List listSessionsForCurrentUser(int page, int size) { String userId = resolveUserId(); - return repository.findByUserIdOrderByUpdatedAtDesc(userId, pageable); + return repository.findByUserIdOrderByUpdatedAtDesc(userId, page, size); } public List 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 - 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; } diff --git a/app/saas/src/main/java/stirling/software/saas/ai/service/AiProxyService.java b/app/saas/src/main/java/stirling/software/saas/ai/service/AiProxyService.java index 0b1b405f27..035d0d9cee 100644 --- a/app/saas/src/main/java/stirling/software/saas/ai/service/AiProxyService.java +++ b/app/saas/src/main/java/stirling/software/saas/ai/service/AiProxyService.java @@ -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; diff --git a/app/saas/src/main/java/stirling/software/saas/config/CreditInterceptorConfig.java b/app/saas/src/main/java/stirling/software/saas/config/CreditInterceptorConfig.java index 0c1c55dec5..57f7e2218c 100644 --- a/app/saas/src/main/java/stirling/software/saas/config/CreditInterceptorConfig.java +++ b/app/saas/src/main/java/stirling/software/saas/config/CreditInterceptorConfig.java @@ -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; diff --git a/app/saas/src/main/java/stirling/software/saas/config/CreditsProperties.java b/app/saas/src/main/java/stirling/software/saas/config/CreditsProperties.java index 0583141d16..b096cb3b5d 100644 --- a/app/saas/src/main/java/stirling/software/saas/config/CreditsProperties.java +++ b/app/saas/src/main/java/stirling/software/saas/config/CreditsProperties.java @@ -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") diff --git a/app/saas/src/main/java/stirling/software/saas/config/SaasDataSourceConfig.java b/app/saas/src/main/java/stirling/software/saas/config/SaasDataSourceConfig.java index 40db7516f7..6d43c9a5d8 100644 --- a/app/saas/src/main/java/stirling/software/saas/config/SaasDataSourceConfig.java +++ b/app/saas/src/main/java/stirling/software/saas/config/SaasDataSourceConfig.java @@ -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; /** diff --git a/app/saas/src/main/java/stirling/software/saas/config/SaasJpaConfig.java b/app/saas/src/main/java/stirling/software/saas/config/SaasJpaConfig.java index 0f45f795e1..2d95e597ec 100644 --- a/app/saas/src/main/java/stirling/software/saas/config/SaasJpaConfig.java +++ b/app/saas/src/main/java/stirling/software/saas/config/SaasJpaConfig.java @@ -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. * *

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") diff --git a/app/saas/src/main/java/stirling/software/saas/config/SaasLicenseOverride.java b/app/saas/src/main/java/stirling/software/saas/config/SaasLicenseOverride.java index f025f50be8..2d7c1079d0 100644 --- a/app/saas/src/main/java/stirling/software/saas/config/SaasLicenseOverride.java +++ b/app/saas/src/main/java/stirling/software/saas/config/SaasLicenseOverride.java @@ -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") diff --git a/app/saas/src/main/java/stirling/software/saas/config/SaasRestTemplateConfig.java b/app/saas/src/main/java/stirling/software/saas/config/SaasRestTemplateConfig.java index fbb0160143..78a553f137 100644 --- a/app/saas/src/main/java/stirling/software/saas/config/SaasRestTemplateConfig.java +++ b/app/saas/src/main/java/stirling/software/saas/config/SaasRestTemplateConfig.java @@ -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. * - *

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}. + *

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") diff --git a/app/saas/src/main/java/stirling/software/saas/config/SupabaseConfigurationProperties.java b/app/saas/src/main/java/stirling/software/saas/config/SupabaseConfigurationProperties.java index 8c9a120af1..fa1533a2a2 100644 --- a/app/saas/src/main/java/stirling/software/saas/config/SupabaseConfigurationProperties.java +++ b/app/saas/src/main/java/stirling/software/saas/config/SupabaseConfigurationProperties.java @@ -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 diff --git a/app/saas/src/main/java/stirling/software/saas/controller/CreditController.java b/app/saas/src/main/java/stirling/software/saas/controller/CreditController.java index 05d86aaae9..6c0479015f 100644 --- a/app/saas/src/main/java/stirling/software/saas/controller/CreditController.java +++ b/app/saas/src/main/java/stirling/software/saas/controller/CreditController.java @@ -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; diff --git a/app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java b/app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java index 07cb5be4fb..38578ce346 100644 --- a/app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java +++ b/app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java @@ -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 memberships = membershipRepository.findByTeamId(teamId); diff --git a/app/saas/src/main/java/stirling/software/saas/controller/UserRoleWebhookController.java b/app/saas/src/main/java/stirling/software/saas/controller/UserRoleWebhookController.java index 18ed15d3f8..80168247d9 100644 --- a/app/saas/src/main/java/stirling/software/saas/controller/UserRoleWebhookController.java +++ b/app/saas/src/main/java/stirling/software/saas/controller/UserRoleWebhookController.java @@ -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; diff --git a/app/saas/src/main/java/stirling/software/saas/interceptor/CreditErrorAdvice.java b/app/saas/src/main/java/stirling/software/saas/interceptor/CreditErrorAdvice.java index 41f700e1d7..f96c3d31b1 100644 --- a/app/saas/src/main/java/stirling/software/saas/interceptor/CreditErrorAdvice.java +++ b/app/saas/src/main/java/stirling/software/saas/interceptor/CreditErrorAdvice.java @@ -37,10 +37,10 @@ import stirling.software.saas.util.CreditHeaderUtils; *

// 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} (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} (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.toResponse(Throwable); HttpServletRequest access must be + // Convert to ExceptionMapper.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) { diff --git a/app/saas/src/main/java/stirling/software/saas/interceptor/CreditSuccessAdvice.java b/app/saas/src/main/java/stirling/software/saas/interceptor/CreditSuccessAdvice.java index 2226d4d1d0..f57d81cea6 100644 --- a/app/saas/src/main/java/stirling/software/saas/interceptor/CreditSuccessAdvice.java +++ b/app/saas/src/main/java/stirling/software/saas/interceptor/CreditSuccessAdvice.java @@ -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>, ServerHttpRequest, ServerHttpResponse). // Re-wire as ContainerResponseFilter.filter(ContainerRequestContext, ContainerResponseContext). // The original returned `body` unchanged; this side-effects credit consumption + response diff --git a/app/saas/src/main/java/stirling/software/saas/interceptor/UnifiedCreditInterceptor.java b/app/saas/src/main/java/stirling/software/saas/interceptor/UnifiedCreditInterceptor.java index 4d1b166c4d..2df1d5e1d9 100644 --- a/app/saas/src/main/java/stirling/software/saas/interceptor/UnifiedCreditInterceptor.java +++ b/app/saas/src/main/java/stirling/software/saas/interceptor/UnifiedCreditInterceptor.java @@ -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) { diff --git a/app/saas/src/main/java/stirling/software/saas/payg/charge/JobChargeService.java b/app/saas/src/main/java/stirling/software/saas/payg/charge/JobChargeService.java index 5ae8b7a998..3a0283498a 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/charge/JobChargeService.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/charge/JobChargeService.java @@ -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; diff --git a/app/saas/src/main/java/stirling/software/saas/payg/docs/DefaultDocumentClassifier.java b/app/saas/src/main/java/stirling/software/saas/payg/docs/DefaultDocumentClassifier.java index b336e4382a..69928f815e 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/docs/DefaultDocumentClassifier.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/docs/DefaultDocumentClassifier.java @@ -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; diff --git a/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java b/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java index 04452b6c30..ac4bb9a89d 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java @@ -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}). * *

// 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 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) { diff --git a/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygFilterProperties.java b/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygFilterProperties.java index c8456aa78e..293d97ee0b 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygFilterProperties.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygFilterProperties.java @@ -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. * */ -// 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 diff --git a/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygOutputExtractor.java b/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygOutputExtractor.java index e413d6c73d..c8ca1ce961 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygOutputExtractor.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygOutputExtractor.java @@ -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. */ diff --git a/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygResponseBodyWrapperFilter.java b/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygResponseBodyWrapperFilter.java index 8c6876fb63..246334c47d 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygResponseBodyWrapperFilter.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygResponseBodyWrapperFilter.java @@ -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 { diff --git a/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygWebMvcConfig.java b/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygWebMvcConfig.java index 14687b54ec..b3ff757838 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygWebMvcConfig.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/filter/PaygWebMvcConfig.java @@ -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; * */ // 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 diff --git a/app/saas/src/main/java/stirling/software/saas/payg/job/JobService.java b/app/saas/src/main/java/stirling/software/saas/payg/job/JobService.java index 2a1dba18b9..b640e4cd1a 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/job/JobService.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/job/JobService.java @@ -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; diff --git a/app/saas/src/main/java/stirling/software/saas/payg/job/StaleJobCloser.java b/app/saas/src/main/java/stirling/software/saas/payg/job/StaleJobCloser.java index f9ecfc7187..8d07e24d7e 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/job/StaleJobCloser.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/job/StaleJobCloser.java @@ -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; diff --git a/app/saas/src/main/java/stirling/software/saas/payg/lineage/ByteHashSignatureExtractor.java b/app/saas/src/main/java/stirling/software/saas/payg/lineage/ByteHashSignatureExtractor.java index 973b15a1f5..e6ba327382 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/lineage/ByteHashSignatureExtractor.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/lineage/ByteHashSignatureExtractor.java @@ -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 diff --git a/app/saas/src/main/java/stirling/software/saas/payg/lineage/DefaultHashLineageDetector.java b/app/saas/src/main/java/stirling/software/saas/payg/lineage/DefaultHashLineageDetector.java index 5962cb5e2f..57e33e5d68 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/lineage/DefaultHashLineageDetector.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/lineage/DefaultHashLineageDetector.java @@ -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; diff --git a/app/saas/src/main/java/stirling/software/saas/payg/lineage/JpaJobLineageStore.java b/app/saas/src/main/java/stirling/software/saas/payg/lineage/JpaJobLineageStore.java index ab18a772e5..4c79830270 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/lineage/JpaJobLineageStore.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/lineage/JpaJobLineageStore.java @@ -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; diff --git a/app/saas/src/main/java/stirling/software/saas/payg/lineage/LineagePruneScheduler.java b/app/saas/src/main/java/stirling/software/saas/payg/lineage/LineagePruneScheduler.java index 8d05f82449..bd25ad1876 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/lineage/LineagePruneScheduler.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/lineage/LineagePruneScheduler.java @@ -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. diff --git a/app/saas/src/main/java/stirling/software/saas/payg/policy/PricingPolicyService.java b/app/saas/src/main/java/stirling/software/saas/payg/policy/PricingPolicyService.java index 0fd7bb45d1..3c93458b40 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/policy/PricingPolicyService.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/policy/PricingPolicyService.java @@ -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 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 eventPublisher) { this.policyRepository = Objects.requireNonNull(policyRepository, "policyRepository"); this.teamExtensionsRepository = Objects.requireNonNull(teamExtensionsRepository, "teamExtensionsRepository"); @@ -87,7 +86,7 @@ public class PricingPolicyService { *

{@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 listAll() { - return policyRepository.findAll(); + return policyRepository.listAll(); } - @Transactional(readOnly = true) + @Transactional public Optional findByVersion(String version) { return policyRepository.findByVersion(version); } - @Transactional(readOnly = true) + @Transactional public Optional 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 overrideId = teamExtensionsRepository - .findById(teamId) + .findByIdOptional(teamId) .map(PaygTeamExtensions::getPricingPolicyId); if (overrideId.isPresent()) { Long id = overrideId.get(); - Optional override = policyRepository.findById(id); + Optional override = policyRepository.findByIdOptional(id); if (override.isPresent()) { return override.get(); } diff --git a/app/saas/src/main/java/stirling/software/saas/payg/policy/admin/PricingPolicyAdminController.java b/app/saas/src/main/java/stirling/software/saas/payg/policy/admin/PricingPolicyAdminController.java index 73feaae62f..14edd151a3 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/policy/admin/PricingPolicyAdminController.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/policy/admin/PricingPolicyAdminController.java @@ -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: } 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(); diff --git a/app/saas/src/main/java/stirling/software/saas/payg/test/PaygCucumberThrowController.java b/app/saas/src/main/java/stirling/software/saas/payg/test/PaygCucumberThrowController.java index 0bc28d256c..34bd72c150 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/test/PaygCucumberThrowController.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/test/PaygCucumberThrowController.java @@ -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; diff --git a/app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java b/app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java index e036753faa..c9f9d16384 100644 --- a/app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java +++ b/app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java @@ -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 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. // --------------------------------------------------------------------------------------------- diff --git a/app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java b/app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java index c5788fc1af..f8d3215bbd 100644 --- a/app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java +++ b/app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java @@ -34,9 +34,9 @@ import stirling.software.saas.service.SupabaseUserService; *

// 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 diff --git a/app/saas/src/main/java/stirling/software/saas/security/TeamSecurityExpressions.java b/app/saas/src/main/java/stirling/software/saas/security/TeamSecurityExpressions.java index a245318d30..435530f0ec 100644 --- a/app/saas/src/main/java/stirling/software/saas/security/TeamSecurityExpressions.java +++ b/app/saas/src/main/java/stirling/software/saas/security/TeamSecurityExpressions.java @@ -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; diff --git a/app/saas/src/main/java/stirling/software/saas/service/AnonymousUserCleanupService.java b/app/saas/src/main/java/stirling/software/saas/service/AnonymousUserCleanupService.java index 6290767562..89cd1eb53f 100644 --- a/app/saas/src/main/java/stirling/software/saas/service/AnonymousUserCleanupService.java +++ b/app/saas/src/main/java/stirling/software/saas/service/AnonymousUserCleanupService.java @@ -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)); } } } diff --git a/app/saas/src/main/java/stirling/software/saas/service/CreditBackfillRunner.java b/app/saas/src/main/java/stirling/software/saas/service/CreditBackfillRunner.java index c66e9ebd0d..b6d1fee7c2 100644 --- a/app/saas/src/main/java/stirling/software/saas/service/CreditBackfillRunner.java +++ b/app/saas/src/main/java/stirling/software/saas/service/CreditBackfillRunner.java @@ -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 diff --git a/app/saas/src/main/java/stirling/software/saas/service/CreditService.java b/app/saas/src/main/java/stirling/software/saas/service/CreditService.java index 54e4d69286..bbcb15bae5 100644 --- a/app/saas/src/main/java/stirling/software/saas/service/CreditService.java +++ b/app/saas/src/main/java/stirling/software/saas/service/CreditService.java @@ -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; diff --git a/app/saas/src/main/java/stirling/software/saas/service/ErrorTrackingService.java b/app/saas/src/main/java/stirling/software/saas/service/ErrorTrackingService.java index 850a9290d0..02f39cb7d3 100644 --- a/app/saas/src/main/java/stirling/software/saas/service/ErrorTrackingService.java +++ b/app/saas/src/main/java/stirling/software/saas/service/ErrorTrackingService.java @@ -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; diff --git a/app/saas/src/main/java/stirling/software/saas/service/SaasTeamService.java b/app/saas/src/main/java/stirling/software/saas/service/SaasTeamService.java index fb9c6d11f8..6675bf426f 100644 --- a/app/saas/src/main/java/stirling/software/saas/service/SaasTeamService.java +++ b/app/saas/src/main/java/stirling/software/saas/service/SaasTeamService.java @@ -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()