{"id":49955394,"url":"https://github.com/aaroncx/taskflow","last_synced_at":"2026-05-17T23:04:46.808Z","repository":{"id":342177059,"uuid":"1172943856","full_name":"AaronCx/taskflow","owner":"AaronCx","description":"Full-stack task management app — Spring Boot 3 + JWT + React/TypeScript + PostgreSQL + Docker","archived":false,"fork":false,"pushed_at":"2026-03-08T01:08:39.000Z","size":158,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-08T04:46:48.985Z","etag":null,"topics":["docker","java","jwt","kafka","microservices","postgresql","react","render","rest-api","spring-boot","task-management","typescript","vercel"],"latest_commit_sha":null,"homepage":"https://taskflow-acx.vercel.app","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AaronCx.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-04T21:16:05.000Z","updated_at":"2026-03-08T01:02:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/AaronCx/taskflow","commit_stats":null,"previous_names":["aaroncx/task-manager","aaroncx/taskflow"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/AaronCx/taskflow","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AaronCx%2Ftaskflow","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AaronCx%2Ftaskflow/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AaronCx%2Ftaskflow/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AaronCx%2Ftaskflow/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AaronCx","download_url":"https://codeload.github.com/AaronCx/taskflow/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AaronCx%2Ftaskflow/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33158772,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-17T22:39:12.733Z","status":"ssl_error","status_checked_at":"2026-05-17T22:39:10.741Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["docker","java","jwt","kafka","microservices","postgresql","react","render","rest-api","spring-boot","task-management","typescript","vercel"],"created_at":"2026-05-17T23:04:42.946Z","updated_at":"2026-05-17T23:04:46.802Z","avatar_url":"https://github.com/AaronCx.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TaskFlow — Full-Stack Task Manager\n\n\u003e **Portfolio project** demonstrating production-ready Java/Spring Boot + React/TypeScript skills.\n\n**[Live Demo](https://taskflow-acx.vercel.app)** — Backend on Render, Frontend on Vercel\n\n[![CI](https://github.com/AaronCx/taskflow/actions/workflows/ci.yml/badge.svg)](https://github.com/AaronCx/taskflow/actions/workflows/ci.yml)\n\n---\n\n## Tech Stack\n\n| Layer | Technology |\n|---|---|\n| **Task API** | Java 17, Spring Boot 3.2, Spring Security, Hibernate/JPA |\n| **Notification API** | Spring Boot 3.2, Spring Kafka (`@KafkaListener`), JPA |\n| **Auth** | JWT (jjwt 0.11.5) — stateless Bearer tokens, shared across services |\n| **Message Bus** | Apache Kafka 3.5 (via Confluent images) + Zookeeper |\n| **Database** | PostgreSQL 15 (shared by both services) |\n| **API Docs** | Springdoc OpenAPI 3 / Swagger UI (both services) |\n| **Frontend** | React 18, TypeScript, Vite, Tailwind CSS, Bun |\n| **HTTP Client** | Axios with in-memory JWT + notification polling |\n| **Routing** | React Router v6 with protected routes |\n| **DevOps** | Docker, Docker Compose, GitHub Actions CI |\n\n---\n\n## Architecture Overview\n\n### Full System (with Kafka)\n\n```\n┌────────────────────────────────────────────────────────────────────────────┐\n│                           Client (Browser)                                 │\n│   React 18 + TypeScript + Tailwind CSS                                     │\n│   JWT in-memory (AuthContext) — never localStorage                         │\n│   Notification bell polls /api/notifications every 30 s                    │\n└───────────┬───────────────────────────────────┬────────────────────────────┘\n            │ /api/auth, /api/tasks              │ /api/notifications\n            │ Bearer token (Axios)               │ Bearer token (same JWT)\n            ▼                                   ▼\n┌───────────────────────┐           ┌───────────────────────────────────────┐\n│  Task Manager API     │           │  Notification Service                 │\n│  Spring Boot  :8080   │           │  Spring Boot  :8081                   │\n│                       │           │                                        │\n│  AuthController       │           │  NotificationController               │\n│  TaskController       │           │    GET  /api/notifications             │\n│  ─────────────────    │           │    GET  /api/notifications/unread-count│\n│  JwtAuthFilter        │           │    PUT  /api/notifications/read-all    │\n│  Spring Security      │           │  ──────────────────────────────────── │\n│  ─────────────────    │           │  JwtAuthFilter  (same secret)         │\n│  AuthService          │           │  NotificationService                  │\n│  TaskService ─────────┼──────┐    │    handleTaskCreated()                │\n│  ─────────────────    │      │    │    handleTaskUpdated()                 │\n│  UserRepository       │      │    │    handleTaskDeleted()                 │\n│  TaskRepository       │      │    │  ──────────────────────────────────── │\n│  DataSeeder           │      │    │  NotificationRepository               │\n└──────────┬────────────┘      │    │  UserRepository  (read-only)          │\n           │ JPA               │    └─────────────┬─────────────────────────┘\n           │                   │                  │ JPA\n           ▼                   │                  ▼\n┌──────────────────────────────┼──────────────────────────────────────────┐\n│                        PostgreSQL 15                                      │\n│    tables:  users  │  tasks  │  notifications                            │\n│             ↑ seeded by DataSeeder on first boot                         │\n└──────────────────────────────┬───────────────────────────────────────────┘\n                               │\n                               ▼\n                  ┌────────────────────────┐\n                  │   Apache Kafka         │\n                  │   (+ Zookeeper)        │\n                  │                        │\n                  │  topics:               │\n                  │    task.created  ──────┼──▶ @KafkaListener\n                  │    task.updated  ──────┼──▶ @KafkaListener\n                  │    task.deleted  ──────┼──▶ @KafkaListener\n                  │                        │\n                  │  Producer: KafkaTemplate (TaskService)\n                  │  Consumer: ConcurrentKafkaListenerContainerFactory\n                  │  Serialiser: JsonSerializer / JsonDeserializer\n                  │  Error handling: DefaultErrorHandler (3 retries, 2 s)\n                  └────────────────────────┘\n```\n\n### Kafka Event Flow\n\n```\nUser Action              Task API                 Kafka              Notifications\n─────────────────────────────────────────────────────────────────────────────────\nPOST /api/tasks    ──▶  TaskService.createTask()\n                         └─ taskRepository.save()\n                         └─ eventProducer.publish() ──▶ task.created ──▶ @KafkaListener\n                                                                          handleTaskCreated()\n                                                                          └─ persist Notification\n                                                                          └─ log meaningful msg\n\nPUT /api/tasks/:id ──▶  TaskService.updateTask()\n                         └─ capture oldStatus\n                         └─ taskRepository.save()\n                         └─ eventProducer.publish() ──▶ task.updated ──▶ @KafkaListener\n                                                                          handleTaskUpdated()\n                                                                          └─ detect status change\n                                                                          └─ persist Notification\n\nDELETE /api/tasks  ──▶  TaskService.deleteTask()\n                         └─ capture task data\n                         └─ taskRepository.delete()\n                         └─ eventProducer.publish() ──▶ task.deleted ──▶ @KafkaListener\n                                                                          handleTaskDeleted()\n                                                                          └─ persist Notification\n\nGET /notifications ──▶  NotificationController ──▶ NotificationService\n                         └─ returns last 20 notifications for current user\n```\n\n### Request Flow (authenticated, task API)\n\n1. React sends `Authorization: Bearer \u003cjwt\u003e` header via Axios.\n2. `JwtAuthenticationFilter` validates the token and populates `SecurityContext`.\n3. Controller receives `@AuthenticationPrincipal User`; service executes business logic.\n4. JPA queries PostgreSQL; response is mapped to a DTO.\n5. After successful DB write, `TaskEventProducer.publish()` fires a Kafka event (non-blocking).\n6. Notification service consumer receives the event and persists a `Notification` row.\n7. Frontend bell icon picks up the new count on the next 30-second poll.\n\n---\n\n## API Endpoints\n\n### Task Manager API — port 8080\n\n| Method | Path | Auth | Description |\n|--------|------|------|-------------|\n| `POST` | `/api/auth/register` | Public | Create a new account |\n| `POST` | `/api/auth/login`    | Public | Authenticate → receive JWT |\n| `GET`  | `/api/tasks`         | Bearer | List tasks (optional `?status=` filter) |\n| `GET`  | `/api/tasks/{id}`    | Bearer | Get a single task |\n| `POST` | `/api/tasks`         | Bearer | Create a task → publishes `task.created` |\n| `PUT`  | `/api/tasks/{id}`    | Bearer | Update a task → publishes `task.updated` |\n| `DELETE` | `/api/tasks/{id}` | Bearer | Delete a task → publishes `task.deleted` |\n\nSwagger UI: **http://localhost:8080/swagger-ui.html**\n\n### Notification Service API — port 8081\n\n| Method | Path | Auth | Description |\n|--------|------|------|-------------|\n| `GET`  | `/api/notifications`               | Bearer | Recent 20 notifications |\n| `GET`  | `/api/notifications/unread-count`   | Bearer | Count for bell badge |\n| `PUT`  | `/api/notifications/read-all`       | Bearer | Mark all as read |\n\nSwagger UI: **http://localhost:8081/swagger-ui.html**\n\n---\n\n## Local Development\n\n### Prerequisites\n\n- Java 17+\n- Maven 3.9+ (or use the included `./mvnw` wrapper)\n- [Bun](https://bun.sh/) (latest)\n- Docker \u0026 Docker Compose\n\n### Option A — Docker Compose (recommended, full stack)\n\n```bash\n# Clone the repo\ngit clone https://github.com/AaronCx/taskflow.git\ncd taskflow\n\n# Start everything: Zookeeper + Kafka + PostgreSQL + API + Notifications\ndocker compose up --build\n\n# Services available at:\n#   Task Manager API:    http://localhost:8080/swagger-ui.html\n#   Notification API:    http://localhost:8081/swagger-ui.html\n#   Kafka (host):        localhost:29092\n```\n\nThen start the frontend separately:\n\n```bash\ncd frontend\nbun install\nbun run dev\n# → http://localhost:5173\n```\n\n### Option B — Manual setup\n\nYou need a running PostgreSQL and Kafka. The easiest way is to start just the\ninfrastructure services via Docker:\n\n```bash\n# Start only infrastructure (Kafka + Zookeeper + PostgreSQL)\ndocker compose up zookeeper kafka db -d\n```\n\nThen in separate terminals:\n\n```bash\n# Task Manager API (port 8080)\ncd backend \u0026\u0026 ./mvnw spring-boot:run\n\n# Notification Service (port 8081)\ncd backend-notifications \u0026\u0026 ./mvnw spring-boot:run\n\n# React frontend (port 5173)\ncd frontend \u0026\u0026 bun install \u0026\u0026 bun run dev\n```\n\n---\n\n## Running Tests\n\n```bash\n# Backend unit tests (uses H2 in-memory — no PostgreSQL needed)\ncd backend\n./mvnw test\n\n# Frontend type-check\ncd frontend\nbun run tsc --noEmit\n```\n\n---\n\n## Demo Credentials\n\nThe database is seeded with sample data on first run:\n\n| Email | Password | Tasks |\n|-------|----------|-------|\n| `alice@demo.com` | `password123` | 8 tasks across all statuses |\n| `bob@demo.com`   | `password123` | 2 tasks |\n\n---\n\n## Project Structure\n\n```\ntaskflow/\n├── backend/                          Task Manager API — Spring Boot 3 (port 8080)\n│   ├── src/main/java/com/portfolio/taskmanager/\n│   │   ├── config/                   SecurityConfig, OpenApiConfig, KafkaProducerConfig\n│   │   ├── controller/               AuthController, TaskController\n│   │   ├── dto/                      Request/Response records\n│   │   ├── entity/                   User, Task (JPA entities)\n│   │   ├── enums/                    TaskStatus, TaskPriority\n│   │   ├── exception/                GlobalExceptionHandler + custom exceptions\n│   │   ├── kafka/                    TaskEvent (record), TaskEventProducer\n│   │   ├── repository/               UserRepository, TaskRepository\n│   │   ├── security/                 JwtTokenProvider, JwtAuthenticationFilter\n│   │   ├── seeder/                   DataSeeder (2 users + 10 tasks on first boot)\n│   │   └── service/                  AuthService, TaskService (publishes Kafka events)\n│   ├── Dockerfile                    Multi-stage build (JDK builder → JRE runtime)\n│   └── pom.xml\n│\n├── backend-notifications/            Notification Service — Spring Boot 3 (port 8081)\n│   ├── src/main/java/com/portfolio/notifications/\n│   │   ├── config/                   SecurityConfig, KafkaConsumerConfig\n│   │   ├── controller/               NotificationController\n│   │   ├── dto/                      NotificationResponse\n│   │   ├── entity/                   Notification, User (read-only view)\n│   │   ├── exception/                GlobalExceptionHandler\n│   │   ├── kafka/                    TaskEvent (mirror), TaskEventConsumer (@KafkaListener)\n│   │   ├── repository/               NotificationRepository, UserRepository\n│   │   ├── security/                 JwtTokenProvider, JwtAuthFilter (same secret)\n│   │   └── service/                  NotificationService (event → message → persist)\n│   ├── Dockerfile\n│   └── pom.xml\n│\n├── frontend/                         React 18 + TypeScript + Vite (port 5173)\n│   └── src/\n│       ├── api/                      axiosClient, auth.ts, tasks.ts, notifications.ts\n│       ├── components/               Layout (with bell), ProtectedRoute, Badges,\n│       │                             NotificationsDropdown (polls every 30 s)\n│       ├── context/                  AuthContext (in-memory JWT)\n│       ├── pages/                    Login, Register, Dashboard, TaskDetail\n│       └── types/                    Shared TypeScript interfaces\n│\n├── .github/workflows/ci.yml          CI: backend + notifications + frontend + docker\n├── docker-compose.yml                Zookeeper + Kafka + PostgreSQL + API + Notifications\n└── README.md\n```\n\n---\n\n## Key Design Decisions\n\n- **Stateless JWT** — no server-side sessions; same secret shared across both services so one login works everywhere.\n- **In-memory token storage** — JWT lives in React state (never `localStorage`) to reduce XSS surface.\n- **Kafka fire-and-forget** — `TaskEventProducer` wraps `KafkaTemplate.send()` in a try-catch; task operations succeed even when Kafka is unavailable.\n- **Consumer error handling** — `DefaultErrorHandler` retries failed messages 3× with 2 s back-off, then skips to prevent partition stall.\n- **`ErrorHandlingDeserializer`** — wraps `JsonDeserializer` so a single malformed Kafka message is skipped, not re-queued forever.\n- **Shared PostgreSQL** — both services connect to the same DB. The notification service reads from `users` (read-only) and owns the `notifications` table.\n- **Ownership scoping** — `TaskRepository.findByIdAndOwner` ensures users can only CRUD their own tasks.\n- **Global exception handler** — every service returns a consistent `ErrorResponse` JSON shape.\n- **Multi-stage Docker builds** — JDK builder stage discarded; runtime images use JRE-only alpine (~250 MB).\n- **DataSeeder guard** — checks `userRepository.count()` before seeding; safe to restart containers.\n\n---\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faaroncx%2Ftaskflow","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faaroncx%2Ftaskflow","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faaroncx%2Ftaskflow/lists"}