Commit Graph

2663 Commits

Author SHA1 Message Date
Marcel
c532ad21bf test(admin): add regression test for skipped section hidden during RUNNING
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:45:03 +02:00
Marcel
0e95bd9160 fix(import): add @Schema annotations and fix IOException test coverage
- Add @Schema(requiredMode = REQUIRED) to SkippedFile and ImportStatus
  record components so TypeScript codegen produces non-optional fields
  when generate:api is next run
- Extract openFileStream(File) as package-private method so the
  IOException path can be tested deterministically without relying on
  OS-level file permissions (which are bypassed when running as root)
- Replace assumeTrue-based IOException test with Mockito spy that stubs
  openFileStream — test now runs in CI unconditionally (45 tests, 0 skipped)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:45:03 +02:00
Marcel
e312cce4e1 fix(test): skip IOException test when running as root
setReadable(false) silently no-ops as root; check canRead() to guard
the assumption correctly so the test is skipped in Docker CI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:45:03 +02:00
Marcel
5587722800 fix(import): address PR review concerns
- remove duplicate List import in AdminControllerTest
- derive skipped() from skippedFiles.size() — drop redundant int field
- use machine codes for SkippedFile.reason (INVALID_PDF_SIGNATURE, FILE_READ_ERROR)
- map reason codes to i18n strings in ImportStatusCard (de/en/es)
- replace raw amber Tailwind classes with warning semantic token
- fix <summary> accessibility: replace list-none with rotating chevron SVG
- replace <p> with <span> inside <summary> (phrasing content rule)
- extract setupOneValidOneFakeImport() helper — remove 3x copy-paste
- add lenient mock to short-file test for defensive coverage
- add IOException path test for isPdfMagicBytes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:45:03 +02:00
Marcel
0451b6630c feat(admin): surface skipped file count in ImportStatusCard
Adds SkippedFile to the local ImportStatus type and updates
ImportStatusCard to show an amber skipped-count section with a
collapsible filename list in the DONE state. Only rendered when
skipped > 0. i18n keys added for de/en/es.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:45:03 +02:00
Marcel
f77fb79cd2 feat(import): validate PDF magic bytes before S3 upload
Reads first 4 bytes of each candidate file before upload; rejects any
file whose header does not match %PDF (0x25 0x50 0x44 0x46). Skipped
files are counted and collected in ImportStatus.skippedFiles so
operators can see what was rejected without querying Loki.

Breaking: ImportStatus record gains skipped + skippedFiles fields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:45:03 +02:00
Marcel
1247b51d9e chore(document): address non-blocking review feedback on lazy-fetch PR
All checks were successful
CI / Unit & Component Tests (push) Successful in 3m11s
CI / OCR Service Tests (push) Successful in 20s
CI / Backend Unit Tests (push) Successful in 3m41s
CI / fail2ban Regex (push) Successful in 44s
CI / Semgrep Security Scan (push) Successful in 19s
CI / Compose Bucket Idempotency (push) Successful in 1m0s
- Add @BatchSize(50) fallback comments on findBySenderId / findByReceiversId
- Replace silent size() discard in getRecentActivity test with assertThat isNotEmpty()
- Add ADR-022 reference comment above @JsonIgnoreProperties on Person and Tag
- Document within-open-transaction limitation in DocumentLazyLoadingTest Javadoc

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:30 +02:00
Marcel
7342c60952 fix(document): fix test assertion structure + add entity graph decision comments
- Refactor DocumentLazyLoadingTest: pull value assertions (assertThat) out
  of assertThatCode lambdas so failures surface as AssertionError rather
  than "unexpected exception: AssertionError" (review item 1)
- Add @EntityGraph("Document.full") to findBySenderId, findByReceiversId,
  findConversation, and findSinglePersonCorrespondence — all return full
  Documents to the controller for JSON serialization (review item 2)
- Add "// Callers access only ..." comments to un-graphed methods where no
  lazy associations are touched: findByTags_Id, findByStatus,
  findByMetadataCompleteFalse(Sort), findByMetadataCompleteFalse(Pageable)
- Remove "what" inline comments from @Transactional(readOnly=true)
  on getRecentActivity and getDocumentById — the why is in ADR-022 (item 4)
- Add named-graph coupling consequence to ADR-022: Document.java and
  DocumentRepository.java graph name strings must stay in sync (item 5)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:30 +02:00
