- 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>
Also switches pom.xml to spring-boot-starter-session-jdbc (Spring Boot 4.x
split the session auto-config into a separate starter; spring-session-jdbc
alone does not register JdbcSessionAutoConfiguration).
Adds SpringSessionConfig#cookieSerializer bean to configure fa_session name
and SameSite=Strict (spring.session.cookie.* properties are no longer
supported by the Boot 4.x auto-configuration layer).
Cleans up application.yaml / application-dev.yaml: removes store-type: jdbc
and the unsupported cookie.* keys.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sentry-spring-boot-starter-jakarta 8.5.0 does not support Spring Boot 4.0 —
it logs an "Incompatible Spring Boot Version" warning and its SentryAutoConfiguration
crashes SF7 bean-name generation. sentry-spring-boot-4 (added in 8.21.0) is the
dedicated Spring Boot 4 module with a fixed auto-configuration class.
- Replace sentry-spring-boot-starter-jakarta:8.5.0 with sentry-spring-boot-4:8.41.0
- Delete SentryConfig.java — workaround no longer needed, auto-config handles init
- Remove spring.autoconfigure.exclude from application.yaml + application-test.yaml
- Delete SentryConfigTest.java — tested the deleted workaround class
- Update ApplicationContextTest: assert Sentry.isEnabled() is false when no DSN set
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SentryAutoConfiguration$HubConfiguration$SentrySpanRestClientConfiguration is a triply-
nested @Configuration class conditionally loaded when RestClient is on the classpath
(always true on Spring Framework 7). Spring Framework 7's bean name generator fails
on such deeply-nested @Import-ed classes, crashing every @SpringBootTest context.
Replace the broken auto-configuration with a minimal SentryConfig bean that calls
Sentry.init() with the same properties (DSN, environment, sample rate, PII guard,
DomainException filter). Unexpected 5xx exceptions are forwarded to Sentry via
Sentry.captureException() in GlobalExceptionHandler.handleGeneric().
Also add management.server.port=0 to application-test.yaml to eliminate TIME_WAIT
conflicts from @DirtiesContext restarts on the fixed management port 8081 (see #593).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dedicated thread pool (core=1, max=2, queue=200) with CallerRunsPolicy
for back-pressure. Keeps thumbnail rendering off the shared taskExecutor
used by OCR and out of the AbortPolicy queue that drops work on overflow.
Quick-upload batches (15+ files) now apply back-pressure instead of
silently dropping thumbnail jobs.
Refs #307
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AuditService.logAfterCommit() called writeLog() inline inside the afterCommit()
callback. At that point Spring's transaction synchronizations are still active on
the thread, so SimpleJpaRepository.save() throws IllegalStateException which the
catch block silently swallowed — leaving audit_log permanently empty.
Fix: submit writeLog() to auditExecutor so it runs on a fresh thread with no active
synchronization context. Also switch auditExecutor from CallerRunsPolicy to AbortPolicy
to prevent the bug from silently recurring when the queue fills under load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Narrow isTrustedProxy to RFC 1918 172.16-31.x.x (was 172.x.x.x)
- Add @Valid/@NotBlank/@Email to RegisterRequest and @Valid to AuthController
- Add FK constraint on invite_token_group_ids.group_id → user_groups(id)
- Add back-to-login link and <main> landmark to register error state
- Add component test suite for register/+page.svelte (11 tests)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without this guard any client could send X-Forwarded-For: <spoofed-ip>
and bypass per-IP rate limiting entirely.
Also switches expireAfterWrite → expireAfterAccess so the 1-minute
window starts at first request, not last, and fixes the .gitignore
entry that accidentally merged **/test-results/ and .worktrees/ into
one broken pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Queue capacity of 100 is disproportionate for 2 worker threads — a
backed-up queue would represent hours of unprocessed OCR jobs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CallerRunsPolicy would cause the HTTP request to hang for minutes
if the queue is full. AbortPolicy with queue=100 is safe — the queue
will never realistically fill for a family archive. If it somehow
does, a clear error is better than a silent multi-minute hang.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Better to wait than to error. Queue capacity 100 holds plenty of
OCR jobs. CallerRunsPolicy means if the queue is somehow full,
the request blocks instead of getting rejected with an exception.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The old pool (1 thread, queue=1) meant OCR blocked all other async
tasks (imports). Now 2 concurrent async tasks with a queue of 10
— enough for OCR + import to run in parallel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The OCR service was getting 403 Forbidden because it tried to
download PDFs from MinIO using plain internal URLs without
authentication. MinIO buckets are private.
- Add S3Presigner bean to MinioConfig
- FileService.generatePresignedUrl(): generates 15-min presigned URLs
- OcrService uses presigned URLs instead of plain internal URLs
- Remove unused s3InternalUrl / bucketName @Value fields from OcrService
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The password-reset E2E test changes the admin password mid-test and relies on a
UI step to restore it. If that step fails or the test is interrupted the account
is left with the wrong password, locking out all subsequent runs.
Fix: in DataInitializer.initE2EData (e2e profile only), always reset the admin
password to the value from ${app.admin.password} (default: admin123) on startup.
This is idempotent — it is safe to run even when the password is already correct.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add e2e to the dev Maven profile's spring.profiles.active so
DataInitializer always runs when developing/testing locally
- Create the reader test user independently of the person-seed guard
so it survives restarts where seed data already exists
- Set SPRING_PROFILES_ACTIVE=dev,e2e in docker-compose backend service
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the e2e profile is active, initE2EData (which creates a reader user)
can run before initAdminUser. The old count() == 0 guard then skips admin
creation entirely, causing every login test to fail with 401.
Switch to findByUsername(adminUsername).isEmpty() so the admin is created
regardless of which CommandLineRunner runs first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add PasswordResetToken entity, repository (Flyway V8 migration)
- PasswordResetService: token generation, validation, nightly cleanup
- AuthController: POST /api/auth/forgot-password and /api/auth/reset-password (both permitAll)
- AuthE2EController (@Profile("e2e")): GET /api/auth/reset-token-for-test for CI testing
- spring-boot-starter-mail dependency; JavaMailSender optional (@Autowired required=false)
- mail health indicator disabled; mail config via MAIL_HOST/PORT/USERNAME/PASSWORD env vars
- 5 unit tests written TDD-style (all pass)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a "Leser" group (READ_ALL only) and "reader" / "reader123"
user to the deterministic e2e seed so the permissions spec can log
in as a read-only user without relying on admin-created test data.
Refs #48
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Local dev databases that existed before Flyway was introduced have tables
but no flyway_schema_history. Flyway refuses to migrate a non-empty schema
without a history table. baselineOnMigrate=true with baselineVersion=4
stamps those databases as already at V4 without re-running migrations.
Fresh databases (CI) have an empty schema so the baseline is never
triggered and all 4 migrations run normally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Spring Boot 4.0 Flyway auto-configuration is not triggering in the CI
environment — confirmed by empty DB and no flyway_schema_history table.
Replace YAML-based auto-config with an explicit @Bean that creates and
runs Flyway directly on startup, independent of any auto-configuration
conditions. Disable the auto-config via spring.flyway.enabled=false to
prevent interference. Add @DependsOn("flyway") to DataInitializer to
enforce that CommandLineRunner beans are only registered after migrations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The CI health check (curl -sf) and Docker Compose health check (wget)
both hit /actuator/health unauthenticated. With anyRequest().authenticated()
the endpoint returned 401, curl -f treated it as failure, and the health
check loop never exited successfully.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adding explicit spring.flyway.* config (url/user/password) ensures Flyway
creates its own JDBC connection and runs migrations independently of the JPA
datasource initialization order in Spring Boot 4.0.
Fix DataInitializer creating a Document with title=null, which would hit the
NOT NULL constraint in the documents table once the admin user init succeeds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Spring Security was blocking /v3/api-docs with 401, preventing
npm run generate:api from fetching the spec. The springdoc paths are
now whitelisted only when the dev Spring profile is active.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three related issues:
- READ_ALL was stored in the DB but missing from the Permission enum
- ADMIN_USER, ADMIN_TAG and ADMIN_PERMISSION were in the enum and used
in controllers but never granted to any user, making those endpoints
permanently inaccessible
- No runtime signal when a DB permission string drifts from the enum
Changes:
- Add READ_ALL to Permission enum
- Grant all six permissions to the Administrators group in DataInitializer
- Warn in CustomUserDetailsService when a DB permission string has no
matching Permission enum value
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous comment implied CSRF was disabled as a temporary dev
convenience. Replaced it with an explanation of why it is safe with
the current Authorization-header-based auth scheme, and added a
clear note on when it must be re-enabled.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
initData was creating an 'Admins' group with identical permissions to
the 'Administrators' group already created by initAdminUser, resulting
in two redundant groups on every fresh start. Removed the duplicate,
dropped the now-unused groupRepo parameter, and corrected the log
message which claimed 50 persons were created when only 4 were.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace application.properties with application.yaml (base/prod config)
and application-dev.yaml (dev overrides: show-sql=true)
- Add Maven 'dev' profile (activeByDefault) and 'prod' profile to pom.xml;
spring-boot:run picks up the active Spring profile automatically
- Guard DataInitializer.initData with @Profile("dev") so test data is
never seeded in production
Local dev: ./mvnw spring-boot:run (dev profile active by default)
Production: SPRING_PROFILES_ACTIVE env var controls the Spring profile;
Maven profiles are irrelevant for the packaged JAR.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The log statement revealed the default admin password in application
logs. Now only the username is logged, using the resolved variable
instead of a hardcoded string.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Missing closing braces caused Spring to inject the literal placeholder
string instead of resolving the property, silently ignoring any
app.admin.username / app.admin.password env-var overrides.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>