IssueFlow is a Spring Boot REST backend with a React + TypeScript SPA for project and ticket tracking. It models users, projects, tickets, comments, mentions, dependencies, attachments, audit logs, soft deletion, and JWT-based authentication, fronted by a Jira-inspired, Kanban-driven UI.
New here? Follow SETUP.md to bring the whole stack up locally with one
docker compose up. For the system design and a live animated architecture diagram, see docs/ARCHITECTURE.md. This README covers the API contract; run.md has the backend deep-dive and curl recipes.
- Java 21
- Spring Boot 3.3.x
- Spring Web
- Spring Security
- Spring Data JPA with Hibernate
- PostgreSQL
- Flyway database migrations
- JJWT for HS256 JWT handling
- Testcontainers, JUnit 5, Mockito, and Spring Security Test
IssueFlow follows a conventional layered backend architecture:
- Controllers expose REST endpoints and accept/return DTO records.
- Services contain business logic and own transactional boundaries.
- Repositories provide persistence through Spring Data JPA.
- JPA entities stay inside the application boundary and are not returned directly by controllers.
- Domain exceptions are mapped centrally to
ApiErrorJSON byGlobalExceptionHandler.
The system is designed around these capabilities:
- User management for ticket ownership, assignments, comments, and authorization roles.
- Project management as top-level containers for tickets.
- Ticket lifecycle management, including status, priority, type, due dates, assignment, and optimistic locking.
- Comment management with persisted
@usernamementions. - Ticket dependency tracking for blocker relationships.
- Attachment metadata management.
- Audit logging for state-changing actions.
- Soft deletion and restore flows for projects and tickets.
- CSV import/export workflows for bulk ticket operations.
- Scheduled workflows such as auto-escalation and auto-assignment.
IssueFlow uses stateless JWT authentication.
JWT settings are configured with the issueflow.jwt prefix:
issueflow:
jwt:
secret: ${JWT_SECRET:dev-secret-change-in-production-must-be-at-least-256-bits-long!!}
expiration-minutes: ${JWT_EXPIRATION_MINUTES:60}
issuer: ${JWT_ISSUER:issueflow}The application signs tokens with HS256. Tokens include a subject, issuer, role claim, issued-at timestamp, expiry, and JTI.
JwtPropertiesbindsissueflow.jwt.secret,issueflow.jwt.expiration-minutes, andissueflow.jwt.issuer.JwtServiceissues tokens and validates signature, expiry, and issuer while parsing claims.TokenDenyListServicestores revoked token JTIs and removes expired revoked tokens every hour.JwtAuthenticationFilterreadsAuthorization: Bearer <token>, validates the JWT, checks the deny-list, loads the user, and populates the Spring Security context.CustomUserDetailsServiceloads active users fromUserRepositoryand maps roles toROLE_ADMINorROLE_DEVELOPER.AuthServiceauthenticates login requests, issues JWTs, returns the current user profile, revokes logout tokens, and audit-logs LOGIN and LOGOUT events.AuthControllerexposesPOST /auth/login,POST /auth/logout, andGET /auth/me.AuditServicepersists structured audit entries for authentication events and other state-changing service workflows.SecurityConfigconfigures stateless sessions, disables CSRF, enables method security, registers the JWT filter, and exposes aBCryptPasswordEncoderbean.
The security chain allows unauthenticated access to:
POST /auth/loginGET /actuator/health/v3/api-docs/**/swagger-ui/**
All other endpoints require a valid JWT. ADMIN-only endpoints should use:
@PreAuthorize("hasRole('ADMIN')")| API | Endpoint | Request | Response |
|---|---|---|---|
| Login | POST /auth/login |
{ "username": "jdoe", "password": "secret" } |
{ "accessToken": "<jwt>", "tokenType": "Bearer", "expiresIn": 3600 } |
| Logout | POST /auth/logout |
Bearer token | 200 OK; the token JTI is added to the deny-list until expiry. |
| Current user | GET /auth/me |
Bearer token | { "id": 1, "username": "jdoe", "email": "jdoe@example.com", "fullName": "John Doe", "role": "DEVELOPER" } |
The following API surface describes the target IssueFlow REST contract.
| Operation | Endpoint | Notes |
|---|---|---|
| List users | GET /users |
Returns user summaries. Requires JWT. |
| Get user | GET /users/{userId} |
Returns one user by ID. Requires JWT. |
| Create user | POST /users |
Creates a user with username, email, full name, role, and password. Requires ADMIN. |
| Update user | POST /users/update/{userId} |
Updates full name and role. Requires ADMIN. |
| Delete user | DELETE /users/{userId} |
Deletes a user. Requires ADMIN. |
| Operation | Endpoint | Notes |
|---|---|---|
| List projects | GET /projects |
Returns visible projects. |
| Get project | GET /projects/{projectId} |
Returns one project. |
| Create project | POST /projects |
Creates a project owned by a user. |
| Update project | PATCH /projects/{projectId} |
Updates project metadata. |
| Soft-delete project | DELETE /projects/{projectId} |
Hides the project from default queries. |
| List deleted projects | GET /projects/deleted |
ADMIN-only restore support. |
| Restore project | POST /projects/{projectId}/restore |
ADMIN-only restore operation. |
| Project workload | GET /projects/{projectId}/workload |
Returns open ticket counts by user. |
| Operation | Endpoint | Notes |
|---|---|---|
| List tickets by project | GET /tickets?projectId={projectId} |
Returns visible tickets for a project. |
| Get ticket | GET /tickets/{ticketId} |
Returns one ticket. |
| Create ticket | POST /tickets |
Creates a ticket with status, priority, type, project, assignee, and due date. |
| Update ticket | PATCH /tickets/{ticketId} |
Updates editable ticket fields. |
| Soft-delete ticket | DELETE /tickets/{ticketId} |
Hides the ticket from default queries. |
| List deleted tickets | GET /tickets/deleted?projectId={projectId} |
ADMIN-only restore support. |
| Restore ticket | POST /tickets/{ticketId}/restore |
ADMIN-only restore operation. |
| Export tickets | GET /tickets/export?projectId={projectId} |
Exports project tickets to CSV. |
| Import tickets | POST /tickets/import |
Imports tickets from multipart CSV upload. |
| Operation | Endpoint | Notes |
|---|---|---|
| List comments | GET /tickets/{ticketId}/comments |
Returns comments for a ticket. |
| Add comment | POST /tickets/{ticketId}/comments |
Supports persisted @username mentions. |
| Update comment | PATCH /tickets/{ticketId}/comments/{commentId} |
Updates comment content. |
| Delete comment | DELETE /tickets/{ticketId}/comments/{commentId} |
Removes a comment according to domain rules. |
| List mentions for user | GET /users/{userId}/mentions |
Supports optional pagination. |
| Operation | Endpoint | Notes |
|---|---|---|
| Add dependency | POST /tickets/{ticketId}/dependencies |
Adds a blocker relationship. |
| List dependencies | GET /tickets/{ticketId}/dependencies |
Lists blockers for a ticket. |
| Remove dependency | DELETE /tickets/{ticketId}/dependencies/{blockerId} |
Removes a blocker relationship. |
| Operation | Endpoint | Notes |
|---|---|---|
| Upload attachment | POST /tickets/{ticketId}/attachments |
Accepts multipart file upload. |
| Delete attachment | DELETE /tickets/{ticketId}/attachments/{attachmentId} |
Removes attachment metadata and storage reference. |
| Operation | Endpoint | Notes |
|---|---|---|
| List audit logs | GET /audit-logs |
Supports filters such as entityType, entityId, action, and actor. |
Errors are returned as ApiError JSON:
{
"timestamp": "2026-05-20T12:00:00Z",
"status": 401,
"error": "Unauthorized",
"message": "Invalid or expired token",
"path": "/tickets",
"details": []
}Domain and infrastructure errors are mapped consistently:
404 Not Foundfor missing resources.409 Conflictfor conflicts and optimistic locking failures.403 Forbiddenfor authorization failures.400 Bad Requestfor malformed JSON and validation failures.422 Unprocessable Entityfor domain validation errors.500 Internal Server Errorfor unexpected failures with a correlation reference.
Optimistic lock conflicts return:
Resource was modified by another user, please retry
The project uses Flyway migrations under src/main/resources/db/migration.
compose.yml provides a local PostgreSQL database configured for the default application profile:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/issueflow
username: issueflow
password: issueflowThe development seed migration creates an initial ADMIN user:
- Username:
admin - Password:
admin123
Start PostgreSQL:
docker compose up -dRun the application:
./mvnw spring-boot:runBuild the application:
./mvnw clean packageRun tests:
./mvnw testRun the focused Phase 3 security tests:
./mvnw "-Dtest=JwtServiceTest,TokenDenyListServiceTest,CustomUserDetailsServiceTest,AuditServiceTest,AuthServiceTest" testOn Windows PowerShell:
.\mvnw.cmd "-Dtest=JwtServiceTest,TokenDenyListServiceTest,CustomUserDetailsServiceTest,AuditServiceTest,AuthServiceTest" testRun the authentication integration test with Docker/Testcontainers available:
.\mvnw.cmd "-Dtest=AuthControllerIntegrationTest" test- Service-level behavior is covered with JUnit 5 and Mockito.
- Repository and controller integration tests use Testcontainers PostgreSQL.
- Full-suite runs require Docker for Testcontainers-backed tests.
If AI assistance is used during development, document meaningful prompts, plans, and generated artifacts in prompts.md or another project-tracked note as required by the assignment.
The frontend is a React 19 + TypeScript SPA built with Vite and Tailwind CSS v4, using a component-based architecture and a Jira-inspired design system.
- React 19, TypeScript, Vite
- Tailwind CSS v4 (design tokens registered via
@theme) - react-router-dom, react-hook-form + zod, axios
- @dnd-kit (Kanban drag-and-drop), recharts (dashboard), lucide-react (icons)
Design tokens live in frontend/src/index.css (@theme) and map to the Atlassian Design System palette, so semantic utilities (bg-primary, text-muted-foreground, bg-card, lozenge Badge variants) resolve everywhere:
- Primary:
#0c66e4· Foreground:#172b4d· Background:#f7f8f9 - Success:
#1f845a· Warning:#f5cd47· Destructive:#c9372c - Accent:
#e9f2ff· Muted:#f1f2f4/#626f86· Border/Input:#dfe1e6 - Font: Inter (loaded non-render-blocking via
<link>inindex.html)
Status and priority render as Jira-style lozenges (tinted background + colored text) via the Badge component.
LoginPage is a premium split-panel screen: an animated gradient-mesh + floating-particle background (pure CSS, transform/opacity only for 60fps), a glassmorphic login card, and micro-interactions (input focus states, password show/hide, button hover-lift/press, staggered entrance). All motion is disabled under prefers-reduced-motion. Auth logic (zod validation, JWT login, redirect-to-intended-route) is unchanged.
- Kanban Board: Drag-and-drop ticket management across status columns
- Dashboard: Summary cards, charts, and audit log display
- Ticket Management: List and detail views with full CRUD operations
- User Management: Admin-only user creation and management
- Audit Logs: Comprehensive activity tracking with filters
- Route-level code splitting: authenticated pages are
React.lazy+Suspense, so heavy deps (recharts, @dnd-kit, react-datepicker) stay out of the initial/login bundle. - Vendor chunking:
vite.config.tssplitsreact-vendor,charts, anddndinto independently-cached chunks. - Fonts: Inter is preconnected and loaded with
display=swapfromindex.html(no render-blocking CSS@importchain).
cd frontend
npm install
npm run dev # http://localhost:5173In development the SPA reaches the API through the Vite /api proxy (see vite.config.ts), which forwards to the backend on http://localhost:8080. See SETUP.md for the full flow.
This project is MIT licensed.