Marcel
328bd2c3b4 docs(backend): document @Transactional(readOnly=true) exception in CLAUDE.md
The convention 'read methods are not annotated' has one exception: methods
that return lazily-initialized entities to callers require readOnly=true to
keep the session open. Documents the rule and links to ADR-022.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:30 +02:00
Marcel
db87a214fd docs(adr): add ADR-022 for EAGER→LAZY fetch strategy with @EntityGraph
Records context (2733 queries/24 requests), the two-graph decision,
@BatchSize fallback, @Transactional(readOnly=true) session-lifetime
requirement, and alternatives considered.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:30 +02:00
Marcel
ad95b09046 refactor(document): extract factory helpers in DocumentLazyLoadingTest
Replace repeated personRepository.save/tagRepository.save/documentRepository.save
boilerplate with savedPerson(), savedTag(), savedDocument() helpers.
Each test body is now 2-3 lines of relevant setup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:30 +02:00
Marcel
1e95ca979b test(document): add query-count assertion for findAll(Spec) non-paginated path
List<Document> findAll(Specification) is called in DocumentService for
receiver-sort, sender-sort, and conversation queries but had no query-count
coverage. Asserts ≤5 statements for 5 docs with @EntityGraph(Document.list).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:30 +02:00
Marcel
1cae9ac311 test(document): assert non-empty result in receiverSort lazy-loading test
assertThatCode(() -> service.searchDocuments(...)) passed vacuously on an
empty page; capture the result, assert totalElements > 0, then assert
getSender().getLastName() is accessible post-return.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:30 +02:00
Marcel
72bd2e11b4 test(document): enable statistics before findById query-count assertion
Without setStatisticsEnabled(true) the counter stays 0 and ≤2 passes
vacuously when the test runs in isolation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:30 +02:00
Marcel
69b3c663c0 fix(document): remove @BatchSize from @ManyToOne sender — not supported
Hibernate throws AnnotationException at startup when @BatchSize is placed
on a @ManyToOne field. @BatchSize is only valid on collections (@OneToMany,
@ManyToMany, @ElementCollection). The N+1 for sender is already covered by
the @EntityGraph overrides on DocumentRepository.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:30 +02:00
Marcel
f470a39ad2 test(document): strengthen getRecentActivity smoke test for post-return access
Previous version only asserted the method call didn't throw. Now the test
captures the returned list and asserts that sender.getLastName() and
tags.size() are accessible outside the transaction, which is the scenario
that would have failed with a LazyInitializationException.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:30 +02:00
Marcel
e2f287d3d8 docs(document): add WHY comments to @Transactional(readOnly=true) methods
These annotations deviate from the project convention (read methods are
normally unannotated). The comment explains that the session must stay
open for callers to access lazy-loaded collections post-return, preventing
future developers from removing the annotation as a cleanup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:30 +02:00
Marcel
914e438793 perf(document): add @BatchSize(50) to sender and trainingLabels
Consistent with the @BatchSize already on receivers and tags. Any lazy
code path not covered by an entity graph will batch-load these associations
instead of issuing one query per document.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:30 +02:00
Marcel
6266c5f721 perf(document): add @EntityGraph(Document.list) for findAll(Pageable)
getRecentActivity calls findAll(Pageable) — the JpaRepository overload
not covered by the existing Specification variants. Without this override,
sender is loaded N+1 per document. Now applies Document.list graph so
sender and tags are fetched eagerly for every findAll(Pageable) call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:30 +02:00
Marcel
f564c30ae2 test(document): add query-count assertion for findAll(Pageable) path
Adds failing test: findAll(Pageable) must not N+1 sender for 5 docs.
Without @EntityGraph override for this overload, each document triggers
a separate SELECT for its lazy sender.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:30 +02:00
Marcel
a5ce46359a test(document): remove redundant global generate_statistics from test config
Stats tracking is already enabled per-test via setStatisticsEnabled(true);
enabling it globally added unnecessary overhead to every test in the suite.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:30 +02:00
Marcel
b45953e567 test(document): add @SpringBootTest smoke tests for lazy-loading correctness
Five integration tests verify that DocumentService and DashboardService
do not throw LazyInitializationException after the EAGER→LAZY migration:
getDocumentById, getRecentActivity, searchDocuments (receiver/sender sort),
and dashboardService.getResume.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:30 +02:00
Marcel
36d1b9c038 fix(document): add @Transactional to read methods that access lazy collections
- getDocumentById: add @Transactional(readOnly=true) — calls
  tagService.resolveEffectiveColors(doc.getTags()) which requires an open
  session after the LAZY switch
