feat(src/modules/cities/cities.controller.ts): добавляет ответы API для получения всех городов и районов

 feat(src/modules/chat/chat.controller.ts): добавляет ответы API для создания чата и получения сообщений

 feat(src/modules/greetings/greetings.controller.ts): добавляет ответы API для получения и создания приветствий

 feat(src/modules/likes/likes.controller.ts): добавляет ответы API для создания лайков и получения совпадений

 feat(src/modules/reports/reports.controller.ts): добавляет ответы API для создания и получения отчетов

 feat(src/modules/feed/feed.controller.ts): добавляет ответ API для получения отфильтрованного фида

 feat(src/auth/auth.controller.ts): добавляет ответы API для регистрации, входа и выхода пользователей

 feat(src/modules/media/media.controller.ts): добавляет ответы API для загрузки и получения медиа

 feat(src/modules/users/users.controller.ts): добавляет ответы API для получения текущего пользователя и управления пользователями

 feat(src/modules/tags/tags.controller.ts): добавляет ответы API для получения и создания тегов

 feat(src/modules/profiles/profiles.controller.ts): добавляет ответы API для управления профилями пользователей

 feat(src/modules/dates/dates.controller.ts): добавляет ответы API для создания и получения встреч
This commit is contained in:
Oscar
2026-06-08 14:22:50 +03:00
parent bc3e48bcad
commit 102b6b4026
24 changed files with 598 additions and 12 deletions

341
frontend-starter-prompt.md Normal file
View File

