-
-
Notifications
You must be signed in to change notification settings - Fork 319
feat: Allow websocket subscription with PAK/PAT #3203
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughAdds HTTP conditional-request handling via a new ProjectLastModifiedManager and applies it to export and translation endpoints; refactors WebSocket auth to a new WebsocketAuthenticationResolver that unifies JWT, PAK, and PAT handling and updates test and client code to the new interfaces. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant V2ExportController
participant ProjectLastModifiedManager
participant ProjectTranslationLastModifiedManager
participant WebRequest
Client->>V2ExportController: GET/POST /export (with optional If-Modified-Since)
V2ExportController->>ProjectLastModifiedManager: onlyWhenProjectDataChanged(request, fn)
ProjectLastModifiedManager->>ProjectTranslationLastModifiedManager: getLastModified(projectId)
ProjectTranslationLastModifiedManager-->>ProjectLastModifiedManager: timestamp
ProjectLastModifiedManager->>WebRequest: checkNotModified(timestamp)
alt Not modified
WebRequest-->>ProjectLastModifiedManager: true
ProjectLastModifiedManager-->>V2ExportController: null
V2ExportController-->>Client: 304 Not Modified (or 412 where applicable)
else Modified
WebRequest-->>ProjectLastModifiedManager: false
ProjectLastModifiedManager->>ProjectLastModifiedManager: build headers (Last-Modified, Cache-Control) and invoke fn
ProjectLastModifiedManager-->>V2ExportController: ResponseEntity with StreamingResponseBody
V2ExportController-->>Client: 200 OK + body + headers
end
sequenceDiagram
participant WSClient
participant WebSocketConfig
participant WebsocketAuthenticationResolver
participant JwtService
participant ApiKeyService
participant SecurityService
WSClient->>WebSocketConfig: CONNECT (Authorization / X-API-Key / legacy headers)
WebSocketConfig->>WebsocketAuthenticationResolver: resolve(authHeader, xApiKeyHeader, legacyJwtHeader)
alt Bearer JWT
WebsocketAuthenticationResolver->>JwtService: validateToken
JwtService-->>WebsocketAuthenticationResolver: TolgeeAuthentication
else X-API-Key (PAK/PAT)
WebsocketAuthenticationResolver->>ApiKeyService: validateKey (distinguish PAK/PAT)
ApiKeyService-->>WebsocketAuthenticationResolver: TolgeeAuthentication (and async last-used update)
else Legacy jwtToken
WebsocketAuthenticationResolver->>JwtService: validateToken
JwtService-->>WebsocketAuthenticationResolver: TolgeeAuthentication
else invalid
WebsocketAuthenticationResolver-->>WebSocketConfig: null
end
WebsocketAuthenticationResolver-->>WebSocketConfig: TolgeeAuthentication / null
WebSocketConfig->>SecurityService: check subscribe permissions for authentication
SecurityService-->>WebSocketConfig: permitted / forbidden
WebSocketConfig-->>WSClient: CONNECTED or ERROR (403/401)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
159086b to
cb93702
Compare
|
I am working on it! |
|
This PR is stale because it has been open for 30 days with no activity. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/app/src/test/kotlin/io/tolgee/websocket/AbstractWebsocketTest.kt (1)
61-64: Stop all WebSockets to avoid cross-test leakage.
anotherUserWebsocketandspyingUserWebsocketaren’t closed; this can cause flakiness and port/socket leaks.Apply teardown and try/finally:
@@ @AfterEach fun after() { - currentUserWebsocket.stop() + if (this::currentUserWebsocket.isInitialized) currentUserWebsocket.stop() + if (this::anotherUserWebsocket.isInitialized) anotherUserWebsocket.stop() }@@ - val spyingUserWebsocket = + val spyingUserWebsocket = WebsocketTestHelper( port, WebsocketTestHelper.Auth(jwtToken = jwtService.emitToken(anotherUser.id)), testData.projectBuilder.self.id, // anotherUser trying to spy on other user's websocket testData.user.id, ) - spyingUserWebsocket.listenForNotificationsChanged() - spyingUserWebsocket.waitForForbidden() - saveNotificationForCurrentUser() - - assertCurrentUserReceivedMessage() - spyingUserWebsocket.receivedMessages.assert.isEmpty() + try { + spyingUserWebsocket.listenForNotificationsChanged() + spyingUserWebsocket.waitForForbidden() + saveNotificationForCurrentUser() + assertCurrentUserReceivedMessage() + spyingUserWebsocket.receivedMessages.assert.isEmpty() + } finally { + spyingUserWebsocket.stop() + }Also applies to: 52-59, 235-253
🧹 Nitpick comments (11)
backend/app/src/test/kotlin/io/tolgee/websocket/AbstractWebsocketTest.kt (3)
48-51: Add coverage for PAK/PAT auth paths.Good refactor to Auth wrapper. Please add tests that subscribe via PAK and PAT (success and forbidden cases), not just JWT, to validate the new resolver end-to-end.
Also applies to: 55-58, 242-246
220-233: Assert the forbidden subscribe explicitly.This negative test should also await the 403 to avoid a false pass due to timing.
fun `doesn't subscribe without permissions`() { currentUserWebsocket.listenForTranslationDataModified() anotherUserWebsocket.listenForTranslationDataModified() + anotherUserWebsocket.waitForForbidden()
215-217: Typo in doc comment.“shell” → “shall”.
backend/api/src/main/kotlin/io/tolgee/controllers/ExportController.kt (1)
49-84: Stream safety and header hygiene.
- Use
use {}to always close streams on error.- Consider adding
Vary: Authorization, X-API-KEYto prevent shared-cache mix-ups when auth differs.- streamingResponseBodyProvider.createStreamingResponseBody { out: OutputStream -> - val zipOutputStream = ZipOutputStream(out) + streamingResponseBodyProvider.createStreamingResponseBody { out: OutputStream -> + val mapper = jacksonObjectMapper() + ZipOutputStream(out).use { zipOutputStream -> val translations = translationService.getTranslations( allLanguages.map { it.tag }.toSet(), null, projectHolder.project.id, '.', ) for ((key, value) in translations) { zipOutputStream.putNextEntry(ZipEntry(String.format("%s.json", key))) - val data = jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsBytes(value) - val byteArrayInputStream = ByteArrayInputStream(data) - IOUtils.copy(byteArrayInputStream, zipOutputStream) - byteArrayInputStream.close() + val data = mapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(value) + ByteArrayInputStream(data).use { byteArrayInputStream -> + IOUtils.copy(byteArrayInputStream, zipOutputStream) + } zipOutputStream.closeEntry() } - zipOutputStream.close() - } + } + }Also please confirm
authenticationFacade.authenticatedUser.idis always present for API key/PAT flows on this endpoint. If not, derive permissions from the token principal instead.backend/api/src/main/kotlin/io/tolgee/component/ProjectLastModifiedManager.kt (1)
10-17: Clarify semantics and add optional cache headers.
- Update KDoc to mention 304 (GET/HEAD) and 412 (POST/PUT/DELETE) outcomes from
checkNotModified.- Add
Vary: Authorization, X-API-KEYby default to avoid cache poisoning across auth contexts.- Consider allowing a custom
CacheControlto be passed in when needed.@@ - * This manager implements the HTTP conditional request mechanism (If-Modified-Since/Last-Modified headers) + * This manager implements HTTP conditional requests (If-Modified-Since/Last-Modified). + * For GET/HEAD it results in 304 Not Modified; for modifying methods (POST/PUT/DELETE) Spring returns 412. @@ - val headersBuilder = ResponseEntity + val headersBuilder = ResponseEntity .ok() .lastModified(lastModified) .cacheControl(DEFAULT_CACHE_CONTROL_HEADER) + .varyBy("Authorization", "X-API-KEY")Also applies to: 41-67, 69-71
backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ExportController/V2ExportControllerCachingTest.kt (3)
48-73: Fix test name and safe header retrieval (GET path).
- Name says POST but the test hits GET.
- Avoid
as Stringon a nullable header; it can throw before the assert.- fun `returns 304 for POST export when data not modified`() { + fun `returns 304 for GET export when data not modified`() { @@ - val lastModifiedHeader = firstResponse.response.getHeaderValue("Last-Modified") as String - Assertions.assertThat(lastModifiedHeader).isNotNull() + val lastModifiedHeader = firstResponse.response.getHeader("Last-Modified") + Assertions.assertThat(lastModifiedHeader).isNotNull() + // Optional: also assert Cache-Control + Assertions.assertThat(firstResponse.response.getHeader("Cache-Control")).isEqualTo("max-age=0")
90-109: Fix misleading comment and assert cache header (POST path).Comment says 304 in a test that asserts 412. Add cache header assertion for parity with GET.
- // Second request with If-Modified-Since header - should return 304 + // Second request with If-Modified-Since header - server returns 412 for POST @@ Assertions.assertThat(secondResponse.response.status).isEqualTo(412) + Assertions.assertThat(secondResponse.response.getHeader("Cache-Control")).isEqualTo("max-age=0")
111-137: Reduce blanket retries and cleanups.The retry matcher swallows NPEs and data integrity issues that may hide real regressions. Consider narrowing exceptions or lowering retries once flakiness is addressed.
backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/batch/V2ExportController.kt (2)
171-181: Close streams via use{} to avoid leaks on error.- return ResponseEntity.ok().headers(headers).body( - streamingResponseBodyProvider.createStreamingResponseBody { out: OutputStream -> - IOUtils.copy(stream, out) - stream.close() - out.close() - }, - ) + return ResponseEntity.ok().headers(headers).body( + streamingResponseBodyProvider.createStreamingResponseBody { out: OutputStream -> + stream.use { input -> out.use { IOUtils.copy(input, it) } } + }, + )
184-192: Use use{} for ZipOutputStream and input streams.- return ResponseEntity.ok().headers(httpHeaders).body( - streamingResponseBodyProvider.createStreamingResponseBody { out: OutputStream -> - streamZipResponse(out, exported) - }, - ) + return ResponseEntity.ok().headers(httpHeaders).body( + streamingResponseBodyProvider.createStreamingResponseBody { out: OutputStream -> + streamZipResponse(out, exported) + }, + ) @@ - private fun streamZipResponse( + private fun streamZipResponse( out: OutputStream, exported: Map<String, InputStream>, ) { - val zipOutputStream = ZipOutputStream(out) - - exported.forEach { (fileAbsolutePath, stream) -> - zipOutputStream.putNextEntry(ZipEntry(fileAbsolutePath)) - IOUtils.copy(stream, zipOutputStream) - stream.close() - zipOutputStream.closeEntry() - } - - zipOutputStream.close() + ZipOutputStream(out).use { zipOutputStream -> + exported.forEach { (fileAbsolutePath, stream) -> + zipOutputStream.putNextEntry(ZipEntry(fileAbsolutePath)) + stream.use { IOUtils.copy(it, zipOutputStream) } + zipOutputStream.closeEntry() + } + } }Also applies to: 194-208
backend/app/src/test/kotlin/io/tolgee/websocket/WebsocketAuthenticationTest.kt (1)
79-145: Verify test annotation usage for PAK/PAT tests.Several tests that validate PAK or PAT authentication use the
@ProjectJWTAuthTestMethodannotation (lines 79, 88, 115, 127, 136, 149) rather than@ProjectApiKeyAuthTestMethod. While this might be intentional if the annotations only control REST API authentication setup (like thecreateKey()call), it could be confusing to readers.Please verify that this annotation usage is correct and consider adding a comment explaining why JWT annotations are used for PAK/PAT websocket authentication tests if this is intentional.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (16)
backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/batch/V2ExportController.kt(3 hunks)backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/translation/TranslationsController.kt(3 hunks)backend/api/src/main/kotlin/io/tolgee/component/ProjectLastModifiedManager.kt(1 hunks)backend/api/src/main/kotlin/io/tolgee/controllers/ExportController.kt(4 hunks)backend/api/src/main/kotlin/io/tolgee/websocket/WebSocketConfig.kt(6 hunks)backend/api/src/main/kotlin/io/tolgee/websocket/WebsocketAuthenticationResolver.kt(1 hunks)backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ExportController/V2ExportAllFormatsTest.kt(2 hunks)backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ExportController/V2ExportControllerCachingTest.kt(1 hunks)backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ExportController/V2ExportControllerTest.kt(7 hunks)backend/app/src/test/kotlin/io/tolgee/batch/BatchJobTestUtil.kt(1 hunks)backend/app/src/test/kotlin/io/tolgee/controllers/ExportControllerTest.kt(2 hunks)backend/app/src/test/kotlin/io/tolgee/websocket/AbstractWebsocketTest.kt(2 hunks)backend/app/src/test/kotlin/io/tolgee/websocket/WebsocketAuthenticationTest.kt(1 hunks)backend/app/src/test/kotlin/io/tolgee/websocket/WebsocketTestHelper.kt(7 hunks)backend/data/src/main/kotlin/io/tolgee/model/Pat.kt(2 hunks)webapp/src/websocket-client/WebsocketClient.ts(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
backend/app/src/test/kotlin/io/tolgee/controllers/ExportControllerTest.kt (2)
backend/testing/src/main/kotlin/io/tolgee/AbstractSpringTest.kt (2)
setForcedDate(272-274)clearForcedDate(276-278)backend/testing/src/main/kotlin/io/tolgee/fixtures/ProjectAuthRequestPerformer.kt (1)
performProjectAuthGet(36-36)
backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ExportController/V2ExportAllFormatsTest.kt (1)
backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ExportController/V2ExportControllerTest.kt (1)
parseZip(227-237)
backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ExportController/V2ExportControllerCachingTest.kt (1)
backend/testing/src/main/kotlin/io/tolgee/AbstractSpringTest.kt (2)
clearCaches(231-236)clearForcedDate(276-278)
🪛 detekt (1.23.8)
backend/api/src/main/kotlin/io/tolgee/websocket/WebSocketConfig.kt
[warning] 94-94: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
🔇 Additional comments (11)
backend/data/src/main/kotlin/io/tolgee/model/Pat.kt (1)
3-3: LGTM!The import is necessary for the new
tokenWithPrefixproperty.webapp/src/websocket-client/WebsocketClient.ts (1)
7-15: Fix type inconsistency:jwtTokenis required but treated as optional in code.The type definition declares
jwtTokenas a required string (line 7), but line 99 checksif (options.authentication.jwtToken), treating it as optional. This inconsistency should be resolved by either:
- Making
jwtTokenoptional in the type:jwtToken?: string;, or- Removing the truthiness check and assuming the token is always provided
Additionally, consider whether this field should accept PAK/PAT tokens. If the backend WebSocket authentication handles multiple token types, renaming to a generic
tokenfield or updating the field documentation would improve clarity.backend/app/src/test/kotlin/io/tolgee/websocket/WebsocketTestHelper.kt (1)
198-204: LGTM: Clear authentication wrapper with proper validation.The Auth data class properly enforces the constraint that exactly one authentication method must be provided, preventing misconfiguration in tests.
backend/api/src/main/kotlin/io/tolgee/websocket/WebSocketConfig.kt (2)
67-97: Consider logging the caught exception for debugging.The API key permission logic correctly validates project match and scope. However, the exception handling at lines 92-96 swallows the original exception, which could make debugging permission issues difficult.
While returning a generic "Forbidden" message to websocket clients is appropriate for security, consider logging the original exception for operators:
val user = authentication.principal try { securityService.checkProjectPermissionNoApiKey(projectId = projectId, Scope.KEYS_VIEW, user) } catch (e: Exception) { + logger.debug("Project permission check failed for user ${user.id} on project $projectId", e) throw MessagingException("Forbidden") }This addresses the static analysis warning about the swallowed exception while maintaining security.
99-123: Good security decision to block API keys from user topics.The explicit rejection of API key authentication for user-specific topics (lines 114-117) is correct, as API keys are project-scoped and should not access user-specific resources like notifications.
Minor inconsistency: Line 110 throws "Forbidden" for null authentication, while line 78 (in
checkProjectPathPermissionsAuth) throws "Unauthenticated". Consider using "Unauthenticated" here as well for consistency:if (authentication == null) { - throw MessagingException("Forbidden") + throw MessagingException("Unauthenticated") }backend/app/src/test/kotlin/io/tolgee/controllers/ExportControllerTest.kt (3)
34-40: Good refactoring: centralized test setup and proper cleanup.The new setup/teardown methods eliminate duplication and ensure proper cleanup of both test data and forced dates after each test.
Also applies to: 114-118
96-112: Good test coverage for HTTP conditional requests.The new tests properly verify Last-Modified header generation and 304 Not Modified responses. Using forced dates ensures deterministic test behavior.
120-139: Clean test helpers for conditional request testing.The helper methods properly handle HTTP date header formatting and comparison at second-level precision, which is appropriate since HTTP date headers don't include milliseconds.
backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/translation/TranslationsController.kt (1)
165-177: Conditional request handling is properly implemented.Verification confirms
ProjectLastModifiedManager.onlyWhenProjectDataChangedcorrectly:
- Sets Last-Modified header via
.lastModified(lastModified)on 200 responses- Returns null (HTTP 304 Not Modified) when
request.checkNotModified()is true- Handles missing modification timestamps by caching the current time via
ProjectTranslationLastModifiedManager.getLastModified()- Sets Cache-Control header to
max-age=0for request validationThe refactoring safely centralizes conditional request logic across controllers without losing functionality.
backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/batch/V2ExportController.kt (2)
76-93: Good: conditional export wrapped via ProjectLastModifiedManager.The flow and header propagation look correct.
Please confirm all external callers/tests are updated for the now-nullable return type.
53-72: Docstrings: solid.Nice API docs on conditional requests (304/412 + Last-Modified/Cache-Control).
Also applies to: 95-120
backend/api/src/main/kotlin/io/tolgee/websocket/WebsocketAuthenticationResolver.kt
Show resolved
Hide resolved
backend/app/src/test/kotlin/io/tolgee/websocket/WebsocketAuthenticationTest.kt
Outdated
Show resolved
Hide resolved
backend/app/src/test/kotlin/io/tolgee/websocket/WebsocketAuthenticationTest.kt
Show resolved
Hide resolved
backend/app/src/test/kotlin/io/tolgee/websocket/WebsocketAuthenticationTest.kt
Outdated
Show resolved
Hide resolved
backend/app/src/test/kotlin/io/tolgee/websocket/WebsocketAuthenticationTest.kt
Outdated
Show resolved
Hide resolved
3656b23 to
f441448
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/api/src/main/kotlin/io/tolgee/websocket/WebSocketConfig.kt (1)
67-101: Don’t swallow the underlying permission failure; attach the cause.
Detekt is right: rethrowing withoutcauseloses diagnostics.try { securityService.checkProjectPermissionNoApiKey(projectId = projectId, Scope.KEYS_VIEW, user) } catch (e: Exception) { - throw MessagingException("Forbidden") + throw MessagingException("Forbidden", e) }
🧹 Nitpick comments (3)
backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/batch/V2ExportController.kt (1)
96-131: (Optional) Stepdown Rule:exportPostcallsexportDatabut appears below it
Consider movingexportPostaboveexportData(or extracting shared logic into a private helper below both) to follow the repo’s Stepdown guidance.backend/api/src/main/kotlin/io/tolgee/websocket/WebSocketConfig.kt (2)
81-93: Align unauthenticated/forbidden semantics (project vs user).
Project topics throw"Unauthenticated"whenauthentication == null, while user topics throw"Forbidden". If this is intentional (avoid user-topic enumeration), consider applying the same policy consistently (or add a short comment why they differ).Also applies to: 117-125
127-130: Redundant cast:authenticationis alreadyTolgeeAuthentication?.
This can be simplified without changing behavior.- val user = (authentication as? TolgeeAuthentication)?.principal + val user = authentication.principal
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/batch/V2ExportController.kt(3 hunks)backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/translation/TranslationsController.kt(3 hunks)backend/api/src/main/kotlin/io/tolgee/component/ProjectLastModifiedManager.kt(1 hunks)backend/api/src/main/kotlin/io/tolgee/controllers/ExportController.kt(4 hunks)backend/api/src/main/kotlin/io/tolgee/websocket/WebSocketConfig.kt(6 hunks)backend/api/src/main/kotlin/io/tolgee/websocket/WebsocketAuthenticationResolver.kt(1 hunks)backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ExportController/V2ExportAllFormatsTest.kt(2 hunks)backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ExportController/V2ExportControllerCachingTest.kt(1 hunks)backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ExportController/V2ExportControllerTest.kt(5 hunks)backend/app/src/test/kotlin/io/tolgee/batch/BatchJobTestUtil.kt(1 hunks)backend/app/src/test/kotlin/io/tolgee/controllers/ExportControllerTest.kt(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
- backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ExportController/V2ExportControllerTest.kt
- backend/app/src/test/kotlin/io/tolgee/batch/BatchJobTestUtil.kt
- backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/translation/TranslationsController.kt
- backend/api/src/main/kotlin/io/tolgee/component/ProjectLastModifiedManager.kt
- backend/api/src/main/kotlin/io/tolgee/websocket/WebsocketAuthenticationResolver.kt
- backend/app/src/test/kotlin/io/tolgee/controllers/ExportControllerTest.kt
- backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ExportController/V2ExportControllerCachingTest.kt
🧰 Additional context used
📓 Path-based instructions (3)
backend/**/*.kt
📄 CodeRabbit inference engine (AGENTS.md)
backend/**/*.kt: After modifying JPA entities, run./gradlew diffChangeLogto generate Liquibase changelog entries (add--no-daemonflag if docker command not found)
Run./gradlew ktlintFormatbefore committing code
Files:
backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/batch/V2ExportController.ktbackend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ExportController/V2ExportAllFormatsTest.ktbackend/api/src/main/kotlin/io/tolgee/websocket/WebSocketConfig.ktbackend/api/src/main/kotlin/io/tolgee/controllers/ExportController.kt
**/*.{ts,tsx,js,jsx,kt,kts}
⚙️ CodeRabbit configuration file
As part of review, please check if the file follows 'The Stepdown Rule': The most important, high-level concepts should be at the top (like a newspaper headline and opening paragraph). Details should increase as you read downward. Functions should be ordered so that a caller appears before the functions it calls. When working with JavaScript components, we allow the main component to live at the bottom of the file as an exception to the rule.
Files:
backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/batch/V2ExportController.ktbackend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ExportController/V2ExportAllFormatsTest.ktbackend/api/src/main/kotlin/io/tolgee/websocket/WebSocketConfig.ktbackend/api/src/main/kotlin/io/tolgee/controllers/ExportController.kt
backend/**/*Test.kt
📄 CodeRabbit inference engine (AGENTS.md)
backend/**/*Test.kt: Use TestData classes for test setup with TestDataService for saving and cleaning test data in unit tests
Use.andAssertThatJsonfor testing API JSON responses in tests
Files:
backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ExportController/V2ExportAllFormatsTest.kt
🧠 Learnings (2)
📚 Learning: 2025-11-28T15:12:44.060Z
Learnt from: CR
Repo: tolgee/tolgee-platform PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T15:12:44.060Z
Learning: Applies to backend/**/*Test.kt : Use `.andAssertThatJson` for testing API JSON responses in tests
Applied to files:
backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ExportController/V2ExportAllFormatsTest.kt
📚 Learning: 2025-11-28T15:12:44.060Z
Learnt from: CR
Repo: tolgee/tolgee-platform PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T15:12:44.060Z
Learning: Applies to backend/**/*Test.kt : Use TestData classes for test setup with TestDataService for saving and cleaning test data in unit tests
Applied to files:
backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ExportController/V2ExportAllFormatsTest.kt
🪛 detekt (1.23.8)
backend/api/src/main/kotlin/io/tolgee/websocket/WebSocketConfig.kt
[warning] 98-98: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
backend/api/src/main/kotlin/io/tolgee/controllers/ExportController.kt
[warning] 65-65: String.format("attachment; filename="%s.zip"", projectHolder.project.name) uses implicitly default locale for string formatting.
(detekt.potential-bugs.ImplicitDefaultLocale)
[warning] 78-78: String.format("%s.json", key) uses implicitly default locale for string formatting.
(detekt.potential-bugs.ImplicitDefaultLocale)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Frontend static check 🪲
- GitHub Check: Build backend 🏗️
🔇 Additional comments (4)
backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ExportController/V2ExportAllFormatsTest.kt (1)
1-1: Package + AssertJ import alignment looks good
Matches the v2 export test suite conventions; no behavior change here.Also applies to: 10-10
backend/api/src/main/kotlin/io/tolgee/controllers/ExportController.kt (1)
7-8: Conditional export gating looks consistent; please verify 304 behavior for this deprecated endpoint
SincedoExportJsonZipnow returns nullableResponseEntity<StreamingResponseBody>?, double-check the manager produces the intended “Not Modified” response (status + headers) for/jsonZip.Also applies to: 39-46, 52-88
backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/batch/V2ExportController.kt (1)
5-6: Good adoption of ProjectLastModifiedManager; please verify POST conditional semantics (412) match docs
The wiring/header propagation looks right; just ensure the manager indeed returns 412 for POST when not modified (and 304 for GET), as documented in the OpenAPI description.Also applies to: 45-52, 77-94
backend/api/src/main/kotlin/io/tolgee/websocket/WebSocketConfig.kt (1)
24-29: Constructor injection change looks fine; verify@Lazyis still needed.
If@Lazyis only here to break a circular dependency, consider documenting the cycle (or removing@Lazyif it’s no longer required).
| headersBuilder.header( | ||
| "Content-Disposition", | ||
| String.format("attachment; filename=\"%s.zip\"", projectHolder.project.name), | ||
| ).body( | ||
| streamingResponseBodyProvider.createStreamingResponseBody { out: OutputStream -> | ||
| val zipOutputStream = ZipOutputStream(out) | ||
| val translations = | ||
| translationService.getTranslations( | ||
| allLanguages.map { it.tag }.toSet(), | ||
| null, | ||
| projectHolder.project.id, | ||
| '.', | ||
| ) | ||
| for ((key, value) in translations) { | ||
| zipOutputStream.putNextEntry(ZipEntry(String.format("%s.json", key))) | ||
| val data = jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsBytes(value) | ||
| val byteArrayInputStream = ByteArrayInputStream(data) | ||
| IOUtils.copy(byteArrayInputStream, zipOutputStream) | ||
| byteArrayInputStream.close() | ||
| zipOutputStream.closeEntry() | ||
| } | ||
| zipOutputStream.close() | ||
| }, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid String.format(...) default-locale + avoid per-entry ObjectMapper creation
Use string templates (or Locale.ROOT) and instantiate the ObjectMapper once.
@@
- headersBuilder.header(
- "Content-Disposition",
- String.format("attachment; filename=\"%s.zip\"", projectHolder.project.name),
- )
+ headersBuilder.header("Content-Disposition", "attachment; filename=\"${projectHolder.project.name}.zip\"")
@@
- for ((key, value) in translations) {
- zipOutputStream.putNextEntry(ZipEntry(String.format("%s.json", key)))
- val data = jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsBytes(value)
+ val mapper = jacksonObjectMapper().writerWithDefaultPrettyPrinter()
+ for ((key, value) in translations) {
+ zipOutputStream.putNextEntry(ZipEntry("$key.json"))
+ val data = mapper.writeValueAsBytes(value)
val byteArrayInputStream = ByteArrayInputStream(data)
IOUtils.copy(byteArrayInputStream, zipOutputStream)
byteArrayInputStream.close()
zipOutputStream.closeEntry()
}Also applies to: 78-79
🧰 Tools
🪛 detekt (1.23.8)
[warning] 65-65: String.format("attachment; filename="%s.zip"", projectHolder.project.name) uses implicitly default locale for string formatting.
(detekt.potential-bugs.ImplicitDefaultLocale)
🤖 Prompt for AI Agents
In backend/api/src/main/kotlin/io/tolgee/controllers/ExportController.kt around
lines 63-66 (and similarly lines 78-79), replace the use of String.format(...)
for the Content-Disposition filename with a Kotlin string template (or
explicitly use Locale.ROOT if formatting is required) to avoid default-locale
dependence, and remove any per-entry ObjectMapper instantiation by creating a
single ObjectMapper instance (e.g., as a private val on the class or injected
bean) and reuse it for all serialization calls.
| if (accessor?.command == StompCommand.CONNECT) { | ||
| val tokenString = accessor.getNativeHeader("jwtToken")?.firstOrNull() | ||
| accessor.user = if (tokenString == null) null else jwtService.validateToken(tokenString) | ||
| val authorization = accessor.getNativeHeader("authorization")?.firstOrNull() | ||
| val xApiKey = accessor.getNativeHeader("x-api-key")?.firstOrNull() | ||
| val legacyJwt = accessor.getNativeHeader("jwtToken")?.firstOrNull() | ||
| accessor.user = websocketAuthenticationResolver.resolve(authorization, xApiKey, legacyJwt) | ||
| } | ||
|
|
||
| val user = (accessor?.user as? TolgeeAuthentication)?.principal | ||
| val authentication = accessor?.user as? TolgeeAuthentication | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential client break: header name casing/spelling might not match (Authorization vs authorization).
getNativeHeader("authorization") won’t see Authorization if native headers are case-sensitive in your setup; consider accepting both (and using getFirstNativeHeader).
- val authorization = accessor.getNativeHeader("authorization")?.firstOrNull()
- val xApiKey = accessor.getNativeHeader("x-api-key")?.firstOrNull()
- val legacyJwt = accessor.getNativeHeader("jwtToken")?.firstOrNull()
+ val authorization =
+ accessor.getFirstNativeHeader("authorization")
+ ?: accessor.getFirstNativeHeader("Authorization")
+ val xApiKey =
+ accessor.getFirstNativeHeader("x-api-key")
+ ?: accessor.getFirstNativeHeader("X-API-KEY")
+ ?: accessor.getFirstNativeHeader("X-Api-Key")
+ val legacyJwt = accessor.getFirstNativeHeader("jwtToken")
accessor.user = websocketAuthenticationResolver.resolve(authorization, xApiKey, legacyJwt)Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In backend/api/src/main/kotlin/io/tolgee/websocket/WebSocketConfig.kt around
lines 47 to 55, the code only reads the lowercase "authorization" header which
can miss "Authorization" or other casing; update the header retrieval to be
case-insensitive and use getFirstNativeHeader (or try both "authorization" and
"Authorization") for authorization, and similarly ensure "x-api-key" is fetched
with getFirstNativeHeader (and check "X-Api-Key" as a fallback) so you reliably
obtain the header values before calling websocketAuthenticationResolver.resolve;
keep the same variable names and fallbacks so behavior doesn’t change otherwise.
| val responseEntity = getExportResponse(params, exported) | ||
| headersBuilder.headers(responseEntity.headers) | ||
| responseEntity.body |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's somewhat messy that we create a response entity just to extract headers and body from it here. It ignores the status code, for example, which could be confusing.
| * This mechanism helps optimize performance by preventing export data computation and loading from the database when | ||
| * not modified. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not opposed to including a bit more comments, but the last paragraph feels like complete filler.
|
|
||
| else -> | ||
| runCatching { pakAuth(xApiKey) } | ||
| .onFailure { logger.debug("PAT authentication failed", it) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| .onFailure { logger.debug("PAT authentication failed", it) } | |
| .onFailure { logger.debug("PAK authentication failed", it) } |
| val hash = apiKeyService.hashKey(parsed) | ||
| val pak = apiKeyService.findDto(hash) ?: throw AuthenticationException(Message.INVALID_PROJECT_API_KEY) | ||
|
|
||
| if (pak.expiresAt?.before(java.util.Date()) == true) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this use CurrentDateProvider?
| val hash = patService.hashToken(key.substring(PAT_PREFIX.length)) | ||
| val pat = patService.findDto(hash) ?: throw AuthenticationException(Message.INVALID_PAT) | ||
|
|
||
| if (pat.expiresAt?.before(java.util.Date()) == true) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this use CurrentDateProvider?
| fun resolve( | ||
| authorizationHeader: String?, | ||
| xApiKeyHeader: String?, | ||
| legacyJwtHeader: String?, | ||
| ): TolgeeAuthentication? { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it a bad idea to pass StompHeaderAccessor instead and extract the headers here? I would do it this way, since this function is specific to WebSocket, and it would bring the header names here.
| if (!matchesProject || !hasScope) { | ||
| throw MessagingException("Forbidden") | ||
| } | ||
| return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't we still verify users' permissions? I believe these API key scopes are not filtered based on what the user has access to. If that's the case, when user scopes change, the user might no longer have enough scopes to view the project, even though the API key says otherwise.
Summary by CodeRabbit
New Features
Tests
✏️ Tip: You can customize this high-level summary in your review settings.