- getRecentActivity: add @Transactional(readOnly=true) — callers may access
  tags/receivers on the returned list; keeps session open for @BatchSize fetches
- updateDocumentTags: add @Transactional — write method was missing annotation

Also adds @JsonIgnoreProperties({"hibernateLazyInitializer","handler"}) to
Person and Tag to prevent Jackson serialization errors on uninitialized
lazy proxies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:30 +02:00
Marcel
56bcbcdd5c refactor(document): switch collections to LAZY + add @EntityGraph + @BatchSize
- receivers, tags, trainingLabels: FetchType.EAGER → FetchType.LAZY
- sender: add explicit FetchType.LAZY (was implicitly lazy, now explicit)
- @NamedEntityGraph("Document.full"): sender + receivers + tags
- @NamedEntityGraph("Document.list"): sender + tags
- DocumentRepository.findById overridden with @EntityGraph("Document.full")
- DocumentRepository.findAll(Specification, Pageable) overridden with
  @EntityGraph("Document.list")
- DocumentRepository.findAll(Specification) overridden with
  @EntityGraph("Document.list") for RECEIVER/SENDER sort paths
- @BatchSize(50) on receivers and tags as fallback for any list path
  that does not go through an @EntityGraph method

Fixes issue #467.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:30 +02:00
Marcel
9b9bfde843 test(document): add query-count assertions for findAll + findById entity graphs
Adds Hibernate statistics to the test config and two new tests in
DocumentRepositoryTest:
- findAll_withSpecAndPageable asserts ≤5 statements for 10 documents
  (currently RED: EAGER @ManyToMany generates 31 secondary SELECTs)
- findById regression guard verifies collections load in ≤2 statements

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:30 +02:00
Marcel
164a917d95 fix(auth): tighten API URL match, add Retry-After header, and add missing tests
Some checks failed
CI / fail2ban Regex (push) Has been cancelled
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Semgrep Security Scan (push) Has been cancelled
CI / Compose Bucket Idempotency (push) Has been cancelled
- frontend/hooks.server.ts: replace request.url.includes('/api/') with
  new URL(request.url).pathname.startsWith('/api/') so a page named
  /my-api/something cannot accidentally match the API gate
- DomainException: add optional retryAfterSeconds field and a new
  tooManyRequests() factory overload that carries the value
- LoginRateLimiter: pass windowMinutes * 60 as retryAfterSeconds when
  throwing TOO_MANY_LOGIN_ATTEMPTS (RFC 6585 §4 SHOULD)
- GlobalExceptionHandler: emit Retry-After header when retryAfterSeconds
  is set on a DomainException
- RateLimitInterceptor: emit Retry-After: 60 on 429 responses (1-min
  window matches the existing MAX_REQUESTS_PER_MINUTE logic)
- LoginRateLimiterTest: assert retryAfterSeconds equals window duration
- RateLimitInterceptorTest: assert Retry-After header is set on 429
- JdbcSessionRevocationAdapterIntegrationTest: new @SpringBootTest +
  Testcontainers test verifying revokeAll deletes all spring_session rows
  and revokeOther leaves the current session intact

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
96c0aa592c fix(auth): address PR #617 review feedback on CSRF/rate-limit implementation
- Remove unreachable `&& !xsrfToken` condition from `handleFetch` guard;
  simplify the redundant `cookieParts.length > 0` check that follows it
- Add `TOO_MANY_LOGIN_ATTEMPTS` to both Error Handling sections in CLAUDE.md
  (backend and frontend) so LLMs are aware of the code without looking it up
- Add reverse-proxy IP trust and IPv6 address-cycling caveats to ADR-022
  Consequences section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
d8520d9714 devops(deps): add bucket4j-core to Renovate package rules
bucket4j-core 8.10.1 is manually pinned in pom.xml outside the Spring BOM.
Adds a packageRules entry so Renovate tracks it: patch updates auto-merge,
minor/major updates open PRs for manual review.

Addresses Tobias Concern 1 from PR #617 review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
873d668653 test(login): add browser component test for rate-limited login UI state
Renders LoginPage with form.rateLimited=true and asserts that the
role="alert" div (clock icon + error message) is visible in the browser.
Previously only the form action's rateLimited=true return value was tested;
now the rendered UI is also verified.

Addresses Sara Concern 4 / Elicit open question from PR #617 review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
4e257a7ca4 test(auth): add integration-level CSRF rejection test; fix SessionRevocationPort wiring
Integration test:
- Adds post_without_csrf_token_returns_403_CSRF_TOKEN_MISSING to
  AuthSessionIntegrationTest, verifying CSRF is active end-to-end (not just
  in @WebMvcTest slices).