@@ -0,0 +1,341 @@
# Стартовый промт — Frontend Dating App
## Контекст проекта
Ты помогаешь разрабатывать фронтенд мобильного/десктопного дейтинг-приложения.
OpenAPI-схема бэкенда лежит в корне проекта (`openapi.json`).
Генерируй типизированный HTTP-клиент из неё автоматически.
---
## Технологический стек
| Слой | Выбор |
|---|---|
| UI-фреймворк | **Vue 3** (Composition API + `<script setup>`) |
| Язык | **TypeScript** везде |
| Сборщик | **Vite** |
| Десктоп / нативный слой | **Tauri 2** |
| Стейт-менеджер | **Pinia** |
| Роутер | **Vue Router 4** |
| HTTP-клиент | **axios** + автогенерированные типы из OpenAPI (`openapi-typescript` + `axios`) |
| WebSocket | **socket.io-client** |
| UI-библиотека | **Naive UI** (или UnoCSS + headless — на твоё усмотрение) |
| Формы | **VeeValidate** + **Zod** |
| Пакетный менеджер | **pnpm** |
| Анимации | **@vueuse/motion** |
| Утилиты | **VueUse** |
| Иконки | **@iconify/vue** |
| Адаптивность | mobile-first; десктоп — расширение, не отдельная версия |
---
## Что такое это приложение
Дейтинг-приложение. Пользователь:
1. Регистрируется по номеру телефона + пароль
2. Создаёт один или несколько публичных **профилей** (у одного аккаунта может быть много профилей)
3. Листает **ленту** других профилей с фильтрацией
4. Ставит **лайки / дизлайки** на профили
5. При взаимном лайке создаётся **матч**
6. Открывает **чат** с одним матчем за раз (один активный чат на профиль)
7. В чате обменивается текстом, фото, голосовыми и видео-сообщениями
8. Договаривается о реальных **встречах** (дата, координаты, статус)
9. Может подать **жалобу** на профиль или сообщение
---
## Архитектура бэкенда (важно для интеграции)
### Базовый URL
```
http://localhost:3000/api/v1
```
### Формат ответа
**Все** успешные ответы обёрнуты:
```json
{ "data": <payload> }
```
Ошибки:
```json
{ "statusCode": 400, "message": "...", "timestamp": "...", "path": "..." }
```
### Авторизация
- Тип: **Bearer JWT**
- Заголовок: `Authorization: Bearer <accessToken>`
- `POST /auth/register` — регистрация `{ phone, password }`
- `POST /auth/login` — вход `{ phone, password }``{ accessToken, refreshToken }`
- `POST /auth/refresh` — обновление `{ refreshToken }``{ accessToken, refreshToken }`
- `POST /auth/logout` — выход (требует Bearer)
- `POST /auth/fcm-token` — обновить push-токен `{ fcmToken }`
- Access-токен: TTL **7 дней** (настраивается на бэке)
- Refresh-токен: TTL **30 дней**, хранится в Redis
### Профили (один аккаунт → много профилей)
- `POST /profiles` — создать профиль
- `GET /profiles/my` — все мои профили
- `GET /profiles/:profileId` — публичный профиль
- `PUT /profiles/:profileId` — обновить (только владелец)
- `DELETE /profiles/:profileId` — удалить (только владелец)
Поля профиля: `name`, `birthDate`, `gender` (`male|female`), `cityId`, `districtId`,
`description`, `nation`, `height`, `weight`, `tagIds[]`
Ответ профиля включает: `city`, `district`, `tags[]`, `media[]` (отсортировано по `sortOrder`)
### Медиа профиля
Маршруты: `POST /profiles/:profileId/media/upload?type=photo|video|audio`
Медиафайлы профиля: **фото, видео, аудио** — хранятся в MinIO.
В ответе профиля приходит массив `media[]` с полями `{ id, path, type, sortOrder }`.
### Лента
```
GET /feed?profileId=<uuid>&page=1&limit=20
&cityId=&districtId=
&ageMin=&ageMax=
&keyword=
&tagIds[]=
```
- Уже лайкнутые профили **не показываются**
- Порядок случайный
- При достижении лимита матчей (`MAX_MATCHES_BEFORE_PAUSE=10`) лайк вернёт `400`
клиент должен показать экран «разбери матчи»
### Лайки и матчи
- `POST /likes``{ sourceProfileId, targetProfileId, type: "like"|"dislike" }`
Ответ: `{ like, match }` — если `match != null`, это взаимный лайк
- `GET /likes/matches?profileId=<uuid>` — все матчи профиля
### Чат
- Один профиль — один активный чат одновременно (`profile.active_chat_id`)
- `POST /chats``{ profileId, matchId }` → открыть чат для матча
- `GET /chats?profileId=<uuid>` — активные чаты
- `GET /chats/:chatId/messages?profileId=<uuid>&page=1&limit=50`
- `POST /chats/:chatId/messages?profileId=<uuid>` — отправить `{ text?, mediaUrl?, mediaType? }`
- `DELETE /chats/:chatId?profileId=<uuid>` — закрыть чат
### Встречи (dates)
- `POST /dates``{ profileId, partnerProfileId, lat, lng, time, statusId? }`
- `GET /dates?profileId=<uuid>`
- `PATCH /dates/:id/status?profileId=<uuid>``{ statusId }`
- `GET /dates/statuses` — справочник статусов (`pending`, `confirmed`, `cancelled`, `rescheduled`)
### Репорты
- `POST /reports``{ sourceProfileId, entityId, entityType: "profile"|"message", description? }`
### Справочники (публичные, без токена)
- `GET /tags`
- `GET /cities`
- `GET /cities/:cityId/districts`
- `GET /greetings` — готовые приветственные фразы
---
## WebSocket (real-time чат)
Библиотека: **socket.io-client**
```typescript
const socket = io('http://localhost:3000/chat', {
auth: {
token: '<accessToken>',
profileId: '<activeProfileId>', // обязательно!
}
})
```
**Клиент отправляет:**
| Событие | Payload | Описание |
|---|---|---|
| `join_chat` | `{ chatId }` | Войти в комнату чата |
| `leave_chat` | `{ chatId }` | Покинуть комнату |
| `send_message` | `{ chatId, text?, mediaUrl?, mediaType? }` | Отправить сообщение |
| `typing` | `{ chatId, isTyping: bool }` | Индикатор печати |
**Сервер отправляет:**
| Событие | Payload | Описание |
|---|---|---|
| `new_message` | `Message` | Новое сообщение в комнате |
| `user_typing` | `{ profileId, isTyping }` | Кто-то печатает |
**Важно:** при смене активного профиля переподключи сокет с новым `profileId`.
---
## Структура проекта
```
src/
api/ # Автогенерированный клиент из openapi.json
client.ts # axios-инстанс с интерцептором токена и рефреша
index.ts # реэкспорт всех операций
stores/
auth.store.ts # user, accessToken, refreshToken, login/logout
profile.store.ts # activeProfile, myProfiles, switch
feed.store.ts # лента, пагинация
chat.store.ts # активный чат, сообщения, WS-соединение
match.store.ts # матчи
composables/
useSocket.ts # хук socket.io с авто-реконнектом
useInfiniteScroll.ts # бесконечная прокрутка
useMediaUpload.ts # загрузка файлов с прогрессом
router/
index.ts
guards.ts # редирект на /login если нет токена, /setup-profile если нет профиля
views/
auth/
LoginView.vue
RegisterView.vue
onboarding/
CreateProfileView.vue # создание первого профиля после регистрации
feed/
FeedView.vue # свайп-лента или карточки
matches/
MatchesView.vue
chat/
ChatsListView.vue
ChatView.vue # реалтайм переписка
dates/
DatesView.vue
profile/
MyProfilesView.vue # список профилей аккаунта
ProfileEditView.vue
ProfilePublicView.vue # как видят другие
settings/
SettingsView.vue
components/
profile/
ProfileCard.vue # карточка в ленте
ProfileAvatar.vue
MediaGallery.vue # фото/видео/аудио галерея профиля
MediaUploader.vue # загрузка с preview
chat/
MessageBubble.vue
TypingIndicator.vue
AudioPlayer.vue # проигрыватель голосовых
feed/
SwipeCard.vue # свайп-карточка (мобильная)
LikeButtons.vue
common/
BottomNav.vue # навигация для мобилки
SideNav.vue # навигация для десктопа
AppLayout.vue # адаптивный layout
tauri/ # Tauri-специфичный код
notifications.ts # нативные уведомления
deepLinks.ts
```
---
## Адаптивность: мобилка и десктоп в одном приложении
Приложение собирается через **Tauri 2**. Один кодовой базой покрывается:
- **Мобилка** (Tauri + iOS/Android через Tauri Mobile) — свайп-жесты, нижняя навигация, полноэкранные карточки
- **Десктоп** (Tauri + Windows/macOS/Linux) — боковое меню, двухколоночный layout, drag-and-drop для медиа
Используй CSS-брейкпоинты **mobile-first**:
- `< 768px` — мобильный layout (BottomNav, полноэкранная лента)
- `≥ 768px` — планшет/десктоп (SideNav, split-view для чата)
Определяй режим через `window.innerWidth` или VueUse `useBreakpoints`.
Tauri-специфичное поведение (нативные уведомления, файловый пикер) оборачивай в composable с graceful degradation до браузерного API.
---
## Ключевые UX-сценарии
### 1. Первый запуск
`Регистрация``Создание профиля` (name, birthDate, gender обязательны) →
`Загрузка фото``Лента`
### 2. Лента и лайк
Карточка профиля → свайп вправо (лайк) / влево (дизлайк) → если матч →
показать `MatchModal` с кнопкой «Написать» → открыть чат
### 3. Лимит матчей
Попытка лайкнуть при `N >= 10 матчей` → API вернёт `400`
показать экран/модал «У тебя X матчей. Разберись с ними, чтобы продолжить поиск»
### 4. Чат
Список чатов → выбор → join WS-комнаты → обмен сообщениями в реалтайме →
закрытие чата = новый чат стал доступен.
Перед открытием чата — можно выбрать приветственную фразу из `GET /greetings`.
### 5. Смена активного профиля
В настройках — список всех профилей аккаунта →
при смене переподключить WS с новым `profileId`.
### 6. Встреча
В активном чате кнопка «Назначить встречу» → форма (карта/координаты + дата) →
`POST /dates` → партнёр видит `pending` → может подтвердить / отменить / перенести.
---
## Хранение состояния авторизации
```typescript
// stores/auth.store.ts (Pinia)
// accessToken + refreshToken хранить в localStorage
// При 401 — автоматически вызывать POST /auth/refresh
// При неудаче refresh — очистить стор, редирект на /login
```
Интерцептор axios:
```typescript
// При 401: один раз попытаться рефрешнуть токен,
// повторить исходный запрос с новым токеном,
// если рефреш тоже 401 — logout
```
---
## Нотификации
- В браузере / PWA: Web Notifications API
- В Tauri: `@tauri-apps/plugin-notification`
- FCM-токен: после логина отправить на `POST /auth/fcm-token`
- Обрабатывать WS-события `new_message` и `match_created` для in-app уведомлений
---
## Генерация API-клиента
В корне проекта лежит `openapi.json`. Сгенерируй типизированный клиент:
```bash
pnpm add -D openapi-typescript
pnpm exec openapi-typescript openapi.json -o src/api/schema.d.ts
```
Используй типы из схемы для всех запросов/ответов.
Создай обёртку над axios, которая автоматически разворачивает `{ data: T }`.
---
## Что НЕ нужно реализовывать сейчас
- Логика оплаты (таблицы `tariff`/`payment` есть в БД, но API оплаты нет)
- Админ-панель (модераторские эндпоинты — ban/activate пользователей — есть в API, но отдельного UI не нужно)
---
## Старт проекта
```bash
pnpm create tauri-app@latest
# выбрать: Vue + TypeScript + Vite
pnpm add pinia vue-router axios socket.io-client vee-validate zod @vueuse/core @vueuse/motion naive-ui @iconify/vue
pnpm add -D openapi-typescript @types/node
# Сгенерировать типы из схемы
pnpm exec openapi-typescript openapi.json -o src/api/schema.d.ts
```
Не спрашивай подтверждения на каждый шаг — работай до тех пор,
пока весь фронтенд не будет готов к первому запуску (`pnpm tauri dev`).

View File

@@ -1,12 +1,14 @@
import { Body, Controller, Post, UseGuards } from '@nestjs/common'; import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto'; import { LoginDto } from './dto/login.dto';
import { RefreshTokenDto } from './dto/refresh-token.dto'; import { RefreshTokenDto } from './dto/refresh-token.dto';
import { RegisterDto } from './dto/register.dto'; import { RegisterDto } from './dto/register.dto';
import { TokensResponseDto } from './dto/tokens-response.dto';
import { Public } from '../common/decorators/public.decorator'; import { Public } from '../common/decorators/public.decorator';
import { CurrentUser } from '../common/decorators/current-user.decorator'; import { CurrentUser } from '../common/decorators/current-user.decorator';
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
import { MessageResponseDto } from '../common/dto/message-response.dto';
@ApiTags('auth') @ApiTags('auth')
@Controller('auth') @Controller('auth')
@@ -16,6 +18,7 @@ export class AuthController {
@Public() @Public()
@Post('register') @Post('register')
@ApiOperation({ summary: 'Register new user' }) @ApiOperation({ summary: 'Register new user' })
@ApiResponse({ status: 201, type: TokensResponseDto })
register(@Body() dto: RegisterDto) { register(@Body() dto: RegisterDto) {
return this.authService.register(dto); return this.authService.register(dto);
} }
@@ -23,6 +26,7 @@ export class AuthController {
@Public() @Public()
@Post('login') @Post('login')
@ApiOperation({ summary: 'Login with phone and password' }) @ApiOperation({ summary: 'Login with phone and password' })
@ApiResponse({ status: 201, type: TokensResponseDto })
login(@Body() dto: LoginDto) { login(@Body() dto: LoginDto) {
return this.authService.login(dto); return this.authService.login(dto);
} }
@@ -31,6 +35,7 @@ export class AuthController {
@Post('logout') @Post('logout')
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ summary: 'Logout current user' }) @ApiOperation({ summary: 'Logout current user' })
@ApiResponse({ status: 201, type: MessageResponseDto })
logout(@CurrentUser('id') userId: string) { logout(@CurrentUser('id') userId: string) {
return this.authService.logout(userId); return this.authService.logout(userId);
} }
@@ -38,6 +43,7 @@ export class AuthController {
@Public() @Public()
@Post('refresh') @Post('refresh')
@ApiOperation({ summary: 'Refresh access token' }) @ApiOperation({ summary: 'Refresh access token' })
@ApiResponse({ status: 201, type: TokensResponseDto })
refresh(@Body() dto: RefreshTokenDto) { refresh(@Body() dto: RefreshTokenDto) {
return this.authService.refreshTokens(dto.refreshToken); return this.authService.refreshTokens(dto.refreshToken);
} }
@@ -46,6 +52,7 @@ export class AuthController {
@Post('fcm-token') @Post('fcm-token')
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ summary: 'Update FCM push token' }) @ApiOperation({ summary: 'Update FCM push token' })
@ApiResponse({ status: 201, type: MessageResponseDto })
updateFcmToken( updateFcmToken(
@CurrentUser('id') userId: string, @CurrentUser('id') userId: string,
@Body('fcmToken') fcmToken: string, @Body('fcmToken') fcmToken: string,

View File

@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
export class TokensResponseDto {
@ApiProperty({ example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' })
accessToken: string;
@ApiProperty({ example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' })
refreshToken: string;
}

View File

@@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
export class MessageResponseDto {
@ApiProperty({ example: 'Operation successful' })
message: string;
}

View File

@@ -1,10 +1,12 @@
import { Body, Controller, Delete, Get, Param, Post, Query, UseGuards } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '../../common/decorators/current-user.decorator'; import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { ChatService } from './chat.service'; import { ChatService } from './chat.service';
import { CreateChatDto } from './dto/create-chat.dto'; import { CreateChatDto } from './dto/create-chat.dto';
import { SendMessageDto } from './dto/send-message.dto'; import { SendMessageDto } from './dto/send-message.dto';
import { ChatDto, MessageDto } from './dto/chat-response.dto';
import { MessageResponseDto } from '../../common/dto/message-response.dto';
@ApiTags('chat') @ApiTags('chat')
@ApiBearerAuth() @ApiBearerAuth()
@@ -15,12 +17,14 @@ export class ChatController {
@Post() @Post()
@ApiOperation({ summary: 'Open a chat for a match' }) @ApiOperation({ summary: 'Open a chat for a match' })
@ApiResponse({ status: 201, type: ChatDto })
createChat(@CurrentUser('id') userId: string, @Body() dto: CreateChatDto) { createChat(@CurrentUser('id') userId: string, @Body() dto: CreateChatDto) {
return this.chatService.createChat(userId, dto); return this.chatService.createChat(userId, dto);
} }
@Get() @Get()
@ApiOperation({ summary: 'Get active chats for a profile' }) @ApiOperation({ summary: 'Get active chats for a profile' })
@ApiResponse({ status: 200, type: [ChatDto] })
getChats( getChats(
@CurrentUser('id') userId: string, @CurrentUser('id') userId: string,
@Query('profileId') profileId: string, @Query('profileId') profileId: string,
@@ -30,6 +34,9 @@ export class ChatController {
@Get(':chatId/messages') @Get(':chatId/messages')
@ApiOperation({ summary: 'Get chat messages' }) @ApiOperation({ summary: 'Get chat messages' })
@ApiQuery({ name: 'page', required: false, schema: { default: 1, type: 'number' } })
@ApiQuery({ name: 'limit', required: false, schema: { default: 50, type: 'number' } })
@ApiResponse({ status: 200, type: [MessageDto] })
getMessages( getMessages(
@CurrentUser('id') userId: string, @CurrentUser('id') userId: string,
@Query('profileId') profileId: string, @Query('profileId') profileId: string,
@@ -42,6 +49,7 @@ export class ChatController {
@Post(':chatId/messages') @Post(':chatId/messages')
@ApiOperation({ summary: 'Send a message' }) @ApiOperation({ summary: 'Send a message' })
@ApiResponse({ status: 201, type: MessageDto })
sendMessage( sendMessage(
@CurrentUser('id') userId: string, @CurrentUser('id') userId: string,
@Query('profileId') profileId: string, @Query('profileId') profileId: string,
@@ -53,6 +61,7 @@ export class ChatController {
@Delete(':chatId') @Delete(':chatId')
@ApiOperation({ summary: 'Close a chat' }) @ApiOperation({ summary: 'Close a chat' })
@ApiResponse({ status: 200, type: MessageResponseDto })
closeChat( closeChat(
@CurrentUser('id') userId: string, @CurrentUser('id') userId: string,
@Query('profileId') profileId: string, @Query('profileId') profileId: string,

View File

@@ -0,0 +1,18 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class ChatDto {
@ApiProperty() id: string;
@ApiProperty() profile1Id: string;
@ApiProperty() profile2Id: string;
@ApiProperty({ enum: ['active', 'closed'] }) status: string;
}
export class MessageDto {
@ApiProperty() id: string;
@ApiProperty() chatId: string;
@ApiProperty() profileId: string;
@ApiPropertyOptional({ nullable: true }) text: string | null;
@ApiPropertyOptional({ nullable: true }) mediaUrl: string | null;
@ApiPropertyOptional({ enum: ['photo', 'voice', 'video'], nullable: true }) mediaType: string | null;
@ApiProperty() createdAt: string;
}

View File

@@ -1,10 +1,11 @@
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { Public } from '../../common/decorators/public.decorator'; import { Public } from '../../common/decorators/public.decorator';
import { Roles } from '../../common/decorators/roles.decorator'; import { Roles } from '../../common/decorators/roles.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RolesGuard } from '../../common/guards/roles.guard'; import { RolesGuard } from '../../common/guards/roles.guard';
import { CitiesService } from './cities.service'; import { CitiesService } from './cities.service';
import { CityResponseDto, DistrictResponseDto } from './dto/city-response.dto';
@ApiTags('cities') @ApiTags('cities')
@Controller('cities') @Controller('cities')
@@ -14,6 +15,7 @@ export class CitiesController {
@Public() @Public()
@Get() @Get()
@ApiOperation({ summary: 'Get all cities' }) @ApiOperation({ summary: 'Get all cities' })
@ApiResponse({ status: 200, type: [CityResponseDto] })
findAll() { findAll() {
return this.citiesService.findAll(); return this.citiesService.findAll();
} }
@@ -21,6 +23,7 @@ export class CitiesController {
@Public() @Public()
@Get(':cityId/districts') @Get(':cityId/districts')
@ApiOperation({ summary: 'Get districts for a city' }) @ApiOperation({ summary: 'Get districts for a city' })
@ApiResponse({ status: 200, type: [DistrictResponseDto] })
findDistricts(@Param('cityId') cityId: string) { findDistricts(@Param('cityId') cityId: string) {
return this.citiesService.findDistricts(cityId); return this.citiesService.findDistricts(cityId);
} }
@@ -30,6 +33,7 @@ export class CitiesController {
@Roles('admin') @Roles('admin')
@Post() @Post()
@ApiOperation({ summary: 'Create city (admin only)' }) @ApiOperation({ summary: 'Create city (admin only)' })
@ApiResponse({ status: 201, type: CityResponseDto })
createCity(@Body() body: { name: string; lat: number; lng: number }) { createCity(@Body() body: { name: string; lat: number; lng: number }) {
return this.citiesService.createCity(body.name, body.lat, body.lng); return this.citiesService.createCity(body.name, body.lat, body.lng);
} }
@@ -39,6 +43,7 @@ export class CitiesController {
@Roles('admin') @Roles('admin')
@Post(':cityId/districts') @Post(':cityId/districts')
@ApiOperation({ summary: 'Create district (admin only)' }) @ApiOperation({ summary: 'Create district (admin only)' })
@ApiResponse({ status: 201, type: DistrictResponseDto })
createDistrict(@Param('cityId') cityId: string, @Body() body: { name: string }) { createDistrict(@Param('cityId') cityId: string, @Body() body: { name: string }) {
return this.citiesService.createDistrict(cityId, body.name); return this.citiesService.createDistrict(cityId, body.name);
} }

View File

@@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
export class CityResponseDto {
@ApiProperty() id: string;
@ApiProperty() name: string;
@ApiProperty() lat: string;
@ApiProperty() lng: string;
}
export class DistrictResponseDto {
@ApiProperty() id: string;
@ApiProperty() cityId: string;
@ApiProperty() name: string;
}

View File

@@ -1,10 +1,11 @@
import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common'; import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '../../common/decorators/current-user.decorator'; import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { DatesService } from './dates.service'; import { DatesService } from './dates.service';
import { CreateDateDto } from './dto/create-date.dto'; import { CreateDateDto } from './dto/create-date.dto';
import { UpdateDateStatusDto } from './dto/update-date-status.dto'; import { UpdateDateStatusDto } from './dto/update-date-status.dto';
import { DateDto, DateStatusDto, DateWithStatusDto } from './dto/dates-response.dto';
@ApiTags('dates') @ApiTags('dates')
@ApiBearerAuth() @ApiBearerAuth()
@@ -15,12 +16,14 @@ export class DatesController {
@Post() @Post()
@ApiOperation({ summary: 'Propose a meetup' }) @ApiOperation({ summary: 'Propose a meetup' })
@ApiResponse({ status: 201, type: DateDto })
create(@CurrentUser('id') userId: string, @Body() dto: CreateDateDto) { create(@CurrentUser('id') userId: string, @Body() dto: CreateDateDto) {
return this.datesService.create(userId, dto); return this.datesService.create(userId, dto);
} }
@Get() @Get()
@ApiOperation({ summary: 'Get dates for a profile' }) @ApiOperation({ summary: 'Get dates for a profile' })
@ApiResponse({ status: 200, type: [DateWithStatusDto] })
getDates( getDates(
@CurrentUser('id') userId: string, @CurrentUser('id') userId: string,
@Query('profileId') profileId: string, @Query('profileId') profileId: string,
@@ -30,6 +33,7 @@ export class DatesController {
@Patch(':id/status') @Patch(':id/status')
@ApiOperation({ summary: 'Update date status' }) @ApiOperation({ summary: 'Update date status' })
@ApiResponse({ status: 200, type: DateDto })
updateStatus( updateStatus(
@CurrentUser('id') userId: string, @CurrentUser('id') userId: string,
@Query('profileId') profileId: string, @Query('profileId') profileId: string,
@@ -41,6 +45,7 @@ export class DatesController {
@Get('statuses') @Get('statuses')
@ApiOperation({ summary: 'Get available date statuses' }) @ApiOperation({ summary: 'Get available date statuses' })
@ApiResponse({ status: 200, type: [DateStatusDto] })
getStatuses() { getStatuses() {
return this.datesService.getStatuses(); return this.datesService.getStatuses();
} }

View File

@@ -0,0 +1,21 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class DateStatusDto {
@ApiProperty() id: string;
@ApiProperty() text: string;
}
export class DateDto {
@ApiProperty() id: string;
@ApiProperty() profile1Id: string;
@ApiProperty() profile2Id: string;
@ApiProperty() lat: string;
@ApiProperty() lng: string;
@ApiProperty() time: string;
@ApiPropertyOptional({ nullable: true }) statusId: string | null;
}
export class DateWithStatusDto {
@ApiProperty({ type: DateDto }) date: DateDto;
@ApiPropertyOptional({ type: DateStatusDto, nullable: true }) date_status: DateStatusDto | null;
}

View File

@@ -1,9 +1,10 @@
import { Controller, Get, Query, UseGuards } from '@nestjs/common'; import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '../../common/decorators/current-user.decorator'; import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { FeedFilterDto } from './dto/feed-filter.dto'; import { FeedFilterDto } from './dto/feed-filter.dto';
import { FeedService } from './feed.service'; import { FeedService } from './feed.service';
import { ProfileResponseDto } from '../profiles/dto/profile-response.dto';
@ApiTags('feed') @ApiTags('feed')
@ApiBearerAuth() @ApiBearerAuth()
@@ -14,6 +15,7 @@ export class FeedController {
@Get() @Get()
@ApiOperation({ summary: 'Get filtered feed (requires profileId)' }) @ApiOperation({ summary: 'Get filtered feed (requires profileId)' })
@ApiResponse({ status: 200, type: [ProfileResponseDto] })
getFeed(@CurrentUser('id') userId: string, @Query() filter: FeedFilterDto) { getFeed(@CurrentUser('id') userId: string, @Query() filter: FeedFilterDto) {
return this.feedService.getFeed(userId, filter); return this.feedService.getFeed(userId, filter);
} }

View File

@@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
export class GreetingDto {
@ApiProperty() id: string;
@ApiProperty() text: string;
}

View File

@@ -1,10 +1,12 @@
import { Body, Controller, Delete, Get, Param, Post, UseGuards } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { Public } from '../../common/decorators/public.decorator'; import { Public } from '../../common/decorators/public.decorator';
import { Roles } from '../../common/decorators/roles.decorator'; import { Roles } from '../../common/decorators/roles.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RolesGuard } from '../../common/guards/roles.guard'; import { RolesGuard } from '../../common/guards/roles.guard';
import { GreetingsService } from './greetings.service'; import { GreetingsService } from './greetings.service';
import { GreetingDto } from './dto/greeting-response.dto';
import { MessageResponseDto } from '../../common/dto/message-response.dto';
@ApiTags('greetings') @ApiTags('greetings')
@Controller('greetings') @Controller('greetings')
@@ -14,6 +16,7 @@ export class GreetingsController {
@Public() @Public()
@Get() @Get()
@ApiOperation({ summary: 'Get all greeting phrases' }) @ApiOperation({ summary: 'Get all greeting phrases' })
@ApiResponse({ status: 200, type: [GreetingDto] })
findAll() { findAll() {
return this.greetingsService.findAll(); return this.greetingsService.findAll();
} }
@@ -23,6 +26,7 @@ export class GreetingsController {
@Roles('admin') @Roles('admin')
@Post() @Post()
@ApiOperation({ summary: 'Add greeting phrase (admin only)' }) @ApiOperation({ summary: 'Add greeting phrase (admin only)' })
@ApiResponse({ status: 201, type: GreetingDto })
create(@Body('text') text: string) { create(@Body('text') text: string) {
return this.greetingsService.create(text); return this.greetingsService.create(text);
} }
@@ -32,6 +36,7 @@ export class GreetingsController {
@Roles('admin') @Roles('admin')
@Delete(':id') @Delete(':id')
@ApiOperation({ summary: 'Delete greeting phrase (admin only)' }) @ApiOperation({ summary: 'Delete greeting phrase (admin only)' })
@ApiResponse({ status: 200, type: MessageResponseDto })
delete(@Param('id') id: string) { delete(@Param('id') id: string) {
return this.greetingsService.delete(id); return this.greetingsService.delete(id);
} }

View File

@@ -0,0 +1,21 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class LikeDto {
@ApiProperty() id: string;
@ApiProperty() sourceProfileId: string;
@ApiProperty() targetProfileId: string;
@ApiProperty({ enum: ['like', 'dislike'] }) type: string;
@ApiProperty() createdAt: string;
}
export class MatchDto {
@ApiProperty() id: string;
@ApiProperty() profile1Id: string;
@ApiProperty() profile2Id: string;
@ApiProperty() createdAt: string;
}
export class CreateLikeResponseDto {
@ApiProperty({ type: LikeDto }) like: LikeDto;
@ApiPropertyOptional({ type: MatchDto, nullable: true }) match: MatchDto | null;
}

View File

@@ -1,8 +1,9 @@
import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common'; import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '../../common/decorators/current-user.decorator'; import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { CreateLikeDto } from './dto/create-like.dto'; import { CreateLikeDto } from './dto/create-like.dto';
import { CreateLikeResponseDto, MatchDto } from './dto/likes-response.dto';
import { LikesService } from './likes.service'; import { LikesService } from './likes.service';
@ApiTags('likes') @ApiTags('likes')
@@ -14,12 +15,14 @@ export class LikesController {
@Post() @Post()
@ApiOperation({ summary: 'Like or dislike a profile' }) @ApiOperation({ summary: 'Like or dislike a profile' })
@ApiResponse({ status: 201, type: CreateLikeResponseDto })
createLike(@CurrentUser('id') userId: string, @Body() dto: CreateLikeDto) { createLike(@CurrentUser('id') userId: string, @Body() dto: CreateLikeDto) {
return this.likesService.createLike(userId, dto); return this.likesService.createLike(userId, dto);
} }
@Get('matches') @Get('matches')
@ApiOperation({ summary: 'Get matches for a profile' }) @ApiOperation({ summary: 'Get matches for a profile' })
@ApiResponse({ status: 200, type: [MatchDto] })
getMyMatches( getMyMatches(
@CurrentUser('id') userId: string, @CurrentUser('id') userId: string,
@Query('profileId') profileId: string, @Query('profileId') profileId: string,

View File

@@ -1,9 +1,11 @@
import { Controller, Delete, Get, Param, Post, Query, Req, UseGuards } from '@nestjs/common'; import { Controller, Delete, Get, Param, Post, Query, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiConsumes, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { FastifyRequest } from 'fastify'; import { FastifyRequest } from 'fastify';
import { CurrentUser } from '../../common/decorators/current-user.decorator'; import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { MediaService } from './media.service'; import { MediaService } from './media.service';
import { MediaItemDto } from '../profiles/dto/profile-response.dto';
import { MessageResponseDto } from '../../common/dto/message-response.dto';
@ApiTags('media') @ApiTags('media')
@ApiBearerAuth() @ApiBearerAuth()
@@ -15,6 +17,7 @@ export class MediaController {
@Post('upload') @Post('upload')
@ApiOperation({ summary: 'Upload photo / video / audio to profile' }) @ApiOperation({ summary: 'Upload photo / video / audio to profile' })
@ApiConsumes('multipart/form-data') @ApiConsumes('multipart/form-data')
@ApiResponse({ status: 201, type: MediaItemDto })
async upload( async upload(
@CurrentUser('id') userId: string, @CurrentUser('id') userId: string,
@Param('profileId') profileId: string, @Param('profileId') profileId: string,
@@ -34,12 +37,14 @@ export class MediaController {
@Get() @Get()
@ApiOperation({ summary: 'Get all media for a profile' }) @ApiOperation({ summary: 'Get all media for a profile' })
@ApiResponse({ status: 200, type: [MediaItemDto] })
getMedia(@Param('profileId') profileId: string) { getMedia(@Param('profileId') profileId: string) {
return this.mediaService.getByProfileId(profileId); return this.mediaService.getByProfileId(profileId);
} }
@Delete(':mediaId') @Delete(':mediaId')
@ApiOperation({ summary: 'Delete media item' }) @ApiOperation({ summary: 'Delete media item' })
@ApiResponse({ status: 200, type: MessageResponseDto })
deleteMedia( deleteMedia(
@CurrentUser('id') userId: string, @CurrentUser('id') userId: string,
@Param('mediaId') mediaId: string, @Param('mediaId') mediaId: string,

View File

@@ -0,0 +1,46 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CityDto {
@ApiProperty() id: string;
@ApiProperty() name: string;
@ApiProperty() lat: string;
@ApiProperty() lng: string;
}
export class DistrictDto {
@ApiProperty() id: string;
@ApiProperty() cityId: string;
@ApiProperty() name: string;
}
export class TagDto {
@ApiProperty() id: string;
@ApiProperty() value: string;
}
export class MediaItemDto {
@ApiProperty() id: string;
@ApiProperty() profileId: string;
@ApiProperty() path: string;
@ApiProperty({ enum: ['photo', 'video', 'audio'] }) type: string;
@ApiProperty() sortOrder: number;
}
export class ProfileResponseDto {
@ApiProperty() id: string;
@ApiProperty() userId: string;
@ApiProperty() name: string;
@ApiProperty({ example: '1995-06-15' }) birthDate: string;
@ApiProperty({ enum: ['male', 'female'] }) gender: string;
@ApiPropertyOptional({ nullable: true }) cityId: string | null;
@ApiPropertyOptional({ nullable: true }) districtId: string | null;
@ApiPropertyOptional({ nullable: true }) description: string | null;
@ApiPropertyOptional({ nullable: true }) nation: string | null;
@ApiPropertyOptional({ nullable: true }) height: number | null;
@ApiPropertyOptional({ nullable: true }) weight: number | null;
@ApiPropertyOptional({ nullable: true }) activeChatId: string | null;
@ApiPropertyOptional({ type: CityDto, nullable: true }) city: CityDto | null;
@ApiPropertyOptional({ type: DistrictDto, nullable: true }) district: DistrictDto | null;
@ApiProperty({ type: [TagDto] }) tags: TagDto[];
@ApiProperty({ type: [MediaItemDto] }) media: MediaItemDto[];
}

View File

@@ -1,10 +1,12 @@
import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '../../common/decorators/current-user.decorator'; import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { CreateProfileDto } from './dto/create-profile.dto'; import { CreateProfileDto } from './dto/create-profile.dto';
import { UpdateProfileDto } from './dto/update-profile.dto'; import { UpdateProfileDto } from './dto/update-profile.dto';
import { ProfileResponseDto } from './dto/profile-response.dto';
import { ProfilesService } from './profiles.service'; import { ProfilesService } from './profiles.service';
import { MessageResponseDto } from '../../common/dto/message-response.dto';
@ApiTags('profiles') @ApiTags('profiles')
@ApiBearerAuth() @ApiBearerAuth()
@@ -15,18 +17,21 @@ export class ProfilesController {
@Post() @Post()
@ApiOperation({ summary: 'Create a new profile (one user can have many)' }) @ApiOperation({ summary: 'Create a new profile (one user can have many)' })
@ApiResponse({ status: 201, type: ProfileResponseDto })
create(@CurrentUser('id') userId: string, @Body() dto: CreateProfileDto) { create(@CurrentUser('id') userId: string, @Body() dto: CreateProfileDto) {
return this.profilesService.create(userId, dto); return this.profilesService.create(userId, dto);
} }
@Get('my') @Get('my')
@ApiOperation({ summary: 'Get all my profiles' }) @ApiOperation({ summary: 'Get all my profiles' })
@ApiResponse({ status: 200, type: [ProfileResponseDto] })
getMyProfiles(@CurrentUser('id') userId: string) { getMyProfiles(@CurrentUser('id') userId: string) {
return this.profilesService.findAllByUserId(userId); return this.profilesService.findAllByUserId(userId);
} }
@Put(':profileId') @Put(':profileId')
@ApiOperation({ summary: 'Update profile by ID (must be owner)' }) @ApiOperation({ summary: 'Update profile by ID (must be owner)' })
@ApiResponse({ status: 200, type: ProfileResponseDto })
update( update(
@CurrentUser('id') userId: string, @CurrentUser('id') userId: string,
@Param('profileId') profileId: string, @Param('profileId') profileId: string,
@@ -37,12 +42,14 @@ export class ProfilesController {
@Get(':profileId') @Get(':profileId')
@ApiOperation({ summary: 'Get profile by ID' }) @ApiOperation({ summary: 'Get profile by ID' })
@ApiResponse({ status: 200, type: ProfileResponseDto })
findOne(@Param('profileId') profileId: string) { findOne(@Param('profileId') profileId: string) {
return this.profilesService.findByProfileId(profileId); return this.profilesService.findByProfileId(profileId);
} }
@Delete(':profileId') @Delete(':profileId')
@ApiOperation({ summary: 'Delete profile (must be owner)' }) @ApiOperation({ summary: 'Delete profile (must be owner)' })
@ApiResponse({ status: 200, type: MessageResponseDto })
delete(@CurrentUser('id') userId: string, @Param('profileId') profileId: string) { delete(@CurrentUser('id') userId: string, @Param('profileId') profileId: string) {
return this.profilesService.delete(userId, profileId); return this.profilesService.delete(userId, profileId);
} }

View File

@@ -0,0 +1,9 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class ReportDto {
@ApiProperty() id: string;
@ApiProperty() sourceProfileId: string;
@ApiProperty() entityId: string;
@ApiProperty({ enum: ['profile', 'message'] }) entityType: string;
@ApiPropertyOptional({ nullable: true }) description: string | null;
}

View File

@@ -1,10 +1,11 @@
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common'; import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '../../common/decorators/current-user.decorator'; import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { Roles } from '../../common/decorators/roles.decorator'; import { Roles } from '../../common/decorators/roles.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RolesGuard } from '../../common/guards/roles.guard'; import { RolesGuard } from '../../common/guards/roles.guard';
import { CreateReportDto } from './dto/create-report.dto'; import { CreateReportDto } from './dto/create-report.dto';
import { ReportDto } from './dto/report-response.dto';
import { ReportsService } from './reports.service'; import { ReportsService } from './reports.service';
@ApiTags('reports') @ApiTags('reports')
@@ -16,6 +17,7 @@ export class ReportsController {
@Post() @Post()
@ApiOperation({ summary: 'Submit a report' }) @ApiOperation({ summary: 'Submit a report' })
@ApiResponse({ status: 201, type: ReportDto })
create(@CurrentUser('id') userId: string, @Body() dto: CreateReportDto) { create(@CurrentUser('id') userId: string, @Body() dto: CreateReportDto) {
return this.reportsService.create(userId, dto); return this.reportsService.create(userId, dto);
} }
@@ -24,6 +26,7 @@ export class ReportsController {
@Roles('admin', 'moderator') @Roles('admin', 'moderator')
@UseGuards(RolesGuard) @UseGuards(RolesGuard)
@ApiOperation({ summary: 'Get all reports (admin/moderator)' }) @ApiOperation({ summary: 'Get all reports (admin/moderator)' })
@ApiResponse({ status: 200, type: [ReportDto] })
getAll() { getAll() {
return this.reportsService.getAll(); return this.reportsService.getAll();
} }

View File

@@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
export class TagResponseDto {
@ApiProperty() id: string;
@ApiProperty() value: string;
}

View File

@@ -1,10 +1,12 @@
import { Body, Controller, Delete, Get, Param, Post, UseGuards } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { Public } from '../../common/decorators/public.decorator'; import { Public } from '../../common/decorators/public.decorator';
import { Roles } from '../../common/decorators/roles.decorator'; import { Roles } from '../../common/decorators/roles.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RolesGuard } from '../../common/guards/roles.guard'; import { RolesGuard } from '../../common/guards/roles.guard';
import { TagsService } from './tags.service'; import { TagsService } from './tags.service';
import { TagResponseDto } from './dto/tag-response.dto';
import { MessageResponseDto } from '../../common/dto/message-response.dto';
@ApiTags('tags') @ApiTags('tags')
@Controller('tags') @Controller('tags')
@@ -14,6 +16,7 @@ export class TagsController {
@Public() @Public()
@Get() @Get()
@ApiOperation({ summary: 'Get all tags' }) @ApiOperation({ summary: 'Get all tags' })
@ApiResponse({ status: 200, type: [TagResponseDto] })
findAll() { findAll() {
return this.tagsService.findAll(); return this.tagsService.findAll();
} }
@@ -23,6 +26,7 @@ export class TagsController {
@Roles('admin') @Roles('admin')
@Post() @Post()
@ApiOperation({ summary: 'Create tag (admin only)' }) @ApiOperation({ summary: 'Create tag (admin only)' })
@ApiResponse({ status: 201, type: TagResponseDto })
create(@Body('value') value: string) { create(@Body('value') value: string) {
return this.tagsService.create(value); return this.tagsService.create(value);
} }
@@ -32,6 +36,7 @@ export class TagsController {
@Roles('admin') @Roles('admin')
@Delete(':id') @Delete(':id')
@ApiOperation({ summary: 'Delete tag (admin only)' }) @ApiOperation({ summary: 'Delete tag (admin only)' })
@ApiResponse({ status: 200, type: MessageResponseDto })
delete(@Param('id') id: string) { delete(@Param('id') id: string) {
return this.tagsService.delete(id); return this.tagsService.delete(id);
} }

View File

@@ -0,0 +1,27 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class RoleDto {
@ApiProperty() id: string;
@ApiProperty() name: string;
}
export class ProfileSummaryDto {
@ApiProperty() id: string;
@ApiProperty() name: string;
@ApiProperty({ enum: ['male', 'female'] }) gender: string;
}
export class UserResponseDto {
@ApiProperty() id: string;
@ApiProperty() phone: string;
@ApiProperty({ enum: ['active', 'banned', 'pending'] }) status: string;
@ApiPropertyOptional({ nullable: true }) roleId: string | null;
@ApiPropertyOptional({ nullable: true }) tariffId: string | null;
@ApiPropertyOptional({ nullable: true }) paymentId: string | null;
@ApiPropertyOptional({ nullable: true }) fcmToken: string | null;
}
export class MeResponseDto extends UserResponseDto {
@ApiPropertyOptional({ type: RoleDto, nullable: true }) role: RoleDto | null;
@ApiProperty({ type: [ProfileSummaryDto] }) profiles: ProfileSummaryDto[];
}

View File

@@ -1,10 +1,12 @@
import { Controller, Get, Param, Patch, UseGuards } from '@nestjs/common'; import { Controller, Get, Param, Patch, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '../../common/decorators/current-user.decorator'; import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { Roles } from '../../common/decorators/roles.decorator'; import { Roles } from '../../common/decorators/roles.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RolesGuard } from '../../common/guards/roles.guard'; import { RolesGuard } from '../../common/guards/roles.guard';
import { UsersService } from './users.service'; import { UsersService } from './users.service';
import { MeResponseDto, UserResponseDto } from './dto/user-response.dto';
import { MessageResponseDto } from '../../common/dto/message-response.dto';
@ApiTags('users') @ApiTags('users')
@ApiBearerAuth() @ApiBearerAuth()
@@ -15,12 +17,14 @@ export class UsersController {
@Get('me') @Get('me')
@ApiOperation({ summary: 'Get current user with profile list' }) @ApiOperation({ summary: 'Get current user with profile list' })
@ApiResponse({ status: 200, type: MeResponseDto })
getMe(@CurrentUser('id') userId: string) { getMe(@CurrentUser('id') userId: string) {
return this.usersService.getMe(userId); return this.usersService.getMe(userId);
} }
@Get(':id') @Get(':id')
@ApiOperation({ summary: 'Get user by ID' }) @ApiOperation({ summary: 'Get user by ID' })
@ApiResponse({ status: 200, type: UserResponseDto })
findOne(@Param('id') id: string) { findOne(@Param('id') id: string) {
return this.usersService.findById(id); return this.usersService.findById(id);
} }
@@ -29,6 +33,7 @@ export class UsersController {
@Roles('admin', 'moderator') @Roles('admin', 'moderator')
@UseGuards(RolesGuard) @UseGuards(RolesGuard)
@ApiOperation({ summary: 'Ban user' }) @ApiOperation({ summary: 'Ban user' })
@ApiResponse({ status: 200, type: MessageResponseDto })
ban(@Param('id') id: string) { ban(@Param('id') id: string) {
return this.usersService.banUser(id); return this.usersService.banUser(id);
} }
@@ -37,6 +42,7 @@ export class UsersController {
@Roles('admin', 'moderator') @Roles('admin', 'moderator')
@UseGuards(RolesGuard) @UseGuards(RolesGuard)
@ApiOperation({ summary: 'Activate user' }) @ApiOperation({ summary: 'Activate user' })
@ApiResponse({ status: 200, type: MessageResponseDto })
activate(@Param('id') id: string) { activate(@Param('id') id: string) {
return this.usersService.activateUser(id); return this.usersService.activateUser(id);
} }