Skip to content

Dev - #32

Closed
HenryHyHy23 wants to merge 157 commits into
mainfrom
dev
Closed

Dev#32
HenryHyHy23 wants to merge 157 commits into
mainfrom
dev

Conversation

@HenryHyHy23

@HenryHyHy23 HenryHyHy23 commented Jun 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Bookmark management with filtering, statistics, and notes
    • Follow system for authors, topics, and sources with status tracking
    • Admin dashboard with user management, ban/unban capabilities, and analytics
    • Enhanced search with dynamic filter options, history management, and summary data
    • Research feed with daily automated synchronization for personalized recommendations
    • Dashboard analytics displaying publication trends, metrics, and topic rankings
  • Tests

    • Admin service and auth security unit tests
  • Chores

    • Database schema migrations for new tables and constraints
    • Configuration updates and project rebranding
    • GitHub Actions workflow for Azure deployment

HenryHyHy23 and others added 30 commits June 3, 2026 14:08
fix(deploy): fix deploy setting

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
.github/workflows/prod_owlreka.yml (1)

19-62: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pin GitHub Actions to full commit SHAs (avoid tag retag risk)
.github/workflows/prod_owlreka.yml uses tag-pinned actions (actions/*@v4``, azure/login@v2, `azure/webapps-deploy@v3`) instead of full-length commit SHAs; zizmor’s unpinned-uses policy requires commit-SHA pinning, and tag retagging/upstream changes could affect the production deploy.

Suggested fix
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@<full-commit-sha>

-        uses: actions/setup-java@v4
+        uses: actions/setup-java@<full-commit-sha>

-        uses: actions/upload-artifact@v4
+        uses: actions/upload-artifact@<full-commit-sha>

-        uses: actions/download-artifact@v4
+        uses: actions/download-artifact@<full-commit-sha>

-        uses: azure/login@v2
+        uses: azure/login@<full-commit-sha>

-        uses: azure/webapps-deploy@v3
+        uses: azure/webapps-deploy@<full-commit-sha>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/prod_owlreka.yml around lines 19 - 62, The workflow
currently pins third-party actions by tag (e.g., actions/checkout@v4,
actions/setup-java@v4, actions/upload-artifact@v4, actions/download-artifact@v4,
azure/login@v2, azure/webapps-deploy@v3); replace each tag reference with the
corresponding full commit SHA for that action (lookup the exact commit SHA from
the action's GitHub repo and update the uses: lines to e.g.
actions/checkout@<full-sha>), verify the SHAs point to the intended release
commits, and commit those updated uses entries so the workflow is pinned to
immutable SHAs.

Source: Linters/SAST tools

scipubtts/src/main/java/com/brotherhood/scipubtts/search/controller/SearchController.java (1)

155-203: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep search-history mutations authenticated.

saveSearchHistory, deleteSearchHistory, and clearSearchHistory now accept @CurrentUserUUID(required = false), so anonymous requests reach the service layer with userId == null instead of failing with 401. For these state-changing "current user" endpoints, that weakens the API contract and risks null-user writes/no-ops depending on downstream handling. Make the mutating history endpoints required again and reserve required = false for read-only flows if anonymous callers should be supported.

Suggested change
     public ResponseEntity<ResponseObject> saveSearchHistory(
-            `@Parameter`(hidden = true) `@CurrentUserUUID`(required = false) UUID userId,
+            `@Parameter`(hidden = true) `@CurrentUserUUID` UUID userId,
             `@RequestBody` SearchHistorySaveRequest request
     ) {
@@
     public ResponseEntity<ResponseObject> deleteSearchHistory(
-            `@Parameter`(hidden = true) `@CurrentUserUUID`(required = false) UUID userId,
+            `@Parameter`(hidden = true) `@CurrentUserUUID` UUID userId,
             `@Parameter`(
@@
     public ResponseEntity<ResponseObject> clearSearchHistory(
-            `@Parameter`(hidden = true) `@CurrentUserUUID`(required = false) UUID userId
+            `@Parameter`(hidden = true) `@CurrentUserUUID` UUID userId
     ) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/search/controller/SearchController.java`
around lines 155 - 203, The three mutating endpoints saveSearchHistory,
deleteSearchHistory, and clearSearchHistory should require an authenticated
user; change the `@CurrentUserUUID` annotations on these methods from required =
false back to required = true (or remove the parameter so it defaults to
required) so anonymous requests receive a 401 instead of reaching the service
with userId == null; update the annotations on the UUID parameter in
saveSearchHistory, deleteSearchHistory, and clearSearchHistory accordingly.
scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/service/MetricService.java (1)

6-13: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix the broken top-level type declarations in this file (build is currently blocked).

MetricService.java currently mixes an unclosed public interface MetricService with a public class MetricService declaration. This matches the CI parse failure and also breaks the service contract shape. Keep this file as the interface only, and place concrete implementation in a separate MetricServiceImpl class/file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/service/MetricService.java`
around lines 6 - 13, The file currently declares both an interface and a class
named MetricService which breaks compilation; keep this file as the interface
only by removing the concrete class declaration here and ensuring the interface
contains the method signature MetricsResponse
calculateAndSaveMetrics(PeriodRequest request). Move the implementation into a
new class named MetricServiceImpl (in the same package) that implements
MetricService, annotate it with `@Service` and `@RequiredArgsConstructor`, implement
the calculateAndSaveMetrics(PeriodRequest request) method body formerly in the
removed class, and add any necessary imports and constructor-injected
dependencies there; update any callers/beans to depend on the MetricService
interface (no other changes required in this file).

Source: Pipeline failures

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/prod_owlreka.yml:
- Around line 27-34: The workflow steps "Build with Maven" and "Upload artifact
for deployment job" are running from the repo root but the Maven project is
rooted at scipubtts; update both steps to run in that module: for the "Build
with Maven" step set the working-directory to the module (e.g.,
working-directory: scipubtts) and run mvn clean install there, and for the
"Upload artifact for deployment job" step set the same working-directory (or
adjust the artifact path) so path points to the module's target/*.jar (e.g.,
path: '${{ github.workspace }}/scipubtts/target/*.jar').
- Around line 21-25: The workflow uses actions/setup-java@v4 with an invalid
java-version value 'java25'; update the java-version input to a valid value
(e.g., '25' for GA or '25-ea' for Early Access) in the job step that uses
actions/setup-java so the action can resolve and install the correct JDK
distribution; keep distribution: 'microsoft' unchanged and ensure the step name
"Set up Java version" and the uses entry "actions/setup-java@v4" remain intact.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/auth/dto/request/RegisterLocalRequest.java`:
- Around line 33-34: The RegisterLocalRequest DTO exposes an unconstrained
String appBaseUrl which can enable open redirect/ token leakage when used to
construct verification links; add validation on the appBaseUrl field (e.g.,
require a well-formed absolute URI with scheme http/https, no
path/query/fragment, and max length) using javax.validation annotations or a
custom validator on RegisterLocalRequest.appBaseUrl and normalize it before use,
and enforce a server-side allowlist of permitted frontend origins (check the
normalized value against the allowlist in the registration flow where the
verification link is built) so only approved origins are accepted; update any
code that consumes RegisterLocalRequest.appBaseUrl to rely on the
validated/normalized value.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/auth/security/oauth2/OAuth2AuthenticationSuccessHandler.java`:
- Around line 38-42: Remove the duplicate field declaration of
AuthorizationRequestRepository<OAuth2AuthorizationRequest>
authorizationRequestRepository so there is only one field: initialize it with
new HttpSessionOAuth2AuthorizationRequestRepository() (keep that single
declaration) to fix the compilation/wiring issue. In the authentication success
flow (method handling OAuth2 success), remove the duplicate targetUrl variable
declaration (only declare it once) and modify the "new-user" (CASE 2) branch so
it does not re-check userRepository.findByEmail(email) and short-circuit;
instead, after generating the signup token, build the redirect URL to
"/register/complete?token=..." using the single targetUrl variable and perform
the redirect so the generated token is handed off to the client (ensure the
previously-generated token variable is used in the redirect). Ensure references
to authorizationRequestRepository and userRepository remain correct after these
changes.
- Around line 141-179: The method in OAuth2AuthenticationSuccessHandler declares
targetUrl twice and treats missing users as an error instead of redirecting them
to complete signup; fix by removing the duplicate String targetUrl declaration
and create a single targetUrl variable that is set conditionally: if
userRepository.findByEmail(email).isEmpty() then build the frontendBaseUrl +
"/register/complete" with the rawToken as a query param (and call
authorizationRequestRepository.removeAuthorizationRequest and redirect there),
otherwise call authSessionService.issueSession(userOptional.get(), false,
request, response) and set targetUrl to frontendBaseUrl + "/oauth2/success";
finally call getRedirectStrategy().sendRedirect(request, response, targetUrl).
Ensure you reference and use rawToken,
authorizationRequestRepository.removeAuthorizationRequest, redirectWithError
only where appropriate, and do not declare String targetUrl more than once.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/common/openalex/OpenAlexClient.java`:
- Around line 76-100: performGetWithRetry currently rethrows raw
RestClientResponseException/RestClientException on the final attempt, so the
terminal BusinessException(ErrorCode.OPENALEX_REQUEST_FAILED) is never reached;
change the two catch blocks in performGetWithRetry so that: (1)
RestClientResponseException still maps 404 to
BusinessException(ErrorCode.OPENALEX_ENTITY_NOT_FOUND); (2) if attempt ==
MAX_RETRIES do not rethrow the raw exception — instead throw new
BusinessException(ErrorCode.OPENALEX_REQUEST_FAILED) (optionally include or log
the original exception as cause); (3) otherwise keep the existing retry logic
(sleep/backoff) and call executeGet as before. Ensure function names referenced:
performGetWithRetry and executeGet.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/config/SecurityConfig.java`:
- Around line 47-48: The docsPublicEnabled flag is currently unused so
Swagger/OpenAPI endpoints are always permitted; update SecurityConfig to
conditionally allow or restrict docs based on docsPublicEnabled by replacing the
hard-coded antMatchers(...).permitAll() for the Swagger/OpenAPI paths with a
branch that, when docsPublicEnabled is true, calls antMatchers(...).permitAll(),
and when false either removes permitAll() or applies authenticated() (e.g.,
antMatchers(...).authenticated()), ensuring this logic is applied on the same
HttpSecurity authorization chain (the method that calls
authorizeRequests()/authorizeHttpRequests() in SecurityConfig). Use the existing
docsPublicEnabled field to drive the decision and keep the Swagger path list
unchanged.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/service/MetricService.java`:
- Around line 77-80: The calculatePercentageChange method incorrectly returns
100.0 whenever previousValue == 0; update calculatePercentageChange(long
currentValue, long previousValue) to first check if previousValue == 0 and
currentValue == 0 and return 0.0 for that no-change case, then if previousValue
== 0 and currentValue > 0 return 100.0, otherwise fall back to the existing
percentage computation (using the normal (current - previous)/previous * 100
logic).

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/search/dto/SearchWorksQueryRequest.java`:
- Around line 104-108: The DTO SearchWorksQueryRequest must continue accepting
the legacy "sort" query parameter so callers using the old format don't lose
sorting: add a nullable String field named sort (with a deprecation note/Schema)
to SearchWorksQueryRequest and update the code path that resolves sort (in
SearchWorksLookupService or wherever sorting is parsed) to check for the legacy
sort value when sortBy/sortDirection are not provided—parse legacy values into
the same internal representation (e.g., "field" or "field:direction" -> sortBy
and sortDirection) so SearchController bindings keep working for both new and
legacy callers.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/search/dto/SearchWorksResponse.java`:
- Line 32: The rename of the SearchWorksResponse record component from topicName
to topic breaks the /api/search/works JSON contract (SearchController serializes
the record directly); restore compatibility by exposing the old JSON property
name while keeping the new internal name: either revert the record component to
topicName, or (preferred) add a compatibility accessor in SearchWorksResponse
such as a public topicName() method that returns topic (or annotate the existing
topic component with `@JsonProperty`("topicName")) so Jackson emits "topicName"
alongside/for the same value; update only the SearchWorksResponse record and
ensure serialization tests still pass.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/SearchFilterBuilder.java`:
- Around line 93-99: The current mapping in SearchFilterBuilder adds exclusive
OpenAlex operators for min/max which breaks inclusive DTO semantics; update the
logic that builds filterParts (the code that currently does
filterParts.add(field + ":>" + minValue) and filterParts.add(field + ":<" +
maxValue)) so that for publication_year (yearFrom/yearTo) you produce an
inclusive hyphen range string like "publication_year:min-max" when either bound
exists (handle open-ended bounds appropriately), and for cited_by_count
(citationMin/citationMax) emulate inclusive bounds by converting them to
exclusive operators with offsets (use "cited_by_count:>(min-1)" when citationMin
provided and "cited_by_count:<(max+1)" when citationMax provided); keep other
fields unchanged and ensure you reference the same filterParts collection and
field/name variables in SearchFilterBuilder.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/SearchOptionsService.java`:
- Line 30: The class-level Map field defaultFilterOptionsCache (type
CachedFilterOptions) is unbounded and can grow indefinitely from page-based
keys; replace it with a bounded, eviction-capable cache (e.g., Caffeine or Guava
Cache, or a size-limited synchronized LinkedHashMap) configured with a sensible
maximumSize and expireAfterWrite/Access policy. Update all places that currently
read/write defaultFilterOptionsCache (the code that composes keys like
"limit:page" and the methods that populate CachedFilterOptions) to use the
cache's get(key, mappingFunction) or getIfPresent/put pattern so entries are
computed on demand and evicted automatically; keep the CachedFilterOptions type
and population logic but move creation into the cache loader/mappingFunction.
Ensure thread-safety by using the cache API rather than manual ConcurrentHashMap
access so high-cardinality page values cannot grow memory without bound.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/SearchSummaryService.java`:
- Around line 29-40: getSummary currently only reads volatile cachedSummary and
on TTL expiry every concurrent caller will fetch from OpenAlex (cache stampede);
fix by introducing a dedicated final Object lock (e.g., summaryRefreshLock) and
use double-checked locking inside getSummary: after detecting currentSummary ==
null || currentSummary.isExpired(), synchronize(summaryRefreshLock) and re-check
cachedSummary/isExpired(), then only the thread that still needs to refresh
calls fetchTotalWorksCount(), creates new CachedSummary and assigns
cachedSummary; keep cachedSummary volatile and leave CachedSummary and
fetchTotalWorksCount unchanged.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/user/service/impl/AccountServiceImpl.java`:
- Around line 39-56: The code currently throws LOCAL_PASSWORD_NOT_AVAILABLE when
user.getPasswordHash() is blank, which prevents Google-only accounts from
setting their first local password; revert to a two-path flow in
AccountServiceImpl: if StringUtils.hasText(user.getPasswordHash())
(hasLocalPassword) keep the existing checks using
passwordEncoder.matches(request.currentPassword(), user.getPasswordHash()) and
throw CURRENT_PASSWORD_INVALID or PASSWORD_REUSE_NOT_ALLOWED as needed, but if
there is no local password allow a "first-password" path that does not require
currentPassword (validate request.newPassword() is present and not empty, and
ensure it isn't trivially invalid), then set the new password hash; remove the
unconditional throw BusinessException(ErrorCode.LOCAL_PASSWORD_NOT_AVAILABLE)
and restore the commented no-local-password branch (also update the same logic
at the other occurrence around lines 79-86) so Google-only users can establish
their initial credential.

---

Outside diff comments:
In @.github/workflows/prod_owlreka.yml:
- Around line 19-62: The workflow currently pins third-party actions by tag
(e.g., actions/checkout@v4, actions/setup-java@v4, actions/upload-artifact@v4,
actions/download-artifact@v4, azure/login@v2, azure/webapps-deploy@v3); replace
each tag reference with the corresponding full commit SHA for that action
(lookup the exact commit SHA from the action's GitHub repo and update the uses:
lines to e.g. actions/checkout@<full-sha>), verify the SHAs point to the
intended release commits, and commit those updated uses entries so the workflow
is pinned to immutable SHAs.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/service/MetricService.java`:
- Around line 6-13: The file currently declares both an interface and a class
named MetricService which breaks compilation; keep this file as the interface
only by removing the concrete class declaration here and ensuring the interface
contains the method signature MetricsResponse
calculateAndSaveMetrics(PeriodRequest request). Move the implementation into a
new class named MetricServiceImpl (in the same package) that implements
MetricService, annotate it with `@Service` and `@RequiredArgsConstructor`, implement
the calculateAndSaveMetrics(PeriodRequest request) method body formerly in the
removed class, and add any necessary imports and constructor-injected
dependencies there; update any callers/beans to depend on the MetricService
interface (no other changes required in this file).

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/search/controller/SearchController.java`:
- Around line 155-203: The three mutating endpoints saveSearchHistory,
deleteSearchHistory, and clearSearchHistory should require an authenticated
user; change the `@CurrentUserUUID` annotations on these methods from required =
false back to required = true (or remove the parameter so it defaults to
required) so anonymous requests receive a 401 instead of reaching the service
with userId == null; update the annotations on the UUID parameter in
saveSearchHistory, deleteSearchHistory, and clearSearchHistory accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fc45bf03-3be4-4fc3-bbea-bced8c9eccda

📥 Commits

Reviewing files that changed from the base of the PR and between 78fdcb9 and a6d4d06.

📒 Files selected for processing (51)
  • .github/workflows/prod_owlreka.yml
  • scipubtts/pom.xml
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/controller/AuthController.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/controller/PasswordRecoveryController.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/dto/request/ForgotPasswordRequest.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/dto/request/LoginRequest.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/dto/request/RegisterLocalRequest.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/dto/request/ResetPasswordRequest.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/dto/request/VerifyResetCodeRequest.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/dto/response/CurrentUserResponse.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/security/jwt/JwtAuthenticationFilter.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/security/oauth2/CustomAuthenticationFailureHandler.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/security/oauth2/CustomOAuth2UserService.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/security/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/security/oauth2/OAuth2AuthenticationSuccessHandler.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/service/impl/AuthServiceImpl.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/common/exception/ErrorCode.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/common/health/HealthController.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/common/openalex/OpenAlexClient.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/config/AuthProperties.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/config/CorsConfig.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/config/CorsConfig1.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/config/JacksonConfig.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/config/OpenAlexConfig.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/config/SecurityConfig.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/service/MetricService.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/detailpaper/controller/PaperDetailController.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/controller/SearchController.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/dto/SearchFilterOptionListResponse.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/dto/SearchFilterOptionsResponse.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/dto/SearchHistorySaveRequest.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/dto/SearchSummaryResponse.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/dto/SearchWorksQueryRequest.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/dto/SearchWorksResponse.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/repository/SearchHistoryRepository.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/OpenAlexMapReader.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/SearchConstants.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/SearchFilterBuilder.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/SearchHistoryService.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/SearchOptionsService.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/SearchQuerySupport.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/SearchService.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/SearchServiceImpl.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/SearchSummaryService.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/SearchWorksLookupService.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/SearchWorksMapper.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/user/service/impl/AccountServiceImpl.java
  • scipubtts/src/main/resources/application-dev.properties
  • scipubtts/src/main/resources/application-local.properties.example
  • scipubtts/src/main/resources/application-prod.properties
  • scipubtts/src/main/resources/application.properties
💤 Files with no reviewable changes (1)
  • scipubtts/src/main/java/com/brotherhood/scipubtts/config/CorsConfig1.java
✅ Files skipped from review due to trivial changes (6)
  • scipubtts/src/main/java/com/brotherhood/scipubtts/common/health/HealthController.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/config/JacksonConfig.java
  • scipubtts/src/main/resources/application-dev.properties
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/controller/PasswordRecoveryController.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/config/AuthProperties.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/dto/request/LoginRequest.java
🚧 Files skipped from review as they are similar to previous changes (11)
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/SearchConstants.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/config/OpenAlexConfig.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/detailpaper/controller/PaperDetailController.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/config/CorsConfig.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/SearchWorksLookupService.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/service/impl/AuthServiceImpl.java
  • scipubtts/src/main/resources/application-prod.properties
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/security/oauth2/CustomAuthenticationFailureHandler.java
  • scipubtts/src/main/resources/application.properties
  • scipubtts/src/main/java/com/brotherhood/scipubtts/common/exception/ErrorCode.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/security/jwt/JwtAuthenticationFilter.java

Comment thread .github/workflows/prod_owlreka.yml Outdated
Comment thread .github/workflows/prod_owlreka.yml Outdated
Comment on lines +33 to 34
@Schema(example = "http://localhost:5173", description = "Frontend base URL used to build the email verification redirect.")
String appBaseUrl

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate and constrain appBaseUrl before using it for verification redirects.

appBaseUrl is currently unconstrained user input. If this value is used to build verification links, it can enable redirect-target tampering and token leakage. Add strict validation here and enforce a server-side allowlist of permitted frontend origins.

🔧 Suggested DTO hardening
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
...
-        `@Schema`(example = "http://localhost:5173", description = "Frontend base URL used to build the email verification redirect.")
-        String appBaseUrl
+        `@Schema`(example = "http://localhost:5173", description = "Frontend base URL used to build the email verification redirect.")
+        `@NotBlank`(message = "App base URL is required")
+        `@Pattern`(
+                regexp = "^https?://[A-Za-z0-9.-]+(?::\\d+)?$",
+                message = "App base URL must be a valid http(s) origin"
+        )
+        String appBaseUrl
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Schema(example = "http://localhost:5173", description = "Frontend base URL used to build the email verification redirect.")
String appBaseUrl
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
Suggested change
@Schema(example = "http://localhost:5173", description = "Frontend base URL used to build the email verification redirect.")
String appBaseUrl
`@Schema`(example = "http://localhost:5173", description = "Frontend base URL used to build the email verification redirect.")
`@NotBlank`(message = "App base URL is required")
`@Pattern`(
regexp = "^https?://[A-Za-z0-9.-]+(?::\\d+)?$",
message = "App base URL must be a valid http(s) origin"
)
String appBaseUrl
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/auth/dto/request/RegisterLocalRequest.java`
around lines 33 - 34, The RegisterLocalRequest DTO exposes an unconstrained
String appBaseUrl which can enable open redirect/ token leakage when used to
construct verification links; add validation on the appBaseUrl field (e.g.,
require a well-formed absolute URI with scheme http/https, no
path/query/fragment, and max length) using javax.validation annotations or a
custom validator on RegisterLocalRequest.appBaseUrl and normalize it before use,
and enforce a server-side allowlist of permitted frontend origins (check the
normalized value against the allowlist in the registration flow where the
verification link is built) so only approved origins are accepted; update any
code that consumes RegisterLocalRequest.appBaseUrl to rely on the
validated/normalized value.

Comment on lines +20 to +53
public String build(SearchWorksQueryRequest request) {
List<String> filterParts = new ArrayList<>();
filterParts.add(SearchConstants.WORKS_SCOPE_FILTER);

addYearFilter(request, filterParts);
addListFilter(filterParts, "type", searchQuerySupport.normalizeTypeValues(request.getType()));
addBooleanFilter(filterParts, "is_oa", request.getOpenAccess());
addListFilter(
filterParts,
"primary_topic.subfield.id",
searchQuerySupport.normalizeSubFieldValues(request.getSubField())
);
addListFilter(filterParts, "authorships.author.id", searchQuerySupport.normalizeEntityIds(request.getAuthor()));
addListFilter(
filterParts,
"authorships.institutions.id",
searchQuerySupport.normalizeEntityIds(request.getInstitution())
);
addBooleanFilter(filterParts, "has_content.pdf", request.getPdf());
addListFilter(
filterParts,
"institutions.country_code",
searchQuerySupport.normalizeCountryValues(request.getCountry())
);
addCitationFilter(request, filterParts);
addListFilter(
filterParts,
"primary_location.source.id",
searchQuerySupport.normalizeEntityIds(request.getSource())
);
addListFilter(filterParts, "awards.id", searchQuerySupport.normalizeEntityIds(request.getAward()));
addOrcidFilter(filterParts, request.getIndexedByOrcid());

return String.join(",", filterParts);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

trendingMode is exposed but never changes search behavior.

This builder never reads request.getTrendingMode(), and SearchWorksMapper still hard-codes matchesTrendingKeyword=false, matchesTrendingTopic=false, and trendingScore=0.0 for every item. Right now the new request flag and response fields are dead API surface on /api/search/works.

Comment on lines +93 to +99
if (minValue != null) {
filterParts.add(field + ":>" + minValue);
}

if (maxValue != null) {
filterParts.add(field + ":<" + maxValue);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In the OpenAlex Works API filter syntax, are >and<exclusive or inclusive, and what is the documented way to express inclusive lower and upper bounds forpublication_yearandcited_by_count?

💡 Result:

In the OpenAlex Works API, the comparison operators > (greater than) and < (less than) are exclusive [1][2][3]. For example, cited_by_count:>100 retrieves works with more than 100 citations [4][3]. To express inclusive bounds, the recommended methods are: 1. For publication_year: The API supports a specific range syntax using a hyphen, which is inclusive [4][5][3]. For example, publication_year:2020-2024 retrieves works published from 2020 through 2024 (inclusive) [4][6]. 2. For cited_by_count (or other numeric fields without dedicated range syntax): You must combine two inequality filters using the AND logic (comma-separated). To make these inclusive, you effectively "shift" the boundary to the next integer [1][7][2]. For an inclusive range [X, Y], you would use: cited_by_count:>X-1,cited_by_count:<Y+1 For example, to get works with 50 to 100 citations inclusive, you would use: filter=cited_by_count:>49,cited_by_count:<101. Note: While the API documentation explicitly highlights the range syntax for dates (like publication_year), it does not provide a native "inclusive range" operator for general numeric attributes beyond combining the exclusive > and < operators [1][2][3].

Citations:


Fix OpenAlex filter bound semantics for DTO min/max (exclusive :&gt;/:&lt; vs inclusive “minimum/maximum”).

OpenAlex’s :&gt; and :&lt; comparison operators are exclusive. If the DTO fields are documented as inclusive “minimum/maximum” values, the current mapping (yearFrom/citationMin → :&gt; min, yearTo/citationMax → :&lt; max) will be off by one.

  • publication_year: use the inclusive hyphen range form publication_year:min-max (not publication_year:&gt;min / publication_year:&lt;max).
  • cited_by_count: emulate inclusive bounds by shifting, e.g. cited_by_count:&gt;(min-1),cited_by_count:&lt;(max+1).

File: scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/SearchFilterBuilder.java (lines 93-99)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/SearchFilterBuilder.java`
around lines 93 - 99, The current mapping in SearchFilterBuilder adds exclusive
OpenAlex operators for min/max which breaks inclusive DTO semantics; update the
logic that builds filterParts (the code that currently does
filterParts.add(field + ":>" + minValue) and filterParts.add(field + ":<" +
maxValue)) so that for publication_year (yearFrom/yearTo) you produce an
inclusive hyphen range string like "publication_year:min-max" when either bound
exists (handle open-ended bounds appropriately), and for cited_by_count
(citationMin/citationMax) emulate inclusive bounds by converting them to
exclusive operators with offsets (use "cited_by_count:>(min-1)" when citationMin
provided and "cited_by_count:<(max+1)" when citationMax provided); keep other
fields unchanged and ensure you reference the same filterParts collection and
field/name variables in SearchFilterBuilder.

private final SearchQuerySupport searchQuerySupport;
private final OpenAlexMapReader openAlexMapReader;
private final SearchSummaryService searchSummaryService;
private final Map<String, CachedFilterOptions> defaultFilterOptionsCache = new ConcurrentHashMap<>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Unbounded default-options cache keys can grow memory without limit.

Line 30 stores default responses in a process-wide map keyed by limit:page, and Lines 124–141/489–491 allow effectively unbounded page-based key creation. Because expired entries are only replaced on same-key access, high-cardinality page values can accumulate and degrade availability over time.

Suggested fix (bounded cache)
-    private final Map<String, CachedFilterOptions> defaultFilterOptionsCache = new ConcurrentHashMap<>();
+    private static final int DEFAULT_FILTER_OPTIONS_CACHE_MAX_KEYS = 200;
+    private final Map<String, CachedFilterOptions> defaultFilterOptionsCache = new ConcurrentHashMap<>();

     private SearchFilterOptionsResponse getDefaultFilterOptions(int limit, int page) {
+        evictExpiredCacheEntries();
+        if (defaultFilterOptionsCache.size() >= DEFAULT_FILTER_OPTIONS_CACHE_MAX_KEYS) {
+            defaultFilterOptionsCache.clear();
+        }
         String cacheKey = buildDefaultCacheKey(limit, page);
         CachedFilterOptions cachedFilterOptions = defaultFilterOptionsCache.get(cacheKey);
@@
     }
+
+    private void evictExpiredCacheEntries() {
+        defaultFilterOptionsCache.entrySet().removeIf(entry -> entry.getValue().isExpired());
+    }

Also applies to: 124-141, 489-491

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/SearchOptionsService.java`
at line 30, The class-level Map field defaultFilterOptionsCache (type
CachedFilterOptions) is unbounded and can grow indefinitely from page-based
keys; replace it with a bounded, eviction-capable cache (e.g., Caffeine or Guava
Cache, or a size-limited synchronized LinkedHashMap) configured with a sensible
maximumSize and expireAfterWrite/Access policy. Update all places that currently
read/write defaultFilterOptionsCache (the code that composes keys like
"limit:page" and the methods that populate CachedFilterOptions) to use the
cache's get(key, mappingFunction) or getIfPresent/put pattern so entries are
computed on demand and evicted automatically; keep the CachedFilterOptions type
and population logic but move creation into the cache loader/mappingFunction.
Ensure thread-safety by using the cache API rather than manual ConcurrentHashMap
access so high-cardinality page values cannot grow memory without bound.

Comment on lines +29 to +40
public SearchSummaryResponse getSummary() {
CachedSummary currentSummary = cachedSummary;

if (currentSummary != null && !currentSummary.isExpired()) {
return currentSummary.response();
}

SearchSummaryResponse freshSummary = new SearchSummaryResponse(fetchTotalWorksCount());
cachedSummary = new CachedSummary(
freshSummary,
Instant.now().plus(SUMMARY_CACHE_TTL)
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

This cache still stampedes on expiry.

cachedSummary is only volatile. Once the TTL elapses, every concurrent request will miss and call OpenAlex before one of them stores the refresh, which can spike latency and upstream traffic every 5 minutes.

Suggested fix
 public class SearchSummaryService {

     private static final Duration SUMMARY_CACHE_TTL = Duration.ofMinutes(5);
+    private final Object summaryRefreshLock = new Object();

     private final OpenAlexClient openAlexClient;
     private final OpenAlexMapReader openAlexMapReader;
     private volatile CachedSummary cachedSummary;
@@
     public SearchSummaryResponse getSummary() {
         CachedSummary currentSummary = cachedSummary;

         if (currentSummary != null && !currentSummary.isExpired()) {
             return currentSummary.response();
         }

-        SearchSummaryResponse freshSummary = new SearchSummaryResponse(fetchTotalWorksCount());
-        cachedSummary = new CachedSummary(
-                freshSummary,
-                Instant.now().plus(SUMMARY_CACHE_TTL)
-        );
-
-        return freshSummary;
+        synchronized (summaryRefreshLock) {
+            currentSummary = cachedSummary;
+            if (currentSummary != null && !currentSummary.isExpired()) {
+                return currentSummary.response();
+            }
+
+            SearchSummaryResponse freshSummary = new SearchSummaryResponse(fetchTotalWorksCount());
+            cachedSummary = new CachedSummary(
+                    freshSummary,
+                    Instant.now().plus(SUMMARY_CACHE_TTL)
+            );
+            return freshSummary;
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/search/service/SearchSummaryService.java`
around lines 29 - 40, getSummary currently only reads volatile cachedSummary and
on TTL expiry every concurrent caller will fetch from OpenAlex (cache stampede);
fix by introducing a dedicated final Object lock (e.g., summaryRefreshLock) and
use double-checked locking inside getSummary: after detecting currentSummary ==
null || currentSummary.isExpired(), synchronize(summaryRefreshLock) and re-check
cachedSummary/isExpired(), then only the thread that still needs to refresh
calls fetchTotalWorksCount(), creates new CachedSummary and assigns
cachedSummary; keep cachedSummary volatile and leave CachedSummary and
fetchTotalWorksCount unchanged.

Comment on lines +39 to 56
// Temporarily disable the "set password for Google-only account" flow
// while login/register is being tested in isolation.
//
// boolean hasLocalPassword = StringUtils.hasText(user.getPasswordHash());
//
// if (hasLocalPassword) {
// if (!StringUtils.hasText(request.currentPassword())
// || !passwordEncoder.matches(request.currentPassword(), user.getPasswordHash())) {
// throw new BusinessException(ErrorCode.CURRENT_PASSWORD_INVALID);
// }
//
// if (passwordEncoder.matches(request.newPassword(), user.getPasswordHash())) {
// throw new BusinessException(ErrorCode.PASSWORD_REUSE_NOT_ALLOWED);
// }
// }

if (!StringUtils.hasText(user.getPasswordHash())) {
throw new BusinessException(ErrorCode.LOCAL_PASSWORD_NOT_AVAILABLE);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

This blocks Google-only accounts from ever setting a password.

The previous no-local-password branch is commented out, and the live path now throws LOCAL_PASSWORD_NOT_AVAILABLE whenever passwordHash is blank. That means accounts with hasPassword = false can be identified by the API but still have no way to establish their first local credential through this service. Restore the separate first-password flow before merging, or keep that state out of the account API until the flow exists.

Also applies to: 79-86

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/user/service/impl/AccountServiceImpl.java`
around lines 39 - 56, The code currently throws LOCAL_PASSWORD_NOT_AVAILABLE
when user.getPasswordHash() is blank, which prevents Google-only accounts from
setting their first local password; revert to a two-path flow in
AccountServiceImpl: if StringUtils.hasText(user.getPasswordHash())
(hasLocalPassword) keep the existing checks using
passwordEncoder.matches(request.currentPassword(), user.getPasswordHash()) and
throw CURRENT_PASSWORD_INVALID or PASSWORD_REUSE_NOT_ALLOWED as needed, but if
there is no local password allow a "first-password" path that does not require
currentPassword (validate request.newPassword() is present and not empty, and
ensure it isn't trivially invalid), then set the new password hash; remove the
unconditional throw BusinessException(ErrorCode.LOCAL_PASSWORD_NOT_AVAILABLE)
and restore the commented no-local-password branch (also update the same logic
at the other occurrence around lines 79-86) so Google-only users can establish
their initial credential.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
scipubtts/src/main/java/com/brotherhood/scipubtts/auth/controller/AuthController.java (1)

185-185: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove debug System.out.println statement.

This debug statement should be removed or replaced with proper logging before merging to production.

Suggested fix
     `@PostMapping`("/register/google/complete")
     public ResponseEntity<ResponseObject> completeGoogleRegister(
             `@Valid` `@RequestBody` CompleteGoogleRegisterRequest request,
             HttpServletRequest httpRequest,
             HttpServletResponse httpResponse
     ) {
-        System.out.println("===== HIT GOOGLE COMPLETE REGISTER =====");
         AuthResponse data = authService.completeGoogleRegister(request, httpRequest, httpResponse);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/auth/controller/AuthController.java`
at line 185, Remove the debug System.out.println statement that prints "=====
HIT GOOGLE COMPLETE REGISTER =====" from the AuthController. Instead of using
System.out.println for debugging, replace it with proper logging using the
appropriate logger (such as SLF4J or Log4J) that is likely already configured in
the project, or simply remove it if it is no longer needed for functionality.
scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/service/impl/TopicServiceImpl.java (1)

161-244: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove debug System.out.println statements.

Multiple debug print statements are present throughout calculateCitationDecay. These should be removed or replaced with proper logging (e.g., SLF4J logger with appropriate log levels) before merging to production.

Suggested approach
+  private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TopicServiceImpl.class);
+
   private double calculateCitationDecay(Topic topic){
-    long totalStart = System.nanoTime();
-    System.out.println("[CitationDecay] START - topicId=" + topic.getTopicId());
+    log.debug("[CitationDecay] START - topicId={}", topic.getTopicId());
     // ... similar changes for other println statements
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/service/impl/TopicServiceImpl.java`
around lines 161 - 244, Remove all System.out.println debug statements from the
calculateCitationDecay method in TopicServiceImpl and replace them with proper
SLF4J logger calls at appropriate log levels. The statements to replace include
those logging CitationDecay start, API completion time, empty response, prepare
phase, and calculation completion. Use a logger instance (such as one injected
via Spring or created as a class field) and select appropriate log levels (info
for general flow, debug for detailed timing information) based on the purpose of
each statement.
🧹 Nitpick comments (4)
scipubtts/src/main/java/com/brotherhood/scipubtts/feed/controller/FeedController.java (1)

19-25: ⚡ Quick win

Use POST for the sync trigger endpoint.

This route performs a state-changing operation, so exposing it as GET is unsafe from an HTTP semantics perspective (prefetch/caching/crawlers can invoke it unexpectedly).

Suggested change
-import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
@@
-    `@GetMapping`("/sync")
+    `@PostMapping`("/sync")
     public ResponseEntity<ResponseObject> sync() {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/feed/controller/FeedController.java`
around lines 19 - 25, The sync endpoint in FeedController is currently using the
GET HTTP method for a state-changing operation. Change the `@GetMapping`
annotation on the sync method to `@PostMapping` to properly follow HTTP semantics,
since this operation performs a state-changing action (triggering a daily feed
synchronization) that should not be invoked by prefetching, caching, or web
crawlers.
.idea/dataSources.xml (1)

4-8: ⚡ Quick win

Remove IDE-local datasource config from version control.

Line 4 and Line 8 add developer-machine IntelliJ datasource metadata. This creates repo noise and environment-specific churn; keep it local and ignore it in VCS.

Suggested cleanup
- .idea/dataSources.xml
+# Remove this file from the PR and keep IDE datasource config local-only.
+# If not already present, ensure `.idea/` is ignored in `.gitignore`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.idea/dataSources.xml around lines 4 - 8, Remove the `.idea/dataSources.xml`
file from version control as it contains IntelliJ-specific datasource metadata
that is environment-dependent and should not be committed. Add
`.idea/dataSources.xml` to your `.gitignore` file to prevent this type of IDE
configuration from being tracked in the future. If the file has already been
committed, use git rm --cached to remove it from the repository while keeping it
locally on your machine. This eliminates environment-specific churn and repo
noise caused by developer-machine configurations.
.github/workflows/prod_owlreka.yml (1)

22-22: 💤 Low value

Consider pinning GitHub Actions to commit SHAs for supply-chain security.

Using version tags (@v4) is common but vulnerable to tag replacement attacks. For production deployments, consider pinning to full commit SHAs. Example for actions/checkout:

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

This applies to all actions in this workflow (checkout, setup-java, upload-artifact, download-artifact, azure/login, azure/webapps-deploy).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/prod_owlreka.yml at line 22, GitHub Actions in this
workflow are pinned to version tags (like `@v4`) instead of full commit SHAs,
which creates supply-chain security vulnerabilities through tag replacement
attacks. Update all action references (checkout, setup-java, upload-artifact,
download-artifact, azure/login, and azure/webapps-deploy) to pin to their full
commit SHA format instead, including an inline comment showing the corresponding
version tag for reference. For example, change `- uses: actions/checkout@v4` to
`- uses: actions/checkout@<full-commit-sha> # v4.x.x`.

Source: Linters/SAST tools

scipubtts/src/main/java/com/brotherhood/scipubtts/admin/service/impl/AdminServiceImpl.java (1)

174-176: 💤 Low value

Consider using a fixed timezone for date calculations.

Using ZoneId.systemDefault() can cause inconsistencies across deployments or if the server timezone configuration changes. For admin dashboards that aggregate data, consider using UTC or a configured application timezone.

private static final ZoneId DASHBOARD_ZONE = ZoneOffset.UTC;
// or inject from configuration:
// `@Value`("${app.dashboard.timezone:UTC}")
// private String dashboardTimezone;

Also applies to: 243-256

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/admin/service/impl/AdminServiceImpl.java`
around lines 174 - 176, Replace the use of `ZoneId.systemDefault()` with a fixed
timezone constant to ensure consistent behavior across deployments. Define a
static final field named `DASHBOARD_ZONE` with `ZoneOffset.UTC` at the class
level in AdminServiceImpl. Then replace the call to `ZoneId.systemDefault()` in
the `getApiUsageLast7Days()` method (line 174-176 anchor) with this constant.
Apply the same replacement to any other occurrences of `ZoneId.systemDefault()`
in the sibling location at lines 243-256.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/admin/repository/AdminDashboardRepository.java`:
- Around line 206-213: The queryOptionalOffsetDateTime method uses
ZoneId.systemDefault() when converting the Timestamp to OffsetDateTime, which
causes the result to vary depending on the server's timezone, leading to
inconsistency across environments. Replace ZoneId.systemDefault() with a stable,
environment-independent timezone such as ZoneOffset.UTC or another fixed zone
identifier to ensure the same database value always produces the same
OffsetDateTime result regardless of where the code is executed.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/bookmark/entity/UserBookmark.java`:
- Around line 32-33: The entityType field in the UserBookmark class has a
default initializer of "WORK", but Lombok's `@Builder` annotation ignores field
initializers by default, causing the field to be null when constructed via the
builder. This violates the nullable = false constraint on the database column.
Add the `@Builder.Default` annotation directly above the entityType field
declaration to ensure the "WORK" default value is applied when using the builder
pattern.

In `@scipubtts/src/main/java/com/brotherhood/scipubtts/config/FlywayConfig.java`:
- Around line 26-27: The ignoreMigrationPatterns field is being injected from
Spring configuration properties at the class level, but the actual Flyway
configuration is using hardcoded values instead of this configured field,
causing the property to never be applied at runtime. Locate where the hardcoded
ignoreMigrationPatterns values are being set (likely in a method that configures
the Flyway bean) and replace those hardcoded values with the
ignoreMigrationPatterns field that already contains the configuration values
from the Spring properties file.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/config/OpenAlexConfig.java`:
- Around line 20-34: The RestClient builder configuration has three issues to
fix. First, remove the redundant first call to baseUrl() on line 21 that
hard-codes "https://api.openalex.org", keeping only the second call with the
configured openAlexBaseUrl variable. Second, remove the redundant first call to
defaultHeader() on line 22 that sets User-Agent to "ScipubTTS", keeping only the
second call that uses HttpHeaders.USER_AGENT constant. Third, fix the empty
api_key parameter in the requestInterceptor method where it currently appends
"api_key=" with an empty string value. Either inject the api_key from
application configuration using a `@Value` annotation on a field like
openAlexApiKey and insert that value into the URI construction, or remove the
entire requestInterceptor block if OpenAlex does not require authentication.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/feed/client/OpenAlexWorksClient.java`:
- Around line 47-55: The fetchWithRetry method in OpenAlexWorksClient only
catches RestClientResponseException for HTTP response errors, but transient
network failures like timeouts and connection resets throw RestClientException
instead and are not retried. Modify the exception handling to catch the parent
RestClientException class, and add logic to distinguish between
RestClientResponseException (which should retry on 429 or 5xx status codes) and
other RestClientException types (which should retry if attempt is less than 3).
This ensures both HTTP error responses and transport-level transient failures
are properly retried before giving up.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/feed/service/impl/ResearchFeedSyncServiceImpl.java`:
- Around line 52-65: The syncDailyFeed method in ResearchFeedSyncServiceImpl
lacks protection against concurrent execution, allowing overlapping runs from
the scheduler and manual triggers. Before creating and saving a new ApiJob with
STATUS_RUNNING, add a check to query the apiJobRepository for any existing jobs
with the same JOB_TYPE and STATUS_RUNNING status. If a running job already
exists, return early to prevent the concurrent execution. Only proceed with
creating and saving the new job if no other running sync is in progress.
- Around line 106-123: The method currently returns void, preventing callers
from knowing whether the sync operation succeeded, partially succeeded, or
failed. Modify the method to return the job status (or a result object
containing the outcome) instead of returning void. After setting the job status
and saving to the repository in the normal flow, return the status value. Also
ensure the catch block returns or rethrows appropriately so that callers can
access the actual outcome of the sync operation and handle partial successes or
failures accordingly.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/schedule/ResearchFeedScheduler.java`:
- Around line 14-16: The syncResearchFeedDaily() method's `@Scheduled` annotation
does not specify a timezone parameter, but the researchFeedSyncService uses
Asia/Ho_Chi_Minh timezone for its date window calculations. Add the zone
parameter to the `@Scheduled` annotation and set it to "Asia/Ho_Chi_Minh" to
ensure the cron job executes at the correct wall-clock time relative to the date
window logic used in ResearchFeedSyncServiceImpl.

---

Outside diff comments:
In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/auth/controller/AuthController.java`:
- Line 185: Remove the debug System.out.println statement that prints "===== HIT
GOOGLE COMPLETE REGISTER =====" from the AuthController. Instead of using
System.out.println for debugging, replace it with proper logging using the
appropriate logger (such as SLF4J or Log4J) that is likely already configured in
the project, or simply remove it if it is no longer needed for functionality.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/service/impl/TopicServiceImpl.java`:
- Around line 161-244: Remove all System.out.println debug statements from the
calculateCitationDecay method in TopicServiceImpl and replace them with proper
SLF4J logger calls at appropriate log levels. The statements to replace include
those logging CitationDecay start, API completion time, empty response, prepare
phase, and calculation completion. Use a logger instance (such as one injected
via Spring or created as a class field) and select appropriate log levels (info
for general flow, debug for detailed timing information) based on the purpose of
each statement.

---

Nitpick comments:
In @.github/workflows/prod_owlreka.yml:
- Line 22: GitHub Actions in this workflow are pinned to version tags (like `@v4`)
instead of full commit SHAs, which creates supply-chain security vulnerabilities
through tag replacement attacks. Update all action references (checkout,
setup-java, upload-artifact, download-artifact, azure/login, and
azure/webapps-deploy) to pin to their full commit SHA format instead, including
an inline comment showing the corresponding version tag for reference. For
example, change `- uses: actions/checkout@v4` to `- uses:
actions/checkout@<full-commit-sha> # v4.x.x`.

In @.idea/dataSources.xml:
- Around line 4-8: Remove the `.idea/dataSources.xml` file from version control
as it contains IntelliJ-specific datasource metadata that is
environment-dependent and should not be committed. Add `.idea/dataSources.xml`
to your `.gitignore` file to prevent this type of IDE configuration from being
tracked in the future. If the file has already been committed, use git rm
--cached to remove it from the repository while keeping it locally on your
machine. This eliminates environment-specific churn and repo noise caused by
developer-machine configurations.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/admin/service/impl/AdminServiceImpl.java`:
- Around line 174-176: Replace the use of `ZoneId.systemDefault()` with a fixed
timezone constant to ensure consistent behavior across deployments. Define a
static final field named `DASHBOARD_ZONE` with `ZoneOffset.UTC` at the class
level in AdminServiceImpl. Then replace the call to `ZoneId.systemDefault()` in
the `getApiUsageLast7Days()` method (line 174-176 anchor) with this constant.
Apply the same replacement to any other occurrences of `ZoneId.systemDefault()`
in the sibling location at lines 243-256.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/feed/controller/FeedController.java`:
- Around line 19-25: The sync endpoint in FeedController is currently using the
GET HTTP method for a state-changing operation. Change the `@GetMapping`
annotation on the sync method to `@PostMapping` to properly follow HTTP semantics,
since this operation performs a state-changing action (triggering a daily feed
synchronization) that should not be invoked by prefetching, caching, or web
crawlers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8b7dc681-a63a-4d85-965b-1744d032d996

📥 Commits

Reviewing files that changed from the base of the PR and between a6d4d06 and 92440c1.

📒 Files selected for processing (67)
  • .github/workflows/prod_owlreka.yml
  • .idea/compiler.xml
  • .idea/dataSources.xml
  • .idea/diff-generator.xml
  • .idea/jpb-settings.xml
  • .idea/sqldialects.xml
  • README.md
  • scipubtts/pom.xml
  • scipubtts/src/main/java/com/brotherhood/scipubtts/ScipubttsApplication.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/admin/controller/AdminController.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/admin/dto/AdminApiCallConsumerResponse.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/admin/dto/AdminApiUsageDailyResponse.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/admin/dto/AdminDashboardStatisticsResponse.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/admin/dto/AdminUserBanSummaryResponse.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/admin/dto/AdminUserPageResponse.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/admin/repository/AdminDashboardRepository.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/admin/service/AdminService.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/admin/service/impl/AdminServiceImpl.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/controller/AuthController.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/dto/request/OAuth2SessionExchangeRequest.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/security/jwt/JwtAuthenticationFilter.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/security/oauth2/CustomAuthenticationFailureHandler.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/security/oauth2/OAuth2AuthenticationSuccessHandler.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/service/AuthService.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/service/impl/AuthServiceImpl.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/bookmark/entity/UserBookmark.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/bookmark/repository/UserBookmarkRepository.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/bookmark/service/impl/BookmarkServiceImpl.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/common/exception/ErrorCode.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/config/FlywayConfig.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/config/OpenAlexConfig.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/config/SecurityConfig.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/controller/DataController.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/controller/StatisticController.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/entity/Topic.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/repository/TopicRepository.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/service/MetricService.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/service/OpenAlexService.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/service/TopicService.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/service/impl/MetricServiceImpl.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/service/impl/TopicServiceImpl.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/feed/client/OpenAlexWorksClient.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/feed/controller/FeedController.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/feed/dto/request/getFeed.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/feed/dto/response/OpenAlexWorksResponse.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/feed/entity/ApiJob.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/feed/model/FeedDraft.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/feed/model/FeedKey.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/feed/model/FeedReason.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/feed/model/FollowTargetGroupView.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/feed/repository/ApiJobRepository.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/feed/repository/ResearchFeedRepository.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/feed/service/FeedPersistenceService.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/feed/service/ResearchFeedSyncService.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/feed/service/impl/FeedPersistenceServiceImpl.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/feed/service/impl/ResearchFeedSyncServiceImpl.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/follow/repository/UserFollowRepository.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/schedule/ResearchFeedScheduler.java
  • scipubtts/src/main/resources/application-dev.properties
  • scipubtts/src/main/resources/application-local.properties.example
  • scipubtts/src/main/resources/application-prod.properties
  • scipubtts/src/main/resources/application.properties
  • scipubtts/src/main/resources/db/migration/README.md
  • scipubtts/src/main/resources/db/migration/V5__fix_topics_id_sequence.sql
  • scipubtts/src/main/resources/db/migration/V6__update_topic_unique_constraint.sql
  • scipubtts/src/main/resources/db/migration/V7__feed_scheduler_update.sql
  • scipubtts/src/test/java/com/brotherhood/scipubtts/admin/service/impl/AdminServiceImplTest.java
✅ Files skipped from review due to trivial changes (15)
  • scipubtts/src/main/java/com/brotherhood/scipubtts/ScipubttsApplication.java
  • scipubtts/src/main/resources/db/migration/V5__fix_topics_id_sequence.sql
  • .idea/sqldialects.xml
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/dto/request/OAuth2SessionExchangeRequest.java
  • .idea/diff-generator.xml
  • scipubtts/src/main/java/com/brotherhood/scipubtts/feed/dto/request/getFeed.java
  • scipubtts/src/main/resources/db/migration/V6__update_topic_unique_constraint.sql
  • scipubtts/src/main/java/com/brotherhood/scipubtts/feed/service/FeedPersistenceService.java
  • README.md
  • scipubtts/src/main/java/com/brotherhood/scipubtts/feed/model/FollowTargetGroupView.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/admin/dto/AdminApiCallConsumerResponse.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/admin/dto/AdminUserBanSummaryResponse.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/admin/dto/AdminDashboardStatisticsResponse.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/admin/dto/AdminApiUsageDailyResponse.java
  • .idea/jpb-settings.xml
🚧 Files skipped from review as they are similar to previous changes (13)
  • .idea/compiler.xml
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/security/jwt/JwtAuthenticationFilter.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/follow/repository/UserFollowRepository.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/entity/Topic.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/service/TopicService.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/service/OpenAlexService.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/auth/security/oauth2/CustomAuthenticationFailureHandler.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/dashboard/repository/TopicRepository.java
  • scipubtts/src/main/resources/application.properties
  • scipubtts/src/main/resources/application-prod.properties
  • scipubtts/src/main/java/com/brotherhood/scipubtts/config/SecurityConfig.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/common/exception/ErrorCode.java
  • scipubtts/src/main/java/com/brotherhood/scipubtts/bookmark/service/impl/BookmarkServiceImpl.java

Comment on lines +206 to +213
private Optional<OffsetDateTime> queryOptionalOffsetDateTime(String sql) {
Timestamp timestamp = jdbcTemplate.queryForObject(sql, Timestamp.class);
if (timestamp == null) {
return Optional.empty();
}

return Optional.of(timestamp.toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make latest-sync timestamp conversion timezone-stable.

Line 212 converts DB time with ZoneId.systemDefault(), which makes the returned value depend on server timezone. The same DB row can produce different offsets across environments.

Proposed fix
-import java.sql.Timestamp;
 import java.time.LocalDate;
 import java.time.OffsetDateTime;
-import java.time.ZoneId;
@@
     private Optional<OffsetDateTime> queryOptionalOffsetDateTime(String sql) {
-        Timestamp timestamp = jdbcTemplate.queryForObject(sql, Timestamp.class);
-        if (timestamp == null) {
-            return Optional.empty();
-        }
-
-        return Optional.of(timestamp.toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime());
+        return jdbcTemplate.query(sql, rs -> {
+            if (!rs.next()) {
+                return Optional.empty();
+            }
+            return Optional.ofNullable(rs.getObject(1, OffsetDateTime.class));
+        });
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/admin/repository/AdminDashboardRepository.java`
around lines 206 - 213, The queryOptionalOffsetDateTime method uses
ZoneId.systemDefault() when converting the Timestamp to OffsetDateTime, which
causes the result to vary depending on the server's timezone, leading to
inconsistency across environments. Replace ZoneId.systemDefault() with a stable,
environment-independent timezone such as ZoneOffset.UTC or another fixed zone
identifier to ensure the same database value always produces the same
OffsetDateTime result regardless of where the code is executed.

Comment on lines +26 to +27
@Value("${spring.flyway.ignore-migration-patterns:*:missing,*:future}")
private String[] ignoreMigrationPatterns;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Configured ignoreMigrationPatterns is never applied.

Lines 26-27 read spring.flyway.ignore-migration-patterns, but Line 39 hardcodes values, so runtime config cannot override behavior.

Suggested fix
-                .ignoreMigrationPatterns("*:missing", "*:future")
+                .ignoreMigrationPatterns(ignoreMigrationPatterns)

Also applies to: 39-39

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scipubtts/src/main/java/com/brotherhood/scipubtts/config/FlywayConfig.java`
around lines 26 - 27, The ignoreMigrationPatterns field is being injected from
Spring configuration properties at the class level, but the actual Flyway
configuration is using hardcoded values instead of this configured field,
causing the property to never be applied at runtime. Locate where the hardcoded
ignoreMigrationPatterns values are being set (likely in a method that configures
the Flyway bean) and replace those hardcoded values with the
ignoreMigrationPatterns field that already contains the configuration values
from the Spring properties file.

Comment on lines +20 to +34
return RestClient.builder()
.baseUrl("https://api.openalex.org")
.defaultHeader("User-Agent", "ScipubTTS")
.requestInterceptor((request, body, execution) -> {
URI originalUri = request.getURI();
String separator = originalUri.getQuery() == null ? "?" : "&";
URI newUri = URI.create(originalUri + separator + "api_key=" + "");
return execution.execute(new HttpRequestWrapper(request) {
@Override
public URI getURI() { return newUri; }
}, body);
})
.baseUrl(openAlexBaseUrl)
.defaultHeader(HttpHeaders.USER_AGENT, "ScipubTTS")
.build();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove redundant builder calls and fix empty api_key parameter.

Three issues in this RestClient configuration:

  1. Empty api_key (Line 26): Appends api_key= with an empty value to every request. Either inject the key from configuration or remove the interceptor entirely if OpenAlex doesn't require authentication.

  2. Duplicate baseUrl() calls: Line 21 sets a hard-coded URL, Line 32 overrides it with the configured value. The first call is redundant.

  3. Duplicate defaultHeader() calls: Lines 22 and 33 both set User-Agent. Only one is needed.

🔧 Suggested fix
   `@Bean`
   public RestClient openAlexRestClient() {
     return RestClient.builder()
-            .baseUrl("https://api.openalex.org")
-            .defaultHeader("User-Agent", "ScipubTTS")
-            .requestInterceptor((request, body, execution) -> {
-              URI originalUri = request.getURI();
-              String separator = originalUri.getQuery() == null ? "?" : "&";
-              URI newUri = URI.create(originalUri + separator + "api_key=" + "");
-              return execution.execute(new HttpRequestWrapper(request) {
-                `@Override`
-                public URI getURI() { return newUri; }
-              }, body);
-            })
             .baseUrl(openAlexBaseUrl)
             .defaultHeader(HttpHeaders.USER_AGENT, "ScipubTTS")
             .build();
   }

If an API key is actually needed, inject it from configuration:

`@Value`("${openalex.api-key:}")
private String openAlexApiKey;

And conditionally add the interceptor only when the key is non-empty.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scipubtts/src/main/java/com/brotherhood/scipubtts/config/OpenAlexConfig.java`
around lines 20 - 34, The RestClient builder configuration has three issues to
fix. First, remove the redundant first call to baseUrl() on line 21 that
hard-codes "https://api.openalex.org", keeping only the second call with the
configured openAlexBaseUrl variable. Second, remove the redundant first call to
defaultHeader() on line 22 that sets User-Agent to "ScipubTTS", keeping only the
second call that uses HttpHeaders.USER_AGENT constant. Third, fix the empty
api_key parameter in the requestInterceptor method where it currently appends
"api_key=" with an empty string value. Either inject the api_key from
application configuration using a `@Value` annotation on a field like
openAlexApiKey and insert that value into the URI construction, or remove the
entire requestInterceptor block if OpenAlex does not require authentication.

Comment on lines +47 to +55
} catch (RestClientResponseException ex) {
int status = ex.getStatusCode().value();

if ((status == 429 || status >= 500) && attempt < 3) {
sleepBackoff(attempt);
return fetchWithRetry(filter, cursor, attempt + 1);
}
throw ex;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n scipubtts/src/main/java/com/brotherhood/scipubtts/feed/client/OpenAlexWorksClient.java | head -100

Repository: SciPub-TTS/SciPub-TTS-BE

Length of output: 4028


🏁 Script executed:

# Check the exception hierarchy and imports
rg -n "import.*RestClient" scipubtts/src/main/java/com/brotherhood/scipubtts/feed/client/OpenAlexWorksClient.java

Repository: SciPub-TTS/SciPub-TTS-BE

Length of output: 189


🌐 Web query:

Spring RestClientException vs RestClientResponseException hierarchy

💡 Result:

In the Spring Framework, RestClientException acts as the base class for exceptions related to RestClient and RestTemplate operations, while RestClientResponseException is a specialized subclass used when the exception contains actual HTTP response data [1][2][3]. Hierarchy Overview 1. RestClientException: This is the root base class for exceptions thrown during REST operations, including server error responses, decoding failures, and I/O errors [2][4]. It extends org.springframework.core.NestedRuntimeException [2][5]. 2. RestClientResponseException: This class extends RestClientException [1][6]. It serves as a common base for all exceptions that include HTTP response details, such as the status code, headers, and the response body [1][6][7]. 3. Notable Subclasses of RestClientResponseException: - HttpStatusCodeException: A common subclass used for 4xx and 5xx client and server errors. - UnknownHttpStatusCodeException: Used when an HTTP status code is received that cannot be resolved [1][6][7]. Key Differences - Purpose: RestClientException is a broad category covering various failure modes (including I/O issues or mapping errors) [2][3]. RestClientResponseException is specifically designed to carry metadata from an HTTP response [1][6]. - Information Provided: Because RestClientResponseException encapsulates the HTTP response, it provides methods like getStatusCode, getResponseHeaders, and getResponseBodyAs(...) to help developers inspect why a request failed [1][6][7]. General RestClientException instances typically only provide a message and the underlying cause [2][5]. When using RestClient or RestTemplate, catching RestClientException allows you to handle all possible REST-related errors, whereas catching RestClientResponseException specifically allows you to access the response details necessary for debugging or handling API-level errors [8][9].

Citations:


Retry logic skips non-HTTP transient failures.

Current retries only handle response-status exceptions (RestClientResponseException for 429/5xx). Transport-level failures like timeouts and connection resets throw RestClientException instead and bypass the retry mechanism entirely, causing premature target sync failures for transient network issues.

Catch RestClientException (the parent class) in addition to RestClientResponseException to retry transient network faults.

Suggested change
+import org.springframework.web.client.RestClientException;
@@
         } catch (RestClientResponseException ex) {
             int status = ex.getStatusCode().value();
@@
             }
             throw ex;
+        } catch (RestClientException ex) {
+            if (attempt < 3) {
+                sleepBackoff(attempt);
+                return fetchWithRetry(filter, cursor, attempt + 1);
+            }
+            throw ex;
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/feed/client/OpenAlexWorksClient.java`
around lines 47 - 55, The fetchWithRetry method in OpenAlexWorksClient only
catches RestClientResponseException for HTTP response errors, but transient
network failures like timeouts and connection resets throw RestClientException
instead and are not retried. Modify the exception handling to catch the parent
RestClientException class, and add logic to distinguish between
RestClientResponseException (which should retry on 429 or 5xx status codes) and
other RestClientException types (which should retry if attempt is less than 3).
This ensures both HTTP error responses and transport-level transient failures
are properly retried before giving up.

Comment on lines +52 to +65
public void syncDailyFeed() {
OffsetDateTime startedAt = OffsetDateTime.now();

ApiJob job = ApiJob.builder()
.jobType(JOB_TYPE)
.status(STATUS_RUNNING)
.startedAt(startedAt)
.totalFetched(0)
.totalSaved(0)
.totalFailed(0)
.build();

job = apiJobRepository.save(job);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Add single-flight protection for sync execution.

There is no guard against overlapping runs (scheduler + manual trigger). Concurrent runs will duplicate OpenAlex traffic and increase failure/rate-limit risk even if DB dedup prevents duplicate inserts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/feed/service/impl/ResearchFeedSyncServiceImpl.java`
around lines 52 - 65, The syncDailyFeed method in ResearchFeedSyncServiceImpl
lacks protection against concurrent execution, allowing overlapping runs from
the scheduler and manual triggers. Before creating and saving a new ApiJob with
STATUS_RUNNING, add a check to query the apiJobRepository for any existing jobs
with the same JOB_TYPE and STATUS_RUNNING status. If a running job already
exists, return early to prevent the concurrent execution. Only proceed with
creating and saving the new job if no other running sync is in progress.

Comment on lines +106 to +123
if (failedTargets == 0) {
job.setStatus(STATUS_SUCCESS);
} else if (saved > 0 || totalFetched > 0) {
job.setStatus(STATUS_PARTIAL_SUCCESS);
} else {
job.setStatus(STATUS_FAILED);
}

apiJobRepository.save(job);

} catch (Exception ex) {
job.setStatus(STATUS_FAILED);
job.setFinishedAt(OffsetDateTime.now());
job.setErrorLog(ex.getMessage());
apiJobRepository.save(job);

throw ex;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Expose sync outcome to callers instead of returning void.

This method can finish with PARTIAL_SUCCESS or FAILED without throwing, but the current contract gives callers no way to reflect that state. Return a result/status object (or throw on non-success) so upstream responses can represent actual outcome.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/feed/service/impl/ResearchFeedSyncServiceImpl.java`
around lines 106 - 123, The method currently returns void, preventing callers
from knowing whether the sync operation succeeded, partially succeeded, or
failed. Modify the method to return the job status (or a result object
containing the outcome) instead of returning void. After setting the job status
and saving to the repository in the normal flow, return the status value. Also
ensure the catch block returns or rethrows appropriately so that callers can
access the actual outcome of the sync operation and handle partial successes or
failures accordingly.

Comment on lines +14 to +16
@Scheduled(cron = "0 0 2 * * *")
public void syncResearchFeedDaily() {
researchFeedSyncService.syncDailyFeed();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "ResearchFeedScheduler.java" -type f

Repository: SciPub-TTS/SciPub-TTS-BE

Length of output: 156


🏁 Script executed:

find . -name "*ResearchFeedSyncService*" -type f

Repository: SciPub-TTS/SciPub-TTS-BE

Length of output: 265


🏁 Script executed:

cat -n ./scipubtts/src/main/java/com/brotherhood/scipubtts/schedule/ResearchFeedScheduler.java | head -30

Repository: SciPub-TTS/SciPub-TTS-BE

Length of output: 745


🏁 Script executed:

cat -n ./scipubtts/src/main/java/com/brotherhood/scipubtts/feed/service/impl/ResearchFeedSyncServiceImpl.java

Repository: SciPub-TTS/SciPub-TTS-BE

Length of output: 12657


Pin the scheduler timezone to match feed date-window calculations.

The cron job does not set zone, while the sync window logic explicitly uses Asia/Ho_Chi_Minh timezone (lines 71, 131 of ResearchFeedSyncServiceImpl). On hosts with a different default timezone, the job runs at the wrong wall-clock time relative to the date window calculations.

Suggested change
-    `@Scheduled`(cron = "0 0 2 * * *")
+    `@Scheduled`(cron = "0 0 2 * * *", zone = "Asia/Ho_Chi_Minh")
     public void syncResearchFeedDaily() {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Scheduled(cron = "0 0 2 * * *")
public void syncResearchFeedDaily() {
researchFeedSyncService.syncDailyFeed();
`@Scheduled`(cron = "0 0 2 * * *", zone = "Asia/Ho_Chi_Minh")
public void syncResearchFeedDaily() {
researchFeedSyncService.syncDailyFeed();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@scipubtts/src/main/java/com/brotherhood/scipubtts/schedule/ResearchFeedScheduler.java`
around lines 14 - 16, The syncResearchFeedDaily() method's `@Scheduled` annotation
does not specify a timezone parameter, but the researchFeedSyncService uses
Asia/Ho_Chi_Minh timezone for its date window calculations. Add the zone
parameter to the `@Scheduled` annotation and set it to "Asia/Ho_Chi_Minh" to
ensure the cron job executes at the correct wall-clock time relative to the date
window logic used in ResearchFeedSyncServiceImpl.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants