diff --git a/README.md b/README.md new file mode 100644 index 0000000..dcb150b --- /dev/null +++ b/README.md @@ -0,0 +1,595 @@ +# 🎯 Surway + +**Современный full-stack сервис для создания и проведения опросов с множественным выбором** + +Легкий, быстрый и масштабируемый микросервис для создания временных опросов с автоматическим истечением срока действия. Построен на Go + Redis в backend и Next.js 15 + React 19 на frontend. + +--- + +## 📋 Описание + +Surway — это full-stack приложение для создания и проведения опросов (surveys/polls) с поддержкой множественного выбора вариантов. Проект использует Redis в качестве основного хранилища данных с нативной поддержкой TTL (Time To Live), что обеспечивает автоматическое удаление устаревших опросов и высокую производительность. + +### Ключевые особенности + +- **Множественный выбор** — пользователи могут выбрать несколько вариантов ответа +- **Временные опросы** — автоматическое удаление по истечении TTL (от 7 до 30 дней) +- **Real-time результаты** — мгновенное отображение результатов голосования +- **REST API** с Swagger документацией +- **Атомарные операции** — безопасный подсчет голосов через Redis pipeline +- **Graceful shutdown** — корректное завершение работы сервера +- **Production-ready** — Docker Compose + Caddy для SSL и reverse proxy +- **Современный UI** — анимации, графики, адаптивный дизайн + +--- + +## 🏗️ Архитектура + +Проект следует принципам Clean Architecture и разделен на независимые слои: + +``` +surway/ +├── backend/ # Go backend +│ ├── cmd/ +│ │ └── api/ +│ │ └── main.go # Точка входа с Swagger аннотациями +│ ├── internal/ +│ │ ├── handler/ # HTTP handlers (Gin framework) +│ │ │ ├── poll.go # CRUD операции для опросов +│ │ │ └── router.go # Роутинг + middleware (CORS, logging) +│ │ ├── service/ # Бизнес-логика +│ │ │ ├── poll.go +│ │ │ └── poll_test.go # Unit тесты +│ │ ├── storage/ # Слой данных +│ │ │ └── redis.go # Redis реализация Storage интерфейса +│ │ ├── model/ # Модели данных +│ │ │ └── poll.go # Poll, Request/Response структуры +│ │ ├── config/ # Конфигурация +│ │ │ └── config.go # Загрузка из env переменных +│ │ └── lib/ +│ │ └── random/ # Генерация коротких ID для опросов +│ │ └── random.go +│ ├── docs/ # Swagger документация (auto-generated) +│ │ ├── docs.go +│ │ └── swagger.yaml +│ ├── Dockerfile # Multi-stage build +│ ├── go.mod +│ └── go.sum +│ +├── frontend/ # Next.js 15 frontend +│ ├── app/ # App Router (Next.js 15) +│ │ ├── page.tsx # Главная страница +│ │ ├── layout.tsx # Root layout +│ │ ├── create/ # Создание опроса +│ │ ├── [id]/ # Страницы опроса (динамический роутинг) +│ │ │ ├── page.tsx # Голосование +│ │ │ └── results/ # Результаты +│ │ ├── config/ # Конфигурация приложения +│ │ └── services/ # API клиент +│ ├── components/ # React компоненты +│ │ ├── ui/ # UI компоненты +│ │ └── analytics/ # Графики и визуализация +│ ├── public/ # Статические файлы +│ ├── Dockerfile # Multi-stage build для production +│ ├── package.json +│ └── next.config.js +│ +├── configs/ # Конфигурационные файлы +│ ├── redis.conf # Настройки Redis для production +│ └── README.md +│ +├── docker-compose.yml # Development environment +├── docker-compose.prod.yml # Production environment с Caddy +├── Caddyfile # Reverse proxy + автоматический SSL +├── Makefile # Команды для разработки +└── .env # Переменные окружения (не коммитится) +``` + +### Компоненты системы + +#### Backend (Go) +- **Handler Layer** — обработка HTTP запросов, валидация, маппинг ошибок +- **Service Layer** — бизнес-логика, генерация ID, создание URL +- **Storage Layer** — работа с Redis через интерфейс +- **Middleware** — CORS, структурированное логирование (slog), recovery +- **Swagger** — автогенерация OpenAPI документации + +#### Frontend (Next.js) +- **Server Components** — для SEO и производительности +- **Client Components** — для интерактивности +- **API Service** — централизованный клиент для работы с backend +- **Recharts** — визуализация результатов в виде графиков +- **Framer Motion** — плавные анимации и переходы +- **Tailwind CSS 4** — современный стилинг + +#### Storage (Redis) +Использует два типа структур данных: +- **String** для метаданных опроса (JSON с TTL) +- **Hash** для счетчиков голосов (атомарный HINCRBY) +- **Pipeline** для атомарности создания/голосования + +--- + +## ✨ Функциональность + +### API Endpoints + +| Метод | Путь | Описание | +|-------|------|----------| +| `POST` | `/api/v1/polls` | Создать новый опрос | +| `POST` | `/api/v1/polls/{id}/vote` | Проголосовать (множественный выбор) | +| `GET` | `/api/v1/polls/{id}/results` | Получить результаты опроса | +| `GET` | `/health` | Health check endpoint | +| `GET` | `/swagger/*` | Swagger UI документация | + +### Примеры запросов + +**Создание опроса:** +```bash +curl -X POST http://localhost:8080/api/v1/polls \ + -H "Content-Type: application/json" \ + -d '{ + "title": "Какие языки программирования вы используете?", + "options": ["Go", "Python", "JavaScript", "Rust", "TypeScript"] + }' +``` + +**Ответ:** +```json +{ + "poll_id": "abc123", + "vote_url": "http://localhost:8080/api/v1/polls/abc123/vote", + "results_url": "http://localhost:8080/api/v1/polls/abc123/results" +} +``` + +**Голосование (множественный выбор):** +```bash +curl -X POST http://localhost:8080/api/v1/polls/abc123/vote \ + -H "Content-Type: application/json" \ + -d '{ + "option_indices": [0, 2, 4] + }' +``` + +**Получение результатов:** +```bash +curl http://localhost:8080/api/v1/polls/abc123/results +``` + +--- + +## 🚀 Быстрый старт + +### Требования + +- **Docker** 20.10+ и **Docker Compose** v2 +- Или локально: + - Go 1.24.2+ + - Node.js 20+ + - Redis 7+ + +### Запуск через Docker Compose (рекомендуется) + +1. **Клонируйте репозиторий:** + ```bash + git clone https://github.com/AlexeyLars/surway.git + cd surway + ``` + +2. **Запустите все сервисы:** + ```bash + make docker-up + # или + docker compose up -d + ``` + +3. **Доступ к приложению:** + - Frontend: http://localhost:3000 + - Backend API: http://localhost:8080 + - Swagger UI: http://localhost:8080/swagger/index.html + - Health check: http://localhost:8080/health + +4. **Просмотр логов:** + ```bash + make docker-logs + # или + docker compose logs -f + ``` + +5. **Остановка:** + ```bash + make docker-down + # или + docker compose down + ``` + +### Локальная разработка + +#### Backend + +1. **Настройте переменные окружения:** + ```bash + cd backend + cp .env.example .env # если есть, или создайте .env + ``` + +2. **Установите зависимости:** + ```bash + make deps + # или + go mod download + ``` + +3. **Запустите Redis:** + ```bash + docker run -d -p 6379:6379 redis:7-alpine + ``` + +4. **Запустите backend:** + ```bash + make run + # или + go run cmd/api/main.go + ``` + +5. **Сгенерируйте Swagger документацию:** + ```bash + go install github.com/swaggo/swag/cmd/swag@latest + swag init -g cmd/api/main.go + ``` + +#### Frontend + +1. **Настройте переменные окружения:** + ```bash + cd frontend + cp env.example .env.local + ``` + +2. **Установите зависимости:** + ```bash + npm install + ``` + +3. **Запустите dev сервер:** + ```bash + npm run dev + ``` + +--- + +## ⚙️ Конфигурация + +### Backend (переменные окружения) + +#### Настройки сервера + +| Переменная | Описание | Значение по умолчанию | +|-----------|----------|----------------------| +| `ENV` | Окружение (dev/prod) | `dev` | +| `SERVER_HOST` | Хост сервера | `0.0.0.0` | +| `SERVER_PORT` | Порт сервера | `8080` | +| `SERVER_READ_TIMEOUT` | Таймаут чтения | `10s` | +| `SERVER_WRITE_TIMEOUT` | Таймаут записи | `10s` | +| `SERVER_SHUTDOWN_TIMEOUT` | Graceful shutdown таймаут | `5s` | +| `BASE_URL` | Базовый URL для генерации ссылок | `http://localhost:8080` | + +#### Настройки Redis + +| Переменная | Описание | Значение по умолчанию | +|-----------|----------|----------------------| +| `REDIS_HOST` | Хост Redis | `localhost` | +| `REDIS_PORT` | Порт Redis | `6379` | +| `REDIS_PASSWORD` | Пароль Redis | _(пусто)_ | +| `REDIS_DB` | Номер БД Redis | `0` | + +#### Настройки опросов + +| Переменная | Описание | Значение по умолчанию | +|-----------|----------|----------------------| +| `POLL_DEFAULT_TTL` | TTL опроса по умолчанию | `168h` (7 дней) | +| `POLL_MAX_TTL` | Максимальный TTL | `720h` (30 дней) | + +### Frontend (переменные окружения) + +**Client-side (NEXT_PUBLIC_*):** +- `NEXT_PUBLIC_API_PROTOCOL` — http/https +- `NEXT_PUBLIC_API_HOST` — localhost +- `NEXT_PUBLIC_API_PORT` — 8080 +- `NEXT_PUBLIC_API_VERSION` — v1 +- `NEXT_PUBLIC_FRONTEND_PROTOCOL` — http +- `NEXT_PUBLIC_FRONTEND_HOST` — localhost +- `NEXT_PUBLIC_FRONTEND_PORT` — 3000 + +**Server-side (для SSR):** +- `API_INTERNAL_PROTOCOL` — http +- `API_INTERNAL_HOST` — backend +- `API_INTERNAL_PORT` — 8080 + +### Пример `.env` файла для backend + +```env +# Environment +ENV=dev + +# Server +SERVER_HOST=0.0.0.0 +SERVER_PORT=8080 +BASE_URL=http://localhost:8080 + +# Redis +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= +REDIS_DB=0 + +# Polls +POLL_DEFAULT_TTL=168h +POLL_MAX_TTL=720h +``` + +--- + +## 🔧 Разработка + +### Makefile команды + +```bash +make help # Показать все доступные команды +make build # Собрать backend бинарник +make run # Запустить backend локально +make test # Запустить тесты с race detector +make test-coverage # Показать покрытие тестами +make docker-up # Запустить Docker Compose +make docker-down # Остановить Docker Compose +make docker-logs # Показать логи контейнеров +make docker-build # Собрать Docker образы +make fmt # Форматировать Go код +make lint # Запустить линтер +make deps # Установить/обновить зависимости +make clean # Удалить артефакты сборки +``` + +### Тестирование + +```bash +# Backend +cd backend +go test -v ./... # Все тесты +go test -v -race ./... # С race detector +go test -cover ./... # С покрытием +go test -coverprofile=coverage.out ./... # Сохранить отчет +go tool cover -html=coverage.out # Открыть HTML отчет + +# Frontend +cd frontend +npm run test # Jest тесты (если настроены) +npm run lint # ESLint проверка +``` + +### Линтинг + +**Backend:** +```bash +# Установка golangci-lint +go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + +# Запуск +make lint +# или +cd backend && golangci-lint run +``` + +**Frontend:** +```bash +cd frontend +npm run lint +``` + +### Swagger документация + +Swagger документация генерируется автоматически из аннотаций в коде: + +```bash +cd backend +swag init -g cmd/api/main.go +``` + +Доступна по адресу: http://localhost:8080/swagger/index.html + +--- + +## 🐳 Docker + +### Development (docker-compose.yml) + +Включает: +- `redis` — Redis 7 с persistence +- `backend` — Go API сервер +- `frontend` — Next.js приложение + +Порты наружу: +- 6379 (Redis) +- 8080 (Backend) +- 3000 (Frontend) + +### Production (docker-compose.prod.yml) + +Включает дополнительно: +- `caddy` — Reverse proxy + автоматический SSL от Let's Encrypt + +Особенности production конфига: +- Порты backend/frontend не выставлены наружу (только через Caddy) +- Redis с кастомной конфигурацией (`configs/redis.conf`) +- Автоматический SSL через Caddy +- Restart policy: `always` + +**Настройка для production:** + +1. Отредактируйте `Caddyfile`: + ``` + your-domain.com { + reverse_proxy frontend:3000 + handle /api/* { + reverse_proxy backend:8080 + } + } + ``` + +2. Обновите переменные в `docker-compose.prod.yml`: + ```yaml + BASE_URL=https://your-domain.com/api + NEXT_PUBLIC_API_HOST=your-domain.com + ``` + +3. Запустите: + ```bash + docker compose -f docker-compose.prod.yml up -d + ``` + +### Структура данных в Redis + +**Метаданные опроса (String с TTL):** +``` +Ключ: poll:{poll_id}:info +Значение: JSON с Poll структурой +TTL: 168h (по умолчанию) +``` + +**Счетчики голосов (Hash с TTL):** +``` +Ключ: poll:{poll_id}:votes +Значение: Hash { + "0": "15", # индекс опции -> количество голосов + "1": "42", + "2": "8", + ... +} +TTL: 168h (синхронизирован с info) +``` + +**Пример:** +```bash +# Получить метаданные +redis-cli GET "poll:abc123:info" + +# Получить голоса +redis-cli HGETALL "poll:abc123:votes" + +# TTL +redis-cli TTL "poll:abc123:info" +``` + +--- + +## 🛠️ Технологический стек + +### Backend +- **Go 1.24.2** — основной язык +- **Gin** — web framework +- **go-redis/v9** — Redis клиент +- **cleanenv** — загрузка конфигурации +- **swaggo/swag** — генерация Swagger документации +- **slog** — структурированное логирование (стандартная библиотека) +- **testify** — тестирование + +### Frontend +- **Next.js 15.5.4** — React framework с App Router +- **React 19.1.0** — UI библиотека +- **TypeScript 5** — типизация +- **Tailwind CSS 4** — утилитарный CSS +- **Recharts 3** — графики и визуализация +- **Framer Motion 12** — анимации +- **lucide-react** — иконки + +### Infrastructure +- **Redis 7** — основное хранилище данных +- **Docker & Docker Compose** — контейнеризация +- **Caddy 2** — reverse proxy + SSL +- **Alpine Linux** — базовые образы + +--- + +## 🗺️ Roadmap + +### В разработке +- [ ] Защита от повторного голосования (cookies/IP) +- [ ] Rate limiting middleware +- [ ] WebSocket для live обновления результатов +- [ ] Темная тема UI +- [ ] Экспорт результатов (CSV, PDF) + +### Планируется +- [ ] Аутентификация и личные кабинеты +- [ ] История созданных опросов +- [ ] Кастомизация опросов (цвета, фон) +- [ ] Аналитика и статистика +- [ ] CI/CD pipeline (GitHub Actions) +- [ ] Метрики и мониторинг (Prometheus + Grafana) +- [ ] E2E тесты (Playwright) +- [ ] Интеграционные тесты для API + +--- + +## 🤝 Контрибьюция + +Приветствуются любые предложения и улучшения! + +### Процесс контрибьюции: + +1. Fork репозитория +2. Создайте feature-ветку от `develop`: + ```bash + git checkout develop + git checkout -b feature/amazing-feature + ``` +3. Сделайте изменения и commit: + ```bash + git commit -m 'feat: add amazing feature' + ``` +4. Push в вашу ветку: + ```bash + git push origin feature/amazing-feature + ``` +5. Откройте Pull Request в `develop` ветку + +### Стандарты кода: + +- **Backend:** следуйте [Effective Go](https://golang.org/doc/effective_go) и запускайте `make lint` +- **Frontend:** используйте ESLint конфигурацию проекта +- **Commits:** используйте [Conventional Commits](https://www.conventionalcommits.org/) + - `feat:` — новая функциональность + - `fix:` — исправление бага + - `docs:` — документация + - `refactor:` — рефакторинг + - `test:` — добавление тестов + - `chore:` — рутинные задачи + +--- + +## 📄 Лицензия + +Этот проект создан в образовательных целях. + +--- + +## 👤 Автор + +**Alexey Lars** +- GitHub: [@AlexeyLars](https://github.com/AlexeyLars) + +--- + +## 🔗 Полезные ссылки + +- [Backend README](backend/README.md) _(если будет создан)_ +- [Frontend README](frontend/README.md) +- [Configs README](configs/README.md) +- [Swagger UI](http://localhost:8080/swagger/index.html) _(при запущенном сервере)_ + +--- + +**Статус проекта:** 🚀 Active Development + +_Создано с использованием Go, Next.js и Redis_ diff --git a/docs/wiki/API-Documentation.md b/docs/wiki/API-Documentation.md new file mode 100644 index 0000000..a35d30c --- /dev/null +++ b/docs/wiki/API-Documentation.md @@ -0,0 +1,527 @@ +# 📡 API Documentation + +Полная документация REST API для Surway сервиса. + +--- + +## 🌐 Base URL + +**Development:** +``` +http://localhost:8080/api/v1 +``` + +**Production:** +``` +https://your-domain.com/api/v1 +``` + +--- + +## 📚 Swagger UI + +Интерактивная документация доступна по адресу: + +``` +http://localhost:8080/swagger/index.html +``` + +Swagger UI позволяет: +- Просматривать все endpoints +- Тестировать запросы прямо из браузера +- Видеть схемы данных +- Экспортировать OpenAPI спецификацию + +--- + +## 🔐 Authentication + +**Текущая версия:** API не требует аутентификации + +**В планах:** +- JWT tokens для личных кабинетов +- API keys для внешних интеграций + +--- + +## 📋 Endpoints + +### Health Check + +#### `GET /health` + +Проверка работоспособности сервиса. + +**Response:** +```json +{ + "status": "ok" +} +``` + +**Status Codes:** +- `200` — сервис работает + +--- + +### Create Poll + +#### `POST /api/v1/polls` + +Создать новый опрос. + +**Request Body:** +```json +{ + "title": "Какие языки программирования вы используете?", + "options": [ + "Go", + "Python", + "JavaScript", + "Rust", + "TypeScript" + ] +} +``` + +**Validation Rules:** +- `title`: обязательное, 3-200 символов +- `options`: массив из 2-10 элементов, каждый элемент 1-100 символов + +**Response:** +```json +{ + "poll_id": "abc123", + "vote_url": "http://localhost:8080/api/v1/polls/abc123/vote", + "results_url": "http://localhost:8080/api/v1/polls/abc123/results" +} +``` + +**Status Codes:** +- `201` — опрос успешно создан +- `400` — невалидные данные +- `500` — внутренняя ошибка сервера + +**Error Response:** +```json +{ + "error": "invalid_request", + "message": "Title is required" +} +``` + +**cURL Example:** +```bash +curl -X POST http://localhost:8080/api/v1/polls \ + -H "Content-Type: application/json" \ + -d '{ + "title": "Какие языки программирования вы используете?", + "options": ["Go", "Python", "JavaScript", "Rust", "TypeScript"] + }' +``` + +--- + +### Vote + +#### `POST /api/v1/polls/{id}/vote` + +Проголосовать в опросе (поддерживается множественный выбор). + +**Path Parameters:** +- `id` (string, required) — ID опроса + +**Request Body:** +```json +{ + "option_indices": [0, 2, 4] +} +``` + +**Validation Rules:** +- `option_indices`: массив индексов, минимум 1 элемент +- Каждый индекс должен быть >= 0 и < количества опций +- Индексы должны быть уникальными (нельзя голосовать за одну опцию дважды) + +**Response:** +```json +{ + "success": true, + "message": "Votes registered successfully (3 options)" +} +``` + +**Status Codes:** +- `200` — голос успешно зарегистрирован +- `400` — невалидные данные / дубликат индекса +- `404` — опрос не найден или истек +- `500` — внутренняя ошибка сервера + +**Error Responses:** + +*Poll not found:* +```json +{ + "error": "poll_not_found", + "message": "Poll not found or expired" +} +``` + +*Invalid option index:* +```json +{ + "error": "invalid_option", + "message": "Invalid option index" +} +``` + +*Duplicate option:* +```json +{ + "error": "duplicate_option", + "message": "Cannot vote for the same option multiple times" +} +``` + +**cURL Example:** +```bash +curl -X POST http://localhost:8080/api/v1/polls/abc123/vote \ + -H "Content-Type: application/json" \ + -d '{ + "option_indices": [0, 2, 4] + }' +``` + +**Примечания:** +- Можно голосовать за одну опцию: `"option_indices": [0]` +- Можно голосовать за несколько: `"option_indices": [0, 1, 3]` +- В текущей версии нет защиты от повторного голосования +- Каждый запрос увеличивает счетчики выбранных опций + +--- + +### Get Results + +#### `GET /api/v1/polls/{id}/results` + +Получить результаты опроса. + +**Path Parameters:** +- `id` (string, required) — ID опроса + +**Response:** +```json +{ + "poll": { + "id": "abc123", + "title": "Какие языки программирования вы используете?", + "options": [ + "Go", + "Python", + "JavaScript", + "Rust", + "TypeScript" + ], + "created_at": "2025-12-08T10:00:00Z", + "expires_at": "2025-12-15T10:00:00Z" + }, + "votes": { + "Go": 42, + "Python": 28, + "JavaScript": 35, + "Rust": 15, + "TypeScript": 30 + }, + "total": 150 +} +``` + +**Status Codes:** +- `200` — результаты успешно получены +- `404` — опрос не найден или истек +- `500` — внутренняя ошибка сервера + +**Error Response:** +```json +{ + "error": "poll_not_found", + "message": "Poll not found or expired" +} +``` + +**cURL Example:** +```bash +curl http://localhost:8080/api/v1/polls/abc123/results +``` + +--- + +## 🔄 Типичные флоу + +### Полный жизненный цикл опроса + +```mermaid +sequenceDiagram + participant User + participant Frontend + participant Backend + participant Redis + + User->>Frontend: Создает опрос + Frontend->>Backend: POST /api/v1/polls + Backend->>Redis: CREATE poll:abc123:info & poll:abc123:votes + Redis-->>Backend: OK + Backend-->>Frontend: {poll_id, vote_url, results_url} + Frontend-->>User: Redirect to vote page + + User->>Frontend: Голосует + Frontend->>Backend: POST /api/v1/polls/abc123/vote + Backend->>Redis: HINCRBY poll:abc123:votes + Redis-->>Backend: OK + Backend-->>Frontend: {success: true} + Frontend-->>User: Redirect to results + + User->>Frontend: Смотрит результаты + Frontend->>Backend: GET /api/v1/polls/abc123/results + Backend->>Redis: GET + HGETALL + Redis-->>Backend: Poll data + votes + Backend-->>Frontend: {poll, votes, total} + Frontend-->>User: Show charts +``` + +--- + +## 📊 Data Models + +### Poll + +```typescript +interface Poll { + id: string; // Уникальный ID (7 символов) + title: string; // Название опроса + options: string[]; // Варианты ответа + created_at: string; // ISO 8601 timestamp + expires_at: string; // ISO 8601 timestamp +} +``` + +### CreatePollRequest + +```typescript +interface CreatePollRequest { + title: string; // 3-200 символов + options: string[]; // 2-10 элементов, каждый 1-100 символов +} +``` + +### CreatePollResponse + +```typescript +interface CreatePollResponse { + poll_id: string; // Сгенерированный ID + vote_url: string; // URL для голосования + results_url: string; // URL для результатов +} +``` + +### VoteRequest + +```typescript +interface VoteRequest { + option_indices: number[]; // Массив индексов, минимум 1 +} +``` + +### VoteResponse + +```typescript +interface VoteResponse { + success: boolean; // true если успешно + message?: string; // Опциональное сообщение +} +``` + +### PollResults + +```typescript +interface PollResults { + poll: Poll; // Метаданные опроса + votes: { // Карта: название опции -> количество голосов + [option: string]: number; + }; + total: number; // Общее количество голосов +} +``` + +### ErrorResponse + +```typescript +interface ErrorResponse { + error: string; // Код ошибки (snake_case) + message?: string; // Человекочитаемое описание +} +``` + +--- + +## ⚡ Rate Limiting + +**Текущая версия:** Rate limiting не реализован + +**В планах:** +- 100 requests/minute для создания опросов +- 1000 requests/minute для голосования +- Unlimited для получения результатов + +--- + +## 🔒 CORS + +API поддерживает CORS для всех origins: + +``` +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: POST, OPTIONS, GET, PUT, DELETE +Access-Control-Allow-Headers: Content-Type, Authorization, ... +``` + +**Production:** рекомендуется ограничить `Allow-Origin` вашим доменом. + +--- + +## 🐛 Error Codes + +| Error Code | HTTP Status | Описание | +|-----------|-------------|----------| +| `invalid_request` | 400 | Невалидные входные данные | +| `poll_not_found` | 404 | Опрос не найден или истек | +| `invalid_option` | 400 | Невалидный индекс опции | +| `duplicate_option` | 400 | Дубликат индекса в option_indices | +| `internal_error` | 500 | Внутренняя ошибка сервера | + +--- + +## 📝 Best Practices + +### Для клиентов API + +1. **Проверяйте HTTP статусы** перед парсингом ответа +2. **Обрабатывайте 404 ошибки** — опрос может истечь +3. **Используйте exponential backoff** при 500 ошибках +4. **Кешируйте результаты** если они не меняются часто +5. **Валидируйте данные** на клиенте перед отправкой + +### Интеграция + +**JavaScript/TypeScript:** +```typescript +const API_URL = 'http://localhost:8080/api/v1'; + +async function createPoll(title: string, options: string[]) { + const response = await fetch(`${API_URL}/polls`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title, options }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Failed to create poll'); + } + + return await response.json(); +} +``` + +**Python:** +```python +import requests + +API_URL = 'http://localhost:8080/api/v1' + +def create_poll(title: str, options: list[str]) -> dict: + response = requests.post( + f'{API_URL}/polls', + json={'title': title, 'options': options} + ) + response.raise_for_status() + return response.json() +``` + +**Go:** +```go +type CreatePollRequest struct { + Title string `json:"title"` + Options []string `json:"options"` +} + +func createPoll(title string, options []string) (*CreatePollResponse, error) { + body, _ := json.Marshal(CreatePollRequest{Title: title, Options: options}) + resp, err := http.Post( + "http://localhost:8080/api/v1/polls", + "application/json", + bytes.NewBuffer(body), + ) + // ... handle response +} +``` + +--- + +## 🧪 Testing + +### Postman Collection + +Можно импортировать Swagger spec в Postman: +``` +http://localhost:8080/swagger/doc.json +``` + +### Manual Testing + +**1. Создать опрос:** +```bash +POLL_ID=$(curl -s -X POST http://localhost:8080/api/v1/polls \ + -H "Content-Type: application/json" \ + -d '{"title":"Test","options":["A","B","C"]}' \ + | jq -r '.poll_id') + +echo "Poll ID: $POLL_ID" +``` + +**2. Проголосовать:** +```bash +curl -X POST http://localhost:8080/api/v1/polls/$POLL_ID/vote \ + -H "Content-Type: application/json" \ + -d '{"option_indices":[0,2]}' +``` + +**3. Получить результаты:** +```bash +curl http://localhost:8080/api/v1/polls/$POLL_ID/results | jq +``` + +--- + +## 🔮 Planned Endpoints + +Планируется добавить: + +- `GET /api/v1/polls/{id}` — получить метаданные опроса без голосов +- `DELETE /api/v1/polls/{id}` — удалить опрос (требует auth) +- `GET /api/v1/polls` — список опросов пользователя (требует auth) +- `PATCH /api/v1/polls/{id}` — изменить настройки опроса +- `GET /api/v1/polls/{id}/export` — экспорт результатов (CSV, PDF) +- `WebSocket /api/v1/polls/{id}/live` — live обновления результатов + +--- + +## 📞 Support + +- **Swagger UI:** http://localhost:8080/swagger/index.html +- **GitHub Issues:** https://github.com/AlexeyLars/surway/issues +- **Backend Source:** `backend/internal/handler/poll.go` + +--- + +_Последнее обновление: December 2025_ diff --git a/docs/wiki/Architecture.md b/docs/wiki/Architecture.md new file mode 100644 index 0000000..8a908b6 --- /dev/null +++ b/docs/wiki/Architecture.md @@ -0,0 +1,517 @@ +# 🏗️ Архитектура Surway + +Детальное описание архитектуры проекта, паттернов проектирования и взаимодействия компонентов. + +--- + +## 📐 Общая архитектура + +Surway следует принципам **Clean Architecture** и **Domain-Driven Design**, разделяя систему на независимые слои с четкими границами. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Users / Clients │ +└────────────────────┬─────────────────┬──────────────────────────┘ + │ │ + ┌────────────▼──────────┐ │ + │ Frontend (Next.js) │ │ + │ - React Components │ │ + │ - API Client │ │ + │ - SSR/CSR │ │ + └────────────┬──────────┘ │ + │ │ + │ HTTP/REST │ HTTP/REST + │ │ + ┌────────────▼────────────────▼──────────────────┐ + │ Backend (Go + Gin) │ + │ ┌──────────────────────────────────────────┐ │ + │ │ Handler Layer (HTTP) │ │ + │ │ - Request validation │ │ + │ │ - Response formatting │ │ + │ │ - Error mapping │ │ + │ └──────────────┬───────────────────────────┘ │ + │ │ │ + │ ┌──────────────▼───────────────────────────┐ │ + │ │ Service Layer (Business Logic) │ │ + │ │ - Poll creation logic │ │ + │ │ - ID generation │ │ + │ │ - URL building │ │ + │ └──────────────┬───────────────────────────┘ │ + │ │ │ + │ ┌──────────────▼───────────────────────────┐ │ + │ │ Storage Interface │ │ + │ └──────────────┬───────────────────────────┘ │ + │ │ │ + │ ┌──────────────▼───────────────────────────┐ │ + │ │ Redis Storage Implementation │ │ + │ │ - CRUD operations │ │ + │ │ - Atomic transactions │ │ + │ └──────────────┬───────────────────────────┘ │ + └─────────────────┼───────────────────────────────┘ + │ + ┌─────────────────▼───────────────────────────────┐ + │ Redis Database │ + │ - String: poll metadata (JSON + TTL) │ + │ - Hash: vote counters │ + └─────────────────────────────────────────────────┘ +``` + +--- + +## 🎯 Backend архитектура (Go) + +### Слоистая архитектура + +#### 1. Handler Layer (`internal/handler/`) + +**Ответственность:** +- Обработка HTTP запросов +- Валидация входных данных (через Gin binding) +- Маппинг бизнес-ошибок в HTTP статусы +- Формирование JSON ответов + +**Компоненты:** +- `poll.go` — CRUD handlers для опросов +- `router.go` — настройка маршрутов и middleware + +**Middleware:** +- `LoggerMiddleware` — структурированное логирование запросов +- `CORSMiddleware` — CORS headers для frontend +- `gin.Recovery()` — восстановление после panic + +**Пример handler:** +```go +func (h *PollHandler) Vote(c *gin.Context) { + pollID := c.Param("id") + + var req model.VoteRequest + if err := c.ShouldBindJSON(&req); err != nil { + // Валидация + c.JSON(400, model.ErrorResponse{...}) + return + } + + // Делегирование в service layer + err := h.service.Vote(c.Request.Context(), pollID, &req) + + // Маппинг ошибок + if errors.Is(err, storage.ErrPollNotFound) { + c.JSON(404, ...) + return + } + + c.JSON(200, model.VoteResponse{Success: true}) +} +``` + +#### 2. Service Layer (`internal/service/`) + +**Ответственность:** +- Бизнес-логика приложения +- Генерация уникальных ID для опросов +- Построение URL для голосования/результатов +- Логирование бизнес-событий +- Валидация бизнес-правил + +**Изоляция:** +- Не знает о HTTP (использует `context.Context`) +- Работает через Storage интерфейс (DI) +- Возвращает бизнес-ошибки (не HTTP) + +**Пример:** +```go +func (s *PollService) CreatePoll(ctx context.Context, req *model.CreatePollRequest) (*model.CreatePollResponse, error) { + // Генерация короткого ID + pollID := random.NewRandomString(7) + + // Создание Poll entity + poll := &model.Poll{ + ID: pollID, + Title: req.Title, + Options: req.Options, + CreatedAt: time.Now(), + ExpiresAt: time.Now().Add(s.config.Poll.DefaultTTL), + } + + // Сохранение через интерфейс + if err := s.storage.CreatePoll(ctx, poll, s.config.Poll.DefaultTTL); err != nil { + s.logger.Error("failed to create poll", ...) + return nil, err + } + + // Построение response с URL + return &model.CreatePollResponse{ + PollID: pollID, + VoteURL: fmt.Sprintf("%s/api/v1/polls/%s/vote", baseURL, pollID), + ResultsURL: fmt.Sprintf("%s/api/v1/polls/%s/results", baseURL, pollID), + }, nil +} +``` + +#### 3. Storage Layer (`internal/storage/`) + +**Ответственность:** +- CRUD операции с данными +- Атомарные транзакции (Redis Pipeline) +- Управление TTL +- Работа с Redis структурами данных + +**Storage Interface:** +```go +type Storage interface { + CreatePoll(ctx context.Context, poll *model.Poll, ttl time.Duration) error + GetPoll(ctx context.Context, pollID string) (*model.Poll, error) + Vote(ctx context.Context, pollID string, optionIndices []int) error + GetResults(ctx context.Context, pollID string) (*model.PollResults, error) + Close() error +} +``` + +**Redis Implementation:** +- **Ключи:** + - `poll:{id}:info` — String с JSON метаданными + - `poll:{id}:votes` — Hash с счетчиками голосов +- **Атомарность:** Использование Pipeline для batch операций +- **TTL:** Синхронизирован для обоих ключей + +**Пример атомарной операции:** +```go +func (s *RedisStorage) Vote(ctx context.Context, pollID string, optionIndices []int) error { + // Валидация + проверка дубликатов + poll, err := s.GetPoll(ctx, pollID) + // ... validation logic ... + + // Атомарное увеличение счетчиков + pipe := s.client.Pipeline() + for _, idx := range optionIndices { + field := fmt.Sprintf("%d", idx) + pipe.HIncrBy(ctx, pollVotesKey(pollID), field, 1) + } + _, err = pipe.Exec(ctx) + + return err +} +``` + +#### 4. Model Layer (`internal/model/`) + +**Domain entities и DTO:** +- `Poll` — основная entity +- `CreatePollRequest/Response` — DTO для создания +- `VoteRequest/Response` — DTO для голосования +- `PollResults` — результаты с агрегированными данными +- `ErrorResponse` — стандартизированные ошибки + +**Validation tags:** +```go +type CreatePollRequest struct { + Title string `json:"title" binding:"required,min=3,max=200"` + Options []string `json:"options" binding:"required,min=2,max=10,dive,required,min=1,max=100"` +} +``` + +--- + +## 🎨 Frontend архитектура (Next.js) + +### App Router структура + +``` +app/ +├── layout.tsx # Root layout +├── page.tsx # Home page (/) +├── create/ # Создание опроса (/create) +│ └── page.tsx +├── [id]/ # Динамический роутинг (/[id]) +│ ├── page.tsx # Голосование +│ └── results/ # Результаты (/[id]/results) +│ └── page.tsx +├── config/ # Конфигурация +│ └── api.ts +└── services/ # API client + └── pollService.ts +``` + +### Архитектурные решения + +#### Server Components vs Client Components + +**Server Components (по умолчанию):** +- Рендеринг на сервере (SSR) +- SEO-friendly +- Уменьшенный bundle size +- Пример: главная страница, страница результатов + +**Client Components (`'use client'`):** +- Интерактивные компоненты +- Работа с state и effects +- Обработка событий +- Пример: формы голосования, графики + +#### API Client Service + +Централизованный сервис для работы с backend: + +```typescript +// app/services/pollService.ts +export const pollService = { + async createPoll(data: CreatePollRequest): Promise { + const response = await fetch(`${API_URL}/polls`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + return response.json(); + }, + + async vote(pollId: string, optionIndices: number[]): Promise { + // ... + }, + + async getResults(pollId: string): Promise { + // ... + } +}; +``` + +#### Конфигурация + +Использование environment variables: +- **Client-side:** `NEXT_PUBLIC_*` (доступны в браузере) +- **Server-side:** обычные env vars (только на сервере) + +```typescript +// app/config/api.ts +export const API_CONFIG = { + protocol: process.env.NEXT_PUBLIC_API_PROTOCOL || 'http', + host: process.env.NEXT_PUBLIC_API_HOST || 'localhost', + port: process.env.NEXT_PUBLIC_API_PORT || '8080', + version: process.env.NEXT_PUBLIC_API_VERSION || 'v1', +}; + +export const API_URL = `${API_CONFIG.protocol}://${API_CONFIG.host}:${API_CONFIG.port}/api/${API_CONFIG.version}`; +``` + +--- + +## 🗄️ Структура данных в Redis + +### Poll Metadata (String) + +**Ключ:** `poll:{poll_id}:info` +**Тип:** String (JSON) +**TTL:** `POLL_DEFAULT_TTL` (168h) + +**Структура:** +```json +{ + "id": "abc123", + "title": "Какие языки программирования вы используете?", + "options": ["Go", "Python", "JavaScript", "Rust", "TypeScript"], + "created_at": "2025-12-08T10:00:00Z", + "expires_at": "2025-12-15T10:00:00Z" +} +``` + +### Vote Counters (Hash) + +**Ключ:** `poll:{poll_id}:votes` +**Тип:** Hash +**TTL:** `POLL_DEFAULT_TTL` (168h, синхронизирован с info) + +**Структура:** +``` +Field Value +----- ----- +"0" -> "42" # Go: 42 голоса +"1" -> "28" # Python: 28 голосов +"2" -> "35" # JavaScript: 35 голосов +"3" -> "15" # Rust: 15 голосов +"4" -> "30" # TypeScript: 30 голосов +``` + +### Преимущества такой структуры + +1. **Атомарность** — `HINCRBY` атомарен, не нужны локи +2. **Производительность** — O(1) для чтения/записи +3. **TTL** — автоматическое удаление устаревших опросов +4. **Простота** — минимальное количество операций + +### Redis Pipeline для атомарности + +```go +// Создание опроса - атомарная операция +pipe := redis.Pipeline() +pipe.Set(ctx, pollInfoKey(id), jsonData, ttl) +pipe.HSet(ctx, pollVotesKey(id), initialVotes) +pipe.Expire(ctx, pollVotesKey(id), ttl) +_, err := pipe.Exec(ctx) + +// Голосование - атомарное увеличение нескольких счетчиков +pipe := redis.Pipeline() +for _, idx := range optionIndices { + pipe.HIncrBy(ctx, pollVotesKey(id), fmt.Sprintf("%d", idx), 1) +} +_, err := pipe.Exec(ctx) +``` + +--- + +## 🔄 Поток данных + +### Создание опроса + +``` +User → Frontend Form + ↓ + POST /api/v1/polls { title, options } + ↓ +Handler.CreatePoll + ↓ validate request +Service.CreatePoll + ↓ generate ID, build Poll entity +Storage.CreatePoll + ↓ Redis Pipeline: + 1. SET poll:{id}:info {json} EX 168h + 2. HSET poll:{id}:votes 0 0 1 0 2 0 ... + 3. EXPIRE poll:{id}:votes 168h + ↓ +Response { poll_id, vote_url, results_url } + ↓ +Frontend → Redirect to /[id] +``` + +### Голосование + +``` +User → Frontend Vote Form + ↓ + POST /api/v1/polls/{id}/vote { option_indices: [0, 2] } + ↓ +Handler.Vote + ↓ validate request, extract id +Service.Vote + ↓ business logic +Storage.Vote + ↓ 1. Check poll exists (EXISTS poll:{id}:info) + ↓ 2. Get poll for validation + ↓ 3. Validate indices and check duplicates + ↓ 4. Redis Pipeline: + HINCRBY poll:{id}:votes "0" 1 + HINCRBY poll:{id}:votes "2" 1 + ↓ +Response { success: true, message: "Votes registered successfully (2 options)" } + ↓ +Frontend → Show success, redirect to results +``` + +### Получение результатов + +``` +User → Frontend Results Page + ↓ + GET /api/v1/polls/{id}/results + ↓ +Handler.GetResults + ↓ extract id +Service.GetResults + ↓ +Storage.GetResults + ↓ 1. GET poll:{id}:info → parse Poll + ↓ 2. HGETALL poll:{id}:votes → get all counters + ↓ 3. Build PollResults { poll, votes map, total } + ↓ +Response { + poll: { id, title, options, ... }, + votes: { "Go": 42, "Python": 28, ... }, + total: 150 +} + ↓ +Frontend → Render charts with Recharts +``` + +--- + +## 🔐 Безопасность и надежность + +### Обработка ошибок + +**Уровни обработки:** +1. **Storage** — возвращает типизированные ошибки (`ErrPollNotFound`, `ErrInvalidOption`) +2. **Service** — логирует и пробрасывает ошибки выше +3. **Handler** — мапит ошибки в HTTP статусы (404, 400, 500) + +### Graceful Shutdown + +```go +// Listening for SIGINT/SIGTERM +quit := make(chan os.Signal, 1) +signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) +<-quit + +// Graceful shutdown с таймаутом +ctx, cancel := context.WithTimeout(context.Background(), cfg.Server.ShutdownTimeout) +defer cancel() + +server.Shutdown(ctx) +storage.Close() +``` + +### Валидация + +**Уровень 1: Gin binding tags** +```go +type CreatePollRequest struct { + Title string `binding:"required,min=3,max=200"` + Options []string `binding:"required,min=2,max=10,dive,required,min=1,max=100"` +} +``` + +**Уровень 2: Business validation** +```go +// Проверка дубликатов индексов +seen := make(map[int]bool) +for _, idx := range optionIndices { + if seen[idx] { + return ErrDuplicateOption + } + seen[idx] = true +} +``` + +### Middleware + +- **Recovery** — отлов panic и возврат 500 +- **Logger** — структурированное логирование всех запросов +- **CORS** — правильные headers для frontend + +--- + +## 📊 Масштабирование + +### Текущая архитектура + +- **Stateless backend** — можно горизонтально масштабировать +- **Redis** — single instance (для начала) +- **Frontend** — SSR/SSG через Next.js + +### Рекомендации для масштабирования + +1. **Redis Cluster** — для высоконагруженных систем +2. **Load Balancer** — перед backend instances (Caddy, nginx, HAProxy) +3. **Redis Sentinel** — для high availability +4. **Кеширование** — CDN для frontend assets +5. **Метрики** — Prometheus для мониторинга + +--- + +## 🔍 Дальнейшее чтение + +- [API Documentation](API-Documentation) — детальное описание endpoints +- [Data Structure](Data-Structure) — подробнее о Redis схеме +- [Development Guide](Development-Guide) — как разрабатывать новые фичи +- [Testing](Testing) — тестирование архитектуры + +--- + +_Последнее обновление: December 2025_ diff --git a/docs/wiki/Configuration.md b/docs/wiki/Configuration.md new file mode 100644 index 0000000..b60fbd4 --- /dev/null +++ b/docs/wiki/Configuration.md @@ -0,0 +1,627 @@ +# ⚙️ Configuration Guide + +Полное руководство по конфигурации Surway проекта. + +--- + +## 📋 Оглавление + +1. [Backend конфигурация](#backend-конфигурация) +2. [Frontend конфигурация](#frontend-конфигурация) +3. [Redis конфигурация](#redis-конфигурация) +4. [Docker конфигурация](#docker-конфигурация) +5. [Production настройки](#production-настройки) +6. [Environment-specific](#environment-specific) + +--- + +## 🔧 Backend конфигурация + +### Environment Variables + +Backend использует переменные окружения для конфигурации. Загрузка через [cleanenv](https://github.com/ilyakaznacheev/cleanenv). + +**Файл:** `backend/internal/config/config.go` + +### Server Configuration + +| Переменная | Тип | По умолчанию | Описание | +|-----------|-----|--------------|----------| +| `SERVER_HOST` | string | `0.0.0.0` | Хост сервера | +| `SERVER_PORT` | int | `8080` | Порт сервера | +| `SERVER_READ_TIMEOUT` | duration | `10s` | Таймаут чтения запроса | +| `SERVER_WRITE_TIMEOUT` | duration | `10s` | Таймаут записи ответа | +| `SERVER_SHUTDOWN_TIMEOUT` | duration | `5s` | Таймаут graceful shutdown | +| `BASE_URL` | string | `http://localhost:8080` | Базовый URL для генерации ссылок | + +**Примеры:** + +```env +# Development +SERVER_HOST=127.0.0.1 +SERVER_PORT=8080 +SERVER_READ_TIMEOUT=10s +SERVER_WRITE_TIMEOUT=10s +BASE_URL=http://localhost:8080 + +# Production +SERVER_HOST=0.0.0.0 +SERVER_PORT=8080 +SERVER_READ_TIMEOUT=30s +SERVER_WRITE_TIMEOUT=30s +BASE_URL=https://your-domain.com/api +``` + +### Redis Configuration + +| Переменная | Тип | По умолчанию | Описание | +|-----------|-----|--------------|----------| +| `REDIS_HOST` | string | `localhost` | Хост Redis | +| `REDIS_PORT` | int | `6379` | Порт Redis | +| `REDIS_PASSWORD` | string | _(пусто)_ | Пароль Redis | +| `REDIS_DB` | int | `0` | Номер базы данных (0-15) | + +**Примеры:** + +```env +# Local development +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= +REDIS_DB=0 + +# Docker +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD=your_secure_password +REDIS_DB=0 + +# Remote Redis +REDIS_HOST=redis.example.com +REDIS_PORT=6379 +REDIS_PASSWORD=very_secure_password_here +REDIS_DB=1 +``` + +### Poll Configuration + +| Переменная | Тип | По умолчанию | Описание | +|-----------|-----|--------------|----------| +| `POLL_DEFAULT_TTL` | duration | `168h` (7 дней) | TTL опроса по умолчанию | +| `POLL_MAX_TTL` | duration | `720h` (30 дней) | Максимальный TTL опроса | + +**Duration format:** +- `h` — часы (hours) +- `m` — минуты (minutes) +- `s` — секунды (seconds) + +**Примеры:** +```env +POLL_DEFAULT_TTL=168h # 7 дней +POLL_DEFAULT_TTL=24h # 1 день +POLL_DEFAULT_TTL=30m # 30 минут +POLL_DEFAULT_TTL=3600s # 1 час + +POLL_MAX_TTL=720h # 30 дней +``` + +### Environment Mode + +| Переменная | Тип | По умолчанию | Описание | +|-----------|-----|--------------|----------| +| `ENV` | string | `dev` | Окружение: `dev`, `prod` | + +**Эффект:** +- `dev` — Gin в debug mode, подробные логи +- `prod` — Gin в release mode, оптимизация + +```env +ENV=dev # Development +ENV=prod # Production +``` + +### Полный пример .env для backend + +```env +# Environment +ENV=dev + +# Server +SERVER_HOST=0.0.0.0 +SERVER_PORT=8080 +SERVER_READ_TIMEOUT=10s +SERVER_WRITE_TIMEOUT=10s +SERVER_SHUTDOWN_TIMEOUT=5s +BASE_URL=http://localhost:8080 + +# Redis +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= +REDIS_DB=0 + +# Polls +POLL_DEFAULT_TTL=168h +POLL_MAX_TTL=720h +``` + +--- + +## 🎨 Frontend конфигурация + +### Environment Variables + +Frontend использует Next.js environment variables. + +**Важно:** +- `NEXT_PUBLIC_*` — доступны в браузере (client-side) +- Остальные — только на сервере (server-side) + +### Client-side Configuration + +| Переменная | Тип | По умолчанию | Описание | +|-----------|-----|--------------|----------| +| `NEXT_PUBLIC_API_PROTOCOL` | string | `http` | Протокол API (http/https) | +| `NEXT_PUBLIC_API_HOST` | string | `localhost` | Хост API | +| `NEXT_PUBLIC_API_PORT` | string | `8080` | Порт API | +| `NEXT_PUBLIC_API_VERSION` | string | `v1` | Версия API | +| `NEXT_PUBLIC_FRONTEND_PROTOCOL` | string | `http` | Протокол frontend | +| `NEXT_PUBLIC_FRONTEND_HOST` | string | `localhost` | Хост frontend | +| `NEXT_PUBLIC_FRONTEND_PORT` | string | `3000` | Порт frontend | + +**Использование:** + +```typescript +// app/config/api.ts +export const API_CONFIG = { + protocol: process.env.NEXT_PUBLIC_API_PROTOCOL || 'http', + host: process.env.NEXT_PUBLIC_API_HOST || 'localhost', + port: process.env.NEXT_PUBLIC_API_PORT || '8080', + version: process.env.NEXT_PUBLIC_API_VERSION || 'v1', +}; + +export const API_URL = `${API_CONFIG.protocol}://${API_CONFIG.host}:${API_CONFIG.port}/api/${API_CONFIG.version}`; +``` + +### Server-side Configuration (SSR) + +| Переменная | Тип | По умолчанию | Описание | +|-----------|-----|--------------|----------| +| `API_INTERNAL_PROTOCOL` | string | `http` | Протокол для SSR запросов | +| `API_INTERNAL_HOST` | string | `backend` | Хост backend (внутри Docker) | +| `API_INTERNAL_PORT` | string | `8080` | Порт backend | + +**Зачем нужно:** +При SSR (Server-Side Rendering) Next.js запрашивает данные с сервера. Внутри Docker сети нужно использовать `backend:8080`, а не `localhost:8080`. + +### Next.js Configuration + +| Переменная | Тип | По умолчанию | Описание | +|-----------|-----|--------------|----------| +| `NODE_ENV` | string | `development` | Окружение Node.js | +| `HOSTNAME` | string | `0.0.0.0` | Хост для Next.js сервера | +| `PORT` | string | `3000` | Порт Next.js сервера | +| `NEXT_TELEMETRY_DISABLED` | string | `1` | Отключить телеметрию | + +### Полный пример .env.local для frontend + +```env +# API Configuration (client-side) +NEXT_PUBLIC_API_PROTOCOL=http +NEXT_PUBLIC_API_HOST=localhost +NEXT_PUBLIC_API_PORT=8080 +NEXT_PUBLIC_API_VERSION=v1 + +# Frontend URLs (client-side) +NEXT_PUBLIC_FRONTEND_PROTOCOL=http +NEXT_PUBLIC_FRONTEND_HOST=localhost +NEXT_PUBLIC_FRONTEND_PORT=3000 + +# SSR Configuration (server-side) +API_INTERNAL_PROTOCOL=http +API_INTERNAL_HOST=localhost +API_INTERNAL_PORT=8080 + +# Next.js +NODE_ENV=development +HOSTNAME=0.0.0.0 +PORT=3000 +NEXT_TELEMETRY_DISABLED=1 +``` + +### Docker Build Args + +Для production builds некоторые переменные передаются как build arguments: + +```dockerfile +# frontend/Dockerfile +ARG NEXT_PUBLIC_API_PROTOCOL +ARG NEXT_PUBLIC_API_HOST +ARG NEXT_PUBLIC_API_PORT +ARG NEXT_PUBLIC_API_VERSION +``` + +**В docker-compose.prod.yml:** + +```yaml +frontend: + build: + context: ./frontend + args: + - NEXT_PUBLIC_API_PROTOCOL=https + - NEXT_PUBLIC_API_HOST=your-domain.com + - NEXT_PUBLIC_API_PORT=443 + - NEXT_PUBLIC_API_VERSION=v1 +``` + +--- + +## 🗄️ Redis конфигурация + +### Production Redis Config + +Файл: `configs/redis.conf` + +**Основные настройки:** + +```conf +# ==================== +# PERSISTENCE +# ==================== + +# RDB снэпшоты +save 900 1 # Сохранить если хотя бы 1 изменение за 15 минут +save 300 10 # Сохранить если хотя бы 10 изменений за 5 минут +save 60 10000 # Сохранить если хотя бы 10000 изменений за 1 минуту + +# AOF (Append Only File) +appendonly yes +appendfsync everysec # Синхронизация каждую секунду (компромисс) + +# ==================== +# MEMORY MANAGEMENT +# ==================== + +maxmemory 256mb +maxmemory-policy allkeys-lru # Удалять least recently used ключи + +# ==================== +# SECURITY +# ==================== + +requirepass your_strong_password_here +protected-mode yes + +# ==================== +# NETWORKING +# ==================== + +bind 0.0.0.0 # Слушать на всех интерфейсах (в Docker) +port 6379 +tcp-backlog 511 +timeout 0 +tcp-keepalive 300 + +# ==================== +# LOGGING +# ==================== + +loglevel notice +logfile "" # Stdout (для Docker logs) + +# ==================== +# SNAPSHOTTING +# ==================== + +stop-writes-on-bgsave-error yes +rdbcompression yes +rdbchecksum yes +dbfilename dump.rdb +dir /data +``` + +### Memory Policies + +| Policy | Описание | +|--------|----------| +| `noeviction` | Не удалять ключи, возвращать ошибку | +| `allkeys-lru` | Удалять LRU ключи из всех ключей | +| `volatile-lru` | Удалять LRU ключи только с TTL | +| `allkeys-random` | Удалять случайные ключи | +| `volatile-random` | Удалять случайные ключи с TTL | +| `volatile-ttl` | Удалять ключи с наименьшим TTL | + +**Рекомендация для Surway:** `allkeys-lru` (все опросы имеют TTL, но LRU более предсказуем) + +### Appendfsync Modes + +| Mode | Описание | Производительность | Надежность | +|------|----------|-------------------|-----------| +| `always` | Синхронизация каждую операцию | Низкая | Максимальная | +| `everysec` | Синхронизация каждую секунду | Средняя | Хорошая | +| `no` | ОС решает когда синхронизировать | Высокая | Низкая | + +**Рекомендация:** `everysec` (хороший баланс) + +--- + +## 🐳 Docker конфигурация + +### Development (docker-compose.yml) + +```yaml +services: + redis: + image: redis:7-alpine + ports: + - "6379:6379" # Доступен извне для debug + command: redis-server --appendonly yes --appendfsync everysec + + backend: + build: ./backend + ports: + - "8080:8080" + environment: + - REDIS_HOST=redis # Внутри Docker сети + - REDIS_PORT=6379 + depends_on: + redis: + condition: service_healthy + + frontend: + build: ./frontend + ports: + - "3000:3000" + environment: + - NEXT_PUBLIC_API_HOST=localhost # Доступ из браузера + - API_INTERNAL_HOST=backend # Доступ с SSR +``` + +### Production (docker-compose.prod.yml) + +```yaml +services: + redis: + image: redis:7-alpine + volumes: + - redis-data:/data + - ./configs/redis.conf:/usr/local/etc/redis/redis.conf + command: redis-server /usr/local/etc/redis/redis.conf + # НЕТ expose портов наружу! + + backend: + expose: + - "8080" # Только внутри Docker сети + # НЕТ ports! + + frontend: + expose: + - "3000" + # НЕТ ports! + + caddy: + ports: + - "80:80" + - "443:443" # Только Caddy доступен извне +``` + +--- + +## 🚀 Production настройки + +### Backend Production + +```env +ENV=prod +SERVER_HOST=0.0.0.0 +SERVER_PORT=8080 +SERVER_READ_TIMEOUT=30s +SERVER_WRITE_TIMEOUT=30s +SERVER_SHUTDOWN_TIMEOUT=10s +BASE_URL=https://your-domain.com/api + +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD=very_strong_password_min_32_chars_recommended +REDIS_DB=0 + +POLL_DEFAULT_TTL=168h +POLL_MAX_TTL=720h +``` + +### Frontend Production + +```env +# Build args (в docker-compose.prod.yml) +NEXT_PUBLIC_API_PROTOCOL=https +NEXT_PUBLIC_API_HOST=your-domain.com +NEXT_PUBLIC_API_PORT=443 +NEXT_PUBLIC_API_VERSION=v1 + +# Runtime +NODE_ENV=production +API_INTERNAL_PROTOCOL=http +API_INTERNAL_HOST=backend +API_INTERNAL_PORT=8080 +NEXT_TELEMETRY_DISABLED=1 +``` + +### Redis Production + +```conf +maxmemory 512mb +maxmemory-policy allkeys-lru +requirepass very_strong_password_min_32_chars +appendonly yes +appendfsync everysec +save 900 1 +save 300 10 +save 60 10000 +``` + +--- + +## 🌍 Environment-specific + +### Development + +**Цели:** +- Быстрая разработка +- Подробные логи +- Hot reload +- Доступ ко всем портам для debug + +**Backend .env:** +```env +ENV=dev +SERVER_HOST=127.0.0.1 +SERVER_PORT=8080 +BASE_URL=http://localhost:8080 +REDIS_HOST=localhost +REDIS_PASSWORD= +``` + +**Frontend .env.local:** +```env +NEXT_PUBLIC_API_HOST=localhost +NEXT_PUBLIC_API_PORT=8080 +``` + +### Staging + +**Цели:** +- Максимально близко к production +- Тестирование перед релизом +- Доступ для QA команды + +**Настройки:** почти как production, но может быть: +- Меньше ресурсов (RAM, CPU) +- Тестовые домены (staging.example.com) +- Отключен rate limiting для удобства тестов + +### Production + +**Цели:** +- Максимальная производительность +- Безопасность +- Надежность +- Мониторинг + +**Обязательно:** +- [ ] Сильные пароли Redis +- [ ] HTTPS only +- [ ] Graceful shutdown +- [ ] Health checks +- [ ] Логирование +- [ ] Backup Redis данных +- [ ] Мониторинг метрик + +--- + +## 🔐 Secrets Management + +### Не коммитьте секреты! + +**Добавьте в .gitignore:** +``` +.env +.env.local +.env.production +*.secret +``` + +### Для production + +Используйте: +- **Docker Secrets** — для Docker Swarm +- **Kubernetes Secrets** — для K8s +- **HashiCorp Vault** — enterprise solution +- **AWS Secrets Manager** — для AWS +- **Environment variables** — через CI/CD + +**Пример Docker Secrets:** + +```yaml +services: + backend: + secrets: + - redis_password + environment: + - REDIS_PASSWORD_FILE=/run/secrets/redis_password + +secrets: + redis_password: + file: ./secrets/redis_password.txt +``` + +--- + +## 🧪 Testing Configurations + +### Test Environment + +```env +ENV=test +SERVER_PORT=8081 +REDIS_HOST=localhost +REDIS_PORT=6380 +REDIS_DB=15 # Отдельная DB для тестов +POLL_DEFAULT_TTL=1m # Короткий TTL для тестов +``` + +--- + +## 📞 Troubleshooting + +### Backend не подключается к Redis + +```bash +# Проверьте переменные +echo $REDIS_HOST +echo $REDIS_PORT + +# Проверьте доступность Redis +redis-cli -h $REDIS_HOST -p $REDIS_PORT ping + +# С паролем +redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASSWORD ping +``` + +### Frontend не может достучаться до API + +```bash +# Проверьте URL +echo $NEXT_PUBLIC_API_HOST +echo $NEXT_PUBLIC_API_PORT + +# Тест из браузера (DevTools Console) +fetch('http://localhost:8080/health') + .then(r => r.json()) + .then(console.log) +``` + +### Docker Compose переменные не работают + +```bash +# Проверьте что .env файл в корне проекта +ls -la .env + +# Проверьте синтаксис +cat .env + +# Используйте явный --env-file +docker compose --env-file .env up +``` + +--- + +## 📚 Дополнительное чтение + +- [cleanenv Documentation](https://github.com/ilyakaznacheev/cleanenv) +- [Next.js Environment Variables](https://nextjs.org/docs/basic-features/environment-variables) +- [Redis Configuration](https://redis.io/topics/config) +- [Docker Compose Environment](https://docs.docker.com/compose/environment-variables/) + +--- + +_Последнее обновление: December 2025_ diff --git a/docs/wiki/Deployment.md b/docs/wiki/Deployment.md new file mode 100644 index 0000000..f771f43 --- /dev/null +++ b/docs/wiki/Deployment.md @@ -0,0 +1,637 @@ +# 🚀 Deployment Guide + +Полное руководство по деплою Surway в production окружение. + +--- + +## 📋 Оглавление + +1. [Требования](#требования) +2. [Docker Compose Production](#docker-compose-production) +3. [Caddy Setup (SSL)](#caddy-setup) +4. [Environment Variables](#environment-variables) +5. [Database (Redis)](#redis-configuration) +6. [Мониторинг](#мониторинг) +7. [Troubleshooting](#troubleshooting) + +--- + +## 🎯 Требования + +### Системные требования + +**Минимальные:** +- CPU: 1 vCore +- RAM: 1 GB +- Disk: 10 GB SSD +- OS: Linux (Ubuntu 22.04 LTS рекомендуется) + +**Рекомендуемые:** +- CPU: 2+ vCores +- RAM: 2+ GB +- Disk: 20+ GB SSD +- OS: Ubuntu 22.04 LTS + +### Софт + +- Docker 20.10+ +- Docker Compose v2+ +- (Опционально) Make для использования Makefile + +### Сеть + +- Открытые порты: 80 (HTTP), 443 (HTTPS) +- Домен с A-записью, указывающей на ваш сервер + +--- + +## 🐳 Docker Compose Production + +### 1. Клонирование репозитория + +```bash +# SSH +git clone git@github.com:AlexeyLars/surway.git + +# HTTPS +git clone https://github.com/AlexeyLars/surway.git + +cd surway +git checkout main # или develop для latest +``` + +### 2. Настройка переменных окружения + +Создайте `.env` файл в корне проекта: + +```bash +nano .env +``` + +**Минимальная конфигурация:** +```env +# Domain +DOMAIN=your-domain.com + +# Environment +ENV=prod + +# Backend +SERVER_HOST=0.0.0.0 +SERVER_PORT=8080 +BASE_URL=https://your-domain.com/api + +# Redis +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD=your_strong_redis_password_here +REDIS_DB=0 + +# Polls +POLL_DEFAULT_TTL=168h +POLL_MAX_TTL=720h +``` + +### 3. Настройка Caddyfile + +Отредактируйте `Caddyfile`: + +```bash +nano Caddyfile +``` + +**Пример конфигурации:** +``` +your-domain.com { + # Логирование + log { + output file /var/log/caddy/access.log + format json + } + + # Frontend (Next.js) + reverse_proxy frontend:3000 + + # Backend API + handle /api/* { + reverse_proxy backend:8080 + } + + # Swagger (опционально отключить в production) + handle /swagger/* { + reverse_proxy backend:8080 + } + + # Health check + handle /health { + reverse_proxy backend:8080 + } + + # Security headers + header { + # Enable HSTS + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" + # Prevent clickjacking + X-Frame-Options "SAMEORIGIN" + # XSS protection + X-Content-Type-Options "nosniff" + X-XSS-Protection "1; mode=block" + # CSP (настройте под свои нужды) + Content-Security-Policy "default-src 'self'" + } + + # Gzip compression + encode gzip + + # SSL автоматически через Let's Encrypt +} + +# Redirect www to non-www (опционально) +www.your-domain.com { + redir https://your-domain.com{uri} permanent +} +``` + +### 4. Настройка docker-compose.prod.yml + +Отредактируйте `docker-compose.prod.yml`: + +```yaml +services: + redis: + image: redis:7-alpine + container_name: poll-redis + volumes: + - redis-data:/data + - ./configs/redis.conf:/usr/local/etc/redis/redis.conf + command: redis-server /usr/local/etc/redis/redis.conf --requirepass ${REDIS_PASSWORD} + healthcheck: + test: ["CMD", "redis-cli", "--pass", "${REDIS_PASSWORD}", "ping"] + interval: 10s + timeout: 3s + retries: 3 + networks: + - app-net + restart: always + + backend: + build: + context: ./backend + dockerfile: Dockerfile + expose: + - "8080" + environment: + - ENV=${ENV} + - SERVER_HOST=${SERVER_HOST} + - SERVER_PORT=${SERVER_PORT} + - BASE_URL=${BASE_URL} + - REDIS_HOST=${REDIS_HOST} + - REDIS_PORT=${REDIS_PORT} + - REDIS_PASSWORD=${REDIS_PASSWORD} + - REDIS_DB=${REDIS_DB} + - POLL_DEFAULT_TTL=${POLL_DEFAULT_TTL} + depends_on: + redis: + condition: service_healthy + networks: + - app-net + restart: always + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + args: + - NEXT_PUBLIC_API_PROTOCOL=https + - NEXT_PUBLIC_API_HOST=${DOMAIN} + - NEXT_PUBLIC_API_PORT=443 + - NEXT_PUBLIC_API_VERSION=v1 + expose: + - "3000" + environment: + - NODE_ENV=production + - HOSTNAME=0.0.0.0 + - API_INTERNAL_PROTOCOL=http + - API_INTERNAL_HOST=backend + - API_INTERNAL_PORT=8080 + depends_on: + backend: + condition: service_started + networks: + - app-net + restart: always + + caddy: + image: caddy:alpine + container_name: caddy-ingress + restart: always + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile + - caddy_data:/data + - caddy_config:/config + - caddy_logs:/var/log/caddy + networks: + - app-net + depends_on: + - frontend + - backend + +volumes: + redis-data: + caddy_data: + caddy_config: + caddy_logs: + +networks: + app-net: + driver: bridge +``` + +### 5. Запуск + +```bash +# Сборка образов +docker compose -f docker-compose.prod.yml build + +# Запуск в фоновом режиме +docker compose -f docker-compose.prod.yml up -d + +# Проверка логов +docker compose -f docker-compose.prod.yml logs -f +``` + +### 6. Проверка + +```bash +# Health check +curl https://your-domain.com/health + +# Проверка API +curl https://your-domain.com/api/v1/polls \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"title":"Test","options":["A","B"]}' +``` + +--- + +## 🔒 Caddy Setup (SSL) + +### Автоматический SSL от Let's Encrypt + +Caddy автоматически получает и обновляет SSL сертификаты! + +**Требования:** +1. Домен должен резолвиться в IP вашего сервера +2. Порты 80 и 443 должны быть открыты +3. Caddy должен иметь права на запись в `/data` volume + +### Проверка SSL + +```bash +# Проверка сертификата +openssl s_client -connect your-domain.com:443 -servername your-domain.com + +# Проверка HTTPS +curl -I https://your-domain.com +``` + +### Custom SSL сертификаты + +Если используете свои сертификаты: + +``` +your-domain.com { + tls /path/to/cert.pem /path/to/key.pem + # ... остальная конфигурация +} +``` + +--- + +## ⚙️ Environment Variables + +### Backend + +| Переменная | Обязательна | Пример | Описание | +|-----------|-------------|--------|----------| +| `ENV` | Да | `prod` | Окружение (dev/prod) | +| `SERVER_HOST` | Да | `0.0.0.0` | Хост сервера | +| `SERVER_PORT` | Да | `8080` | Порт сервера | +| `BASE_URL` | Да | `https://domain.com/api` | Базовый URL для ссылок | +| `REDIS_HOST` | Да | `redis` | Хост Redis | +| `REDIS_PORT` | Да | `6379` | Порт Redis | +| `REDIS_PASSWORD` | Нет | `secret` | Пароль Redis | +| `REDIS_DB` | Нет | `0` | Номер БД Redis | +| `POLL_DEFAULT_TTL` | Нет | `168h` | TTL опроса по умолчанию | +| `POLL_MAX_TTL` | Нет | `720h` | Максимальный TTL | + +### Frontend + +**Build-time (ARG):** +- `NEXT_PUBLIC_API_PROTOCOL` — https +- `NEXT_PUBLIC_API_HOST` — your-domain.com +- `NEXT_PUBLIC_API_PORT` — 443 +- `NEXT_PUBLIC_API_VERSION` — v1 + +**Runtime:** +- `NODE_ENV` — production +- `API_INTERNAL_PROTOCOL` — http +- `API_INTERNAL_HOST` — backend +- `API_INTERNAL_PORT` — 8080 + +--- + +## 🗄️ Redis Configuration + +### Production настройки + +Создайте `configs/redis.conf`: + +```conf +# Persistence +save 900 1 +save 300 10 +save 60 10000 +appendonly yes +appendfsync everysec + +# Memory management +maxmemory 256mb +maxmemory-policy allkeys-lru + +# Security +requirepass your_strong_redis_password_here +protected-mode yes + +# Performance +tcp-backlog 511 +timeout 0 +tcp-keepalive 300 + +# Logging +loglevel notice +logfile "" + +# Снэпшоты +stop-writes-on-bgsave-error yes +rdbcompression yes +rdbchecksum yes +dbfilename dump.rdb +dir /data +``` + +### Backup Redis данных + +```bash +# Создание backup +docker exec poll-redis redis-cli --pass your_password BGSAVE + +# Копирование dump.rdb +docker cp poll-redis:/data/dump.rdb ./backup-$(date +%Y%m%d).rdb + +# Восстановление +docker cp backup-20251208.rdb poll-redis:/data/dump.rdb +docker restart poll-redis +``` + +--- + +## 📊 Мониторинг + +### Логи + +**Просмотр логов всех сервисов:** +```bash +docker compose -f docker-compose.prod.yml logs -f +``` + +**Логи отдельного сервиса:** +```bash +docker compose -f docker-compose.prod.yml logs -f backend +docker compose -f docker-compose.prod.yml logs -f frontend +docker compose -f docker-compose.prod.yml logs -f redis +docker compose -f docker-compose.prod.yml logs -f caddy +``` + +**Caddy access logs:** +```bash +docker exec caddy-ingress tail -f /var/log/caddy/access.log +``` + +### Health Checks + +```bash +# Backend health +curl https://your-domain.com/health + +# Redis health +docker exec poll-redis redis-cli --pass your_password ping + +# Проверка всех контейнеров +docker ps +``` + +### Метрики (планируется) + +В планах интеграция с: +- **Prometheus** — сбор метрик +- **Grafana** — визуализация +- **AlertManager** — алерты + +--- + +## 🔧 Обновление + +### Rolling update + +```bash +# 1. Pull latest code +cd /path/to/surway +git pull origin main + +# 2. Rebuild images +docker compose -f docker-compose.prod.yml build + +# 3. Recreate containers +docker compose -f docker-compose.prod.yml up -d + +# 4. Проверка логов +docker compose -f docker-compose.prod.yml logs -f +``` + +### Zero-downtime deployment + +Для zero-downtime нужно: +1. Использовать load balancer +2. Запускать несколько инстансов backend +3. Обновлять поочередно + +**Пример с 2 backend инстансами:** +```yaml +backend: + # ... конфигурация + deploy: + replicas: 2 +``` + +--- + +## 🔥 Troubleshooting + +### Backend не запускается + +**Проблема:** `failed to connect to redis` + +**Решение:** +```bash +# Проверьте Redis +docker compose -f docker-compose.prod.yml logs redis + +# Проверьте пароль +docker exec poll-redis redis-cli --pass your_password ping + +# Проверьте сеть +docker network inspect surway_app-net +``` + +### Frontend не загружается + +**Проблема:** 502 Bad Gateway + +**Решение:** +```bash +# Проверьте логи frontend +docker compose -f docker-compose.prod.yml logs frontend + +# Перезапустите frontend +docker compose -f docker-compose.prod.yml restart frontend +``` + +### SSL не работает + +**Проблема:** Caddy не может получить сертификат + +**Причины:** +1. Домен не резолвится в IP сервера +2. Порты 80/443 закрыты +3. Firewall блокирует запросы + +**Решение:** +```bash +# Проверьте DNS +dig your-domain.com + +# Проверьте порты +sudo netstat -tulpn | grep -E ':(80|443)' + +# Проверьте логи Caddy +docker compose -f docker-compose.prod.yml logs caddy +``` + +### Redis переполнен + +**Проблема:** `OOM command not allowed` + +**Решение:** +```bash +# Увеличьте maxmemory в redis.conf +maxmemory 512mb + +# Или используйте eviction policy +maxmemory-policy allkeys-lru + +# Перезапустите Redis +docker compose -f docker-compose.prod.yml restart redis +``` + +### Низкая производительность + +**Диагностика:** +```bash +# CPU и память контейнеров +docker stats + +# Redis статистика +docker exec poll-redis redis-cli --pass your_password INFO stats + +# Количество ключей +docker exec poll-redis redis-cli --pass your_password DBSIZE +``` + +--- + +## 🔐 Security Checklist + +- [ ] Сильный пароль для Redis +- [ ] Firewall настроен (открыты только 80, 443, 22) +- [ ] SSH доступ только по ключам +- [ ] Регулярные обновления системы +- [ ] Backup Redis данных +- [ ] Мониторинг логов +- [ ] Rate limiting (в планах) +- [ ] HTTPS для всех запросов +- [ ] Security headers в Caddy +- [ ] Отключен Swagger в production (опционально) + +--- + +## 📦 Backup Strategy + +### Автоматический backup + +Создайте cron job: + +```bash +# Откройте crontab +crontab -e + +# Добавьте строку (backup каждый день в 3 AM) +0 3 * * * /usr/bin/docker exec poll-redis redis-cli --pass your_password BGSAVE && /usr/bin/docker cp poll-redis:/data/dump.rdb /backups/redis-$(date +\%Y\%m\%d).rdb +``` + +### Хранение backups + +Рекомендуется: +1. Локальное хранение (7 дней) +2. Offsite backup (S3, Google Cloud Storage) +3. Регулярная проверка восстановления + +--- + +## 🌐 CDN и масштабирование + +### Использование CDN + +Для статических файлов frontend: +- Cloudflare +- AWS CloudFront +- Fastly + +### Horizontal scaling + +**Backend:** +```yaml +backend: + deploy: + replicas: 3 +``` + +**Redis:** +- Redis Sentinel для HA +- Redis Cluster для шардинга + +--- + +## 📞 Support + +- **Issues:** https://github.com/AlexeyLars/surway/issues +- **Wiki:** https://github.com/AlexeyLars/surway/wiki +- **Author:** [@AlexeyLars](https://github.com/AlexeyLars) + +--- + +_Последнее обновление: December 2025_ diff --git a/docs/wiki/Development-Guide.md b/docs/wiki/Development-Guide.md new file mode 100644 index 0000000..79d5dec --- /dev/null +++ b/docs/wiki/Development-Guide.md @@ -0,0 +1,725 @@ +# 💻 Development Guide + +Полное руководство по разработке и контрибьюции в проект Surway. + +--- + +## 📋 Содержание + +1. [Начало работы](#начало-работы) +2. [Структура проекта](#структура-проекта) +3. [Backend разработка](#backend-разработка) +4. [Frontend разработка](#frontend-разработка) +5. [Тестирование](#тестирование) +6. [Code Style](#code-style) +7. [Git Workflow](#git-workflow) +8. [Contributing](#contributing) + +--- + +## 🚀 Начало работы + +### Prerequisites + +- Git +- Go 1.24.2+ +- Node.js 20+ +- Redis 7+ (или Docker) +- Make (опционально) + +### Клонирование и setup + +```bash +# 1. Fork репозитория на GitHub + +# 2. Клонирование вашего fork +git clone git@github.com:YOUR_USERNAME/surway.git +cd surway + +# 3. Добавление upstream remote +git remote add upstream git@github.com:AlexeyLars/surway.git + +# 4. Создание ветки для разработки +git checkout -b feature/my-awesome-feature develop +``` + +### Локальный запуск + +**Вариант 1: Docker Compose (рекомендуется)** +```bash +make docker-up +# или +docker compose up -d +``` + +**Вариант 2: Локально** + +*Backend:* +```bash +# Запустите Redis +docker run -d -p 6379:6379 redis:7-alpine + +# Установите зависимости +cd backend +go mod download + +# Запустите сервер +go run cmd/api/main.go +``` + +*Frontend:* +```bash +cd frontend +npm install +cp env.example .env.local +npm run dev +``` + +### Проверка + +- Backend: http://localhost:8080/health +- Frontend: http://localhost:3000 +- Swagger: http://localhost:8080/swagger/index.html + +--- + +## 📁 Структура проекта + +``` +surway/ +├── backend/ # Go backend +│ ├── cmd/ +│ │ └── api/ +│ │ └── main.go # Entry point +│ ├── internal/ # Приватный код +│ │ ├── handler/ # HTTP handlers +│ │ ├── service/ # Бизнес-логика +│ │ ├── storage/ # Работа с БД +│ │ ├── model/ # Модели данных +│ │ ├── config/ # Конфигурация +│ │ └── lib/ # Утилиты +│ ├── docs/ # Swagger docs (auto-generated) +│ ├── Dockerfile +│ ├── go.mod +│ └── go.sum +│ +├── frontend/ # Next.js frontend +│ ├── app/ # App Router +│ │ ├── page.tsx +│ │ ├── layout.tsx +│ │ ├── create/ +│ │ ├── [id]/ +│ │ ├── config/ +│ │ └── services/ +│ ├── components/ # React компоненты +│ ├── public/ +│ ├── Dockerfile +│ └── package.json +│ +├── configs/ # Конфигурационные файлы +├── docs/ # Документация +│ └── wiki/ # Wiki страницы +├── Makefile # Удобные команды +├── docker-compose.yml # Dev environment +├── docker-compose.prod.yml # Prod environment +└── README.md +``` + +--- + +## 🔧 Backend разработка (Go) + +### Архитектура слоев + +**1. Handler Layer** (`internal/handler/`) +- HTTP обработка +- Валидация запросов +- Маппинг ответов + +**2. Service Layer** (`internal/service/`) +- Бизнес-логика +- Не знает о HTTP +- Работает через интерфейсы + +**3. Storage Layer** (`internal/storage/`) +- CRUD операции +- Работа с Redis +- Реализует Storage интерфейс + +### Добавление нового endpoint + +**Пример: добавим `GET /api/v1/polls/{id}` для получения метаданных опроса** + +**1. Добавить метод в Storage интерфейс:** + +```go +// internal/storage/redis.go +type Storage interface { + CreatePoll(ctx context.Context, poll *model.Poll, ttl time.Duration) error + GetPoll(ctx context.Context, pollID string) (*model.Poll, error) // Уже есть + Vote(ctx context.Context, pollID string, optionIndices []int) error + GetResults(ctx context.Context, pollID string) (*model.PollResults, error) + Close() error +} +``` + +**2. Добавить handler:** + +```go +// internal/handler/poll.go + +// GetPoll godoc +// @Summary Get poll metadata +// @Description Return poll metadata without vote counts +// @Tags polls +// @Produce json +// @Param id path string true "Poll ID" +// @Success 200 {object} model.Poll +// @Failure 404 {object} model.ErrorResponse +// @Router /polls/{id} [get] +func (h *PollHandler) GetPoll(c *gin.Context) { + pollID := c.Param("id") + + poll, err := h.service.GetPoll(c.Request.Context(), pollID) + if err != nil { + if errors.Is(err, storage.ErrPollNotFound) { + c.JSON(http.StatusNotFound, model.ErrorResponse{ + Error: "poll_not_found", + Message: "Poll not found or expired", + }) + return + } + + c.JSON(http.StatusInternalServerError, model.ErrorResponse{ + Error: "internal_error", + Message: "Failed to get poll", + }) + return + } + + c.JSON(http.StatusOK, poll) +} +``` + +**3. Добавить метод в Service:** + +```go +// internal/service/poll.go + +func (s *PollService) GetPoll(ctx context.Context, pollID string) (*model.Poll, error) { + poll, err := s.storage.GetPoll(ctx, pollID) + if err != nil { + if err == storage.ErrPollNotFound { + s.logger.WarnContext(ctx, "poll not found", slog.String("poll_id", pollID)) + return nil, err + } + + s.logger.ErrorContext(ctx, "failed to get poll", + slog.String("poll_id", pollID), + slog.String("error", err.Error()), + ) + return nil, fmt.Errorf("failed to get poll: %w", err) + } + + return poll, nil +} +``` + +**4. Зарегистрировать роут:** + +```go +// internal/handler/router.go + +func SetupRouter(handler *PollHandler, logger *slog.Logger, releaseMode bool) *gin.Engine { + // ... existing code ... + + v1 := router.Group("/api/v1") + { + polls := v1.Group("/polls") + { + polls.POST("", handler.CreatePoll) + polls.GET("/:id", handler.GetPoll) // НОВЫЙ ENDPOINT + polls.POST("/:id/vote", handler.Vote) + polls.GET("/:id/results", handler.GetResults) + } + } + + return router +} +``` + +**5. Регенерировать Swagger:** + +```bash +cd backend +swag init -g cmd/api/main.go +``` + +**6. Тестирование:** + +```bash +# Запустить сервер +go run cmd/api/main.go + +# Тест +curl http://localhost:8080/api/v1/polls/abc123 +``` + +### Логирование + +Используйте структурированное логирование: + +```go +s.logger.InfoContext(ctx, "poll created", + slog.String("poll_id", pollID), + slog.String("title", req.Title), + slog.Int("options_count", len(req.Options)), +) + +s.logger.ErrorContext(ctx, "failed to create poll", + slog.String("poll_id", pollID), + slog.String("error", err.Error()), +) +``` + +### Тестирование + +**Unit тесты:** + +```go +// internal/service/poll_test.go + +func TestPollService_CreatePoll(t *testing.T) { + // Arrange + mockStorage := &MockStorage{} + mockConfig := &config.Config{ + Poll: config.PollConfig{ + DefaultTTL: 168 * time.Hour, + }, + } + logger := slog.Default() + service := NewPollService(mockStorage, mockConfig, logger) + + req := &model.CreatePollRequest{ + Title: "Test Poll", + Options: []string{"A", "B", "C"}, + } + + // Act + resp, err := service.CreatePoll(context.Background(), req) + + // Assert + assert.NoError(t, err) + assert.NotEmpty(t, resp.PollID) + assert.Contains(t, resp.VoteURL, resp.PollID) +} +``` + +**Запуск тестов:** + +```bash +cd backend +go test ./... # Все тесты +go test -v ./internal/service/... # Конкретный пакет +go test -race ./... # С race detector +go test -cover ./... # С покрытием +``` + +### Makefile команды + +```bash +make help # Показать команды +make build # Собрать бинарник +make run # Запустить локально +make test # Запустить тесты +make test-coverage # Покрытие тестами +make fmt # Форматировать код +make lint # Линтинг +make deps # Обновить зависимости +``` + +--- + +## 🎨 Frontend разработка (Next.js) + +### Структура App Router + +``` +app/ +├── layout.tsx # Root layout (обертка для всех страниц) +├── page.tsx # Home page (/) +├── create/ +│ └── page.tsx # Create poll page (/create) +├── [id]/ # Dynamic routing +│ ├── page.tsx # Vote page (/[id]) +│ └── results/ +│ └── page.tsx # Results page (/[id]/results) +├── config/ +│ └── api.ts # API configuration +└── services/ + └── pollService.ts # API client +``` + +### Добавление новой страницы + +**Пример: страница "О проекте"** + +**1. Создайте страницу:** + +```tsx +// app/about/page.tsx + +export default function AboutPage() { + return ( +
+

