6319552675a7d1f27842a85043302d43be9ee20e
- 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)
CannaManage
Multi-tenant cannabis club management platform for German Anbauvereinigungen (cultivation associations) under CanG §19.
Overview
CannaManage handles member management, distribution tracking, and legal compliance for cannabis cultivation clubs in Germany. It enforces the strict quotas mandated by the Cannabis Act (CanG) — including monthly limits (50g adult / 30g under-21), daily limits (25g), and THC restrictions for minors.
Tech Stack
| Component | Technology |
|---|---|
| Runtime | Java 21 (Temurin) |
| Framework | Spring Boot 4.0.6 |
| Security | Spring Security 7.0 + JWT (JJWT 0.12.6) |
| ORM | Hibernate 7 / JPA |
| Database | PostgreSQL (prod), H2 (test) |
| Migrations | Flyway 10 |
| API Docs | SpringDoc OpenAPI 2.8.6 |
| Build | Maven (multi-module) |
| Container | Docker Compose (Postgres + app) |
Project Structure
cannamanage/
├── cannamanage-domain/ # JPA entities, enums, TenantContext
├── cannamanage-service/ # Business logic, repositories, ComplianceService
├── cannamanage-api/ # Spring Boot app, controllers, security, DTOs
├── docs/
│ └── sprint-2/ # Sprint planning docs
└── docker-compose.yml # Local dev environment
Modules
cannamanage-domain
JPA entities with multi-tenant isolation via @Filter("tenantFilter"):
Member— club members with age trackingDistribution— cannabis distribution recordsMonthlyQuota— per-member monthly usage trackingBatch/Strain/StockMovement— inventory managementClub— association registrationUser— authentication accounts
cannamanage-service
ComplianceService— CanG §19 quota enforcement (25 unit tests)- Repositories for all entities
cannamanage-api
- Auth — JWT login + refresh token rotation (SHA-256 hashed)
- Members — CRUD for association members
- Distributions — compliance-gated distribution recording
- Stock — batch and inventory management
- Compliance — quota status API
- Multi-tenant isolation via
TenantFilterAspect(Hibernate @Filter activation)
API Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/v1/auth/login |
Public | Login with email + password |
| POST | /api/v1/auth/refresh |
Public | Refresh token rotation |
| GET | /api/v1/compliance/quota/{memberId} |
ADMIN, MEMBER | Monthly quota status |
| GET/POST/PUT | /api/v1/members/** |
ADMIN, MEMBER | Member CRUD |
| POST | /api/v1/distributions/** |
ADMIN, MEMBER | Record distributions |
| GET/POST | /api/v1/stock/** |
ADMIN | Stock management |
Swagger UI: http://localhost:8080/swagger-ui.html
Running Locally
# Start PostgreSQL
docker compose up -d
# Run the app
JAVA_HOME=/path/to/jdk-21 ./mvnw spring-boot:run -pl cannamanage-api
# Run all tests (H2 in-memory)
JAVA_HOME=/path/to/jdk-21 ./mvnw clean verify
Testing
- 37 tests total — all green
- 25 unit tests (
ComplianceServiceTest) — quota enforcement logic - 7 integration tests (
AuthControllerIntegrationTest) — full HTTP auth flow - 5 integration tests (
ComplianceControllerIntegrationTest) — quota API with JWT
Integration tests use @SpringBootTest(webEnvironment = RANDOM_PORT) with H2 and Spring's RestClient.
Security Model
- Stateless JWT — no session, no UserDetailsService
- Roles: ADMIN (full access), MEMBER (self-service), STAFF (Sprint 3)
- Multi-tenancy: Hibernate
@Filteractivated per-request via AOP aspect - Refresh tokens: SHA-256 hashed (Spring Security 7 enforces BCrypt 72-byte limit)
- Token rotation on refresh — old tokens invalidated
Sprint History
| Sprint | Focus | Status |
|---|---|---|
| 1 | Domain entities, ComplianceService, 25 tests | ✅ Done |
| 2 | REST API, Spring Security, JWT, OpenAPI, integration tests | ✅ Done |
| 3 | Member portal, STAFF role, real-time notifications | 📋 Planned |
License
Private — Patrick Plate
Description
Languages
Java
61%
TypeScript
38.1%
JavaScript
0.4%
CSS
0.3%
Shell
0.2%