SessionRevocationConfig (new):
- Replaces fragile @ConditionalOnBean/@ConditionalOnMissingBean on @Service
  beans with a single @Configuration @Bean method that accepts
  JdbcIndexedSessionRepository as @Autowired(required=false). Spring
  resolves the optional parameter reliably after auto-configuration fires,
  choosing JdbcSessionRevocationAdapter when available and
  NoOpSessionRevocationAdapter otherwise.
- JdbcSessionRevocationAdapter and NoOpSessionRevocationAdapter are now
  plain implementation classes (no @Service/@Conditional annotations).

Addresses Sara Concern 2 from PR #617 review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
d0bb6729cd test(user): add CSRF failure tests for changePassword and forceLogout endpoints
Adds two @WebMvcTest assertions verifying that POST /api/users/me/password
and POST /api/users/{id}/force-logout without an XSRF-TOKEN header return
403 with code CSRF_TOKEN_MISSING.

Addresses Nora Concern 9 from PR #617 review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
32ede3e3ce refactor(test): use static imports for verify/assertThat in controller and rate-limiter tests
UserControllerTest: replaces fully-qualified org.mockito.Mockito.verify() and
ArgumentMatchers.eq() with the static imports already present in the file.
LoginRateLimiterTest: replaces three org.assertj.core.api.Assertions.assertThat()
calls with the static-import form; adds missing assertThat import.

Addresses Felix Suggestions 2 and 4 from PR #617 review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
5da78e5e30 docs(architecture): update CSRF section and add CSRF_TOKEN_MISSING / TOO_MANY_LOGIN_ATTEMPTS error codes
- Remove stale "CSRF protection is disabled" claim; describe the double-submit
  cookie pattern now in use (CookieCsrfTokenRepository + X-XSRF-TOKEN header)
- Link to ADR-022 for the full rationale
- Add CSRF_TOKEN_MISSING and TOO_MANY_LOGIN_ATTEMPTS to the exception row

Fixes Markus's blocker.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
cb108faaf8 refactor(auth): replace @Autowired(required=false) with SessionRevocationPort + constructor injection
Extract SessionRevocationPort interface with JdbcSessionRevocationAdapter
(@ConditionalOnBean) and NoOpSessionRevocationAdapter (@ConditionalOnMissingBean).
AuthService now uses @RequiredArgsConstructor with final fields for both
LoginRateLimiter and SessionRevocationPort, removing all null guards.
AuthServiceTest drops ReflectionTestUtils.setField and uses @Mock on the port.

Fixes Felix's blocker: @Autowired(required=false) field injection in AuthService.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
611b82ccde refactor(user): migrate UserController to @RequiredArgsConstructor + final fields
The circular-dependency that originally forced @AllArgsConstructor was
removed when changePassword orchestration moved into the controller.
No cycle now exists between UserController, UserService, AuthService,
or AuditService — final fields and constructor injection are safe again.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
64d8f9d904 fix(auth): normalise email to lowercase before rate-limit key lookup
Case variants of the same address (e.g. User@EXAMPLE.COM vs user@example.com)
now share a single Bucket4j bucket, preventing a trivial bypass of per-email
limits via mixed-case submissions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
6f452a9a8b docs(claude): add LoginRateLimiter and RateLimitProperties to auth package entry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
20fe5637c1 docs(arch): update security C4 diagram for CSRF + rate limiting
Remove stale "CSRF is disabled pending #524" note; update secFilter
description to reflect the enabled double-submit cookie pattern.
Add LoginRateLimiter and RateLimitProperties components with their
relationships to AuthService. Update frontend→secFilter rel to show
X-XSRF-TOKEN header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
9bf8cf831d fix(login): add role=alert to error divs; fix clock icon color to red
Regular error div was missing role="alert" — screen readers did not
announce it on dynamic display. Rate-limited clock icon used text-ink-3
(muted grey) instead of text-red-600, visually inconsistent with the
surrounding error text. Also removes the erroneous aria-invalid="true"
from the rate-limit alert div (not a permitted attribute on role=alert).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
9f4a1141ef docs(arch): update auth sequence diagram to Phase 2 (CSRF, rate limit, revocation)
Extends the diagram from ADR-020 Phase 1 to cover:
- Rate limiter gate before credential validation in login
- CSRF double-submit cookie handshake for mutating requests
- Session revocation on password change (revokeOtherSessions) and
  password reset (revokeAllSessions)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
