This is a secure backend application built with Spring Boot, featuring JWT authentication, PostgreSQL database, and ActiveMQ messaging.
The application supports three main profile modes:
The local profile uses H2 in-memory database and is suitable for development and testing.
To run with local profile:
./mvnw spring-boot:run -Dspring-boot.run.profiles=localThis profile enables demo seed data automatically.
The Docker Compose setup uses PostgreSQL and includes the demo seed profile so it behaves like the lightweight local experience.
To run with Docker profile:
docker compose up --buildThis starts the backend with docker,demo, so it loads the demo users, sample products, and sample orders into PostgreSQL.
When you need the same seeded PostgreSQL-backed experience without Docker Compose, add the demo profile on top of docker:
./mvnw spring-boot:run -Dspring-boot.run.profiles=docker,demoOnly local and demo enable startup seed data.
Public deployments should create one explicit admin account through environment-backed configuration instead of demo seed data.
Required variables:
APP_BOOTSTRAP_ADMIN_ENABLED=true
APP_BOOTSTRAP_ADMIN_USERNAME=admin
APP_BOOTSTRAP_ADMIN_PASSWORD=<16+ character secret>
APP_BOOTSTRAP_ADMIN_EMAIL=admin@example.comWhen enabled, startup creates that admin if it does not already exist. If the configured username/email already belongs to a non-admin user, startup fails instead of silently mutating the account.
Public deployments can also bootstrap a safe baseline product catalog without re-enabling demo users or sample orders.
Required variable:
APP_BOOTSTRAP_PRODUCTS_ENABLED=trueOptional override:
APP_BOOTSTRAP_PRODUCTS_CATALOG=classpath:bootstrap/products.jsonWhen enabled, startup inserts the versioned catalog only when the products table is empty. If products already exist, bootstrap skips creation. This keeps clean demo resets deterministic without reintroducing local/demo seed users.
This will start:
- Backend service on port 4001
- Frontend service on port 8081
- PostgreSQL database on port 5432
- ActiveMQ on ports 61616 (broker) and 8161 (web console)
You can interact with the PostgreSQL database in several ways:
- Using docker exec and the psql command-line tool:
# Connect to the database
docker exec -it test-secure-backend-postgres-1 psql -U postgres -d testdb
# Common psql commands:
\dt # List tables
\d table_name # Describe table
\q # Quit psql
# Example queries:
SELECT * FROM app_user;
SELECT * FROM products;
SELECT * FROM cart_items;
SELECT * FROM orders;- Using external tools:
- Host: localhost
- Port: 5432
- Database: testdb
- Username: postgres
- Password: postgres
The main tables in the database:
app_user: Stores user informationemail_event: Stores per-user email delivery metadata for the app-owned verification endpointproducts: Stores product catalogcart_items: Stores shopping cart itemsorders: Stores order information
The API documentation is available at:
- Swagger UI: http://localhost:4001/swagger-ui/index.html
- OpenAPI JSON: http://localhost:4001/v3/api-docs
- OpenAPI YAML: http://localhost:4001/v3/api-docs.yaml
Demo seed data is available only in local or docker,demo runs. Public/server deployments should keep it disabled.
- Admin users (username/password):
- admin/LocalDemoAdmin123!
- Client users:
- client/client
- client2/client2
- client3/client3
- Sample products in various categories
- Public-safe deployments can bootstrap only the product catalog through
APP_BOOTSTRAP_PRODUCTS_ENABLED=true
Public deployments must not rely on these credentials.
The application uses JWT tokens for authentication. To access protected endpoints:
- Get a token using the
/api/v1/users/signinendpoint - Include the token in the Authorization header:
Bearer <token>
Sign in responses now include both an access token and a refresh token. The refresh token can be exchanged via POST /api/v1/users/refresh even when the access token expires, and calling POST /api/v1/users/logout revokes the refresh token on the server.
Production deployments must provide JWT_SECRET_KEY with at least 32 random bytes and set JWT_REQUIRE_SECURE_KEY=true. The application then refuses to start with a missing, short, or known development key. Rotating the key invalidates existing access tokens, so users may need to refresh their session or sign in again.
The training stack also includes an OIDC-based SSO bridge by default. The frontend authenticates with the configured identity provider and exchanges the returned OIDC ID token through POST /api/v1/users/sso/exchange. The backend validates the external token, provisions or reuses a local user, and returns the same app-issued JWT and refresh token shape as password login. Protected APIs continue to accept only the app JWT, not raw identity-provider tokens. Set APP_SSO_ENABLED=false only when you intentionally want to disable the exchange endpoint.
The local identity provider is Keycloak, started by the sibling awesome-localstack repository. Keycloak owns the SSO users and their passwords. This backend owns only the application session, local user record, app roles, refresh tokens, carts, orders, and other domain data. In other words, SSO proves who the user is; the backend still issues and validates the app's own JWT for protected API calls.
The default local SSO configuration is:
- issuer:
http://localhost:8082/realms/awesome-testing - audience/client id:
awesome-testing-frontend - exchange endpoint:
POST /api/v1/users/sso/exchange - admin console:
http://localhost:8082/admin/ - Keycloak admin login:
admin/admin
The LocalStack Keycloak realm includes two training users:
sso-client/SsoClient123!sso-admin/SsoAdmin123!
The local Keycloak client also enables direct access grants for Playwright training fixtures. That lets tests obtain an ID token over HTTP, exchange it through the backend, and start UI tests with app-issued tokens already in browser storage. This is a local training convenience, not a production recommendation.
The easiest local SSO check uses the sibling LocalStack repository:
cd ../awesome-localstack
docker compose -f lightweight-docker-compose.yml upThen open http://localhost:8081/login, click Sign in with SSO, and log in through Keycloak with:
sso-client/SsoClient123!sso-admin/SsoAdmin123!
Expected result: the browser returns to http://localhost:8081, the app stores its normal token and refreshToken, and protected pages work with the app-issued JWT.
For a curl-level negative check, an invalid ID token should be rejected:
curl -i -X POST http://localhost:4001/api/v1/users/sso/exchange \
-H "Content-Type: application/json" \
-H "Origin: http://localhost:8081" \
--data '{"idToken":"not-a-real-token"}'Expected result: 401 with {"message":"Invalid SSO token"}.
For Playwright E2E tests in ../playwright-2025, run the local Keycloak/backend/frontend stack. The specs assume SSO is enabled in the LocalStack profile:
cd ../playwright-2025
npx playwright test tests/ui/sso.live.ui.spec.ts tests/ui/sso.fixture.ui.spec.ts tests/api/sso.exchange.api.spec.tssso.live.ui.spec.ts drives the real browser redirect through Keycloak. uiSsoAuthFixture obtains the Keycloak ID token over HTTP, exchanges it with this backend, and injects only the resulting app token and refreshToken into local storage before the test starts.
- User authentication with JWT tokens
- Role-based authorization (ADMIN and CLIENT roles)
- User management (signup, signin, edit, delete)
- Email sending functionality via ActiveMQ
- Ollama integration for AI text generation and chat
- Product management
- Shopping cart functionality
- Order management
- Swagger/OpenAPI documentation
- Comprehensive test coverage
- Run
./mvnw verifyto execute the entire unit/integration suite. The build fails if line coverage drops below 40%, and JaCoCo reports are emitted totarget/site/jacoco/index.html. - Core scenarios are covered with focused unit tests for business services (users, products, carts, orders, email), security components (token provider, filter, authentication handler, security config), and controller utilities/exception handlers.
- Repository-specific behavior is validated with
@DataJpaTestsuites forOrderRepositoryandCartItemRepository, ensuring the custom JPQL queries behave correctly against H2.
- Java 25 (Temurin distribution recommended)
- Maven Wrapper (bundled Maven 3.9.16)
- ActiveMQ (for email functionality)
- Clone the repository
- Configure ActiveMQ connection in
application.yml - Run the application:
mvn spring-boot:run
The application will start on http://localhost:8080
mvn test- POST
/api/v1/users/signin- Authenticate user and get JWT token - POST
/api/v1/users/signup- Register a new client user - POST
/api/v1/users/refresh- Refresh JWT token using a refresh token - POST
/api/v1/users/sso/exchange- Exchange a valid OIDC ID token for an app JWT and refresh token - POST
/api/v1/users/logout- Revoke current refresh token and logout - POST
/api/v1/users/password/forgot- Anonymous endpoint that queues a password-reset email (always responds with 202) - POST
/api/v1/users/password/reset- Completes a reset using the emailed token and a new password (anonymous)
- GET
/api/v1/users/me- Get current user information - GET
/api/v1/users/me/email-events- Get the authenticated user's latest email events - GET
/api/v1/users- Get all users (ADMIN only) - GET
/api/v1/users/{username}- Get user by username - PUT
/api/v1/users/{username}- Update user - DELETE
/api/v1/users/{username}- Delete user (ADMIN only)
- GET
/api/v1/products- Get all products (authenticated) - GET
/api/v1/products/{id}- Get product by ID (authenticated) - POST
/api/v1/products- Create new product (ADMIN only) - PUT
/api/v1/products/{id}- Update product (ADMIN only) - DELETE
/api/v1/products/{id}- Delete product (ADMIN only)
- GET
/api/v1/cart- Get current user's cart - POST
/api/v1/cart/items- Add item to cart - PUT
/api/v1/cart/items/{productId}- Update item quantity - DELETE
/api/v1/cart/items/{productId}- Remove item from cart - DELETE
/api/v1/cart- Clear cart
- POST
/api/v1/orders- Create a new order - GET
/api/v1/orders- Get user's orders - GET
/api/v1/orders/{id}- Get order by ID - PUT
/api/v1/orders/{id}/status- Update order status (ADMIN only) - POST
/api/v1/orders/{id}/cancel- Cancel order
- POST
/api/v1/qr/create- Generate QR code from text (authenticated)
- POST
/api/v1/email- Send an email (authenticated users only) - GET
/api/v1/users/me/email-events- Inspect the authenticated user's latest email statuses without exposing Mailhog - GET
/api/v1/local/email/outbox(local profile only) - Inspect the in-memory email queue when running without Artemis - DELETE
/api/v1/local/email/outbox(local profile only) - Clear the local outbox buffer for a clean test run
Public-safe email verification is now app-owned rather than Mailhog-owned. For authenticated users, the
/api/v1/users/me/email-events endpoint returns only their own recent email metadata with statuses such as
QUEUED, SENT_TO_SMTP_SINK, or FAILED. Because this deployment uses Mailhog as a fake inbox, the backend
does not claim real-world inbox delivery; it only reports whether the message was queued and handed off to the
test mail sink.
When running with the local profile the backend does not connect to Artemis. Instead, every outgoing EmailDto
payload is captured by the local outbox endpoint described above so that developers (or the frontend) can retrieve the
latest password-reset link without needing SMTP infrastructure. Each record contains the destination, payload, and a
timestamp. Clearing the outbox before a test run makes it easy to retrieve only the latest link. In Docker/localstack
profiles, reset messages are dispatched through Artemis to the dedicated JMS consumer which forwards them to Mailhog.
Every email message now carries a template identifier (e.g., PASSWORD_RESET_REQUESTED) and a properties map
containing contextual data such as the reset link, expiry window, and username. Downstream consumers can render their
own copy using those properties while the legacy subject/message fields remain populated for backward compatibility.
The application integrates with Ollama to provide AI text generation and chat capabilities. These features are available through secure endpoints that require authentication.
-
POST
/api/v1/ollama/generate- Generate text using Ollama models- Single text generation without conversation history
- Requires authentication with
ROLE_CLIENTorROLE_ADMIN - Supports Server-Sent Events (SSE) for streaming responses
- Request body:
{ "model": "qwen3.5:2b", "prompt": "Your prompt here", "options": {}, "think": false }
-
POST
/api/v1/ollama/chat- Chat with Ollama models (stateless)- Supports multi-message conversations with history
- Client maintains conversation history by sending all previous messages
- Requires authentication with
ROLE_CLIENTorROLE_ADMIN - Supports Server-Sent Events (SSE) for streaming responses
- Request body:
{ "model": "qwen3.5:2b", "messages": [ { "role": "system", "content": "You are a helpful assistant." }, { "role": "user", "content": "Hello!" }, { "role": "assistant", "content": "Hi there!" }, { "role": "user", "content": "How are you?" } ], "options": {}, "think": false }
-
POST
/api/v1/ollama/chat/tools- Chat with Ollama models and invoke backend functions (stateless)-
Accepts the same conversation history as
/api/v1/ollama/chatplus atoolsarray that describes available functions -
Streams every chunk (assistant thinking, tool calls, tool results, and final reply) so workshop participants can watch the loop in real time
-
Currently exposes two functions:
get_product_snapshot– anchor every SKU answer with trusted JSON (price/stock/description). Small local models can hallucinate if they guess, so this is always the first hop when you are in the product lane.list_products– grab a slice of the catalog right after a snapshot so the model can compare SKUs; keep it paired withget_product_snapshotto avoid invented cross-product claims.- We intentionally removed the Grokipedia lane, so every function call now focuses on internal inventory data.
-
Sample request:
{ "model": "qwen3.5:2b", "messages": [ { "role": "system", "content": "You are a helpful shopping assistant." }, { "role": "user", "content": "How much does the Retro Console cost?" } ], "tools": [ { "type": "function", "function": { "name": "get_product_snapshot", "description": "Return catalog metadata for a product so you can answer shopper questions accurately.", "parameters": { "type": "object", "properties": { "productId": { "type": "integer", "description": "Numeric product id from the catalog." }, "name": { "type": "string", "description": "Exact product name when the id is unknown." } }, "oneOf": [ { "required": ["productId"] }, { "required": ["name"] } ] } } } ] } -
When the model decides to call
get_product_snapshot, the backend executesProductService, streams arole: "tool"payload containing the JSON snapshot (or{ "error": "..." }), and then resubmits the expanded history back to Ollama so the final assistant reply references the real data.
-
-
GET
/api/v1/ollama/chat/tools/definitions- Returns the JSON schema for every supported tool so SDKs/frontends can stay in sync with the backend contract (requires the same auth as the chat endpoints)
Each user can configure two system prompts that the backend injects automatically:
- GET/PUT
/api/v1/users/chat-system-prompt– controls the general conversation tone for/api/v1/ollama/chat. - GET/PUT
/api/v1/users/tool-system-prompt– explains how to use the catalog tools for/api/v1/ollama/chat/tools.
Clients can fetch these endpoints to display/edit the prompts, but they no longer need to include the strings in the messages array; the controller prepends them before relaying any request to Ollama.
Both endpoints support the following parameters:
model(required): The Ollama model to use (e.g., "qwen3.5:2b")options(optional): Model-specific options (e.g., temperature, max tokens)think(optional): Set totruefor 'thinking' models that benefit from reasoning before responding. Defaults tofalse
For the /generate endpoint:
prompt(required): The text prompt to generate from
For the /chat endpoint:
messages(required): Array of conversation messages with role and content
The Ollama service can be configured in application.yml:
ollama:
base-url: http://localhost:11434 # Default Ollama server URLThe application includes a real-time HTTP traffic monitoring system implemented with WebSockets. This feature allows tracking and visualization of all HTTP requests in the application.
- Real-time tracking of HTTP requests and responses
- Captures HTTP method, path, status code, response time, and timestamp
- Events are broadcast via WebSocket to connected clients
- Secured access requiring authentication
- Traffic is captured using a servlet filter (
TrafficLoggingFilter) - Events are stored in a thread-safe concurrent queue
- A scheduled publisher broadcasts events to WebSocket subscribers
- Uses STOMP protocol over WebSocket for messaging
- WebSocket Connection:
/api/v1/ws-traffic - Subscription Topic:
/topic/traffic - Data Format:
{ "method": "GET", "path": "/api/v1/products", "status": 200, "durationMs": 45, "timestamp": "2023-03-22T10:15:30.123Z" }
-
Connect to the WebSocket endpoint:
const socket = new SockJS('/api/v1/ws-traffic'); const stompClient = Stomp.over(socket); // Include JWT token for authentication const headers = { 'Authorization': 'Bearer ' + jwtToken }; stompClient.connect(headers, function(frame) { // Subscribe to traffic events stompClient.subscribe('/topic/traffic', function(message) { const trafficEvent = JSON.parse(message.body); console.log('New traffic event:', trafficEvent); // Handle event (e.g., update UI) }); });
-
Traffic events are automatically captured and broadcast as they occur in the application
A simple HTML-based visualization tool is provided to help monitor traffic events in real-time:
- Run the web server on port 8081 (this is CORS requirement, my frontend uses the same port - see
WebSecurityConfig). Use absolute path
jwebserver -p 8081 -d /Users/slawek/IdeaProjects/test-secure-backend
- Go to
http://localhost:8081/traffic-monitor.html - Enter your server URL (default: http://localhost:4001)
- Paste a valid JWT token (obtained from
/api/v1/users/signinendpoint) - Click "Connect" to establish the WebSocket connection
- Watch as HTTP traffic events appear in real-time
- GET
/api/v1/traffic/info- Get WebSocket connection information (authenticated)- Returns:
{ "endpoint": "/api/v1/ws-traffic", "topic": "/topic/traffic", "description": "WebSocket endpoint for real-time HTTP traffic events" }
- Returns:
The application follows a comprehensive testing strategy focusing on endpoint-level integration tests. Key aspects include:
-
Independent Endpoint Testing
- Each endpoint is tested in isolation
- Tests are organized by feature in separate packages
- All possible HTTP response codes are tested for each endpoint
-
Test Organization
- Tests are ordered by HTTP status code (2xx first, then 4xx, 5xx)
- Each test class focuses on a single endpoint functionality
- Example test coverage for an endpoint:
- 200/201 - Successful operations
- 400 - Bad Request (invalid input)
- 401 - Unauthorized (no authentication)
- 403 - Forbidden (insufficient permissions)
- 404 - Not Found (resource doesn't exist)
-
Test Data Generation
- Uses factory pattern for test data creation
- Factories generate random, valid test data using Faker
- Located in
test/factorypackage
-
Test Structure
- Given/When/Then format for clear test organization
- Descriptive test method names indicating the scenario
- Comprehensive assertions for response status and body
Example test class organization:
class SomeEndpointTest {
void shouldReturnSuccessfully(); // 200 OK
void shouldCreate(); // 201 Created
void shouldGet400WhenInvalidInput();
void shouldGet401WhenNoAuthorizationHeader();
void shouldGet403WhenNotAuthorized();
void shouldGet404WhenNotFound();
}When working with AI assistants, keep in mind:
-
Test failures may be caused by recent changes since git HEAD is kept stable. To see the changes use:
git --no-pager diff
-
To run a single test and save the output to the testlogs folder, use JUnit notation:
mvn test -Dtest=TestClassName#testMethodName > ./testlogs/test-output.log
This helps in analyzing test failures by providing detailed logs.
-
The test logs can be read and analyzed by AI to help diagnose issues.
-
When making changes, always verify that all tests pass using:
mvn test