Tasker - kanban-подобное веб-приложение для управления задачами и досками. Проект построен как набор сервисов вокруг доменной модели "доски - колонки - карточки", с разделением write/read-моделей и отдельным сервисом аутентификации.
Основной сценарий использования:
- пользователь регистрируется и входит в систему
- создаёт доски (по шаблону или с нуля)
- добавляет колонки и карточки
- назначает исполнителей и метки, управляет дедлайнами
- просматривает доски и карточки через read-модель, оптимизированную под чтение
Проект организован как monorepo на .NET 8 и React:
- бэкенд на C# с элементами DDD и CQRS
- отдельные сервисы для Auth, BoardWrite и BoardRead
- MySQL как write-хранилище
- Cassandra как хранилище снапшотов read-модели
- Redis для сессий и проверки access токенов
- Kafka-совместимый брокер Redpanda для интеграционных событий
- фронтенд на Vite + React + TypeScript
- мониторинг через Prometheus и Grafana
- Nginx как единая точка входа в docker окружении
В deploy/compose/docker-compose.yml поднимается 11 контейнеров:
- Образ:
mysql:8.0 - Назначение: основное write-хранилище для Auth и BoardWrite
- Порт на хосте:
3306 - Персистентность: volume
mysql_data:/var/lib/mysql - Используется сервисами:
auth-api(база пользователей, пароли, доменные события)boardwrite-api(доски, колонки, карточки, связи с метками и исполнителями)boardread-apiкак fallback источник для read-модели и пользователей
- Образ:
cassandra:4.1 - Назначение: хранение снапшотов досок для read-модели
- Порт на хосте:
9042 - Персистентность: volume
cassandra_data1:/var/lib/cassandra - При старте
boardread-apiиboardwrite-apiсоздают keyspacetasker_readи таблицуboard_snapshots
- Образ:
redpandadata/redpanda:v24.2.7 - Назначение: Kafka-совместимый брокер для доменных событий
- Порт на хосте:
19092(внешний kafka endpoint) - Персистентность: volume
redpanda_data:/var/lib/redpanda/data - Используется:
auth-apiиboardwrite-apiчерез общую библиотекуTasker.Shared.Kafka
- Образ:
redis - Назначение:
- хранение активных сессий и refresh токенов Auth
- проверка access токенов в BoardRead и BoardWrite
- Порт на хосте:
6379 - К нему подключаются:
auth-api-RedisAuthSessionStoreboardread-apiиboardwrite-api-RedisAccessTokenValidator
- Dockerfile:
src/services/Auth/Tasker.Auth.Api/Dockerfile - Объявленный url внутри контейнера:
http://+:8080 - Публикуется только во внутренней сети docker, наружу выходит через nginx
- Отвечает за:
- регистрацию пользователей
- логин и выдачу access токенов
- хранение сессий в Redis
- Использует:
- MySQL (
AuthDbContext) - Redis (
RedisAuthSessionStore) - Redpanda (Kafka продюсер для событий)
- MySQL (
Основные HTTP эндпоинты:
-
POST /api/v1/auth/register- тело:
RegisterUserCommand(Email, DisplayName, Password) - результат:
RegisterUserResultсUserId
- тело:
-
POST /api/v1/auth/login- тело:
LoginUserCommandBody { Email, Password } - TTL сессии берётся из
Auth:SessionTtlMinutes - результат:
LoginUserResultсuserId,accessToken,expiresInSeconds
- тело:
- Dockerfile:
src/services/BoardRead/Tasker.BoardRead.Api/Dockerfile - Назначение: read-модель досок и пользователей
- Внутренний порт:
8080(за nginx снаружи) - Хранение:
- Cassandra как основной источник снапшотов досок
- MySQL (AuthDbContext, BoardWriteDbContext) как fallback для чтения
- Аутентификация:
- кастомный
AccessTokenAuthenticationHandler - проверка токена через Redis (
RedisAccessTokenValidator)
- кастомный
Основные HTTP эндпоинты (все под [Authorize]):
-
GET /api/v1/boards/my- возвращает список досок текущего пользователя:
- тип ответа:
IReadOnlyCollection<BoardView>
-
GET /api/v1/boards/{boardId}- возвращает детальное представление доски:
- тип ответа:
BoardDetailsView 404если доска не найдена
- Dockerfile:
src/services/BoardWrite/Tasker.BoardWrite.Api/Dockerfile - Назначение: write-модель досок и их изменений
- Внутренний порт:
8080(за nginx) - Отвечает за:
- создание и обновление досок
- управление колонками
- управление карточками
- метки, исполнители, дедлайны
- запись снапшотов в Cassandra
- публикацию интеграционных событий в Kafka
Контроллер: Tasker.BoardWrite.Api.Controllers.Boards.BoardsController
Основные эндпоинты:
-
Доски:
GET /api/v1/boards/my- список досок текущего пользователя (временно на write стороне)POST /api/v1/boards- создание доскиGET /api/v1/boards/{boardId}- детали доски (временно на write стороне)GET /api/v1/boards/templates- список доступных шаблонов досок
-
Колонки:
POST /api/v1/boards/{boardId}/columns- создание новой колонки
-
Карточки:
POST /api/v1/boards/{boardId}/cards- создание карточкиPUT /api/v1/boards/{boardId}/cards/{cardId}- обновление карточкиPOST /api/v1/boards/{boardId}/cards/{cardId}/move- перенос в другую колонкуPOST /api/v1/boards/{boardId}/cards/{cardId}/due-date- установка или сброс дедлайна
-
Участники:
POST /api/v1/boards/{boardId}/members- добавление участника доскиPOST /api/v1/boards/{boardId}/cards/{cardId}/assignees- назначить исполнителяPOST /api/v1/boards/{boardId}/cards/{cardId}/assignees/remove- снять исполнителя
-
Метки:
POST /api/v1/boards/{boardId}/labels- создать метку на доскеPOST /api/v1/boards/{boardId}/cards/{cardId}/labels- назначить метку карточкеPOST /api/v1/boards/{boardId}/cards/{cardId}/labels/remove- снять метку с карточки
- Образ:
prom/prometheus:latest - Назначение: сбор метрик из сервисов
- Порт на хосте:
9090 - Конфигурация:
deploy/compose/prometheus/prometheus.ymlмонтируется в/etc/prometheus/prometheus.yml - Персистентность: volume
prometheus_data:/prometheus
Сервисы отдают метрики через OpenTelemetry и Prometheus endpoint.
- Образ:
grafana/grafana:latest - Назначение: визуализация метрик
- Порт на хосте:
3000 - Персистентность: volume
grafana_data:/var/lib/grafana - Админ логин и пароль задаются через env:
GRAFANA_ADMIN_USERGRAFANA_ADMIN_PASSWORD
- По умолчанию авторегистрация пользователей выключена
- Каталог:
Web/tasker-ui-web - Стек:
- Vite
- React
- TypeScript
- Контейнер таргетирует Vite dev server
- Порт на хосте:
5173 - Основные маршруты SPA:
/login- форма входа/register- регистрация/- список досок текущего пользователя/boards/:boardId- просмотр и управление конкретной доской
Авторизация на фронтенде:
AuthContextхранит:userIdaccessToken- флаги
isAuthenticatedиisInitialized
- данные подтягиваются из
localStorageна старте RequireAuthзащищает приватные маршруты и редиректит на/loginпри отсутствии токена
Конфигурация API url через переменные окружения:
VITE_AUTH_API_BASE_URL- база для AuthVITE_BOARDREAD_API_BASE_URL- база для BoardReadVITE_BOARDWRITE_API_BASE_URL- база для BoardWrite
В docker окружении фронтенд по умолчанию настроен на работу через nginx по адресу http://localhost:8080.
- Образ:
nginx:1.27-alpine - Публикует порт
8080на хосте - Конфиг:
deploy/compose/nginx/nginx.confмонтируется в/etc/nginx/nginx.conf - Работает как reverse proxy:
/- проксирование на Vite dev serverfrontend:5173/api/auth- прокси наauth-api/api/boardread- прокси наboardread-api/api/boardwrite- прокси наboardwrite-api
Итог: при запуске через docker весь внешний трафик к API идёт через http://localhost:8080/....
Предварительные шаги:
-
Скопировать файл переменных окружения:
- из
deploy/compose/.env-exampleвdeploy/compose/.env - при необходимости поправить пароли и логины
- из
-
Собрать и поднять стек:
cd deploy/compose
docker compose -f docker-compose.yml up --build -dПосле успешного запуска:
- фронтенд через nginx:
http://localhost:8080 - фронтенд напрямую (Vite):
http://localhost:5173 - Prometheus:
http://localhost:9090 - Grafana:
http://localhost:3000
Фронтенд в docker конфигурирован ходить на API через nginx (http://localhost:8080/api/...).
Этот вариант удобен когда:
- сервисы .NET запускаются через
dotnet runиз IDE - фронтенд запускается командой
npm run dev
Типичная схема:
-
Поднять инфраструктуру (MySQL, Cassandra, Redis, Redpanda, Prometheus, Grafana) через docker или отдельные контейнеры.
-
Запустить
Auth,BoardWrite,BoardReadлокально на портах5001,5003,5002. -
Для фронтенда использовать
.env.developmentсо значениями:VITE_AUTH_API_BASE_URL=http://localhost:5001/api/v1VITE_BOARDREAD_API_BASE_URL=http://localhost:5002/api/v1VITE_BOARDWRITE_API_BASE_URL=http://localhost:5003/api/v1
-
Запустить фронтенд:
cd Web/tasker-ui-webnpm installnpm run dev
-
Открыть
http://localhost:5173.
В этом режиме CORS включён в каждом .NET сервисе для origin http://localhost:5173.
-
Регистрация и логин:
POST /api/v1/auth/registerPOST /api/v1/auth/login
-
Работа с досками (write):
GET /api/v1/boards/myPOST /api/v1/boardsGET /api/v1/boards/{boardId}GET /api/v1/boards/templates
-
Работа с колонками:
POST /api/v1/boards/{boardId}/columns
-
Работа с карточками:
POST /api/v1/boards/{boardId}/cardsPUT /api/v1/boards/{boardId}/cards/{cardId}POST /api/v1/boards/{boardId}/cards/{cardId}/movePOST /api/v1/boards/{boardId}/cards/{cardId}/due-date
-
Участники и исполнители:
POST /api/v1/boards/{boardId}/membersPOST /api/v1/boards/{boardId}/cards/{cardId}/assigneesPOST /api/v1/boards/{boardId}/cards/{cardId}/assignees/remove
-
Метки:
POST /api/v1/boards/{boardId}/labelsPOST /api/v1/boards/{boardId}/cards/{cardId}/labelsPOST /api/v1/boards/{boardId}/cards/{cardId}/labels/remove
-
Все эндпоинты BoardRead и BoardWrite защищены
[Authorize]. -
Клиент обязан передавать заголовок:
Authorization: Bearer <accessToken>
-
Access токен выдаётся Auth сервисом и валидируется через Redis.
Несколько известных особенностей и потенциальных проблем:
-
Сильная зависимость от Redis:
- Auth хранит в Redis активные сессии и refresh токены
- BoardRead и BoardWrite проверяют access токены через Redis
- при падении Redis весь доступ к API становится невозможен
-
Read модель и fallback:
- BoardRead использует Cassandra как основное хранилище снапшотов досок, но всё ещё имеет fallback в MySQL контексты Auth и BoardWrite
- это создаёт дополнительную связанность между сервисами и увеличивает нагрузку на MySQL
-
Eventual consistency:
- изменения на write стороне публикуются в Kafka и обновляют read модель
- в моменты нагрузки возможны короткие периоды, когда свежие изменения ещё не видны на стороне чтения
-
Дополнительная прослойка nginx:
- в docker окружении весь HTTP трафик идёт через nginx
- это даёт единый входной пункт и удобный роутинг, но добавляет ещё один слой для диагностики при проблемах с сетью
Tasker строится как pet проект с прицелом на реалистичную архитектуру: отдельные домены, read/write модели, событийная интеграция и наблюдаемость через метрики и дашборды.