О проекте Surway

+

+ Surway — современный сервис для создания и проведения опросов... +

+
+ ); +} +``` + +Страница автоматически доступна по `/about`! + +### Server vs Client Components + +**Server Component (по умолчанию):** + +```tsx +// app/[id]/results/page.tsx + +export default async function ResultsPage({ params }: { params: { id: string } }) { + // Данные загружаются на сервере + const results = await pollService.getResults(params.id); + + return ( +
+

{results.poll.title}

+ +
+ ); +} +``` + +**Client Component:** + +```tsx +// components/VoteForm.tsx +'use client'; // ОБЯЗАТЕЛЬНО для интерактивных компонентов + +import { useState } from 'react'; + +export function VoteForm({ options }: { options: string[] }) { + const [selected, setSelected] = useState([]); + + const handleVote = async () => { + // ... voting logic + }; + + return ( +
+ {options.map((option, index) => ( + + ))} + +
+ ); +} +``` + +### API Client + +Используйте централизованный API client: + +```typescript +// app/services/pollService.ts + +const API_URL = `${API_CONFIG.protocol}://${API_CONFIG.host}:${API_CONFIG.port}/api/${API_CONFIG.version}`; + +export const pollService = { + async createPoll(data: CreatePollRequest): Promise { + const response = await fetch(`${API_URL}/polls`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + cache: 'no-store', // Для Server Components + }); + + if (!response.ok) { + throw new Error('Failed to create poll'); + } + + return response.json(); + }, + + // ... другие методы +}; +``` + +### Стилизация (Tailwind CSS) + +```tsx +// Пример компонента с Tailwind + +export function Button({ children, onClick }: ButtonProps) { + return ( + + ); +} +``` + +### Анимации (Framer Motion) + +```tsx +'use client'; + +import { motion } from 'framer-motion'; + +export function AnimatedCard({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} +``` + +### Тестирование + +```bash +cd frontend +npm run lint # ESLint +npm run build # Проверка на ошибки сборки +npm run dev # Dev сервер +``` + +--- + +## 🧪 Тестирование + +### Backend + +**Unit тесты:** +```bash +cd backend +go test ./internal/service/... +go test ./internal/storage/... +``` + +**Integration тесты:** +```bash +# Требуется запущенный Redis +docker run -d -p 6379:6379 redis:7-alpine +go test -tags=integration ./... +``` + +**Coverage:** +```bash +go test -coverprofile=coverage.out ./... +go tool cover -html=coverage.out +``` + +### Frontend + +**Lint:** +```bash +cd frontend +npm run lint +``` + +**Build test:** +```bash +npm run build +``` + +--- + +## 📏 Code Style + +### Go + +Следуйте [Effective Go](https://golang.org/doc/effective_go) и: + +- **gofmt** — форматирование (автоматически в большинстве IDE) +- **golangci-lint** — комплексный линтер + +```bash +# Установка +go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + +# Запуск +cd backend +golangci-lint run +``` + +**Conventions:** +- Публичные функции начинаются с заглавной буквы +- Приватные — со строчной +- Короткие переменные в циклах: `i`, `err`, `ctx` +- Комментарии к публичным функциям +- Обработка ошибок явно (не игнорируйте `err`) + +### TypeScript/React + +**ESLint конфиг:** +```bash +cd frontend +npm run lint +``` + +**Conventions:** +- Компоненты в PascalCase: `VoteForm.tsx` +- Функции/переменные в camelCase: `handleVote` +- Константы в UPPER_CASE: `API_URL` +- Используйте TypeScript типы +- Избегайте `any` + +--- + +## 🔀 Git Workflow + +### Branching Strategy + +``` +main (stable, production-ready) + └── develop (latest development) + ├── feature/add-websocket + ├── feature/auth-system + └── fix/redis-connection +``` + +### Создание feature ветки + +```bash +# 1. Обновите develop +git checkout develop +git pull upstream develop + +# 2. Создайте feature ветку +git checkout -b feature/my-awesome-feature + +# 3. Разработка +# ... код ... + +# 4. Commit +git add . +git commit -m "feat: add awesome feature" + +# 5. Push в ваш fork +git push origin feature/my-awesome-feature + +# 6. Создайте Pull Request на GitHub +``` + +### Commit Messages + +Используйте [Conventional Commits](https://www.conventionalcommits.org/): + +``` +feat: add WebSocket support for live updates +fix: resolve Redis connection timeout issue +docs: update API documentation +refactor: simplify poll creation logic +test: add unit tests for PollService +chore: update dependencies +``` + +**Формат:** +``` +: + +[optional body] + +[optional footer] +``` + +**Types:** +- `feat` — новая функциональность +- `fix` — исправление бага +- `docs` — документация +- `refactor` — рефакторинг (без изменения функциональности) +- `test` — добавление/изменение тестов +- `chore` — рутинные задачи (обновление зависимостей и т.д.) +- `perf` — оптимизация производительности +- `ci` — CI/CD изменения + +--- + +## 🤝 Contributing + +### Pull Request Process + +1. **Fork** репозиторий +2. **Создайте** feature ветку от `develop` +3. **Сделайте** изменения +4. **Добавьте** тесты (если применимо) +5. **Проверьте** что тесты проходят +6. **Запустите** линтер +7. **Commit** с правильным форматом сообщения +8. **Push** в ваш fork +9. **Откройте** Pull Request в `develop` ветку upstream +10. **Дождитесь** code review + +### Code Review Checklist + +**Reviewer проверяет:** +- [ ] Код следует стандартам проекта +- [ ] Есть тесты для новой функциональности +- [ ] Документация обновлена +- [ ] Нет breaking changes (или они документированы) +- [ ] Commit messages в правильном формате +- [ ] CI/CD пайплайн зеленый + +**Author должен:** +- [ ] Ответить на комментарии reviewer +- [ ] Внести правки если нужно +- [ ] Обновить PR после изменений + +--- + +## 🔍 Полезные ресурсы + +### Документация + +- [Go Documentation](https://golang.org/doc/) +- [Gin Framework](https://gin-gonic.com/docs/) +- [Next.js Documentation](https://nextjs.org/docs) +- [Redis Documentation](https://redis.io/documentation) + +### Наша документация + +- [Architecture](Architecture) +- [API Documentation](API-Documentation) +- [Deployment](Deployment) +- [Configuration](Configuration) + +--- + +## 💬 Вопросы? + +- **GitHub Issues:** https://github.com/AlexeyLars/surway/issues +- **GitHub Discussions:** https://github.com/AlexeyLars/surway/discussions +- **Author:** [@AlexeyLars](https://github.com/AlexeyLars) + +--- + +_Последнее обновление: December 2025_ diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md new file mode 100644 index 0000000..077c217 --- /dev/null +++ b/docs/wiki/Home.md @@ -0,0 +1,98 @@ +# 🎯 Surway Wiki + +Добро пожаловать в документацию проекта **Surway** — современного full-stack сервиса для создания и проведения опросов с множественным выбором. + +## 📚 Содержание + +### Основное +- **[Home](Home)** — эта страница +- **[Быстрый старт](Quick-Start)** — начало работы с проектом +- **[FAQ](FAQ)** — часто задаваемые вопросы + +### Архитектура и дизайн +- **[Архитектура](Architecture)** — детальное описание архитектуры системы +- **[Структура данных](Data-Structure)** — как хранятся данные в Redis +- **[API спецификация](API-Documentation)** — полная документация API endpoints + +### Разработка +- **[Development Guide](Development-Guide)** — гайд для разработчиков +- **[Configuration](Configuration)** — полное описание конфигурации +- **[Testing](Testing)** — как писать и запускать тесты +- **[Contributing](Contributing)** — как контрибьютить в проект + +### Деплой и DevOps +- **[Deployment](Deployment)** — деплой в production +- **[Docker Guide](Docker-Guide)** — работа с Docker и Docker Compose +- **[Monitoring](Monitoring)** — мониторинг и логирование + +--- + +## 🚀 Что такое Surway? + +Surway — это микросервис для создания временных опросов с: + +- **Множественным выбором** — голосуйте за несколько вариантов +- **Автоматическим TTL** — опросы удаляются по истечении срока +- **Real-time результатами** — мгновенное обновление статистики +- **Современным UI** — красивый интерфейс с анимациями +- **REST API** — интеграция с любыми клиентами +- **Production-ready** — готов к деплою с Docker и SSL + +--- + +## 🏗️ Технологический стек + +### Backend +- Go 1.24.2 + Gin framework +- Redis 7 для хранения данных +- Swagger для документации API +- Структурированное логирование (slog) + +### Frontend +- Next.js 15 + React 19 +- TypeScript 5 +- Tailwind CSS 4 +- Recharts для графиков +- Framer Motion для анимаций + +### Infrastructure +- Docker & Docker Compose +- Caddy для reverse proxy и SSL +- Redis для persistence + +--- + +## 📖 С чего начать? + +1. **Новичок в проекте?** → Начните с [Быстрого старта](Quick-Start) +2. **Хотите понять архитектуру?** → Читайте [Architecture](Architecture) +3. **Нужно настроить проект?** → Смотрите [Configuration](Configuration) +4. **Готовы к разработке?** → Изучите [Development Guide](Development-Guide) +5. **Деплоите в production?** → Следуйте [Deployment](Deployment) + +--- + +## 🤝 Поддержка и контакты + +- **GitHub Issues:** [github.com/AlexeyLars/surway/issues](https://github.com/AlexeyLars/surway/issues) +- **Pull Requests:** Приветствуются! См. [Contributing](Contributing) +- **Автор:** [@AlexeyLars](https://github.com/AlexeyLars) + +--- + +## 📊 Статус проекта + +🚀 **Active Development** — проект активно развивается + +### Последние обновления +- ✅ REST API с Swagger +- ✅ Frontend на Next.js 15 +- ✅ Docker Compose для dev и prod +- ✅ Множественный выбор в опросах +- ✅ Graceful shutdown +- 🚧 WebSocket для live updates (в планах) +- 🚧 Аутентификация (в планах) + +--- + +_Последнее обновление: December 2025_