cb818f4bfa docs(adr): add ADR-022 for CSRF, session revocation, and rate limiting
Documents the double-submit cookie CSRF pattern, sequential token-bucket
rate limiter with refund mechanic, and session revocation on password
change/reset — all implemented as part of issue #524.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
9c195ff5cb refactor(security): extract static ERROR_WRITER; update ADR ref to ADR-022
Replaces per-invocation new ObjectMapper() in the accessDeniedHandler
lambda with a static field (avoids repeated allocation). ObjectMapper
cannot be injected in SecurityConfig because @WebMvcTest slices exclude
JacksonAutoConfiguration; the static instance is safe since the response
only serialises fixed String keys.

Also corrects the ADR cross-reference in the CSRF comment from ADR-020
(Spring Session JDBC) to ADR-022 (CSRF + session revocation).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
54d32c9163 test(security): add CSRF rejection test to DocumentControllerTest
Adds regression coverage for the custom accessDeniedHandler in
SecurityConfig: a POST without X-XSRF-TOKEN returns 403 with error
code CSRF_TOKEN_MISSING, not a generic Spring 403.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
0b5ab73963 fix(auth): sequential rate-limit check with ipEmail token refund on IP failure
Addresses Felix (blocker 1): the old implementation consumed from both buckets
before checking either result, silently eroding the per-email quota when only the
per-IP limit was blocking. The fix checks ipEmail first, then IP; on IP failure it
refunds the ipEmail token so legitimate users behind a shared IP are not penalised.

Also adds two new test cases:
- different_email_from_same_ip_not_blocked_by_sibling_email_exhaustion (Sara)
- ip_exhaustion_does_not_consume_ipEmail_tokens_for_blocked_attempts (red → green)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
956387471d fix(auth): guard revokeOtherSessions/revokeAllSessions against null sessionRepository
Addresses Nora (blocker 1) and Felix (suggestion): both revocation methods
now return 0 immediately when sessionRepository is unavailable (non-web
test contexts where JdbcHttpSessionAutoConfiguration does not fire).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
78fd9e026e feat(frontend): add CSRF injection, rate-limit i18n, and 429 login handling
- handleFetch injects X-XSRF-TOKEN + XSRF-TOKEN cookie on all mutating
  backend API requests (double-submit cookie pattern); generates a fresh
  UUID when no XSRF-TOKEN cookie exists yet
- ErrorCode union gains CSRF_TOKEN_MISSING and TOO_MANY_LOGIN_ATTEMPTS;
  getErrorMessage maps both to i18n keys
- de/en/es messages add error_csrf_token_missing and
  error_too_many_login_attempts translations
- Login action maps HTTP 429 to fail(429, { ..., rateLimited: true });
  page shows a muted clock icon with aria-invalid on rate-limit errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
4d6fb06e02 feat(auth): add Bucket4j + Caffeine login rate limiter (10/15 min per IP+email, 20/15 min per IP)
LoginRateLimiter uses two Caffeine LoadingCaches of Bucket4j buckets —
one keyed on IP:email (10 attempts/15 min) and one on IP alone (20/15 min
backstop). Exceeding either throws DomainException(TOO_MANY_LOGIN_ATTEMPTS)
and emits LOGIN_RATE_LIMITED audit. Successful login invalidates both
buckets via invalidateOnSuccess. Buckets expire after windowMinutes of
inactivity (no clock advance needed — Caffeine handles eviction).
AuthService integrates it as an optional @Autowired field so non-web
test contexts still work without a Caffeine dependency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
8944f8bb44 feat(auth): revoke all sessions on password reset
After updating the user password during a reset flow, calls
authService.revokeAllSessions(email) to invalidate every active session
for the account — prevents an attacker with a stolen session from
retaining access after the owner resets their password.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
1b178767ab feat(auth): revoke other sessions on password change; add force-logout endpoint
changePassword now calls authService.revokeOtherSessions() after the
password is updated and emits a LOGOUT audit with reason=password_change.

POST /api/users/{id}/force-logout (ADMIN_USER permission) revokes all
sessions for the target user and emits ADMIN_FORCE_LOGOUT audit. Returns
{"revokedCount": N} with 200.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
7d10653c41 feat(auth): add revokeOtherSessions and revokeAllSessions to AuthService
Uses JdbcIndexedSessionRepository (optional field — null-safe in non-web
test contexts) to delete all sessions for a principal except the current
one (revokeOtherSessions) or all sessions unconditionally (revokeAllSessions).
Both methods return the count of deleted sessions for audit payloads.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00