- Replace VPS SSH deploy workflow with a self-contained job that runs on the
TrueNAS act_runner (host docker socket mounted). Checks out the pushed commit,
builds, and rolls out the cannamanage compose stack in-place (project=cannamanage),
then health-checks backend :8081 + frontend :3000.
- Commit docker-compose.truenas.yml (port remap 8081 + AUTH_SECRET) into the repo;
it was previously host-only, so a fresh checkout could not reproduce the deploy.
Use the !override tag for the backend ports list.
- Fix 1: Login rate limiting (5 attempts/min/IP) on POST /api/v1/auth/login
- New LoginRateLimiter (ConcurrentHashMap + @Scheduled reset every 60s)
- HTTP 429 with German message on exceed
- Client IP via X-Forwarded-For with proxy fallback
- @EnableScheduling on CannaManageApplication
- Fix 2: CORS origins configurable via cannamanage.cors.allowed-origins env var
- Defaults to localhost + docker frontend for dev
- SecurityConfig reads with @Value, splits comma-separated list
- Fix 3: Audit JSON safety — replaced manual string concat with Jackson ObjectMapper
- New AuditService.toMetadataJson(Map) helper
- RetentionService and AuthorityExportService refactored
- Fix 4: Tomcat max-http-form-post-size=2MB prevents DoS via oversized payloads
- Fix 5: @Valid added to @RequestBody on 17+ endpoints across
ComplianceRecordsController, FinanceController, ConsentController,
StaffController, ComplianceDeadlineController, SubscriptionController,
ForumController (admin + portal)
- Fix 6: Content-Security-Policy 'default-src \'self\'; frame-ancestors \'none\''
+ frameOptions(deny) on both API + portal filter chains
- IDOR (HIGH): DocumentController download/delete now verify document.clubId matches TenantContext; returns 403 on mismatch via new loadOwnedDocument() helper
- Path Traversal (HIGH): DocumentService.sanitizeFilename() strips path components, removes control/reserved chars, caps at 200 chars, falls back to UUID. Applied to uploadDocument() and archiveProtocol()
- JWT Dev Secret (HIGH): @PostConstruct guard in JwtService throws IllegalStateException if secret null/<32 chars/equals fail-loud marker. application.properties default replaced with CHANGE_ME_IN_PRODUCTION_THIS_WILL_FAIL_ON_STARTUP placeholder (env var CANNAMANAGE_SECURITY_JWT_SECRET set in docker-compose.yml; test profiles have their own valid secrets)
- SecurityConfig (MEDIUM): explicit /api/v1/documents/** matcher with hasAnyRole(ADMIN, STAFF, MEMBER) for defense-in-depth
Verified: Docker rebuild healthy, backend starts cleanly (JWT guard accepts env var), Playwright 203 pass (2 pre-existing login failures unrelated — dev compose profile has no seed users; admin@test.de only loaded via docker-compose.test.yml)
Backend:
- ComplianceDashboardService: traffic-light status per ComplianceArea
(KCANG/FINANCE/DSGVO/VEREIN) based on deadlines, payments, board positions
- RetentionService: scheduled anonymization of expired member data (KCanG §24,
5 years), with dry-run preview and retention report endpoints
- ComplianceDeadlineSeeder: seeds 5 standard recurring deadlines on club creation
- ComplianceDashboardController: GET /api/v1/compliance/dashboard,
GET /retention, POST /retention/preview
- Repository additions: countOverdue, countActive board positions/members
Frontend:
- /compliance page with traffic-light status cards per area
- Overdue deadlines section (highlighted red) with 'days overdue' badges
- Upcoming deadlines with 'days until due' badges and 'Complete' buttons
- Retention info cards (KCanG §24: 5y, AO §147: 10y, DSGVO: 2y)
- Navigation: added 'Compliance-Status' to sidebar under Compliance group
- compliance-dashboard.ts service with mock data for dev mode
Build verified: pnpm build passes clean.
- Sidebar: reorganized into 4 collapsible groups (Betrieb, Kommunikation, Verwaltung, Compliance)
- Berichtszentrale: new /reports-center page with report cards grouped by category (Finance, KCanG, DSGVO, Admin), format selector, date range pickers, Behörden-Export dialog with password protection
- Dashboard: added Outstanding Payments and Monthly Income KPI cards, Upcoming Events widget, Latest Announcements widget, conditional alert cards
- Pricing: fixed mobile overflow at 375px viewport on comparison table
- Frontend service: new compliance-reports.ts with React Query hooks for report generation, authority export, and download
- i18n: added reportsCenter.* and dashboard widget keys to de.json and en.json
The consent check endpoint (/consent/check) returns 500 via the
proxy when the backend returns 403 (missing JWT forwarding).
Previously this caused the banner to show permanently since
consentCheck was undefined. Now isError = true hides the banner
(fail-open strategy — don't block users when backend is unavailable).
Bug 1: Clicking 'Ablehnen' now properly dismisses the dialog by calling
the delete account mutation and signing out (previously it redirected to
/settings/privacy which re-rendered the banner in a loop).
Bug 2: Restructured the dialog layout with flex-col + overflow-y-auto on
the content area only. Header and action buttons are pinned (shrink-0)
so they're always accessible on short viewports. Added max-h constraint
with min() to cap at 600px or 90vh.
Root cause: The BCrypt hash in init.sql was the famous Stack Overflow
hash of 'password' (a0),
not the hash of 'test123' as documented.
Also fixed three test issues in system-test.spec.ts:
1. waitForURL regex /dashboard|\//' matched any URL with '/' (instant resolve)
→ replaced with predicate that waits for URL to not contain /login
2. Reports locator used invalid Playwright selector syntax
→ fixed to use proper :has-text() selector for 'Berichte'
3. Navigation test used 'nav a' but app uses shadcn data-sidebar
→ broadened selector to include [data-sidebar] a
4. Console error filter excluded only favicon/maps/hydration
→ also exclude 'Failed to load resource' and 'MISSING_MESSAGE'
(pre-existing issues from incomplete API endpoints)
ConsentController.resolveUserId() and DsgvoController.resolveUserId() read
auth.getName() as an email and did findByEmailAndTenantId(...), but JwtAuthFilter
sets the Authentication principal to the userId (UUID) — the JWT subject is the
userId, not the email. So auth.getName() returns a UUID string, the email lookup
never matched, and every consent/dsgvo call threw 'User not found' (404/500).
This made the DSGVO consent banner unusable: /consent/check 404'd (banner always
shown) and clicking Accept POSTed /consent which 500'd with no UI feedback — the
button appeared to 'not react'.
Fix: parse auth.getName() as the userId UUID directly and verify existsById.
Root cause (found via Playwright browser probe — curl could not detect client-side
hydration errors):
1. ROOT-LAYOUT INTL CRASH (the 'Oops' on every page incl /login):
app/layout.tsx renders global client components (PwaInstallPrompt → useTranslations,
Toaster, Sonner) as siblings of {children} inside <Providers>, but only each
route-group layout wrapped its own children in NextIntlClientProvider. So those
global components mounted with NO intl context → 'No intl context found' → React
hydration crash → global-error 'Oops'. Fix: wrap the root body in
NextIntlClientProvider via getMessages() (RootLayout now async). Nested providers
stay valid (next-intl supports nesting).
2. PWA MIDDLEWARE INTERCEPTION (manifest.json syntax error + stale cache):
middleware matcher did not exclude /manifest.json or /sw.js, so unauthenticated
browsers got 307→/login (HTML) for both. Browser parsed HTML as JSON
('manifest.json:1 Syntax error') and an HTML/old service worker kept serving
stale bundles ('website hasn't changed' after redeploys). Fix: exclude
manifest.json, sw.js, icons, offline from the matcher.
3. SERVICE-WORKER STALE CACHE: bump CACHE_NAME v1→v2 so the activate handler purges
old cached bundles from clients that loaded the broken build.
Also adds scripts/debug/dashboard-probe.mjs — a Playwright probe that logs in and
captures real client-side console/network errors + screenshot.
Login reached the backend (HTTP 200) but NextAuth returned CredentialsSignin.
Cause: authorize() read data.member.id/email/clubName/clubId, but the backend
LoginResponse is flat — { accessToken, refreshToken, expiresIn, role } with no
member object. Accessing data.member.id on undefined threw, so authorize()
returned null.
Decode the JWT payload to recover identity claims (sub=userId, email,
tenant_id=clubId) and use the flat top-level role. Adds a small decodeJwtPayload
helper (claims only, no signature verification needed here).
After a successful login the backend returned HTTP 500:
io.jsonwebtoken.io.DecodingException: Illegal base64 character: '-'.
JwtService.getSigningKey() does Decoders.BASE64.decode(secret) before building
the HMAC key (JJWT 0.12 convention). The compose secret was the plaintext
'docker-dev-secret-key-minimum-32-characters-long-for-hmac', which contains
hyphens and is not valid base64, so token signing threw once auth succeeded.
Replace with a proper base64 value (openssl rand -base64 48). The base
application.properties default was already correctly base64-encoded; only the
docker override was wrong.
Backend now starts and Flyway migrations run, but /actuator/health returned
503 (DOWN) so Docker marked the container unhealthy and the frontend refused
to start. Cause: spring-boot-starter-mail registers a mail health indicator
that tries to connect to localhost:1025; there is no SMTP container in this
deployment, so it reports DOWN and drags the aggregate health to DOWN.
Disable the mail health indicator in the docker profile. Mail being down must
not make the whole service unhealthy in an SMTP-less deployment.
Backend crashed on startup with 'Schema validation: missing table
[audit_events]'. Root cause: this is Spring Boot 4.0.6, which modularized
autoconfiguration. FlywayAutoConfiguration moved out of spring-boot-autoconfigure
into a dedicated spring-boot-flyway module that is only pulled in by
spring-boot-starter-flyway. The pom only had flyway-database-postgresql
(+ transitive flyway-core) but NOT the starter, so spring.flyway.enabled=true
was inert: no migrations ran, flyway_schema_history was never created, and
Hibernate ddl-auto=validate failed on the empty schema.
Adds spring-boot-starter-flyway (autoconfigure module + flyway-core); keeps
flyway-database-postgresql for the Postgres dialect.
Ref: https://spring.io/blog/2025/10/28/modularizing-spring-boot/
Root cause of the SSG/page-data build crash: src/app/layout.tsx evaluated
new URL(process.env.BASE_URL) at module load. BASE_URL was never set as a
build-time ENV, so it was undefined -> ERR_INVALID_URL, input: 'undefined'.
Because this is the root layout, its metadata is collected for every route,
explaining why both /impressum (marketing) and /portal-login (non-marketing)
failed identically. The earlier NextAuth/middleware/force-dynamic fixes could
not help because metadata evaluation happens before any of that.
- layout.tsx: fall back to http://localhost:3000 when BASE_URL is unset
- Dockerfile: add BASE_URL build-time placeholder (matches AUTH_URL pattern)