diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 20ebb06..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dbe9c82 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode/ \ No newline at end of file diff --git a/Documentation/Swagger/cardly.yaml b/Documentation/Swagger/cardly.yaml index 0e9b37b..84fd093 100644 --- a/Documentation/Swagger/cardly.yaml +++ b/Documentation/Swagger/cardly.yaml @@ -1,6 +1,6 @@ openapi: 3.0.0 info: - title: Cadrly API + title: Cardly API version: 0.1.0 description: Приложение для обмена коллекционными карточками @@ -45,20 +45,20 @@ paths: email: type: string format: email - example: "user@example.com" + example: 'user@example.com' description: Email пользователя username: type: string minLength: 3 maxLength: 25 - example: "Коллекционер_123" - description: "Имя пользователя (3-25 символов)" + example: 'Коллекционер_123' + description: 'Имя пользователя (3-25 символов)' password: type: string format: password minLength: 8 - example: "Password123!" - description: "Пароль (минимум 8 символов)" + example: 'Password123!' + description: 'Пароль (минимум 8 символов)' required: [email, username, password] responses: 200: @@ -70,10 +70,10 @@ paths: properties: tempToken: type: string - example: "temp_abc123" + example: 'temp_abc123' message: type: string - example: "Код подтверждения отправлен на email" + example: 'Код подтверждения отправлен на email' 400: description: Неверный формат данных content: @@ -83,7 +83,7 @@ paths: properties: error: type: string - example: "Некорректный формат введенных данных" + example: 'Некорректный формат введенных данных' 409: description: Пользователь уже существует content: @@ -93,7 +93,7 @@ paths: properties: error: type: string - example: "Пользователь с таким email уже существует" + example: 'Пользователь с таким email уже существует' 500: $ref: '#/components/responses/ServerError' @@ -114,13 +114,13 @@ paths: properties: tempToken: type: string - example: "temp_abc123" - description: "Временный токен из ответа регистрации" + example: 'temp_abc123' + description: 'Временный токен из ответа регистрации' code: type: string pattern: "^\\d{6}$" - example: "123456" - description: "6-значный код подтверждения" + example: '123456' + description: '6-значный код подтверждения' required: [tempToken, code] responses: 200: @@ -129,7 +129,7 @@ paths: Set-Cookie: schema: type: string - example: "session_id=abcde12345; HttpOnly; Path=/; Secure" + example: 'session_id=abcde12345; HttpOnly; Path=/; Secure' content: application/json: schema: @@ -143,7 +143,7 @@ paths: properties: error: type: string - example: "Неверный код подтверждения" + example: 'Неверный код подтверждения' canRetry: type: boolean example: true @@ -156,7 +156,7 @@ paths: properties: error: type: string - example: "Код устарел, запросите новый" + example: 'Код устарел, запросите новый' 500: $ref: '#/components/responses/ServerError' @@ -175,7 +175,7 @@ paths: properties: tempToken: type: string - example: "temp_abc123" + example: 'temp_abc123' required: [tempToken] responses: 200: @@ -187,12 +187,12 @@ paths: properties: message: type: string - example: "Новый код подтверждения отправлен" + example: 'Новый код подтверждения отправлен' 400: $ref: '#/components/responses/BadRequest' 500: $ref: '#/components/responses/ServerError' - + /auth/login: post: tags: [Authentication] @@ -211,11 +211,11 @@ paths: email: type: string format: email - example: "user@example.com" + example: 'user@example.com' password: type: string format: password - example: "SecurePass123!" + example: 'SecurePass123!' required: [email, password] responses: 200: @@ -224,7 +224,7 @@ paths: Set-Cookie: schema: type: string - example: "session_id=abcde12345; HttpOnly; Path=/; Secure" + example: 'session_id=abcde12345; HttpOnly; Path=/; Secure' content: application/json: schema: @@ -238,8 +238,8 @@ paths: properties: error: type: string - example: "Неверный фортмат email" - + example: 'Неверный формат email' + 401: description: Неверные учетные данные content: @@ -249,10 +249,10 @@ paths: properties: error: type: string - example: "Неверный email или пароль" + example: 'Неверный email или пароль' 500: $ref: '#/components/responses/ServerError' - + /auth/forgot-password: post: tags: [Authentication] @@ -269,7 +269,7 @@ paths: email: type: string format: email - example: "user@example.com" + example: 'user@example.com' required: [email] responses: 200: @@ -281,10 +281,10 @@ paths: properties: resetToken: type: string - example: "reset_xyz789" + example: 'reset_xyz789' message: type: string - example: "Код подтверждения отправлен на email" + example: 'Код подтверждения отправлен на email' 400: description: Неверный формат данных content: @@ -294,7 +294,7 @@ paths: properties: error: type: string - example: "Некорректный формат emaila" + example: 'Некорректный формат emaila' 404: description: Пользователь с указанным email не найден content: @@ -304,10 +304,10 @@ paths: properties: error: type: string - example: "Пользователь с таким email не зарегистрирован" + example: 'Пользователь с таким email не зарегистрирован' 500: $ref: '#/components/responses/ServerError' - + /auth/reset-password: post: tags: [Authentication] @@ -323,16 +323,16 @@ paths: properties: resetToken: type: string - example: "reset_xyz789" + example: 'reset_xyz789' code: type: string pattern: "^\\d{6}$" - example: "654321" + example: '654321' newPassword: type: string format: password minLength: 8 - example: "NewSecurePass123!" + example: 'NewSecurePass123!' required: [resetToken, code, newPassword] responses: 200: @@ -344,7 +344,7 @@ paths: properties: message: type: string - example: "Пароль успешно изменен" + example: 'Пароль успешно изменен' 400: description: Неверный код или токен content: @@ -354,10 +354,10 @@ paths: properties: error: type: string - example: "Неверный код подтверждения" + example: 'Неверный код подтверждения' 500: $ref: '#/components/responses/ServerError' - + /auth/check: get: tags: [Authentication] @@ -386,10 +386,10 @@ paths: example: false message: type: string - example: "Требуется авторизация" + example: 'Требуется авторизация' 500: $ref: '#/components/responses/ServerError' - + /auth/refresh: post: tags: [Authentication] @@ -406,7 +406,7 @@ paths: Set-Cookie: schema: type: string - example: "session_id=new_abcde12345; HttpOnly; Path=/; Secure" + example: 'session_id=new_abcde12345; HttpOnly; Path=/; Secure' content: application/json: schema: @@ -415,7 +415,7 @@ paths: $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' - + /auth/logout: post: tags: [Authentication] @@ -432,10 +432,10 @@ paths: Set-Cookie: schema: type: string - example: "session_id=; HttpOnly; Path=/; Secure; Expires=Thu, 01 Jan 1970 00:00:00 GMT" + example: 'session_id=; HttpOnly; Path=/; Secure; Expires=Thu, 01 Jan 1970 00:00:00 GMT' 500: $ref: '#/components/responses/ServerError' - + /profile: get: tags: [Profile] @@ -455,35 +455,35 @@ paths: type: object properties: userStats: - $ref: "#/components/schemas/UserStats" + $ref: '#/components/schemas/UserStats' favoriteCards: type: array items: - $ref: "#/components/schemas/Card" + $ref: '#/components/schemas/Card' maxItems: 5 favoriteAchievements: type: array items: - $ref: "#/components/schemas/Achievement" + $ref: '#/components/schemas/Achievement' maxItems: 4 - user_ID: + user_id: type: integer example: 123 - description: "ID пользователя" + description: 'ID пользователя' username: type: string - example: "Коллекционер_123" - description: "Имя пользователя" + example: 'Коллекционер_123' + description: 'Имя пользователя' avatar_url: type: string format: url - example: "https://example.com/avatars/user123.jpg" - description: "Ссылка на аватар пользователя" + example: 'https://example.com/avatars/user123.jpg' + description: 'Ссылка на аватар пользователя' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' - + /profile/achievements: get: tags: [Profile] @@ -503,10 +503,10 @@ paths: achievements: type: array items: - $ref: "#/components/schemas/Achievement" + $ref: '#/components/schemas/Achievement' 500: $ref: '#/components/responses/ServerError' - + /profile/achievements/favorites/count: get: tags: [Profile] @@ -532,7 +532,7 @@ paths: example: true 500: $ref: '#/components/responses/ServerError' - + /profile/achievements/{achievement_ID}/favorite-add: post: tags: [Profile] @@ -554,7 +554,7 @@ paths: description: Невозможно добавить в избранное (лимит достигнут) 500: $ref: '#/components/responses/ServerError' - + /profile/achievements/{achievement_ID}/favorite-delete: delete: tags: [Profile] @@ -574,7 +574,7 @@ paths: description: Успешно удалено из избранного 500: $ref: '#/components/responses/ServerError' - + /profile/settings: get: tags: [Profile] @@ -589,10 +589,10 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/UserSettings" + $ref: '#/components/schemas/UserSettings' 500: $ref: '#/components/responses/ServerError' - + /profile/settings-change: put: tags: [Profile] @@ -606,18 +606,18 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/UserSettings" + $ref: '#/components/schemas/UserSettings' responses: 200: description: Настройки обновлены content: application/json: schema: - $ref: "#/components/schemas/UserSettings" + $ref: '#/components/schemas/UserSettings' 500: $ref: '#/components/responses/ServerError' - - /other-profile/{user_ID}: + + /other-profile/{user_id}: get: tags: [OtherProfile] summary: Получение данных профиля другого пользователя @@ -627,7 +627,7 @@ paths: - Проверяет настройки приватности инвентаря parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -640,30 +640,30 @@ paths: type: object properties: userStats: - $ref: "#/components/schemas/UserStats" + $ref: '#/components/schemas/UserStats' favoriteCards: type: array items: - $ref: "#/components/schemas/Card" + $ref: '#/components/schemas/Card' maxItems: 5 favoriteAchievements: type: array items: - $ref: "#/components/schemas/Achievement" + $ref: '#/components/schemas/Achievement' maxItems: 4 inventoryVisible: type: boolean example: true id: type: integer - example: "456" + example: '456' username: type: string - example: "Игрок_456" + example: 'Игрок_456' 500: $ref: '#/components/responses/ServerError' - - /other-profile/{user_ID}/achievements: + + /other-profile/{user_id}/achievements: get: tags: [OtherProfile] summary: Получение списка достижений другого пользователя @@ -671,7 +671,7 @@ paths: Возвращает полный список достижений с текущим статусом выполнения parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -686,11 +686,11 @@ paths: achievements: type: array items: - $ref: "#/components/schemas/Achievement" + $ref: '#/components/schemas/Achievement' 500: $ref: '#/components/responses/ServerError' - /other-profile/{user_ID}/inventory: + /other-profile/{user_id}/inventory: get: tags: [OtherProfile] summary: Просмотр инвентаря другого пользователя @@ -698,7 +698,7 @@ paths: Возвращает инвентарь пользователя, если он не скрыт в настройках parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -713,11 +713,11 @@ paths: cards: type: array items: - $ref: "#/components/schemas/Card" + $ref: '#/components/schemas/Card' collections: type: array items: - $ref: "#/components/schemas/Collection" + $ref: '#/components/schemas/Collection' 403: description: Инвентарь скрыт content: @@ -727,10 +727,10 @@ paths: properties: error: type: string - example: "Пользователь скрыл свой инвентарь" + example: 'Пользователь скрыл свой инвентарь' 500: $ref: '#/components/responses/ServerError' - + /other-profile/inventory/sort: post: tags: [OtherProfile] @@ -740,7 +740,7 @@ paths: (по редкости, по коллекциям, механизм сортировки подробнее описан в инвентаре) parameters: - in: query - name: user_ID + name: user_id required: true schema: type: integer @@ -765,10 +765,10 @@ paths: inventory: type: array items: - $ref: "#/components/schemas/Card" + $ref: '#/components/schemas/Card' 500: $ref: '#/components/responses/ServerError' - + /other-profile/card/{card_ID}/view: get: tags: [OtherProfile] @@ -796,10 +796,10 @@ paths: type: object properties: card: - $ref: "#/components/schemas/Card" + $ref: '#/components/schemas/Card' 500: $ref: '#/components/responses/ServerError' - + /other-profile/card/{card_ID}/initiate-trade: post: tags: [OtherProfile] @@ -830,7 +830,7 @@ paths: properties: redirectUrl: type: string - example: "/trades/create?requested_card=123&owner=456" + example: '/trades/create?requested_card=123&owner=456' 400: description: Невозможно предложить обмен content: @@ -840,7 +840,7 @@ paths: properties: error: type: string - example: "Недостаточно карт для обмена" + example: 'Недостаточно карт для обмена' 409: description: Конфликт при попытке обмена content: @@ -850,25 +850,25 @@ paths: properties: error: type: string - example: "Невозможно предложить обмен - у владельца только один экземпляр этой карты" + example: 'Невозможно предложить обмен - у владельца только один экземпляр этой карты' cardQuantity: type: integer example: 1 - description: "Количество экземпляров карты у владельца" + description: 'Количество экземпляров карты у владельца' 500: $ref: '#/components/responses/ServerError' - - /other-profile/{user_ID}/report: + + /other-profile/{user_id}/report: post: tags: [OtherProfile] summary: Подача жалобы на пользователя description: | - Перенаправляет на страницу создания жалобы. + Перенаправляет на страницу создания жалобы. security: - bearerAuth: [] parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -882,15 +882,15 @@ paths: properties: reason: type: string - enum: ["Неуместный никнейм", "Неуместный аватар", "Другое"] - example: "Неуместный никнейм" - description: "Причина жалобы" + enum: ['Неуместный никнейм', 'Неуместный аватар', 'Другое'] + example: 'Неуместный никнейм' + description: 'Причина жалобы' comment: type: string maxLength: 500 nullable: true - example: "Никнейм содержит нецензурные слова" - description: "Дополнительные комментарии" + example: 'Никнейм содержит нецензурные слова' + description: 'Дополнительные комментарии' required: [reason] responses: 200: @@ -902,12 +902,12 @@ paths: properties: redirectUrl: type: string - example: "/report/create?reported_user=123" + example: '/report/create?reported_user=123' reportDraft: - $ref: "#/components/schemas/Report" + $ref: '#/components/schemas/Report' 500: $ref: '#/components/responses/ServerError' - + /inventory: get: tags: [Inventory] @@ -916,7 +916,7 @@ paths: Загружает данные инвентаря пользователя. - Проверяет авторизацию пользователя - Если пользователь не авторизован - возвращает ошибку 401 - - Если авторизован - возвращает список его карт, список избранных карт и баланс + - Если авторизован - возвращает список его карт, список избранных карт и баланс security: - bearerAuth: [] responses: @@ -930,24 +930,24 @@ paths: inventory: type: array items: - $ref: "#/components/schemas/Card" + $ref: '#/components/schemas/Card' favoriteCards: type: array items: type: integer - description: "ID избранных карт" + description: 'ID избранных карт' collections: type: array items: - $ref: "#/components/schemas/Collection" + $ref: '#/components/schemas/Collection' balance: type: integer example: 2500 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' - + /inventory/card/{card_ID}/quantity: get: tags: [Inventory] @@ -1008,7 +1008,7 @@ paths: example: true 500: $ref: '#/components/responses/ServerError' - + /inventory/card/{card_ID}/favorite: get: tags: [Inventory] @@ -1079,7 +1079,7 @@ paths: description: Карта успешно удалена из избранного 500: $ref: '#/components/responses/ServerError' - + /inventory/sort: post: tags: [Inventory] @@ -1114,10 +1114,10 @@ paths: cards: type: array items: - $ref: "#/components/schemas/Card" + $ref: '#/components/schemas/Card' 500: $ref: '#/components/responses/ServerError' - + /inventory/destroy: post: tags: [Inventory] @@ -1137,7 +1137,7 @@ paths: properties: card_ID: type: integer - minimum: 1 + minimum: 1 example: 501 required: [card_ID] responses: @@ -1150,7 +1150,7 @@ paths: properties: newBalance: type: integer - example: 2550 + example: 2550 destroyedCardId: type: integer example: 501 @@ -1182,10 +1182,10 @@ paths: isOnTrade: type: boolean example: true - description: "Выставлена ли карта на обмен" + description: 'Выставлена ли карта на обмен' 500: $ref: '#/components/responses/ServerError' - + /inventory/put-on-trade: post: tags: [Inventory] @@ -1205,7 +1205,7 @@ paths: properties: card_ID: type: integer - minimum: 1 + minimum: 1 example: 501 required: [card_ID] responses: @@ -1218,12 +1218,12 @@ paths: properties: redirectUrl: type: string - example: "/trades/create?requested_card=123&owner=456" + example: '/trades/create?requested_card=123&owner=456' 400: description: Невозможно выставить карту на обмен 500: $ref: '#/components/responses/ServerError' - + /inventory/card/{card_ID}/trade-cancel: post: tags: [Inventory] @@ -1247,9 +1247,9 @@ paths: properties: message: type: string - example: "Карта успешно снята с обмена" + example: 'Карта успешно снята с обмена' 500: - $ref: '#/components/responses/ServerError' + $ref: '#/components/responses/ServerError' /home/search: get: tags: [Home] @@ -1270,7 +1270,7 @@ paths: schema: type: array items: - $ref: "#/components/schemas/User" + $ref: '#/components/schemas/User' 400: description: Неверный запрос content: @@ -1280,7 +1280,7 @@ paths: properties: error: type: string - example: "Введите минимум 3 символа для поиска" + example: 'Введите минимум 3 символа для поиска' 404: description: Пользователь не найден content: @@ -1290,10 +1290,10 @@ paths: properties: error: type: string - example: "Пользователь с введеными данными не найден" + example: 'Пользователь с введеными данными не найден' 500: $ref: '#/components/responses/ServerError' - + /home/notifications: get: tags: [Home] @@ -1310,12 +1310,12 @@ paths: schema: type: array items: - $ref: "#/components/schemas/Notification" + $ref: '#/components/schemas/Notification' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' - + /home/notifications/{notification_ID}: get: tags: [Home] @@ -1331,19 +1331,19 @@ paths: schema: type: integer example: 123 - description: "ID уведомления" + description: 'ID уведомления' responses: 200: description: Детальная информация об уведомлении content: application/json: schema: - $ref: "#/components/schemas/Notification" + $ref: '#/components/schemas/Notification' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: - $ref: '#/components/responses/ServerError' - + $ref: '#/components/responses/ServerError' + /home/notifications/{notification_ID}/navigate: post: tags: [Home] @@ -1368,7 +1368,7 @@ paths: properties: redirectUrl: type: string - example: "/trades/123" + example: '/trades/123' 404: description: Предложение обмена не найдено content: @@ -1378,7 +1378,7 @@ paths: properties: error: type: string - example: "Данное предложение обмена уже не существует" + example: 'Данное предложение обмена уже не существует' 500: $ref: '#/components/responses/ServerError' /home/generate-card/themes: @@ -1397,7 +1397,7 @@ paths: schema: type: array items: - $ref: "#/components/schemas/Theme" + $ref: '#/components/schemas/Theme' 500: $ref: '#/components/responses/ServerError' /home/generate-card: @@ -1418,7 +1418,7 @@ paths: theme_ID: type: integer example: 901 - description: "ID выбранной темы" + description: 'ID выбранной темы' required: [theme_ID] responses: 200: @@ -1429,7 +1429,7 @@ paths: type: object properties: card: - $ref: "#/components/schemas/Card" + $ref: '#/components/schemas/Card' newBalance: type: integer example: 1500 @@ -1442,7 +1442,7 @@ paths: properties: error: type: string - example: "Недостаточно средств для генерации карты" + example: 'Недостаточно средств для генерации карты' requiredAmount: type: integer example: 1000 @@ -1462,7 +1462,7 @@ paths: schema: type: array items: - $ref: "#/components/schemas/News" + $ref: '#/components/schemas/News' 404: description: Новостей не найдено content: @@ -1472,7 +1472,7 @@ paths: properties: error: type: string - example: "Новостей пока нет" + example: 'Новостей пока нет' 500: $ref: '#/components/responses/ServerError' /home/news/{news_ID}: @@ -1493,7 +1493,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/News" + $ref: '#/components/schemas/News' 500: $ref: '#/components/responses/ServerError' /home/quests: @@ -1515,13 +1515,13 @@ paths: dailyQuests: type: array items: - $ref: "#/components/schemas/Quest" + $ref: '#/components/schemas/Quest' weeklyQuests: type: array items: - $ref: "#/components/schemas/Quest" + $ref: '#/components/schemas/Quest' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' /home/quests/{quest_ID}/change-status: @@ -1539,7 +1539,7 @@ paths: schema: type: integer example: 123 - description: "ID квеста" + description: 'ID квеста' responses: 200: description: Статус квеста успешно изменен @@ -1549,15 +1549,15 @@ paths: type: object properties: quest: - $ref: "#/components/schemas/Quest" + $ref: '#/components/schemas/Quest' message: type: string - example: "Статус квеста успешно изменен" + example: 'Статус квеста успешно изменен' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' - + /home/quests/claim-reward: post: tags: [Home] @@ -1576,7 +1576,7 @@ paths: questType: type: string enum: [daily, weekly] - example: "daily" + example: 'daily' required: [questType] responses: 200: @@ -1592,7 +1592,7 @@ paths: receivedPacks: type: array items: - $ref: "#/components/schemas/Pack" + $ref: '#/components/schemas/Pack' newBalance: type: integer example: 2000 @@ -1605,7 +1605,7 @@ paths: properties: error: type: string - example: "Выполнены не все квесты, необходимые для получения награды" + example: 'Выполнены не все квесты, необходимые для получения награды' 500: $ref: '#/components/responses/ServerError' /trades: @@ -1629,7 +1629,7 @@ paths: schema: type: array items: - $ref: "#/components/schemas/Trade" + $ref: '#/components/schemas/Trade' 500: $ref: '#/components/responses/ServerError' /trades/{trade_id}: @@ -1650,7 +1650,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/Trade" + $ref: '#/components/schemas/Trade' 404: description: Предложение обмена не найдено content: @@ -1660,7 +1660,7 @@ paths: properties: error: type: string - example: "Предложение обмена не найдено" + example: 'Предложение обмена не найдено' 500: $ref: '#/components/responses/ServerError' /trades/initiate: @@ -1685,7 +1685,7 @@ paths: requestedCardId: type: array items: - type: integer + type: integer description: ID карт, на которые пользователь готов совершить "быстрый обмен" example: [100, 200] required: [receivingUserId, requestedCardId, offeredCardId] @@ -1695,7 +1695,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/Trade" + $ref: '#/components/schemas/Trade' 400: description: Невозможно создать предложение обмена content: @@ -1705,9 +1705,9 @@ paths: properties: error: type: string - example: "Недостаточно карт для обмена" + example: 'Недостаточно карт для обмена' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' /trades/my: @@ -1725,7 +1725,7 @@ paths: schema: type: integer example: 123 - description: "ID текущего пользователя" + description: 'ID текущего пользователя' responses: 200: description: Список моих обменов @@ -1734,9 +1734,9 @@ paths: schema: type: array items: - $ref: "#/components/schemas/Trade" + $ref: '#/components/schemas/Trade' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' /trades/{trade_id}/accept: @@ -1763,10 +1763,10 @@ paths: properties: message: type: string - example: "Обмен успешно завершен" - + example: 'Обмен успешно завершен' + 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 403: description: Нет прав для принятия этого обмена content: @@ -1776,10 +1776,10 @@ paths: properties: error: type: string - example: "Вы не можете принять этот обмен" + example: 'Вы не можете принять этот обмен' 500: $ref: '#/components/responses/ServerError' - + /trades/{trade_id}/reject: post: tags: [Trades] @@ -1804,12 +1804,12 @@ paths: properties: message: type: string - example: "Обмен отклонен" + example: 'Обмен отклонен' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' - + /trades/{trade_id}/cancel: post: tags: [Trades] @@ -1834,12 +1834,12 @@ paths: properties: message: type: string - example: "Обмен отменен" + example: 'Обмен отменен' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' - + /shop/packs: get: tags: [Shop] @@ -1854,7 +1854,7 @@ paths: schema: type: array items: - $ref: "#/components/schemas/Pack" + $ref: '#/components/schemas/Pack' 500: $ref: '#/components/responses/ServerError' @@ -1876,7 +1876,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/Pack" + $ref: '#/components/schemas/Pack' 500: $ref: '#/components/responses/ServerError' /shop/packs/{pack_id}/buy: @@ -1906,11 +1906,11 @@ paths: receivedCards: type: array items: - $ref: "#/components/schemas/Card" + $ref: '#/components/schemas/Card' newBalance: type: integer 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 402: description: Недостаточно средств content: @@ -1920,7 +1920,7 @@ paths: properties: error: type: string - example: "Недостаточно средств на балансе" + example: 'Недостаточно средств на балансе' requiredAmount: type: integer example: 1000 @@ -1942,7 +1942,7 @@ paths: schema: type: integer example: 1 - description: "ID набора карт" + description: 'ID набора карт' responses: 200: description: Набор успешно открыт @@ -1954,17 +1954,17 @@ paths: receivedCards: type: array items: - $ref: "#/components/schemas/Card" - description: "Карты, полученные из набора" + $ref: '#/components/schemas/Card' + description: 'Карты, полученные из набора' newCardsAdded: type: boolean example: true - description: "Были ли карты успешно добавлены в инвентарь" + description: 'Были ли карты успешно добавлены в инвентарь' 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' - + /shop/coins/offers: get: tags: [Shop] @@ -1979,7 +1979,7 @@ paths: schema: type: array items: - $ref: "#/components/schemas/CoinOffer" + $ref: '#/components/schemas/CoinOffer' 500: $ref: '#/components/responses/ServerError' /shop/coins/offers/{offer_id}: @@ -2000,7 +2000,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/CoinOffer" + $ref: '#/components/schemas/CoinOffer' 500: $ref: '#/components/responses/ServerError' /shop/coins/offers/{offer_id}/purchase: @@ -2027,7 +2027,7 @@ paths: redirectUrl: type: string format: uri - example: "https://cardly.ru/shop/payment-callback" + example: 'https://cardly.ru/shop/payment-callback' required: [redirectUrl] responses: 200: @@ -2041,15 +2041,15 @@ paths: type: string format: uri 401: - $ref: "#/components/responses/Unauthorized" + $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/ServerError' /shop/payments/process: post: tags: [Shop] - summary: Обработка платежа + summary: Обработка платежа description: | - обработка результата платежа от платежного шлюза + обработка результата платежа от платежного шлюза security: - bearerAuth: [] requestBody: @@ -2057,7 +2057,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/PaymentCallback" + $ref: '#/components/schemas/PaymentCallback' responses: 200: description: Платеж обработан @@ -2208,7 +2208,7 @@ paths: 403: $ref: '#/components/responses/Forbidden' 500: - $ref: '#/components/responses/ServerError' + $ref: '#/components/responses/ServerError' delete: tags: [Admin] summary: Удаление коллекцию @@ -2287,7 +2287,7 @@ paths: 403: $ref: '#/components/responses/Forbidden' 500: - $ref: '#/components/responses/ServerError' + $ref: '#/components/responses/ServerError' delete: tags: [Admin] summary: Удаление набор @@ -2335,7 +2335,7 @@ paths: $ref: '#/components/responses/Forbidden' 500: $ref: '#/components/responses/ServerError' - + get: tags: [Admin] summary: Получение списка предложений покупки монет @@ -2357,7 +2357,7 @@ paths: $ref: '#/components/responses/Forbidden' 500: $ref: '#/components/responses/ServerError' - + /admin/coin-offers/{offer_id}: put: tags: [Admin] @@ -2389,8 +2389,8 @@ paths: 403: $ref: '#/components/responses/Forbidden' 500: - $ref: '#/components/responses/ServerError' - + $ref: '#/components/responses/ServerError' + delete: tags: [Admin] summary: Удаление предложения покупки монет @@ -2469,7 +2469,7 @@ paths: 403: $ref: '#/components/responses/Forbidden' 500: - $ref: '#/components/responses/ServerError' + $ref: '#/components/responses/ServerError' delete: tags: [Admin] summary: Удаление новости @@ -2548,7 +2548,7 @@ paths: 403: $ref: '#/components/responses/Forbidden' 500: - $ref: '#/components/responses/ServerError' + $ref: '#/components/responses/ServerError' delete: tags: [Admin] summary: Удаление достижения @@ -2627,7 +2627,7 @@ paths: properties: message: type: string - example: "Обмен успешно аннулирован" + example: 'Обмен успешно аннулирован' 401: $ref: '#/components/responses/Unauthorized' 403: @@ -2704,20 +2704,20 @@ paths: status: type: string enum: [pending, reviewed, resolved] - example: "resolved" + example: 'resolved' action: type: string enum: [none, warning, temporary_ban, permanent_ban] - example: "temporary_ban" + example: 'temporary_ban' banDurationDays: type: integer nullable: true example: 7 - description: "Длительность бана в днях (только для temporary_ban)" + description: 'Длительность бана в днях (только для temporary_ban)' comment: type: string nullable: true - example: "Пользователь получил временный бан на 7 дней за нарушение правил" + example: 'Пользователь получил временный бан на 7 дней за нарушение правил' required: [status, action] responses: 200: @@ -2734,7 +2734,7 @@ paths: description: Жалоба не найдена 500: $ref: '#/components/responses/ServerError' - /admin/users/{user_ID}/ban: + /admin/users/{user_id}/ban: post: tags: [Admin] summary: Блокировка пользователя @@ -2743,7 +2743,7 @@ paths: - bearerAuth: [] parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -2757,15 +2757,15 @@ paths: banType: type: string enum: [temporary, permanent] - example: "temporary" + example: 'temporary' durationDays: type: integer nullable: true example: 7 - description: "Длительность бана в днях" + description: 'Длительность бана в днях' reason: type: string - example: "Нарушение правил сообщества" + example: 'Нарушение правил сообщества' required: [banType, reason] responses: 200: @@ -2777,12 +2777,12 @@ paths: properties: message: type: string - example: "Пользователь успешно заблокирован" + example: 'Пользователь успешно заблокирован' banEndDate: type: string format: date-time nullable: true - example: "2024-06-20T12:00:00Z" + example: '2024-06-20T12:00:00Z' 401: $ref: '#/components/responses/Unauthorized' 403: @@ -2791,7 +2791,7 @@ paths: description: Пользователь не найден 500: $ref: '#/components/responses/ServerError' - /admin/users/{user_ID}/unban: + /admin/users/{user_id}/unban: post: tags: [Admin] summary: Разблокировка пользователя @@ -2800,7 +2800,7 @@ paths: - bearerAuth: [] parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -2814,7 +2814,7 @@ paths: properties: message: type: string - example: "Пользователь успешно разблокирован" + example: 'Пользователь успешно разблокирован' 401: $ref: '#/components/responses/Unauthorized' 403: @@ -2823,7 +2823,7 @@ paths: description: Пользователь не найден 500: $ref: '#/components/responses/ServerError' - /admin/users/{user_ID}/delete: + /admin/users/{user_id}/delete: delete: tags: [Admin] summary: Удаление пользователя @@ -2832,7 +2832,7 @@ paths: - bearerAuth: [] parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -2846,8 +2846,8 @@ paths: 404: description: Пользователь не найден 500: - $ref: '#/components/responses/ServerError' - /admin/users/{user_ID}/inventory/{card_ID}: + $ref: '#/components/responses/ServerError' + /admin/users/{user_id}/inventory/{card_ID}: delete: tags: [Admin] summary: Удаление карты из инвентаря пользователя @@ -2856,7 +2856,7 @@ paths: - bearerAuth: [] parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -2876,7 +2876,7 @@ paths: $ref: '#/components/responses/Forbidden' 500: $ref: '#/components/responses/ServerError' - /admin/users/{user_ID}/achievements/{achievement_ID}: + /admin/users/{user_id}/achievements/{achievement_ID}: delete: tags: [Admin] summary: Отзыв достижения у пользователя @@ -2885,7 +2885,7 @@ paths: - bearerAuth: [] parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -2905,7 +2905,7 @@ paths: $ref: '#/components/responses/Forbidden' 500: $ref: '#/components/responses/ServerError' - /admin/users/{user_ID}/quests/{quest_ID}/reset: + /admin/users/{user_id}/quests/{quest_ID}/reset: post: tags: [Admin] summary: Сброс выполнения квеста @@ -2914,7 +2914,7 @@ paths: - bearerAuth: [] parameters: - in: path - name: user_ID + name: user_id required: true schema: type: integer @@ -2948,18 +2948,18 @@ paths: properties: message: type: string - example: "Квест успешно сброшен" + example: 'Квест успешно сброшен' rewardsRemoved: type: boolean example: true 400: - description: Неверный запрос + description: Неверный запрос 401: $ref: '#/components/responses/Unauthorized' 403: $ref: '#/components/responses/Forbidden' 500: - $ref: '#/components/responses/ServerError' + $ref: '#/components/responses/ServerError' /admin/stats: get: tags: [Admin] @@ -3004,7 +3004,7 @@ paths: 403: $ref: '#/components/responses/Forbidden' 500: - $ref: '#/components/responses/ServerError' + $ref: '#/components/responses/ServerError' components: schemas: News: @@ -3013,27 +3013,28 @@ components: news_ID: type: integer example: 1 - description: "Уникальный идентификатор новости" + description: 'Уникальный идентификатор новости' title: type: string - example: "Новое обновление системы" - description: "Заголовок новости" + example: 'Новое обновление системы' + description: 'Заголовок новости' content: type: string - example: "Мы добавили новые карты в коллекцию" - description: "Содержание новости" + example: 'Мы добавили новые карты в коллекцию' + description: 'Содержание новости' pictures: type: array items: type: string format: url - example: ["https://example.com/news1.jpg", "https://example.com/news2.jpg"] - description: "Ссылки на изображения" + example: + ['https://example.com/news1.jpg', 'https://example.com/news2.jpg'] + description: 'Ссылки на изображения' datePosted: type: string format: date-time - example: "2024-05-20T14:48:00Z" - description: "Дата публикации" + example: '2024-05-20T14:48:00Z' + description: 'Дата публикации' required: - news_ID - title @@ -3046,24 +3047,24 @@ components: achievement_ID: type: integer example: 101 - description: "Уникальный идентификатор достижения" + description: 'Уникальный идентификатор достижения' name: type: string - example: "Мастер коллекционирования" - description: "Название достижения" + example: 'Мастер коллекционирования' + description: 'Название достижения' imageURL: type: string format: url - example: "https://example.com/achievements/master.png" - description: "Ссылка на изображение" + example: 'https://example.com/achievements/master.png' + description: 'Ссылка на изображение' description: type: string - example: "Соберите 10 полных коллекций" - description: "Описание достижения" + example: 'Соберите 10 полных коллекций' + description: 'Описание достижения' isUnlocked: type: boolean example: false - description: "Статус получения" + description: 'Статус получения' required: - achievement_ID - name @@ -3077,26 +3078,26 @@ components: notification_ID: type: integer example: 201 - description: "Уникальный идентификатор уведомления" + description: 'Уникальный идентификатор уведомления' user: - $ref: "#/components/schemas/User" - description: "Пользователь, которому отправлено уведомление" + $ref: '#/components/schemas/User' + description: 'Пользователь, которому отправлено уведомление' message: type: string - example: "Ваша карта была успешно обменяна" - description: "Текст уведомления" + example: 'Ваша карта была успешно обменяна' + description: 'Текст уведомления' links: type: array items: type: string format: url - example: ["https://example.com/exchange-details"] - description: "Ссылка на подробности обмена" + example: ['https://example.com/exchange-details'] + description: 'Ссылка на подробности обмена' notificationDateTime: type: string format: date-time - example: "2024-05-20T15:30:00Z" - description: "Дата и время уведомления" + example: '2024-05-20T15:30:00Z' + description: 'Дата и время уведомления' required: - notification_ID - user @@ -3109,27 +3110,27 @@ components: report_ID: type: integer example: 301 - description: "Уникальный идентификатор жалобы" + description: 'Уникальный идентификатор жалобы' reporter: - $ref: "#/components/schemas/User" - description: "Пользователь, отправивший жалобу" + $ref: '#/components/schemas/User' + description: 'Пользователь, отправивший жалобу' reportedUser: - $ref: "#/components/schemas/User" - description: "Пользователь, на которого пожаловались" + $ref: '#/components/schemas/User' + description: 'Пользователь, на которого пожаловались' reportDateTime: type: string format: date-time - example: "2024-05-20T16:45:00Z" - description: "Дата и время жалобы" + example: '2024-05-20T16:45:00Z' + description: 'Дата и время жалобы' reason: type: string - example: "Непристойный контент" - description: "непристойный никнейм" + example: 'Непристойный контент' + description: 'непристойный никнейм' status: type: string - enum: ["На рассмотрении", "Подтверждено", "Отклонено"] - example: "На расмотрении" - description: "Статус рассмотрения" + enum: ['На рассмотрении', 'Подтверждено', 'Отклонено'] + example: 'На расмотрении' + description: 'Статус рассмотрения' required: - report_ID - reporter @@ -3144,25 +3145,25 @@ components: pack_ID: type: integer example: 401 - description: "Уникальный идентификатор набора" + description: 'Уникальный идентификатор набора' name: type: string - example: "Стартовый набор" - description: "Название набора" + example: 'Стартовый набор' + description: 'Название набора' imageURL: type: string format: url - example: "https://example.com/packs/starter.png" - description: "Ссылка на изображение" + example: 'https://example.com/packs/starter.png' + description: 'Ссылка на изображение' cards: type: array items: - $ref: "#/components/schemas/Card" - description: "Карты в наборе" + $ref: '#/components/schemas/Card' + description: 'Карты в наборе' price: type: integer example: 1000 - description: "Цена" + description: 'Цена' required: - pack_ID - name @@ -3176,38 +3177,38 @@ components: card_ID: type: integer example: 501 - description: "Уникальный идентификатор карты" + description: 'Уникальный идентификатор карты' name: type: string - example: "Ледяной феникс" - description: "Название карты" + example: 'Ледяной феникс' + description: 'Название карты' imageURL: type: string format: url - example: "https://example.com/cards/pheonix.png" - description: "Ссылка на изображение" + example: 'https://example.com/cards/pheonix.png' + description: 'Ссылка на изображение' description: type: string - example: "Ледяной Феникс — это мифическое существо, воплощающее силу зимы и вечного обновления. Его перья сверкают как морозный утренний иней, а глаза светятся холодным синим светом." - description: "Описание карты" + example: 'Ледяной Феникс — это мифическое существо, воплощающее силу зимы и вечного обновления. Его перья сверкают как морозный утренний иней, а глаза светятся холодным синим светом.' + description: 'Описание карты' rarity: type: string - enum: ["Обычная", "Редкая", "Эпическая", "Легендарная", "Уникальная"] - example: "Редкая" - description: "Редкость карты" + enum: ['Обычная', 'Редкая', 'Эпическая', 'Легендарная', 'Уникальная'] + example: 'Редкая' + description: 'Редкость карты' min_price: type: integer example: 50 - description: "Минимальная цена - используется для разбора карточки" + description: 'Минимальная цена - используется для разбора карточки' theme: type: string nullable: true - example: "Мифическое существо" - description: "Тематика, на которую была сгенерирвана карта (только для уникальных карт)" + example: 'Мифическое существо' + description: 'Тематика, на которую была сгенерирвана карта (только для уникальных карт)' isGenerated: type: boolean example: false - description: "Флаг показывающий сгенерированная ли карта (true для уникальных, false для остальных)" + description: 'Флаг показывающий сгенерированная ли карта (true для уникальных, false для остальных)' required: - card_ID - name @@ -3219,65 +3220,65 @@ components: User: type: object properties: - user_ID: + user_id: type: integer example: 1 - description: "Уникальный идентификатор" + description: 'Уникальный идентификатор' username: type: string - example: "Коллекционер_123" - description: "Имя пользователя" + example: 'Коллекционер_123' + description: 'Имя пользователя' email: type: string format: email - example: "user@example.com" - description: "Электронная почта" + example: 'user@example.com' + description: 'Электронная почта' password: type: string format: password writeOnly: true - description: "Пароль" + description: 'Пароль' favoriteCards: type: array items: - $ref: "#/components/schemas/Card" - description: "Избранные карты" + $ref: '#/components/schemas/Card' + description: 'Избранные карты' onChange: type: array items: - $ref: "#/components/schemas/Card" - description: "Карты на обмен" + $ref: '#/components/schemas/Card' + description: 'Карты на обмен' inventoryCards: type: array items: - $ref: "#/components/schemas/Card" - description: "Карты в инвентаре" + $ref: '#/components/schemas/Card' + description: 'Карты в инвентаре' balance: type: integer example: 2500 - description: "Баланс" + description: 'Баланс' avatar_url: type: string format: url - example: "https://example.com/avatars/user1.png" - description: "Аватар пользователя" + example: 'https://example.com/avatars/user1.png' + description: 'Аватар пользователя' achievements: type: array items: - $ref: "#/components/schemas/Achievement" - description: "Достижения" + $ref: '#/components/schemas/Achievement' + description: 'Достижения' favoriteAchievements: type: array items: - $ref: "#/components/schemas/Achievement" - description: "Избранные достижения" + $ref: '#/components/schemas/Achievement' + description: 'Избранные достижения' notifications: type: array items: - $ref: "#/components/schemas/Notification" - description: "Уведомления" + $ref: '#/components/schemas/Notification' + description: 'Уведомления' required: - - user_ID + - user_id - username - email - password @@ -3291,31 +3292,31 @@ components: payment_ID: type: integer example: 601 - description: "Уникальный идентификатор платежа" + description: 'Уникальный идентификатор платежа' user: - $ref: "#/components/schemas/User" - description: "Пользователь" + $ref: '#/components/schemas/User' + description: 'Пользователь' paymentSUM: type: number format: double example: 199.99 - description: "Сумма платежа" + description: 'Сумма платежа' bankCardData: type: array items: type: string writeOnly: true - description: "Данные карты (только для записи)" + description: 'Данные карты (только для записи)' paymentStatus: type: string - enum: ["В обработке", "Оплачено", "Ошибка"] - example: "Оплачено" - description: "Статус платежа" + enum: ['В обработке', 'Оплачено', 'Ошибка'] + example: 'Оплачено' + description: 'Статус платежа' paymentDateTime: type: string format: date-time - example: "2024-05-20T17:30:00Z" - description: "Дата и время платежа" + example: '2024-05-20T17:30:00Z' + description: 'Дата и время платежа' required: - payment_ID - user @@ -3329,32 +3330,32 @@ components: trade_ID: type: integer example: 701 - description: "Уникальный идентификатор обмена" + description: 'Уникальный идентификатор обмена' offeringUser: - $ref: "#/components/schemas/User" - description: "Пользователь, предлагающий обмен" + $ref: '#/components/schemas/User' + description: 'Пользователь, предлагающий обмен' offeringCards: type: array items: - $ref: "#/components/schemas/Card" - description: "Предлагаемые карты" + $ref: '#/components/schemas/Card' + description: 'Предлагаемые карты' receivingUser: - $ref: "#/components/schemas/User" - description: "Пользователь, получающий предложение" + $ref: '#/components/schemas/User' + description: 'Пользователь, получающий предложение' receivingCards: type: array items: - $ref: "#/components/schemas/Card" - description: "Запрашиваемые карты" + $ref: '#/components/schemas/Card' + description: 'Запрашиваемые карты' isConfirmed: type: boolean example: false - description: "Статус подтверждения" + description: 'Статус подтверждения' tradeDateTime: type: string format: date-time - example: "2024-05-20T18:00:00Z" - description: "Дата и время обмена" + example: '2024-05-20T18:00:00Z' + description: 'Дата и время обмена' required: - trade_ID - offeringUser @@ -3370,21 +3371,21 @@ components: collection_ID: type: integer example: 801 - description: "Уникальный идентификатор коллекции" + description: 'Уникальный идентификатор коллекции' name: type: string - example: "Драконы" - description: "Название коллекции" + example: 'Драконы' + description: 'Название коллекции' cards: type: array items: - $ref: "#/components/schemas/Card" - description: "Карты в коллекции" + $ref: '#/components/schemas/Card' + description: 'Карты в коллекции' imageURL: type: string format: url - example: "https://example.com/collections/dragons.png" - description: "Обложка коллекции" + example: 'https://example.com/collections/dragons.png' + description: 'Обложка коллекции' required: - collection_ID - name @@ -3397,19 +3398,19 @@ components: theme_ID: type: integer example: 901 - description: "Уникальный идентификатор темы" + description: 'Уникальный идентификатор темы' name: type: string - example: "Мифические существа" - description: "Название темы" + example: 'Мифические существа' + description: 'Название темы' description: type: string - example: "Создайте изображение мифического существа" - description: "Описание темы" + example: 'Создайте изображение мифического существа' + description: 'Описание темы' required: - theme_ID - name - + UserStats: type: object properties: @@ -3428,7 +3429,7 @@ components: example: 1 name: type: string - example: "Стартовый набор" + example: 'Стартовый набор' coinsAmount: type: integer example: 100 @@ -3442,7 +3443,7 @@ components: description: type: string nullable: true - example: "Стартовый набор. Вы можете получить..." + example: 'Стартовый набор. Вы можете получить...' PaymentCallback: type: object @@ -3451,7 +3452,7 @@ components: type: string status: type: string - enum: ["Успешно", "Ошибка", "В процессе"] + enum: ['Успешно', 'Ошибка', 'В процессе'] amount: type: number currency: @@ -3463,22 +3464,21 @@ components: - status - amount - UserSettings: type: object properties: notificationsEnabled: type: boolean example: true - description: "Включены ли уведомления" + description: 'Включены ли уведомления' showInventory: type: boolean example: false - description: "Виден ли инвентарь другим пользователям" + description: 'Виден ли инвентарь другим пользователям' autoDeclineTrades: type: boolean example: false - description: "Автоматически отклонять все входящие предложения обмена" + description: 'Автоматически отклонять все входящие предложения обмена' required: - notificationsEnabled - showInventory @@ -3490,40 +3490,40 @@ components: quest_ID: type: integer example: 1 - description: "Уникальный идентификатор квеста" + description: 'Уникальный идентификатор квеста' name: type: string - example: "Соберите 5 карт" - description: "Название квеста" + example: 'Соберите 5 карт' + description: 'Название квеста' description: type: string - example: "Соберите 5 карт из коллекции Драконы" - description: "Описание квеста" + example: 'Соберите 5 карт из коллекции Драконы' + description: 'Описание квеста' progress: type: integer example: 3 - description: "Текущий прогресс" + description: 'Текущий прогресс' target: type: integer example: 5 - description: "Целевое значение" + description: 'Целевое значение' rewardCoins: type: integer example: 200 - description: "Награда в монетах" + description: 'Награда в монетах' rewardPacks: type: array items: - $ref: "#/components/schemas/Pack" - description: "Награда в наборах" + $ref: '#/components/schemas/Pack' + description: 'Награда в наборах' isCompleted: type: boolean example: false - description: "Выполнен ли квест" + description: 'Выполнен ли квест' isClaimed: type: boolean example: false - description: "Получена ли награда" + description: 'Получена ли награда' required: - quest_ID - name @@ -3537,10 +3537,10 @@ components: responses: BadRequest: description: Неверный формат запроса - + Unauthorized: description: Ошибка аутентификации - + ServerError: description: Что-то пошло не так content: @@ -3550,24 +3550,23 @@ components: properties: error: type: string - example: "Что-то пошло не так, попробуйте позже" + example: 'Что-то пошло не так, попробуйте позже' code: type: string - example: "INTERNAL_SERVER_ERROR" + example: 'INTERNAL_SERVER_ERROR' timestamp: type: string format: date-time - example: "2024-05-20T12:00:00Z" + example: '2024-05-20T12:00:00Z' - InvalidToken: description: Невалидный токен - + Forbidden: description: Доступ запрещен - + securitySchemes: bearerAuth: type: http scheme: bearer - bearerFormat: JWT \ No newline at end of file + bearerFormat: JWT diff --git a/README.md b/README.md index 550236c..b7bb223 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,63 @@ **Cardly** - это сервис для обмена коллекционными карточками и наборами, включая карточки с изображениями животных, пейзажей и т.д. +## Сборка и развертывание + +### Требования + +#### Для мобильного приложения: +- Flutter SDK (версия указана в pubspec.yaml) +- Android Studio +- JDK 17 или выше +- Android SDK (для Android сборки) + + +#### Для бэкенда: +- JDK 17 или выше +- Kotlin 1.9.25 или выше +- Docker и Docker Compose +- Gradle 8.13 или выше +- Spring Boot 3.4.4 или выше + +### Сборка мобильного приложения + +1. Перейдите в директорию frontend: +```bash +cd frontend +``` + +2. Установите зависимости: +```bash +flutter pub get +``` + +3. Для сборки Android APK: +```bash +flutter build apk --release +``` +APK файл будет находиться в `build/app/outputs/flutter-apk/app-release.apk` + +### Развертывание бэкенда + +1. Перейдите в директорию backend: +```bash +cd backend +``` + +2. Запустите с помощью Docker Compose: +```bash +docker-compose up --build +``` + +### Конфигурация + +#### Бэкенд +- Для изменения порта или других параметров отредактируйте `docker-compose.yml` + +#### Мобильное приложение +- API URL настраивается в `frontend/lib/config/api_config.dart` + + --- ## Команда (ТП 5-1) diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..2b66d13 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,31 @@ +.env.example + +# IntelliJ IDEA / VSCode +.idea/ +.vscode/ + +# Kotlin/Gradle +.gradle/ +build/ +out/ +!gradle/wrapper/gradle-wrapper.jar + +# OS garbage +.DS_Store +Thumbs.db + +# Logs and temp +*.log +*.tmp +*.swp + +# Git +.git +.gitignore + +# Docker-related +docker-compose.override.yml + +# Test artifacts +test-results/ +coverage/ diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..3bcf4a6 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,38 @@ +### --- DATABASE CONFIG --- ### +DB_URL=jdbc:postgresql://localhost:5432/cardly +DB_USERNAME=your_db_username +DB_PASSWORD=your_db_password +DB_DRIVER=org.postgresql.Driver + +### --- JPA CONFIG --- ### +JPA_DDL_AUTO=update +JPA_DIALECT=org.hibernate.dialect.PostgreSQLDialect +JPA_SHOW_SQL=true + +### --- METRICS / PROMETHEUS --- ### +MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE=* +METRICS_PROMETHEUS_ENABLED=true + +### --- JWT CONFIG --- ### +JWT_SECRET=your_super_secret_jwt_key +JWT_EXPIRATION=2592000000 + +### --- SERVER CONFIG --- ### +SERVER_PORT=8080 + +### --- LOGGING CONFIG --- ### +SPRING_SECURITY_LOG_LEVEL=INFO + +### --- MAIL CONFIG --- ### +EMAIL_HOST=smtp.yandex.ru +EMAIL_PORT=587 +EMAIL_USERNAME=your_email@yandex.ru +EMAIL_PASSWORD=your_email_password +EMAIL_SMTP_AUTH=true +EMAIL_SMTP_STARTTLS_ENABLE=true + +### --- SWAGGER CONFIG --- ### +SWAGGER_UI_PATH=/swagger-ui.html +API_DOCS_PATH=/v3/api-docs +SWAGGER_OPERATIONS_SORTER=method +SWAGGER_TAGS_SORTER=alpha diff --git a/backend/.gitignore b/backend/.gitignore index 5a979af..4886d99 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -4,6 +4,8 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +*.log +*.env ### STS ### .apt_generated diff --git a/backend/Dockerfile b/backend/Dockerfile index fd2a066..e6a4aa2 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,18 +1,27 @@ -FROM eclipse-temurin:17-jdk as build +FROM eclipse-temurin:17-jdk AS build WORKDIR /app COPY gradlew . COPY gradle gradle -COPY build.gradle . -COPY settings.gradle . -COPY src src +COPY build.gradle settings.gradle ./ -# Предоставляем права на исполнение градл-враппера RUN chmod +x ./gradlew -RUN ./gradlew build -x test +RUN mkdir -p /app/.gradle + +# Кэшируем скачанный gradle +RUN chown -R root:root /app/.gradle + +RUN ./gradlew --no-daemon --gradle-user-home=/app/.gradle dependencies || true + +COPY src src + +# Собираем проект с тем же кэшем +RUN ./gradlew --no-daemon --gradle-user-home=/app/.gradle build -x test + +# Финальный образ FROM eclipse-temurin:17-jre WORKDIR /app COPY --from=build /app/build/libs/*.jar app.jar -ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/backend/build.gradle b/backend/build.gradle index 997b35b..e2865c2 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -26,28 +26,42 @@ repositories { } dependencies { + // Core Spring + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-mail' - implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' - implementation 'org.jetbrains.kotlin:kotlin-reflect' - implementation 'io.jsonwebtoken:jjwt-api:0.11.5' - implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' - implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'io.github.cdimascio:dotenv-java:3.0.0' // OpenAPI 3.0 (Swagger) для документации implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' implementation 'org.springdoc:springdoc-openapi-starter-common:2.3.0' - compileOnly 'org.projectlombok:lombok' + // DB runtimeOnly 'org.postgresql:postgresql' + + // Метрики + implementation 'io.micrometer:micrometer-registry-prometheus' + + // Kotlin + Jackson + implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' + implementation 'org.jetbrains.kotlin:kotlin-reflect' + + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + // Lombok (только на этапе компиляции) + compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + + // Тестирование testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5' testImplementation 'org.springframework.security:spring-security-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } kotlin { diff --git a/backend/configs/grafana/dashboards/App Observitility.json b/backend/configs/grafana/dashboards/App Observitility.json new file mode 100644 index 0000000..93e3e9e --- /dev/null +++ b/backend/configs/grafana/dashboards/App Observitility.json @@ -0,0 +1,681 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 4, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 13, + "panels": [], + "title": "Аналитика по авторизации", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 1 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "expr": "auth_register_duration_seconds", + "refId": "A" + } + ], + "title": "📈 Продолжительность регистрации", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 7 + }, + "id": 4, + "options": { + "displayMode": "gradient", + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "maxVizHeight": 300, + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "valueMode": "color" + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "code", + "expr": "auth_register_attempt_total", + "fullMetaSearch": false, + "includeNullMetadata": true, + "range": true, + "refId": "Всего регистрационных запросов", + "useBackend": false + }, + { + "expr": "auth_register_conflict_total", + "refId": "B" + }, + { + "expr": "auth_register_failure_total", + "refId": "C" + } + ], + "title": "🔐 Статистика аутентификации", + "type": "bargauge" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 13 + }, + "id": 12, + "panels": [], + "title": "Аналитика машины", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 14 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "application_ready_time_seconds", + "refId": "A" + } + ], + "title": "Время готовности приложения", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 6, + "y": 14 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "expr": "application_started_time_seconds", + "refId": "A" + } + ], + "title": "🚀 Время запуска приложения", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "process_cpu_usage{instance=\"app:8080\", job=\"app\"}" + }, + "properties": [ + { + "id": "displayName", + "value": "Process cpu usage" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "system_cpu_usage{instance=\"app:8080\", job=\"app\"}" + }, + "properties": [ + { + "id": "displayName", + "value": "System cpu usage" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 14 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "editorMode": "code", + "expr": "process_cpu_usage", + "range": true, + "refId": "A" + }, + { + "expr": "system_cpu_usage", + "refId": "B" + } + ], + "title": "🔥 Использование процессора", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 12, + "x": 12, + "y": 20 + }, + "id": 11, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "code", + "expr": "max(process_uptime_seconds{job=\"app\", instance=\"app:8080\"})", + "fullMetaSearch": false, + "includeNullMetadata": true, + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "🕒 Время работы процесса", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 12, + "x": 0, + "y": 22 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "expr": "http_server_requests_active_seconds", + "refId": "A" + } + ], + "title": "📊 Активные HTTP-запросы", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 25 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.1", + "targets": [ + { + "expr": "http_server_requests_seconds", + "refId": "A" + } + ], + "title": "🌐 Время обработки HTTP-запросов", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "10s", + "schemaVersion": 41, + "tags": ["app", "system", "monitoring"], + "templating": { + "list": [] + }, + "time": { + "from": "2025-05-25T15:28:11.694Z", + "to": "2025-05-26T03:28:11.694Z" + }, + "timepicker": {}, + "timezone": "browser", + "title": "🔥 Cardly", + "uid": "aemzyawh463nke", + "version": 21 +} diff --git a/backend/configs/grafana/datasources.yaml b/backend/configs/grafana/datasources.yaml new file mode 100644 index 0000000..9a4ee8c --- /dev/null +++ b/backend/configs/grafana/datasources.yaml @@ -0,0 +1,17 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + basicAuth: false + isDefault: true + editable: true + jsonData: + tlsSkipVerify: true + httpMethod: POST + timeInterval: 1s + maxLines: 10000 + minInterval: 1s + readOnly: false \ No newline at end of file diff --git a/backend/configs/prometheus.yml b/backend/configs/prometheus.yml new file mode 100644 index 0000000..d084519 --- /dev/null +++ b/backend/configs/prometheus.yml @@ -0,0 +1,12 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: 'app' + metrics_path: '/actuator/prometheus' + static_configs: + - targets: ['app:8080'] + + - job_name: 'node-exporter' + static_configs: + - targets: ['node-exporter:9100'] diff --git a/backend/db/migration/V1__init_tables.sql b/backend/db/migration/V1__init_tables.sql new file mode 100644 index 0000000..1da3848 --- /dev/null +++ b/backend/db/migration/V1__init_tables.sql @@ -0,0 +1,195 @@ +-- Таблица users +CREATE TABLE users ( + user_id SERIAL PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + username VARCHAR(255) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + is_enabled BOOLEAN NOT NULL DEFAULT FALSE, + activation_token VARCHAR(255), + password_reset_token VARCHAR(255), + password_reset_token_expiry BIGINT, + balance INTEGER NOT NULL DEFAULT 1000, + avatar_url VARCHAR(255) +); + +-- Таблица collections +CREATE TABLE collections ( + collection_id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + image_url VARCHAR(255) NOT NULL +); + +-- Таблица cards +CREATE TABLE cards ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + image_url VARCHAR(255) NOT NULL, + rarity VARCHAR(50) NOT NULL, + disassemble_price INTEGER NOT NULL, + is_generated BOOLEAN NOT NULL, + description TEXT, + theme VARCHAR(255), + collection_id INTEGER, + owner_id INTEGER, + CONSTRAINT fk_collection FOREIGN KEY (collection_id) REFERENCES collections(collection_id), + CONSTRAINT fk_owner FOREIGN KEY (owner_id) REFERENCES users(user_id) +); + +-- Таблица achievements +CREATE TABLE achievements ( + achievement_id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + image_url VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + is_unlocked BOOLEAN NOT NULL DEFAULT FALSE +); + +-- Таблица notifications +CREATE TABLE notifications ( + notification_id SERIAL PRIMARY KEY, + user_id INT NOT NULL, + message TEXT NOT NULL, + notification_date_time TIMESTAMP NOT NULL, + CONSTRAINT fk_user FOREIGN KEY(user_id) REFERENCES users(user_id) +); + +-- Таблица packs +CREATE TABLE packs ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + image_url VARCHAR(255) NOT NULL, + price INT NOT NULL +); + +-- Таблица quests +CREATE TABLE quests ( + quest_id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + progress INT NOT NULL, + target INT NOT NULL, + reward_coins INT NOT NULL, + is_completed BOOLEAN NOT NULL, + is_claimed BOOLEAN NOT NULL +); + +-- Таблица user_inventory_cards (многие ко многим) +CREATE TABLE user_inventory_cards ( + user_id INTEGER NOT NULL, + card_id INTEGER NOT NULL, + PRIMARY KEY (user_id, card_id), + CONSTRAINT fk_inventory_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, + CONSTRAINT fk_inventory_card FOREIGN KEY (card_id) REFERENCES cards(id) ON DELETE CASCADE +); + +-- Таблица user_favorite_cards (многие ко многим) +CREATE TABLE user_favorite_cards ( + user_id INTEGER NOT NULL, + card_id INTEGER NOT NULL, + PRIMARY KEY (user_id, card_id), + CONSTRAINT fk_favorite_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, + CONSTRAINT fk_favorite_card FOREIGN KEY (card_id) REFERENCES cards(id) ON DELETE CASCADE +); + +-- Таблица user_onchange_cards (многие ко многим) +CREATE TABLE user_onchange_cards ( + user_id INTEGER NOT NULL, + card_id INTEGER NOT NULL, + PRIMARY KEY (user_id, card_id), + CONSTRAINT fk_onchange_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, + CONSTRAINT fk_onchange_card FOREIGN KEY (card_id) REFERENCES cards(id) ON DELETE CASCADE +); + +-- Таблица user_achievements (многие ко многим) +CREATE TABLE user_achievements ( + user_id INTEGER NOT NULL, + achievement_id INTEGER NOT NULL, + PRIMARY KEY (user_id, achievement_id), + CONSTRAINT fk_achievements_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, + CONSTRAINT fk_achievements_achievement FOREIGN KEY (achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE +); + +-- Таблица user_favorite_achievements (многие ко многим) +CREATE TABLE user_favorite_achievements ( + user_id INTEGER NOT NULL, + achievement_id INTEGER NOT NULL, + PRIMARY KEY (user_id, achievement_id), + CONSTRAINT fk_favorite_achievements_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, + CONSTRAINT fk_favorite_achievements_achievement FOREIGN KEY (achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE +); + +-- Таблица user_notifications (многие ко многим) +CREATE TABLE user_notifications ( + user_id INTEGER NOT NULL, + notification_id INTEGER NOT NULL, + PRIMARY KEY (user_id, notification_id), + CONSTRAINT fk_notifications_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, + CONSTRAINT fk_notifications_notification FOREIGN KEY (notification_id) REFERENCES notifications(notification_id) ON DELETE CASCADE +); + +-- Таблица pack_cards +CREATE TABLE pack_cards ( + pack_id BIGINT NOT NULL, + card_id INT NOT NULL, + CONSTRAINT fk_pack FOREIGN KEY (pack_id) REFERENCES packs(id) ON DELETE CASCADE, + CONSTRAINT fk_card FOREIGN KEY (card_id) REFERENCES cards(id) ON DELETE CASCADE, + PRIMARY KEY (pack_id, card_id) +); + +-- Таблица quest_reward_packs +CREATE TABLE quest_reward_packs ( + quest_id INT NOT NULL, + pack_id BIGINT NOT NULL, + CONSTRAINT fk_quest FOREIGN KEY (quest_id) REFERENCES quests(quest_id) ON DELETE CASCADE, + CONSTRAINT fk_pack FOREIGN KEY (pack_id) REFERENCES packs(id) ON DELETE CASCADE, + PRIMARY KEY (quest_id, pack_id) +); + +-- Таблица coin_offers +CREATE TABLE coin_offers ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + coins_amount INT NOT NULL, + price DOUBLE PRECISION NOT NULL, + image_url VARCHAR(255) NOT NULL, + description VARCHAR(1000) +); + +-- Таблица news_entity +CREATE TABLE news_entity ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + content TEXT NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL +); + +-- Таблица notification_links +CREATE TABLE notification_links ( + notification_id INT NOT NULL, + link TEXT NOT NULL, + CONSTRAINT fk_notification FOREIGN KEY(notification_id) REFERENCES notifications(notification_id) +); + +-- Таблица reports +CREATE TABLE reports ( + id BIGSERIAL PRIMARY KEY, + reporter_id INT NOT NULL, + reported_user_id INT NOT NULL, + report_date_time TIMESTAMP NOT NULL, + reason TEXT NOT NULL, + status VARCHAR(50) NOT NULL, + + CONSTRAINT fk_reporter FOREIGN KEY (reporter_id) REFERENCES users(user_id) ON DELETE CASCADE, + CONSTRAINT fk_reported_user FOREIGN KEY (reported_user_id) REFERENCES users(user_id) ON DELETE CASCADE +); + +-- Таблица user_stats +CREATE TABLE user_stats ( + id BIGSERIAL PRIMARY KEY, + total_cards INT NOT NULL DEFAULT 0, + completed_collections INT NOT NULL DEFAULT 0, + user_id INT NOT NULL UNIQUE, + + CONSTRAINT fk_user_stats_user FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE +); diff --git a/backend/db/migration/V2__add_completed_collections.sql b/backend/db/migration/V2__add_completed_collections.sql new file mode 100644 index 0000000..dccdedb --- /dev/null +++ b/backend/db/migration/V2__add_completed_collections.sql @@ -0,0 +1,7 @@ +CREATE TABLE user_completed_collections ( + user_id INT NOT NULL, + collection_id INT NOT NULL, + PRIMARY KEY (user_id, collection_id), + FOREIGN KEY (user_id) REFERENCES users(user_id), + FOREIGN KEY (collection_id) REFERENCES collections(collection_id) +); \ No newline at end of file diff --git a/backend/db/migration/V3__add_role_attribute.sql b/backend/db/migration/V3__add_role_attribute.sql new file mode 100644 index 0000000..12fcf4d --- /dev/null +++ b/backend/db/migration/V3__add_role_attribute.sql @@ -0,0 +1,2 @@ +ALTER TABLE users +ADD COLUMN role VARCHAR(50) NOT NULL DEFAULT 'USER'; diff --git a/backend/db/migration/V4__new_tables.sql b/backend/db/migration/V4__new_tables.sql new file mode 100644 index 0000000..5fcf44f --- /dev/null +++ b/backend/db/migration/V4__new_tables.sql @@ -0,0 +1,62 @@ +-- Таблица тем +CREATE TABLE themes ( + theme_id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT +); + +-- Таблица настроек пользователя +CREATE TABLE user_settings ( + settings_id SERIAL PRIMARY KEY, + notifications_enabled BOOLEAN NOT NULL, + show_inventory BOOLEAN NOT NULL, + auto_decline_trades BOOLEAN NOT NULL, + user_id INT NOT NULL UNIQUE, + CONSTRAINT fk_user_settings_user FOREIGN KEY (user_id) REFERENCES users(user_id) +); + +-- Таблица платежей +CREATE TABLE payments ( + payment_id SERIAL PRIMARY KEY, + user_id INT NOT NULL, + payment_sum DOUBLE PRECISION NOT NULL, + payment_status VARCHAR(50) NOT NULL, + payment_datetime TIMESTAMP NOT NULL, + CONSTRAINT fk_payments_user FOREIGN KEY (user_id) REFERENCES users(user_id) +); + +-- Таблица с данными карты (элемент коллекции) +CREATE TABLE payment_card_data ( + payment_id INT NOT NULL, + card_data TEXT, + CONSTRAINT fk_payment_card_data FOREIGN KEY (payment_id) REFERENCES payments(payment_id) +); + +-- Таблица обменов +CREATE TABLE trades ( + trade_id SERIAL PRIMARY KEY, + offering_user_id INT NOT NULL, + receiving_user_id INT NOT NULL, + is_confirmed BOOLEAN NOT NULL, + trade_date_time TIMESTAMP NOT NULL, + CONSTRAINT fk_trades_offering_user FOREIGN KEY (offering_user_id) REFERENCES users(user_id), + CONSTRAINT fk_trades_receiving_user FOREIGN KEY (receiving_user_id) REFERENCES users(user_id) +); + +-- Связь trade -> offeringCards +CREATE TABLE trade_offering_cards ( + trade_id INT NOT NULL, + card_id INT NOT NULL, + PRIMARY KEY (trade_id, card_id), + CONSTRAINT fk_trade_offering_cards_trade FOREIGN KEY (trade_id) REFERENCES trades(trade_id), + CONSTRAINT fk_trade_offering_cards_card FOREIGN KEY (card_id) REFERENCES cards(id) +); + +-- Связь trade -> receivingCards +CREATE TABLE trade_receiving_cards ( + trade_id INT NOT NULL, + card_id INT NOT NULL, + PRIMARY KEY (trade_id, card_id), + CONSTRAINT fk_trade_receiving_cards_trade FOREIGN KEY (trade_id) REFERENCES trades(trade_id), + CONSTRAINT fk_trade_receiving_cards_card FOREIGN KEY (card_id) REFERENCES cards(id) +); diff --git a/backend/db/migration/V5__connect_card_theme.sql b/backend/db/migration/V5__connect_card_theme.sql new file mode 100644 index 0000000..e74ac78 --- /dev/null +++ b/backend/db/migration/V5__connect_card_theme.sql @@ -0,0 +1,10 @@ +ALTER TABLE cards + ADD COLUMN theme_id INT; + +ALTER TABLE cards + ADD CONSTRAINT fk_cards_theme + FOREIGN KEY (theme_id) + REFERENCES themes(theme_id); + +ALTER TABLE cards + DROP COLUMN theme; diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index a0b4613..56582a8 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -1,20 +1,19 @@ -version: '4.4' - services: app: + container_name: cardly_app build: . ports: - "8080:8080" - environment: - - DB_HOST=db - - DB_PORT=5432 - - DB_NAME=auth_db - - DB_USER=appuser - - DB_PASSWORD=password + env_file: + - .env depends_on: - db + networks: + - backend + - monitoring db: + container_name: cardly_db image: postgres:14 ports: - "5432:5432" @@ -23,7 +22,90 @@ services: - POSTGRES_USER=appuser - POSTGRES_PASSWORD=password volumes: - - postgres-data:/var/lib/postgresql/data + - postgres_data:/var/lib/postgresql/data + networks: + - backend + + prometheus: + container_name: cardly_prometheus + image: prom/prometheus + volumes: + - ./configs/prometheus.yml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + networks: + - monitoring + + grafana: + container_name: cardly_grafana + image: grafana/grafana + ports: + - "3000:3000" + volumes: + - ./configs/grafana/datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml + - grafana_data:/var/lib/grafana + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false + networks: + - monitoring + + node-exporter: + container_name: cardly_node_exporter + image: prom/node-exporter + ports: + - "9100:9100" + networks: + - monitoring + restart: unless-stopped + pid: "host" + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - '--path.procfs=/host/proc' + - '--path.sysfs=/host/sys' + - '--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|host|etc)($|/)' + flyway: + container_name: cardly_flyway + image: flyway/flyway + depends_on: + - db + command: -url=jdbc:postgresql://db:5432/auth_db -user=appuser -password=password -connectRetries=10 migrate + volumes: + - ./db/migration:/flyway/sql + networks: + - backend + # НЕ запускаем автоматически + deploy: + replicas: 0 + + pgadmin: + container_name: cardly_pgadmin + image: dpage/pgadmin4 + restart: unless-stopped + environment: + PGADMIN_DEFAULT_EMAIL: admin@example.com + PGADMIN_DEFAULT_PASSWORD: admin + ports: + - "5050:80" + volumes: + - pgadmin_data:/var/lib/pgadmin + depends_on: + - db + networks: + - backend + volumes: - postgres-data: \ No newline at end of file + postgres_data: + grafana_data: + pgadmin_data: + +networks: + backend: + driver: bridge + monitoring: + driver: bridge \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/AppApplication.kt b/backend/src/main/kotlin/ru/vsu/app/AppApplication.kt deleted file mode 100644 index 95b267d..0000000 --- a/backend/src/main/kotlin/ru/vsu/app/AppApplication.kt +++ /dev/null @@ -1,11 +0,0 @@ -package ru.vsu.app - -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.runApplication - -@SpringBootApplication -class AppApplication - -fun main(args: Array) { - runApplication(*args) -} diff --git a/backend/src/main/kotlin/ru/vsu/app/Application.kt b/backend/src/main/kotlin/ru/vsu/app/Application.kt new file mode 100644 index 0000000..dd8a1ea --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/Application.kt @@ -0,0 +1,22 @@ +package ru.vsu.app + +import io.github.cdimascio.dotenv.Dotenv +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class Application + +fun main(args: Array) { + val dotenv = Dotenv.configure() + .ignoreIfMissing() + .load() + + dotenv.entries().forEach { entry -> + if (System.getenv(entry.key) == null && System.getProperty(entry.key) == null) { + System.setProperty(entry.key, entry.value) + } + } + + runApplication(*args) +} diff --git a/backend/src/main/kotlin/ru/vsu/app/config/OpenApiConfig.kt b/backend/src/main/kotlin/ru/vsu/app/config/OpenApiConfig.kt index 9df3c64..822b9d8 100644 --- a/backend/src/main/kotlin/ru/vsu/app/config/OpenApiConfig.kt +++ b/backend/src/main/kotlin/ru/vsu/app/config/OpenApiConfig.kt @@ -32,13 +32,13 @@ class OpenApiConfig { private fun getApiInfo(): Info { return Info() - .title("REST API Приложения") - .description("API для аутентификации, сброса пароля и получения информации о пользователе") + .title("REST API Cardly") + .description("API для взаимодействия с приложением Cardly") .version("1.0") .contact( Contact() - .name("Anastasia") - .email("example@example.com") + .name("Anna Dobrova") + .email("anyaadobrova@gmail.com") ) } } \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/config/PublicEndpointsConfig.kt b/backend/src/main/kotlin/ru/vsu/app/config/PublicEndpointsConfig.kt new file mode 100644 index 0000000..bff084b --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/config/PublicEndpointsConfig.kt @@ -0,0 +1,24 @@ +package ru.vsu.app.config + +import org.springframework.stereotype.Component + +@Component +class PublicEndpointsConfig { + val endpoints = listOf( + "/api/auth/register", + "/api/auth/login", + "/api/auth/forgot-password", + "/api/auth/reset-password", + "/api/auth/activate", + "/api/auth/resend-code", + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger-ui.html", + "/webjars/**", + "/actuator/prometheus", + "/error" + ) + val passiveTokenEndpoints = listOf( + "/api/auth/check" + ) +} diff --git a/backend/src/main/kotlin/ru/vsu/app/config/SecurityConfig.kt b/backend/src/main/kotlin/ru/vsu/app/config/SecurityConfig.kt index b5ae8d5..db54275 100644 --- a/backend/src/main/kotlin/ru/vsu/app/config/SecurityConfig.kt +++ b/backend/src/main/kotlin/ru/vsu/app/config/SecurityConfig.kt @@ -15,12 +15,14 @@ import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import ru.vsu.app.security.JwtAuthenticationFilter +import ru.vsu.app.config.PublicEndpointsConfig @Configuration @EnableWebSecurity class SecurityConfig( private val jwtAuthFilter: JwtAuthenticationFilter, - private val userDetailsService: UserDetailsService + private val userDetailsService: UserDetailsService, + private val publicEndpointsConfig: PublicEndpointsConfig ) { @Bean @@ -28,17 +30,8 @@ class SecurityConfig( http .csrf { it.disable() } .authorizeHttpRequests { - it.requestMatchers( - "/api/auth/register", - "/api/auth/login", - "/api/auth/forgot-password", - "/api/auth/reset-password", - "/api/auth/activate", - "/v3/api-docs/**", - "/swagger-ui/**", - "/swagger-ui.html", - "/webjars/**" - ).permitAll() + it.requestMatchers("/admin/**").hasRole("ADMIN") + it.requestMatchers(*publicEndpointsConfig.endpoints.toTypedArray(), *publicEndpointsConfig.passiveTokenEndpoints.toTypedArray()).permitAll() .anyRequest().authenticated() } .sessionManagement { diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt new file mode 100644 index 0000000..af8577d --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/controller/AdminController.kt @@ -0,0 +1,887 @@ +package ru.vsu.app.controller + +import ru.vsu.app.dto.requests.AdminReportsReportIDPutRequest +import ru.vsu.app.dto.responses.admin.AdminStatsGet200Response +import ru.vsu.app.dto.responses.admin.AdminTradesTradeIDInvalidatePost200Response +import ru.vsu.app.dto.requests.AdminTradesTradeIDInvalidatePostRequest +import ru.vsu.app.dto.responses.admin.AdminUsersUserIDBanPost200Response +import ru.vsu.app.dto.requests.AdminUsersUserIDBanPostRequest +import ru.vsu.app.dto.responses.admin.AdminUsersUserIDQuestsQuestIDResetPost200Response +import ru.vsu.app.dto.requests.AdminUsersUserIDQuestsQuestIDResetPostRequest +import ru.vsu.app.dto.responses.admin.AdminUsersUserIDUnbanPost200Response +import ru.vsu.app.model.ThemeEntity + +import ru.vsu.app.dto.AchievementDto +import ru.vsu.app.dto.CardDto +import ru.vsu.app.dto.CoinOfferDto +import ru.vsu.app.dto.CollectionDto +import ru.vsu.app.dto.NewsDto +import ru.vsu.app.dto.PackDto +import ru.vsu.app.dto.responses.common.InternalServerError +import ru.vsu.app.dto.ReportDto +import ru.vsu.app.dto.TradeDto +import ru.vsu.app.dto.ThemeDto + +import ru.vsu.app.service.AchievementService +import ru.vsu.app.service.CardService +import ru.vsu.app.service.CoinOfferService +import ru.vsu.app.service.CollectionService +import ru.vsu.app.service.NewsService +import ru.vsu.app.service.PackService +import ru.vsu.app.service.ReportService +import ru.vsu.app.service.AdminService +import ru.vsu.app.service.TradeService +import ru.vsu.app.service.ThemeService +import ru.vsu.app.service.UserService + +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* +import io.swagger.v3.oas.annotations.tags.Tag + +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* +import org.springframework.validation.annotation.Validated +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.Valid +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size + +import kotlin.collections.List +import kotlin.collections.Map + +@RestController +@Validated +@PreAuthorize("hasRole('ADMIN')") +@RequestMapping("\${api.base-path:/api}") +@SecurityRequirement(name = "Bearer Authentication") +@Tag(name = "Admin", description = "Функции администратора") +class AdminController( + private val achievementService: AchievementService, + private val cardService: CardService, + private val coinOfferService: CoinOfferService, + private val collectionService: CollectionService, + private val newsService: NewsService, + private val packService: PackService, + private val reportService: ReportService, + private val adminService: AdminService, + private val tradeService: TradeService, + private val userService: UserService, + private val themeService: ThemeService +) { + + @Operation( + summary = "Удаление достижения", + operationId = "adminAchievementsAchievementIDDelete", + description = """Удаляет достижение из системы""", + responses = [ + ApiResponse(responseCode = "204", description = "Достижение успешно удалено"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/admin/achievements/{achievement_ID}"], + produces = ["application/json"] + ) +fun adminAchievementsAchievementIDDelete(@Parameter(description = "", required = true) @PathVariable("achievement_ID") achievementID: kotlin.Int): ResponseEntity { + try { + achievementService.deleteAchievement(achievementID) + return ResponseEntity.noContent().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при удалении достижения", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Обновление достижения", + operationId = "adminAchievementsAchievementIDPut", + description = """Обновляет информацию о достижении""", + responses = [ + ApiResponse(responseCode = "200", description = "Достижение успешно обновлено", content = [Content(schema = Schema(implementation = AchievementDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.PUT], + value = ["/admin/achievements/{achievement_ID}"], + produces = ["application/json"], + consumes = ["application/json"] + ) +fun adminAchievementsAchievementIDPut(@Parameter(description = "", required = true) @PathVariable("achievement_ID") achievementID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody achievement: AchievementDto): ResponseEntity { + try { + val updatedAchievement = achievementService.updateAchievement(achievementID, achievement) + return ResponseEntity.ok(updatedAchievement) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при обновлении достижения", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Создание достижения", + operationId = "adminAchievementsPost", + description = """Создает новое достижение в системе""", + responses = [ + ApiResponse(responseCode = "201", description = "Достижение успешно создано", content = [Content(schema = Schema(implementation = AchievementDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/achievements"], + produces = ["application/json"], + consumes = ["application/json"] + ) +fun adminAchievementsPost(@Parameter(description = "", required = true) @Valid @RequestBody achievement: AchievementDto): ResponseEntity { + try { + val createdAchievement = achievementService.createAchievement(achievement) + return ResponseEntity.status(HttpStatus.CREATED).body(createdAchievement) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при создании достижения", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Удаление карты", + operationId = "adminCardsCardIDDelete", + description = """Удаляет карту из системы""", + responses = [ + ApiResponse(responseCode = "204", description = "Карта успешно удалена"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/admin/cards/{card_ID}"], + produces = ["application/json"] + ) +fun adminCardsCardIDDelete(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + try { + cardService.deleteCard(cardID) + return ResponseEntity.noContent().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при удалении карты", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Обновление карты", + operationId = "adminCardsCardIDPut", + description = """Обновляет информацию о карте""", + responses = [ + ApiResponse(responseCode = "200", description = "Карта успешно обновлена", content = [Content(schema = Schema(implementation = CardDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.PUT], + value = ["/admin/cards/{card_ID}"], + produces = ["application/json"], + consumes = ["application/json"] + ) +fun adminCardsCardIDPut(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody card: CardDto): ResponseEntity { + try { + val updatedCard = cardService.updateCard(cardID, card) + return ResponseEntity.ok(updatedCard) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при обновлении карты", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Создание новой карты", + operationId = "adminCardsPost", + description = """Создает новую карту в системе""", + responses = [ + ApiResponse(responseCode = "201", description = "Карта успешно создана", content = [Content(schema = Schema(implementation = CardDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/cards"], + produces = ["application/json"], + consumes = ["application/json"] + ) +fun adminCardsPost(@Parameter(description = "", required = true) @Valid @RequestBody card: CardDto): ResponseEntity { + try { + val createdCard = cardService.createCard(card) + return ResponseEntity.status(HttpStatus.CREATED).body(createdCard) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при создании карты", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Получение списка предложений покупки монет", + operationId = "adminCoinOffersGet", + description = """Возвращает список всех предложений покупки монет""", + responses = [ + ApiResponse(responseCode = "200", description = "Список предложений успешно получен", content = [Content(array = ArraySchema(schema = Schema(implementation = CoinOfferDto::class)))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/admin/coin-offers"], + produces = ["application/json"] + ) +fun adminCoinOffersGet(): ResponseEntity { + try { + val coinOffers = coinOfferService.getAllCoinOffers() + return ResponseEntity.ok(coinOffers) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при получении списка предложений покупки монет", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Удаление предложения покупки монет", + operationId = "adminCoinOffersOfferIdDelete", + description = """Удаляет предложение покупки монет из системы""", + responses = [ + ApiResponse(responseCode = "204", description = "Предложение успешно удалено"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/admin/coin-offers/{offer_id}"], + produces = ["application/json"] + ) +fun adminCoinOffersOfferIdDelete(@Parameter(description = "", required = true) @PathVariable("offer_id") offerId: kotlin.Int): ResponseEntity { + try { + coinOfferService.deleteCoinOffer(offerId) + return ResponseEntity.noContent().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при удалении предложения покупки монет", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Обновление предложения покупки монет", + operationId = "adminCoinOffersOfferIdPut", + description = """Обновляет информацию о предложении покупки монет""", + responses = [ + ApiResponse(responseCode = "200", description = "Предложение успешно обновлено", content = [Content(schema = Schema(implementation = CoinOfferDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.PUT], + value = ["/admin/coin-offers/{offer_id}"], + produces = ["application/json"], + consumes = ["application/json"] + ) +fun adminCoinOffersOfferIdPut(@Parameter(description = "", required = true) @PathVariable("offer_id") offerId: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody coinOffer: CoinOfferDto): ResponseEntity { + try { + val updatedCoinOffer = coinOfferService.updateCoinOffer(offerId, coinOffer) + return ResponseEntity.ok(updatedCoinOffer) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при обновлении предложения покупки монет", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Создание предложения покупки монет", + operationId = "adminCoinOffersPost", + description = """Создает новое предложение покупки монет в системе""", + responses = [ + ApiResponse(responseCode = "201", description = "Предложение успешно создано", content = [Content(schema = Schema(implementation = CoinOfferDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/coin-offers"], + produces = ["application/json"], + consumes = ["application/json"] + ) +fun adminCoinOffersPost(@Parameter(description = "", required = true) @Valid @RequestBody coinOffer: CoinOfferDto): ResponseEntity { + try { + val createdCoinOffer = coinOfferService.createCoinOffer(coinOffer) + return ResponseEntity.status(HttpStatus.CREATED).body(createdCoinOffer) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при создании предложения покупки монет", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Удаление коллекцию", + operationId = "adminCollectionsCollectionIDDelete", + description = """Удаляет коллекцию из системы""", + responses = [ + ApiResponse(responseCode = "204", description = "Коллекция успешно удалена"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/admin/collections/{collection_ID}"], + produces = ["application/json"] + ) +fun adminCollectionsCollectionIDDelete(@Parameter(description = "", required = true) @PathVariable("collection_ID") collectionID: kotlin.Int): ResponseEntity { + try { + collectionService.deleteCollection(collectionID) + return ResponseEntity.noContent().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при удалении коллекции", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Обновление коллекции", + operationId = "adminCollectionsCollectionIDPut", + description = """Обновляет информацию о коллекции""", + responses = [ + ApiResponse(responseCode = "200", description = "Коллекция успешно обновлена", content = [Content(schema = Schema(implementation = CollectionDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.PUT], + value = ["/admin/collections/{collection_ID}"], + produces = ["application/json"], + consumes = ["application/json"] + ) +fun adminCollectionsCollectionIDPut(@Parameter(description = "", required = true) @PathVariable("collection_ID") collectionID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody collection: CollectionDto): ResponseEntity { + try { + val updatedCollection = collectionService.updateCollection(collectionID, collection) + return ResponseEntity.ok(updatedCollection) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при обновлении коллекции", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Создание новой коллекции", + operationId = "adminCollectionsPost", + description = """Создает новую коллекцию карт""", + responses = [ + ApiResponse(responseCode = "201", description = "Коллекция успешно создана", content = [Content(schema = Schema(implementation = CollectionDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/collections"], + produces = ["application/json"], + consumes = ["application/json"] + ) +fun adminCollectionsPost(@Parameter(description = "", required = true) @Valid @RequestBody collection: CollectionDto): ResponseEntity { + try { + val createdCollection = collectionService.createCollection(collection) + return ResponseEntity.status(HttpStatus.CREATED).body(createdCollection) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при создании коллекции", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Удаление новости", + operationId = "adminNewsNewsIDDelete", + description = """Удаляет новость из системы""", + responses = [ + ApiResponse(responseCode = "204", description = "Новость успешно удалена"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/admin/news/{news_ID}"], + produces = ["application/json"] + ) +fun adminNewsNewsIDDelete(@Parameter(description = "", required = true) @PathVariable("news_ID") newsID: kotlin.Int): ResponseEntity { + try { + newsService.deleteNews(newsID) + return ResponseEntity.noContent().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при удалении новости", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Обновление новости", + operationId = "adminNewsNewsIDPut", + description = """Обновляет информацию о новости""", + responses = [ + ApiResponse(responseCode = "200", description = "Новость успешно обновлена", content = [Content(schema = Schema(implementation = NewsDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.PUT], + value = ["/admin/news/{news_ID}"], + produces = ["application/json"], + consumes = ["application/json"] + ) +fun adminNewsNewsIDPut(@Parameter(description = "", required = true) @PathVariable("news_ID") newsID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody news: NewsDto): ResponseEntity { + try { + val updatedNews = newsService.updateNews(newsID, news) + return ResponseEntity.ok(updatedNews) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при обновлении новости", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Создание новости", + operationId = "adminNewsPost", + description = """Создает новую новость в системе""", + responses = [ + ApiResponse(responseCode = "201", description = "Новость успешно создана", content = [Content(schema = Schema(implementation = NewsDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/news"], + produces = ["application/json"], + consumes = ["application/json"] + ) +fun adminNewsPost(@Parameter(description = "", required = true) @Valid @RequestBody news: NewsDto): ResponseEntity { + try { + val createdNews = newsService.createNews(news) + return ResponseEntity.status(HttpStatus.CREATED).body(createdNews) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при создании новости", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Удаление набор", + operationId = "adminPacksPackIDDelete", + description = """Удаляет набор из системы""", + responses = [ + ApiResponse(responseCode = "204", description = "Набор успешно удален"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/admin/packs/{pack_ID}"], + produces = ["application/json"] + ) +fun adminPacksPackIDDelete(@Parameter(description = "", required = true) @PathVariable("pack_ID") packID: kotlin.Int): ResponseEntity { + try { + packService.deletePack(packID) + return ResponseEntity.noContent().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при удалении набора", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Обновление набора", + operationId = "adminPacksPackIDPut", + description = """Обновляет информацию о наборе""", + responses = [ + ApiResponse(responseCode = "200", description = "Набор успешно обновлен", content = [Content(schema = Schema(implementation = PackDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.PUT], + value = ["/admin/packs/{pack_ID}"], + produces = ["application/json"], + consumes = ["application/json"] + ) +fun adminPacksPackIDPut(@Parameter(description = "", required = true) @PathVariable("pack_ID") packID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody pack: PackDto): ResponseEntity { + try { + val updatedPack = packService.updatePack(packID, pack) + return ResponseEntity.ok(updatedPack) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при обновлении набора", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Создание нового набора", + operationId = "adminPacksPost", + description = """Создает новый набор карт""", + responses = [ + ApiResponse(responseCode = "201", description = "Набор успешно создан", content = [Content(schema = Schema(implementation = PackDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/packs"], + produces = ["application/json"], + consumes = ["application/json"] + ) +fun adminPacksPost(@Parameter(description = "", required = true) @Valid @RequestBody pack: PackDto): ResponseEntity { + try { + val createdPack = packService.createPack(pack) + return ResponseEntity.status(HttpStatus.CREATED).body(createdPack) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при создании набора", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Просмотр списка жалоб", + operationId = "adminReportsGet", + description = """Возвращает список всех жалоб в системе""", + responses = [ + ApiResponse(responseCode = "200", description = "Список жалоб", content = [Content(array = ArraySchema(schema = Schema(implementation = ReportDto::class)))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/admin/reports"], + produces = ["application/json"] + ) +fun adminReportsGet(): ResponseEntity { + try { + val reports = reportService.getAllReports() + return ResponseEntity.ok(reports) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при получении списка жалоб", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Просмотр деталей жалобы", + operationId = "adminReportsReportIDGet", + description = """Возвращает полную информацию о жалобе""", + responses = [ + ApiResponse(responseCode = "200", description = "Детали жалобы", content = [Content(schema = Schema(implementation = ReportDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/admin/reports/{report_ID}"], + produces = ["application/json"] + ) +fun adminReportsReportIDGet(@Parameter(description = "", required = true) @PathVariable("report_ID") reportID: kotlin.Int): ResponseEntity { + try { + val report = reportService.getReportById(reportID) + return ResponseEntity.ok(report) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при получении деталей жалобы", e.message ?: "Неизвестная ошибка")) + } +} + +@Operation( + summary = "Разблокировка пользователя", + operationId = "adminUsersUserIDUnbanPost", + description = """Снимает блокировку с пользователя""", + responses = [ + ApiResponse(responseCode = "200", description = "Пользователь разблокирован", content = [Content(schema = Schema(implementation = AdminUsersUserIDUnbanPost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "404", description = "Пользователь не найден"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) + ], + security = [ SecurityRequirement(name = "bearerAuth") ] +) +@RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/users/{user_id}/unban"], + produces = ["application/json"] +) +fun adminUsersUserIDUnbanPost(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int): ResponseEntity { + try { + val result = userService.unbanUser(userID) + return ResponseEntity.ok(result) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при разблокировке пользователя", e.message ?: "Неизвестная ошибка")) + } +} + +@Operation( + summary = "Создание новой темы", + operationId = "adminThemesPost", + description = """Создает новую тему в системе""", + responses = [ + ApiResponse(responseCode = "201", description = "Тема успешно создана", content = [Content(schema = Schema(implementation = ThemeDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) + ], + security = [ SecurityRequirement(name = "bearerAuth") ] +) +@RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/themes"], + produces = ["application/json"], + consumes = ["application/json"] +) +fun adminThemesPost(@Parameter(description = "", required = true) @Valid @RequestBody theme: ThemeEntity): ResponseEntity { + try { + val createdTheme = themeService.createTheme(theme) + return ResponseEntity.status(HttpStatus.CREATED).body(createdTheme) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при создании темы", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Получение статистики системы", + operationId = "adminStatsGet", + description = """Возвращает статистику по пользователям, картам, обменам и т.д.""", + responses = [ + ApiResponse(responseCode = "200", description = "Статистика системы", content = [Content(schema = Schema(implementation = AdminStatsGet200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/admin/stats"], + produces = ["application/json"] + ) +fun adminStatsGet(): ResponseEntity { + try { + val stats = adminService.getSystemStats() + return ResponseEntity.ok(stats) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при получении статистики системы", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Получение списка обменов", + operationId = "adminTradesGet", + description = """Возвращает список всех обменов в системе""", + responses = [ + ApiResponse(responseCode = "200", description = "Список обменов", content = [Content(array = ArraySchema(schema = Schema(implementation = TradeDto::class)))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/admin/trades"], + produces = ["application/json"] + ) +fun adminTradesGet(): ResponseEntity { + try { + val trades = tradeService.getAllTrades(null) + return ResponseEntity.ok(trades) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при получении списка обменов", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Признание обмена недействительным", + operationId = "adminTradesTradeIDInvalidatePost", + description = """Помечает обмен как недействительный и возвращает карты участникам +""", + responses = [ + ApiResponse(responseCode = "200", description = "Обмен аннулирован", content = [Content(schema = Schema(implementation = AdminTradesTradeIDInvalidatePost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/trades/{trade_ID}/invalidate"], + produces = ["application/json"], + consumes = ["application/json"] + ) +fun adminTradesTradeIDInvalidatePost(@Parameter(description = "", required = true) @PathVariable("trade_ID") tradeID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody adminTradesTradeIDInvalidatePostRequest: AdminTradesTradeIDInvalidatePostRequest): ResponseEntity { + try { + val result = tradeService.invalidateTrade(tradeID, adminTradesTradeIDInvalidatePostRequest) + return ResponseEntity.ok(result) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при аннулировании обмена", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Отзыв достижения у пользователя", + operationId = "adminUsersUserIDAchievementsAchievementIDDelete", + description = """Удаляет достижение из списка полученных пользователем""", + responses = [ + ApiResponse(responseCode = "204", description = "Достижение успешно отозвано"), + ApiResponse(responseCode = "400", description = "Неверный запрос"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/admin/users/{user_id}/achievements/{achievement_ID}"], + produces = ["application/json"] + ) +fun adminUsersUserIDAchievementsAchievementIDDelete(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int,@Parameter(description = "", required = true) @PathVariable("achievement_ID") achievementID: kotlin.Int): ResponseEntity { + try { + userService.revokeAchievement(userID, achievementID) + return ResponseEntity.noContent().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при отзыве достижения у пользователя", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Блокировка пользователя", + operationId = "adminUsersUserIDBanPost", + description = """Блокирует пользователя на указанный срок или навсегда""", + responses = [ + ApiResponse(responseCode = "200", description = "Пользователь заблокирован", content = [Content(schema = Schema(implementation = AdminUsersUserIDBanPost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "404", description = "Пользователь не найден"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/users/{user_id}/ban"], + produces = ["application/json"], + consumes = ["application/json"] + ) +fun adminUsersUserIDBanPost(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody adminUsersUserIDBanPostRequest: AdminUsersUserIDBanPostRequest): ResponseEntity { + try { + val result = userService.banUser(userID, adminUsersUserIDBanPostRequest) + return ResponseEntity.ok(result) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при блокировке пользователя", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Удаление пользователя", + operationId = "adminUsersUserIDDeleteDelete", + description = """Полностью удаляет аккаунт пользователя из системы""", + responses = [ + ApiResponse(responseCode = "204", description = "Пользователь успешно удален"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "404", description = "Пользователь не найден"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/admin/users/{user_id}/delete"], + produces = ["application/json"] + ) +fun adminUsersUserIDDeleteDelete(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int): ResponseEntity { + try { + userService.deleteUser(userID) + return ResponseEntity.noContent().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при удалении пользователя", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Удаление карты из инвентаря пользователя", + operationId = "adminUsersUserIDInventoryCardIDDelete", + description = """Удаляет конкретную карту из коллекции пользователя""", + responses = [ + ApiResponse(responseCode = "204", description = "Карта успешно удалена из инвентаря"), + ApiResponse(responseCode = "400", description = "Неверный запрос"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/admin/users/{user_id}/inventory/{card_ID}"], + produces = ["application/json"] + ) +fun adminUsersUserIDInventoryCardIDDelete(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int,@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + try { + userService.deleteCardFromInventory(userID, cardID) + return ResponseEntity.noContent().build() + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при удалении карты из инвентаря пользователя", e.message ?: "Неизвестная ошибка")) + } +} + + @Operation( + summary = "Сброс выполнения квеста", + operationId = "adminUsersUserIDQuestsQuestIDResetPost", + description = """Сбрасывает статус выполнения квеста для пользователя""", + responses = [ + ApiResponse(responseCode = "200", description = "Квест успешно сброшен", content = [Content(schema = Schema(implementation = AdminUsersUserIDQuestsQuestIDResetPost200Response::class))]), + ApiResponse(responseCode = "400", description = "Неверный запрос"), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Доступ запрещен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/admin/users/{user_id}/quests/{quest_ID}/reset"], + produces = ["application/json"], + consumes = ["application/json"] + ) +fun adminUsersUserIDQuestsQuestIDResetPost(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int,@Parameter(description = "", required = true) @PathVariable("quest_ID") questID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody adminUsersUserIDQuestsQuestIDResetPostRequest: AdminUsersUserIDQuestsQuestIDResetPostRequest): ResponseEntity { + try { + val result = userService.resetQuest(userID, questID, adminUsersUserIDQuestsQuestIDResetPostRequest) + return ResponseEntity.ok(result) + } catch (e: Exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError("Ошибка при сбросе выполнения квеста", e.message ?: "Неизвестная ошибка")) + } +} + +} diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/AuthController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/AuthController.kt index 598741e..a963133 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/AuthController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/AuthController.kt @@ -1,127 +1,498 @@ package ru.vsu.app.controller -import io.swagger.v3.oas.annotations.Operation -import io.swagger.v3.oas.annotations.media.Content -import io.swagger.v3.oas.annotations.media.Schema -import io.swagger.v3.oas.annotations.responses.ApiResponse as SwaggerApiResponse -import io.swagger.v3.oas.annotations.responses.ApiResponses +import ru.vsu.app.dto.responses.auth.login.LoginUser200Response +import ru.vsu.app.dto.responses.auth.login.LoginUser400Response +import ru.vsu.app.dto.responses.auth.login.LoginUser401Response +import ru.vsu.app.dto.requests.LoginUserRequest + +import ru.vsu.app.dto.responses.auth.register.RegisterUser200Response +import ru.vsu.app.dto.responses.auth.register.RegisterUser400Response +import ru.vsu.app.dto.responses.auth.register.RegisterUser409Response +import ru.vsu.app.dto.requests.RegisterUserRequest + +import ru.vsu.app.dto.responses.common.InternalServerError + +import ru.vsu.app.dto.responses.auth.requestpassword.RequestPasswordReset200Response +import ru.vsu.app.dto.responses.auth.requestpassword.RequestPasswordReset400Response +import ru.vsu.app.dto.responses.auth.requestpassword.RequestPasswordReset404Response +import ru.vsu.app.dto.requests.RequestPasswordResetRequest + +import ru.vsu.app.dto.responses.auth.requestpassword.ResetPassword200Response +import ru.vsu.app.dto.responses.auth.requestpassword.ResetPassword400Response +import ru.vsu.app.dto.requests.ResetPasswordRequest + +import ru.vsu.app.dto.responses.auth.verifyemail.ResendVerificationCode200Response +import ru.vsu.app.dto.requests.ResendVerificationCodeRequest + +import ru.vsu.app.dto.responses.auth.verifyemail.VerifyEmail400Response +import ru.vsu.app.dto.responses.auth.verifyemail.VerifyEmail410Response +import ru.vsu.app.dto.requests.VerifyEmailRequest + +import ru.vsu.app.dto.responses.auth.AuthCheckGet401Response + +import ru.vsu.app.dto.responses.auth.refreshtoken.AuthRefreshToken200Response +import ru.vsu.app.dto.responses.auth.refreshtoken.AuthRefreshToken401Response +import ru.vsu.app.dto.requests.AuthRefreshRequest + +import ru.vsu.app.dto.UserDto + +import ru.vsu.app.service.AuthService +import ru.vsu.app.service.JwtService + +import ru.vsu.app.metrics.AuthMetrics + +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity -import org.springframework.security.core.annotation.AuthenticationPrincipal -import org.springframework.security.core.userdetails.UserDetails + import org.springframework.web.bind.annotation.* -import ru.vsu.app.dto.* -import ru.vsu.app.service.UserService +import org.springframework.validation.annotation.Validated +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.Valid +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size + +import kotlin.collections.List +import kotlin.collections.Map @RestController -@RequestMapping("/api/auth") -@Tag(name = "Аутентификация", description = "API для регистрации, входа и управления аккаунтом") -class AuthController(private val userService: UserService) { - - @Operation(summary = "Регистрация нового пользователя") - @ApiResponses(value = [ - SwaggerApiResponse(responseCode = "200", description = "Пользователь успешно зарегистрирован"), - SwaggerApiResponse(responseCode = "400", description = "Некорректные данные или пользователь уже существует") - ]) - @PostMapping("/register") - fun register(@RequestBody request: RegisterRequest): ResponseEntity { - val response = userService.register(request) - return if (response.success) { - ResponseEntity.ok(response) - } else { - ResponseEntity.badRequest().body(response) +@Validated +@RequestMapping("\${api.base-path:/api}") +@SecurityRequirement(name = "Bearer Authentication") +@Tag(name = "Authentication", description = "Регистрация, вход и управление аккаунтом") +class AuthController( + private val authService: AuthService, + private val jwtService: JwtService, + private val authMetrics: AuthMetrics +) { + + @Operation( + summary = "Проверка статуса аутентификации", + operationId = "authCheckGet", + description = """Проверяет, авторизован ли пользователь по сессионной cookie. +Возвращает данные пользователя, если сессия активна. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Пользователь авторизован", content = [Content(schema = Schema(implementation = UserDto::class))]), + ApiResponse(responseCode = "401", description = "Пользователь не авторизован", content = [Content(schema = Schema(implementation = AuthCheckGet401Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/auth/check"], + produces = ["application/json"] + ) + fun authCheckGet(): ResponseEntity { + authMetrics.authCheckAttempt() + val startTime = System.currentTimeMillis() + + val user = authService.getCurrentUser() + val response: ResponseEntity = if (user != null) { + authMetrics.authCheckAuthorized() + ResponseEntity.ok(user) + } else { + authMetrics.authCheckUnauthorized() + ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(AuthCheckGet401Response(false, "Пользователь не авторизован")) + } + + val durationMs = System.currentTimeMillis() - startTime + authMetrics.authCheckDuration(durationMs) + + return response + } + + @Operation( + summary = "Обновление сессии", + operationId = "authRefreshPost", + description = """Обновляет сессионную cookie. +Требуется валидная существующая сессия. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Сессия успешно обновлена", content = [Content(schema = Schema(implementation = AuthRefreshToken200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации", content = [Content(schema = Schema(implementation = AuthRefreshToken401Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/auth/refresh"], + produces = ["application/json"] + ) + fun authRefreshPost( + @Parameter(description = "", required = true) + @Valid @RequestBody request: AuthRefreshRequest + ): ResponseEntity { + val start = System.currentTimeMillis() + authMetrics.refreshAttempt() + + val result = authService.refreshSession(request) + + authMetrics.refreshDuration(System.currentTimeMillis() - start) + + return when (result) { + is AuthRefreshToken200Response -> { + authMetrics.refreshSuccess() + ResponseEntity.ok(result) + } + is AuthRefreshToken401Response -> { + authMetrics.refreshFailure() + ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(result) + } + is InternalServerError -> { + authMetrics.refreshFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result) + } + else -> { + authMetrics.refreshFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build() + } } } - @Operation(summary = "Активация аккаунта по коду") - @ApiResponses(value = [ - SwaggerApiResponse(responseCode = "200", description = "Аккаунт успешно активирован"), - SwaggerApiResponse(responseCode = "400", description = "Неверный код активации") - ]) - @PostMapping("/activate") - fun activateAccount(@RequestBody request: ActivateAccountRequest): ResponseEntity { - val response = userService.activateAccount(request) - return if (response.success) { - ResponseEntity.ok(response) - } else { - ResponseEntity.badRequest().body(response) + @Operation( + summary = "Вход в систему", + operationId = "loginUser", + description = """Аутентификация пользователя по email и паролю. + Если данные введены верно — создает сессионную cookie и возвращает данные пользователя. + """, + responses = [ + ApiResponse(responseCode = "200", description = "Успешный вход", content = [Content(schema = Schema(implementation = LoginUser200Response::class))]), + ApiResponse(responseCode = "400", description = "Неверный формат или данные", content = [Content(schema = Schema(implementation = LoginUser400Response::class))]), + ApiResponse(responseCode = "401", description = "Неверные учётные данные", content = [Content(schema = Schema(implementation = LoginUser401Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) + ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/auth/login"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun loginUser( + @Parameter(description = "", required = true) + @Valid @RequestBody loginUserRequest: LoginUserRequest + ): ResponseEntity { + val start = System.currentTimeMillis() + authMetrics.loginAttempt() + + val response = try { + authService.loginUser(loginUserRequest) + } catch (ex: Exception) { + authMetrics.loginFailure() + authMetrics.loginDuration(System.currentTimeMillis() - start) + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(InternalServerError("Ошибка при попытке входа", ex.message ?: "Неизвестная ошибка")) + } + + val duration = System.currentTimeMillis() - start + authMetrics.loginDuration(duration) + + return when (response) { + is UserDto -> { + val token = jwtService.generateToken(response.email) + + authMetrics.loginSuccess(response.userID.toString()) + ResponseEntity.ok(LoginUser200Response(user = response, accessToken = token)) + } + is LoginUser400Response -> { + authMetrics.loginFailure() + ResponseEntity.badRequest().body(response) + } + is LoginUser401Response -> { + authMetrics.loginFailure() + ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response) + } + else -> { + authMetrics.loginFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build() + } } } - @Operation(summary = "Вход в систему") - @ApiResponses(value = [ - SwaggerApiResponse( - responseCode = "200", - description = "Успешная аутентификация, возвращает JWT токен", - content = [Content(schema = Schema(implementation = LoginResponse::class))] - ), - SwaggerApiResponse( - responseCode = "401", - description = "Неверные учетные данные", - content = [Content(schema = Schema(implementation = ApiResponse::class))] - ) - ]) - @PostMapping("/login") - fun login(@RequestBody request: LoginRequest): ResponseEntity { - val response = userService.login(request) - return if (response != null) { - ResponseEntity.ok(response) - } else { - ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body(ApiResponse("Неверные учетные данные или аккаунт не активирован", false)) + @Operation( + summary = "Регистрация нового пользователя", + operationId = "registerUser", + description = """Принимает email, имя пользователя и пароль. + Если email и пароль валидны, отправляет код подтверждения. + Возвращает временный токен для верификации. + """, + responses = [ + ApiResponse(responseCode = "200", description = "Код подтверждения отправлен", content = [Content(schema = Schema(implementation = RegisterUser200Response::class))]), + ApiResponse(responseCode = "400", description = "Неверный формат данных", content = [Content(schema = Schema(implementation = RegisterUser400Response::class))]), + ApiResponse(responseCode = "409", description = "Пользователь уже существует", content = [Content(schema = Schema(implementation = RegisterUser409Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) + ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/auth/register"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun registerUser( + @Parameter(description = "", required = true) + @Valid @RequestBody registerUserRequest: RegisterUserRequest + ): ResponseEntity { + val start = System.currentTimeMillis() + authMetrics.registerAttempt() + + val response = try { + authService.register(registerUserRequest) + } catch (ex: Exception) { + authMetrics.registerFailure() + authMetrics.registerDuration(System.currentTimeMillis() - start) + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex) + } + + val duration = System.currentTimeMillis() - start + authMetrics.registerDuration(duration) + + return when (response) { + is RegisterUser200Response -> { + authMetrics.registerSuccess() + ResponseEntity.ok(response) + } + is RegisterUser409Response -> { + authMetrics.registerConflict() + ResponseEntity.status(HttpStatus.CONFLICT).body(response) + } + is RegisterUser400Response -> { + authMetrics.registerValidationError() + ResponseEntity.badRequest().body(response) + } + is InternalServerError -> { + authMetrics.registerFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response) + } + else -> { + authMetrics.registerFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build() + } } } - @Operation(summary = "Получение информации о текущем пользователе") - @ApiResponses(value = [ - SwaggerApiResponse( - responseCode = "200", - description = "Информация о пользователе", - content = [Content(schema = Schema(implementation = UserInfoResponse::class))] - ), - SwaggerApiResponse(responseCode = "401", description = "Неавторизованный доступ") - ]) - @GetMapping("/user-info") - fun getUserInfo(@AuthenticationPrincipal userDetails: UserDetails): ResponseEntity { - val response = userService.getUserInfo(userDetails.username) - return ResponseEntity.ok(response) + @Operation( + summary = "Запрос кода для сброса пароля", + operationId = "requestPasswordReset", + description = """Отправляет код подтверждения для сброса пароля, если email существует в системе""", + responses = [ + ApiResponse(responseCode = "200", description = "Код подтверждения отправлен на email", content = [Content(schema = Schema(implementation = RequestPasswordReset200Response::class))]), + ApiResponse(responseCode = "400", description = "Неверный формат данных", content = [Content(schema = Schema(implementation = RequestPasswordReset400Response::class))]), + ApiResponse(responseCode = "404", description = "Пользователь с указанным email не найден", content = [Content(schema = Schema(implementation = RequestPasswordReset404Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/auth/forgot-password"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun requestPasswordReset(@Parameter(description = "", required = true) @Valid @RequestBody request: RequestPasswordResetRequest): ResponseEntity { + val start = System.currentTimeMillis() + authMetrics.passwordResetAttempt() + + return try { + val result = authService.requestPasswordReset(request) + + val duration = System.currentTimeMillis() - start + authMetrics.passwordResetDuration(duration) + + when (result) { + is RequestPasswordReset200Response -> { + authMetrics.passwordResetSuccess() + ResponseEntity.ok(result) + } + is RequestPasswordReset404Response -> { + authMetrics.passwordResetFailure() + ResponseEntity.status(HttpStatus.NOT_FOUND).body(result) + } + is RequestPasswordReset400Response -> { + authMetrics.passwordResetFailure() + ResponseEntity.badRequest().body(result) + } + else -> { + authMetrics.passwordResetFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(InternalServerError(error = "Ошибка: Неожиданный формат ответа")) + } + } + } catch (ex: Exception) { + authMetrics.passwordResetFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(InternalServerError(error = "Ошибка при сбросе пароля: "+ ex.message ?: "Неизвестная ошибка")) + } } - @Operation(summary = "Запрос на сброс пароля") - @ApiResponses(value = [ - SwaggerApiResponse( - responseCode = "200", - description = "Код для сброса пароля отправлен на email", - content = [Content(schema = Schema(implementation = ApiResponse::class))] - ) - ]) - @PostMapping("/forgot-password") - fun forgotPassword(@RequestBody request: ForgotPasswordRequest): ResponseEntity { - val response = userService.forgotPassword(request) - return ResponseEntity.ok(response) + @Operation( + summary = "Повторная отправка кода", + operationId = "resendVerificationCode", + description = """Отправляет новый код подтверждения""", + responses = [ + ApiResponse(responseCode = "200", description = "Новый код отправлен", content = [Content(schema = Schema(implementation = ResendVerificationCode200Response::class))]), + ApiResponse(responseCode = "400", description = "Неверный формат запроса"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/auth/resend-code"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun resendVerificationCode( + @Parameter(description = "Email пользователя", required = true) + @Valid @RequestBody resendVerificationCodeRequest: ResendVerificationCodeRequest + ): ResponseEntity { + val start = System.currentTimeMillis() + authMetrics.resendVerificationCodeAttempt() + + val response = try { + authService.resendVerificationCode(resendVerificationCodeRequest) + } catch (ex: Exception) { + authMetrics.resendVerificationCodeFailure() + authMetrics.resendVerificationCodeDuration(System.currentTimeMillis() - start) + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(InternalServerError(error = ex.message ?: "Internal error")) + } + + val duration = System.currentTimeMillis() - start + authMetrics.resendVerificationCodeDuration(duration) + + return when (response) { + is ResendVerificationCode200Response -> { + authMetrics.resendVerificationCodeSuccess() + ResponseEntity.ok(response) + } + is InternalServerError -> { + authMetrics.resendVerificationCodeFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response) + } + else -> { + authMetrics.resendVerificationCodeFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build() + } + } } - - @Operation(summary = "Установка нового пароля по коду из email") - @ApiResponses(value = [ - SwaggerApiResponse( - responseCode = "200", - description = "Пароль успешно изменен", - content = [Content(schema = Schema(implementation = ApiResponse::class))] - ), - SwaggerApiResponse( - responseCode = "400", - description = "Неверный код или код устарел", - content = [Content(schema = Schema(implementation = ApiResponse::class))] - ) - ]) - @PostMapping("/reset-password") - fun resetPassword(@RequestBody request: ResetPasswordRequest): ResponseEntity { - val response = userService.resetPassword(request) - return if (response.success) { - ResponseEntity.ok(response) - } else { - ResponseEntity.badRequest().body(response) + + @Operation( + summary = "Сброс пароля", + operationId = "resetPassword", + description = """Устанавливает новый пароль после подтверждения кода""", + responses = [ + ApiResponse(responseCode = "200", description = "Пароль изменен", content = [Content(schema = Schema(implementation = ResetPassword200Response::class))]), + ApiResponse(responseCode = "400", description = "Неверный код или токен", content = [Content(schema = Schema(implementation = ResetPassword400Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/auth/reset-password"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun resetPassword( + @Parameter(description = "", required = true) + @Valid @RequestBody resetPasswordRequest: ResetPasswordRequest + ): ResponseEntity { + val start = System.currentTimeMillis() + authMetrics.resetPasswordAttempt() + + val response = try { + authService.resetPassword(resetPasswordRequest) + } catch (ex: Exception) { + authMetrics.resetPasswordFailure() + authMetrics.resetPasswordDuration(System.currentTimeMillis() - start) + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(InternalServerError("Ошибка при сбросе пароля", ex.message ?: "Неизвестная ошибка")) + } + + val duration = System.currentTimeMillis() - start + authMetrics.resetPasswordDuration(duration) + + return when (response) { + is ResetPassword200Response -> { + authMetrics.resetPasswordSuccess() + ResponseEntity.ok(response) + } + is ResetPassword400Response -> { + authMetrics.resetPasswordValidationError() + ResponseEntity.badRequest().body(response) + } + is InternalServerError -> { + authMetrics.resetPasswordFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response) + } + else -> { + authMetrics.resetPasswordFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Неизвестная ошибка") + } + } + } + + @Operation( + summary = "Подтверждение email", + operationId = "verifyEmail", + description = """Проверяет код подтверждения. +Если код подтверждения верный - создает аккаунт и возвращает данные пользователя. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Успешная регистрация", content = [Content(schema = Schema(implementation = UserDto::class))]), + ApiResponse(responseCode = "400", description = "Неверный код или токен", content = [Content(schema = Schema(implementation = VerifyEmail400Response::class))]), + ApiResponse(responseCode = "410", description = "Истек срок действия кода", content = [Content(schema = Schema(implementation = VerifyEmail410Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/auth/verify"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun verifyEmail(@Parameter(description = "", required = true) @Valid @RequestBody verifyEmailRequest: VerifyEmailRequest): ResponseEntity { + val start = System.currentTimeMillis() + authMetrics.verifyAttempt() + + val response = try { + authService.verifyEmail(verifyEmailRequest) + } catch (ex: Exception) { + authMetrics.verifyFailure() + authMetrics.verifyDuration(System.currentTimeMillis() - start) + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(InternalServerError("Ошибка при подтверждении email", ex.message ?: "Неизвестная ошибка")) + } + + val duration = System.currentTimeMillis() - start + authMetrics.verifyDuration(duration) + + return when (response) { + is UserDto -> { + authMetrics.verifySuccess(response.userID.toString()) + ResponseEntity.ok(response) + } + is VerifyEmail400Response -> { + authMetrics.verifyValidationError() + ResponseEntity.badRequest().body(response) + } + is VerifyEmail410Response -> { + authMetrics.verifyExpired() + ResponseEntity.status(HttpStatus.GONE).body(response) + } + else -> { + authMetrics.verifyFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build() + } } } -} \ No newline at end of file +} diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/CardController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/CardController.kt index 27e197e..1e3cf77 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/CardController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/CardController.kt @@ -1,41 +1,86 @@ -package ru.vsu.app.controller +// package ru.vsu.app.controller -import org.springframework.http.ResponseEntity -import org.springframework.security.core.annotation.AuthenticationPrincipal -import org.springframework.web.bind.annotation.* -import ru.vsu.app.dto.CardResponse -import ru.vsu.app.model.User -import ru.vsu.app.service.CardService +// import org.springframework.http.ResponseEntity +// import org.springframework.security.core.annotation.AuthenticationPrincipal +// import org.springframework.web.bind.annotation.* +// import io.swagger.v3.oas.annotations.tags.Tag +// import io.swagger.v3.oas.annotations.Operation +// import io.swagger.v3.oas.annotations.Parameter +// import io.swagger.v3.oas.annotations.media.Content +// import io.swagger.v3.oas.annotations.media.Schema +// import io.swagger.v3.oas.annotations.responses.ApiResponse +// import ru.vsu.app.dto.CardDto +// import ru.vsu.app.model.UserEntity +// import ru.vsu.app.service.InventoryService -@RestController -@RequestMapping("/api/cards") -class CardController( - private val cardService: CardService -) { - @GetMapping("/inventory") - fun getUserInventory( - @AuthenticationPrincipal user: User, - @RequestParam(required = false, defaultValue = "rarity") sortBy: String - ): ResponseEntity> { - val cards = cardService.getUserCards(user, sortBy) - return ResponseEntity.ok(cards) - } +// @RestController +// @RequestMapping("/api/card") +// @SecurityRequirement(name = "Bearer Authentication") +// @Tag(name = "Card", description = "Функции, связанные с карточками пользователя") +// class CardController( +// private val cardService: InventoryService +// ) { - @GetMapping("/{cardId}") - fun getCardDetails( - @PathVariable cardId: Long, - @AuthenticationPrincipal user: User - ): ResponseEntity { - val card = cardService.getCardDetails(cardId, user) - return ResponseEntity.ok(card) - } +// @Operation( +// summary = "Получить инвентарь пользователя", +// description = "Возвращает список карточек, принадлежащих пользователю. Можно указать параметр сортировки (по умолчанию — по редкости).", +// parameters = [ +// Parameter(name = "sortBy", description = "Критерий сортировки (rarity, name, и т.д.)", required = false) +// ], +// responses = [ +// ApiResponse(responseCode = "200", description = "Список карточек успешно получен", +// content = [Content(mediaType = "application/json", schema = Schema(implementation = CardResponse::class))]) +// ] +// ) +// @GetMapping("/inventory") +// fun getUserInventory( +// @AuthenticationPrincipal user: UserEntity, +// @RequestParam(required = false, defaultValue = "rarity") sortBy: String +// ): ResponseEntity { +// val cards = cardService.getUserCards(user, sortBy) +// return ResponseEntity.ok(cards) +// } - @PostMapping("/{cardId}/disassemble") - fun disassembleCard( - @PathVariable cardId: Long, - @AuthenticationPrincipal user: User - ): ResponseEntity> { - val coinsReceived = cardService.disassembleCard(cardId, user) - return ResponseEntity.ok(mapOf("coinsReceived" to coinsReceived)) - } -} \ No newline at end of file +// @Operation( +// summary = "Получить подробную информацию о карточке", +// description = "Возвращает подробные сведения о конкретной карточке пользователя по её ID.", +// parameters = [ +// Parameter(name = "cardId", description = "ID карточки", required = true) +// ], +// responses = [ +// ApiResponse(responseCode = "200", description = "Карточка успешно получена", +// content = [Content(mediaType = "application/json", schema = Schema(implementation = CardResponse::class))]), +// ApiResponse(responseCode = "404", description = "Карточка не найдена") +// ] +// ) +// @GetMapping("/{cardId}") +// fun getCardDetails( +// @PathVariable cardId: Long, +// @AuthenticationPrincipal user: UserEntity +// ): ResponseEntity { +// val card = cardService.getCardDetails(cardId, user) +// return ResponseEntity.ok(card) +// } + +// @Operation( +// summary = "Разобрать карточку на монеты", +// description = "Позволяет пользователю разобрать карточку и получить за неё внутриигровую валюту.", +// parameters = [ +// Parameter(name = "cardId", description = "ID карточки", required = true) +// ], +// responses = [ +// ApiResponse(responseCode = "200", description = "Карточка успешно разобрана", +// content = [Content(mediaType = "application/json", schema = Schema(example = """{"coinsReceived": 100}"""))]), +// ApiResponse(responseCode = "400", description = "Невозможно разобрать карточку"), +// ApiResponse(responseCode = "404", description = "Карточка не найдена") +// ] +// ) +// @PostMapping("/{cardId}/disassemble") +// fun disassembleCard( +// @PathVariable cardId: Long, +// @AuthenticationPrincipal user: UserEntity +// ): ResponseEntity { +// val coinsReceived = cardService.disassembleCard(cardId, user) +// return ResponseEntity.ok(mapOf("coinsReceived" to coinsReceived)) +// } +// } diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/HomeController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/HomeController.kt new file mode 100644 index 0000000..d19712f --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/controller/HomeController.kt @@ -0,0 +1,272 @@ +package ru.vsu.app.controller + +import ru.vsu.app.dto.responses.home.HomeGenerateCardPost200Response +import ru.vsu.app.dto.responses.home.HomeGenerateCardPost402Response +import ru.vsu.app.dto.requests.HomeGenerateCardPostRequest +import ru.vsu.app.dto.responses.home.HomeNewsGet404Response +import ru.vsu.app.dto.responses.home.HomeNotificationsNotificationIDNavigatePost200Response +import ru.vsu.app.dto.responses.home.HomeNotificationsNotificationIDNavigatePost404Response +import ru.vsu.app.dto.responses.home.HomeQuestsClaimRewardPost200Response +import ru.vsu.app.dto.responses.home.HomeQuestsClaimRewardPost400Response +import ru.vsu.app.dto.requests.HomeQuestsClaimRewardPostRequest +import ru.vsu.app.dto.responses.home.HomeQuestsGet200Response +import ru.vsu.app.dto.responses.home.HomeQuestsQuestIDChangeStatusPost200Response +import ru.vsu.app.dto.responses.home.HomeSearchGet400Response +import ru.vsu.app.dto.responses.home.HomeSearchGet404Response +import ru.vsu.app.dto.NewsDto +import ru.vsu.app.dto.NotificationDto +import ru.vsu.app.dto.responses.common.InternalServerError +import ru.vsu.app.dto.ThemeDto +import ru.vsu.app.dto.UserDto +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* +import org.springframework.validation.annotation.Validated +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.Valid +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size + +import kotlin.collections.List +import kotlin.collections.Map + +@RestController +@Validated +@RequestMapping("\${api.base-path:/api}") +@SecurityRequirement(name = "Bearer Authentication") +@Tag(name = "Home", description = "Операции на странице \"Главное меню\"") +class HomeController() { + @Operation( + summary = "Генерация уникальной карты", + operationId = "homeGenerateCardPost", + description = """Генерирует уникальную карту по выбранной теме, если на балансе достаточно средств +""", + responses = [ + ApiResponse(responseCode = "200", description = "Карта успешно сгенерирована", content = [Content(schema = Schema(implementation = HomeGenerateCardPost200Response::class))]), + ApiResponse(responseCode = "402", description = "Недостаточно средств", content = [Content(schema = Schema(implementation = HomeGenerateCardPost402Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/home/generate-card"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun homeGenerateCardPost(@Parameter(description = "", required = true) @Valid @RequestBody homeGenerateCardPostRequest: HomeGenerateCardPostRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение списка тем для генерации карт", + operationId = "homeGenerateCardThemesGet", + description = """Возвращает список доступных тем для генерации уникальных карт +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список тем", content = [Content(array = ArraySchema(schema = Schema(implementation = ThemeDto::class)))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/home/generate-card/themes"], + produces = ["application/json"] + ) + fun homeGenerateCardThemesGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение списока новостей", + operationId = "homeNewsGet", + description = """Возвращает список новостей +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список новостей", content = [Content(array = ArraySchema(schema = Schema(implementation = NewsDto::class)))]), + ApiResponse(responseCode = "404", description = "Новостей не найдено", content = [Content(schema = Schema(implementation = HomeNewsGet404Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/home/news"], + produces = ["application/json"] + ) + fun homeNewsGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Просмотр новости", + operationId = "homeNewsNewsIDGet", + description = """Возвращает полную информацию по конкретной новости +""", + responses = [ + ApiResponse(responseCode = "200", description = "Детальная информация о новости", content = [Content(schema = Schema(implementation = NewsDto::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/home/news/{news_ID}"], + produces = ["application/json"] + ) + fun homeNewsNewsIDGet(@Parameter(description = "", required = true) @PathVariable("news_ID") newsID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Просмотр уведомлений", + operationId = "homeNotificationsGet", + description = """Возвращает список уведомлений пользователя +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список уведомлений", content = [Content(array = ArraySchema(schema = Schema(implementation = NotificationDto::class)))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/home/notifications"], + produces = ["application/json"] + ) + fun homeNotificationsGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Просмотр уведомления", + operationId = "homeNotificationsNotificationIDGet", + description = """Возвращает информацию по конкретному уведомлению +""", + responses = [ + ApiResponse(responseCode = "200", description = "Детальная информация об уведомлении", content = [Content(schema = Schema(implementation = NotificationDto::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/home/notifications/{notification_ID}"], + produces = ["application/json"] + ) + fun homeNotificationsNotificationIDGet(@Parameter(description = "", required = true) @PathVariable("notification_ID") notificationID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Переход по ссылке в уведомлении", + operationId = "homeNotificationsNotificationIDNavigatePost", + description = """Перенаправляет на страницу обмена, связанного с уведомлением +""", + responses = [ + ApiResponse(responseCode = "200", description = "Перенаправление на страницу обмена", content = [Content(schema = Schema(implementation = HomeNotificationsNotificationIDNavigatePost200Response::class))]), + ApiResponse(responseCode = "404", description = "Предложение обмена не найдено", content = [Content(schema = Schema(implementation = HomeNotificationsNotificationIDNavigatePost404Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/home/notifications/{notification_ID}/navigate"], + produces = ["application/json"] + ) + fun homeNotificationsNotificationIDNavigatePost(@Parameter(description = "", required = true) @PathVariable("notification_ID") notificationID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение награды за выполнение квестов", + operationId = "homeQuestsClaimRewardPost", + description = """Выдает награду за выполнение квестов +""", + responses = [ + ApiResponse(responseCode = "200", description = "Награда получена", content = [Content(schema = Schema(implementation = HomeQuestsClaimRewardPost200Response::class))]), + ApiResponse(responseCode = "400", description = "Не все квесты выполнены", content = [Content(schema = Schema(implementation = HomeQuestsClaimRewardPost400Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/home/quests/claim-reward"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun homeQuestsClaimRewardPost(@Parameter(description = "", required = true) @Valid @RequestBody homeQuestsClaimRewardPostRequest: HomeQuestsClaimRewardPostRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение списка квестов", + operationId = "homeQuestsGet", + description = """Возвращает список ежедневных и еженедельных квестов с их статусом +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список квестов", content = [Content(schema = Schema(implementation = HomeQuestsGet200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/home/quests"], + produces = ["application/json"] + ) + fun homeQuestsGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Изменение статуса выполнения квеста", + operationId = "homeQuestsQuestIDChangeStatusPost", + description = """Изменение статуса выполнения квеста +""", + responses = [ + ApiResponse(responseCode = "200", description = "Статус квеста успешно изменен", content = [Content(schema = Schema(implementation = HomeQuestsQuestIDChangeStatusPost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/home/quests/{quest_ID}/change-status"], + produces = ["application/json"] + ) + fun homeQuestsQuestIDChangeStatusPost(@Parameter(description = "", required = true) @PathVariable("quest_ID") questID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Поиск пользователей", + operationId = "homeSearchGet", + description = """Поиск пользователей по никнейму или ID +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список найденных пользователей", content = [Content(array = ArraySchema(schema = Schema(implementation = UserDto::class)))]), + ApiResponse(responseCode = "400", description = "Неверный запрос", content = [Content(schema = Schema(implementation = HomeSearchGet400Response::class))]), + ApiResponse(responseCode = "404", description = "Пользователь не найден", content = [Content(schema = Schema(implementation = HomeSearchGet404Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/home/search"], + produces = ["application/json"] + ) + fun homeSearchGet( @RequestParam(value = "query", required = false) query: kotlin.String?): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt new file mode 100644 index 0000000..4b1f0e2 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/controller/InventoryController.kt @@ -0,0 +1,322 @@ +package ru.vsu.app.controller + +import ru.vsu.app.dto.responses.inventory.InventoryCardCardIDFavoriteGet200Response + +import ru.vsu.app.dto.responses.inventory.InventoryCardCardIDQuantityGet200Response + +import ru.vsu.app.dto.responses.inventory.InventoryCardCardIDTradeCancelPost200Response +import ru.vsu.app.dto.responses.inventory.InventoryCardCardIDTradeStatusGet200Response + +import ru.vsu.app.dto.responses.inventory.InventoryDestroyPost200Response + +import ru.vsu.app.dto.responses.inventory.InventoryFavoritesCountGet200Response + +import ru.vsu.app.dto.responses.inventory.InventoryGet200Response +import ru.vsu.app.dto.responses.inventory.InventoryGet401Response + +import ru.vsu.app.dto.requests.InventoryDestroyPostRequest + +import ru.vsu.app.dto.responses.profile.OtherProfileCardCardIDInitiateTradePost200Response + +import ru.vsu.app.dto.responses.common.InternalServerError + +import ru.vsu.app.model.UserEntity + +import ru.vsu.app.service.InventoryService + +import ru.vsu.app.metrics.InventoryMetrics + +import ru.vsu.app.security.CustomUserDetails + +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* +import io.swagger.v3.oas.annotations.tags.Tag + +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* +import org.springframework.validation.annotation.Validated +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.server.ResponseStatusException + +import jakarta.validation.Valid +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size + + +import kotlin.collections.List +import kotlin.collections.Map + +@RestController +@Validated +@RequestMapping("\${api.base-path:/api}") +@SecurityRequirement(name = "Bearer Authentication") +@Tag(name = "Inventory", description = "Операции на странице \"Инвентарь\"") +class InventoryController( + private val inventoryService: InventoryService, + private val inventoryMetrics: InventoryMetrics +) { + + @Operation( + summary = "Добавление карты в избранное", + operationId = "inventoryCardCardIDFavoriteAddPost", + description = """Добавляет карту в избранное, если: +- Карта не добавлена в "избранное" +- Общее количество избранных карт < 5 +""", + responses = [ + ApiResponse(responseCode = "200", description= "Карта успешно добавлена в избранное"), + ApiResponse(responseCode = "400", description = "Невозможно добавить карту в избранное"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/inventory/card/{card_ID}/favorite-add"], + produces = ["application/json"] + ) + fun inventoryCardCardIDFavoriteAddPost(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Удаление карты из избранного", + operationId = "inventoryCardCardIDFavoriteDeleteDelete", + description = """Удаляет карту из избранного. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Карта успешно удалена из избранного"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/inventory/card/{card_ID}/favorite-delete"], + produces = ["application/json"] + ) + fun inventoryCardCardIDFavoriteDeleteDelete(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Проверка статуса карты", + operationId = "inventoryCardCardIDFavoriteGet", + description = """Проверяет, добавлена ли карта в избранное. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Статус избранного", content = [Content(schema = Schema(implementation = InventoryCardCardIDFavoriteGet200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/inventory/card/{card_ID}/favorite"], + produces = ["application/json"] + ) + fun inventoryCardCardIDFavoriteGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Проверка количества экземпляров карты", + operationId = "inventoryCardCardIDQuantityGet", + description = """Возвращает количество экземпляров карты у пользователя. +Если экземпляров больше 1 - можно разбирать/выставлять на обмен. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Информация о количестве карт", content = [Content(schema = Schema(implementation = InventoryCardCardIDQuantityGet200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/inventory/card/{card_ID}/quantity"], + produces = ["application/json"] + ) + fun inventoryCardCardIDQuantityGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Снятие карты с обмена", + operationId = "inventoryCardCardIDTradeCancelPost", + description = """Возвращает карту из раздела "На обмен" в Инвентарь""", + responses = [ + ApiResponse(responseCode = "200", description = "Карта успешно снята с обмена", content = [Content(schema = Schema(implementation = InventoryCardCardIDTradeCancelPost200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/inventory/card/{card_ID}/trade-cancel"], + produces = ["application/json"] + ) + fun inventoryCardCardIDTradeCancelPost(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Проверка статуса обмена карты", + operationId = "inventoryCardCardIDTradeStatusGet", + description = """Проверяет, выставлена ли карта на обмен. +Возвращает статус карты. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Статус обмена карты", content = [Content(schema = Schema(implementation = InventoryCardCardIDTradeStatusGet200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/inventory/card/{card_ID}/trade-status"], + produces = ["application/json"] + ) + fun inventoryCardCardIDTradeStatusGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Разбор карты на валюту", + operationId = "inventoryDestroyPost", + description = """Разбирает карту на валюту, если: +- У пользователя есть минимум 2 экземпляра этой карты +- Добавляет стоимость карты на баланс пользователя +""", + responses = [ + ApiResponse(responseCode = "200", description = "Карта успешно разобрана", content = [Content(schema = Schema(implementation = InventoryDestroyPost200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/inventory/destroy"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun inventoryDestroyPost(@Parameter(description = "", required = true) @Valid @RequestBody inventoryDestroyPostRequest: InventoryDestroyPostRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение количества избранных карт", + operationId = "inventoryFavoritesCountGet", + description = """Возвращает текущее количество избранных карт. +Если количество меньше 5 - можно добавлять новые карты в избранное. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Количество избранных карт", content = [Content(schema = Schema(implementation = InventoryFavoritesCountGet200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/inventory/favorites/count"], + produces = ["application/json"] + ) + fun inventoryFavoritesCountGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение данных инвентаря", + operationId = "inventoryGet", + description = """ + Загружает данные инвентаря пользователя. + + - Требуется авторизация (JWT) + - Возвращает: + - список карт пользователя, + - список избранных карт, + - текущий баланс пользователя. + """, + responses = [ + ApiResponse( + responseCode = "200", + description = "Инвентарь успешно загружен", + content = [Content(schema = Schema(implementation = InventoryGet200Response::class))] + ), + ApiResponse( + responseCode = "401", + description = "Пользователь не авторизован", + content = [Content(schema = Schema(implementation = InventoryGet401Response::class))] + ), + ApiResponse( + responseCode = "500", + description = "Внутренняя ошибка сервера", + content = [Content(schema = Schema(implementation = InternalServerError::class))] + ) + ], + security = [SecurityRequirement(name = "bearerAuth")] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/inventory"], + produces = ["application/json"] + ) + fun inventoryGet( + @Parameter(hidden = true) @AuthenticationPrincipal user: CustomUserDetails + ): ResponseEntity { + val userEntity = user.getUser() + val start = System.currentTimeMillis() + inventoryMetrics.inventoryGetAttempt() + + return try { + val result = inventoryService.getInventoryData(userEntity) + + inventoryMetrics.inventoryGetSuccess() + inventoryMetrics.inventoryGetDuration(System.currentTimeMillis() - start) + + ResponseEntity.ok(result) + } catch (ex: ResponseStatusException) { + if (ex.statusCode == HttpStatus.UNAUTHORIZED) { + inventoryMetrics.inventoryGetFailure() + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body( + InventoryGet401Response(false, "Неавторизованный доступ: ${ex.reason}") + ) + } else { + throw ex + } + } catch (ex: Exception) { + inventoryMetrics.inventoryGetFailure() + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( + InternalServerError(error = "Ошибка при получении инвентаря: ${ex.message ?: "Неизвестная ошибка"}") + ) + } + } + + @Operation( + summary = "Выставление карты на обмен", + operationId = "inventoryPutOnTradePost", + description = """Выставляет карту на обмен, если: +- У пользователя есть минимум 2 экземпляра этой карты +- Карта еще не выставлена на обмен +""", + responses = [ + ApiResponse(responseCode = "200", description = "Карта готова к обмену", content = [Content(schema = Schema(implementation = OtherProfileCardCardIDInitiateTradePost200Response::class))]), + ApiResponse(responseCode = "400", description = "Невозможно выставить карту на обмен"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/inventory/put-on-trade"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun inventoryPutOnTradePost(@Parameter(description = "", required = true) @Valid @RequestBody inventoryDestroyPostRequest: InventoryDestroyPostRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/OtherProfileController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/OtherProfileController.kt new file mode 100644 index 0000000..820d6a5 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/controller/OtherProfileController.kt @@ -0,0 +1,166 @@ +package ru.vsu.app.controller + +import ru.vsu.app.dto.responses.profile.OtherProfileCardCardIDInitiateTradePost400Response +import ru.vsu.app.dto.responses.profile.OtherProfileCardCardIDInitiateTradePost409Response +import ru.vsu.app.dto.responses.profile.OtherProfileCardCardIDInitiateTradePost200Response +import ru.vsu.app.dto.responses.profile.OtherProfileCardCardIDViewGet200Response +import ru.vsu.app.dto.responses.profile.OtherProfileUserIDGet200Response +import ru.vsu.app.dto.responses.profile.OtherProfileUserIDInventoryGet200Response +import ru.vsu.app.dto.responses.profile.OtherProfileUserIDInventoryGet403Response +import ru.vsu.app.dto.responses.profile.OtherProfileUserIDReportPost200Response +import ru.vsu.app.dto.requests.OtherProfileUserIDReportPostRequest +import ru.vsu.app.dto.responses.profile.ProfileAchievementsGet200Response +import ru.vsu.app.dto.responses.common.InternalServerError +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* +import org.springframework.validation.annotation.Validated +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.Valid +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size + +import kotlin.collections.List +import kotlin.collections.Map + +@RestController +@Validated +@RequestMapping("\${api.base-path:/api}") +@SecurityRequirement(name = "Bearer Authentication") +@Tag(name = "OtherProfile", description = "Операции при просмотре профиля другого пользователя") +class OtherProfileController() { + + @Operation( + summary = "Предложение обмена", + operationId = "otherProfileCardCardIDInitiateTradePost", + description = """Создает предложение обмена для указанной карты +и перенаправляет на страницу создания обмена +""", + responses = [ + ApiResponse(responseCode = "200", description = "Перенаправление на создание обмена", content = [Content(schema = Schema(implementation = OtherProfileCardCardIDInitiateTradePost200Response::class))]), + ApiResponse(responseCode = "400", description = "Невозможно предложить обмен", content = [Content(schema = Schema(implementation = OtherProfileCardCardIDInitiateTradePost400Response::class))]), + ApiResponse(responseCode = "409", description = "Конфликт при попытке обмена", content = [Content(schema = Schema(implementation = OtherProfileCardCardIDInitiateTradePost409Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/other-profile/card/{card_ID}/initiate-trade"], + produces = ["application/json"] + ) + fun otherProfileCardCardIDInitiateTradePost(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int, @RequestParam(value = "owner_ID", required = true) ownerID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Просмотр карты другого пользователя", + operationId = "otherProfileCardCardIDViewGet", + description = """Возвращает детальную информацию о карте другого пользователя +и возвращает кнопку для создания предложения обмена +""", + responses = [ + ApiResponse(responseCode = "200", description = "Информация о карте", content = [Content(schema = Schema(implementation = OtherProfileCardCardIDViewGet200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/other-profile/card/{card_ID}/view"], + produces = ["application/json"] + ) + fun otherProfileCardCardIDViewGet(@Parameter(description = "", required = true) @PathVariable("card_ID") cardID: kotlin.Int, @RequestParam(value = "owner_ID", required = true) ownerID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение списка достижений другого пользователя", + operationId = "otherProfileUserIDAchievementsGet", + description = """Возвращает полный список достижений с текущим статусом выполнения +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список достижений", content = [Content(schema = Schema(implementation = ProfileAchievementsGet200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/other-profile/{user_id}/achievements"], + produces = ["application/json"] + ) + fun otherProfileUserIDAchievementsGet(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение данных профиля другого пользователя", + operationId = "otherProfileUserIDGet", + description = """Загружает данные профиля другого пользователя. +- Возвращает статистику, избранные карты и достижения +- Проверяет настройки приватности инвентаря +""", + responses = [ + ApiResponse(responseCode = "200", description = "Данные профиля", content = [Content(schema = Schema(implementation = OtherProfileUserIDGet200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/other-profile/{user_id}"], + produces = ["application/json"] + ) + fun otherProfileUserIDGet(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Просмотр инвентаря другого пользователя", + operationId = "otherProfileUserIDInventoryGet", + description = """Возвращает инвентарь пользователя, если он не скрыт в настройках +""", + responses = [ + ApiResponse(responseCode = "200", description = "Инвентарь пользователя", content = [Content(schema = Schema(implementation = OtherProfileUserIDInventoryGet200Response::class))]), + ApiResponse(responseCode = "403", description = "Инвентарь скрыт", content = [Content(schema = Schema(implementation = OtherProfileUserIDInventoryGet403Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/other-profile/{user_id}/inventory"], + produces = ["application/json"] + ) + fun otherProfileUserIDInventoryGet(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Подача жалобы на пользователя", + operationId = "otherProfileUserIDReportPost", + description = """Перенаправляет на страницу создания жалобы. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Перенаправление на страницу создания жалобы", content = [Content(schema = Schema(implementation = OtherProfileUserIDReportPost200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/other-profile/{user_id}/report"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun otherProfileUserIDReportPost(@Parameter(description = "", required = true) @PathVariable("user_id") userID: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody otherProfileUserIDReportPostRequest: OtherProfileUserIDReportPostRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/ProfileController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/ProfileController.kt new file mode 100644 index 0000000..6ed257d --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/controller/ProfileController.kt @@ -0,0 +1,180 @@ +package ru.vsu.app.controller + +import ru.vsu.app.dto.responses.profile.ProfileAchievementsFavoritesCountGet200Response +import ru.vsu.app.dto.responses.profile.ProfileAchievementsGet200Response +import ru.vsu.app.dto.responses.profile.ProfileGet200Response +import ru.vsu.app.dto.responses.common.InternalServerError +import ru.vsu.app.dto.UserSettingsDto +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* +import org.springframework.validation.annotation.Validated +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.Valid +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size + +import kotlin.collections.List +import kotlin.collections.Map + +@RestController +@Validated +@RequestMapping("\${api.base-path:/api}") +@SecurityRequirement(name = "Bearer Authentication") +@Tag(name = "Profile", description = "Операции в профиле пользователя") +class ProfileController() { + + @Operation( + summary = "Добавить достижение в избранное", + operationId = "profileAchievementsAchievementIDFavoriteAddPost", + description = """Добавляет достижение в избранное (максимум 4) +""", + responses = [ + ApiResponse(responseCode = "200", description = "Успешно добавлено в избранное"), + ApiResponse(responseCode = "400", description = "Невозможно добавить в избранное (лимит достигнут)"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/profile/achievements/{achievement_ID}/favorite-add"], + produces = ["application/json"] + ) + fun profileAchievementsAchievementIDFavoriteAddPost(@Parameter(description = "", required = true) @PathVariable("achievement_ID") achievementID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Удалить достижение из избранного", + operationId = "profileAchievementsAchievementIDFavoriteDeleteDelete", + description = """Удаляет достижение из избранного +""", + responses = [ + ApiResponse(responseCode = "200", description = "Успешно удалено из избранного"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.DELETE], + value = ["/profile/achievements/{achievement_ID}/favorite-delete"], + produces = ["application/json"] + ) + fun profileAchievementsAchievementIDFavoriteDeleteDelete(@Parameter(description = "", required = true) @PathVariable("achievement_ID") achievementID: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Проверка количества избранных достижений", + operationId = "profileAchievementsFavoritesCountGet", + description = """Возвращает текущее количество избранных достижений +""", + responses = [ + ApiResponse(responseCode = "200", description = "Количество избранных", content = [Content(schema = Schema(implementation = ProfileAchievementsFavoritesCountGet200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/profile/achievements/favorites/count"], + produces = ["application/json"] + ) + fun profileAchievementsFavoritesCountGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение списка достижений", + operationId = "profileAchievementsGet", + description = """Возвращает полный список достижений с текущим статусом выполнения +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список достижений", content = [Content(schema = Schema(implementation = ProfileAchievementsGet200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/profile/achievements"], + produces = ["application/json"] + ) + fun profileAchievementsGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение данных профиля", + operationId = "profileGet", + description = """Загружает данные профиля пользователя. +- Проверяет авторизацию +- Возвращает статистику, избранные карты и достижения +""", + responses = [ + ApiResponse(responseCode = "200", description = "Данные профиля", content = [Content(schema = Schema(implementation = ProfileGet200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/profile"], + produces = ["application/json"] + ) + fun profileGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Обновление настроек", + operationId = "profileSettingsChangePut", + description = """Обновляет настройки профиля +""", + responses = [ + ApiResponse(responseCode = "200", description = "Настройки обновлены", content = [Content(schema = Schema(implementation = UserSettingsDto::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.PUT], + value = ["/profile/settings-change"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun profileSettingsChangePut(@Parameter(description = "", required = true) @Valid @RequestBody userSettings: UserSettingsDto): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } + + @Operation( + summary = "Получение настроек", + operationId = "profileSettingsGet", + description = """Возвращает текущие настройки профиля +""", + responses = [ + ApiResponse(responseCode = "200", description = "Настройки профиля", content = [Content(schema = Schema(implementation = UserSettingsDto::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/profile/settings"], + produces = ["application/json"] + ) + fun profileSettingsGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/ShopController (old).kt b/backend/src/main/kotlin/ru/vsu/app/controller/ShopController (old).kt new file mode 100644 index 0000000..258e939 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/controller/ShopController (old).kt @@ -0,0 +1,90 @@ +// package ru.vsu.app.controller + +// import io.swagger.v3.oas.annotations.Operation +// import io.swagger.v3.oas.annotations.Parameter +// import io.swagger.v3.oas.annotations.tags.Tag +// import org.springframework.http.HttpStatus +// import org.springframework.http.ResponseEntity +// import org.springframework.security.core.annotation.AuthenticationPrincipal +// import org.springframework.validation.annotation.Validated +// import org.springframework.web.bind.annotation.* +// import ru.vsu.app.dto.* +// import ru.vsu.app.model.UserEntity +// import ru.vsu.app.service.ShopService +// import jakarta.validation.Valid + +// @RestController +// @RequestMapping("/api/shop") +// @Tag(name = "Shop", description = "API для работы с магазином") +// @Validated +// class ShopController(private val shopService: ShopService) { + +// @Operation(summary = "Получение списка наборов карт", description = "Возвращает список доступных для покупки наборов карт") +// @GetMapping("/packs") +// fun getAllPacks(): ResponseEntity { +// val packs = shopService.getAllPacks() +// return ResponseEntity.ok(packs) +// } + +// @Operation(summary = "Получение информации о наборе карт", description = "Возвращает подробную информацию о наборе и его содержимом") +// @GetMapping("/packs/{packId}") +// fun getPackDetails( +// @Parameter(description = "ID набора карт", required = true) +// @PathVariable packId: Long +// ): ResponseEntity { +// val pack = shopService.getPackDetails(packId) +// return ResponseEntity.ok(pack) +// } + +// @Operation(summary = "Покупка набора карт", description = "Покупает набор карт, если пользователь авторизован и на балансе достаточно средств") +// @PostMapping("/packs/{packId}/buy") +// fun buyPack( +// @Parameter(description = "ID набора карт", required = true) +// @PathVariable packId: Long, +// @AuthenticationPrincipal user: UserEntity +// ): ResponseEntity { +// return ResponseEntity.ok(shopService.buyPack(packId, user)) +// } + +// @Operation(summary = "Получение списка предложений монет", description = "Возвращает список доступных пакетов монет") +// @GetMapping("/coins/offers") +// fun getAllCoinOffers(): ResponseEntity { +// val offers = shopService.getAllCoinOffers() +// return ResponseEntity.ok(offers) +// } + +// @Operation(summary = "Получение информации о предложении монет", description = "Возвращает детализацию конкретного предложения монет") +// @GetMapping("/coins/offers/{offerId}") +// fun getCoinOfferDetails( +// @Parameter(description = "ID предложения монет", required = true) +// @PathVariable offerId: Long +// ): ResponseEntity { +// val offer = shopService.getCoinOfferDetails(offerId) +// return ResponseEntity.ok(offer) +// } + +// data class PurchaseCoinsRequest( +// @Parameter(description = "URL для редиректа после покупки", required = true) +// val redirectUrl: String +// ) + +// @Operation(summary = "Покупка монет", description = "Инициирует процесс покупки монет через платежный шлюз") +// @PostMapping("/coins/offers/{offerId}/purchase") +// fun purchaseCoins( +// @Parameter(description = "ID предложения монет", required = true) +// @PathVariable offerId: Long, +// @Valid @RequestBody request: PurchaseCoinsRequest +// ): ResponseEntity { +// val purchaseResponse = shopService.purchaseCoins(offerId, request.redirectUrl) +// return ResponseEntity.ok(purchaseResponse) +// } + +// @Operation(summary = "Обработка платежа", description = "Обрабатывает результат платежа от платежного шлюза") +// @PostMapping("/payments/process") +// fun processPayment( +// @Valid @RequestBody request: PaymentCallbackRequest +// ): ResponseEntity { +// val success = shopService.processPayment(request) +// return ResponseEntity.ok(mapOf("success" to success)) +// } +// } diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/ShopController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/ShopController.kt index ca961b1..13a33d1 100644 --- a/backend/src/main/kotlin/ru/vsu/app/controller/ShopController.kt +++ b/backend/src/main/kotlin/ru/vsu/app/controller/ShopController.kt @@ -1,66 +1,204 @@ package ru.vsu.app.controller -import io.swagger.v3.oas.annotations.Operation +import ru.vsu.app.dto.CoinOfferDto +import ru.vsu.app.dto.PackDto +import ru.vsu.app.dto.PaymentCallbackDto +import ru.vsu.app.dto.responses.common.InternalServerError +import ru.vsu.app.dto.responses.shop.ShopCoinsOffersOfferIdPurchasePost200Response +import ru.vsu.app.dto.requests.ShopCoinsOffersOfferIdPurchasePostRequest +import ru.vsu.app.dto.responses.shop.ShopPacksPackIdBuyPost200Response +import ru.vsu.app.dto.responses.shop.ShopPacksPackIdBuyPost402Response +import ru.vsu.app.dto.responses.shop.ShopPacksPackIdOpenPost200Response +import ru.vsu.app.dto.responses.shop.ShopPaymentsProcessPost200Response +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity -import org.springframework.security.core.annotation.AuthenticationPrincipal + import org.springframework.web.bind.annotation.* -import ru.vsu.app.dto.* -import ru.vsu.app.model.User -import ru.vsu.app.service.ShopService +import org.springframework.validation.annotation.Validated +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +import jakarta.validation.Valid +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size + +import kotlin.collections.List +import kotlin.collections.Map @RestController -@RequestMapping("/api/shop") -@Tag(name = "Shop", description = "API для работы с магазином") -class ShopController(private val shopService: ShopService) { +@Validated +@SecurityRequirement(name = "Bearer Authentication") +@RequestMapping("\${api.base-path:/api}") +@Tag(name = "Shop", description = "Операции на странице \"Магазин\"") +class ShopController() { + + @Operation( + summary = "Получение предложений по монетам", + operationId = "shopCoinsOffersGet", + description = """Возвращает список доступных пакетов монет +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список предложений", content = [Content(array = ArraySchema(schema = Schema(implementation = CoinOfferDto::class)))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/shop/coins/offers"], + produces = ["application/json"] + ) + fun shopCoinsOffersGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) + } - @Operation(summary = "Получение списка наборов") - @GetMapping("/packs") - fun getAllPacks(): ResponseEntity> { - return ResponseEntity.ok(shopService.getAllPacks()) + @Operation( + summary = "Просмотр предложения монет", + operationId = "shopCoinsOffersOfferIdGet", + description = """Возвращает детализацию предложения по монетам +""", + responses = [ + ApiResponse(responseCode = "200", description = "Информация о предложении", content = [Content(schema = Schema(implementation = CoinOfferDto::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/shop/coins/offers/{offer_id}"], + produces = ["application/json"] + ) + fun shopCoinsOffersOfferIdGet(@Parameter(description = "", required = true) @PathVariable("offer_id") offerId: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @Operation(summary = "Получение информации о наборе") - @GetMapping("/packs/{packId}") - fun getPackDetails(@PathVariable packId: Long): ResponseEntity { - return ResponseEntity.ok(shopService.getPackDetails(packId)) + @Operation( + summary = "Покупка монет", + operationId = "shopCoinsOffersOfferIdPurchasePost", + description = """Инициирует процесс покупки монет через платежный шлюз +""", + responses = [ + ApiResponse(responseCode = "200", description = "Перенаправление на платежный шлюз", content = [Content(schema = Schema(implementation = ShopCoinsOffersOfferIdPurchasePost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/shop/coins/offers/{offer_id}/purchase"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun shopCoinsOffersOfferIdPurchasePost(@Parameter(description = "", required = true) @PathVariable("offer_id") offerId: kotlin.Int,@Parameter(description = "", required = true) @Valid @RequestBody shopCoinsOffersOfferIdPurchasePostRequest: ShopCoinsOffersOfferIdPurchasePostRequest): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @Operation(summary = "Покупка набора") - @PostMapping("/packs/{packId}/buy") - fun buyPack( - @PathVariable packId: Long, - @AuthenticationPrincipal user: User - ): ResponseEntity { - return ResponseEntity.ok(shopService.buyPack(packId, user)) + @Operation( + summary = "Получение списка наборов", + operationId = "shopPacksGet", + description = """Возвращает список доступных для покупки наборов карт +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список наборов", content = [Content(array = ArraySchema(schema = Schema(implementation = PackDto::class)))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/shop/packs"], + produces = ["application/json"] + ) + fun shopPacksGet(): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @Operation(summary = "Получение списка предложений монет") - @GetMapping("/coins/offers") - fun getAllCoinOffers(): ResponseEntity> { - return ResponseEntity.ok(shopService.getAllCoinOffers()) + @Operation( + summary = "Покупка набора карт", + operationId = "shopPacksPackIdBuyPost", + description = """Покупает набор карт, если: +- Пользователь авторизован +- На балансе достаточно средств +""", + responses = [ + ApiResponse(responseCode = "200", description = "Успешная покупка", content = [Content(schema = Schema(implementation = ShopPacksPackIdBuyPost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "402", description = "Недостаточно средств", content = [Content(schema = Schema(implementation = ShopPacksPackIdBuyPost402Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/shop/packs/{pack_id}/buy"], + produces = ["application/json"] + ) + fun shopPacksPackIdBuyPost(@Parameter(description = "", required = true) @PathVariable("pack_id") packId: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @Operation(summary = "Получение информации о предложении монет") - @GetMapping("/coins/offers/{offerId}") - fun getCoinOfferDetails(@PathVariable offerId: Long): ResponseEntity { - return ResponseEntity.ok(shopService.getCoinOfferDetails(offerId)) + @Operation( + summary = "Просмотр содержимого набора", + operationId = "shopPacksPackIdGet", + description = """Возвращает подробную информацию о наборе и возможных картах +""", + responses = [ + ApiResponse(responseCode = "200", description = "Информация о наборе", content = [Content(schema = Schema(implementation = PackDto::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/shop/packs/{pack_id}"], + produces = ["application/json"] + ) + fun shopPacksPackIdGet(@Parameter(description = "", required = true) @PathVariable("pack_id") packId: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @Operation(summary = "Покупка монет") - @PostMapping("/coins/offers/{offerId}/purchase") - fun purchaseCoins( - @PathVariable offerId: Long, - @RequestBody request: Map - ): ResponseEntity { - val redirectUrl = request["redirectUrl"] ?: throw IllegalArgumentException("redirectUrl is required") - return ResponseEntity.ok(shopService.purchaseCoins(offerId, redirectUrl)) + @Operation( + summary = "Открытие купленного набора карт", + operationId = "shopPacksPackIdOpenPost", + description = """Открывает ранее купленный набор карт, показывая выпавшие карты +и добавляя их в инвентарь пользователя. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Набор успешно открыт", content = [Content(schema = Schema(implementation = ShopPacksPackIdOpenPost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/shop/packs/{pack_id}/open"], + produces = ["application/json"] + ) + fun shopPacksPackIdOpenPost(@Parameter(description = "", required = true) @PathVariable("pack_id") packId: kotlin.Int): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } - @Operation(summary = "Обработка платежа") - @PostMapping("/payments/process") - fun processPayment(@RequestBody request: PaymentCallbackRequest): ResponseEntity> { - val success = shopService.processPayment(request) - return ResponseEntity.ok(mapOf("success" to success)) + @Operation( + summary = "Обработка платежа", + operationId = "shopPaymentsProcessPost", + description = """обработка результата платежа от платежного шлюза +""", + responses = [ + ApiResponse(responseCode = "200", description = "Платеж обработан", content = [Content(schema = Schema(implementation = ShopPaymentsProcessPost200Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/shop/payments/process"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun shopPaymentsProcessPost(@Parameter(description = "", required = true) @Valid @RequestBody paymentCallback: PaymentCallbackDto): ResponseEntity { + return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } -} \ No newline at end of file +} diff --git a/backend/src/main/kotlin/ru/vsu/app/controller/TradesController.kt b/backend/src/main/kotlin/ru/vsu/app/controller/TradesController.kt new file mode 100644 index 0000000..d5bc87b --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/controller/TradesController.kt @@ -0,0 +1,215 @@ +package ru.vsu.app.controller + +import ru.vsu.app.dto.responses.profile.OtherProfileCardCardIDInitiateTradePost400Response +import ru.vsu.app.dto.responses.common.InternalServerError +import ru.vsu.app.dto.TradeDto +import ru.vsu.app.dto.requests.TradesInitiatePostRequest +import ru.vsu.app.dto.responses.trades.TradesTradeIdAcceptPost200Response +import ru.vsu.app.dto.responses.trades.TradesTradeIdAcceptPost403Response +import ru.vsu.app.dto.responses.trades.TradesTradeIdCancelPost200Response +import ru.vsu.app.dto.responses.trades.TradesTradeIdGet404Response +import ru.vsu.app.dto.responses.trades.TradesTradeIdRejectPost200Response +import ru.vsu.app.service.TradeService +import ru.vsu.app.security.CustomUserDetails +import io.swagger.v3.oas.annotations.* +import io.swagger.v3.oas.annotations.enums.* +import io.swagger.v3.oas.annotations.media.* +import io.swagger.v3.oas.annotations.responses.* +import io.swagger.v3.oas.annotations.security.* +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import org.springframework.validation.annotation.Validated +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.core.userdetails.UserDetails + +import jakarta.validation.Valid +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size + +import kotlin.collections.List +import kotlin.collections.Map + +@RestController +@Validated +@SecurityRequirement(name = "Bearer Authentication") +@RequestMapping("\${api.base-path:/api}") +@Tag(name = "Trades", description = "Операции на странице \"Обменник\"") +class TradesController( + private val tradeService: TradeService +) { + + @Operation( + summary = "Получить все доступные предложения обмена", + operationId = "tradesGet", + description = """Возвращает список всех доступных предложений обмена. +Можно провести поиск по названию карты. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список предложений обмена", content = [Content(array = ArraySchema(schema = Schema(implementation = TradeDto::class)))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/trades"], + produces = ["application/json"] + ) + fun tradesGet(@RequestParam(value = "search", required = false) search: String?): ResponseEntity> { + return ResponseEntity.ok(tradeService.getAllTrades(search)) + } + + @Operation( + summary = "Создать новое предложение обмена", + operationId = "tradesInitiatePost", + description = """Создает новое предложение обмена. +""", + responses = [ + ApiResponse(responseCode = "201", description = "Предложение обмена успешно создано", content = [Content(schema = Schema(implementation = TradeDto::class))]), + ApiResponse(responseCode = "400", description = "Невозможно создать предложение обмена", content = [Content(schema = Schema(implementation = OtherProfileCardCardIDInitiateTradePost400Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/trades/initiate"], + produces = ["application/json"], + consumes = ["application/json"] + ) + fun tradesInitiatePost( + @Parameter(description = "", required = true) @Valid @RequestBody tradesInitiatePostRequest: TradesInitiatePostRequest, + @AuthenticationPrincipal userDetails: UserDetails + ): ResponseEntity { + val customUserDetails = userDetails as CustomUserDetails + val trade = tradeService.initiateTrade(tradesInitiatePostRequest, customUserDetails.getUser().userId) + return ResponseEntity.status(HttpStatus.CREATED).body(trade) + } + + @Operation( + summary = "Получение моих обменов", + operationId = "tradesMyGet", + description = """Возвращает список всех обменов текущего пользователя с их статусами. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Список моих обменов", content = [Content(array = ArraySchema(schema = Schema(implementation = TradeDto::class)))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/trades/my"], + produces = ["application/json"] + ) + fun tradesMyGet(@AuthenticationPrincipal userDetails: UserDetails): ResponseEntity> { + val customUserDetails = userDetails as CustomUserDetails + val trades = tradeService.getUserTrades(customUserDetails.getUser().userId) + return ResponseEntity.ok(trades) + } + + @Operation( + summary = "Принять предложение обмена", + operationId = "tradesTradeIdAcceptPost", + description = """Принимает предложение обмена, если текущий пользователь - получатель обмена. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Обмен успешно принят", content = [Content(schema = Schema(implementation = TradesTradeIdAcceptPost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "403", description = "Нет прав для принятия этого обмена", content = [Content(schema = Schema(implementation = TradesTradeIdAcceptPost403Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/trades/{trade_id}/accept"], + produces = ["application/json"] + ) + fun tradesTradeIdAcceptPost( + @Parameter(description = "", required = true) @PathVariable("trade_id") tradeId: Int, + @AuthenticationPrincipal userDetails: UserDetails + ): ResponseEntity { + val customUserDetails = userDetails as CustomUserDetails + val trade = tradeService.acceptTrade(tradeId, customUserDetails.getUser().userId) + return ResponseEntity.ok(trade) + } + + @Operation( + summary = "Отменить предложение обмена", + operationId = "tradesTradeIdCancelPost", + description = """Отменяет предложение обмена, если текущий пользователь является инициатором. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Обмен успешно отменен", content = [Content(schema = Schema(implementation = TradesTradeIdCancelPost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/trades/{trade_id}/cancel"], + produces = ["application/json"] + ) + fun tradesTradeIdCancelPost( + @Parameter(description = "", required = true) @PathVariable("trade_id") tradeId: Int, + @AuthenticationPrincipal userDetails: UserDetails + ): ResponseEntity { + val customUserDetails = userDetails as CustomUserDetails + val trade = tradeService.cancelTrade(tradeId, customUserDetails.getUser().userId) + return ResponseEntity.ok(trade) + } + + @Operation( + summary = "Получить детали предложения обмена", + operationId = "tradesTradeIdGet", + description = """Возвращает полную информацию о конкретном предложении обмена. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Детали предложения обмена", content = [Content(schema = Schema(implementation = TradeDto::class))]), + ApiResponse(responseCode = "404", description = "Предложение обмена не найдено", content = [Content(schema = Schema(implementation = TradesTradeIdGet404Response::class))]), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ] + ) + @RequestMapping( + method = [RequestMethod.GET], + value = ["/trades/{trade_id}"], + produces = ["application/json"] + ) + fun tradesTradeIdGet(@Parameter(description = "", required = true) @PathVariable("trade_id") tradeId: Int): ResponseEntity { + val trade = tradeService.getTradeById(tradeId) + return ResponseEntity.ok(trade) + } + + @Operation( + summary = "Отклонить предложение обмена", + operationId = "tradesTradeIdRejectPost", + description = """Отклоняет предложение обмена, если текущий пользователь - получатель обмена. +""", + responses = [ + ApiResponse(responseCode = "200", description = "Обмен успешно отклонен", content = [Content(schema = Schema(implementation = TradesTradeIdRejectPost200Response::class))]), + ApiResponse(responseCode = "401", description = "Ошибка аутентификации"), + ApiResponse(responseCode = "500", description = "Что-то пошло не так", content = [Content(schema = Schema(implementation = InternalServerError::class))]) ], + security = [ SecurityRequirement(name = "bearerAuth") ] + ) + @RequestMapping( + method = [RequestMethod.POST], + value = ["/trades/{trade_id}/reject"], + produces = ["application/json"] + ) + fun tradesTradeIdRejectPost( + @Parameter(description = "", required = true) @PathVariable("trade_id") tradeId: Int, + @AuthenticationPrincipal userDetails: UserDetails + ): ResponseEntity { + val customUserDetails = userDetails as CustomUserDetails + val trade = tradeService.rejectTrade(tradeId, customUserDetails.getUser().userId) + return ResponseEntity.ok(trade) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/AchievementDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/AchievementDto.kt new file mode 100644 index 0000000..3c5a92c --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/AchievementDto.kt @@ -0,0 +1,43 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param achievementID Уникальный идентификатор достижения + * @param name Название достижения + * @param imageURL Ссылка на изображение + * @param description Описание достижения + * @param isUnlocked Статус получения + */ +data class AchievementDto( + + @Schema(example = "101", required = true, description = "Уникальный идентификатор достижения") + @get:JsonProperty("achievement_ID", required = true) val achievementID: kotlin.Int, + + @Schema(example = "Мастер коллекционирования", required = true, description = "Название достижения") + @get:JsonProperty("name", required = true) val name: kotlin.String, + + @Schema(example = "https://example.com/achievements/master.png", required = true, description = "Ссылка на изображение") + @get:JsonProperty("imageURL", required = true) val imageURL: kotlin.String, + + @Schema(example = "Соберите 10 полных коллекций", required = true, description = "Описание достижения") + @get:JsonProperty("description", required = true) val description: kotlin.String, + + @Schema(example = "false", required = true, description = "Статус получения") + @get:JsonProperty("isUnlocked", required = true) val isUnlocked: kotlin.Boolean + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/AuthDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/AuthDto.kt deleted file mode 100644 index ac6a2dc..0000000 --- a/backend/src/main/kotlin/ru/vsu/app/dto/AuthDto.kt +++ /dev/null @@ -1,90 +0,0 @@ -package ru.vsu.app.dto - -import io.swagger.v3.oas.annotations.media.Schema - -@Schema(description = "Запрос на вход в систему") -data class LoginRequest( - @Schema(description = "Email пользователя", example = "user@example.com") - val email: String, - - @Schema(description = "Пароль пользователя", example = "password123") - val password: String -) - -@Schema(description = "Ответ на успешный вход в систему") -data class LoginResponse( - @Schema(description = "JWT токен для авторизации", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") - val token: String, - - @Schema(description = "Имя пользователя", example = "johndoe") - val username: String, - - @Schema(description = "Email пользователя", example = "user@example.com") - val email: String -) - -@Schema(description = "Запрос на регистрацию нового пользователя") -data class RegisterRequest( - @Schema(description = "Email пользователя", example = "user@example.com") - val email: String, - - @Schema(description = "Имя пользователя", example = "johndoe") - val username: String, - - @Schema(description = "Пароль пользователя", example = "password123") - val password: String -) - -@Schema(description = "Ответ на запрос регистрации") -data class RegisterResponse( - @Schema(description = "Сообщение о результате операции", example = "Пользователь успешно зарегистрирован") - val message: String, - - @Schema(description = "Флаг успешности операции", example = "true") - val success: Boolean -) - -@Schema(description = "Запрос на сброс пароля") -data class ForgotPasswordRequest( - @Schema(description = "Email пользователя", example = "user@example.com") - val email: String -) - -@Schema(description = "Запрос на верификацию кода сброса пароля") -data class ResetPasswordRequest( - @Schema(description = "Код подтверждения", example = "123456") - val token: String, - - @Schema(description = "Новый пароль", example = "newPassword123") - val newPassword: String -) - -@Schema(description = "Запрос на активацию аккаунта по коду") -data class ActivateAccountRequest( - @Schema(description = "Email пользователя", example = "user@example.com") - val email: String, - - @Schema(description = "Код активации", example = "123456") - val code: String -) - -@Schema(description = "Информация о пользователе") -data class UserInfoResponse( - @Schema(description = "Идентификатор пользователя", example = "1") - val id: Long, - - @Schema(description = "Имя пользователя", example = "johndoe") - val username: String, - - @Schema(description = "Email пользователя", example = "user@example.com") - val email: String -) - -@Schema(description = "Стандартный ответ API") -data class ApiResponse( - @Schema(description = "Сообщение о результате операции", example = "Операция выполнена успешно") - val message: String, - - @Schema(description = "Флаг успешности операции", example = "true") - val success: Boolean -) \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/CardDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/CardDto.kt index 3e9877e..44a7b25 100644 --- a/backend/src/main/kotlin/ru/vsu/app/dto/CardDto.kt +++ b/backend/src/main/kotlin/ru/vsu/app/dto/CardDto.kt @@ -1,12 +1,62 @@ package ru.vsu.app.dto +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.Valid + +/** + * + * @param cardID Уникальный идентификатор карты + * @param name Название карты + * @param imageURL Ссылка на изображение + * @param rarity Редкость карты + * @param minPrice Минимальная цена - используется для разбора карточки + * @param isGenerated Флаг показывающий сгенерированная ли карта (true для уникальных, false для остальных) + * @param description Описание карты + * @param theme Тематика, на которую была сгенерирвана карта (только для уникальных карт) + */ data class CardDto( - val id: Long, - val name: String, - val imageUrl: String, - val rarity: Int, - val collection: String, - val description: String, - val type: String, - val disassemblePrice: Int -) \ No newline at end of file + + @Schema(example = "501", required = true, description = "Уникальный идентификатор карты") + @get:JsonProperty("card_ID", required = true) val cardID: Int, + + @Schema(example = "Ледяной феникс", required = true, description = "Название карты") + @get:JsonProperty("name", required = true) val name: String, + + @Schema(example = "https://example.com/cards/pheonix.png", required = true, description = "Ссылка на изображение") + @get:JsonProperty("imageURL", required = true) val imageURL: String, + + @Schema(example = "Редкая", required = true, description = "Редкость карты") + @get:JsonProperty("rarity", required = true) val rarity: Rarity, + + @Schema(example = "50", required = true, description = "Минимальная цена - используется для разбора карточки") + @get:JsonProperty("min_price", required = true) val minPrice: Int, + + @Schema(example = "false", required = true, description = "Флаг показывающий сгенерированная ли карта") + @get:JsonProperty("isGenerated", required = true) val isGenerated: Boolean, + + @Schema(example = "Ледяной Феникс — это мифическое существо...", description = "Описание карты") + @get:JsonProperty("description") val description: String? = null, + + @field:Valid + @Schema(description = "Тематика, на которую была сгенерирована карта (только для уникальных карт)") + @get:JsonProperty("theme") val theme: ThemeDto? = null +) { + enum class Rarity(@get:JsonValue val value: String) { + Обычная("Обычная"), + Редкая("Редкая"), + Эпическая("Эпическая"), + Легендарная("Легендарная"), + Уникальная("Уникальная"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: String): Rarity { + return values().first { it.value == value } + } + } + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/CardResponse.kt b/backend/src/main/kotlin/ru/vsu/app/dto/CardResponse.kt deleted file mode 100644 index 80febdf..0000000 --- a/backend/src/main/kotlin/ru/vsu/app/dto/CardResponse.kt +++ /dev/null @@ -1,12 +0,0 @@ -package ru.vsu.app.dto - -data class CardResponse( - val id: Long, - val name: String, - val imageUrl: String, - val rarity: Int, - val collection: String, - val description: String, - val type: String, - val disassemblePrice: Int -) \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/CoinOfferDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/CoinOfferDto.kt new file mode 100644 index 0000000..53ba6d8 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/CoinOfferDto.kt @@ -0,0 +1,48 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param offerId + * @param name + * @param coinsAmount + * @param price + * @param imageUrl + * @param description + */ +data class CoinOfferDto( + + @Schema(example = "1", description = "") + @get:JsonProperty("offer_id") val offerId: kotlin.Int? = null, + + @Schema(example = "Стартовый набор", description = "") + @get:JsonProperty("name") val name: kotlin.String? = null, + + @Schema(example = "100", description = "") + @get:JsonProperty("coinsAmount") val coinsAmount: kotlin.Int? = null, + + @Schema(example = "99.9", description = "") + @get:JsonProperty("price") val price: kotlin.Double? = null, + + @field:Valid + @Schema(description = "") + @get:JsonProperty("imageUrl") val imageUrl: java.net.URI? = null, + + @Schema(example = "Стартовый набор. Вы можете получить...", description = "") + @get:JsonProperty("description") val description: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/CollectionDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/CollectionDto.kt new file mode 100644 index 0000000..5704329 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/CollectionDto.kt @@ -0,0 +1,41 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.dto.CardDto +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param collectionID Уникальный идентификатор коллекции + * @param name Название коллекции + * @param cards Карты в коллекции + * @param imageURL Обложка коллекции + */ +data class CollectionDto( + + @Schema(example = "801", required = true, description = "Уникальный идентификатор коллекции") + @get:JsonProperty("collection_ID", required = true) val collectionID: kotlin.Int, + + @Schema(example = "Драконы", required = true, description = "Название коллекции") + @get:JsonProperty("name", required = true) val name: kotlin.String, + + @field:Valid + @Schema(required = true, description = "Карты в коллекции") + @get:JsonProperty("cards", required = true) val cards: kotlin.collections.List, + + @Schema(example = "https://example.com/collections/dragons.png", required = true, description = "Обложка коллекции") + @get:JsonProperty("imageURL", required = true) val imageURL: kotlin.String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/NewsDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/NewsDto.kt new file mode 100644 index 0000000..0629f3e --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/NewsDto.kt @@ -0,0 +1,43 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param newsID Уникальный идентификатор новости + * @param title Заголовок новости + * @param content Содержание новости + * @param datePosted Дата публикации + * @param pictures Ссылки на изображения + */ +data class NewsDto( + + @Schema(example = "1", required = true, description = "Уникальный идентификатор новости") + @get:JsonProperty("news_ID", required = true) val newsID: kotlin.Int, + + @Schema(example = "Новое обновление системы", required = true, description = "Заголовок новости") + @get:JsonProperty("title", required = true) val title: kotlin.String, + + @Schema(example = "Мы добавили новые карты в коллекцию", required = true, description = "Содержание новости") + @get:JsonProperty("content", required = true) val content: kotlin.String, + + @Schema(example = "2024-05-20T14:48Z", required = true, description = "Дата публикации") + @get:JsonProperty("datePosted", required = true) val datePosted: java.time.LocalDateTime, + + @Schema(example = "[\"https://example.com/news1.jpg\",\"https://example.com/news2.jpg\"]", description = "Ссылки на изображения") + @get:JsonProperty("pictures") val pictures: kotlin.collections.List? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/NotificationDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/NotificationDto.kt new file mode 100644 index 0000000..9ac41f7 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/NotificationDto.kt @@ -0,0 +1,43 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param notificationID Уникальный идентификатор уведомления + * @param userID Уникальный идентификатор пользователя + * @param message Текст уведомления + * @param notificationDateTime Дата и время уведомления + * @param links Ссылка на подробности обмена + */ +data class NotificationDto( + + @Schema(example = "201", required = true, description = "Уникальный идентификатор уведомления") + @get:JsonProperty("notification_ID", required = true) val notificationID: kotlin.Int, + + @Schema(example = "1", required = true, description = "Уникальный идентификатор пользователя") + @get:JsonProperty("user_id", required = true) val userID: kotlin.Int, + + @Schema(example = "Ваша карта была успешно обменяна", required = true, description = "Текст уведомления") + @get:JsonProperty("message", required = true) val message: kotlin.String, + + @Schema(example = "2024-05-20T15:30Z", required = true, description = "Дата и время уведомления") + @get:JsonProperty("notificationDateTime", required = true) val notificationDateTime: java.time.LocalDateTime, + + @Schema(example = "[\"https://example.com/exchange-details\"]", description = "Ссылка на подробности обмена") + @get:JsonProperty("links") val links: kotlin.collections.List? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/PackDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/PackDto.kt new file mode 100644 index 0000000..a88261f --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/PackDto.kt @@ -0,0 +1,45 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.dto.CardDto +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param packID Уникальный идентификатор набора + * @param name Название набора + * @param imageURL Ссылка на изображение + * @param cards Карты в наборе + * @param price Цена + */ +data class PackDto( + + @Schema(example = "401", required = true, description = "Уникальный идентификатор набора") + @get:JsonProperty("pack_ID", required = true) val packID: kotlin.Int, + + @Schema(example = "Стартовый набор", required = true, description = "Название набора") + @get:JsonProperty("name", required = true) val name: kotlin.String, + + @Schema(example = "https://example.com/packs/starter.png", required = true, description = "Ссылка на изображение") + @get:JsonProperty("imageURL", required = true) val imageURL: kotlin.String, + + @field:Valid + @Schema(required = true, description = "Карты в наборе") + @get:JsonProperty("cards", required = true) val cards: kotlin.collections.List, + + @Schema(example = "1000", required = true, description = "Цена") + @get:JsonProperty("price", required = true) val price: kotlin.Int + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/PaymentCallbackDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/PaymentCallbackDto.kt new file mode 100644 index 0000000..0a383e7 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/PaymentCallbackDto.kt @@ -0,0 +1,64 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param transactionId + * @param status + * @param amount + * @param currency + * @param offerId + */ +data class PaymentCallbackDto( + + @Schema(required = true, description = "") + @get:JsonProperty("transactionId", required = true) val transactionId: kotlin.String, + + @Schema(required = true, description = "") + @get:JsonProperty("status", required = true) val status: PaymentCallbackDto.Status, + + @Schema(required = true, description = "") + @get:JsonProperty("amount", required = true) val amount: java.math.BigDecimal, + + @Schema(description = "") + @get:JsonProperty("currency") val currency: kotlin.String? = null, + + @Schema(description = "") + @get:JsonProperty("offerId") val offerId: kotlin.Int? = null + ) { + + /** + * + * Values: Успешно,Ошибка,В_процессе + */ + enum class Status(@get:JsonValue val value: kotlin.String) { + + Успешно("Успешно"), + Ошибка("Ошибка"), + В_процессе("В процессе"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): Status { + return values().first{it -> it.value == value} + } + } + } + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/PaymentDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/PaymentDto.kt new file mode 100644 index 0000000..840edb5 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/PaymentDto.kt @@ -0,0 +1,70 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue +import ru.vsu.app.dto.UserDto +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param paymentID Уникальный идентификатор платежа + * @param user + * @param paymentSUM Сумма платежа + * @param paymentStatus Статус платежа + * @param paymentDateTime Дата и время платежа + * @param bankCardData Данные карты (только для записи) + */ +data class PaymentDto( + + @Schema(example = "601", required = true, description = "Уникальный идентификатор платежа") + @get:JsonProperty("payment_ID", required = true) val paymentID: kotlin.Int, + + @field:Valid + @Schema(required = true, description = "") + @get:JsonProperty("user", required = true) val user: UserDto, + + @Schema(example = "199.99", required = true, description = "Сумма платежа") + @get:JsonProperty("paymentSUM", required = true) val paymentSUM: kotlin.Double, + + @Schema(example = "Оплачено", required = true, description = "Статус платежа") + @get:JsonProperty("paymentStatus", required = true) val paymentStatus: PaymentDto.PaymentStatus, + + @Schema(example = "2024-05-20T17:30Z", required = true, description = "Дата и время платежа") + @get:JsonProperty("paymentDateTime", required = true) val paymentDateTime: java.time.LocalDateTime, + + @Schema(description = "Данные карты (только для записи)") + @get:JsonProperty("bankCardData") val bankCardData: kotlin.collections.List? = null + ) { + + /** + * Статус платежа + * Values: В_обработке,Оплачено,Ошибка + */ + enum class PaymentStatus(@get:JsonValue val value: kotlin.String) { + + В_обработке("В обработке"), + Оплачено("Оплачено"), + Ошибка("Ошибка"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): PaymentStatus { + return values().first{it -> it.value == value} + } + } + } + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/QuestDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/QuestDto.kt new file mode 100644 index 0000000..9b51588 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/QuestDto.kt @@ -0,0 +1,61 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.dto.PackDto +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param questID Уникальный идентификатор квеста + * @param name Название квеста + * @param description Описание квеста + * @param progress Текущий прогресс + * @param target Целевое значение + * @param rewardCoins Награда в монетах + * @param isCompleted Выполнен ли квест + * @param isClaimed Получена ли награда + * @param rewardPacks Награда в наборах + */ +data class QuestDto( + + @Schema(example = "1", required = true, description = "Уникальный идентификатор квеста") + @get:JsonProperty("quest_ID", required = true) val questID: kotlin.Int, + + @Schema(example = "Соберите 5 карт", required = true, description = "Название квеста") + @get:JsonProperty("name", required = true) val name: kotlin.String, + + @Schema(example = "Соберите 5 карт из коллекции Драконы", required = true, description = "Описание квеста") + @get:JsonProperty("description", required = true) val description: kotlin.String, + + @Schema(example = "3", required = true, description = "Текущий прогресс") + @get:JsonProperty("progress", required = true) val progress: kotlin.Int, + + @Schema(example = "5", required = true, description = "Целевое значение") + @get:JsonProperty("target", required = true) val target: kotlin.Int, + + @Schema(example = "200", required = true, description = "Награда в монетах") + @get:JsonProperty("rewardCoins", required = true) val rewardCoins: kotlin.Int, + + @Schema(example = "false", required = true, description = "Выполнен ли квест") + @get:JsonProperty("isCompleted", required = true) val isCompleted: kotlin.Boolean, + + @Schema(example = "false", required = true, description = "Получена ли награда") + @get:JsonProperty("isClaimed", required = true) val isClaimed: kotlin.Boolean, + + @field:Valid + @Schema(description = "Награда в наборах") + @get:JsonProperty("rewardPacks") val rewardPacks: kotlin.collections.List? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/ReportDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/ReportDto.kt new file mode 100644 index 0000000..b0a76d9 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/ReportDto.kt @@ -0,0 +1,71 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue +import ru.vsu.app.dto.UserDto +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param reportID Уникальный идентификатор жалобы + * @param reporter + * @param reportedUser + * @param reportDateTime Дата и время жалобы + * @param reason непристойный никнейм + * @param status Статус рассмотрения + */ +data class ReportDto( + + @Schema(example = "301", required = true, description = "Уникальный идентификатор жалобы") + @get:JsonProperty("report_ID", required = true) val reportID: kotlin.Int, + + @field:Valid + @Schema(required = true, description = "") + @get:JsonProperty("reporter", required = true) val reporter: UserDto, + + @field:Valid + @Schema(required = true, description = "") + @get:JsonProperty("reportedUser", required = true) val reportedUser: UserDto, + + @Schema(example = "2024-05-20T16:45Z", required = true, description = "Дата и время жалобы") + @get:JsonProperty("reportDateTime", required = true) val reportDateTime: java.time.LocalDateTime, + + @Schema(example = "Непристойный контент", required = true, description = "непристойный никнейм") + @get:JsonProperty("reason", required = true) val reason: kotlin.String, + + @Schema(example = "На расмотрении", required = true, description = "Статус рассмотрения") + @get:JsonProperty("status", required = true) val status: ReportDto.Status + ) { + + /** + * Статус рассмотрения + * Values: На_рассмотрении,Подтверждено,Отклонено + */ + enum class Status(@get:JsonValue val value: kotlin.String) { + + На_рассмотрении("На рассмотрении"), + Подтверждено("Подтверждено"), + Отклонено("Отклонено"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): Status { + return values().first{it -> it.value == value} + } + } + } + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/ShopDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/ShopDto.kt deleted file mode 100644 index a3b1b3d..0000000 --- a/backend/src/main/kotlin/ru/vsu/app/dto/ShopDto.kt +++ /dev/null @@ -1,35 +0,0 @@ -package ru.vsu.app.dto - -data class PackResponse( - val id: Long, - val name: String, - val imageUrl: String, - val price: Int, - val cards: List -) - -data class CoinOfferResponse( - val id: Long, - val name: String, - val coinsAmount: Int, - val price: Double, - val imageUrl: String, - val description: String? -) - -data class PurchasePackResponse( - val receivedCards: List, - val newBalance: Int -) - -data class PurchaseCoinsResponse( - val paymentUrl: String -) - -data class PaymentCallbackRequest( - val transactionId: String, - val status: String, - val amount: Double, - val currency: String, - val offerId: Long -) \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/ThemeDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/ThemeDto.kt new file mode 100644 index 0000000..a2273ee --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/ThemeDto.kt @@ -0,0 +1,35 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param themeID Уникальный идентификатор темы + * @param name Название темы + * @param description Описание темы + */ +data class ThemeDto( + + @Schema(example = "901", required = true, description = "Уникальный идентификатор темы") + @get:JsonProperty("theme_ID", required = true) val themeID: kotlin.Int, + + @Schema(example = "Мифические существа", required = true, description = "Название темы") + @get:JsonProperty("name", required = true) val name: kotlin.String, + + @Schema(example = "Создайте изображение мифического существа", description = "Описание темы") + @get:JsonProperty("description") val description: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/TradeDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/TradeDto.kt new file mode 100644 index 0000000..6c37a99 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/TradeDto.kt @@ -0,0 +1,57 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.dto.CardDto +import ru.vsu.app.dto.UserDto +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param tradeID Уникальный идентификатор обмена + * @param offeringUser + * @param offeringCards Предлагаемые карты + * @param receivingUser + * @param receivingCards Запрашиваемые карты + * @param isConfirmed Статус подтверждения + * @param tradeDateTime Дата и время обмена + */ +data class TradeDto( + + @Schema(example = "701", required = true, description = "Уникальный идентификатор обмена") + @get:JsonProperty("trade_ID", required = true) val tradeID: kotlin.Int, + + @field:Valid + @Schema(required = true, description = "") + @get:JsonProperty("offeringUser", required = true) val offeringUser: UserDto, + + @field:Valid + @Schema(required = true, description = "Предлагаемые карты") + @get:JsonProperty("offeringCards", required = true) val offeringCards: kotlin.collections.List, + + @field:Valid + @Schema(required = true, description = "") + @get:JsonProperty("receivingUser", required = true) val receivingUser: UserDto, + + @field:Valid + @Schema(required = true, description = "Запрашиваемые карты") + @get:JsonProperty("receivingCards", required = true) val receivingCards: kotlin.collections.List, + + @Schema(example = "false", required = true, description = "Статус подтверждения") + @get:JsonProperty("isConfirmed", required = true) val isConfirmed: kotlin.Boolean, + + @Schema(example = "2024-05-20T18:00Z", required = true, description = "Дата и время обмена") + @get:JsonProperty("tradeDateTime", required = true) val tradeDateTime: java.time.LocalDateTime + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/UserDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/UserDto.kt new file mode 100644 index 0000000..ed61854 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/UserDto.kt @@ -0,0 +1,78 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.dto.AchievementDto +import ru.vsu.app.dto.CardDto +import ru.vsu.app.dto.NotificationDto +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param userID Уникальный идентификатор + * @param username Имя пользователя + * @param email Электронная почта + * @param password Пароль + * @param inventoryCards Карты в инвентаре + * @param balance Баланс + * @param achievements Достижения + * @param favoriteCards Избранные карты + * @param onChange Карты на обмен + * @param avatarUrl Аватар пользователя + * @param favoriteAchievements Избранные достижения + * @param notifications Уведомления + */ +data class UserDto( + + @Schema(example = "1", required = true, description = "Уникальный идентификатор") + @get:JsonProperty("user_id", required = true) val userID: kotlin.Int, + + @Schema(example = "Коллекционер_123", required = true, description = "Имя пользователя") + @get:JsonProperty("username", required = true) val username: kotlin.String, + + @get:Email + @Schema(example = "user@example.com", required = true, description = "Электронная почта") + @get:JsonProperty("email", required = true) val email: kotlin.String, + + @field:Valid + @Schema(required = true, description = "Карты в инвентаре") + @get:JsonProperty("inventoryCards", required = true) val inventoryCards: kotlin.collections.List, + + @Schema(example = "2500", required = true, description = "Баланс") + @get:JsonProperty("balance", required = true) val balance: kotlin.Int, + + @field:Valid + @Schema(required = true, description = "Достижения") + @get:JsonProperty("achievements", required = true) val achievements: kotlin.collections.List, + + @field:Valid + @Schema(description = "Избранные карты") + @get:JsonProperty("favoriteCards") val favoriteCards: kotlin.collections.List? = null, + + @field:Valid + @Schema(description = "Карты на обмен") + @get:JsonProperty("onChange") val onChange: kotlin.collections.List? = null, + + @Schema(example = "https://example.com/avatars/user1.png", description = "Аватар пользователя") + @get:JsonProperty("avatar_url") val avatarUrl: kotlin.String? = null, + + @field:Valid + @Schema(description = "Избранные достижения") + @get:JsonProperty("favoriteAchievements") val favoriteAchievements: kotlin.collections.List? = null, + + @field:Valid + @Schema(description = "Уведомления") + @get:JsonProperty("notifications") val notifications: kotlin.collections.List? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/UserSettingsDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/UserSettingsDto.kt new file mode 100644 index 0000000..8a244b4 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/UserSettingsDto.kt @@ -0,0 +1,35 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param notificationsEnabled Включены ли уведомления + * @param showInventory Виден ли инвентарь другим пользователям + * @param autoDeclineTrades Автоматически отклонять все входящие предложения обмена + */ +data class UserSettingsDto( + + @Schema(example = "true", required = true, description = "Включены ли уведомления") + @get:JsonProperty("notificationsEnabled", required = true) val notificationsEnabled: kotlin.Boolean, + + @Schema(example = "false", required = true, description = "Виден ли инвентарь другим пользователям") + @get:JsonProperty("showInventory", required = true) val showInventory: kotlin.Boolean, + + @Schema(example = "false", required = true, description = "Автоматически отклонять все входящие предложения обмена") + @get:JsonProperty("autoDeclineTrades", required = true) val autoDeclineTrades: kotlin.Boolean + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/UserStatsDto.kt b/backend/src/main/kotlin/ru/vsu/app/dto/UserStatsDto.kt new file mode 100644 index 0000000..0445fd3 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/UserStatsDto.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param totalCards + * @param completedCollections + */ +data class UserStatsDto( + + @Schema(example = "150", description = "") + @get:JsonProperty("totalCards") val totalCards: kotlin.Int? = null, + + @Schema(example = "5", description = "") + @get:JsonProperty("completedCollections") val completedCollections: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminReportsReportIDPutRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminReportsReportIDPutRequest.kt new file mode 100644 index 0000000..b509743 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminReportsReportIDPutRequest.kt @@ -0,0 +1,80 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param status + * @param action + * @param banDurationDays Длительность бана в днях (только для temporary_ban) + * @param comment + */ +data class AdminReportsReportIDPutRequest( + + @Schema(example = "resolved", required = true, description = "") + @get:JsonProperty("status", required = true) val status: AdminReportsReportIDPutRequest.Status, + + @Schema(example = "temporary_ban", required = true, description = "") + @get:JsonProperty("action", required = true) val action: AdminReportsReportIDPutRequest.Action, + + @Schema(example = "7", description = "Длительность бана в днях (только для temporary_ban)") + @get:JsonProperty("banDurationDays") val banDurationDays: kotlin.Int? = null, + + @Schema(example = "Пользователь получил временный бан на 7 дней за нарушение правил", description = "") + @get:JsonProperty("comment") val comment: kotlin.String? = null + ) { + + /** + * + * Values: pending,reviewed,resolved + */ + enum class Status(@get:JsonValue val value: kotlin.String) { + + pending("pending"), + reviewed("reviewed"), + resolved("resolved"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): Status { + return values().first{it -> it.value == value} + } + } + } + + /** + * + * Values: none,warning,temporary_ban,permanent_ban + */ + enum class Action(@get:JsonValue val value: kotlin.String) { + + none("none"), + warning("warning"), + temporary_ban("temporary_ban"), + permanent_ban("permanent_ban"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): Action { + return values().first{it -> it.value == value} + } + } + } + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminTradesTradeIDInvalidatePostRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminTradesTradeIDInvalidatePostRequest.kt new file mode 100644 index 0000000..1b9cd35 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminTradesTradeIDInvalidatePostRequest.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param reason Причина аннулирования обмена + */ +data class AdminTradesTradeIDInvalidatePostRequest( + + @Schema(required = true, description = "Причина аннулирования обмена") + @get:JsonProperty("reason", required = true) val reason: kotlin.String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminUsersUserIDBanPostRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminUsersUserIDBanPostRequest.kt new file mode 100644 index 0000000..a11f553 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminUsersUserIDBanPostRequest.kt @@ -0,0 +1,55 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param banType + * @param reason + * @param durationDays Длительность бана в днях + */ +data class AdminUsersUserIDBanPostRequest( + + @Schema(example = "temporary", required = true, description = "") + @get:JsonProperty("banType", required = true) val banType: AdminUsersUserIDBanPostRequest.BanType, + + @Schema(example = "Нарушение правил сообщества", required = true, description = "") + @get:JsonProperty("reason", required = true) val reason: kotlin.String, + + @Schema(example = "7", description = "Длительность бана в днях") + @get:JsonProperty("durationDays") val durationDays: kotlin.Int? = null + ) { + + /** + * + * Values: temporary,permanent + */ + enum class BanType(@get:JsonValue val value: kotlin.String) { + + temporary("temporary"), + permanent("permanent"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): BanType { + return values().first{it -> it.value == value} + } + } + } + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminUsersUserIDQuestsQuestIDResetPostRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminUsersUserIDQuestsQuestIDResetPostRequest.kt new file mode 100644 index 0000000..5df3e81 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AdminUsersUserIDQuestsQuestIDResetPostRequest.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param reason Причина сброса квеста + * @param removeRewards Нужно ли удалить награды за квест + */ +data class AdminUsersUserIDQuestsQuestIDResetPostRequest( + + @Schema(required = true, description = "Причина сброса квеста") + @get:JsonProperty("reason", required = true) val reason: kotlin.String, + + @Schema(description = "Нужно ли удалить награды за квест") + @get:JsonProperty("removeRewards") val removeRewards: kotlin.Boolean? = false + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/AuthRefreshRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AuthRefreshRequest.kt new file mode 100644 index 0000000..ed86c82 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/AuthRefreshRequest.kt @@ -0,0 +1,29 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param accessToken + */ +data class AuthRefreshRequest( + + @field:NotNull + @Schema(description = "Обновлённый JWT токен для доступа", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") + @get:JsonProperty("accessToken") + val accessToken: String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/HomeGenerateCardPostRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/HomeGenerateCardPostRequest.kt new file mode 100644 index 0000000..0ff782d --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/HomeGenerateCardPostRequest.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param themeID ID выбранной темы + */ +data class HomeGenerateCardPostRequest( + + @Schema(example = "901", required = true, description = "ID выбранной темы") + @get:JsonProperty("theme_ID", required = true) val themeID: kotlin.Int + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/HomeQuestsClaimRewardPostRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/HomeQuestsClaimRewardPostRequest.kt new file mode 100644 index 0000000..a0d1901 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/HomeQuestsClaimRewardPostRequest.kt @@ -0,0 +1,47 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param questType + */ +data class HomeQuestsClaimRewardPostRequest( + + @Schema(example = "daily", required = true, description = "") + @get:JsonProperty("questType", required = true) val questType: HomeQuestsClaimRewardPostRequest.QuestType + ) { + + /** + * + * Values: daily,weekly + */ + enum class QuestType(@get:JsonValue val value: kotlin.String) { + + daily("daily"), + weekly("weekly"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): QuestType { + return values().first{it -> it.value == value} + } + } + } + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/InventoryDestroyPostRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/InventoryDestroyPostRequest.kt new file mode 100644 index 0000000..0181295 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/InventoryDestroyPostRequest.kt @@ -0,0 +1,28 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param cardID + */ +data class InventoryDestroyPostRequest( + + @get:Min(1) + @Schema(example = "501", required = true, description = "") + @get:JsonProperty("card_ID", required = true) val cardID: kotlin.Int + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/LoginUserRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/LoginUserRequest.kt new file mode 100644 index 0000000..7872234 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/LoginUserRequest.kt @@ -0,0 +1,32 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param email + * @param password + */ +data class LoginUserRequest( + + @get:Email + @Schema(example = "user@example.com", required = true, description = "") + @get:JsonProperty("email", required = true) val email: kotlin.String, + + @Schema(example = "SecurePass123!", required = true, description = "") + @get:JsonProperty("password", required = true) val password: kotlin.String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/NewsRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/NewsRequest.kt new file mode 100644 index 0000000..3c786d3 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/NewsRequest.kt @@ -0,0 +1,22 @@ +package ru.vsu.app.dto.requests + +import com.fasterxml.jackson.annotation.JsonProperty +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import java.time.LocalDateTime + +data class NewsRequest( + @Schema(description = "Заголовок новости", example = "Новое обновление") + @get:JsonProperty("title", required = true) + @field:NotBlank + val title: String, + + @Schema(description = "Содержание новости", example = "Добавлены новые карты") + @get:JsonProperty("content", required = true) + @field:NotBlank + val content: String, + + @Schema(description = "Список изображений") + @get:JsonProperty("pictures") + val pictures: List? = null +) diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/OtherProfileUserIDReportPostRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/OtherProfileUserIDReportPostRequest.kt new file mode 100644 index 0000000..a1cece4 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/OtherProfileUserIDReportPostRequest.kt @@ -0,0 +1,53 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param reason Причина жалобы + * @param comment Дополнительные комментарии + */ +data class OtherProfileUserIDReportPostRequest( + + @Schema(example = "Неуместный никнейм", required = true, description = "Причина жалобы") + @get:JsonProperty("reason", required = true) val reason: OtherProfileUserIDReportPostRequest.Reason, + + @get:Size(max=500) + @Schema(example = "Никнейм содержит нецензурные слова", description = "Дополнительные комментарии") + @get:JsonProperty("comment") val comment: kotlin.String? = null + ) { + + /** + * Причина жалобы + * Values: Неуместный_никнейм,Неуместный_аватар,Другое + */ + enum class Reason(@get:JsonValue val value: kotlin.String) { + + Неуместный_никнейм("Неуместный никнейм"), + Неуместный_аватар("Неуместный аватар"), + Другое("Другое"); + + companion object { + @JvmStatic + @JsonCreator + fun forValue(value: kotlin.String): Reason { + return values().first{it -> it.value == value} + } + } + } + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/RegisterUserRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/RegisterUserRequest.kt new file mode 100644 index 0000000..194a619 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/RegisterUserRequest.kt @@ -0,0 +1,38 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param email Email пользователя + * @param username Имя пользователя (3-25 символов) + * @param password Пароль (минимум 8 символов) + */ +data class RegisterUserRequest( + + @get:Email + @Schema(example = "user@example.com", required = true, description = "Email пользователя") + @get:JsonProperty("email", required = true) val email: kotlin.String, + + @get:Size(min=3,max=25) + @Schema(example = "Коллекционер_123", required = true, description = "Имя пользователя (3-25 символов)") + @get:JsonProperty("username", required = true) val username: kotlin.String, + + @get:Size(min=8) + @Schema(example = "Password123!", required = true, description = "Пароль (минимум 8 символов)") + @get:JsonProperty("password", required = true) val password: kotlin.String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/RequestPasswordResetRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/RequestPasswordResetRequest.kt new file mode 100644 index 0000000..afa50e6 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/RequestPasswordResetRequest.kt @@ -0,0 +1,28 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param email + */ +data class RequestPasswordResetRequest( + + @get:Email + @Schema(example = "user@example.com", required = true, description = "") + @get:JsonProperty("email", required = true) val email: kotlin.String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/ResendVerificationCodeRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/ResendVerificationCodeRequest.kt new file mode 100644 index 0000000..823aa14 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/ResendVerificationCodeRequest.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param tempToken + */ +data class ResendVerificationCodeRequest( + + @Schema(example = "temp_abc123", required = true, description = "") + @get:JsonProperty("tempToken", required = true) val tempToken: kotlin.String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/ResetPasswordRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/ResetPasswordRequest.kt new file mode 100644 index 0000000..db9f219 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/ResetPasswordRequest.kt @@ -0,0 +1,37 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param resetToken + * @param code + * @param newPassword + */ +data class ResetPasswordRequest( + + @Schema(example = "reset_xyz789", required = true, description = "") + @get:JsonProperty("resetToken", required = true) val resetToken: kotlin.String, + + @get:Pattern(regexp="^\\d{6}$") + @Schema(example = "654321", required = true, description = "") + @get:JsonProperty("code", required = true) val code: kotlin.String, + + @get:Size(min=8) + @Schema(example = "NewSecurePass123!", required = true, description = "") + @get:JsonProperty("newPassword", required = true) val newPassword: kotlin.String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/ShopCoinsOffersOfferIdPurchasePostRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/ShopCoinsOffersOfferIdPurchasePostRequest.kt new file mode 100644 index 0000000..5f18896 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/ShopCoinsOffersOfferIdPurchasePostRequest.kt @@ -0,0 +1,28 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param redirectUrl + */ +data class ShopCoinsOffersOfferIdPurchasePostRequest( + + @field:Valid + @Schema(example = "https://cardly.ru/shop/payment-callback", required = true, description = "") + @get:JsonProperty("redirectUrl", required = true) val redirectUrl: java.net.URI + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/TradesInitiatePostRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/TradesInitiatePostRequest.kt new file mode 100644 index 0000000..402aebd --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/TradesInitiatePostRequest.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param offeredCardId ID карты, которую предлагает текущий пользователь + * @param requestedCardId ID карт, на которые пользователь готов совершить \"быстрый обмен\" + */ +data class TradesInitiatePostRequest( + + @Schema(example = "401", required = true, description = "ID карты, которую предлагает текущий пользователь") + @get:JsonProperty("offeredCardId", required = true) val offeredCardId: kotlin.Int, + + @Schema(example = "[100,200]", required = true, description = "ID карт, на которые пользователь готов совершить \"быстрый обмен\"") + @get:JsonProperty("requestedCardId", required = true) val requestedCardId: kotlin.collections.List + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/requests/VerifyEmailRequest.kt b/backend/src/main/kotlin/ru/vsu/app/dto/requests/VerifyEmailRequest.kt new file mode 100644 index 0000000..de91b79 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/requests/VerifyEmailRequest.kt @@ -0,0 +1,32 @@ +package ru.vsu.app.dto.requests + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param tempToken Временный токен из ответа регистрации + * @param code 6-значный код подтверждения + */ +data class VerifyEmailRequest( + + @Schema(example = "temp_abc123", required = true, description = "Временный токен из ответа регистрации") + @get:JsonProperty("tempToken", required = true) val tempToken: kotlin.String, + + @get:Pattern(regexp="^\\d{6}$") + @Schema(example = "123456", required = true, description = "6-значный код подтверждения") + @get:JsonProperty("code", required = true) val code: kotlin.String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminStatsGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminStatsGet200Response.kt new file mode 100644 index 0000000..b8c78f0 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminStatsGet200Response.kt @@ -0,0 +1,55 @@ +package ru.vsu.app.dto.responses.admin + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param totalUsers + * @param activeUsers + * @param bannedUsers + * @param totalCards + * @param totalTrades + * @param completedTrades + * @param invalidatedTrades + * @param pendingReports + */ +data class AdminStatsGet200Response( + + @Schema(example = "1000", description = "") + @get:JsonProperty("totalUsers") val totalUsers: kotlin.Int? = null, + + @Schema(example = "750", description = "") + @get:JsonProperty("activeUsers") val activeUsers: kotlin.Int? = null, + + @Schema(example = "25", description = "") + @get:JsonProperty("bannedUsers") val bannedUsers: kotlin.Int? = null, + + @Schema(example = "5000", description = "") + @get:JsonProperty("totalCards") val totalCards: kotlin.Int? = null, + + @Schema(example = "1200", description = "") + @get:JsonProperty("totalTrades") val totalTrades: kotlin.Int? = null, + + @Schema(example = "1000", description = "") + @get:JsonProperty("completedTrades") val completedTrades: kotlin.Int? = null, + + @Schema(example = "50", description = "") + @get:JsonProperty("invalidatedTrades") val invalidatedTrades: kotlin.Int? = null, + + @Schema(example = "15", description = "") + @get:JsonProperty("pendingReports") val pendingReports: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminTradesTradeIDInvalidatePost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminTradesTradeIDInvalidatePost200Response.kt new file mode 100644 index 0000000..df120ba --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminTradesTradeIDInvalidatePost200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.admin + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + */ +data class AdminTradesTradeIDInvalidatePost200Response( + + @Schema(example = "Обмен успешно аннулирован", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDBanPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDBanPost200Response.kt new file mode 100644 index 0000000..19fa64c --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDBanPost200Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.admin + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + * @param banEndDate + */ +data class AdminUsersUserIDBanPost200Response( + + @Schema(example = "Пользователь успешно заблокирован", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null, + + @Schema(example = "2024-06-20T12:00Z", description = "") + @get:JsonProperty("banEndDate") val banEndDate: java.time.LocalDateTime? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDQuestsQuestIDResetPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDQuestsQuestIDResetPost200Response.kt new file mode 100644 index 0000000..9cf0466 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDQuestsQuestIDResetPost200Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.admin + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + * @param rewardsRemoved + */ +data class AdminUsersUserIDQuestsQuestIDResetPost200Response( + + @Schema(example = "Квест успешно сброшен", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null, + + @Schema(example = "true", description = "") + @get:JsonProperty("rewardsRemoved") val rewardsRemoved: kotlin.Boolean? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDUnbanPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDUnbanPost200Response.kt new file mode 100644 index 0000000..676999c --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/admin/AdminUsersUserIDUnbanPost200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.admin + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + */ +data class AdminUsersUserIDUnbanPost200Response( + + @Schema(example = "Пользователь успешно разблокирован", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/AuthCheckGet401Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/AuthCheckGet401Response.kt new file mode 100644 index 0000000..12d8f36 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/AuthCheckGet401Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.auth + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param isAuthenticated + * @param message + */ +data class AuthCheckGet401Response( + + @Schema(example = "false", description = "") + @get:JsonProperty("isAuthenticated") val isAuthenticated: kotlin.Boolean? = null, + + @Schema(example = "Требуется авторизация", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser200Response.kt new file mode 100644 index 0000000..f4e99c8 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser200Response.kt @@ -0,0 +1,23 @@ +package ru.vsu.app.dto.responses.auth.login + +import com.fasterxml.jackson.annotation.JsonProperty +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotNull +import ru.vsu.app.dto.UserDto + +/** + * + * @param user + * @param accessToken + */ +data class LoginUser200Response( + @field:NotNull + @Schema(description = "Данные пользователя", required = true) + @get:JsonProperty("user") + val user: UserDto, + + @field:NotNull + @Schema(description = "JWT токен для доступа", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") + @get:JsonProperty("accessToken") + val accessToken: String +) diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser400Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser400Response.kt new file mode 100644 index 0000000..e5a85bb --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser400Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.auth.login + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class LoginUser400Response( + + @Schema(example = "Неверный фортмат email", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser401Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser401Response.kt new file mode 100644 index 0000000..159921e --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/login/LoginUser401Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.auth.login + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class LoginUser401Response( + + @Schema(example = "Неверный email или пароль", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/refreshtoken/AuthRefreshToken200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/refreshtoken/AuthRefreshToken200Response.kt new file mode 100644 index 0000000..9bfe9a5 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/refreshtoken/AuthRefreshToken200Response.kt @@ -0,0 +1,29 @@ +package ru.vsu.app.dto.responses.auth.refreshtoken + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param accessToken + */ +data class AuthRefreshToken200Response( + + @field:NotNull + @Schema(description = "Обновлённый JWT токен для доступа", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") + @get:JsonProperty("accessToken") + val accessToken: String + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/refreshtoken/AuthRefreshToken401Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/refreshtoken/AuthRefreshToken401Response.kt new file mode 100644 index 0000000..4fc8d57 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/refreshtoken/AuthRefreshToken401Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.auth.refreshtoken + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param isAuthenticated + * @param message + */ +data class AuthRefreshToken401Response( + + @Schema(example = "false", description = "") + @get:JsonProperty("isAuthenticated") val isAuthenticated: kotlin.Boolean? = null, + + @Schema(example = "Требуется авторизация", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser200Response.kt new file mode 100644 index 0000000..7235868 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser200Response.kt @@ -0,0 +1,29 @@ +package ru.vsu.app.dto.responses.auth.register + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param tempToken + * @param message + */ +data class RegisterUser200Response( + + @Schema(example = "temp_abc123", description = "") + @get:JsonProperty("tempToken") val tempToken: kotlin.String? = null, + + @Schema(example = "Код подтверждения отправлен на email", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null +) + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser400Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser400Response.kt new file mode 100644 index 0000000..ca7be33 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser400Response.kt @@ -0,0 +1,25 @@ +package ru.vsu.app.dto.responses.auth.register + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class RegisterUser400Response( + + @Schema(example = "Некорректный формат введенных данных", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null +) + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser409Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser409Response.kt new file mode 100644 index 0000000..6ccff31 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/register/RegisterUser409Response.kt @@ -0,0 +1,24 @@ +package ru.vsu.app.dto.responses.auth.register + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class RegisterUser409Response( + + @Schema(example = "Пользователь с таким email уже существует", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null +) \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset200Response.kt new file mode 100644 index 0000000..bb48a43 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset200Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.auth.requestpassword + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param resetToken + * @param message + */ +data class RequestPasswordReset200Response( + + @Schema(example = "reset_xyz789", description = "") + @get:JsonProperty("resetToken") val resetToken: kotlin.String? = null, + + @Schema(example = "Код подтверждения отправлен на email", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset400Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset400Response.kt new file mode 100644 index 0000000..9f4cbc0 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset400Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.auth.requestpassword + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class RequestPasswordReset400Response( + + @Schema(example = "Некорректный формат email", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset404Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset404Response.kt new file mode 100644 index 0000000..30d2585 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/RequestPasswordReset404Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.auth.requestpassword + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class RequestPasswordReset404Response( + + @Schema(example = "Пользователь с таким email не зарегистрирован", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/ResetPassword200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/ResetPassword200Response.kt new file mode 100644 index 0000000..64ebe0d --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/ResetPassword200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.auth.requestpassword + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + */ +data class ResetPassword200Response( + + @Schema(example = "Пароль успешно изменен", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/ResetPassword400Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/ResetPassword400Response.kt new file mode 100644 index 0000000..75d07af --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/requestpassword/ResetPassword400Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.auth.requestpassword + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class ResetPassword400Response( + + @Schema(example = "Неверный код подтверждения", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/ResendVerificationCode200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/ResendVerificationCode200Response.kt new file mode 100644 index 0000000..a465b56 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/ResendVerificationCode200Response.kt @@ -0,0 +1,29 @@ +package ru.vsu.app.dto.responses.auth.verifyemail + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + */ +data class ResendVerificationCode200Response( + + @Schema(example = "Новый код подтверждения отправлен", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null, + @Schema(example = "Новый временный токен", description = "") + @get:JsonProperty("tempToken") val tempToken: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/VerifyEmail400Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/VerifyEmail400Response.kt new file mode 100644 index 0000000..a488df6 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/VerifyEmail400Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.auth.verifyemail + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + * @param canRetry + */ +data class VerifyEmail400Response( + + @Schema(example = "Неверный код подтверждения", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null, + + @Schema(example = "true", description = "") + @get:JsonProperty("canRetry") val canRetry: kotlin.Boolean? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/VerifyEmail410Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/VerifyEmail410Response.kt new file mode 100644 index 0000000..9abd6bf --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/auth/verifyemail/VerifyEmail410Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.auth.verifyemail + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class VerifyEmail410Response( + + @Schema(example = "Код устарел, запросите новый", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/common/InternalServerError.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/common/InternalServerError.kt new file mode 100644 index 0000000..5a2c78d --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/common/InternalServerError.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.common + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + * @param code + * @param timestamp + */ +data class InternalServerError( + @Schema(example = "Что-то пошло не так, попробуйте позже", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null, + + @Schema(example = "INTERNAL_SERVER_ERROR", description = "") + @get:JsonProperty("code") val code: kotlin.String? = null, + + @Schema(example = "2024-05-20T12:00Z", description = "") + @get:JsonProperty("timestamp") val timestamp: java.time.LocalDateTime? = null +) diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeGenerateCardPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeGenerateCardPost200Response.kt new file mode 100644 index 0000000..3127e89 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeGenerateCardPost200Response.kt @@ -0,0 +1,33 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.CardEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param card + * @param newBalance + */ +data class HomeGenerateCardPost200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("card") val card: CardEntity? = null, + + @Schema(example = "1500", description = "") + @get:JsonProperty("newBalance") val newBalance: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeGenerateCardPost402Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeGenerateCardPost402Response.kt new file mode 100644 index 0000000..8dcf42a --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeGenerateCardPost402Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + * @param requiredAmount + */ +data class HomeGenerateCardPost402Response( + + @Schema(example = "Недостаточно средств для генерации карты", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null, + + @Schema(example = "1000", description = "") + @get:JsonProperty("requiredAmount") val requiredAmount: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNewsGet404Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNewsGet404Response.kt new file mode 100644 index 0000000..799f8e8 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNewsGet404Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class HomeNewsGet404Response( + + @Schema(example = "Новостей пока нет", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNotificationsNotificationIDNavigatePost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNotificationsNotificationIDNavigatePost200Response.kt new file mode 100644 index 0000000..978560b --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNotificationsNotificationIDNavigatePost200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param redirectUrl + */ +data class HomeNotificationsNotificationIDNavigatePost200Response( + + @Schema(example = "/trades/123", description = "") + @get:JsonProperty("redirectUrl") val redirectUrl: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNotificationsNotificationIDNavigatePost404Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNotificationsNotificationIDNavigatePost404Response.kt new file mode 100644 index 0000000..c9325ac --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeNotificationsNotificationIDNavigatePost404Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class HomeNotificationsNotificationIDNavigatePost404Response( + + @Schema(example = "Данное предложение обмена уже не существует", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsClaimRewardPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsClaimRewardPost200Response.kt new file mode 100644 index 0000000..d778afb --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsClaimRewardPost200Response.kt @@ -0,0 +1,37 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.PackEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param receivedCoins + * @param receivedPacks + * @param newBalance + */ +data class HomeQuestsClaimRewardPost200Response( + + @Schema(example = "500", description = "") + @get:JsonProperty("receivedCoins") val receivedCoins: kotlin.Int? = null, + + @field:Valid + @Schema(description = "") + @get:JsonProperty("receivedPacks") val receivedPacks: kotlin.collections.List? = null, + + @Schema(example = "2000", description = "") + @get:JsonProperty("newBalance") val newBalance: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsClaimRewardPost400Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsClaimRewardPost400Response.kt new file mode 100644 index 0000000..08cac8b --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsClaimRewardPost400Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class HomeQuestsClaimRewardPost400Response( + + @Schema(example = "Выполнены не все квесты, необходимые для получения награды", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsGet200Response.kt new file mode 100644 index 0000000..24e4084 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsGet200Response.kt @@ -0,0 +1,34 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.QuestEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param dailyQuests + * @param weeklyQuests + */ +data class HomeQuestsGet200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("dailyQuests") val dailyQuests: kotlin.collections.List? = null, + + @field:Valid + @Schema(description = "") + @get:JsonProperty("weeklyQuests") val weeklyQuests: kotlin.collections.List? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsQuestIDChangeStatusPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsQuestIDChangeStatusPost200Response.kt new file mode 100644 index 0000000..3356b43 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeQuestsQuestIDChangeStatusPost200Response.kt @@ -0,0 +1,33 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.QuestEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param quest + * @param message + */ +data class HomeQuestsQuestIDChangeStatusPost200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("quest") val quest: QuestEntity? = null, + + @Schema(example = "Статус квеста успешно изменен", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeSearchGet400Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeSearchGet400Response.kt new file mode 100644 index 0000000..1695715 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeSearchGet400Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class HomeSearchGet400Response( + + @Schema(example = "Введите минимум 3 символа для поиска", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeSearchGet404Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeSearchGet404Response.kt new file mode 100644 index 0000000..feb6302 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/HomeSearchGet404Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.home + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class HomeSearchGet404Response( + + @Schema(example = "Пользователь с введеными данными не найден", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/NewsResponse.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/NewsResponse.kt new file mode 100644 index 0000000..2e85a19 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/home/NewsResponse.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.home + +import com.fasterxml.jackson.annotation.JsonProperty +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + +data class NewsResponse( + @Schema(description = "ID новости") + @get:JsonProperty("id") + val id: Int, + + @Schema(description = "Заголовок новости") + @get:JsonProperty("title") + val title: String, + + @Schema(description = "Содержание новости") + @get:JsonProperty("content") + val content: String, + + @Schema(description = "Дата создания") + @get:JsonProperty("createdAt") + val createdAt: LocalDateTime, + + @Schema(description = "Дата обновления") + @get:JsonProperty("updatedAt") + val updatedAt: LocalDateTime, + + @Schema(description = "Список изображений") + @get:JsonProperty("pictures") + val pictures: List? = null +) diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDFavoriteGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDFavoriteGet200Response.kt new file mode 100644 index 0000000..9ddd54e --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDFavoriteGet200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.inventory + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param isFavorite + */ +data class InventoryCardCardIDFavoriteGet200Response( + + @Schema(example = "false", description = "") + @get:JsonProperty("isFavorite") val isFavorite: kotlin.Boolean? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDQuantityGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDQuantityGet200Response.kt new file mode 100644 index 0000000..a5668e6 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDQuantityGet200Response.kt @@ -0,0 +1,35 @@ +package ru.vsu.app.dto.responses.inventory + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param count + * @param canDisassemble + * @param canTrade + */ +data class InventoryCardCardIDQuantityGet200Response( + + @Schema(example = "3", description = "") + @get:JsonProperty("count") val count: kotlin.Int? = null, + + @Schema(example = "true", description = "") + @get:JsonProperty("canDisassemble") val canDisassemble: kotlin.Boolean? = null, + + @Schema(example = "true", description = "") + @get:JsonProperty("canTrade") val canTrade: kotlin.Boolean? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDTradeCancelPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDTradeCancelPost200Response.kt new file mode 100644 index 0000000..02f6de8 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDTradeCancelPost200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.inventory + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + */ +data class InventoryCardCardIDTradeCancelPost200Response( + + @Schema(example = "Карта успешно снята с обмена", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDTradeStatusGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDTradeStatusGet200Response.kt new file mode 100644 index 0000000..ccea041 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryCardCardIDTradeStatusGet200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.inventory + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param isOnTrade Выставлена ли карта на обмен + */ +data class InventoryCardCardIDTradeStatusGet200Response( + + @Schema(example = "true", description = "Выставлена ли карта на обмен") + @get:JsonProperty("isOnTrade") val isOnTrade: kotlin.Boolean? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryDestroyPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryDestroyPost200Response.kt new file mode 100644 index 0000000..cd26fc1 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryDestroyPost200Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.inventory + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param newBalance + * @param destroyedCardId + */ +data class InventoryDestroyPost200Response( + + @Schema(example = "2550", description = "") + @get:JsonProperty("newBalance") val newBalance: kotlin.Int? = null, + + @Schema(example = "501", description = "") + @get:JsonProperty("destroyedCardId") val destroyedCardId: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryFavoritesCountGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryFavoritesCountGet200Response.kt new file mode 100644 index 0000000..59eaddd --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryFavoritesCountGet200Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.inventory + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param count + * @param canAddMore + */ +data class InventoryFavoritesCountGet200Response( + + @Schema(example = "3", description = "") + @get:JsonProperty("count") val count: kotlin.Int? = null, + + @Schema(example = "true", description = "") + @get:JsonProperty("canAddMore") val canAddMore: kotlin.Boolean? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryGet200Response.kt new file mode 100644 index 0000000..aef4a46 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryGet200Response.kt @@ -0,0 +1,45 @@ +package ru.vsu.app.dto.responses.inventory + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty + +import ru.vsu.app.dto.CardDto +import ru.vsu.app.dto.CollectionDto + +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param inventory + * @param favoriteCards ID избранных карт + * @param collections + * @param balance + */ +data class InventoryGet200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("inventory") val inventory: kotlin.collections.List? = null, + + @Schema(description = "ID избранных карт") + @get:JsonProperty("favoriteCards") val favoriteCards: kotlin.collections.List? = null, + + @field:Valid + @Schema(description = "") + @get:JsonProperty("collections") val collections: kotlin.collections.List? = null, + + @Schema(example = "2500", description = "") + @get:JsonProperty("balance") val balance: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryGet401Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryGet401Response.kt new file mode 100644 index 0000000..ad06499 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/inventory/InventoryGet401Response.kt @@ -0,0 +1,34 @@ +package ru.vsu.app.dto.responses.inventory + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.CardEntity +import ru.vsu.app.model.CollectionEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param isAuthenticated + * @param message + */ +data class InventoryGet401Response( + + @Schema(example = "false", description = "") + @get:JsonProperty("isAuthenticated") val isAuthenticated: kotlin.Boolean? = null, + + @Schema(example = "Требуется авторизация", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost200Response.kt new file mode 100644 index 0000000..e45c436 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param redirectUrl + */ +data class OtherProfileCardCardIDInitiateTradePost200Response( + + @Schema(example = "/trades/create?requested_card=123&owner=456", description = "") + @get:JsonProperty("redirectUrl") val redirectUrl: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost400Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost400Response.kt new file mode 100644 index 0000000..281c564 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost400Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class OtherProfileCardCardIDInitiateTradePost400Response( + + @Schema(example = "Недостаточно карт для обмена", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost409Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost409Response.kt new file mode 100644 index 0000000..c1377a4 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDInitiateTradePost409Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + * @param cardQuantity Количество экземпляров карты у владельца + */ +data class OtherProfileCardCardIDInitiateTradePost409Response( + + @Schema(example = "Невозможно предложить обмен - у владельца только один экземпляр этой карты", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null, + + @Schema(example = "1", description = "Количество экземпляров карты у владельца") + @get:JsonProperty("cardQuantity") val cardQuantity: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDViewGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDViewGet200Response.kt new file mode 100644 index 0000000..78a5cdf --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileCardCardIDViewGet200Response.kt @@ -0,0 +1,29 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.CardEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param card + */ +data class OtherProfileCardCardIDViewGet200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("card") val card: CardEntity? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDGet200Response.kt new file mode 100644 index 0000000..8ad479c --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDGet200Response.kt @@ -0,0 +1,55 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.AchievementEntity +import ru.vsu.app.model.CardEntity +import ru.vsu.app.model.UserStatsEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param userStats + * @param favoriteCards + * @param favoriteAchievements + * @param inventoryVisible + * @param id + * @param username + */ +data class OtherProfileUserIDGet200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("userStats") val userStats: UserStatsEntity? = null, + + @field:Valid + @get:Size(max=5) + @Schema(description = "") + @get:JsonProperty("favoriteCards") val favoriteCards: kotlin.collections.List? = null, + + @field:Valid + @get:Size(max=4) + @Schema(description = "") + @get:JsonProperty("favoriteAchievements") val favoriteAchievements: kotlin.collections.List? = null, + + @Schema(example = "true", description = "") + @get:JsonProperty("inventoryVisible") val inventoryVisible: kotlin.Boolean? = null, + + @Schema(example = "456", description = "") + @get:JsonProperty("id") val id: kotlin.Int? = null, + + @Schema(example = "Игрок_456", description = "") + @get:JsonProperty("username") val username: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDInventoryGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDInventoryGet200Response.kt new file mode 100644 index 0000000..50dfc71 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDInventoryGet200Response.kt @@ -0,0 +1,35 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.CardEntity +import ru.vsu.app.model.CollectionEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param cards + * @param collections + */ +data class OtherProfileUserIDInventoryGet200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("cards") val cards: kotlin.collections.List? = null, + + @field:Valid + @Schema(description = "") + @get:JsonProperty("collections") val collections: kotlin.collections.List? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDInventoryGet403Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDInventoryGet403Response.kt new file mode 100644 index 0000000..4028971 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDInventoryGet403Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class OtherProfileUserIDInventoryGet403Response( + + @Schema(example = "Пользователь скрыл свой инвентарь", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDReportPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDReportPost200Response.kt new file mode 100644 index 0000000..3e90200 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/OtherProfileUserIDReportPost200Response.kt @@ -0,0 +1,33 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.ReportEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param redirectUrl + * @param reportDraft + */ +data class OtherProfileUserIDReportPost200Response( + + @Schema(example = "/report/create?reported_user=123", description = "") + @get:JsonProperty("redirectUrl") val redirectUrl: kotlin.String? = null, + + @field:Valid + @Schema(description = "") + @get:JsonProperty("reportDraft") val reportDraft: ReportEntity? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileAchievementsFavoritesCountGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileAchievementsFavoritesCountGet200Response.kt new file mode 100644 index 0000000..517a7de --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileAchievementsFavoritesCountGet200Response.kt @@ -0,0 +1,32 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param count + * @param canAddMore + */ +data class ProfileAchievementsFavoritesCountGet200Response( + + @get:Max(4) + @Schema(example = "3", description = "") + @get:JsonProperty("count") val count: kotlin.Int? = null, + + @Schema(example = "true", description = "") + @get:JsonProperty("canAddMore") val canAddMore: kotlin.Boolean? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileAchievementsGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileAchievementsGet200Response.kt new file mode 100644 index 0000000..8266696 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileAchievementsGet200Response.kt @@ -0,0 +1,29 @@ +package ru.vsu.app.dto.responses.profile + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.dto.AchievementDto +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param achievements + */ +data class ProfileAchievementsGet200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("achievements") val achievements: kotlin.collections.List? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileGet200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileGet200Response.kt new file mode 100644 index 0000000..1b68e42 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/profile/ProfileGet200Response.kt @@ -0,0 +1,44 @@ +package ru.vsu.app.dto.responses.profile + +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.AchievementEntity +import ru.vsu.app.model.CardEntity +import ru.vsu.app.model.UserStatsEntity +import jakarta.validation.Valid +import jakarta.validation.constraints.Size +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param userStats + * @param favoriteCards + * @param favoriteAchievements + * @param userID ID пользователя + * @param username Имя пользователя + * @param avatarUrl Ссылка на аватар пользователя + */ +data class ProfileGet200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("userStats") val userStats: UserStatsEntity? = null, + + @field:Valid + @field:Size(max = 5) + @Schema(description = "") + @get:JsonProperty("favoriteCards") val favoriteCards: List? = null, + + @field:Valid + @field:Size(max = 4) + @Schema(description = "") + @get:JsonProperty("favoriteAchievements") val favoriteAchievements: List? = null, + + @Schema(example = "123", description = "ID пользователя") + @get:JsonProperty("user_id") val userID: Int? = null, + + @Schema(example = "Коллекционер_123", description = "Имя пользователя") + @get:JsonProperty("username") val username: String? = null, + + @Schema(example = "https://example.com/avatars/user123.jpg", description = "Ссылка на аватар пользователя") + @get:JsonProperty("avatar_url") val avatarUrl: String? = null +) diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopCoinsOffersOfferIdPurchasePost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopCoinsOffersOfferIdPurchasePost200Response.kt new file mode 100644 index 0000000..805e559 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopCoinsOffersOfferIdPurchasePost200Response.kt @@ -0,0 +1,28 @@ +package ru.vsu.app.dto.responses.shop + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param paymentUrl + */ +data class ShopCoinsOffersOfferIdPurchasePost200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("paymentUrl") val paymentUrl: java.net.URI? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdBuyPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdBuyPost200Response.kt new file mode 100644 index 0000000..0213755 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdBuyPost200Response.kt @@ -0,0 +1,33 @@ +package ru.vsu.app.dto.responses.shop + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.CardEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param receivedCards + * @param newBalance + */ +data class ShopPacksPackIdBuyPost200Response( + + @field:Valid + @Schema(description = "") + @get:JsonProperty("receivedCards") val receivedCards: kotlin.collections.List? = null, + + @Schema(description = "") + @get:JsonProperty("newBalance") val newBalance: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdBuyPost402Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdBuyPost402Response.kt new file mode 100644 index 0000000..db9b552 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdBuyPost402Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.shop + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + * @param requiredAmount + */ +data class ShopPacksPackIdBuyPost402Response( + + @Schema(example = "Недостаточно средств на балансе", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null, + + @Schema(example = "1000", description = "") + @get:JsonProperty("requiredAmount") val requiredAmount: kotlin.Int? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdOpenPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdOpenPost200Response.kt new file mode 100644 index 0000000..a5774e0 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPacksPackIdOpenPost200Response.kt @@ -0,0 +1,33 @@ +package ru.vsu.app.dto.responses.shop + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import ru.vsu.app.model.CardEntity +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param receivedCards Карты, полученные из набора + * @param newCardsAdded Были ли карты успешно добавлены в инвентарь + */ +data class ShopPacksPackIdOpenPost200Response( + + @field:Valid + @Schema(description = "Карты, полученные из набора") + @get:JsonProperty("receivedCards") val receivedCards: kotlin.collections.List? = null, + + @Schema(example = "true", description = "Были ли карты успешно добавлены в инвентарь") + @get:JsonProperty("newCardsAdded") val newCardsAdded: kotlin.Boolean? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPaymentsProcessPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPaymentsProcessPost200Response.kt new file mode 100644 index 0000000..24db0f3 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/shop/ShopPaymentsProcessPost200Response.kt @@ -0,0 +1,31 @@ +package ru.vsu.app.dto.responses.shop + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param success + * @param transactionId + */ +data class ShopPaymentsProcessPost200Response( + + @Schema(description = "") + @get:JsonProperty("success") val success: kotlin.Boolean? = null, + + @Schema(description = "") + @get:JsonProperty("transactionId") val transactionId: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdAcceptPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdAcceptPost200Response.kt new file mode 100644 index 0000000..e5e33b7 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdAcceptPost200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.trades + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + */ +data class TradesTradeIdAcceptPost200Response( + + @Schema(example = "Обмен успешно завершен", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdAcceptPost403Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdAcceptPost403Response.kt new file mode 100644 index 0000000..9814af0 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdAcceptPost403Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.trades + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class TradesTradeIdAcceptPost403Response( + + @Schema(example = "Вы не можете принять этот обмен", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdCancelPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdCancelPost200Response.kt new file mode 100644 index 0000000..dbcf305 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdCancelPost200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.trades + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + */ +data class TradesTradeIdCancelPost200Response( + + @Schema(example = "Обмен отменен", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdGet404Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdGet404Response.kt new file mode 100644 index 0000000..ddc8b5c --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdGet404Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.trades + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param error + */ +data class TradesTradeIdGet404Response( + + @Schema(example = "Предложение обмена не найдено", description = "") + @get:JsonProperty("error") val error: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdRejectPost200Response.kt b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdRejectPost200Response.kt new file mode 100644 index 0000000..40e61fe --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/dto/responses/trades/TradesTradeIdRejectPost200Response.kt @@ -0,0 +1,27 @@ +package ru.vsu.app.dto.responses.trades + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * + * @param message + */ +data class TradesTradeIdRejectPost200Response( + + @Schema(example = "Обмен отклонен", description = "") + @get:JsonProperty("message") val message: kotlin.String? = null + ) { + +} + diff --git a/backend/src/main/kotlin/ru/vsu/app/mapper/AchievementMapper.kt b/backend/src/main/kotlin/ru/vsu/app/mapper/AchievementMapper.kt new file mode 100644 index 0000000..67f114e --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/mapper/AchievementMapper.kt @@ -0,0 +1,18 @@ +package ru.vsu.app.mapper + +import org.springframework.stereotype.Component +import ru.vsu.app.dto.AchievementDto +import ru.vsu.app.model.AchievementEntity + +@Component +class AchievementMapper { + fun toDto(entity: AchievementEntity): AchievementDto { + return AchievementDto( + achievementID = entity.achievementID, + name = entity.name, + imageURL = entity.imageURL, + description = entity.description, + isUnlocked = entity.isUnlocked + ) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/mapper/CardMapper.kt b/backend/src/main/kotlin/ru/vsu/app/mapper/CardMapper.kt new file mode 100644 index 0000000..c8b1418 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/mapper/CardMapper.kt @@ -0,0 +1,24 @@ +package ru.vsu.app.mapper + +import org.springframework.stereotype.Component +import ru.vsu.app.dto.CardDto +import ru.vsu.app.model.CardEntity +import ru.vsu.app.mapper.ThemeMapper + +@Component +class CardMapper( + private val themeMapper: ThemeMapper +) { + fun toDto(cardEntity: CardEntity): CardDto { + return CardDto( + cardID = cardEntity.id, + name = cardEntity.name, + description = cardEntity.description, + rarity = CardDto.Rarity.forValue(cardEntity.rarity.value), + imageURL = cardEntity.imageUrl, + minPrice = cardEntity.disassemblePrice, + isGenerated = cardEntity.isGenerated, + theme = cardEntity.theme?.let { themeMapper.toDto(it) } + ) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/mapper/CollectionMapper.kt b/backend/src/main/kotlin/ru/vsu/app/mapper/CollectionMapper.kt new file mode 100644 index 0000000..a1f1e90 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/mapper/CollectionMapper.kt @@ -0,0 +1,20 @@ +package ru.vsu.app.mapper + +import org.springframework.stereotype.Component +import ru.vsu.app.dto.CollectionDto +import ru.vsu.app.model.CollectionEntity +import ru.vsu.app.mapper.CardMapper + +@Component +class CollectionMapper( + private val cardMapper: CardMapper +) { + fun toDto(entity: CollectionEntity): CollectionDto { + return CollectionDto( + collectionID = entity.collectionID, + name = entity.name, + cards = entity.cards.map { cardMapper.toDto(it) }, + imageURL = entity.imageURL + ) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/mapper/NotificationMapper.kt b/backend/src/main/kotlin/ru/vsu/app/mapper/NotificationMapper.kt new file mode 100644 index 0000000..dfaa1b9 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/mapper/NotificationMapper.kt @@ -0,0 +1,18 @@ +package ru.vsu.app.mapper + +import org.springframework.stereotype.Component +import ru.vsu.app.dto.NotificationDto +import ru.vsu.app.model.NotificationEntity + +@Component +class NotificationMapper { + fun toDto(entity: NotificationEntity): NotificationDto { + return NotificationDto( + notificationID = entity.notificationID, + userID = entity.user.userId, + message = entity.message, + notificationDateTime = entity.notificationDateTime, + links = entity.links.takeIf { it.isNotEmpty() } + ) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/mapper/ThemeMapper.kt b/backend/src/main/kotlin/ru/vsu/app/mapper/ThemeMapper.kt new file mode 100644 index 0000000..34092f7 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/mapper/ThemeMapper.kt @@ -0,0 +1,25 @@ +package ru.vsu.app.mapper + +import org.springframework.stereotype.Component +import ru.vsu.app.dto.ThemeDto +import ru.vsu.app.model.ThemeEntity + +@Component +class ThemeMapper { + + fun toDto(entity: ThemeEntity): ThemeDto { + return ThemeDto( + themeID = entity.themeID, + name = entity.name, + description = entity.description + ) + + fun toEntity(dto: ThemeDto): ThemeEntity { + return ThemeEntity( + themeID = dto.themeID, + name = dto.name, + description = dto.description + ) + } +} +} diff --git a/backend/src/main/kotlin/ru/vsu/app/mapper/TradeMapper.kt b/backend/src/main/kotlin/ru/vsu/app/mapper/TradeMapper.kt new file mode 100644 index 0000000..fa6e649 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/mapper/TradeMapper.kt @@ -0,0 +1,23 @@ +package ru.vsu.app.mapper + +import ru.vsu.app.dto.TradeDto +import ru.vsu.app.model.TradeEntity +import org.springframework.stereotype.Component + +@Component +class TradeMapper( + private val userMapper: UserMapper, + private val cardMapper: CardMapper +) { + fun toDto(entity: TradeEntity): TradeDto { + return TradeDto( + tradeID = entity.tradeID, + offeringUser = userMapper.toDto(entity.offeringUser), + offeringCards = entity.offeringCards.map { cardMapper.toDto(it) }, + receivingUser = userMapper.toDto(entity.receivingUser), + receivingCards = entity.receivingCards.map { cardMapper.toDto(it) }, + isConfirmed = entity.isConfirmed, + tradeDateTime = entity.tradeDateTime + ) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/mapper/UserMapper.kt b/backend/src/main/kotlin/ru/vsu/app/mapper/UserMapper.kt new file mode 100644 index 0000000..e0023bc --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/mapper/UserMapper.kt @@ -0,0 +1,28 @@ +package ru.vsu.app.mapper + +import org.springframework.stereotype.Component +import ru.vsu.app.dto.* +import ru.vsu.app.model.* + +@Component +class UserMapper( + private val cardMapper: CardMapper, + private val achievementMapper: AchievementMapper, + private val notificationMapper: NotificationMapper +) { + fun toDto(entity: UserEntity): UserDto { + return UserDto( + userID = entity.userId, + username = entity.username, + email = entity.email, + inventoryCards = entity.inventoryCards.map { cardMapper.toDto(it) }, + balance = entity.balance, + achievements = entity.achievements.map { achievementMapper.toDto(it) }, + favoriteCards = if (entity.favoriteCards.isEmpty()) null else entity.favoriteCards.map { cardMapper.toDto(it) }, + onChange = if (entity.onChange.isEmpty()) null else entity.onChange.map { cardMapper.toDto(it) }, + avatarUrl = entity.avatarUrl, + favoriteAchievements = if (entity.favoriteAchievements.isEmpty()) null else entity.favoriteAchievements.map { achievementMapper.toDto(it) }, + notifications = if (entity.notifications.isEmpty()) null else entity.notifications.map { notificationMapper.toDto(it) } + ) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/metrics/AuthMetrics.kt b/backend/src/main/kotlin/ru/vsu/app/metrics/AuthMetrics.kt new file mode 100644 index 0000000..1859339 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/metrics/AuthMetrics.kt @@ -0,0 +1,176 @@ +package ru.vsu.app.metrics + +import org.springframework.stereotype.Service + +@Service +class AuthMetrics(private val metrics: MetricsRegistry) { + + // ===== Авторизация ===== + fun loginAttempt() { + metrics.count("auth.login.attempt") + } + + fun loginSuccess(userId: String) { + metrics.count("auth.login.success", listOf(MetricTags.user(userId))) + } + + fun loginFailure() { + metrics.count("auth.login.failure") + } + + fun loginDuration(duration: Long) { + metrics.timer("auth.login.duration", duration) + } + + // ===== Регистрация ===== + + fun registerAttempt() { + metrics.count("auth.register.attempt") + } + + fun registerSuccess() { + metrics.count("auth.register.success") + } + + fun registerConflict() { + metrics.count("auth.register.conflict") // 409 + } + + fun registerValidationError() { + metrics.count("auth.register.validation_error") // 400 + } + + fun registerFailure() { + metrics.count("auth.register.failure") // 500 или default + } + + fun registerDuration(durationMs: Long) { + metrics.timer("auth.register.duration", durationMs) + } + + // ===== Верификация кода Email ===== + + fun verifyAttempt() { + metrics.count("auth.verify.attempt") + } + + fun verifySuccess(userId: String) { + metrics.count("auth.verify.success", listOf(MetricTags.user(userId))) + } + + fun verifyValidationError() { + metrics.count("auth.verify.validation_error") // 400 + } + + fun verifyExpired() { + metrics.count("auth.verify.expired") // 410 + } + + fun verifyFailure() { + metrics.count("auth.verify.failure") // 500 или default + } + + fun verifyDuration(durationMs: Long, userId: String? = null) { + if (userId != null) { + metrics.timer("auth.verify.duration", durationMs, listOf(MetricTags.user(userId))) + } else { + metrics.timer("auth.verify.duration", durationMs) + } + } + + // ===== Переотправка кода на Email ===== + + fun resendVerificationCodeAttempt() { + metrics.count("auth.resend_code.attempt") + } + + fun resendVerificationCodeSuccess() { + metrics.count("auth.resend_code.success") + } + + fun resendVerificationCodeValidationError() { + metrics.count("auth.resend_code.validation_error") // 400 + } + + fun resendVerificationCodeFailure() { + metrics.count("auth.resend_code.failure") // 500 или default + } + + fun resendVerificationCodeDuration(durationMs: Long) { + metrics.timer("auth.resend_code.duration", durationMs) + } + + // ===== Сброс пароля через токен и код ===== + fun resetPasswordAttempt() { + metrics.count("auth.reset.password.attempt") + } + + + fun resetPasswordDuration(durationMs: Long) { + metrics.timer("auth.reset.password.duration", durationMs) + } + + fun resetPasswordSuccess() { + metrics.count("auth.reset.password.success") + } + + fun resetPasswordValidationError() { + metrics.count("reset.password.validation.error") + } + + fun resetPasswordFailure() { + metrics.count("reset.password.failure") + } + + // ===== Обновление пароля после проверки ===== + fun passwordResetSuccess() { + metrics.count("password.reset.success") + } + + fun passwordResetFailure() { + metrics.count("password.reset.failure") + } + + fun passwordResetDuration(durationMs: Long) { + metrics.timer("password.reset.duration", durationMs) + } + + fun passwordResetAttempt() { + metrics.count("password.reset.attempt") + } + + // ===== Обновление токена (refresh) ===== + fun refreshSuccess() { + metrics.count("auth.refresh.success") + } + + fun refreshFailure() { + metrics.count("auth.refresh.failure") + } + + fun refreshDuration(durationMs: Long) { + metrics.timer("auth.refresh.duration", durationMs) + } + + fun refreshAttempt() { + metrics.count("auth.refresh.attempt") + } + + // ===== Проверка авторизации ===== + fun authCheckAttempt() { + metrics.count("auth.check.attempt") + } + + fun authCheckAuthorized() { + metrics.count("auth.check.authorized") + } + + fun authCheckUnauthorized() { + metrics.count("auth.check.unauthorized") + } + + fun authCheckDuration(durationMs: Long) { + metrics.timer("auth.check.duration", durationMs) + } + +} diff --git a/backend/src/main/kotlin/ru/vsu/app/metrics/InventoryMetrics.kt b/backend/src/main/kotlin/ru/vsu/app/metrics/InventoryMetrics.kt new file mode 100644 index 0000000..7fd4dfa --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/metrics/InventoryMetrics.kt @@ -0,0 +1,89 @@ +package ru.vsu.app.metrics + +import org.springframework.stereotype.Service + +@Service +class InventoryMetrics(private val metrics: MetricsRegistry) { + + // ===== Получение инвентаря ===== + + fun inventoryGetAttempt() { + metrics.count("inventory.get.attempt") + } + + fun inventoryGetSuccess() { + metrics.count("inventory.get.success") + } + + fun inventoryGetUnauthorized() { + metrics.count("inventory.get.unauthorized") // 401 + } + + fun inventoryGetValidationError() { + metrics.count("inventory.get.validation_error") // 400 + } + + fun inventoryGetFailure() { + metrics.count("inventory.get.failure") // 500 или unexpected + } + + fun inventoryGetDuration(durationMs: Long, userId: String? = null) { + if (userId != null) { + metrics.timer("inventory.get.duration", durationMs, listOf(MetricTags.user(userId))) + } else { + metrics.timer("inventory.get.duration", durationMs) + } + } + + // ===== Обновление/изменение инвентаря ===== + + fun inventoryUpdateAttempt() { + metrics.count("inventory.update.attempt") + } + + fun inventoryUpdateSuccess(userId: String) { + metrics.count("inventory.update.success", listOf(MetricTags.user(userId))) + } + + fun inventoryUpdateValidationError() { + metrics.count("inventory.update.validation_error") + } + + fun inventoryUpdateFailure() { + metrics.count("inventory.update.failure") + } + + fun inventoryUpdateDuration(durationMs: Long, userId: String? = null) { + if (userId != null) { + metrics.timer("inventory.update.duration", durationMs, listOf(MetricTags.user(userId))) + } else { + metrics.timer("inventory.update.duration", durationMs) + } + } + + // ===== Удаление предмета из инвентаря ===== + + fun inventoryDeleteAttempt() { + metrics.count("inventory.delete.attempt") + } + + fun inventoryDeleteSuccess(userId: String) { + metrics.count("inventory.delete.success", listOf(MetricTags.user(userId))) + } + + fun inventoryDeleteValidationError() { + metrics.count("inventory.delete.validation_error") + } + + fun inventoryDeleteFailure() { + metrics.count("inventory.delete.failure") + } + + fun inventoryDeleteDuration(durationMs: Long, userId: String? = null) { + if (userId != null) { + metrics.timer("inventory.delete.duration", durationMs, listOf(MetricTags.user(userId))) + } else { + metrics.timer("inventory.delete.duration", durationMs) + } + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/metrics/MetricTags.kt b/backend/src/main/kotlin/ru/vsu/app/metrics/MetricTags.kt new file mode 100644 index 0000000..eea52ca --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/metrics/MetricTags.kt @@ -0,0 +1,6 @@ +package ru.vsu.app.metrics + +object MetricTags { + fun user(id: String) = "userId" to id + fun source(value: String) = "source" to value +} diff --git a/backend/src/main/kotlin/ru/vsu/app/metrics/MetricsRegistry.kt b/backend/src/main/kotlin/ru/vsu/app/metrics/MetricsRegistry.kt new file mode 100644 index 0000000..61210fe --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/metrics/MetricsRegistry.kt @@ -0,0 +1,18 @@ +package ru.vsu.app.metrics + +import io.micrometer.core.instrument.MeterRegistry +import org.springframework.stereotype.Component +import java.util.concurrent.TimeUnit + +@Component +class MetricsRegistry(private val registry: MeterRegistry) { + + fun count(name: String, tags: List> = emptyList()) { + registry.counter(name, *tags.toTypedArray().flatMap { listOf(it.first, it.second) }.toTypedArray()).increment() + } + + fun timer(name: String, durationMillis: Long, tags: List> = emptyList()) { + registry.timer(name, *tags.toTypedArray().flatMap { listOf(it.first, it.second) }.toTypedArray()) + .record(durationMillis, TimeUnit.MILLISECONDS) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/model/AchievementEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/AchievementEntity.kt new file mode 100644 index 0000000..2a873fb --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/AchievementEntity.kt @@ -0,0 +1,26 @@ +package ru.vsu.app.model + +import jakarta.persistence.* +import java.util.* + +@Entity +@Table(name = "achievements") +data class AchievementEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "achievement_id") + val achievementID: Int = 0, + + @Column(name = "name", nullable = false) + val name: String, + + @Column(name = "image_url", nullable = false) + val imageURL: String, + + @Column(name = "description", nullable = false) + val description: String, + + @Column(name = "is_unlocked", nullable = false) + val isUnlocked: Boolean = false +) diff --git a/backend/src/main/kotlin/ru/vsu/app/model/Card.kt b/backend/src/main/kotlin/ru/vsu/app/model/Card.kt deleted file mode 100644 index 0e0645a..0000000 --- a/backend/src/main/kotlin/ru/vsu/app/model/Card.kt +++ /dev/null @@ -1,36 +0,0 @@ -package ru.vsu.app.model - -import jakarta.persistence.* - -@Entity -@Table(name = "cards") -data class Card( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long = 0, - - @Column(nullable = false) - val name: String, - - @Column(nullable = false) - val imageUrl: String, - - @Column(nullable = false) - val rarity: Int, - - @Column(nullable = false) - val collection: String, - - @Column(nullable = false, length = 1000) - val description: String, - - @Column(nullable = false) - val type: String, - - @Column(nullable = false) - val disassemblePrice: Int = 150, - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - val owner: User? = null -) \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/model/CardEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/CardEntity.kt new file mode 100644 index 0000000..77569cb --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/CardEntity.kt @@ -0,0 +1,59 @@ +package ru.vsu.app.model + +import jakarta.persistence.* + +import ru.vsu.app.model.UserEntity +import ru.vsu.app.model.ThemeEntity + +@Entity +@Table(name = "cards") +data class CardEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + val id: Int = 0, + + @Column(name = "name", nullable = false) + val name: String, + + @Column(name = "image_url", nullable = false) + val imageUrl: String, + + @Column(name = "rarity", nullable = false) + @Enumerated(EnumType.STRING) + val rarity: Rarity, + + @Column(name = "disassemble_price", nullable = false) + val disassemblePrice: Int, + + @Column(name = "is_generated", nullable = false) + val isGenerated: Boolean, + + @Column(name = "description") + val description: String? = null, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "theme_id") + val theme: ThemeEntity? = null, + + @ManyToOne + @JoinColumn(name = "collection_id") + val collection: CollectionEntity? = null, + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "owner_id") + val owner: UserEntity? = null, +) { + enum class Rarity(val value: String) { + Обычная("Обычная"), + Редкая("Редкая"), + Эпическая("Эпическая"), + Легендарная("Легендарная"), + Уникальная("Уникальная"); + + companion object { + fun forValue(value: String): Rarity = + values().first { it.value == value } + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/model/CoinOffer.kt b/backend/src/main/kotlin/ru/vsu/app/model/CoinOfferEntity.kt similarity index 94% rename from backend/src/main/kotlin/ru/vsu/app/model/CoinOffer.kt rename to backend/src/main/kotlin/ru/vsu/app/model/CoinOfferEntity.kt index 9b81ff0..59cd881 100644 --- a/backend/src/main/kotlin/ru/vsu/app/model/CoinOffer.kt +++ b/backend/src/main/kotlin/ru/vsu/app/model/CoinOfferEntity.kt @@ -4,7 +4,7 @@ import jakarta.persistence.* @Entity @Table(name = "coin_offers") -data class CoinOffer( +data class CoinOfferEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0, diff --git a/backend/src/main/kotlin/ru/vsu/app/model/CollectionEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/CollectionEntity.kt new file mode 100644 index 0000000..ff9b09f --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/CollectionEntity.kt @@ -0,0 +1,22 @@ +package ru.vsu.app.model + +import jakarta.persistence.* + +@Entity +@Table(name = "collections") +data class CollectionEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "collection_id") + val collectionID: Int = 0, + + @Column(nullable = false) + val name: String, + + @OneToMany(mappedBy = "collection", cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) + val cards: List = emptyList(), + + @Column(name = "image_url", nullable = false) + val imageURL: String +) diff --git a/backend/src/main/kotlin/ru/vsu/app/model/NewsEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/NewsEntity.kt new file mode 100644 index 0000000..f418d5e --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/NewsEntity.kt @@ -0,0 +1,16 @@ +package ru.vsu.app.model + +import jakarta.persistence.* +import java.time.LocalDateTime + +@Entity +data class NewsEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Int? = null, + + var title: String, + var content: String, + val createdAt: LocalDateTime, + var updatedAt: LocalDateTime +) diff --git a/backend/src/main/kotlin/ru/vsu/app/model/NotificationEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/NotificationEntity.kt new file mode 100644 index 0000000..203e4a9 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/NotificationEntity.kt @@ -0,0 +1,29 @@ +package ru.vsu.app.model + +import jakarta.persistence.* +import java.time.LocalDateTime + +@Entity +@Table(name = "notifications") +data class NotificationEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "notification_ID") + val notificationID: Int = 0, + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "user_id", nullable = false) + val user: UserEntity, + + @Column(nullable = false) + val message: String, + + @Column(nullable = false) + val notificationDateTime: LocalDateTime, + + @ElementCollection + @CollectionTable(name = "notification_links", joinColumns = [JoinColumn(name = "notification_ID")]) + @Column(name = "link") + val links: List = emptyList() +) diff --git a/backend/src/main/kotlin/ru/vsu/app/model/Pack.kt b/backend/src/main/kotlin/ru/vsu/app/model/PackEntity.kt similarity index 88% rename from backend/src/main/kotlin/ru/vsu/app/model/Pack.kt rename to backend/src/main/kotlin/ru/vsu/app/model/PackEntity.kt index 02a263f..f24f0f2 100644 --- a/backend/src/main/kotlin/ru/vsu/app/model/Pack.kt +++ b/backend/src/main/kotlin/ru/vsu/app/model/PackEntity.kt @@ -4,7 +4,7 @@ import jakarta.persistence.* @Entity @Table(name = "packs") -data class Pack( +data class PackEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0, @@ -24,5 +24,5 @@ data class Pack( joinColumns = [JoinColumn(name = "pack_id")], inverseJoinColumns = [JoinColumn(name = "card_id")] ) - val cards: List = emptyList() + val cards: List = emptyList() ) \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/model/PaymentEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/PaymentEntity.kt new file mode 100644 index 0000000..622d141 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/PaymentEntity.kt @@ -0,0 +1,45 @@ +package ru.vsu.app.model + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.* +import java.time.LocalDateTime + +@Entity +@Table(name = "payments") +data class PaymentEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "payment_id") + val paymentID: Int = 0, + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) + @JsonIgnore + val user: UserEntity, + + @Column(name = "payment_sum", nullable = false) + val paymentSUM: Double, + + @Enumerated(EnumType.STRING) + @Column(name = "payment_status", nullable = false) + val paymentStatus: PaymentStatus, + + @Column(name = "payment_datetime", nullable = false) + val paymentDateTime: LocalDateTime, + + // Эти данные сохраняются только при создании, поэтому можно не сохранять в БД или сохранять как JSON/строку + @ElementCollection + @CollectionTable(name = "payment_card_data", joinColumns = [JoinColumn(name = "payment_id")]) + @Column(name = "card_data") + val bankCardData: List? = null +) { + + enum class PaymentStatus(val value: String) { + В_обработке("В обработке"), + Оплачено("Оплачено"), + Ошибка("Ошибка"); + + override fun toString(): String = value + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/model/QuestEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/QuestEntity.kt new file mode 100644 index 0000000..4b6f8c5 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/QuestEntity.kt @@ -0,0 +1,42 @@ +package ru.vsu.app.model + +import jakarta.persistence.* + +@Entity +@Table(name = "quests") +data class QuestEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "quest_id") + val questID: Int = 0, + + @Column(nullable = false) + val name: String, + + @Column(nullable = false) + val description: String, + + @Column(nullable = false) + val progress: Int, + + @Column(nullable = false) + val target: Int, + + @Column(name = "reward_coins", nullable = false) + val rewardCoins: Int, + + @Column(name = "is_completed", nullable = false) + val isCompleted: Boolean, + + @Column(name = "is_claimed", nullable = false) + val isClaimed: Boolean, + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "quest_reward_packs", + joinColumns = [JoinColumn(name = "quest_id")], + inverseJoinColumns = [JoinColumn(name = "pack_id")] + ) + val rewardPacks: List? = null +) diff --git a/backend/src/main/kotlin/ru/vsu/app/model/ReportEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/ReportEntity.kt new file mode 100644 index 0000000..e0bc694 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/ReportEntity.kt @@ -0,0 +1,37 @@ +package ru.vsu.app.model + +import jakarta.persistence.* +import java.time.LocalDateTime + +@Entity +@Table(name = "reports") +data class ReportEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0, + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "reporter_id", nullable = false) + val reporter: UserEntity, + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "reported_user_id", nullable = false) + val reportedUser: UserEntity, + + @Column(nullable = false) + val reportDateTime: LocalDateTime, + + @Column(nullable = false) + val reason: String, + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + val status: Status +) { + enum class Status { + На_рассмотрении, + Подтверждено, + Отклонено + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/model/ThemeEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/ThemeEntity.kt new file mode 100644 index 0000000..a965275 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/ThemeEntity.kt @@ -0,0 +1,19 @@ +package ru.vsu.app.model + +import jakarta.persistence.* + +@Entity +@Table(name = "themes") +data class ThemeEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "theme_id") + val themeID: Int = 0, + + @Column(name = "name", nullable = false) + val name: String, + + @Column(name = "description") + val description: String? = null +) diff --git a/backend/src/main/kotlin/ru/vsu/app/model/TradeEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/TradeEntity.kt new file mode 100644 index 0000000..dcd4b75 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/TradeEntity.kt @@ -0,0 +1,44 @@ +package ru.vsu.app.model + +import jakarta.persistence.* +import java.time.LocalDateTime + +@Entity +@Table(name = "trades") +data class TradeEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "trade_id") + val tradeID: Int = 0, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "offering_user_id", nullable = false) + val offeringUser: UserEntity, + + @ManyToMany + @JoinTable( + name = "trade_offering_cards", + joinColumns = [JoinColumn(name = "trade_id")], + inverseJoinColumns = [JoinColumn(name = "card_id")] + ) + val offeringCards: List = emptyList(), + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "receiving_user_id", nullable = false) + val receivingUser: UserEntity, + + @ManyToMany + @JoinTable( + name = "trade_receiving_cards", + joinColumns = [JoinColumn(name = "trade_id")], + inverseJoinColumns = [JoinColumn(name = "card_id")] + ) + val receivingCards: List = emptyList(), + + @Column(name = "is_confirmed", nullable = false) + val isConfirmed: Boolean = false, + + @Column(name = "trade_date_time", nullable = false) + val tradeDateTime: LocalDateTime +) diff --git a/backend/src/main/kotlin/ru/vsu/app/model/User.kt b/backend/src/main/kotlin/ru/vsu/app/model/User.kt deleted file mode 100644 index f010311..0000000 --- a/backend/src/main/kotlin/ru/vsu/app/model/User.kt +++ /dev/null @@ -1,31 +0,0 @@ -package ru.vsu.app.model - -import jakarta.persistence.* - -@Entity -@Table(name = "users") -data class User( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long = 0, - - @Column(unique = true, nullable = false) - val email: String, - - @Column(unique = true, nullable = false) - val username: String, - - @Column(nullable = false) - val passwordHash: String, - - val isEnabled: Boolean = false, - - val activationToken: String? = null, - - val passwordResetToken: String? = null, - - val passwordResetTokenExpiry: Long? = null, - - @Column(nullable = false) - var coins: Int = 1000 -) \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/model/UserEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/UserEntity.kt new file mode 100644 index 0000000..0e92a2e --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/UserEntity.kt @@ -0,0 +1,98 @@ +package ru.vsu.app.model + +import jakarta.persistence.* + +@Entity +@Table(name = "users") +data class UserEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_id") + val userId: Int = 0, + + @Column(unique = true, nullable = false) + val email: String, + + @Column(unique = true, nullable = false) + val username: String, + + @Column(nullable = false) + val passwordHash: String, + + val isEnabled: Boolean = false, + + val activationToken: String? = null, + + val passwordResetToken: String? = null, + + val passwordResetTokenExpiry: Long? = null, + + @Column(nullable = false) + var balance: Int = 1000, + + @Column(nullable = true, name = "avatar_url") + var avatarUrl: String? = null, + + @OneToMany(targetEntity = CardEntity::class, fetch = FetchType.EAGER) + @JoinTable( + name = "user_inventory_cards", + joinColumns = [JoinColumn(name = "user_id")], + inverseJoinColumns = [JoinColumn(name = "card_id")] + ) + var inventoryCards: List = emptyList(), + + @OneToMany(targetEntity = CardEntity::class, fetch = FetchType.EAGER) + @JoinTable( + name = "user_favorite_cards", + joinColumns = [JoinColumn(name = "user_id")], + inverseJoinColumns = [JoinColumn(name = "card_id")] + ) + var favoriteCards: List = emptyList(), + + @OneToMany(targetEntity = CardEntity::class, fetch = FetchType.EAGER) + @JoinTable( + name = "user_onchange_cards", + joinColumns = [JoinColumn(name = "user_id")], + inverseJoinColumns = [JoinColumn(name = "card_id")] + ) + var onChange: List = emptyList(), + + @OneToMany(targetEntity = AchievementEntity::class, fetch = FetchType.EAGER) + @JoinTable( + name = "user_achievements", + joinColumns = [JoinColumn(name = "user_id")], + inverseJoinColumns = [JoinColumn(name = "achievement_id")] + ) + var achievements: List = emptyList(), + + @OneToMany(targetEntity = AchievementEntity::class, fetch = FetchType.EAGER) + @JoinTable( + name = "user_favorite_achievements", + joinColumns = [JoinColumn(name = "user_id")], + inverseJoinColumns = [JoinColumn(name = "achievement_id")] + ) + var favoriteAchievements: List = emptyList(), + + @OneToMany(targetEntity = NotificationEntity::class, fetch = FetchType.EAGER) + @JoinTable( + name = "user_notifications", + joinColumns = [JoinColumn(name = "user_id")], + inverseJoinColumns = [JoinColumn(name = "notification_id")] + ) + var notifications: List = emptyList(), + + @OneToMany(targetEntity = CollectionEntity::class, fetch = FetchType.EAGER) + @JoinTable( + name = "user_completed_collections", + joinColumns = [JoinColumn(name = "user_id")], + inverseJoinColumns = [JoinColumn(name = "collection_id")] + ) + var completedCollections: List = emptyList(), + + @Enumerated(EnumType.STRING) + var role: Role = Role.USER +) + +enum class Role { + USER, ADMIN +} diff --git a/backend/src/main/kotlin/ru/vsu/app/model/UserSettingsEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/UserSettingsEntity.kt new file mode 100644 index 0000000..a464f8c --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/UserSettingsEntity.kt @@ -0,0 +1,26 @@ +package ru.vsu.app.model + +import jakarta.persistence.* + +@Entity +@Table(name = "user_settings") +data class UserSettingsEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "settings_id") + val id: Int = 0, + + @Column(name = "notifications_enabled", nullable = false) + val notificationsEnabled: Boolean = true, + + @Column(name = "show_inventory", nullable = false) + val showInventory: Boolean = false, + + @Column(name = "auto_decline_trades", nullable = false) + val autoDeclineTrades: Boolean = false, + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", unique = true, nullable = false) + val user: UserEntity +) diff --git a/backend/src/main/kotlin/ru/vsu/app/model/UserStatsEntity.kt b/backend/src/main/kotlin/ru/vsu/app/model/UserStatsEntity.kt new file mode 100644 index 0000000..2fbca9f --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/model/UserStatsEntity.kt @@ -0,0 +1,22 @@ +package ru.vsu.app.model + +import jakarta.persistence.* + +@Entity +@Table(name = "user_stats") +data class UserStatsEntity( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0, + + @Column(nullable = false) + val totalCards: Int = 0, + + @Column(nullable = false) + val completedCollections: Int = 0, + + @OneToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "user_id", nullable = false, unique = true) + val user: UserEntity +) diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/AchievementRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/AchievementRepository.kt new file mode 100644 index 0000000..710ad88 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/AchievementRepository.kt @@ -0,0 +1,10 @@ +package ru.vsu.app.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import ru.vsu.app.model.AchievementEntity + +@Repository +interface AchievementRepository : JpaRepository { + fun findByIsUnlocked(isUnlocked: Boolean): List +} diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/CardRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/CardRepository.kt index 758f8bd..d0c67ad 100644 --- a/backend/src/main/kotlin/ru/vsu/app/repository/CardRepository.kt +++ b/backend/src/main/kotlin/ru/vsu/app/repository/CardRepository.kt @@ -2,12 +2,12 @@ package ru.vsu.app.repository import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository -import ru.vsu.app.model.Card -import ru.vsu.app.model.User +import ru.vsu.app.model.CardEntity +import ru.vsu.app.model.UserEntity @Repository -interface CardRepository : JpaRepository { - fun findAllByOwner(user: User): List - fun findAllByOwnerOrderByRarityDesc(user: User): List - fun findAllByOwnerOrderByCollectionAsc(user: User): List +interface CardRepository : JpaRepository { + fun findAllByOwner(user: UserEntity): List + fun findAllByOwnerOrderByRarityDesc(user: UserEntity): List + fun findAllByOwnerOrderByCollectionAsc(user: UserEntity): List } \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/CoinOfferRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/CoinOfferRepository.kt index 35927bb..216dc7f 100644 --- a/backend/src/main/kotlin/ru/vsu/app/repository/CoinOfferRepository.kt +++ b/backend/src/main/kotlin/ru/vsu/app/repository/CoinOfferRepository.kt @@ -2,7 +2,7 @@ package ru.vsu.app.repository import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository -import ru.vsu.app.model.CoinOffer +import ru.vsu.app.model.CoinOfferEntity @Repository -interface CoinOfferRepository : JpaRepository \ No newline at end of file +interface CoinOfferRepository : JpaRepository \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/CollectionRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/CollectionRepository.kt new file mode 100644 index 0000000..f97d881 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/CollectionRepository.kt @@ -0,0 +1,12 @@ +package ru.vsu.app.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +import ru.vsu.app.model.CollectionEntity +import ru.vsu.app.model.UserEntity + +@Repository +interface CollectionRepository : JpaRepository { + fun findByNameContainingIgnoreCase(name: String): List +} diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/InventoryRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/InventoryRepository.kt new file mode 100644 index 0000000..0fdd01d --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/InventoryRepository.kt @@ -0,0 +1,8 @@ +package ru.vsu.app.repository + +import org.springframework.data.jpa.repository.JpaRepository +import ru.vsu.app.model.CardEntity +import ru.vsu.app.model.UserEntity + +interface InventoryRepository : JpaRepository { +} diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/NewsRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/NewsRepository.kt new file mode 100644 index 0000000..79ca468 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/NewsRepository.kt @@ -0,0 +1,8 @@ +package ru.vsu.app.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import ru.vsu.app.model.NewsEntity + +@Repository +interface NewsRepository : JpaRepository diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/NotificationRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/NotificationRepository.kt new file mode 100644 index 0000000..9cc2810 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/NotificationRepository.kt @@ -0,0 +1,10 @@ +package ru.vsu.app.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import ru.vsu.app.model.NotificationEntity + +@Repository +interface NotificationRepository : JpaRepository { + fun findByUserUserId(userId: Int): List +} diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/PackRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/PackRepository.kt index e93b266..f810570 100644 --- a/backend/src/main/kotlin/ru/vsu/app/repository/PackRepository.kt +++ b/backend/src/main/kotlin/ru/vsu/app/repository/PackRepository.kt @@ -2,7 +2,7 @@ package ru.vsu.app.repository import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository -import ru.vsu.app.model.Pack +import ru.vsu.app.model.PackEntity @Repository -interface PackRepository : JpaRepository \ No newline at end of file +interface PackRepository : JpaRepository \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/QuestRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/QuestRepository.kt new file mode 100644 index 0000000..3554014 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/QuestRepository.kt @@ -0,0 +1,11 @@ +package ru.vsu.app.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import ru.vsu.app.model.QuestEntity + +@Repository +interface QuestRepository : JpaRepository { + fun findByIsCompleted(isCompleted: Boolean): List + fun findByIsClaimed(isClaimed: Boolean): List +} diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/ReportRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/ReportRepository.kt new file mode 100644 index 0000000..296e18f --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/ReportRepository.kt @@ -0,0 +1,10 @@ +package ru.vsu.app.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import ru.vsu.app.model.ReportEntity + +@Repository +interface ReportRepository : JpaRepository { + fun findByStatus(status: ReportEntity.Status): List +} diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/ThemeRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/ThemeRepository.kt new file mode 100644 index 0000000..c70a23b --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/ThemeRepository.kt @@ -0,0 +1,6 @@ +package ru.vsu.app.repository + +import org.springframework.data.jpa.repository.JpaRepository +import ru.vsu.app.model.ThemeEntity + +interface ThemeRepository : JpaRepository diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/TradeRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/TradeRepository.kt new file mode 100644 index 0000000..3044e62 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/TradeRepository.kt @@ -0,0 +1,12 @@ +package ru.vsu.app.repository + +import ru.vsu.app.model.TradeEntity +import ru.vsu.app.model.UserEntity +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface TradeRepository : JpaRepository { + fun findByOfferingUserOrReceivingUser(offeringUser: UserEntity, receivingUser: UserEntity): List + fun findByOfferingCardsNameContainingOrReceivingCardsNameContaining(offeringCardName: String, receivingCardName: String): List +} \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/UserRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/UserRepository.kt index 3c4b0e8..686ed83 100644 --- a/backend/src/main/kotlin/ru/vsu/app/repository/UserRepository.kt +++ b/backend/src/main/kotlin/ru/vsu/app/repository/UserRepository.kt @@ -2,15 +2,30 @@ package ru.vsu.app.repository import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository -import ru.vsu.app.model.User +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param +import ru.vsu.app.model.UserEntity import java.util.Optional @Repository -interface UserRepository : JpaRepository { - fun findByEmail(email: String): Optional - fun findByUsername(username: String): Optional - fun findByActivationToken(activationToken: String): Optional - fun findByPasswordResetToken(passwordResetToken: String): Optional +interface UserRepository : JpaRepository { + fun findByEmail(email: String): Optional + fun findByUsername(username: String): Optional + fun findByActivationToken(activationToken: String): Optional + fun findByPasswordResetToken(passwordResetToken: String): Optional fun existsByEmail(email: String): Boolean fun existsByUsername(username: String): Boolean + + @Query(""" + SELECT u FROM UserEntity u + LEFT JOIN FETCH u.inventoryCards + LEFT JOIN FETCH u.favoriteCards + LEFT JOIN FETCH u.onChange + LEFT JOIN FETCH u.achievements + LEFT JOIN FETCH u.favoriteAchievements + LEFT JOIN FETCH u.notifications + WHERE u.userId = :userId + """) + fun findByIdWithRelations(@Param("userId") userId: Int): UserEntity? + } \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/repository/UserStatsRepository.kt b/backend/src/main/kotlin/ru/vsu/app/repository/UserStatsRepository.kt new file mode 100644 index 0000000..3a97b2b --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/repository/UserStatsRepository.kt @@ -0,0 +1,11 @@ +package ru.vsu.app.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import ru.vsu.app.model.UserStatsEntity +import ru.vsu.app.model.UserEntity + +@Repository +interface UserStatsRepository : JpaRepository { + fun findByUser(user: UserEntity): UserStatsEntity? +} diff --git a/backend/src/main/kotlin/ru/vsu/app/security/CustomUserDetails.kt b/backend/src/main/kotlin/ru/vsu/app/security/CustomUserDetails.kt new file mode 100644 index 0000000..5f7d309 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/security/CustomUserDetails.kt @@ -0,0 +1,29 @@ +package ru.vsu.app.security + +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.userdetails.UserDetails +import ru.vsu.app.model.UserEntity + +class CustomUserDetails( + private val user: UserEntity +) : UserDetails { + + override fun getAuthorities(): Collection { + return listOf(SimpleGrantedAuthority("ROLE_${user.role.name}")) + } + + override fun getPassword(): String = user.passwordHash + + override fun getUsername(): String = user.email + + override fun isAccountNonExpired(): Boolean = true + + override fun isAccountNonLocked(): Boolean = true + + override fun isCredentialsNonExpired(): Boolean = true + + override fun isEnabled(): Boolean = user.isEnabled + + fun getUser(): UserEntity = user +} diff --git a/backend/src/main/kotlin/ru/vsu/app/security/CustomUserDetailsService.kt b/backend/src/main/kotlin/ru/vsu/app/security/CustomUserDetailsService.kt index 9e7feff..5a30cf0 100644 --- a/backend/src/main/kotlin/ru/vsu/app/security/CustomUserDetailsService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/security/CustomUserDetailsService.kt @@ -17,11 +17,6 @@ class CustomUserDetailsService( val user = userRepository.findByEmail(username) .orElseThrow { UsernameNotFoundException("Пользователь не найден: $username") } - return User.builder() - .username(user.email) - .password(user.passwordHash) - .authorities(SimpleGrantedAuthority("USER")) - .disabled(!user.isEnabled) - .build() + return CustomUserDetails(user) } } \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/security/JwtAuthenticationFilter.kt b/backend/src/main/kotlin/ru/vsu/app/security/JwtAuthenticationFilter.kt index f789811..a48198f 100644 --- a/backend/src/main/kotlin/ru/vsu/app/security/JwtAuthenticationFilter.kt +++ b/backend/src/main/kotlin/ru/vsu/app/security/JwtAuthenticationFilter.kt @@ -5,69 +5,61 @@ import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.web.authentication.WebAuthenticationDetailsSource import org.springframework.stereotype.Component import org.springframework.util.AntPathMatcher import org.springframework.web.filter.OncePerRequestFilter import ru.vsu.app.service.JwtService +import ru.vsu.app.config.PublicEndpointsConfig + +import ru.vsu.app.security.CustomUserDetailsService @Component class JwtAuthenticationFilter( private val jwtService: JwtService, - private val userDetailsService: UserDetailsService + private val userDetailsService: CustomUserDetailsService, + private val publicEndpointsConfig: PublicEndpointsConfig ) : OncePerRequestFilter() { private val pathMatcher = AntPathMatcher() - - private val publicUrls = listOf( - "/api/auth/register", - "/api/auth/login", - "/api/auth/forgot-password", - "/api/auth/activate", - "/v3/api-docs/**", - "/swagger-ui/**", - "/swagger-ui.html", - "/webjars/**" - ) - - override fun shouldNotFilter(request: HttpServletRequest): Boolean { - return publicUrls.any { pattern -> - pathMatcher.match(pattern, request.servletPath) - } - } override fun doFilterInternal( request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain ) { + val servletPath = request.servletPath + + // Public - не обрабатываем токен, просто идем дальше + if (publicEndpointsConfig.endpoints.any { pattern -> pathMatcher.match(pattern, servletPath) }) { + filterChain.doFilter(request, response) + return + } + val authHeader = request.getHeader("Authorization") - if (authHeader == null || !authHeader.startsWith("Bearer ")) { filterChain.doFilter(request, response) return } - + val jwt = authHeader.substring(7) val userEmail = jwtService.extractUsername(jwt) - + if (SecurityContextHolder.getContext().authentication == null) { val userDetails = userDetailsService.loadUserByUsername(userEmail) - + if (jwtService.isTokenValid(jwt, userDetails)) { val authToken = UsernamePasswordAuthenticationToken( userDetails, null, userDetails.authorities ) - + authToken.details = WebAuthenticationDetailsSource().buildDetails(request) - SecurityContextHolder.getContext().authentication = authToken } } - + filterChain.doFilter(request, response) } -} \ No newline at end of file +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/AchievementService.kt b/backend/src/main/kotlin/ru/vsu/app/service/AchievementService.kt new file mode 100644 index 0000000..b0350e1 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/AchievementService.kt @@ -0,0 +1,21 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.AchievementDto +import org.springframework.stereotype.Service + +@Service +class AchievementService { + fun deleteAchievement(achievementID: Int) { + // Implementation for deleting an achievement + } + + fun updateAchievement(achievementID: Int, achievement: AchievementDto): AchievementDto { + // Implementation for updating an achievement + return achievement + } + + fun createAchievement(achievement: AchievementDto): AchievementDto { + // Implementation for creating an achievement + return achievement + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/AdminService.kt b/backend/src/main/kotlin/ru/vsu/app/service/AdminService.kt new file mode 100644 index 0000000..36c36b7 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/AdminService.kt @@ -0,0 +1,12 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.responses.admin.AdminStatsGet200Response +import org.springframework.stereotype.Service + +@Service +class AdminService { + fun getSystemStats(): AdminStatsGet200Response { + // Implementation for getting system statistics + return AdminStatsGet200Response() + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/AuthService.kt b/backend/src/main/kotlin/ru/vsu/app/service/AuthService.kt new file mode 100644 index 0000000..88cb84e --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/AuthService.kt @@ -0,0 +1,309 @@ +package ru.vsu.app.service + +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Service +import org.springframework.security.core.context.SecurityContextHolder + +import ru.vsu.app.dto.requests.ResendVerificationCodeRequest + +import ru.vsu.app.dto.responses.auth.login.LoginUser200Response +import ru.vsu.app.dto.responses.auth.login.LoginUser400Response +import ru.vsu.app.dto.responses.auth.login.LoginUser401Response +import ru.vsu.app.dto.requests.LoginUserRequest + +import ru.vsu.app.dto.responses.auth.register.RegisterUser200Response +import ru.vsu.app.dto.responses.auth.register.RegisterUser400Response +import ru.vsu.app.dto.responses.auth.register.RegisterUser409Response +import ru.vsu.app.dto.requests.RegisterUserRequest + +import ru.vsu.app.dto.responses.auth.verifyemail.VerifyEmail400Response +import ru.vsu.app.dto.responses.auth.verifyemail.VerifyEmail410Response +import ru.vsu.app.dto.responses.auth.verifyemail.ResendVerificationCode200Response +import ru.vsu.app.dto.requests.VerifyEmailRequest + +import ru.vsu.app.dto.responses.auth.requestpassword.RequestPasswordReset400Response +import ru.vsu.app.dto.responses.auth.requestpassword.RequestPasswordReset404Response +import ru.vsu.app.dto.responses.auth.requestpassword.RequestPasswordReset200Response +import ru.vsu.app.dto.requests.RequestPasswordResetRequest + +import ru.vsu.app.dto.responses.auth.refreshtoken.AuthRefreshToken200Response +import ru.vsu.app.dto.responses.auth.refreshtoken.AuthRefreshToken401Response +import ru.vsu.app.dto.requests.AuthRefreshRequest + +import ru.vsu.app.dto.responses.auth.requestpassword.ResetPassword200Response +import ru.vsu.app.dto.responses.auth.requestpassword.ResetPassword400Response +import ru.vsu.app.dto.requests.ResetPasswordRequest + +import ru.vsu.app.dto.responses.common.InternalServerError + +import ru.vsu.app.dto.UserDto +import ru.vsu.app.dto.CardDto +import ru.vsu.app.dto.AchievementDto + +import ru.vsu.app.model.UserEntity + +import ru.vsu.app.repository.UserRepository + +import ru.vsu.app.service.JwtService +import ru.vsu.app.service.EmailService + +import ru.vsu.app.mapper.UserMapper + +import kotlin.random.Random + +import io.jsonwebtoken.JwtException +import io.jsonwebtoken.ExpiredJwtException + +import java.util.regex.Pattern + +@Service +class AuthService( + private val userRepository: UserRepository, + private val userMapper: UserMapper, + private val jwtService: JwtService, + private val emailService: EmailService, + private val passwordEncoder: PasswordEncoder +) { + + fun getCurrentUser(): UserDto? { + println(SecurityContextHolder.getContext()) + val authentication = SecurityContextHolder.getContext().authentication + + if (authentication == null || !authentication.isAuthenticated || authentication.principal == "anonymousUser") { + return null + } + + val email = authentication.name + + println(email) + + val user = userRepository.findByEmail(email).orElse(null)?: return null + + return userMapper.toDto(user) + } + + fun register(request: RegisterUserRequest): Any { + val (email, username, password) = request + + if (userRepository.existsByEmail(email)) { + return RegisterUser409Response("Пользователь с таким email уже существует") + } + + if (userRepository.existsByUsername(username)) { + return RegisterUser409Response("Пользователь с таким username уже существует") + } + + val activationCode = generateSixDigitCode() + + try { + emailService.sendActivationEmail(email, activationCode) + } catch (ex: Exception) { + ex.printStackTrace() + return RegisterUser400Response("Ошибка при отправке email. Проверьте корректность адреса") + } + + val newUser = UserEntity( + email = email, + username = username, + passwordHash = passwordEncoder.encode(password), + activationToken = activationCode + ) + + userRepository.save(newUser) + + return RegisterUser200Response( + message = "Пользователь успешно зарегистрирован. Проверьте email для получения кода активации.", + tempToken = jwtService.generateToken(email) + ) + } + + // Функция генерации 6-значного кода + private fun generateSixDigitCode(): String { + return (100000 + Random.nextInt(900000)).toString() + } + + fun verifyEmail(request: VerifyEmailRequest): Any { + val token = request.tempToken + + if (jwtService.isTokenExpired(token)) { + return VerifyEmail410Response("Срок действия токена истёк") + } + + val email = jwtService.extractUsername(token) + val user = userRepository.findByEmail(email).orElse(null) + ?: return VerifyEmail400Response("Пользователь не найден") + + if (user.isEnabled) { + return VerifyEmail400Response("Аккаунт уже подтвержден") + } + + val storedCode = user.activationToken + ?: return VerifyEmail410Response("Срок действия кода истёк или он уже использован") + + + if (storedCode != request.code) { + return VerifyEmail400Response("Неверный код") + } + + // всё валидно - активируем аккаунт + val updatedUser = user.copy( + isEnabled = true, + activationToken = null + ) + + userRepository.save(updatedUser) + + return userMapper.toDto(updatedUser) + } + + fun resendVerificationCode(request: ResendVerificationCodeRequest): Any { + val token = request.tempToken + + val email = jwtService.extractUsernameIgnoreExpiration(token) + ?: return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid token") + + val user = userRepository.findByEmail(email).orElse(null) + ?: return ResponseEntity.status(HttpStatus.BAD_REQUEST) + + if (user.isEnabled) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + } + + val newActivationCode = generateSixDigitCode() + + try { + emailService.sendActivationEmail(email, newActivationCode) + } catch (ex: Exception) { + ex.printStackTrace() + return InternalServerError(error = "Ошибка при отправке email. Проверьте корректность адреса") + } + + val updatedUser = user.copy( + activationToken = newActivationCode + ) + + userRepository.save(updatedUser) + + val newTempToken = jwtService.generateToken(email) + + return ResendVerificationCode200Response( + message = "Новый код подтверждения отправлен", + tempToken = newTempToken + ) + } + + fun loginUser(request: LoginUserRequest): Any { + val emailPattern = Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$") + if (!emailPattern.matcher(request.email).matches() || request.password.length < 6) { + return LoginUser400Response( + error = "Неверный формат email или пароль слишком короткий" + ) + } + + return try { + val userEntity = userRepository.findByEmail(request.email).orElse(null) + if (userEntity == null) { + return LoginUser401Response( + error = "Неверные учётные данные" + ) + } + + if (!userEntity.isEnabled) { + return LoginUser401Response( + error = "Аккаунт не активирован" + ) + } + + if (!passwordEncoder.matches(request.password, userEntity.passwordHash)) { + return LoginUser401Response( + error = "Неверные учётные данные" + ) + } + + userMapper.toDto(userEntity) + } catch (ex: Exception) { + InternalServerError( + error = "Ошибка при попытке входа: ${ex.message ?: "Неизвестная ошибка"}" + ) + } + } + + fun refreshSession( + request: AuthRefreshRequest + ): Any { + return try { + val isExpired = jwtService.isTokenExpired(request.accessToken) + if (isExpired) { + return AuthRefreshToken401Response(false, "Access token истёк") + } + val accessToken = request.accessToken + + val email = jwtService.extractUsername(accessToken) + + val user = userRepository.findByEmail(email).orElse(null) + ?: return AuthRefreshToken401Response(false, "Пользователь не найден") + + val newAccessToken = jwtService.generateToken(email) + + AuthRefreshToken200Response(accessToken = newAccessToken) + + } catch (ex: JwtException) { + AuthRefreshToken401Response(false, "Невалидный или просроченный access token") + } catch (ex: Exception) { + InternalServerError(error = "Ошибка при обновлении токена: " + ex.message ?: "Неизвестная ошибка") + } + } + + fun requestPasswordReset(request: RequestPasswordResetRequest): Any { + val emailPattern = Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$") + if (!emailPattern.matcher(request.email).matches()) { + return RequestPasswordReset400Response( + error = "Неверный формат email или пароль слишком короткий" + ) + } + + val user = userRepository.findByEmail(request.email) + ?: return RequestPasswordReset404Response("Пользователь с таким email не зарегистрирован") + + val code = generateSixDigitCode() + val resetToken = jwtService.generatePasswordResetToken(request.email, code) + + emailService.sendPasswordResetEmail(request.email, code) + + return RequestPasswordReset200Response( + resetToken = resetToken, + message = "Код подтверждения отправлен на email" + ) + } + + + fun resetPassword(request: ResetPasswordRequest): Any { + try { + val payload = jwtService.parseResetToken(request.resetToken) + + if (request.code != payload.code) { + return ResetPassword400Response("Неверный код подтверждения") + } + + val user = userRepository.findByEmail(payload.email).orElse(null) + ?: return ResetPassword400Response("Пользователь не найден") + + val updatedUser = user.copy(passwordHash = passwordEncoder.encode(request.newPassword)) + userRepository.save(updatedUser) + + return ResetPassword200Response("Пароль успешно изменен") + + } catch (ex: ExpiredJwtException) { + return ResetPassword400Response("Срок действия токена истёк") + } catch (ex: JwtException) { + return ResetPassword400Response("Невалидный токен") + } catch (ex: Exception) { + ex.printStackTrace() + return InternalServerError("Ошибка сброса пароля: ${ex.message}") + } + } + + +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/CardAdminService.kt b/backend/src/main/kotlin/ru/vsu/app/service/CardAdminService.kt new file mode 100644 index 0000000..15480b3 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/CardAdminService.kt @@ -0,0 +1,9 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.CardDto + +interface CardAdminService { + fun createCard(card: CardDto): CardDto + fun updateCard(cardID: Int, card: CardDto): CardDto + fun deleteCard(cardID: Int) +} \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/service/CardService.kt b/backend/src/main/kotlin/ru/vsu/app/service/CardService.kt index a45597b..bf534e2 100644 --- a/backend/src/main/kotlin/ru/vsu/app/service/CardService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/service/CardService.kt @@ -1,69 +1,21 @@ package ru.vsu.app.service +import ru.vsu.app.dto.CardDto import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional -import ru.vsu.app.dto.CardResponse -import ru.vsu.app.model.Card -import ru.vsu.app.model.User -import ru.vsu.app.repository.CardRepository -import ru.vsu.app.repository.UserRepository -import jakarta.persistence.EntityNotFoundException @Service -class CardService( - private val cardRepository: CardRepository, - private val userRepository: UserRepository -) { - fun getUserCards(user: User, sortBy: String = "rarity"): List { - val cards = when (sortBy) { - "rarity" -> cardRepository.findAllByOwnerOrderByRarityDesc(user) - "collection" -> cardRepository.findAllByOwnerOrderByCollectionAsc(user) - else -> cardRepository.findAllByOwner(user) - } - - return cards.map { card -> mapToCardResponse(card) } +class CardService { + fun deleteCard(cardID: Int) { + // Implementation for deleting a card } - fun getCardDetails(cardId: Long, user: User): CardResponse { - val card = cardRepository.findById(cardId) - .orElseThrow { EntityNotFoundException("Card not found with id: $cardId") } - - if (card.owner?.id != user.id) { - throw IllegalStateException("User does not own this card") - } - - return mapToCardResponse(card) + fun updateCard(cardID: Int, card: CardDto): CardDto { + // Implementation for updating a card + return card } - @Transactional - fun disassembleCard(cardId: Long, user: User): Int { - val card = cardRepository.findById(cardId) - .orElseThrow { EntityNotFoundException("Card not found with id: $cardId") } - - if (card.owner?.id != user.id) { - throw IllegalStateException("User does not own this card") - } - - // Add coins to user's balance - val updatedUser = user.copy(coins = user.coins + card.disassemblePrice) - userRepository.save(updatedUser) - - // Delete the card - cardRepository.delete(card) - - return card.disassemblePrice - } - - private fun mapToCardResponse(card: Card): CardResponse { - return CardResponse( - id = card.id, - name = card.name, - imageUrl = card.imageUrl, - rarity = card.rarity, - collection = card.collection, - description = card.description, - type = card.type, - disassemblePrice = card.disassemblePrice - ) + fun createCard(card: CardDto): CardDto { + // Implementation for creating a card + return card } -} \ No newline at end of file +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/CoinOfferService.kt b/backend/src/main/kotlin/ru/vsu/app/service/CoinOfferService.kt new file mode 100644 index 0000000..9d341a2 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/CoinOfferService.kt @@ -0,0 +1,26 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.CoinOfferDto +import org.springframework.stereotype.Service + +@Service +class CoinOfferService { + fun getAllCoinOffers(): List { + // Implementation for getting all coin offers + return emptyList() + } + + fun deleteCoinOffer(offerId: Int) { + // Implementation for deleting a coin offer + } + + fun updateCoinOffer(offerId: Int, coinOffer: CoinOfferDto): CoinOfferDto { + // Implementation for updating a coin offer + return coinOffer + } + + fun createCoinOffer(coinOffer: CoinOfferDto): CoinOfferDto { + // Implementation for creating a coin offer + return coinOffer + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/CollectionAdminService.kt b/backend/src/main/kotlin/ru/vsu/app/service/CollectionAdminService.kt new file mode 100644 index 0000000..a4122d3 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/CollectionAdminService.kt @@ -0,0 +1,9 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.CollectionDto + +interface CollectionAdminService { + fun createCollection(collection: CollectionDto): CollectionDto + fun updateCollection(collectionID: Int, collection: CollectionDto): CollectionDto + fun deleteCollection(collectionID: Int) +} \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/service/CollectionService.kt b/backend/src/main/kotlin/ru/vsu/app/service/CollectionService.kt new file mode 100644 index 0000000..08247ec --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/CollectionService.kt @@ -0,0 +1,21 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.CollectionDto +import org.springframework.stereotype.Service + +@Service +class CollectionService { + fun deleteCollection(collectionID: Int) { + // Implementation for deleting a collection + } + + fun updateCollection(collectionID: Int, collection: CollectionDto): CollectionDto { + // Implementation for updating a collection + return collection + } + + fun createCollection(collection: CollectionDto): CollectionDto { + // Implementation for creating a collection + return collection + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/EmailService.kt b/backend/src/main/kotlin/ru/vsu/app/service/EmailService.kt index f9700c3..095daa9 100644 --- a/backend/src/main/kotlin/ru/vsu/app/service/EmailService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/service/EmailService.kt @@ -3,13 +3,18 @@ package ru.vsu.app.service import org.springframework.mail.SimpleMailMessage import org.springframework.mail.javamail.JavaMailSender import org.springframework.stereotype.Service +import org.springframework.beans.factory.annotation.Value @Service -class EmailService(private val mailSender: JavaMailSender) { +class EmailService( + private val mailSender: JavaMailSender, + @Value("\${EMAIL_USERNAME}") + private val fromEmail: String) { fun sendActivationEmail(to: String, code: String) { val message = SimpleMailMessage() message.setTo(to) + message.setFrom(fromEmail) message.setSubject("Подтверждение регистрации") message.setText(""" Спасибо за регистрацию! @@ -26,6 +31,7 @@ class EmailService(private val mailSender: JavaMailSender) { fun sendPasswordResetEmail(to: String, code: String) { val message = SimpleMailMessage() message.setTo(to) + message.setFrom(fromEmail) message.setSubject("Сброс пароля") message.setText(""" Вы запросили сброс пароля. diff --git a/backend/src/main/kotlin/ru/vsu/app/service/InventoryService.kt b/backend/src/main/kotlin/ru/vsu/app/service/InventoryService.kt new file mode 100644 index 0000000..c5ab915 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/InventoryService.kt @@ -0,0 +1,84 @@ +package ru.vsu.app.service + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +import ru.vsu.app.dto.responses.inventory.InventoryGet200Response + +import ru.vsu.app.model.CardEntity +import ru.vsu.app.model.UserEntity + +import ru.vsu.app.repository.InventoryRepository +import ru.vsu.app.repository.CollectionRepository +import ru.vsu.app.repository.UserRepository + +import ru.vsu.app.mapper.CardMapper +import ru.vsu.app.mapper.CollectionMapper + +import jakarta.persistence.EntityNotFoundException + +@Service +class InventoryService( + private val inventoryRepository: InventoryRepository, + private val userRepository: UserRepository, + private val collectionMapper: CollectionMapper, + private val collectionRepository: CollectionRepository, + private val cardMapper: CardMapper +) { + + // fun getUserCards(user: UserEntity, sortBy: String = "rarity"): List { + // val cards = when (sortBy) { + // "rarity" -> InventoryRepository.findAllByOwnerOrderByRarityDesc(user) + // "collection" -> InventoryRepository.findAllByOwnerOrderByCollectionAsc(user) + // else -> InventoryRepository.findAllByOwner(user) + // } + + // return cards.map { card -> mapToCardResponse(card) } + // } + + // fun getCardDetails(cardId: Long, user: UserEntity): CardResponse { + // val card = InventoryRepository.findById(cardId) + // .orElseThrow { EntityNotFoundException("Card not found with id: $cardId") } + + // if (card.owner?.id != user.id) { + // throw IllegalStateException("User does not own this card") + // } + + // return mapToCardResponse(card) + // } + + // @Transactional + // fun disassembleCard(cardId: Long, user: UserEntity): Int { + // val card = InventoryRepository.findById(cardId) + // .orElseThrow { EntityNotFoundException("Card not found with id: $cardId") } + + // if (card.owner?.id != user.id) { + // throw IllegalStateException("User does not own this card") + // } + + // // Add coins to user's balance + // val updatedUser = user.copy(coins = user.coins + card.disassemblePrice) + // userRepository.save(updatedUser) + + // // Delete the card + // InventoryRepository.delete(card) + + // return card.disassemblePrice + // } + + + fun getInventoryData(user: UserEntity): InventoryGet200Response { + val allCards = user.inventoryCards + val favoriteCards = user.favoriteCards + val inventoryDtos = allCards.map { cardMapper.toDto(it) } + val completedColls = user.completedCollections + val collectionDtos = completedColls.map { collectionMapper.toDto(it) } + + return InventoryGet200Response( + inventory = inventoryDtos, + favoriteCards = favoriteCards.map { it.id }, + collections = collectionDtos, + balance = user.balance + ) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/service/JwtService.kt b/backend/src/main/kotlin/ru/vsu/app/service/JwtService.kt index 2d5ae9f..76384e4 100644 --- a/backend/src/main/kotlin/ru/vsu/app/service/JwtService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/service/JwtService.kt @@ -4,6 +4,7 @@ import io.jsonwebtoken.Claims import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.ExpiredJwtException import io.jsonwebtoken.security.Keys import org.springframework.beans.factory.annotation.Value import org.springframework.security.core.userdetails.UserDetails @@ -24,6 +25,16 @@ class JwtService { return extractClaim(token) { obj: Claims -> obj.subject } } + fun extractUsernameIgnoreExpiration(token: String): String? { + return try { + extractClaim(token) { it.subject } + } catch (ex: ExpiredJwtException) { + ex.claims.subject // достаём email из протухшего токена + } catch (ex: Exception) { + null + } + } + fun extractClaim(token: String, claimsResolver: (Claims) -> T): T { val claims = extractAllClaims(token) return claimsResolver(claims) @@ -49,7 +60,7 @@ class JwtService { return username == userDetails.username && !isTokenExpired(token) } - private fun isTokenExpired(token: String): Boolean { + fun isTokenExpired(token: String): Boolean { return extractExpiration(token).before(Date()) } @@ -66,8 +77,40 @@ class JwtService { .body } + fun generatePasswordResetToken(email: String, code: String): String { + val now = Date() + val expiration = Date(now.time + 24 * 60 * 60 * 1000) // 24 часа + + return Jwts.builder() + .setSubject(email) + .claim("code", code) + .setIssuedAt(now) + .setExpiration(expiration) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact() + } + + fun parseResetToken(token: String): ResetTokenPayload { + val claims = Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .body + + return ResetTokenPayload( + email = claims.subject, + code = claims["code"] as String + ) + } + + private fun getSigningKey(): Key { val keyBytes = Decoders.BASE64.decode(secretKey) return Keys.hmacShaKeyFor(keyBytes) } -} \ No newline at end of file +} + +data class ResetTokenPayload( + val email: String, + val code: String +) \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/service/NewsService.kt b/backend/src/main/kotlin/ru/vsu/app/service/NewsService.kt new file mode 100644 index 0000000..a326760 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/NewsService.kt @@ -0,0 +1,21 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.NewsDto +import org.springframework.stereotype.Service + +@Service +class NewsService { + fun deleteNews(newsID: Int) { + // Implementation for deleting news + } + + fun updateNews(newsID: Int, news: NewsDto): NewsDto { + // Implementation for updating news + return news + } + + fun createNews(news: NewsDto): NewsDto { + // Implementation for creating news + return news + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/PackService.kt b/backend/src/main/kotlin/ru/vsu/app/service/PackService.kt new file mode 100644 index 0000000..11381a5 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/PackService.kt @@ -0,0 +1,104 @@ +package ru.vsu.app.service + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import ru.vsu.app.dto.PackDto +import ru.vsu.app.dto.CardDto +import ru.vsu.app.model.PackEntity +import ru.vsu.app.model.CardEntity +import ru.vsu.app.repository.PackRepository +import jakarta.persistence.EntityNotFoundException + +@Service +class PackService( + private val packRepository: PackRepository +) { + @Transactional + fun createPack(pack: PackDto): PackDto { + val packEntity = PackEntity( + name = pack.name, + imageUrl = pack.imageURL, + price = pack.price, + cards = pack.cards.map { cardDto -> + CardEntity( + name = cardDto.name, + imageUrl = cardDto.imageURL, + rarity = CardEntity.Rarity.Обычная, // Default value + disassemblePrice = 0, // Default value + isGenerated = false // Default value + ) + }.toList() + ) + + val savedPack = packRepository.save(packEntity) + + return PackDto( + packID = savedPack.id.toInt(), + name = savedPack.name, + imageURL = savedPack.imageUrl, + cards = savedPack.cards.map { cardEntity -> + CardDto( + cardID = cardEntity.id, + name = cardEntity.name, + imageURL = cardEntity.imageUrl, + rarity = CardDto.Rarity.Обычная, + minPrice = cardEntity.disassemblePrice, + isGenerated = cardEntity.isGenerated, + description = cardEntity.description, + theme = null + ) + }.toList(), + price = savedPack.price + ) + } + + @Transactional + fun updatePack(packID: Int, pack: PackDto): PackDto { + val existingPack = packRepository.findById(packID.toLong()) + .orElseThrow { EntityNotFoundException("Pack not found with id: $packID") } + + val updatedPack = existingPack.copy( + name = pack.name, + imageUrl = pack.imageURL, + price = pack.price, + cards = pack.cards.map { cardDto -> + CardEntity( + name = cardDto.name, + imageUrl = cardDto.imageURL, + rarity = CardEntity.Rarity.Обычная, // Default value + disassemblePrice = 0, // Default value + isGenerated = false // Default value + ) + }.toList() + ) + + val savedPack = packRepository.save(updatedPack) + + return PackDto( + packID = savedPack.id.toInt(), + name = savedPack.name, + imageURL = savedPack.imageUrl, + cards = savedPack.cards.map { cardEntity -> + CardDto( + cardID = cardEntity.id, + name = cardEntity.name, + imageURL = cardEntity.imageUrl, + rarity = CardDto.Rarity.Обычная, + minPrice = cardEntity.disassemblePrice, + isGenerated = cardEntity.isGenerated, + description = cardEntity.description, + theme = null + ) + }.toList(), + price = savedPack.price + ) + } + + @Transactional + fun deletePack(packID: Int) { + if (!packRepository.existsById(packID.toLong())) { + throw EntityNotFoundException("Pack not found with id: $packID") + } + packRepository.deleteById(packID.toLong()) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/ReportService.kt b/backend/src/main/kotlin/ru/vsu/app/service/ReportService.kt new file mode 100644 index 0000000..713e1e6 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/ReportService.kt @@ -0,0 +1,45 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.ReportDto +import ru.vsu.app.dto.requests.AdminReportsReportIDPutRequest +import org.springframework.stereotype.Service +import ru.vsu.app.dto.UserDto +import java.time.LocalDateTime + +interface ReportService { + fun getAllReports(): List + fun getReportById(reportID: Int): ReportDto? + fun updateReportStatus(reportID: Int, request: AdminReportsReportIDPutRequest): ReportDto +} + +@Service +class ReportServiceImpl : ReportService { + override fun getAllReports(): List { + // Implementation for getting all reports + return emptyList() + } + + override fun getReportById(reportID: Int): ReportDto { + // Implementation for getting a report by ID + return ReportDto( + reportID = reportID, + reporter = UserDto(userID = 1, username = "Reporter", email = "reporter@example.com", inventoryCards = emptyList(), balance = 0, achievements = emptyList()), + reportedUser = UserDto(userID = 2, username = "ReportedUser", email = "reporteduser@example.com", inventoryCards = emptyList(), balance = 0, achievements = emptyList()), + reportDateTime = LocalDateTime.now(), + reason = "Reason", + status = ReportDto.Status.На_рассмотрении + ) + } + + override fun updateReportStatus(reportID: Int, request: AdminReportsReportIDPutRequest): ReportDto { + // Implementation for updating report status + return ReportDto( + reportID = reportID, + reporter = UserDto(userID = 1, username = "Reporter", email = "reporter@example.com", inventoryCards = emptyList(), balance = 0, achievements = emptyList()), + reportedUser = UserDto(userID = 2, username = "ReportedUser", email = "reporteduser@example.com", inventoryCards = emptyList(), balance = 0, achievements = emptyList()), + reportDateTime = LocalDateTime.now(), + reason = "Reason", + status = ReportDto.Status.Подтверждено + ) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/ShopService.kt b/backend/src/main/kotlin/ru/vsu/app/service/ShopService.kt index 7c37b5d..e461cae 100644 --- a/backend/src/main/kotlin/ru/vsu/app/service/ShopService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/service/ShopService.kt @@ -1,154 +1,156 @@ -package ru.vsu.app.service - -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional -import ru.vsu.app.dto.* -import ru.vsu.app.model.User -import ru.vsu.app.repository.PackRepository -import ru.vsu.app.repository.CoinOfferRepository -import ru.vsu.app.repository.UserRepository -import ru.vsu.app.repository.CardRepository -import jakarta.persistence.EntityNotFoundException - -@Service -class ShopService( - private val packRepository: PackRepository, - private val coinOfferRepository: CoinOfferRepository, - private val userRepository: UserRepository, - private val cardRepository: CardRepository -) { - fun getAllPacks(): List { - return packRepository.findAll().map { pack -> - PackResponse( - id = pack.id, - name = pack.name, - imageUrl = pack.imageUrl, - price = pack.price, - cards = pack.cards.map { card -> - CardResponse( - id = card.id, - name = card.name, - imageUrl = card.imageUrl, - rarity = card.rarity, - collection = card.collection, - description = card.description, - type = card.type, - disassemblePrice = card.disassemblePrice - ) - } - ) - } - } - - fun getPackDetails(packId: Long): PackResponse { - val pack = packRepository.findById(packId) - .orElseThrow { EntityNotFoundException("Pack not found with id: $packId") } - - return PackResponse( - id = pack.id, - name = pack.name, - imageUrl = pack.imageUrl, - price = pack.price, - cards = pack.cards.map { card -> - CardResponse( - id = card.id, - name = card.name, - imageUrl = card.imageUrl, - rarity = card.rarity, - collection = card.collection, - description = card.description, - type = card.type, - disassemblePrice = card.disassemblePrice - ) - } - ) - } - - @Transactional - fun buyPack(packId: Long, user: User): PurchasePackResponse { - val pack = packRepository.findById(packId) - .orElseThrow { EntityNotFoundException("Pack not found with id: $packId") } - - if (user.coins < pack.price) { - throw IllegalStateException("Insufficient funds") - } - - // Update user's balance - val updatedUser = user.copy(coins = user.coins - pack.price) - userRepository.save(updatedUser) - - // Add cards to user's inventory - val receivedCards = pack.cards.map { card -> - val userCard = card.copy(owner = updatedUser) - cardRepository.save(userCard) - } - - return PurchasePackResponse( - receivedCards = receivedCards.map { card -> - CardResponse( - id = card.id, - name = card.name, - imageUrl = card.imageUrl, - rarity = card.rarity, - collection = card.collection, - description = card.description, - type = card.type, - disassemblePrice = card.disassemblePrice - ) - }, - newBalance = updatedUser.coins - ) - } - - fun getAllCoinOffers(): List { - return coinOfferRepository.findAll().map { offer -> - CoinOfferResponse( - id = offer.id, - name = offer.name, - coinsAmount = offer.coinsAmount, - price = offer.price, - imageUrl = offer.imageUrl, - description = offer.description - ) - } - } - - fun getCoinOfferDetails(offerId: Long): CoinOfferResponse { - val offer = coinOfferRepository.findById(offerId) - .orElseThrow { EntityNotFoundException("Coin offer not found with id: $offerId") } - - return CoinOfferResponse( - id = offer.id, - name = offer.name, - coinsAmount = offer.coinsAmount, - price = offer.price, - imageUrl = offer.imageUrl, - description = offer.description - ) - } - - fun purchaseCoins(offerId: Long, redirectUrl: String): PurchaseCoinsResponse { - val offer = coinOfferRepository.findById(offerId) - .orElseThrow { EntityNotFoundException("Coin offer not found with id: $offerId") } - - // Here you would integrate with your payment gateway - // This is a simplified example - return PurchaseCoinsResponse( - paymentUrl = "$redirectUrl?offer_id=$offerId" - ) - } - - @Transactional - fun processPayment(request: PaymentCallbackRequest): Boolean { - if (request.status != "Успешно") { - return false - } - - val offer = coinOfferRepository.findById(request.offerId) - .orElseThrow { EntityNotFoundException("Coin offer not found with id: ${request.offerId}") } - - // Here you would validate the payment with your payment gateway - // and update the user's balance accordingly - return true - } -} \ No newline at end of file +// package ru.vsu.app.service + +// import org.springframework.stereotype.Service +// import org.springframework.transaction.annotation.Transactional +// import ru.vsu.app.dto.responses.* +// import ru.vsu.app.model.UserEntity +// import ru.vsu.app.model.CardEntity +// import ru.vsu.app.model.PackEntity +// import ru.vsu.app.model.CoinOfferEntity +// import ru.vsu.app.repository.PackRepository +// import ru.vsu.app.repository.CoinOfferRepository +// import ru.vsu.app.repository.UserRepository +// import ru.vsu.app.repository.InventoryRepository +// import jakarta.persistence.EntityNotFoundException + +// @Service +// class ShopService( +// private val packRepository: PackRepository, +// private val coinOfferRepository: CoinOfferRepository, +// private val userRepository: UserRepository, +// private val InventoryRepository: InventoryRepository +// ) { +// fun getAllPacks(): List { +// return packRepository.findAll().map { pack: PackEntity -> +// PackResponse( +// id = pack.id, +// name = pack.name, +// imageUrl = pack.imageUrl, +// price = pack.price, +// cards = pack.cards.map { card: CardEntity -> +// CardResponse( +// id = card.id, +// name = card.name, +// imageUrl = card.imageUrl, +// rarity = card.rarity, +// collection = card.collection, +// description = card.description, +// disassemblePrice = card.disassemblePrice +// ) +// } +// ) +// } +// } + +// fun getPackDetails(packId: Long): PackResponse { +// val pack = packRepository.findById(packId) +// .orElseThrow { EntityNotFoundException("Pack not found with id: $packId") } + +// return PackResponse( +// id = pack.id, +// name = pack.name, +// imageUrl = pack.imageUrl, +// price = pack.price, +// cards = pack.cards.map { card: CardEntity -> +// CardResponse( +// id = card.id, +// name = card.name, +// imageUrl = card.imageUrl, +// rarity = card.rarity, +// collection = card.collection, +// description = card.description, +// type = card.type, +// disassemblePrice = card.disassemblePrice +// ) +// } +// ) +// } + +// @Transactional +// fun buyPack(packId: Long, user: UserEntity): PurchasePackResponse { +// val pack = packRepository.findById(packId) +// .orElseThrow { EntityNotFoundException("Pack not found with id: $packId") } + +// if (user.coins < pack.price) { +// throw IllegalStateException("Insufficient funds") +// } + +// // Update user's balance +// val updatedUser = user.copy(coins = user.coins - pack.price) +// userRepository.save(updatedUser) + +// // Add cards to user's inventory +// val receivedCards = pack.cards.map { card: CardEntity -> +// val userCard = card.copy(owner = updatedUser) +// InventoryRepository.save(userCard) +// } + +// return PurchasePackResponse( +// receivedCards = receivedCards.map { card: CardEntity -> +// CardResponse( +// id = card.id, +// name = card.name, +// imageUrl = card.imageUrl, +// rarity = card.rarity, +// collection = card.collection, +// description = card.description, +// type = card.type, +// disassemblePrice = card.disassemblePrice +// ) +// }, +// newBalance = updatedUser.coins +// ) +// } + +// fun getAllCoinOffers(): List { +// return coinOfferRepository.findAll().map { offer: CoinOfferEntity -> +// CoinOfferResponse( +// id = offer.id, +// name = offer.name, +// coinsAmount = offer.coinsAmount, +// price = offer.price, +// imageUrl = offer.imageUrl, +// description = offer.description +// ) +// } +// } + +// fun getCoinOfferDetails(offerId: Long): CoinOfferResponse { +// val offer = coinOfferRepository.findById(offerId) +// .orElseThrow { EntityNotFoundException("Coin offer not found with id: $offerId") } + +// return CoinOfferResponse( +// id = offer.id, +// name = offer.name, +// coinsAmount = offer.coinsAmount, +// price = offer.price, +// imageUrl = offer.imageUrl, +// description = offer.description +// ) +// } + +// fun purchaseCoins(offerId: Long, redirectUrl: String): PurchaseCoinsResponse { +// coinOfferRepository.findById(offerId) +// .orElseThrow { EntityNotFoundException("Coin offer not found with id: $offerId") } + +// // Here you would integrate with your payment gateway +// // This is a simplified example +// return PurchaseCoinsResponse( +// paymentUrl = "$redirectUrl?offer_id=$offerId" +// ) +// } + +// @Transactional +// fun processPayment(request: PaymentCallbackRequest): Boolean { +// if (request.status != "Успешно") { +// return false +// } + +// coinOfferRepository.findById(request.offerId) +// .orElseThrow { EntityNotFoundException("Coin offer not found with id: ${request.offerId}") } + +// // Here you would validate the payment with your payment gateway +// // and update the user's balance accordingly +// return true +// } +// } \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/service/ThemeService.kt b/backend/src/main/kotlin/ru/vsu/app/service/ThemeService.kt new file mode 100644 index 0000000..0a3b402 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/ThemeService.kt @@ -0,0 +1,35 @@ +package ru.vsu.app.service + +import org.springframework.stereotype.Service +import ru.vsu.app.model.ThemeEntity +import ru.vsu.app.repository.ThemeRepository +import ru.vsu.app.dto.ThemeDto + +@Service +class ThemeService(private val themeRepository: ThemeRepository) { + + fun getAllThemes(): List { + return themeRepository.findAll() + } + + fun getThemeById(id: Long): ThemeEntity? { + return themeRepository.findById(id).orElse(null) + } + + fun createTheme(theme: ThemeEntity): ThemeEntity { + return try { + themeRepository.save(theme) + } catch (e: Exception) { + throw RuntimeException("Failed to create theme", e) + } + } + + fun updateTheme(id: Long, theme: ThemeEntity): ThemeEntity { + val existingTheme = themeRepository.findById(id).orElseThrow { Exception("Theme not found") } + return themeRepository.save(existingTheme) + } + + fun deleteTheme(id: Long) { + themeRepository.deleteById(id) + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/TradeAdminService.kt b/backend/src/main/kotlin/ru/vsu/app/service/TradeAdminService.kt new file mode 100644 index 0000000..0a6f5a0 --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/TradeAdminService.kt @@ -0,0 +1,10 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.TradeDto +import ru.vsu.app.dto.requests.AdminTradesTradeIDInvalidatePostRequest +import ru.vsu.app.dto.responses.admin.AdminTradesTradeIDInvalidatePost200Response + +interface TradeAdminService { + fun getAllTrades(): List + fun invalidateTrade(tradeID: Int, request: AdminTradesTradeIDInvalidatePostRequest): AdminTradesTradeIDInvalidatePost200Response +} \ No newline at end of file diff --git a/backend/src/main/kotlin/ru/vsu/app/service/TradeService.kt b/backend/src/main/kotlin/ru/vsu/app/service/TradeService.kt new file mode 100644 index 0000000..47a59fd --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/service/TradeService.kt @@ -0,0 +1,151 @@ +package ru.vsu.app.service + +import ru.vsu.app.dto.TradeDto +import ru.vsu.app.dto.requests.AdminTradesTradeIDInvalidatePostRequest +import ru.vsu.app.dto.responses.admin.AdminTradesTradeIDInvalidatePost200Response +import ru.vsu.app.dto.requests.TradesInitiatePostRequest +import ru.vsu.app.model.TradeEntity +import ru.vsu.app.model.UserEntity +import ru.vsu.app.model.CardEntity +import ru.vsu.app.repository.TradeRepository +import ru.vsu.app.repository.UserRepository +import ru.vsu.app.repository.CardRepository +import ru.vsu.app.mapper.TradeMapper +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.http.HttpStatus +import org.springframework.web.server.ResponseStatusException +import java.time.LocalDateTime + +@Service +class TradeService( + private val tradeRepository: TradeRepository, + private val userRepository: UserRepository, + private val cardRepository: CardRepository, + private val tradeMapper: TradeMapper +) { + fun getAllTrades(search: String?): List { + val trades = if (search != null) { + tradeRepository.findByOfferingCardsNameContainingOrReceivingCardsNameContaining(search, search) + } else { + tradeRepository.findAll() + } + return trades.map { tradeMapper.toDto(it) } + } + + fun getTradeById(tradeId: Int): TradeDto { + val trade = tradeRepository.findById(tradeId) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Trade not found") } + return tradeMapper.toDto(trade) + } + + fun getUserTrades(userId: Int): List { + val user = userRepository.findById(userId) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "User not found") } + val trades = tradeRepository.findByOfferingUserOrReceivingUser(user, user) + return trades.map { tradeMapper.toDto(it) } + } + + @Transactional + fun initiateTrade(request: TradesInitiatePostRequest, currentUserId: Int): TradeDto { + val offeringUser = userRepository.findById(currentUserId) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Offering user not found") } + + val offeredCard = cardRepository.findById(request.offeredCardId) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Offered card not found") } + + if (!offeringUser.inventoryCards.contains(offeredCard)) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "User doesn't own the offered card") + } + + val requestedCards = request.requestedCardId.map { cardId -> + cardRepository.findById(cardId) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Requested card not found") } + } + + val trade = TradeEntity( + offeringUser = offeringUser, + offeringCards = listOf(offeredCard), + receivingUser = requestedCards.first().owner ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Requested card has no owner"), + receivingCards = requestedCards, + tradeDateTime = LocalDateTime.now() + ) + + val savedTrade = tradeRepository.save(trade) + return tradeMapper.toDto(savedTrade) + } + + @Transactional + fun acceptTrade(tradeId: Int, currentUserId: Int): TradeDto { + val trade = tradeRepository.findById(tradeId) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Trade not found") } + + if (trade.receivingUser.userId != currentUserId) { + throw ResponseStatusException(HttpStatus.FORBIDDEN, "User is not authorized to accept this trade") + } + + if (trade.isConfirmed) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Trade is already confirmed") + } + + // Perform the card exchange + val offeringUser = trade.offeringUser + val receivingUser = trade.receivingUser + + offeringUser.inventoryCards = offeringUser.inventoryCards + trade.receivingCards + receivingUser.inventoryCards = receivingUser.inventoryCards + trade.offeringCards + offeringUser.inventoryCards = offeringUser.inventoryCards - trade.offeringCards + receivingUser.inventoryCards = receivingUser.inventoryCards - trade.receivingCards + + val updatedTrade = trade.copy(isConfirmed = true) + val savedTrade = tradeRepository.save(updatedTrade) + return tradeMapper.toDto(savedTrade) + } + + @Transactional + fun rejectTrade(tradeId: Int, currentUserId: Int): TradeDto { + val trade = tradeRepository.findById(tradeId) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Trade not found") } + + if (trade.receivingUser.userId != currentUserId) { + throw ResponseStatusException(HttpStatus.FORBIDDEN, "User is not authorized to reject this trade") + } + + val updatedTrade = trade.copy(isConfirmed = false) + val savedTrade = tradeRepository.save(updatedTrade) + return tradeMapper.toDto(savedTrade) + } + + @Transactional + fun cancelTrade(tradeId: Int, currentUserId: Int): TradeDto { + val trade = tradeRepository.findById(tradeId) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Trade not found") } + + if (trade.offeringUser.userId != currentUserId) { + throw ResponseStatusException(HttpStatus.FORBIDDEN, "User is not authorized to cancel this trade") + } + + tradeRepository.delete(trade) + return tradeMapper.toDto(trade) + } + + @Transactional + fun invalidateTrade(tradeId: Int, request: AdminTradesTradeIDInvalidatePostRequest): AdminTradesTradeIDInvalidatePost200Response { + val trade = tradeRepository.findById(tradeId) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Trade not found") } + + if (trade.isConfirmed) { + // Reverse the trade + val offeringUser = trade.offeringUser + val receivingUser = trade.receivingUser + + offeringUser.inventoryCards = offeringUser.inventoryCards + trade.offeringCards + receivingUser.inventoryCards = receivingUser.inventoryCards + trade.receivingCards + offeringUser.inventoryCards = offeringUser.inventoryCards - trade.receivingCards + receivingUser.inventoryCards = receivingUser.inventoryCards - trade.offeringCards + } + + tradeRepository.delete(trade) + return AdminTradesTradeIDInvalidatePost200Response() + } +} diff --git a/backend/src/main/kotlin/ru/vsu/app/service/UserService.kt b/backend/src/main/kotlin/ru/vsu/app/service/UserService.kt index 92d4c35..5f4c4ac 100644 --- a/backend/src/main/kotlin/ru/vsu/app/service/UserService.kt +++ b/backend/src/main/kotlin/ru/vsu/app/service/UserService.kt @@ -1,184 +1,38 @@ package ru.vsu.app.service -import org.springframework.security.crypto.password.PasswordEncoder +import ru.vsu.app.dto.requests.AdminUsersUserIDBanPostRequest +import ru.vsu.app.dto.requests.AdminUsersUserIDQuestsQuestIDResetPostRequest +import ru.vsu.app.dto.responses.admin.AdminUsersUserIDBanPost200Response +import ru.vsu.app.dto.responses.admin.AdminUsersUserIDQuestsQuestIDResetPost200Response +import ru.vsu.app.dto.responses.admin.AdminUsersUserIDUnbanPost200Response import org.springframework.stereotype.Service -import ru.vsu.app.dto.* -import ru.vsu.app.model.User -import ru.vsu.app.repository.UserRepository -import kotlin.random.Random @Service -class UserService( - private val userRepository: UserRepository, - private val passwordEncoder: PasswordEncoder, - private val emailService: EmailService, - private val jwtService: JwtService -) { - - fun register(request: RegisterRequest): RegisterResponse { - // Проверяем, что пользователь с таким email или username еще не существует - if (userRepository.existsByEmail(request.email)) { - return RegisterResponse("Пользователь с таким email уже существует", false) - } - - if (userRepository.existsByUsername(request.username)) { - return RegisterResponse("Пользователь с таким username уже существует", false) - } - - // Генерируем 6-значный код активации - val activationCode = generateSixDigitCode() - - // Создаем пользователя - val user = User( - email = request.email, - username = request.username, - passwordHash = passwordEncoder.encode(request.password), - isEnabled = false, - activationToken = activationCode - ) - - userRepository.save(user) - - // Отправляем email с кодом подтверждения - emailService.sendActivationEmail(user.email, activationCode) - - return RegisterResponse("Пользователь успешно зарегистрирован. Проверьте email для получения кода активации.", true) - } - - fun activateAccount(request: ActivateAccountRequest): ApiResponse { - val userOptional = userRepository.findByEmail(request.email) - - if (userOptional.isEmpty) { - return ApiResponse("Пользователь с таким email не найден", false) - } - - val user = userOptional.get() - - // Если аккаунт уже активирован - if (user.isEnabled) { - return ApiResponse("Аккаунт уже активирован", true) - } - - // Проверяем код активации - if (user.activationToken != request.code) { - return ApiResponse("Неверный код активации", false) - } - - // Обновляем пользователя - val updatedUser = user.copy( - isEnabled = true, - activationToken = null - ) - - userRepository.save(updatedUser) - - return ApiResponse("Аккаунт успешно активирован", true) +class UserService { + fun revokeAchievement(userID: Int, achievementID: Int) { + // Implementation for revoking an achievement from a user } - - fun login(request: LoginRequest): LoginResponse? { - val userOptional = userRepository.findByEmail(request.email) - - if (userOptional.isEmpty) { - return null - } - - val user = userOptional.get() - - // Проверяем, активирован ли аккаунт - if (!user.isEnabled) { - return null - } - - if (!passwordEncoder.matches(request.password, user.passwordHash)) { - return null - } - - val token = jwtService.generateToken(user.email) - - return LoginResponse( - token = token, - username = user.username, - email = user.email - ) + + fun banUser(userID: Int, request: AdminUsersUserIDBanPostRequest): AdminUsersUserIDBanPost200Response { + // Implementation for banning a user + return AdminUsersUserIDBanPost200Response() } - - fun getUserInfo(email: String): UserInfoResponse { - val user = userRepository.findByEmail(email).orElseThrow { - IllegalArgumentException("Пользователь не найден") - } - - return UserInfoResponse( - id = user.id, - username = user.username, - email = user.email - ) + + fun deleteUser(userID: Int) { + // Implementation for deleting a user } - - fun forgotPassword(request: ForgotPasswordRequest): ApiResponse { - val userOptional = userRepository.findByEmail(request.email) - - if (userOptional.isEmpty) { - return ApiResponse("Пользователь с таким email не найден", false) - } - - val user = userOptional.get() - - // Проверяем, активирован ли аккаунт - if (!user.isEnabled) { - return ApiResponse("Аккаунт не активирован", false) - } - - // Генерируем 6-значный код для сброса пароля - val resetCode = generateSixDigitCode() - - // Код будет действителен в течение 15 минут - val expiryTime = System.currentTimeMillis() + (15 * 60 * 1000) // 15 минут - - // Обновляем пользователя с кодом сброса пароля - val updatedUser = user.copy( - passwordResetToken = resetCode, - passwordResetTokenExpiry = expiryTime - ) - - userRepository.save(updatedUser) - - // Отправляем код сброса пароля по email - emailService.sendPasswordResetEmail(user.email, resetCode) - - return ApiResponse("Код для сброса пароля отправлен на ваш email", true) + + fun deleteCardFromInventory(userID: Int, cardID: Int) { + // Implementation for deleting a card from a user's inventory } - - fun resetPassword(request: ResetPasswordRequest): ApiResponse { - val userOptional = userRepository.findByPasswordResetToken(request.token) - - if (userOptional.isEmpty) { - return ApiResponse("Неверный код сброса пароля", false) - } - - val user = userOptional.get() - - // Сохраняем значение токена в локальную переменную - val tokenExpiry = user.passwordResetTokenExpiry - - // Проверяем срок действия кода - if (tokenExpiry == null || System.currentTimeMillis() > tokenExpiry) { - return ApiResponse("Срок действия кода истек", false) - } - - // Обновляем пользователя с новым паролем - val updatedUser = user.copy( - passwordHash = passwordEncoder.encode(request.newPassword), - passwordResetToken = null, - passwordResetTokenExpiry = null - ) - - userRepository.save(updatedUser) - - return ApiResponse("Пароль успешно изменен", true) + + fun resetQuest(userID: Int, questID: Int, request: AdminUsersUserIDQuestsQuestIDResetPostRequest): AdminUsersUserIDQuestsQuestIDResetPost200Response { + // Implementation for resetting a quest for a user + return AdminUsersUserIDQuestsQuestIDResetPost200Response() } - - // Функция генерации 6-значного кода - private fun generateSixDigitCode(): String { - return (100000 + Random.nextInt(900000)).toString() + + fun unbanUser(userID: Int): AdminUsersUserIDUnbanPost200Response { + // Implementation for unbanning a user + return AdminUsersUserIDUnbanPost200Response() } -} \ No newline at end of file +} diff --git a/backend/src/main/kotlin/ru/vsu/app/util/OptionalExtensions.kt b/backend/src/main/kotlin/ru/vsu/app/util/OptionalExtensions.kt new file mode 100644 index 0000000..d84d61f --- /dev/null +++ b/backend/src/main/kotlin/ru/vsu/app/util/OptionalExtensions.kt @@ -0,0 +1,5 @@ +package ru.vsu.app.util + +import java.util.Optional + +fun Optional.toNullable(): T? = this.orElse(null) diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 8f3f700..4ac81a6 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1,30 +1,40 @@ spring.application.name=app -spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:auth_db} -spring.datasource.username=${DB_USER:appuser} -spring.datasource.password=${DB_PASSWORD:password} -spring.datasource.driver-class-name=org.postgresql.Driver +# DB config +spring.datasource.url=${DB_URL} +spring.datasource.username=${DB_USERNAME} +spring.datasource.password=${DB_PASSWORD} +spring.datasource.driver-class-name=${DB_DRIVER} -spring.jpa.hibernate.ddl-auto=update -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.show-sql=true +# JPA config +spring.jpa.hibernate.ddl-auto=${JPA_DDL_AUTO} +spring.jpa.properties.hibernate.dialect=${JPA_DIALECT} +spring.jpa.show-sql=${JPA_SHOW_SQL} -application.security.jwt.secret-key=404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970 -application.security.jwt.expiration=86400000 +# Micrometr config +management.endpoints.web.exposure.include=${MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE} +management.metrics.export.prometheus.enabled=${METRICS_PROMETHEUS_ENABLED} -server.port=8080 +# JWT config +application.security.jwt.secret-key=${JWT_SECRET} +application.security.jwt.expiration=${JWT_EXPIRATION} -logging.level.org.springframework.security=DEBUG +# Server config +server.port=${SERVER_PORT} -spring.mail.host=smtp.gmail.com -spring.mail.port=587 -spring.mail.username=your-email@gmail.com -spring.mail.password=your-email-password -spring.mail.properties.mail.smtp.auth=true -spring.mail.properties.mail.smtp.starttls.enable=true +# Logging config +logging.level.org.springframework.security=${SPRING_SECURITY_LOG_LEVEL} + +# Email config +spring.mail.host=${EMAIL_HOST} +spring.mail.port=${EMAIL_PORT} +spring.mail.username=${EMAIL_USERNAME} +spring.mail.password=${EMAIL_PASSWORD} +spring.mail.properties.mail.smtp.auth=${EMAIL_SMTP_AUTH} +spring.mail.properties.mail.smtp.starttls.enable=${EMAIL_SMTP_STARTTLS_ENABLE} # Настройки Swagger OpenAPI -springdoc.swagger-ui.path=/swagger-ui.html -springdoc.api-docs.path=/v3/api-docs -springdoc.swagger-ui.operationsSorter=method -springdoc.swagger-ui.tagsSorter=alpha +springdoc.swagger-ui.path=${SWAGGER_UI_PATH} +springdoc.api-docs.path=${API_DOCS_PATH} +springdoc.swagger-ui.operationsSorter=${SWAGGER_OPERATIONS_SORTER} +springdoc.swagger-ui.tagsSorter=${SWAGGER_TAGS_SORTER} diff --git a/backend/src/main/resources/openapi.yaml b/backend/src/main/resources/openapi.yaml new file mode 100644 index 0000000..b13bc30 --- /dev/null +++ b/backend/src/main/resources/openapi.yaml @@ -0,0 +1,3573 @@ +openapi: 3.1.0 +info: + title: Cadrly API + version: 0.1.0 + description: Приложение для обмена коллекционными карточками + +servers: + - description: Test server + url: http://localhost:8080/api + +tags: + - name: Authentication + description: Регистрация, вход и управление аккаунтом + - name: Profile + description: Операции в профиле пользователя + - name: OtherProfile + description: Операции при просмотре профиля другого пользователя + - name: Inventory + description: Операции на странице "Инвентарь" + - name: Home + description: Операции на странице "Главное меню" + - name: Trades + description: Операции на странице "Обменник" + - name: Shop + description: Операции на странице "Магазин" + - name: Admin + description: Функции администратора + +paths: + /auth/register: + post: + tags: [Authentication] + summary: Регистрация нового пользователя + description: | + Принимает email, имя пользователя и пароль. + Если email и пароль валидны, отправляет код подтверждения. + Возвращает временный токен для верификации. + operationId: registerUser + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + format: email + example: 'user@example.com' + description: Email пользователя + username: + type: string + minLength: 3 + maxLength: 25 + example: 'Коллекционер_123' + description: 'Имя пользователя (3-25 символов)' + password: + type: string + format: password + minLength: 8 + example: 'Password123!' + description: 'Пароль (минимум 8 символов)' + required: [email, username, password] + responses: + 200: + description: Код подтверждения отправлен + content: + application/json: + schema: + type: object + properties: + tempToken: + type: string + example: 'temp_abc123' + message: + type: string + example: 'Код подтверждения отправлен на email' + 400: + description: Неверный формат данных + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Некорректный формат введенных данных' + 409: + description: Пользователь уже существует + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Пользователь с таким email уже существует' + 500: + $ref: '#/components/responses/ServerError' + + /auth/verify: + post: + tags: [Authentication] + summary: Подтверждение email + description: | + Проверяет код подтверждения. + Если код подтверждения верный - создает аккаунт и возвращает данные пользователя. + operationId: verifyEmail + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + tempToken: + type: string + example: 'temp_abc123' + description: 'Временный токен из ответа регистрации' + code: + type: string + pattern: "^\\d{6}$" + example: '123456' + description: '6-значный код подтверждения' + required: [tempToken, code] + responses: + 200: + description: Успешная регистрация + headers: + Set-Cookie: + schema: + type: string + example: 'session_id=abcde12345; HttpOnly; Path=/; Secure' + content: + application/json: + schema: + $ref: '#/components/schemas/User' + 400: + description: Неверный код или токен + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Неверный код подтверждения' + canRetry: + type: boolean + example: true + 410: + description: Истек срок действия кода + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Код устарел, запросите новый' + 500: + $ref: '#/components/responses/ServerError' + + /auth/resend-code: + post: + tags: [Authentication] + summary: Повторная отправка кода + description: Отправляет новый код подтверждения + operationId: resendVerificationCode + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + tempToken: + type: string + example: 'temp_abc123' + required: [tempToken] + responses: + 200: + description: Новый код отправлен + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Новый код подтверждения отправлен' + 400: + $ref: '#/components/responses/BadRequest' + 500: + $ref: '#/components/responses/ServerError' + + /auth/login: + post: + tags: [Authentication] + summary: Вход в систему + description: | + Аутентификация пользователя по email и паролю. + Если данные введены верно - устанавливает сессионную cookie. + operationId: loginUser + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + format: email + example: 'user@example.com' + password: + type: string + format: password + example: 'SecurePass123!' + required: [email, password] + responses: + 200: + description: Успешный вход + headers: + Set-Cookie: + schema: + type: string + example: 'session_id=abcde12345; HttpOnly; Path=/; Secure' + content: + application/json: + schema: + $ref: '#/components/schemas/User' + 400: + description: Неверный формат email + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Неверный фортмат email' + + 401: + description: Неверные учетные данные + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Неверный email или пароль' + 500: + $ref: '#/components/responses/ServerError' + + /auth/forgot-password: + post: + tags: [Authentication] + summary: Запрос кода для сброса пароля + description: Отправляет код подтверждения для сброса пароля, если email существует в системе + operationId: requestPasswordReset + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + format: email + example: 'user@example.com' + required: [email] + responses: + 200: + description: Код подтверждения отправлен на email + content: + application/json: + schema: + type: object + properties: + resetToken: + type: string + example: 'reset_xyz789' + message: + type: string + example: 'Код подтверждения отправлен на email' + 400: + description: Неверный формат данных + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Некорректный формат emaila' + 404: + description: Пользователь с указанным email не найден + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Пользователь с таким email не зарегистрирован' + 500: + $ref: '#/components/responses/ServerError' + + /auth/reset-password: + post: + tags: [Authentication] + summary: Сброс пароля + description: Устанавливает новый пароль после подтверждения кода + operationId: resetPassword + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + resetToken: + type: string + example: 'reset_xyz789' + code: + type: string + pattern: "^\\d{6}$" + example: '654321' + newPassword: + type: string + format: password + minLength: 8 + example: 'NewSecurePass123!' + required: [resetToken, code, newPassword] + responses: + 200: + description: Пароль изменен + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Пароль успешно изменен' + 400: + description: Неверный код или токен + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Неверный код подтверждения' + 500: + $ref: '#/components/responses/ServerError' + + /auth/check: + get: + tags: [Authentication] + summary: Проверка статуса аутентификации + description: | + Проверяет, авторизован ли пользователь по сессионной cookie. + Возвращает данные пользователя, если сессия активна. + security: + - bearerAuth: [] + responses: + 200: + description: Пользователь авторизован + content: + application/json: + schema: + $ref: '#/components/schemas/User' + 401: + description: Пользователь не авторизован + content: + application/json: + schema: + type: object + properties: + isAuthenticated: + type: boolean + example: false + message: + type: string + example: 'Требуется авторизация' + 500: + $ref: '#/components/responses/ServerError' + + /auth/refresh: + post: + tags: [Authentication] + summary: Обновление сессии + description: | + Обновляет сессионную cookie. + Требуется валидная существующая сессия. + security: + - bearerAuth: [] + responses: + 200: + description: Сессия успешно обновлена + headers: + Set-Cookie: + schema: + type: string + example: 'session_id=new_abcde12345; HttpOnly; Path=/; Secure' + content: + application/json: + schema: + $ref: '#/components/schemas/User' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + + /auth/logout: + post: + tags: [Authentication] + summary: Выход из системы + description: | + Завершает текущую сессию пользователя. + Удаляет сессионную cookie. + security: + - bearerAuth: [] + responses: + 204: + description: Успешный выход + headers: + Set-Cookie: + schema: + type: string + example: 'session_id=; HttpOnly; Path=/; Secure; Expires=Thu, 01 Jan 1970 00:00:00 GMT' + 500: + $ref: '#/components/responses/ServerError' + + /profile: + get: + tags: [Profile] + summary: Получение данных профиля + description: | + Загружает данные профиля пользователя. + - Проверяет авторизацию + - Возвращает статистику, избранные карты и достижения + security: + - bearerAuth: [] + responses: + 200: + description: Данные профиля + content: + application/json: + schema: + type: object + properties: + userStats: + $ref: '#/components/schemas/UserStats' + favoriteCards: + type: array + items: + $ref: '#/components/schemas/Card' + maxItems: 5 + favoriteAchievements: + type: array + items: + $ref: '#/components/schemas/Achievement' + maxItems: 4 + user_id: + type: integer + example: 123 + description: 'ID пользователя' + username: + type: string + example: 'Коллекционер_123' + description: 'Имя пользователя' + avatar_url: + type: string + format: url + example: 'https://example.com/avatars/user123.jpg' + description: 'Ссылка на аватар пользователя' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + + /profile/achievements: + get: + tags: [Profile] + summary: Получение списка достижений + description: | + Возвращает полный список достижений с текущим статусом выполнения + security: + - bearerAuth: [] + responses: + 200: + description: Список достижений + content: + application/json: + schema: + type: object + properties: + achievements: + type: array + items: + $ref: '#/components/schemas/Achievement' + 500: + $ref: '#/components/responses/ServerError' + + /profile/achievements/favorites/count: + get: + tags: [Profile] + summary: Проверка количества избранных достижений + description: | + Возвращает текущее количество избранных достижений + security: + - bearerAuth: [] + responses: + 200: + description: Количество избранных + content: + application/json: + schema: + type: object + properties: + count: + type: integer + maximum: 4 + example: 3 + canAddMore: + type: boolean + example: true + 500: + $ref: '#/components/responses/ServerError' + + /profile/achievements/{achievement_ID}/favorite-add: + post: + tags: [Profile] + summary: Добавить достижение в избранное + description: | + Добавляет достижение в избранное (максимум 4) + security: + - bearerAuth: [] + parameters: + - in: path + name: achievement_ID + required: true + schema: + type: integer + responses: + 200: + description: Успешно добавлено в избранное + 400: + description: Невозможно добавить в избранное (лимит достигнут) + 500: + $ref: '#/components/responses/ServerError' + + /profile/achievements/{achievement_ID}/favorite-delete: + delete: + tags: [Profile] + summary: Удалить достижение из избранного + description: | + Удаляет достижение из избранного + security: + - bearerAuth: [] + parameters: + - in: path + name: achievement_ID + required: true + schema: + type: integer + responses: + 200: + description: Успешно удалено из избранного + 500: + $ref: '#/components/responses/ServerError' + + /profile/settings: + get: + tags: [Profile] + summary: Получение настроек + description: | + Возвращает текущие настройки профиля + security: + - bearerAuth: [] + responses: + 200: + description: Настройки профиля + content: + application/json: + schema: + $ref: '#/components/schemas/UserSettings' + 500: + $ref: '#/components/responses/ServerError' + + /profile/settings-change: + put: + tags: [Profile] + summary: Обновление настроек + description: | + Обновляет настройки профиля + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserSettings' + responses: + 200: + description: Настройки обновлены + content: + application/json: + schema: + $ref: '#/components/schemas/UserSettings' + 500: + $ref: '#/components/responses/ServerError' + + /other-profile/{user_id}: + get: + tags: [OtherProfile] + summary: Получение данных профиля другого пользователя + description: | + Загружает данные профиля другого пользователя. + - Возвращает статистику, избранные карты и достижения + - Проверяет настройки приватности инвентаря + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + responses: + 200: + description: Данные профиля + content: + application/json: + schema: + type: object + properties: + userStats: + $ref: '#/components/schemas/UserStats' + favoriteCards: + type: array + items: + $ref: '#/components/schemas/Card' + maxItems: 5 + favoriteAchievements: + type: array + items: + $ref: '#/components/schemas/Achievement' + maxItems: 4 + inventoryVisible: + type: boolean + example: true + id: + type: integer + example: '456' + username: + type: string + example: 'Игрок_456' + 500: + $ref: '#/components/responses/ServerError' + + /other-profile/{user_id}/achievements: + get: + tags: [OtherProfile] + summary: Получение списка достижений другого пользователя + description: | + Возвращает полный список достижений с текущим статусом выполнения + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + responses: + 200: + description: Список достижений + content: + application/json: + schema: + type: object + properties: + achievements: + type: array + items: + $ref: '#/components/schemas/Achievement' + 500: + $ref: '#/components/responses/ServerError' + + /other-profile/{user_id}/inventory: + get: + tags: [OtherProfile] + summary: Просмотр инвентаря другого пользователя + description: | + Возвращает инвентарь пользователя, если он не скрыт в настройках + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + responses: + 200: + description: Инвентарь пользователя + content: + application/json: + schema: + type: object + properties: + cards: + type: array + items: + $ref: '#/components/schemas/Card' + collections: + type: array + items: + $ref: '#/components/schemas/Collection' + 403: + description: Инвентарь скрыт + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Пользователь скрыл свой инвентарь' + 500: + $ref: '#/components/responses/ServerError' + + /other-profile/inventory/sort: + post: + tags: [OtherProfile] + summary: Сортировка карт в инвентаре другого пользователя + description: | + Сортирует карты в инвентаре другого пользователя + (по редкости, по коллекциям, механизм сортировки подробнее описан в инвентаре) + parameters: + - in: query + name: user_id + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + sortType: + type: string + enum: [rarity_desc, rarity_asc, collection, reset] + responses: + 200: + description: Отсортированный список карт + content: + application/json: + schema: + type: object + properties: + inventory: + type: array + items: + $ref: '#/components/schemas/Card' + 500: + $ref: '#/components/responses/ServerError' + + /other-profile/card/{card_ID}/view: + get: + tags: [OtherProfile] + summary: Просмотр карты другого пользователя + description: | + Возвращает детальную информацию о карте другого пользователя + и возвращает кнопку для создания предложения обмена + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + - in: query + name: owner_ID + required: true + schema: + type: integer + responses: + 200: + description: Информация о карте + content: + application/json: + schema: + type: object + properties: + card: + $ref: '#/components/schemas/Card' + 500: + $ref: '#/components/responses/ServerError' + + /other-profile/card/{card_ID}/initiate-trade: + post: + tags: [OtherProfile] + summary: Предложение обмена + description: | + Создает предложение обмена для указанной карты + и перенаправляет на страницу создания обмена + security: + - bearerAuth: [] + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + - in: query + name: owner_ID + required: true + schema: + type: integer + responses: + 200: + description: Перенаправление на создание обмена + content: + application/json: + schema: + type: object + properties: + redirectUrl: + type: string + example: '/trades/create?requested_card=123&owner=456' + 400: + description: Невозможно предложить обмен + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Недостаточно карт для обмена' + 409: + description: Конфликт при попытке обмена + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Невозможно предложить обмен - у владельца только один экземпляр этой карты' + cardQuantity: + type: integer + example: 1 + description: 'Количество экземпляров карты у владельца' + 500: + $ref: '#/components/responses/ServerError' + + /other-profile/{user_id}/report: + post: + tags: [OtherProfile] + summary: Подача жалобы на пользователя + description: | + Перенаправляет на страницу создания жалобы. + security: + - bearerAuth: [] + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + description: ID пользователя, на которого подается жалоба + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + reason: + type: string + enum: ['Неуместный никнейм', 'Неуместный аватар', 'Другое'] + example: 'Неуместный никнейм' + description: 'Причина жалобы' + comment: + type: string + maxLength: 500 + nullable: true + example: 'Никнейм содержит нецензурные слова' + description: 'Дополнительные комментарии' + required: [reason] + responses: + 200: + description: Перенаправление на страницу создания жалобы + content: + application/json: + schema: + type: object + properties: + redirectUrl: + type: string + example: '/report/create?reported_user=123' + reportDraft: + $ref: '#/components/schemas/Report' + 500: + $ref: '#/components/responses/ServerError' + + /inventory: + get: + tags: [Inventory] + summary: Получение данных инвентаря + description: | + Загружает данные инвентаря пользователя. + - Проверяет авторизацию пользователя + - Если пользователь не авторизован - возвращает ошибку 401 + - Если авторизован - возвращает список его карт, список избранных карт и баланс + security: + - bearerAuth: [] + responses: + 200: + description: Успешная загрузка инвентаря + content: + application/json: + schema: + type: object + properties: + inventory: + type: array + items: + $ref: '#/components/schemas/Card' + favoriteCards: + type: array + items: + type: integer + description: 'ID избранных карт' + collections: + type: array + items: + $ref: '#/components/schemas/Collection' + balance: + type: integer + example: 2500 + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + + /inventory/card/{card_ID}/quantity: + get: + tags: [Inventory] + summary: Проверка количества экземпляров карты + description: | + Возвращает количество экземпляров карты у пользователя. + Если экземпляров больше 1 - можно разбирать/выставлять на обмен. + security: + - bearerAuth: [] + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + responses: + 200: + description: Информация о количестве карт + content: + application/json: + schema: + type: object + properties: + count: + type: integer + example: 3 + canDisassemble: + type: boolean + example: true + canTrade: + type: boolean + example: true + 500: + $ref: '#/components/responses/ServerError' + + /inventory/favorites/count: + get: + tags: [Inventory] + summary: Получение количества избранных карт + description: | + Возвращает текущее количество избранных карт. + Если количество меньше 5 - можно добавлять новые карты в избранное. + security: + - bearerAuth: [] + responses: + 200: + description: Количество избранных карт + content: + application/json: + schema: + type: object + properties: + count: + type: integer + example: 3 + canAddMore: + type: boolean + example: true + 500: + $ref: '#/components/responses/ServerError' + + /inventory/card/{card_ID}/favorite: + get: + tags: [Inventory] + summary: Проверка статуса карты + description: | + Проверяет, добавлена ли карта в избранное. + security: + - bearerAuth: [] + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + responses: + 200: + description: Статус избранного + content: + application/json: + schema: + type: object + properties: + isFavorite: + type: boolean + example: false + 500: + $ref: '#/components/responses/ServerError' + /inventory/card/{card_ID}/favorite-add: + post: + tags: [Inventory] + summary: Добавление карты в избранное + description: | + Добавляет карту в избранное, если: + - Карта не добавлена в "избранное" + - Общее количество избранных карт < 5 + security: + - bearerAuth: [] + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + responses: + 200: + description: Карта успешно добавлена в избранное + 400: + description: Невозможно добавить карту в избранное + 500: + $ref: '#/components/responses/ServerError' + + /inventory/card/{card_ID}/favorite-delete: + delete: + tags: [Inventory] + summary: Удаление карты из избранного + description: | + Удаляет карту из избранного. + security: + - bearerAuth: [] + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + responses: + 200: + description: Карта успешно удалена из избранного + 500: + $ref: '#/components/responses/ServerError' + + /inventory/sort: + post: + tags: [Inventory] + summary: Сортировка карт в инвентаре + description: | + Сортирует карты в инвентаре(по редкости, по коллекциям): + - При выборе по редкости: + - 1 нажатие: по убыванию редкости (Уникальная → Легендарная → Эпическая → Редкая → Обычная) + - 2 нажатие: по возрастанию редкости + - 3 нажатие: сброс сортировки + - При выборе "по коллекциям": сначала идут карты из одной коллекции, коллекции сортируются по ID. + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + sortType: + type: string + enum: [rarity_desc, rarity_asc, collection, reset] + responses: + 200: + description: Отсортированный список карт + content: + application/json: + schema: + type: object + properties: + cards: + type: array + items: + $ref: '#/components/schemas/Card' + 500: + $ref: '#/components/responses/ServerError' + + /inventory/destroy: + post: + tags: [Inventory] + summary: Разбор карты на валюту + description: | + Разбирает карту на валюту, если: + - У пользователя есть минимум 2 экземпляра этой карты + - Добавляет стоимость карты на баланс пользователя + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + card_ID: + type: integer + minimum: 1 + example: 501 + required: [card_ID] + responses: + 200: + description: Карта успешно разобрана + content: + application/json: + schema: + type: object + properties: + newBalance: + type: integer + example: 2550 + destroyedCardId: + type: integer + example: 501 + 500: + $ref: '#/components/responses/ServerError' + /inventory/card/{card_ID}/trade-status: + get: + tags: [Inventory] + summary: Проверка статуса обмена карты + description: | + Проверяет, выставлена ли карта на обмен. + Возвращает статус карты. + security: + - bearerAuth: [] + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + responses: + 200: + description: Статус обмена карты + content: + application/json: + schema: + type: object + properties: + isOnTrade: + type: boolean + example: true + description: 'Выставлена ли карта на обмен' + 500: + $ref: '#/components/responses/ServerError' + + /inventory/put-on-trade: + post: + tags: [Inventory] + summary: Выставление карты на обмен + description: | + Выставляет карту на обмен, если: + - У пользователя есть минимум 2 экземпляра этой карты + - Карта еще не выставлена на обмен + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + card_ID: + type: integer + minimum: 1 + example: 501 + required: [card_ID] + responses: + 200: + description: Карта готова к обмену + content: + application/json: + schema: + type: object + properties: + redirectUrl: + type: string + example: '/trades/create?requested_card=123&owner=456' + 400: + description: Невозможно выставить карту на обмен + 500: + $ref: '#/components/responses/ServerError' + + /inventory/card/{card_ID}/trade-cancel: + post: + tags: [Inventory] + summary: Снятие карты с обмена + description: Возвращает карту из раздела "На обмен" в Инвентарь + security: + - bearerAuth: [] + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + responses: + 200: + description: Карта успешно снята с обмена + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Карта успешно снята с обмена' + 500: + $ref: '#/components/responses/ServerError' + /home/search: + get: + tags: [Home] + summary: Поиск пользователей + description: | + Поиск пользователей по никнейму или ID + parameters: + - in: query + name: query + description: Никнейм или ID пользователя + schema: + type: string + responses: + 200: + description: Список найденных пользователей + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + 400: + description: Неверный запрос + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Введите минимум 3 символа для поиска' + 404: + description: Пользователь не найден + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Пользователь с введеными данными не найден' + 500: + $ref: '#/components/responses/ServerError' + + /home/notifications: + get: + tags: [Home] + summary: Просмотр уведомлений + description: | + Возвращает список уведомлений пользователя + security: + - bearerAuth: [] + responses: + 200: + description: Список уведомлений + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Notification' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + + /home/notifications/{notification_ID}: + get: + tags: [Home] + summary: Просмотр уведомления + description: | + Возвращает информацию по конкретному уведомлению + security: + - bearerAuth: [] + parameters: + - in: path + name: notification_ID + required: true + schema: + type: integer + example: 123 + description: 'ID уведомления' + responses: + 200: + description: Детальная информация об уведомлении + content: + application/json: + schema: + $ref: '#/components/schemas/Notification' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + + /home/notifications/{notification_ID}/navigate: + post: + tags: [Home] + summary: Переход по ссылке в уведомлении + description: | + Перенаправляет на страницу обмена, связанного с уведомлением + security: + - bearerAuth: [] + parameters: + - in: path + name: notification_ID + required: true + schema: + type: integer + responses: + 200: + description: Перенаправление на страницу обмена + content: + application/json: + schema: + type: object + properties: + redirectUrl: + type: string + example: '/trades/123' + 404: + description: Предложение обмена не найдено + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Данное предложение обмена уже не существует' + 500: + $ref: '#/components/responses/ServerError' + /home/generate-card/themes: + get: + tags: [Home] + summary: Получение списка тем для генерации карт + description: | + Возвращает список доступных тем для генерации уникальных карт + security: + - bearerAuth: [] + responses: + 200: + description: Список тем + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Theme' + 500: + $ref: '#/components/responses/ServerError' + /home/generate-card: + post: + tags: [Home] + summary: Генерация уникальной карты + description: | + Генерирует уникальную карту по выбранной теме, если на балансе достаточно средств + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + theme_ID: + type: integer + example: 901 + description: 'ID выбранной темы' + required: [theme_ID] + responses: + 200: + description: Карта успешно сгенерирована + content: + application/json: + schema: + type: object + properties: + card: + $ref: '#/components/schemas/Card' + newBalance: + type: integer + example: 1500 + 402: + description: Недостаточно средств + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Недостаточно средств для генерации карты' + requiredAmount: + type: integer + example: 1000 + 500: + $ref: '#/components/responses/ServerError' + /home/news: + get: + tags: [Home] + summary: Получение списока новостей + description: | + Возвращает список новостей + responses: + 200: + description: Список новостей + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/News' + 404: + description: Новостей не найдено + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Новостей пока нет' + 500: + $ref: '#/components/responses/ServerError' + /home/news/{news_ID}: + get: + tags: [Home] + summary: Просмотр новости + description: | + Возвращает полную информацию по конкретной новости + parameters: + - in: path + name: news_ID + required: true + schema: + type: integer + responses: + 200: + description: Детальная информация о новости + content: + application/json: + schema: + $ref: '#/components/schemas/News' + 500: + $ref: '#/components/responses/ServerError' + /home/quests: + get: + tags: [Home] + summary: Получение списка квестов + description: | + Возвращает список ежедневных и еженедельных квестов с их статусом + security: + - bearerAuth: [] + responses: + 200: + description: Список квестов + content: + application/json: + schema: + type: object + properties: + dailyQuests: + type: array + items: + $ref: '#/components/schemas/Quest' + weeklyQuests: + type: array + items: + $ref: '#/components/schemas/Quest' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + /home/quests/{quest_ID}/change-status: + post: + tags: [Home] + summary: Изменение статуса выполнения квеста + description: | + Изменение статуса выполнения квеста + security: + - bearerAuth: [] + parameters: + - in: path + name: quest_ID + required: true + schema: + type: integer + example: 123 + description: 'ID квеста' + responses: + 200: + description: Статус квеста успешно изменен + content: + application/json: + schema: + type: object + properties: + quest: + $ref: '#/components/schemas/Quest' + message: + type: string + example: 'Статус квеста успешно изменен' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + + /home/quests/claim-reward: + post: + tags: [Home] + summary: Получение награды за выполнение квестов + description: | + Выдает награду за выполнение квестов + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + questType: + type: string + enum: [daily, weekly] + example: 'daily' + required: [questType] + responses: + 200: + description: Награда получена + content: + application/json: + schema: + type: object + properties: + receivedCoins: + type: integer + example: 500 + receivedPacks: + type: array + items: + $ref: '#/components/schemas/Pack' + newBalance: + type: integer + example: 2000 + 400: + description: Не все квесты выполнены + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Выполнены не все квесты, необходимые для получения награды' + 500: + $ref: '#/components/responses/ServerError' + /trades: + get: + tags: [Trades] + summary: Получить все доступные предложения обмена + description: | + Возвращает список всех доступных предложений обмена. + Можно провести поиск по названию карты. + parameters: + - in: query + name: search + description: Поиск по названию карты + schema: + type: string + responses: + 200: + description: Список предложений обмена + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Trade' + 500: + $ref: '#/components/responses/ServerError' + /trades/{trade_id}: + get: + tags: [Trades] + summary: Получить детали предложения обмена + description: | + Возвращает полную информацию о конкретном предложении обмена. + parameters: + - in: path + name: trade_id + required: true + schema: + type: integer + responses: + 200: + description: Детали предложения обмена + content: + application/json: + schema: + $ref: '#/components/schemas/Trade' + 404: + description: Предложение обмена не найдено + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Предложение обмена не найдено' + 500: + $ref: '#/components/responses/ServerError' + /trades/initiate: + post: + tags: [Trades] + summary: Создать новое предложение обмена + description: | + Создает новое предложение обмена. + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + offeredCardId: + type: integer + example: 401 + description: ID карты, которую предлагает текущий пользователь + requestedCardId: + type: array + items: + type: integer + description: ID карт, на которые пользователь готов совершить "быстрый обмен" + example: [100, 200] + required: [receivingUserId, requestedCardId, offeredCardId] + responses: + 201: + description: Предложение обмена успешно создано + content: + application/json: + schema: + $ref: '#/components/schemas/Trade' + 400: + description: Невозможно создать предложение обмена + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Недостаточно карт для обмена' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + /trades/my: + get: + tags: [Trades] + summary: Получение моих обменов + description: | + Возвращает список всех обменов текущего пользователя с их статусами. + security: + - bearerAuth: [] + parameters: + - in: query + name: user_id + required: true + schema: + type: integer + example: 123 + description: 'ID текущего пользователя' + responses: + 200: + description: Список моих обменов + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Trade' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + /trades/{trade_id}/accept: + post: + tags: [Trades] + summary: Принять предложение обмена + description: | + Принимает предложение обмена, если текущий пользователь - получатель обмена. + security: + - bearerAuth: [] + parameters: + - in: path + name: trade_id + required: true + schema: + type: integer + responses: + 200: + description: Обмен успешно принят + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Обмен успешно завершен' + + 401: + $ref: '#/components/responses/Unauthorized' + 403: + description: Нет прав для принятия этого обмена + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Вы не можете принять этот обмен' + 500: + $ref: '#/components/responses/ServerError' + + /trades/{trade_id}/reject: + post: + tags: [Trades] + summary: Отклонить предложение обмена + description: | + Отклоняет предложение обмена, если текущий пользователь - получатель обмена. + security: + - bearerAuth: [] + parameters: + - in: path + name: trade_id + required: true + schema: + type: integer + responses: + 200: + description: Обмен успешно отклонен + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Обмен отклонен' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + + /trades/{trade_id}/cancel: + post: + tags: [Trades] + summary: Отменить предложение обмена + description: | + Отменяет предложение обмена, если текущий пользователь является инициатором. + security: + - bearerAuth: [] + parameters: + - in: path + name: trade_id + required: true + schema: + type: integer + responses: + 200: + description: Обмен успешно отменен + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Обмен отменен' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + + /shop/packs: + get: + tags: [Shop] + summary: Получение списка наборов + description: | + Возвращает список доступных для покупки наборов карт + responses: + 200: + description: Список наборов + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pack' + 500: + $ref: '#/components/responses/ServerError' + + /shop/packs/{pack_id}: + get: + tags: [Shop] + summary: Просмотр содержимого набора + description: | + Возвращает подробную информацию о наборе и возможных картах + parameters: + - in: path + name: pack_id + required: true + schema: + type: integer + responses: + 200: + description: Информация о наборе + content: + application/json: + schema: + $ref: '#/components/schemas/Pack' + 500: + $ref: '#/components/responses/ServerError' + /shop/packs/{pack_id}/buy: + post: + tags: [Shop] + summary: Покупка набора карт + description: | + Покупает набор карт, если: + - Пользователь авторизован + - На балансе достаточно средств + security: + - bearerAuth: [] + parameters: + - in: path + name: pack_id + required: true + schema: + type: integer + responses: + 200: + description: Успешная покупка + content: + application/json: + schema: + type: object + properties: + receivedCards: + type: array + items: + $ref: '#/components/schemas/Card' + newBalance: + type: integer + 401: + $ref: '#/components/responses/Unauthorized' + 402: + description: Недостаточно средств + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Недостаточно средств на балансе' + requiredAmount: + type: integer + example: 1000 + 500: + $ref: '#/components/responses/ServerError' + /shop/packs/{pack_id}/open: + post: + tags: [Shop] + summary: Открытие купленного набора карт + description: | + Открывает ранее купленный набор карт, показывая выпавшие карты + и добавляя их в инвентарь пользователя. + security: + - bearerAuth: [] + parameters: + - in: path + name: pack_id + required: true + schema: + type: integer + example: 1 + description: 'ID набора карт' + responses: + 200: + description: Набор успешно открыт + content: + application/json: + schema: + type: object + properties: + receivedCards: + type: array + items: + $ref: '#/components/schemas/Card' + description: 'Карты, полученные из набора' + newCardsAdded: + type: boolean + example: true + description: 'Были ли карты успешно добавлены в инвентарь' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + + /shop/coins/offers: + get: + tags: [Shop] + summary: Получение предложений по монетам + description: | + Возвращает список доступных пакетов монет + responses: + 200: + description: Список предложений + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CoinOffer' + 500: + $ref: '#/components/responses/ServerError' + /shop/coins/offers/{offer_id}: + get: + tags: [Shop] + summary: Просмотр предложения монет + description: | + Возвращает детализацию предложения по монетам + parameters: + - in: path + name: offer_id + required: true + schema: + type: integer + responses: + 200: + description: Информация о предложении + content: + application/json: + schema: + $ref: '#/components/schemas/CoinOffer' + 500: + $ref: '#/components/responses/ServerError' + /shop/coins/offers/{offer_id}/purchase: + post: + tags: [Shop] + summary: Покупка монет + description: | + Инициирует процесс покупки монет через платежный шлюз + security: + - bearerAuth: [] + parameters: + - in: path + name: offer_id + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + redirectUrl: + type: string + format: uri + example: 'https://cardly.ru/shop/payment-callback' + required: [redirectUrl] + responses: + 200: + description: Перенаправление на платежный шлюз + content: + application/json: + schema: + type: object + properties: + paymentUrl: + type: string + format: uri + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/ServerError' + /shop/payments/process: + post: + tags: [Shop] + summary: Обработка платежа + description: | + обработка результата платежа от платежного шлюза + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PaymentCallback' + responses: + 200: + description: Платеж обработан + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + transactionId: + type: string + 500: + $ref: '#/components/responses/ServerError' + /admin/cards: + post: + tags: [Admin] + summary: Создание новой карты + description: Создает новую карту в системе + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Card' + responses: + 201: + description: Карта успешно создана + content: + application/json: + schema: + $ref: '#/components/schemas/Card' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/cards/{card_ID}: + put: + tags: [Admin] + summary: Обновление карты + description: Обновляет информацию о карте + security: + - bearerAuth: [] + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Card' + responses: + 200: + description: Карта успешно обновлена + content: + application/json: + schema: + $ref: '#/components/schemas/Card' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + delete: + tags: [Admin] + summary: Удаление карты + description: Удаляет карту из системы + security: + - bearerAuth: [] + parameters: + - in: path + name: card_ID + required: true + schema: + type: integer + responses: + 204: + description: Карта успешно удалена + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/collections: + post: + tags: [Admin] + summary: Создание новой коллекции + description: Создает новую коллекцию карт + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Collection' + responses: + 201: + description: Коллекция успешно создана + content: + application/json: + schema: + $ref: '#/components/schemas/Collection' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/collections/{collection_ID}: + put: + tags: [Admin] + summary: Обновление коллекции + description: Обновляет информацию о коллекции + security: + - bearerAuth: [] + parameters: + - in: path + name: collection_ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Collection' + responses: + 200: + description: Коллекция успешно обновлена + content: + application/json: + schema: + $ref: '#/components/schemas/Collection' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + delete: + tags: [Admin] + summary: Удаление коллекцию + description: Удаляет коллекцию из системы + security: + - bearerAuth: [] + parameters: + - in: path + name: collection_ID + required: true + schema: + type: integer + responses: + 204: + description: Коллекция успешно удалена + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/packs: + post: + tags: [Admin] + summary: Создание нового набора + description: Создает новый набор карт + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Pack' + responses: + 201: + description: Набор успешно создан + content: + application/json: + schema: + $ref: '#/components/schemas/Pack' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/packs/{pack_ID}: + put: + tags: [Admin] + summary: Обновление набора + description: Обновляет информацию о наборе + security: + - bearerAuth: [] + parameters: + - in: path + name: pack_ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Pack' + responses: + 200: + description: Набор успешно обновлен + content: + application/json: + schema: + $ref: '#/components/schemas/Pack' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + delete: + tags: [Admin] + summary: Удаление набор + description: Удаляет набор из системы + security: + - bearerAuth: [] + parameters: + - in: path + name: pack_ID + required: true + schema: + type: integer + responses: + 204: + description: Набор успешно удален + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/coin-offers: + post: + tags: [Admin] + summary: Создание предложения покупки монет + description: Создает новое предложение покупки монет в системе + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CoinOffer' + responses: + 201: + description: Предложение успешно создано + content: + application/json: + schema: + $ref: '#/components/schemas/CoinOffer' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + + get: + tags: [Admin] + summary: Получение списка предложений покупки монет + description: Возвращает список всех предложений покупки монет + security: + - bearerAuth: [] + responses: + 200: + description: Список предложений успешно получен + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CoinOffer' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + + /admin/coin-offers/{offer_id}: + put: + tags: [Admin] + summary: Обновление предложения покупки монет + description: Обновляет информацию о предложении покупки монет + security: + - bearerAuth: [] + parameters: + - in: path + name: offer_id + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CoinOffer' + responses: + 200: + description: Предложение успешно обновлено + content: + application/json: + schema: + $ref: '#/components/schemas/CoinOffer' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + + delete: + tags: [Admin] + summary: Удаление предложения покупки монет + description: Удаляет предложение покупки монет из системы + security: + - bearerAuth: [] + parameters: + - in: path + name: offer_id + required: true + schema: + type: integer + responses: + 204: + description: Предложение успешно удалено + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/news: + post: + tags: [Admin] + summary: Создание новости + description: Создает новую новость в системе + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/News' + responses: + 201: + description: Новость успешно создана + content: + application/json: + schema: + $ref: '#/components/schemas/News' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/news/{news_ID}: + put: + tags: [Admin] + summary: Обновление новости + description: Обновляет информацию о новости + security: + - bearerAuth: [] + parameters: + - in: path + name: news_ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/News' + responses: + 200: + description: Новость успешно обновлена + content: + application/json: + schema: + $ref: '#/components/schemas/News' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + delete: + tags: [Admin] + summary: Удаление новости + description: Удаляет новость из системы + security: + - bearerAuth: [] + parameters: + - in: path + name: news_ID + required: true + schema: + type: integer + responses: + 204: + description: Новость успешно удалена + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/achievements: + post: + tags: [Admin] + summary: Создание достижения + description: Создает новое достижение в системе + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Achievement' + responses: + 201: + description: Достижение успешно создано + content: + application/json: + schema: + $ref: '#/components/schemas/Achievement' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/achievements/{achievement_ID}: + put: + tags: [Admin] + summary: Обновление достижения + description: Обновляет информацию о достижении + security: + - bearerAuth: [] + parameters: + - in: path + name: achievement_ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Achievement' + responses: + 200: + description: Достижение успешно обновлено + content: + application/json: + schema: + $ref: '#/components/schemas/Achievement' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + delete: + tags: [Admin] + summary: Удаление достижения + description: Удаляет достижение из системы + security: + - bearerAuth: [] + parameters: + - in: path + name: achievement_ID + required: true + schema: + type: integer + responses: + 204: + description: Достижение успешно удалено + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/trades: + get: + tags: [Admin] + summary: Получение списка обменов + description: Возвращает список всех обменов в системе + security: + - bearerAuth: [] + responses: + 200: + description: Список обменов + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Trade' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/trades/{trade_ID}/invalidate: + post: + tags: [Admin] + summary: Признание обмена недействительным + description: | + Помечает обмен как недействительный и возвращает карты участникам + security: + - bearerAuth: [] + parameters: + - in: path + name: trade_ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + reason: + type: string + description: Причина аннулирования обмена + required: [reason] + responses: + 200: + description: Обмен аннулирован + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Обмен успешно аннулирован' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/reports: + get: + tags: [Admin] + summary: Просмотр списка жалоб + description: Возвращает список всех жалоб в системе + security: + - bearerAuth: [] + responses: + 200: + description: Список жалоб + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Report' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/reports/{report_ID}: + get: + tags: [Admin] + summary: Просмотр деталей жалобы + description: Возвращает полную информацию о жалобе + security: + - bearerAuth: [] + parameters: + - in: path + name: report_ID + required: true + schema: + type: integer + responses: + 200: + description: Детали жалобы + content: + application/json: + schema: + $ref: '#/components/schemas/Report' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + put: + tags: [Admin] + summary: Обновление статуса жалобы + description: Обновляет статус жалобы и применяет меры к пользователю + security: + - bearerAuth: [] + parameters: + - in: path + name: report_ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: [pending, reviewed, resolved] + example: 'resolved' + action: + type: string + enum: [none, warning, temporary_ban, permanent_ban] + example: 'temporary_ban' + banDurationDays: + type: integer + nullable: true + example: 7 + description: 'Длительность бана в днях (только для temporary_ban)' + comment: + type: string + nullable: true + example: 'Пользователь получил временный бан на 7 дней за нарушение правил' + required: [status, action] + responses: + 200: + description: Статус жалобы обновлен + content: + application/json: + schema: + $ref: '#/components/schemas/Report' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + description: Жалоба не найдена + 500: + $ref: '#/components/responses/ServerError' + /admin/users/{user_id}/ban: + post: + tags: [Admin] + summary: Блокировка пользователя + description: Блокирует пользователя на указанный срок или навсегда + security: + - bearerAuth: [] + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + banType: + type: string + enum: [temporary, permanent] + example: 'temporary' + durationDays: + type: integer + nullable: true + example: 7 + description: 'Длительность бана в днях' + reason: + type: string + example: 'Нарушение правил сообщества' + required: [banType, reason] + responses: + 200: + description: Пользователь заблокирован + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Пользователь успешно заблокирован' + banEndDate: + type: string + format: date-time + nullable: true + example: '2024-06-20T12:00:00Z' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + description: Пользователь не найден + 500: + $ref: '#/components/responses/ServerError' + /admin/users/{user_id}/unban: + post: + tags: [Admin] + summary: Разблокировка пользователя + description: Снимает блокировку с пользователя + security: + - bearerAuth: [] + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + responses: + 200: + description: Пользователь разблокирован + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Пользователь успешно разблокирован' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + description: Пользователь не найден + 500: + $ref: '#/components/responses/ServerError' + /admin/users/{user_id}/delete: + delete: + tags: [Admin] + summary: Удаление пользователя + description: Полностью удаляет аккаунт пользователя из системы + security: + - bearerAuth: [] + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + responses: + 204: + description: Пользователь успешно удален + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + description: Пользователь не найден + 500: + $ref: '#/components/responses/ServerError' + /admin/users/{user_id}/inventory/{card_ID}: + delete: + tags: [Admin] + summary: Удаление карты из инвентаря пользователя + description: Удаляет конкретную карту из коллекции пользователя + security: + - bearerAuth: [] + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + - in: path + name: card_ID + required: true + schema: + type: integer + responses: + 204: + description: Карта успешно удалена из инвентаря + 400: + description: Неверный запрос + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/users/{user_id}/achievements/{achievement_ID}: + delete: + tags: [Admin] + summary: Отзыв достижения у пользователя + description: Удаляет достижение из списка полученных пользователем + security: + - bearerAuth: [] + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + - in: path + name: achievement_ID + required: true + schema: + type: integer + responses: + 204: + description: Достижение успешно отозвано + 400: + description: Неверный запрос + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/users/{user_id}/quests/{quest_ID}/reset: + post: + tags: [Admin] + summary: Сброс выполнения квеста + description: Сбрасывает статус выполнения квеста для пользователя + security: + - bearerAuth: [] + parameters: + - in: path + name: user_id + required: true + schema: + type: integer + - in: path + name: quest_ID + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + reason: + type: string + description: Причина сброса квеста + removeRewards: + type: boolean + default: false + description: Нужно ли удалить награды за квест + required: [reason] + responses: + 200: + description: Квест успешно сброшен + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Квест успешно сброшен' + rewardsRemoved: + type: boolean + example: true + 400: + description: Неверный запрос + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' + /admin/stats: + get: + tags: [Admin] + summary: Получение статистики системы + description: Возвращает статистику по пользователям, картам, обменам и т.д. + security: + - bearerAuth: [] + responses: + 200: + description: Статистика системы + content: + application/json: + schema: + type: object + properties: + totalUsers: + type: integer + example: 1000 + activeUsers: + type: integer + example: 750 + bannedUsers: + type: integer + example: 25 + totalCards: + type: integer + example: 5000 + totalTrades: + type: integer + example: 1200 + completedTrades: + type: integer + example: 1000 + invalidatedTrades: + type: integer + example: 50 + pendingReports: + type: integer + example: 15 + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 500: + $ref: '#/components/responses/ServerError' +components: + schemas: + News: + type: object + properties: + news_ID: + type: integer + example: 1 + description: 'Уникальный идентификатор новости' + title: + type: string + example: 'Новое обновление системы' + description: 'Заголовок новости' + content: + type: string + example: 'Мы добавили новые карты в коллекцию' + description: 'Содержание новости' + pictures: + type: array + items: + type: string + format: url + example: + ['https://example.com/news1.jpg', 'https://example.com/news2.jpg'] + description: 'Ссылки на изображения' + datePosted: + type: string + format: date-time + example: '2024-05-20T14:48:00Z' + description: 'Дата публикации' + required: + - news_ID + - title + - content + - datePosted + + Achievement: + type: object + properties: + achievement_ID: + type: integer + example: 101 + description: 'Уникальный идентификатор достижения' + name: + type: string + example: 'Мастер коллекционирования' + description: 'Название достижения' + imageURL: + type: string + format: url + example: 'https://example.com/achievements/master.png' + description: 'Ссылка на изображение' + description: + type: string + example: 'Соберите 10 полных коллекций' + description: 'Описание достижения' + isUnlocked: + type: boolean + example: false + description: 'Статус получения' + required: + - achievement_ID + - name + - imageURL + - description + - isUnlocked + + Notification: + type: object + properties: + notification_ID: + type: integer + example: 201 + description: 'Уникальный идентификатор уведомления' + user: + $ref: '#/components/schemas/User' + description: 'Пользователь, которому отправлено уведомление' + message: + type: string + example: 'Ваша карта была успешно обменяна' + description: 'Текст уведомления' + links: + type: array + items: + type: string + format: url + example: ['https://example.com/exchange-details'] + description: 'Ссылка на подробности обмена' + notificationDateTime: + type: string + format: date-time + example: '2024-05-20T15:30:00Z' + description: 'Дата и время уведомления' + required: + - notification_ID + - user + - message + - notificationDateTime + + Report: + type: object + properties: + report_ID: + type: integer + example: 301 + description: 'Уникальный идентификатор жалобы' + reporter: + $ref: '#/components/schemas/User' + description: 'Пользователь, отправивший жалобу' + reportedUser: + $ref: '#/components/schemas/User' + description: 'Пользователь, на которого пожаловались' + reportDateTime: + type: string + format: date-time + example: '2024-05-20T16:45:00Z' + description: 'Дата и время жалобы' + reason: + type: string + example: 'Непристойный контент' + description: 'непристойный никнейм' + status: + type: string + enum: ['На рассмотрении', 'Подтверждено', 'Отклонено'] + example: 'На расмотрении' + description: 'Статус рассмотрения' + required: + - report_ID + - reporter + - reportedUser + - reportDateTime + - reason + - status + + Pack: + type: object + properties: + pack_ID: + type: integer + example: 401 + description: 'Уникальный идентификатор набора' + name: + type: string + example: 'Стартовый набор' + description: 'Название набора' + imageURL: + type: string + format: url + example: 'https://example.com/packs/starter.png' + description: 'Ссылка на изображение' + cards: + type: array + items: + $ref: '#/components/schemas/Card' + description: 'Карты в наборе' + price: + type: integer + example: 1000 + description: 'Цена' + required: + - pack_ID + - name + - imageURL + - cards + - price + + Card: + type: object + properties: + card_ID: + type: integer + example: 501 + description: 'Уникальный идентификатор карты' + name: + type: string + example: 'Ледяной феникс' + description: 'Название карты' + imageURL: + type: string + format: url + example: 'https://example.com/cards/pheonix.png' + description: 'Ссылка на изображение' + description: + type: string + example: 'Ледяной Феникс — это мифическое существо, воплощающее силу зимы и вечного обновления. Его перья сверкают как морозный утренний иней, а глаза светятся холодным синим светом.' + description: 'Описание карты' + rarity: + type: string + enum: ['Обычная', 'Редкая', 'Эпическая', 'Легендарная', 'Уникальная'] + example: 'Редкая' + description: 'Редкость карты' + min_price: + type: integer + example: 50 + description: 'Минимальная цена - используется для разбора карточки' + theme: + type: string + nullable: true + example: 'Мифическое существо' + description: 'Тематика, на которую была сгенерирвана карта (только для уникальных карт)' + isGenerated: + type: boolean + example: false + description: 'Флаг показывающий сгенерированная ли карта (true для уникальных, false для остальных)' + required: + - card_ID + - name + - imageURL + - rarity + - min_price + - isGenerated + + User: + type: object + properties: + user_id: + type: integer + example: 1 + description: 'Уникальный идентификатор' + username: + type: string + example: 'Коллекционер_123' + description: 'Имя пользователя' + email: + type: string + format: email + example: 'user@example.com' + description: 'Электронная почта' + password: + type: string + format: password + writeOnly: true + description: 'Пароль' + favoriteCards: + type: array + items: + $ref: '#/components/schemas/Card' + description: 'Избранные карты' + onChange: + type: array + items: + $ref: '#/components/schemas/Card' + description: 'Карты на обмен' + inventoryCards: + type: array + items: + $ref: '#/components/schemas/Card' + description: 'Карты в инвентаре' + balance: + type: integer + example: 2500 + description: 'Баланс' + avatar_url: + type: string + format: url + example: 'https://example.com/avatars/user1.png' + description: 'Аватар пользователя' + achievements: + type: array + items: + $ref: '#/components/schemas/Achievement' + description: 'Достижения' + favoriteAchievements: + type: array + items: + $ref: '#/components/schemas/Achievement' + description: 'Избранные достижения' + notifications: + type: array + items: + $ref: '#/components/schemas/Notification' + description: 'Уведомления' + required: + - user_id + - username + - email + - password + - balance + - inventoryCards + - achievements + + Payment: + type: object + properties: + payment_ID: + type: integer + example: 601 + description: 'Уникальный идентификатор платежа' + user: + $ref: '#/components/schemas/User' + description: 'Пользователь' + paymentSUM: + type: number + format: double + example: 199.99 + description: 'Сумма платежа' + bankCardData: + type: array + items: + type: string + writeOnly: true + description: 'Данные карты (только для записи)' + paymentStatus: + type: string + enum: ['В обработке', 'Оплачено', 'Ошибка'] + example: 'Оплачено' + description: 'Статус платежа' + paymentDateTime: + type: string + format: date-time + example: '2024-05-20T17:30:00Z' + description: 'Дата и время платежа' + required: + - payment_ID + - user + - paymentSUM + - paymentStatus + - paymentDateTime + + Trade: + type: object + properties: + trade_ID: + type: integer + example: 701 + description: 'Уникальный идентификатор обмена' + offeringUser: + $ref: '#/components/schemas/User' + description: 'Пользователь, предлагающий обмен' + offeringCards: + type: array + items: + $ref: '#/components/schemas/Card' + description: 'Предлагаемые карты' + receivingUser: + $ref: '#/components/schemas/User' + description: 'Пользователь, получающий предложение' + receivingCards: + type: array + items: + $ref: '#/components/schemas/Card' + description: 'Запрашиваемые карты' + isConfirmed: + type: boolean + example: false + description: 'Статус подтверждения' + tradeDateTime: + type: string + format: date-time + example: '2024-05-20T18:00:00Z' + description: 'Дата и время обмена' + required: + - trade_ID + - offeringUser + - offeringCards + - receivingUser + - receivingCards + - isConfirmed + - tradeDateTime + + Collection: + type: object + properties: + collection_ID: + type: integer + example: 801 + description: 'Уникальный идентификатор коллекции' + name: + type: string + example: 'Драконы' + description: 'Название коллекции' + cards: + type: array + items: + $ref: '#/components/schemas/Card' + description: 'Карты в коллекции' + imageURL: + type: string + format: url + example: 'https://example.com/collections/dragons.png' + description: 'Обложка коллекции' + required: + - collection_ID + - name + - cards + - imageURL + + Theme: + type: object + properties: + theme_ID: + type: integer + example: 901 + description: 'Уникальный идентификатор темы' + name: + type: string + example: 'Мифические существа' + description: 'Название темы' + description: + type: string + example: 'Создайте изображение мифического существа' + description: 'Описание темы' + required: + - theme_ID + - name + + UserStats: + type: object + properties: + totalCards: + type: integer + example: 150 + completedCollections: + type: integer + example: 5 + + CoinOffer: + type: object + properties: + offer_id: + type: integer + example: 1 + name: + type: string + example: 'Стартовый набор' + coinsAmount: + type: integer + example: 100 + price: + type: number + format: double + example: 99.90 + imageUrl: + type: string + format: uri + description: + type: string + nullable: true + example: 'Стартовый набор. Вы можете получить...' + + PaymentCallback: + type: object + properties: + transactionId: + type: string + status: + type: string + enum: ['Успешно', 'Ошибка', 'В процессе'] + amount: + type: number + currency: + type: string + offerId: + type: integer + required: + - transactionId + - status + - amount + + UserSettings: + type: object + properties: + notificationsEnabled: + type: boolean + example: true + description: 'Включены ли уведомления' + showInventory: + type: boolean + example: false + description: 'Виден ли инвентарь другим пользователям' + autoDeclineTrades: + type: boolean + example: false + description: 'Автоматически отклонять все входящие предложения обмена' + required: + - notificationsEnabled + - showInventory + - autoDeclineTrades + + Quest: + type: object + properties: + quest_ID: + type: integer + example: 1 + description: 'Уникальный идентификатор квеста' + name: + type: string + example: 'Соберите 5 карт' + description: 'Название квеста' + description: + type: string + example: 'Соберите 5 карт из коллекции Драконы' + description: 'Описание квеста' + progress: + type: integer + example: 3 + description: 'Текущий прогресс' + target: + type: integer + example: 5 + description: 'Целевое значение' + rewardCoins: + type: integer + example: 200 + description: 'Награда в монетах' + rewardPacks: + type: array + items: + $ref: '#/components/schemas/Pack' + description: 'Награда в наборах' + isCompleted: + type: boolean + example: false + description: 'Выполнен ли квест' + isClaimed: + type: boolean + example: false + description: 'Получена ли награда' + required: + - quest_ID + - name + - description + - progress + - target + - rewardCoins + - isCompleted + - isClaimed + + responses: + BadRequest: + description: Неверный формат запроса + + Unauthorized: + description: Ошибка аутентификации + + ServerError: + description: Что-то пошло не так + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: 'Что-то пошло не так, попробуйте позже' + code: + type: string + example: 'INTERNAL_SERVER_ERROR' + timestamp: + type: string + format: date-time + example: '2024-05-20T12:00:00Z' + + InvalidToken: + description: Невалидный токен + + Forbidden: + description: Доступ запрещен + + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/backend/src/test/kotlin/ru/vsu/app/api/AdminApiTest.kt b/backend/src/test/kotlin/ru/vsu/app/api/AdminApiTest.kt new file mode 100644 index 0000000..4919a86 --- /dev/null +++ b/backend/src/test/kotlin/ru/vsu/app/api/AdminApiTest.kt @@ -0,0 +1,470 @@ +package org.openapitools.api + +import org.openapitools.model.Achievement +import org.openapitools.model.AdminReportsReportIDPutRequest +import org.openapitools.model.AdminStatsGet200Response +import org.openapitools.model.AdminTradesTradeIDInvalidatePost200Response +import org.openapitools.model.AdminTradesTradeIDInvalidatePostRequest +import org.openapitools.model.AdminUsersUserIDBanPost200Response +import org.openapitools.model.AdminUsersUserIDBanPostRequest +import org.openapitools.model.AdminUsersUserIDQuestsQuestIDResetPost200Response +import org.openapitools.model.AdminUsersUserIDQuestsQuestIDResetPostRequest +import org.openapitools.model.AdminUsersUserIDUnbanPost200Response +import org.openapitools.model.Card +import org.openapitools.model.CoinOffer +import org.openapitools.model.Collection +import org.openapitools.model.News +import org.openapitools.model.Pack +import org.openapitools.model.InternalServerError +import org.openapitools.model.Report +import org.openapitools.model.Trade +import org.junit.jupiter.api.Test +import org.springframework.http.ResponseEntity + +class AdminApiTest { + + private val api: AdminApiController = AdminApiController() + + /** + * To test AdminApiController.adminAchievementsAchievementIDDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminAchievementsAchievementIDDeleteTest() { + val achievementID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminAchievementsAchievementIDDelete(achievementID) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminAchievementsAchievementIDPut + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminAchievementsAchievementIDPutTest() { + val achievementID: kotlin.Int = TODO() + val achievement: Achievement = TODO() + val response: ResponseEntity = api.adminAchievementsAchievementIDPut(achievementID, achievement) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminAchievementsPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminAchievementsPostTest() { + val achievement: Achievement = TODO() + val response: ResponseEntity = api.adminAchievementsPost(achievement) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCardsCardIDDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCardsCardIDDeleteTest() { + val cardID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminCardsCardIDDelete(cardID) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCardsCardIDPut + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCardsCardIDPutTest() { + val cardID: kotlin.Int = TODO() + val card: Card = TODO() + val response: ResponseEntity = api.adminCardsCardIDPut(cardID, card) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCardsPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCardsPostTest() { + val card: Card = TODO() + val response: ResponseEntity = api.adminCardsPost(card) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCoinOffersGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCoinOffersGetTest() { + val response: ResponseEntity = api.adminCoinOffersGet() + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCoinOffersOfferIdDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCoinOffersOfferIdDeleteTest() { + val offerId: kotlin.Int = TODO() + val response: ResponseEntity = api.adminCoinOffersOfferIdDelete(offerId) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCoinOffersOfferIdPut + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCoinOffersOfferIdPutTest() { + val offerId: kotlin.Int = TODO() + val coinOffer: CoinOffer = TODO() + val response: ResponseEntity = api.adminCoinOffersOfferIdPut(offerId, coinOffer) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCoinOffersPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCoinOffersPostTest() { + val coinOffer: CoinOffer = TODO() + val response: ResponseEntity = api.adminCoinOffersPost(coinOffer) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCollectionsCollectionIDDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCollectionsCollectionIDDeleteTest() { + val collectionID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminCollectionsCollectionIDDelete(collectionID) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCollectionsCollectionIDPut + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCollectionsCollectionIDPutTest() { + val collectionID: kotlin.Int = TODO() + val collection: Collection = TODO() + val response: ResponseEntity = api.adminCollectionsCollectionIDPut(collectionID, collection) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminCollectionsPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminCollectionsPostTest() { + val collection: Collection = TODO() + val response: ResponseEntity = api.adminCollectionsPost(collection) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminNewsNewsIDDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminNewsNewsIDDeleteTest() { + val newsID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminNewsNewsIDDelete(newsID) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminNewsNewsIDPut + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminNewsNewsIDPutTest() { + val newsID: kotlin.Int = TODO() + val news: News = TODO() + val response: ResponseEntity = api.adminNewsNewsIDPut(newsID, news) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminNewsPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminNewsPostTest() { + val news: News = TODO() + val response: ResponseEntity = api.adminNewsPost(news) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminPacksPackIDDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminPacksPackIDDeleteTest() { + val packID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminPacksPackIDDelete(packID) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminPacksPackIDPut + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminPacksPackIDPutTest() { + val packID: kotlin.Int = TODO() + val pack: Pack = TODO() + val response: ResponseEntity = api.adminPacksPackIDPut(packID, pack) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminPacksPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminPacksPostTest() { + val pack: Pack = TODO() + val response: ResponseEntity = api.adminPacksPost(pack) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminReportsGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminReportsGetTest() { + val response: ResponseEntity = api.adminReportsGet() + + // TODO: test validations + } + + /** + * To test AdminApiController.adminReportsReportIDGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminReportsReportIDGetTest() { + val reportID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminReportsReportIDGet(reportID) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminReportsReportIDPut + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminReportsReportIDPutTest() { + val reportID: kotlin.Int = TODO() + val adminReportsReportIDPutRequest: AdminReportsReportIDPutRequest = TODO() + val response: ResponseEntity = api.adminReportsReportIDPut(reportID, adminReportsReportIDPutRequest) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminStatsGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminStatsGetTest() { + val response: ResponseEntity = api.adminStatsGet() + + // TODO: test validations + } + + /** + * To test AdminApiController.adminTradesGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminTradesGetTest() { + val response: ResponseEntity = api.adminTradesGet() + + // TODO: test validations + } + + /** + * To test AdminApiController.adminTradesTradeIDInvalidatePost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminTradesTradeIDInvalidatePostTest() { + val tradeID: kotlin.Int = TODO() + val adminTradesTradeIDInvalidatePostRequest: AdminTradesTradeIDInvalidatePostRequest = TODO() + val response: ResponseEntity = api.adminTradesTradeIDInvalidatePost(tradeID, adminTradesTradeIDInvalidatePostRequest) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminUsersUserIDAchievementsAchievementIDDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminUsersUserIDAchievementsAchievementIDDeleteTest() { + val userID: kotlin.Int = TODO() + val achievementID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminUsersUserIDAchievementsAchievementIDDelete(userID, achievementID) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminUsersUserIDBanPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminUsersUserIDBanPostTest() { + val userID: kotlin.Int = TODO() + val adminUsersUserIDBanPostRequest: AdminUsersUserIDBanPostRequest = TODO() + val response: ResponseEntity = api.adminUsersUserIDBanPost(userID, adminUsersUserIDBanPostRequest) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminUsersUserIDDeleteDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminUsersUserIDDeleteDeleteTest() { + val userID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminUsersUserIDDeleteDelete(userID) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminUsersUserIDInventoryCardIDDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminUsersUserIDInventoryCardIDDeleteTest() { + val userID: kotlin.Int = TODO() + val cardID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminUsersUserIDInventoryCardIDDelete(userID, cardID) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminUsersUserIDQuestsQuestIDResetPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminUsersUserIDQuestsQuestIDResetPostTest() { + val userID: kotlin.Int = TODO() + val questID: kotlin.Int = TODO() + val adminUsersUserIDQuestsQuestIDResetPostRequest: AdminUsersUserIDQuestsQuestIDResetPostRequest = TODO() + val response: ResponseEntity = api.adminUsersUserIDQuestsQuestIDResetPost(userID, questID, adminUsersUserIDQuestsQuestIDResetPostRequest) + + // TODO: test validations + } + + /** + * To test AdminApiController.adminUsersUserIDUnbanPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun adminUsersUserIDUnbanPostTest() { + val userID: kotlin.Int = TODO() + val response: ResponseEntity = api.adminUsersUserIDUnbanPost(userID) + + // TODO: test validations + } +} diff --git a/backend/src/test/kotlin/ru/vsu/app/api/AuthApiTest.kt b/backend/src/test/kotlin/ru/vsu/app/api/AuthApiTest.kt new file mode 100644 index 0000000..d5b5b90 --- /dev/null +++ b/backend/src/test/kotlin/ru/vsu/app/api/AuthApiTest.kt @@ -0,0 +1,154 @@ +package org.openapitools.api + +import org.openapitools.model.AuthCheckGet401Response +import org.openapitools.model.LoginUser400Response +import org.openapitools.model.LoginUser401Response +import org.openapitools.model.LoginUserRequest +import org.openapitools.model.RegisterUser200Response +import org.openapitools.model.RegisterUser400Response +import org.openapitools.model.RegisterUser409Response +import org.openapitools.model.InternalServerError +import org.openapitools.model.RegisterUserRequest +import org.openapitools.model.RequestPasswordReset200Response +import org.openapitools.model.RequestPasswordReset400Response +import org.openapitools.model.RequestPasswordReset404Response +import org.openapitools.model.RequestPasswordResetRequest +import org.openapitools.model.ResendVerificationCode200Response +import org.openapitools.model.ResendVerificationCodeRequest +import org.openapitools.model.ResetPassword200Response +import org.openapitools.model.ResetPassword400Response +import org.openapitools.model.ResetPasswordRequest +import org.openapitools.model.User +import org.openapitools.model.VerifyEmail400Response +import org.openapitools.model.VerifyEmail410Response +import org.openapitools.model.VerifyEmailRequest +import org.junit.jupiter.api.Test +import org.springframework.http.ResponseEntity + +class AuthApiTest { + + private val api: AuthApiController = AuthApiController() + + /** + * To test AuthApiController.authCheckGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun authCheckGetTest() { + val response: ResponseEntity = api.authCheckGet() + + // TODO: test validations + } + + /** + * To test AuthApiController.authLogoutPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun authLogoutPostTest() { + val response: ResponseEntity = api.authLogoutPost() + + // TODO: test validations + } + + /** + * To test AuthApiController.authRefreshPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun authRefreshPostTest() { + val response: ResponseEntity = api.authRefreshPost() + + // TODO: test validations + } + + /** + * To test AuthApiController.loginUser + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun loginUserTest() { + val loginUserRequest: LoginUserRequest = TODO() + val response: ResponseEntity = api.loginUser(loginUserRequest) + + // TODO: test validations + } + + /** + * To test AuthApiController.registerUser + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun registerUserTest() { + val registerUserRequest: RegisterUserRequest = TODO() + val response: ResponseEntity = api.registerUser(registerUserRequest) + + // TODO: test validations + } + + /** + * To test AuthApiController.requestPasswordReset + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun requestPasswordResetTest() { + val requestPasswordResetRequest: RequestPasswordResetRequest = TODO() + val response: ResponseEntity = api.requestPasswordReset(requestPasswordResetRequest) + + // TODO: test validations + } + + /** + * To test AuthApiController.resendVerificationCode + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun resendVerificationCodeTest() { + val resendVerificationCodeRequest: ResendVerificationCodeRequest = TODO() + val response: ResponseEntity = api.resendVerificationCode(resendVerificationCodeRequest) + + // TODO: test validations + } + + /** + * To test AuthApiController.resetPassword + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun resetPasswordTest() { + val resetPasswordRequest: ResetPasswordRequest = TODO() + val response: ResponseEntity = api.resetPassword(resetPasswordRequest) + + // TODO: test validations + } + + /** + * To test AuthApiController.verifyEmail + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun verifyEmailTest() { + val verifyEmailRequest: VerifyEmailRequest = TODO() + val response: ResponseEntity = api.verifyEmail(verifyEmailRequest) + + // TODO: test validations + } +} diff --git a/backend/src/test/kotlin/ru/vsu/app/api/HomeApiTest.kt b/backend/src/test/kotlin/ru/vsu/app/api/HomeApiTest.kt new file mode 100644 index 0000000..1c28168 --- /dev/null +++ b/backend/src/test/kotlin/ru/vsu/app/api/HomeApiTest.kt @@ -0,0 +1,177 @@ +package org.openapitools.api + +import org.openapitools.model.HomeGenerateCardPost200Response +import org.openapitools.model.HomeGenerateCardPost402Response +import org.openapitools.model.HomeGenerateCardPostRequest +import org.openapitools.model.HomeNewsGet404Response +import org.openapitools.model.HomeNotificationsNotificationIDNavigatePost200Response +import org.openapitools.model.HomeNotificationsNotificationIDNavigatePost404Response +import org.openapitools.model.HomeQuestsClaimRewardPost200Response +import org.openapitools.model.HomeQuestsClaimRewardPost400Response +import org.openapitools.model.HomeQuestsClaimRewardPostRequest +import org.openapitools.model.HomeQuestsGet200Response +import org.openapitools.model.HomeQuestsQuestIDChangeStatusPost200Response +import org.openapitools.model.HomeSearchGet400Response +import org.openapitools.model.HomeSearchGet404Response +import org.openapitools.model.News +import org.openapitools.model.Notification +import org.openapitools.model.InternalServerError +import org.openapitools.model.Theme +import org.openapitools.model.User +import org.junit.jupiter.api.Test +import org.springframework.http.ResponseEntity + +class HomeApiTest { + + private val api: HomeApiController = HomeApiController() + + /** + * To test HomeApiController.homeGenerateCardPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeGenerateCardPostTest() { + val homeGenerateCardPostRequest: HomeGenerateCardPostRequest = TODO() + val response: ResponseEntity = api.homeGenerateCardPost(homeGenerateCardPostRequest) + + // TODO: test validations + } + + /** + * To test HomeApiController.homeGenerateCardThemesGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeGenerateCardThemesGetTest() { + val response: ResponseEntity = api.homeGenerateCardThemesGet() + + // TODO: test validations + } + + /** + * To test HomeApiController.homeNewsGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeNewsGetTest() { + val response: ResponseEntity = api.homeNewsGet() + + // TODO: test validations + } + + /** + * To test HomeApiController.homeNewsNewsIDGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeNewsNewsIDGetTest() { + val newsID: kotlin.Int = TODO() + val response: ResponseEntity = api.homeNewsNewsIDGet(newsID) + + // TODO: test validations + } + + /** + * To test HomeApiController.homeNotificationsGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeNotificationsGetTest() { + val response: ResponseEntity = api.homeNotificationsGet() + + // TODO: test validations + } + + /** + * To test HomeApiController.homeNotificationsNotificationIDGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeNotificationsNotificationIDGetTest() { + val notificationID: kotlin.Int = TODO() + val response: ResponseEntity = api.homeNotificationsNotificationIDGet(notificationID) + + // TODO: test validations + } + + /** + * To test HomeApiController.homeNotificationsNotificationIDNavigatePost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeNotificationsNotificationIDNavigatePostTest() { + val notificationID: kotlin.Int = TODO() + val response: ResponseEntity = api.homeNotificationsNotificationIDNavigatePost(notificationID) + + // TODO: test validations + } + + /** + * To test HomeApiController.homeQuestsClaimRewardPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeQuestsClaimRewardPostTest() { + val homeQuestsClaimRewardPostRequest: HomeQuestsClaimRewardPostRequest = TODO() + val response: ResponseEntity = api.homeQuestsClaimRewardPost(homeQuestsClaimRewardPostRequest) + + // TODO: test validations + } + + /** + * To test HomeApiController.homeQuestsGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeQuestsGetTest() { + val response: ResponseEntity = api.homeQuestsGet() + + // TODO: test validations + } + + /** + * To test HomeApiController.homeQuestsQuestIDChangeStatusPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeQuestsQuestIDChangeStatusPostTest() { + val questID: kotlin.Int = TODO() + val response: ResponseEntity = api.homeQuestsQuestIDChangeStatusPost(questID) + + // TODO: test validations + } + + /** + * To test HomeApiController.homeSearchGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun homeSearchGetTest() { + val query: kotlin.String? = TODO() + val response: ResponseEntity = api.homeSearchGet(query) + + // TODO: test validations + } +} diff --git a/backend/src/test/kotlin/ru/vsu/app/api/InventoryApiTest.kt b/backend/src/test/kotlin/ru/vsu/app/api/InventoryApiTest.kt new file mode 100644 index 0000000..35b405b --- /dev/null +++ b/backend/src/test/kotlin/ru/vsu/app/api/InventoryApiTest.kt @@ -0,0 +1,173 @@ +package org.openapitools.api + +import org.openapitools.model.InventoryCardCardIDFavoriteGet200Response +import org.openapitools.model.InventoryCardCardIDQuantityGet200Response +import org.openapitools.model.InventoryCardCardIDTradeCancelPost200Response +import org.openapitools.model.InventoryCardCardIDTradeStatusGet200Response +import org.openapitools.model.InventoryDestroyPost200Response +import org.openapitools.model.InventoryDestroyPostRequest +import org.openapitools.model.InventoryFavoritesCountGet200Response +import org.openapitools.model.InventoryGet200Response +import org.openapitools.model.InventorySortPost200Response +import org.openapitools.model.OtherProfileCardCardIDInitiateTradePost200Response +import org.openapitools.model.OtherProfileInventorySortPostRequest +import org.openapitools.model.InternalServerError +import org.junit.jupiter.api.Test +import org.springframework.http.ResponseEntity + +class InventoryApiTest { + + private val api: InventoryApiController = InventoryApiController() + + /** + * To test InventoryApiController.inventoryCardCardIDFavoriteAddPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryCardCardIDFavoriteAddPostTest() { + val cardID: kotlin.Int = TODO() + val response: ResponseEntity = api.inventoryCardCardIDFavoriteAddPost(cardID) + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventoryCardCardIDFavoriteDeleteDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryCardCardIDFavoriteDeleteDeleteTest() { + val cardID: kotlin.Int = TODO() + val response: ResponseEntity = api.inventoryCardCardIDFavoriteDeleteDelete(cardID) + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventoryCardCardIDFavoriteGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryCardCardIDFavoriteGetTest() { + val cardID: kotlin.Int = TODO() + val response: ResponseEntity = api.inventoryCardCardIDFavoriteGet(cardID) + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventoryCardCardIDQuantityGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryCardCardIDQuantityGetTest() { + val cardID: kotlin.Int = TODO() + val response: ResponseEntity = api.inventoryCardCardIDQuantityGet(cardID) + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventoryCardCardIDTradeCancelPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryCardCardIDTradeCancelPostTest() { + val cardID: kotlin.Int = TODO() + val response: ResponseEntity = api.inventoryCardCardIDTradeCancelPost(cardID) + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventoryCardCardIDTradeStatusGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryCardCardIDTradeStatusGetTest() { + val cardID: kotlin.Int = TODO() + val response: ResponseEntity = api.inventoryCardCardIDTradeStatusGet(cardID) + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventoryDestroyPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryDestroyPostTest() { + val inventoryDestroyPostRequest: InventoryDestroyPostRequest = TODO() + val response: ResponseEntity = api.inventoryDestroyPost(inventoryDestroyPostRequest) + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventoryFavoritesCountGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryFavoritesCountGetTest() { + val response: ResponseEntity = api.inventoryFavoritesCountGet() + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventoryGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryGetTest() { + val response: ResponseEntity = api.inventoryGet() + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventoryPutOnTradePost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventoryPutOnTradePostTest() { + val inventoryDestroyPostRequest: InventoryDestroyPostRequest = TODO() + val response: ResponseEntity = api.inventoryPutOnTradePost(inventoryDestroyPostRequest) + + // TODO: test validations + } + + /** + * To test InventoryApiController.inventorySortPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun inventorySortPostTest() { + val otherProfileInventorySortPostRequest: OtherProfileInventorySortPostRequest = TODO() + val response: ResponseEntity = api.inventorySortPost(otherProfileInventorySortPostRequest) + + // TODO: test validations + } +} diff --git a/backend/src/test/kotlin/ru/vsu/app/api/OtherProfileApiTest.kt b/backend/src/test/kotlin/ru/vsu/app/api/OtherProfileApiTest.kt new file mode 100644 index 0000000..3f1515f --- /dev/null +++ b/backend/src/test/kotlin/ru/vsu/app/api/OtherProfileApiTest.kt @@ -0,0 +1,124 @@ +package org.openapitools.api + +import org.openapitools.model.OtherProfileCardCardIDInitiateTradePost200Response +import org.openapitools.model.OtherProfileCardCardIDInitiateTradePost400Response +import org.openapitools.model.OtherProfileCardCardIDInitiateTradePost409Response +import org.openapitools.model.OtherProfileCardCardIDViewGet200Response +import org.openapitools.model.OtherProfileInventorySortPost200Response +import org.openapitools.model.OtherProfileInventorySortPostRequest +import org.openapitools.model.OtherProfileUserIDGet200Response +import org.openapitools.model.OtherProfileUserIDInventoryGet200Response +import org.openapitools.model.OtherProfileUserIDInventoryGet403Response +import org.openapitools.model.OtherProfileUserIDReportPost200Response +import org.openapitools.model.OtherProfileUserIDReportPostRequest +import org.openapitools.model.ProfileAchievementsGet200Response +import org.openapitools.model.InternalServerError +import org.junit.jupiter.api.Test +import org.springframework.http.ResponseEntity + +class OtherProfileApiTest { + + private val api: OtherProfileApiController = OtherProfileApiController() + + /** + * To test OtherProfileApiController.otherProfileCardCardIDInitiateTradePost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun otherProfileCardCardIDInitiateTradePostTest() { + val cardID: kotlin.Int = TODO() + val ownerID: kotlin.Int = TODO() + val response: ResponseEntity = api.otherProfileCardCardIDInitiateTradePost(cardID, ownerID) + + // TODO: test validations + } + + /** + * To test OtherProfileApiController.otherProfileCardCardIDViewGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun otherProfileCardCardIDViewGetTest() { + val cardID: kotlin.Int = TODO() + val ownerID: kotlin.Int = TODO() + val response: ResponseEntity = api.otherProfileCardCardIDViewGet(cardID, ownerID) + + // TODO: test validations + } + + /** + * To test OtherProfileApiController.otherProfileInventorySortPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun otherProfileInventorySortPostTest() { + val userID: kotlin.Int = TODO() + val otherProfileInventorySortPostRequest: OtherProfileInventorySortPostRequest = TODO() + val response: ResponseEntity = api.otherProfileInventorySortPost(userID, otherProfileInventorySortPostRequest) + + // TODO: test validations + } + + /** + * To test OtherProfileApiController.otherProfileUserIDAchievementsGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun otherProfileUserIDAchievementsGetTest() { + val userID: kotlin.Int = TODO() + val response: ResponseEntity = api.otherProfileUserIDAchievementsGet(userID) + + // TODO: test validations + } + + /** + * To test OtherProfileApiController.otherProfileUserIDGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun otherProfileUserIDGetTest() { + val userID: kotlin.Int = TODO() + val response: ResponseEntity = api.otherProfileUserIDGet(userID) + + // TODO: test validations + } + + /** + * To test OtherProfileApiController.otherProfileUserIDInventoryGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun otherProfileUserIDInventoryGetTest() { + val userID: kotlin.Int = TODO() + val response: ResponseEntity = api.otherProfileUserIDInventoryGet(userID) + + // TODO: test validations + } + + /** + * To test OtherProfileApiController.otherProfileUserIDReportPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun otherProfileUserIDReportPostTest() { + val userID: kotlin.Int = TODO() + val otherProfileUserIDReportPostRequest: OtherProfileUserIDReportPostRequest = TODO() + val response: ResponseEntity = api.otherProfileUserIDReportPost(userID, otherProfileUserIDReportPostRequest) + + // TODO: test validations + } +} diff --git a/backend/src/test/kotlin/ru/vsu/app/api/ProfileApiTest.kt b/backend/src/test/kotlin/ru/vsu/app/api/ProfileApiTest.kt new file mode 100644 index 0000000..c305c87 --- /dev/null +++ b/backend/src/test/kotlin/ru/vsu/app/api/ProfileApiTest.kt @@ -0,0 +1,108 @@ +package org.openapitools.api + +import org.openapitools.model.ProfileAchievementsFavoritesCountGet200Response +import org.openapitools.model.ProfileAchievementsGet200Response +import org.openapitools.model.ProfileGet200Response +import org.openapitools.model.InternalServerError +import org.openapitools.model.UserSettings +import org.junit.jupiter.api.Test +import org.springframework.http.ResponseEntity + +class ProfileApiTest { + + private val api: ProfileApiController = ProfileApiController() + + /** + * To test ProfileApiController.profileAchievementsAchievementIDFavoriteAddPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun profileAchievementsAchievementIDFavoriteAddPostTest() { + val achievementID: kotlin.Int = TODO() + val response: ResponseEntity = api.profileAchievementsAchievementIDFavoriteAddPost(achievementID) + + // TODO: test validations + } + + /** + * To test ProfileApiController.profileAchievementsAchievementIDFavoriteDeleteDelete + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun profileAchievementsAchievementIDFavoriteDeleteDeleteTest() { + val achievementID: kotlin.Int = TODO() + val response: ResponseEntity = api.profileAchievementsAchievementIDFavoriteDeleteDelete(achievementID) + + // TODO: test validations + } + + /** + * To test ProfileApiController.profileAchievementsFavoritesCountGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun profileAchievementsFavoritesCountGetTest() { + val response: ResponseEntity = api.profileAchievementsFavoritesCountGet() + + // TODO: test validations + } + + /** + * To test ProfileApiController.profileAchievementsGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun profileAchievementsGetTest() { + val response: ResponseEntity = api.profileAchievementsGet() + + // TODO: test validations + } + + /** + * To test ProfileApiController.profileGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun profileGetTest() { + val response: ResponseEntity = api.profileGet() + + // TODO: test validations + } + + /** + * To test ProfileApiController.profileSettingsChangePut + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun profileSettingsChangePutTest() { + val userSettings: UserSettings = TODO() + val response: ResponseEntity = api.profileSettingsChangePut(userSettings) + + // TODO: test validations + } + + /** + * To test ProfileApiController.profileSettingsGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun profileSettingsGetTest() { + val response: ResponseEntity = api.profileSettingsGet() + + // TODO: test validations + } +} diff --git a/backend/src/test/kotlin/ru/vsu/app/api/ShopApiTest.kt b/backend/src/test/kotlin/ru/vsu/app/api/ShopApiTest.kt new file mode 100644 index 0000000..4e1635f --- /dev/null +++ b/backend/src/test/kotlin/ru/vsu/app/api/ShopApiTest.kt @@ -0,0 +1,130 @@ +package org.openapitools.api + +import org.openapitools.model.CoinOffer +import org.openapitools.model.Pack +import org.openapitools.model.PaymentCallback +import org.openapitools.model.InternalServerError +import org.openapitools.model.ShopCoinsOffersOfferIdPurchasePost200Response +import org.openapitools.model.ShopCoinsOffersOfferIdPurchasePostRequest +import org.openapitools.model.ShopPacksPackIdBuyPost200Response +import org.openapitools.model.ShopPacksPackIdBuyPost402Response +import org.openapitools.model.ShopPacksPackIdOpenPost200Response +import org.openapitools.model.ShopPaymentsProcessPost200Response +import org.junit.jupiter.api.Test +import org.springframework.http.ResponseEntity + +class ShopApiTest { + + private val api: ShopApiController = ShopApiController() + + /** + * To test ShopApiController.shopCoinsOffersGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun shopCoinsOffersGetTest() { + val response: ResponseEntity = api.shopCoinsOffersGet() + + // TODO: test validations + } + + /** + * To test ShopApiController.shopCoinsOffersOfferIdGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun shopCoinsOffersOfferIdGetTest() { + val offerId: kotlin.Int = TODO() + val response: ResponseEntity = api.shopCoinsOffersOfferIdGet(offerId) + + // TODO: test validations + } + + /** + * To test ShopApiController.shopCoinsOffersOfferIdPurchasePost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun shopCoinsOffersOfferIdPurchasePostTest() { + val offerId: kotlin.Int = TODO() + val shopCoinsOffersOfferIdPurchasePostRequest: ShopCoinsOffersOfferIdPurchasePostRequest = TODO() + val response: ResponseEntity = api.shopCoinsOffersOfferIdPurchasePost(offerId, shopCoinsOffersOfferIdPurchasePostRequest) + + // TODO: test validations + } + + /** + * To test ShopApiController.shopPacksGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun shopPacksGetTest() { + val response: ResponseEntity = api.shopPacksGet() + + // TODO: test validations + } + + /** + * To test ShopApiController.shopPacksPackIdBuyPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun shopPacksPackIdBuyPostTest() { + val packId: kotlin.Int = TODO() + val response: ResponseEntity = api.shopPacksPackIdBuyPost(packId) + + // TODO: test validations + } + + /** + * To test ShopApiController.shopPacksPackIdGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun shopPacksPackIdGetTest() { + val packId: kotlin.Int = TODO() + val response: ResponseEntity = api.shopPacksPackIdGet(packId) + + // TODO: test validations + } + + /** + * To test ShopApiController.shopPacksPackIdOpenPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun shopPacksPackIdOpenPostTest() { + val packId: kotlin.Int = TODO() + val response: ResponseEntity = api.shopPacksPackIdOpenPost(packId) + + // TODO: test validations + } + + /** + * To test ShopApiController.shopPaymentsProcessPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun shopPaymentsProcessPostTest() { + val paymentCallback: PaymentCallback = TODO() + val response: ResponseEntity = api.shopPaymentsProcessPost(paymentCallback) + + // TODO: test validations + } +} diff --git a/backend/src/test/kotlin/ru/vsu/app/api/TradesApiTest.kt b/backend/src/test/kotlin/ru/vsu/app/api/TradesApiTest.kt new file mode 100644 index 0000000..60b7502 --- /dev/null +++ b/backend/src/test/kotlin/ru/vsu/app/api/TradesApiTest.kt @@ -0,0 +1,116 @@ +package org.openapitools.api + +import org.openapitools.model.OtherProfileCardCardIDInitiateTradePost400Response +import org.openapitools.model.InternalServerError +import org.openapitools.model.Trade +import org.openapitools.model.TradesInitiatePostRequest +import org.openapitools.model.TradesTradeIdAcceptPost200Response +import org.openapitools.model.TradesTradeIdAcceptPost403Response +import org.openapitools.model.TradesTradeIdCancelPost200Response +import org.openapitools.model.TradesTradeIdGet404Response +import org.openapitools.model.TradesTradeIdRejectPost200Response +import org.junit.jupiter.api.Test +import org.springframework.http.ResponseEntity + +class TradesApiTest { + + private val api: TradesApiController = TradesApiController() + + /** + * To test TradesApiController.tradesGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun tradesGetTest() { + val search: kotlin.String? = TODO() + val response: ResponseEntity = api.tradesGet(search) + + // TODO: test validations + } + + /** + * To test TradesApiController.tradesInitiatePost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun tradesInitiatePostTest() { + val tradesInitiatePostRequest: TradesInitiatePostRequest = TODO() + val response: ResponseEntity = api.tradesInitiatePost(tradesInitiatePostRequest) + + // TODO: test validations + } + + /** + * To test TradesApiController.tradesMyGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun tradesMyGetTest() { + val userId: kotlin.Int = TODO() + val response: ResponseEntity = api.tradesMyGet(userId) + + // TODO: test validations + } + + /** + * To test TradesApiController.tradesTradeIdAcceptPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun tradesTradeIdAcceptPostTest() { + val tradeId: kotlin.Int = TODO() + val response: ResponseEntity = api.tradesTradeIdAcceptPost(tradeId) + + // TODO: test validations + } + + /** + * To test TradesApiController.tradesTradeIdCancelPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun tradesTradeIdCancelPostTest() { + val tradeId: kotlin.Int = TODO() + val response: ResponseEntity = api.tradesTradeIdCancelPost(tradeId) + + // TODO: test validations + } + + /** + * To test TradesApiController.tradesTradeIdGet + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun tradesTradeIdGetTest() { + val tradeId: kotlin.Int = TODO() + val response: ResponseEntity = api.tradesTradeIdGet(tradeId) + + // TODO: test validations + } + + /** + * To test TradesApiController.tradesTradeIdRejectPost + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun tradesTradeIdRejectPostTest() { + val tradeId: kotlin.Int = TODO() + val response: ResponseEntity = api.tradesTradeIdRejectPost(tradeId) + + // TODO: test validations + } +} diff --git "a/frontend/assets/icons/\320\264\320\276\321\201\321\202\320\270\320\266\320\265\320\275\320\270\320\265.png" "b/frontend/assets/icons/\320\264\320\276\321\201\321\202\320\270\320\266\320\265\320\275\320\270\320\265.png" new file mode 100644 index 0000000..fbc08f4 Binary files /dev/null and "b/frontend/assets/icons/\320\264\320\276\321\201\321\202\320\270\320\266\320\265\320\275\320\270\320\265.png" differ diff --git "a/frontend/assets/icons/\320\266\320\260\320\273\320\276\320\261\320\260.png" "b/frontend/assets/icons/\320\266\320\260\320\273\320\276\320\261\320\260.png" new file mode 100644 index 0000000..46bc450 Binary files /dev/null and "b/frontend/assets/icons/\320\266\320\260\320\273\320\276\320\261\320\260.png" differ diff --git a/frontend/lib/controllers/auth_controller.dart b/frontend/lib/controllers/auth_controller.dart index 5b4ee60..83aa5ec 100644 --- a/frontend/lib/controllers/auth_controller.dart +++ b/frontend/lib/controllers/auth_controller.dart @@ -2,18 +2,21 @@ import 'package:flutter/material.dart'; import '../models/user_model.dart'; import '../services/api_service.dart'; import '../views/home_screen.dart'; +import '../views/email_verification_screen.dart'; +import '../main.dart'; class AuthController with ChangeNotifier { UserModel? _currentUser; bool _isLoading = false; String? _token; + String? _tempToken; final ApiService _apiService = ApiService(); UserModel? get currentUser => _currentUser; bool get isLoading => _isLoading; bool get isAuthenticated => _token != null; - static const String _baseUrl = 'http://87.236.23.130:8080/api'; + static const String _baseUrl = 'http://217.114.7.39:8080/api'; static const String _loginUrl = '$_baseUrl/auth/login'; static const String loginError = 'Неверный логин или пароль'; @@ -54,7 +57,7 @@ class AuthController with ChangeNotifier { if (context != null) { Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (context) => const HomeScreen()), + MaterialPageRoute(builder: (context) => const MainScreen()), ); } } catch (e) { @@ -71,21 +74,28 @@ class AuthController with ChangeNotifier { notifyListeners(); try { - final success = await _apiService.register(email, username, password); + final response = await _apiService.register(email, username, password); - if (success) { - _currentUser = UserModel( - email: email, - username: username, - isEmailVerified: false, - ); - } + _tempToken = response['tempToken']; + await _apiService.saveToken(_tempToken!); + _currentUser = UserModel( + email: email, + username: username, + isEmailVerified: false, + ); _isLoading = false; notifyListeners(); - if (context != null && success) { - + if (context != null && _tempToken != null) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => EmailVerificationScreen( + email: email, + tempToken: _tempToken!, + ), + ), + ); } } catch (e) { _isLoading = false; @@ -96,34 +106,35 @@ class AuthController with ChangeNotifier { } } - Future verifyEmail(String email, String code, {BuildContext? context}) async { + Future verifyEmail(String tempToken, String code, {BuildContext? context}) async { _isLoading = true; notifyListeners(); try { - final success = await _apiService.activateAccount(email, code); + final userModel = await _apiService.activateAccount(tempToken, code); - if (success && _currentUser != null) { - _currentUser = UserModel( - email: _currentUser!.email, - username: _currentUser!.username, - token: _currentUser!.token, - isEmailVerified: true, - ); + _currentUser = userModel; + _token = userModel.token; + if (_token != null) { + await _apiService.saveToken(_token!); + } + _tempToken = null; + await _apiService.removeToken(); + if (_token != null) { + await _apiService.saveToken(_token!); } _isLoading = false; notifyListeners(); - if (context != null && success) { - + if (context != null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Email успешно подтвержден')), ); - if (_token == null) { - Navigator.of(context).popUntil((route) => route.isFirst); - } + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => const MainScreen()), + ); } } catch (e) { _isLoading = false; @@ -245,4 +256,21 @@ class AuthController with ChangeNotifier { throw Exception(errorMessage); } } + + Future loginAsGuest({BuildContext? context}) async { + _currentUser = UserModel( + email: 'guest@cardly.com', + username: 'Гость', + isEmailVerified: true, + isGuest: true, + ); + + notifyListeners(); + + if (context != null) { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => const MainScreen()), + ); + } + } } \ No newline at end of file diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 80b9e26..3d119ca 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -1,13 +1,22 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'controllers/auth_controller.dart'; +import 'services/analytics_service.dart'; import 'views/splash_screen.dart'; import 'views/auth_screen.dart'; import 'views/email_verification_screen.dart'; import 'views/forgot_password_screen.dart'; import 'views/change_password_screen.dart'; +import 'views/home_screen.dart'; +import 'views/inventory_screen.dart'; +import 'views/shop_screen.dart'; +import 'views/exchanges_screen.dart'; +import 'utils/auth_utils.dart'; +import 'views/authorization_dialog.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await AnalyticsService.initialize(); runApp(const MyApp()); } @@ -79,7 +88,22 @@ class MyApp extends StatelessWidget { elevation: 0, ), ), - home: const SplashScreen(), + home: Builder( + builder: (context) { + return FutureBuilder( + future: Provider.of(context, listen: false).tryAutoLogin(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const SplashScreen(); + } + if (snapshot.hasError) { + return const AuthScreen(); + } + return snapshot.data == true ? const MainScreen() : const AuthScreen(); + }, + ); + }, + ), routes: { '/auth': (context) => const AuthScreen(), '/login': (context) => const AuthScreen(initialTabIndex: 0), @@ -91,3 +115,204 @@ class MyApp extends StatelessWidget { ); } } + +class MainScreen extends StatefulWidget { + final int? initialIndex; + final String? notification; + + const MainScreen({ + super.key, + this.initialIndex, + this.notification, + }); + + @override + State createState() => _MainScreenState(); +} + +class _MainScreenState extends State { + late int _currentIndex; + final PageController _pageController = PageController(); + + @override + void initState() { + super.initState(); + _currentIndex = widget.initialIndex ?? 0; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _pageController.jumpToPage(_currentIndex); + // Track initial screen view + AnalyticsService.trackScreenView(_screenNames[_currentIndex]); + if (widget.notification != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Container( + width: 367, + height: 61, + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(15.0), + ), + padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), + child: Center( + child: Text( + widget.notification!, + style: const TextStyle( + color: Colors.black, + fontSize: 18.0, + fontWeight: FontWeight.w500, + fontFamily: 'Jost', + ), + textAlign: TextAlign.center, + ), + ), + ), + backgroundColor: Colors.transparent, + duration: const Duration(seconds: 1), + behavior: SnackBarBehavior.floating, + margin: EdgeInsets.only( + bottom: MediaQuery.of(context).size.height - 200, + left: 16, + right: 16, + ), + elevation: 0, + ), + ); + } + } + }); + } + + final List _screenNames = [ + 'home_screen', + 'inventory_screen', + 'shop_screen', + 'exchanges_screen', + ]; + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + void _onItemTapped(int index) { + final authController = Provider.of(context, listen: false); + if (authController.currentUser?.isGuest ?? false) { + if (!AuthUtils.isGuestAllowedScreen(_screenNames[index])) { + showDialog( + context: context, + barrierDismissible: true, + builder: (context) => const AuthorizationDialog(), + ); + return; + } + } + + setState(() { + _currentIndex = index; + }); + _pageController.jumpToPage(index); + // Track screen view when user navigates + AnalyticsService.trackScreenView(_screenNames[index]); + } + + @override + Widget build(BuildContext context) { + final List screens = [ + const HomeScreen(), + const InventoryScreen(), + const ShopScreen(), + ExchangesScreen(initialTabIndex: _currentIndex == 3 ? 0 : null), + ]; + + return Scaffold( + body: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + children: screens, + ), + bottomNavigationBar: Container( + decoration: const BoxDecoration( + color: Color(0xFFD6A067), + ), + child: BottomNavigationBar( + currentIndex: _currentIndex, + onTap: _onItemTapped, + backgroundColor: Colors.transparent, + elevation: 0, + type: BottomNavigationBarType.fixed, + selectedItemColor: Colors.black, + unselectedItemColor: Colors.black54, + showSelectedLabels: true, + showUnselectedLabels: true, + selectedIconTheme: const IconThemeData( + size: 28, + ), + unselectedIconTheme: const IconThemeData( + size: 24, + ), + selectedLabelStyle: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + fontFamily: 'Jost', + ), + unselectedLabelStyle: const TextStyle( + fontSize: 11, + fontFamily: 'Jost', + ), + items: [ + BottomNavigationBarItem( + icon: Image.asset('assets/icons/главная.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/главная.png', height: 24), + ), + label: 'Главная', + ), + BottomNavigationBarItem( + icon: Image.asset('assets/icons/Инвентарь.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/Инвентарь.png', height: 24), + ), + label: 'Инвентарь', + ), + BottomNavigationBarItem( + icon: Image.asset('assets/icons/магазин.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/магазин.png', height: 24), + ), + label: 'Магазин', + ), + BottomNavigationBarItem( + icon: Image.asset('assets/icons/обменник.png', height: 24), + activeIcon: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDD6B0), + borderRadius: BorderRadius.circular(8), + ), + child: Image.asset('assets/icons/обменник.png', height: 24), + ), + label: 'Обменник', + ), + ], + ), + ), + ); + } +} diff --git a/frontend/lib/models/achievement_model.dart b/frontend/lib/models/achievement_model.dart new file mode 100644 index 0000000..d713c5e --- /dev/null +++ b/frontend/lib/models/achievement_model.dart @@ -0,0 +1,47 @@ +class Achievement { + final int achievement_ID; + final String name; + final String description; + final String imageURL; + final int progress; + final int maxProgress; + final bool isCompleted; + final bool isFavorite; + + Achievement({ + required this.achievement_ID, + required this.name, + required this.description, + required this.imageURL, + required this.progress, + required this.maxProgress, + required this.isCompleted, + required this.isFavorite, + }); + + factory Achievement.fromJson(Map json) { + return Achievement( + achievement_ID: json['achievement_ID'], + name: json['name'], + description: json['description'], + imageURL: json['imageURL'], + progress: json['progress'], + maxProgress: json['maxProgress'], + isCompleted: json['isCompleted'], + isFavorite: json['isFavorite'], + ); + } + + Map toJson() { + return { + 'achievement_ID': achievement_ID, + 'name': name, + 'description': description, + 'imageURL': imageURL, + 'progress': progress, + 'maxProgress': maxProgress, + 'isCompleted': isCompleted, + 'isFavorite': isFavorite, + }; + } +} \ No newline at end of file diff --git a/frontend/lib/models/card_model.dart b/frontend/lib/models/card_model.dart new file mode 100644 index 0000000..578cccc --- /dev/null +++ b/frontend/lib/models/card_model.dart @@ -0,0 +1,43 @@ +class CardModel { + final int card_ID; + final String name; + final String description; + final String imageURL; + final String rarity; + final bool isInCollection; + final DateTime dateObtained; + + CardModel({ + required this.card_ID, + required this.name, + required this.description, + required this.imageURL, + required this.rarity, + this.isInCollection = false, + required this.dateObtained, + }); + + factory CardModel.fromJson(Map json) { + return CardModel( + card_ID: json['card_ID'], + name: json['name'], + description: json['description'], + imageURL: json['imageURL'], + rarity: json['rarity'], + isInCollection: json['isInCollection'] ?? false, + dateObtained: DateTime.parse(json['dateObtained']), + ); + } + + Map toJson() { + return { + 'card_ID': card_ID, + 'name': name, + 'description': description, + 'imageURL': imageURL, + 'rarity': rarity, + 'isInCollection': isInCollection, + 'dateObtained': dateObtained.toIso8601String(), + }; + } +} \ No newline at end of file diff --git a/frontend/lib/models/news_model.dart b/frontend/lib/models/news_model.dart new file mode 100644 index 0000000..f3ff519 --- /dev/null +++ b/frontend/lib/models/news_model.dart @@ -0,0 +1,35 @@ +class News { + final int news_ID; + final String title; + final String content; + final String imageURL; + final DateTime publishDate; + + News({ + required this.news_ID, + required this.title, + required this.content, + required this.imageURL, + required this.publishDate, + }); + + factory News.fromJson(Map json) { + return News( + news_ID: json['news_ID'], + title: json['title'], + content: json['content'], + imageURL: json['imageURL'], + publishDate: DateTime.parse(json['publishDate']), + ); + } + + Map toJson() { + return { + 'news_ID': news_ID, + 'title': title, + 'content': content, + 'imageURL': imageURL, + 'publishDate': publishDate.toIso8601String(), + }; + } +} \ No newline at end of file diff --git a/frontend/lib/models/notification_model.dart b/frontend/lib/models/notification_model.dart new file mode 100644 index 0000000..5f367ec --- /dev/null +++ b/frontend/lib/models/notification_model.dart @@ -0,0 +1,43 @@ +class Notification { + final int notification_ID; + final String title; + final String message; + final String type; + final DateTime timestamp; + final bool isRead; + final String? redirectURL; + + Notification({ + required this.notification_ID, + required this.title, + required this.message, + required this.type, + required this.timestamp, + required this.isRead, + this.redirectURL, + }); + + factory Notification.fromJson(Map json) { + return Notification( + notification_ID: json['notification_ID'], + title: json['title'], + message: json['message'], + type: json['type'], + timestamp: DateTime.parse(json['timestamp']), + isRead: json['isRead'], + redirectURL: json['redirectURL'], + ); + } + + Map toJson() { + return { + 'notification_ID': notification_ID, + 'title': title, + 'message': message, + 'type': type, + 'timestamp': timestamp.toIso8601String(), + 'isRead': isRead, + 'redirectURL': redirectURL, + }; + } +} \ No newline at end of file diff --git a/frontend/lib/models/quest_model.dart b/frontend/lib/models/quest_model.dart new file mode 100644 index 0000000..365d536 --- /dev/null +++ b/frontend/lib/models/quest_model.dart @@ -0,0 +1,47 @@ +class Quest { + final int quest_ID; + final String title; + final String description; + final String type; // 'daily' или 'weekly' + final int progress; + final int maxProgress; + final int reward; + final bool isCompleted; + + Quest({ + required this.quest_ID, + required this.title, + required this.description, + required this.type, + required this.progress, + required this.maxProgress, + required this.reward, + required this.isCompleted, + }); + + factory Quest.fromJson(Map json) { + return Quest( + quest_ID: json['quest_ID'], + title: json['title'], + description: json['description'], + type: json['type'], + progress: json['progress'], + maxProgress: json['maxProgress'], + reward: json['reward'], + isCompleted: json['isCompleted'], + ); + } + + Map toJson() { + return { + 'quest_ID': quest_ID, + 'title': title, + 'description': description, + 'type': type, + 'progress': progress, + 'maxProgress': maxProgress, + 'reward': reward, + 'isCompleted': isCompleted, + }; + } +} \ No newline at end of file diff --git a/frontend/lib/models/shop_model.dart b/frontend/lib/models/shop_model.dart new file mode 100644 index 0000000..e42f2bb --- /dev/null +++ b/frontend/lib/models/shop_model.dart @@ -0,0 +1,192 @@ +import 'card_model.dart'; + +class PackModel { + final int id; + final String name; + final String imageUrl; + final int price; + final List cards; + + PackModel({ + required this.id, + required this.name, + required this.imageUrl, + required this.price, + required this.cards, + }); + + factory PackModel.fromJson(Map json) { + return PackModel( + id: json['id'] as int, + name: json['name'] as String, + imageUrl: json['imageUrl'] as String, + price: json['price'] as int, + cards: (json['cards'] as List) + .map((card) => CardModel.fromJson(card)) + .toList(), + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'imageUrl': imageUrl, + 'price': price, + 'cards': cards.map((card) => card.toJson()).toList(), + }; + } +} + +class CoinOfferModel { + final int id; + final String name; + final String? description; + final int coinsAmount; + final double price; + final String imageUrl; + + CoinOfferModel({ + required this.id, + required this.name, + this.description, + required this.coinsAmount, + required this.price, + required this.imageUrl, + }); + + factory CoinOfferModel.fromJson(Map json) { + return CoinOfferModel( + id: json['id'] as int, + name: json['name'] as String, + description: json['description'] as String?, + coinsAmount: json['coinsAmount'] as int, + price: json['price'] as double, + imageUrl: json['imageUrl'] as String, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'description': description, + 'coinsAmount': coinsAmount, + 'price': price, + 'imageUrl': imageUrl, + }; + } +} + +class PurchasePackResponse { + final List receivedCards; + final int newBalance; + + PurchasePackResponse({ + required this.receivedCards, + required this.newBalance, + }); + + factory PurchasePackResponse.fromJson(Map json) { + return PurchasePackResponse( + receivedCards: (json['receivedCards'] as List) + .map((card) => CardModel.fromJson(card)) + .toList(), + newBalance: json['newBalance'] as int, + ); + } +} + +class PurchaseCoinsResponse { + final String paymentUrl; + + PurchaseCoinsResponse({ + required this.paymentUrl, + }); + + factory PurchaseCoinsResponse.fromJson(Map json) { + return PurchaseCoinsResponse( + paymentUrl: json['paymentUrl'] as String, + ); + } +} + +class Pack { + final int pack_ID; + final String name; + final String description; + final String imageURL; + final int price; + final int cardsCount; + final bool isAvailable; + + Pack({ + required this.pack_ID, + required this.name, + required this.description, + required this.imageURL, + required this.price, + required this.cardsCount, + this.isAvailable = true, + }); + + factory Pack.fromJson(Map json) { + return Pack( + pack_ID: json['pack_ID'], + name: json['name'], + description: json['description'], + imageURL: json['imageURL'], + price: json['price'], + cardsCount: json['cardsCount'], + isAvailable: json['isAvailable'] ?? true, + ); + } + + Map toJson() { + return { + 'pack_ID': pack_ID, + 'name': name, + 'description': description, + 'imageURL': imageURL, + 'price': price, + 'cardsCount': cardsCount, + 'isAvailable': isAvailable, + }; + } +} + +class CoinOffer { + final int offer_ID; + final int coins; + final double price; + final bool isPopular; + final bool isBestValue; + + CoinOffer({ + required this.offer_ID, + required this.coins, + required this.price, + this.isPopular = false, + this.isBestValue = false, + }); + + factory CoinOffer.fromJson(Map json) { + return CoinOffer( + offer_ID: json['offer_ID'], + coins: json['coins'], + price: json['price'].toDouble(), + isPopular: json['isPopular'] ?? false, + isBestValue: json['isBestValue'] ?? false, + ); + } + + Map toJson() { + return { + 'offer_ID': offer_ID, + 'coins': coins, + 'price': price, + 'isPopular': isPopular, + 'isBestValue': isBestValue, + }; + } +} \ No newline at end of file diff --git a/frontend/lib/models/theme_model.dart b/frontend/lib/models/theme_model.dart new file mode 100644 index 0000000..fdfd04a --- /dev/null +++ b/frontend/lib/models/theme_model.dart @@ -0,0 +1,25 @@ +class Theme { + final int theme_ID; + final String name; + final String description; + final String imageURL; + final int cost; + + Theme({ + required this.theme_ID, + required this.name, + required this.description, + required this.imageURL, + required this.cost, + }); + + factory Theme.fromJson(Map json) { + return Theme( + theme_ID: json['theme_ID'], + name: json['name'], + description: json['description'], + imageURL: json['imageURL'], + cost: json['cost'], + ); + } +} \ No newline at end of file diff --git a/frontend/lib/models/trade_model.dart b/frontend/lib/models/trade_model.dart new file mode 100644 index 0000000..0d33106 --- /dev/null +++ b/frontend/lib/models/trade_model.dart @@ -0,0 +1,35 @@ +import 'card_model.dart'; + +class Trade { + final int trade_ID; + final int initiatorID; + final String initiatorUsername; + final CardModel offeredCard; + final List requestedCards; + final String status; // 'pending', 'accepted', 'rejected', 'cancelled' + final DateTime createdAt; + + Trade({ + required this.trade_ID, + required this.initiatorID, + required this.initiatorUsername, + required this.offeredCard, + required this.requestedCards, + required this.status, + required this.createdAt, + }); + + factory Trade.fromJson(Map json) { + return Trade( + trade_ID: json['trade_ID'], + initiatorID: json['initiatorID'], + initiatorUsername: json['initiatorUsername'], + offeredCard: CardModel.fromJson(json['offeredCard']), + requestedCards: (json['requestedCards'] as List) + .map((card) => CardModel.fromJson(card)) + .toList(), + status: json['status'], + createdAt: DateTime.parse(json['createdAt']), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/models/user_model.dart b/frontend/lib/models/user_model.dart index 2a0a450..4784648 100644 --- a/frontend/lib/models/user_model.dart +++ b/frontend/lib/models/user_model.dart @@ -3,12 +3,14 @@ class UserModel { final String? username; final String? token; final bool isEmailVerified; + final bool isGuest; UserModel({ required this.email, this.username, this.token, this.isEmailVerified = false, + this.isGuest = false, }); factory UserModel.fromJson(Map json) { @@ -17,6 +19,7 @@ class UserModel { username: json['username'], token: json['token'], isEmailVerified: true, // Если пользователь успешно вошел, считаем, что почта подтверждена + isGuest: json['isGuest'] ?? false, ); } @@ -26,6 +29,7 @@ class UserModel { 'username': username, 'token': token, 'isEmailVerified': isEmailVerified, + 'isGuest': isGuest, }; } } \ No newline at end of file diff --git a/frontend/lib/services/analytics_service.dart b/frontend/lib/services/analytics_service.dart new file mode 100644 index 0000000..3cecbb5 --- /dev/null +++ b/frontend/lib/services/analytics_service.dart @@ -0,0 +1,49 @@ +import 'package:appmetrica_plugin/appmetrica_plugin.dart'; + +class AnalyticsService { + static const String _apiKey = '9b0749c4-7f6c-4b03-a593-caef66f15f41'; + + static Future initialize() async { + await AppMetrica.activate(AppMetricaConfig(_apiKey)); + } + + static void trackScreenView(String screenName) { + AppMetrica.reportEvent('screen_view_$screenName'); + } + + static void trackCardGeneration() { + AppMetrica.reportEvent('card_generation_started'); + } + + static void trackCardGenerationComplete() { + AppMetrica.reportEvent('card_generation_complete'); + } + + static void trackExchange() { + AppMetrica.reportEvent('exchange_started'); + } + + static void trackExchangeComplete() { + AppMetrica.reportEvent('exchange_complete'); + } + + static void reportError(String errorName, Object error, [StackTrace? stackTrace]) { + AppMetrica.reportError( + message: errorName, + errorDescription: AppMetricaErrorDescription.fromObjectAndStackTrace( + error, + stackTrace ?? StackTrace.current, + ), + ); + } + + static void trackError(String errorName, String errorMessage) { + AppMetrica.reportError( + message: errorName, + errorDescription: AppMetricaErrorDescription.fromObjectAndStackTrace( + errorMessage, + StackTrace.current, + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/services/api_service.dart b/frontend/lib/services/api_service.dart index 9818f4c..1051a86 100644 --- a/frontend/lib/services/api_service.dart +++ b/frontend/lib/services/api_service.dart @@ -2,16 +2,27 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import '../models/user_model.dart'; +import '../models/card_model.dart'; +import '../models/shop_model.dart'; +import '../models/theme_model.dart' as app_theme; +import '../models/achievement_model.dart'; +import '../models/news_model.dart'; +import '../models/quest_model.dart'; +import '../models/notification_model.dart'; +import '../models/trade_model.dart'; class ApiService { - static const String baseUrl = 'http://87.236.23.130:8080/api'; + static const String baseUrl = 'http://217.114.7.39:8080/api'; static const String loginUrl = '$baseUrl/auth/login'; static const String registerUrl = '$baseUrl/auth/register'; - static const String activateUrl = '$baseUrl/auth/activate'; - static const String resendActivationUrl = '$baseUrl/auth/resend-activation-code'; + static const String activateUrl = '$baseUrl/auth/verify'; + static const String resendActivationUrl = '$baseUrl/auth/resend-code'; static const String forgotPasswordUrl = '$baseUrl/auth/forgot-password'; static const String resetPasswordUrl = '$baseUrl/auth/reset-password'; + // Карты + static const String cardsUrl = '$baseUrl/cards'; + static const String shopUrl = '$baseUrl/shop'; Future> getHeaders() async { final token = await getSavedToken(); @@ -70,7 +81,7 @@ class ApiService { } } - Future register(String email, String username, String password) async { + Future> register(String email, String username, String password) async { try { print('Отправка запроса на регистрацию: $registerUrl'); print('Данные запроса: email=$email, username=$username, password=${password.replaceAll(RegExp(r'.'), '*')}'); @@ -92,11 +103,11 @@ class ApiService { if (response.statusCode == 200 || response.statusCode == 201) { final responseData = json.decode(response.body); - final success = responseData['success'] ?? false; + final tempToken = responseData['tempToken']; final message = responseData['message'] ?? 'Пользователь успешно зарегистрирован'; - print('Результат регистрации: $message (успех: $success)'); - return success; + print('Результат регистрации: $message (tempToken: $tempToken)'); + return responseData; } else { final errorData = json.decode(response.body); final errorMessage = errorData['message'] ?? 'Ошибка регистрации. Попробуйте снова.'; @@ -125,18 +136,19 @@ class ApiService { await prefs.remove('auth_token'); } - Future activateAccount(String email, String code) async { + Future activateAccount(String tempToken, String code) async { try { print('Отправка запроса на активацию аккаунта: $activateUrl'); - print('Данные запроса: email=$email, code=$code'); + print('Данные запроса: tempToken=$tempToken, code=$code'); + + final headers = await getHeaders(); + headers['Content-Type'] = 'application/json'; final response = await http.post( Uri.parse(activateUrl), - headers: { - 'Content-Type': 'application/json', - }, + headers: headers, body: json.encode({ - 'email': email, + 'tempToken': tempToken, 'code': code, }), ); @@ -146,11 +158,16 @@ class ApiService { if (response.statusCode == 200) { final responseData = json.decode(response.body); - final success = responseData['success'] ?? false; - final message = responseData['message'] ?? 'Операция выполнена успешно'; + final userModel = UserModel.fromJson(responseData); - print('Результат активации: $message (успех: $success)'); - return success; + if (userModel.token != null) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('auth_token', userModel.token!); + print('Токен успешно сохранен'); + } + + print('Аккаунт успешно активирован для: ${userModel.email}'); + return userModel; } else { final errorData = json.decode(response.body); final errorMessage = errorData['message'] ?? 'Ошибка активации аккаунта. Попробуйте снова.'; @@ -168,11 +185,12 @@ class ApiService { print('Отправка запроса на повторную отправку кода: $resendActivationUrl'); print('Данные запроса: email=$email'); + final headers = await getHeaders(); + headers['Content-Type'] = 'application/json'; + final response = await http.post( Uri.parse(resendActivationUrl), - headers: { - 'Content-Type': 'application/json', - }, + headers: headers, body: json.encode({ 'email': email, }), @@ -304,4 +322,676 @@ class ApiService { throw Exception('Ошибка сети: ${e.toString()}'); } } + + // Карты + Future> getUserInventory({String sortBy = 'rarity'}) async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$cardsUrl/inventory?sortBy=$sortBy'), + headers: headers, + ); + + if (response.statusCode == 200) { + final List jsonList = json.decode(response.body); + return jsonList.map((json) => CardModel.fromJson(json)).toList(); + } else { + throw Exception('Ошибка при получении инвентаря: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future getCardDetails(int cardId) async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$cardsUrl/$cardId'), + headers: headers, + ); + + if (response.statusCode == 200) { + return CardModel.fromJson(json.decode(response.body)); + } else { + throw Exception('Ошибка при получении деталей карты: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future disassembleCard(int cardId) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$cardsUrl/$cardId/disassemble'), + headers: headers, + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + return data['coinsReceived'] as int; + } else { + throw Exception('Ошибка при разборе карты: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + // Магазин + Future> getAllPacks() async { + try { + final response = await http.get(Uri.parse('$baseUrl/shop/packs')); + + if (response.statusCode == 200) { + final List jsonList = json.decode(response.body); + return jsonList.map((json) => Pack.fromJson(json)).toList(); + } else { + throw Exception('Ошибка при получении списка наборов: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future getPackDetails(int packId) async { + try { + final response = await http.get( + Uri.parse('$baseUrl/shop/packs/$packId'), + ); + + if (response.statusCode == 200) { + return Pack.fromJson(json.decode(response.body)); + } else { + throw Exception('Ошибка при получении деталей набора: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> buyPack(int packId) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/shop/packs/$packId/buy'), + headers: headers, + ); + + if (response.statusCode == 200) { + return json.decode(response.body); + } else if (response.statusCode == 402) { + final error = json.decode(response.body); + throw Exception('Недостаточно средств. Требуется: ${error['requiredAmount']}'); + } else { + throw Exception('Ошибка при покупке набора: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> openPack(int packId) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/shop/packs/$packId/open'), + headers: headers, + ); + + if (response.statusCode == 200) { + return json.decode(response.body); + } else { + throw Exception('Ошибка при открытии набора: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> getCoinOffers() async { + try { + final response = await http.get( + Uri.parse('$baseUrl/shop/coins/offers'), + ); + + if (response.statusCode == 200) { + final List jsonList = json.decode(response.body); + return jsonList.map((json) => CoinOffer.fromJson(json)).toList(); + } else { + throw Exception('Ошибка при получении предложений монет: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future getCoinOfferDetails(int offerId) async { + try { + final response = await http.get( + Uri.parse('$baseUrl/shop/coins/offers/$offerId'), + ); + + if (response.statusCode == 200) { + return CoinOffer.fromJson(json.decode(response.body)); + } else { + throw Exception('Ошибка при получении деталей предложения: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future purchaseCoins(int offerId, String redirectUrl) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/shop/coins/offers/$offerId/purchase'), + headers: headers, + body: json.encode({'redirectUrl': redirectUrl}), + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + return data['paymentUrl']; + } else { + throw Exception('Ошибка при покупке монет: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + // Генерация карт + Future> getCardGenerationThemes() async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$baseUrl/home/generate-card/themes'), + headers: headers, + ); + + if (response.statusCode == 200) { + final List jsonList = json.decode(response.body); + return jsonList.map((json) => app_theme.Theme.fromJson(json)).toList(); + } else { + throw Exception('Ошибка при получении тем: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> generateCard(int themeId) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/home/generate-card'), + headers: headers, + body: json.encode({'theme_ID': themeId}), + ); + + if (response.statusCode == 200) { + return json.decode(response.body); + } else if (response.statusCode == 402) { + final error = json.decode(response.body); + throw Exception('Недостаточно средств. Требуется: ${error['requiredAmount']}'); + } else { + throw Exception('Ошибка при генерации карты: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + // Профиль + Future> getProfile() async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$baseUrl/profile'), + headers: headers, + ); + + if (response.statusCode == 200) { + return json.decode(response.body); + } else { + throw Exception('Ошибка при получении профиля: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> getAchievements() async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$baseUrl/profile/achievements'), + headers: headers, + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + return (data['achievements'] as List) + .map((json) => Achievement.fromJson(json)) + .toList(); + } else { + throw Exception('Ошибка при получении достижений: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future addAchievementToFavorites(int achievementId) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/profile/achievements/$achievementId/favorite-add'), + headers: headers, + ); + + return response.statusCode == 200; + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future removeAchievementFromFavorites(int achievementId) async { + try { + final headers = await getHeaders(); + final response = await http.delete( + Uri.parse('$baseUrl/profile/achievements/$achievementId/favorite-delete'), + headers: headers, + ); + + return response.statusCode == 200; + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> getProfileSettings() async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$baseUrl/profile/settings'), + headers: headers, + ); + + if (response.statusCode == 200) { + return json.decode(response.body); + } else { + throw Exception('Ошибка при получении настроек: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> updateProfileSettings(Map settings) async { + try { + final headers = await getHeaders(); + final response = await http.put( + Uri.parse('$baseUrl/profile/settings-change'), + headers: headers, + body: json.encode(settings), + ); + + if (response.statusCode == 200) { + return json.decode(response.body); + } else { + throw Exception('Ошибка при обновлении настроек: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + // Другие профили + Future> getOtherProfile(int userId) async { + try { + final response = await http.get( + Uri.parse('$baseUrl/other-profile/$userId'), + ); + + if (response.statusCode == 200) { + return json.decode(response.body); + } else { + throw Exception('Ошибка при получении профиля: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> getOtherProfileAchievements(int userId) async { + try { + final response = await http.get( + Uri.parse('$baseUrl/other-profile/$userId/achievements'), + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + return (data['achievements'] as List) + .map((json) => Achievement.fromJson(json)) + .toList(); + } else { + throw Exception('Ошибка при получении достижений: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> getOtherProfileInventory(int userId) async { + try { + final response = await http.get( + Uri.parse('$baseUrl/other-profile/$userId/inventory'), + ); + + if (response.statusCode == 200) { + return json.decode(response.body); + } else if (response.statusCode == 403) { + throw Exception('Инвентарь скрыт'); + } else { + throw Exception('Ошибка при получении инвентаря: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future reportUser(int userId, String reason, {String? comment}) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/other-profile/$userId/report'), + headers: headers, + body: json.encode({ + 'reason': reason, + if (comment != null) 'comment': comment, + }), + ); + + return response.statusCode == 200; + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + // Новости + Future> getNews() async { + try { + final response = await http.get(Uri.parse('$baseUrl/home/news')); + + if (response.statusCode == 200) { + final List jsonList = json.decode(response.body); + return jsonList.map((json) => News.fromJson(json)).toList(); + } else { + throw Exception('Ошибка при получении новостей: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future getNewsDetails(int newsId) async { + try { + final response = await http.get(Uri.parse('$baseUrl/home/news/$newsId')); + + if (response.statusCode == 200) { + return News.fromJson(json.decode(response.body)); + } else { + throw Exception('Ошибка при получении новости: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + // Квесты + Future>> getQuests() async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$baseUrl/home/quests'), + headers: headers, + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + return { + 'dailyQuests': (data['dailyQuests'] as List) + .map((json) => Quest.fromJson(json)) + .toList(), + 'weeklyQuests': (data['weeklyQuests'] as List) + .map((json) => Quest.fromJson(json)) + .toList(), + }; + } else { + throw Exception('Ошибка при получении квестов: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future changeQuestStatus(int questId) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/home/quests/$questId/change-status'), + headers: headers, + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + return Quest.fromJson(data['quest']); + } else { + throw Exception('Ошибка при изменении статуса квеста: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> claimQuestReward(String questType) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/home/quests/claim-reward'), + headers: headers, + body: json.encode({'questType': questType}), + ); + + if (response.statusCode == 200) { + return json.decode(response.body); + } else { + throw Exception('Ошибка при получении награды: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + // Уведомления + Future> getNotifications() async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$baseUrl/home/notifications'), + headers: headers, + ); + + if (response.statusCode == 200) { + final List jsonList = json.decode(response.body); + return jsonList.map((json) => Notification.fromJson(json)).toList(); + } else { + throw Exception('Ошибка при получении уведомлений: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future getNotificationDetails(int notificationId) async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$baseUrl/home/notifications/$notificationId'), + headers: headers, + ); + + if (response.statusCode == 200) { + return Notification.fromJson(json.decode(response.body)); + } else { + throw Exception('Ошибка при получении уведомления: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future navigateToNotification(int notificationId) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/home/notifications/$notificationId/navigate'), + headers: headers, + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + return data['redirectUrl']; + } else { + throw Exception('Ошибка при переходе по уведомлению: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + // Обмен картами + Future> getAllTrades({String? search}) async { + try { + String url = '$baseUrl/trades'; + if (search != null && search.isNotEmpty) { + url += '?search=$search'; + } + + final response = await http.get(Uri.parse(url)); + + if (response.statusCode == 200) { + final List jsonList = json.decode(response.body); + return jsonList.map((json) => Trade.fromJson(json)).toList(); + } else { + throw Exception('Ошибка при получении списка обменов: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future getTradeDetails(int tradeId) async { + try { + final response = await http.get( + Uri.parse('$baseUrl/trades/$tradeId'), + ); + + if (response.statusCode == 200) { + return Trade.fromJson(json.decode(response.body)); + } else { + throw Exception('Ошибка при получении деталей обмена: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future initiateTrade(int offeredCardId, List requestedCardIds) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/trades/initiate'), + headers: headers, + body: json.encode({ + 'offeredCardId': offeredCardId, + 'requestedCardId': requestedCardIds, + }), + ); + + if (response.statusCode == 201) { + return Trade.fromJson(json.decode(response.body)); + } else { + throw Exception('Ошибка при создании обмена: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future> getMyTrades() async { + try { + final headers = await getHeaders(); + final response = await http.get( + Uri.parse('$baseUrl/trades/my'), + headers: headers, + ); + + if (response.statusCode == 200) { + final List jsonList = json.decode(response.body); + return jsonList.map((json) => Trade.fromJson(json)).toList(); + } else { + throw Exception('Ошибка при получении моих обменов: ${response.body}'); + } + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future acceptTrade(int tradeId) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/trades/$tradeId/accept'), + headers: headers, + ); + + return response.statusCode == 200; + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future rejectTrade(int tradeId) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/trades/$tradeId/reject'), + headers: headers, + ); + + return response.statusCode == 200; + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } + + Future cancelTrade(int tradeId) async { + try { + final headers = await getHeaders(); + final response = await http.post( + Uri.parse('$baseUrl/trades/$tradeId/cancel'), + headers: headers, + ); + + return response.statusCode == 200; + } catch (e) { + throw Exception('Ошибка сети: $e'); + } + } } \ No newline at end of file diff --git a/frontend/lib/utils/auth_utils.dart b/frontend/lib/utils/auth_utils.dart new file mode 100644 index 0000000..f71e4c4 --- /dev/null +++ b/frontend/lib/utils/auth_utils.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import '../controllers/auth_controller.dart'; +import '../views/authorization_dialog.dart'; +import 'package:provider/provider.dart'; + +class AuthUtils { + static bool isGuestAllowedScreen(String screenName) { + final allowedScreens = { + 'home_screen': true, + 'news_screen': true, + 'news_detail_screen': true, + 'exchanges_screen': true, + 'exchange_details_screen': true, + 'shop_screen': true, + 'search_players_screen': true, + 'card_detail_screen': true, + // Защищенные экраны + 'profile_screen': false, + 'inventory_screen': false, + 'create_exchange_screen': false, + 'my_exchanges_screen': false, + 'create_card_screen': false, + 'achievements_screen': false, + 'settings_screen': false, + 'notifications_screen': false, + 'quests_screen': false, + }; + return allowedScreens[screenName] ?? false; + } + + static bool checkGuestAccess(BuildContext context, String screenName) { + final authController = Provider.of(context, listen: false); + final isGuest = authController.currentUser?.isGuest ?? false; + + if (isGuest && !isGuestAllowedScreen(screenName)) { + showDialog( + context: context, + barrierDismissible: true, + builder: (context) => const AuthorizationDialog(), + ); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/frontend/lib/utils/error_formatter.dart b/frontend/lib/utils/error_formatter.dart new file mode 100644 index 0000000..7c0a049 --- /dev/null +++ b/frontend/lib/utils/error_formatter.dart @@ -0,0 +1,33 @@ +class ErrorFormatter { + static String formatError(dynamic error) { + final errorStr = error.toString().toLowerCase(); + + if (errorStr.contains('socketconnection') || + errorStr.contains('connection refused') || + errorStr.contains('network is unreachable') || + errorStr.contains('connection timed out')) { + return 'Ошибка сети. Проверьте подключение к интернету'; + } + + if (errorStr.contains('unauthorized') || errorStr.contains('unauthenticated')) { + return 'Необходима авторизация'; + } + + if (errorStr.contains('not found')) { + return 'Запрашиваемые данные не найдены'; + } + + if (errorStr.contains('permission') || errorStr.contains('forbidden')) { + return 'Нет доступа к запрашиваемым данным'; + } + + // Если это ошибка от нашего API (которую мы специально выбросили с понятным сообщением) + if (error is Exception && !errorStr.contains('exception')) { + // Убираем слово "Exception" из сообщения + return error.toString().replaceAll('Exception: ', ''); + } + + // Для всех остальных случаев + return 'Произошла ошибка. Попробуйте позже'; + } +} \ No newline at end of file diff --git a/frontend/lib/views/achievements_screen.dart b/frontend/lib/views/achievements_screen.dart new file mode 100644 index 0000000..520bfaf --- /dev/null +++ b/frontend/lib/views/achievements_screen.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import '../models/achievement_model.dart'; +import '../services/api_service.dart'; +import '../utils/error_formatter.dart'; +import '../utils/auth_utils.dart'; +import '../services/analytics_service.dart'; + +class AchievementsScreen extends StatefulWidget { + final bool isOtherUser; + final int userId; + + const AchievementsScreen({ + Key? key, + required this.isOtherUser, + required this.userId + }) : super(key: key); + + @override + _AchievementsScreenState createState() => _AchievementsScreenState(); +} + +class _AchievementsScreenState extends State { + late bool _isLoading; + late List _achievements; + late String _error; + + @override + void initState() { + super.initState(); + _isLoading = true; + _achievements = []; + _error = ''; + AnalyticsService.trackScreenView('achievements_screen'); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (AuthUtils.checkGuestAccess(context, 'achievements_screen')) { + _loadAchievements(); + } else { + Navigator.of(context).pop(); + } + }); + } + + Future _loadAchievements() async { + try { + setState(() { + _isLoading = true; + }); + + final achievements = widget.isOtherUser + ? await ApiService().getOtherProfileAchievements(widget.userId) + : await ApiService().getAchievements(); + + setState(() { + _achievements = achievements; + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _error = ErrorFormatter.formatError(e); + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Достижения${widget.isOtherUser ? ' игрока' : ''}'), + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _error.isNotEmpty + ? Center(child: Text(_error)) + : ListView.builder( + itemCount: _achievements.length, + itemBuilder: (context, index) { + final achievement = _achievements[index]; + return ListTile( + leading: Image.network(achievement.imageURL), + title: Text(achievement.name), + subtitle: Text(achievement.description), + trailing: achievement.isCompleted + ? const Icon(Icons.check_circle, color: Colors.green) + : const Icon(Icons.lock, color: Colors.grey), + ); + }, + ), + ); + } +} diff --git a/frontend/lib/views/auth_screen.dart b/frontend/lib/views/auth_screen.dart index 21c68ac..e63ad02 100644 --- a/frontend/lib/views/auth_screen.dart +++ b/frontend/lib/views/auth_screen.dart @@ -82,17 +82,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM context: context, ); - if (!mounted) return; - - // Переход на экран подтверждения email - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EmailVerificationScreen( - email: _registerEmailController.text.trim(), - ), - ), - ); + // Убираем навигацию отсюда, так как она теперь происходит в AuthController } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( @@ -108,14 +98,15 @@ class _AuthScreenState extends State with SingleTickerProviderStateM final isLoading = authController.isLoading; return Scaffold( + backgroundColor: const Color(0xFFFBF6EF), body: Column( children: [ // Логотип и название - const SizedBox(height: 40), + const SizedBox(height: 80), Image.asset( 'assets/icons/карты.png', height: 80, - color: const Color(0xFFD9A76A), + color: const Color(0xFFDBAA76), ), const SizedBox(height: 10), const SizedBox(height: 10), @@ -123,8 +114,9 @@ class _AuthScreenState extends State with SingleTickerProviderStateM 'Cardly', style: TextStyle( fontSize: 32, - color: Color(0xFFD9A76A), + color: Color(0xFFDBAA76), fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), const SizedBox(height: 30), @@ -133,11 +125,12 @@ class _AuthScreenState extends State with SingleTickerProviderStateM Container( margin: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(8), ), child: TabBar( controller: _tabController, + dividerColor: Colors.transparent, indicatorSize: TabBarIndicatorSize.tab, indicator: BoxDecoration( color: const Color(0xFFD6A067), @@ -152,6 +145,12 @@ class _AuthScreenState extends State with SingleTickerProviderStateM Tab(text: 'Войти'), Tab(text: 'Создать'), ], + labelStyle: const TextStyle( + fontFamily: 'Jost', + ), + unselectedLabelStyle: const TextStyle( + fontFamily: 'Jost', + ), ), ), @@ -173,6 +172,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -180,7 +180,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM controller: _loginEmailController, decoration: const InputDecoration( filled: true, - fillColor: Color(0xFFEDD6B0), + fillColor: Color(0xFFEAD7C3), border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.all(Radius.circular(8)), @@ -206,6 +206,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -213,7 +214,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM controller: _loginPasswordController, decoration: InputDecoration( filled: true, - fillColor: const Color(0xFFEDD6B0), + fillColor: const Color(0xFFEAD7C3), border: const OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.all(Radius.circular(8)), @@ -261,6 +262,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM style: TextStyle( fontSize: 14, color: Colors.black, + fontFamily: 'Jost', ), ), ), @@ -274,29 +276,49 @@ class _AuthScreenState extends State with SingleTickerProviderStateM onPressed: isLoading ? null : _login, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16), + minimumSize: const Size(double.infinity, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), - disabledBackgroundColor: const Color(0xFFD6A067).withOpacity(0.7), ), child: isLoading - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.black, - ), - ) - : const Text( - 'Вход', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : const Text( + 'Войти', + style: TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w500, + fontFamily: 'Jost', + ), ), - ), + ), + ), + + const SizedBox(height: 16), + + Center( + child: TextButton( + onPressed: isLoading ? null : () { + Provider.of(context, listen: false) + .loginAsGuest(context: context); + }, + child: const Text( + 'Войти как гость', + style: TextStyle( + color: Color.fromARGB(255, 0, 0, 0), + fontSize: 15, + fontFamily: 'Jost', + decoration: TextDecoration.underline, + ), + ), ), ), ], @@ -316,6 +338,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -323,7 +346,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM controller: _nicknameController, decoration: const InputDecoration( filled: true, - fillColor: Color(0xFFEDD6B0), + fillColor: Color(0xFFEAD7C3), border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.all(Radius.circular(8)), @@ -345,6 +368,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -352,7 +376,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM controller: _registerEmailController, decoration: const InputDecoration( filled: true, - fillColor: Color(0xFFEDD6B0), + fillColor: Color(0xFFEAD7C3), border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.all(Radius.circular(8)), @@ -378,6 +402,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -385,7 +410,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM controller: _registerPasswordController, decoration: InputDecoration( filled: true, - fillColor: const Color(0xFFEDD6B0), + fillColor: const Color(0xFFEAD7C3), border: const OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.all(Radius.circular(8)), @@ -422,6 +447,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -429,7 +455,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM controller: _confirmPasswordController, decoration: InputDecoration( filled: true, - fillColor: const Color(0xFFEDD6B0), + fillColor: const Color(0xFFEAD7C3), border: const OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.all(Radius.circular(8)), @@ -466,13 +492,13 @@ class _AuthScreenState extends State with SingleTickerProviderStateM child: ElevatedButton( onPressed: isLoading ? null : _register, style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), + backgroundColor: const Color(0xFFDBAA76), foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), - disabledBackgroundColor: const Color(0xFFD6A067).withOpacity(0.7), + disabledBackgroundColor: const Color(0xFFDBAA76).withOpacity(0.7), ), child: isLoading ? const SizedBox( @@ -488,6 +514,7 @@ class _AuthScreenState extends State with SingleTickerProviderStateM style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + fontFamily: 'Roboto', ), ), ), @@ -500,15 +527,6 @@ class _AuthScreenState extends State with SingleTickerProviderStateM ), ), - Container( - width: 100, - height: 4, - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(2), - ), - ), ], ), ); diff --git a/frontend/lib/views/authorization_dialog.dart b/frontend/lib/views/authorization_dialog.dart new file mode 100644 index 0000000..d27278c --- /dev/null +++ b/frontend/lib/views/authorization_dialog.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'auth_screen.dart'; + +class AuthorizationDialog extends StatelessWidget { + const AuthorizationDialog({super.key}); + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: const Color(0xFFFFF4E3), // Match background color + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + child: Stack( + clipBehavior: Clip.none, // Allows positioned elements outside the stack bounds + children: [ + // Main content container + Container( + padding: const EdgeInsets.only(top: 24.0, bottom: 16.0, left: 16.0, right: 16.0), + child: Column( + mainAxisSize: MainAxisSize.min, // Use minimum space vertically + crossAxisAlignment: CrossAxisAlignment.stretch, // Stretch children horizontally + children: [ + // Title Text + const Text( + 'Для продолжения необходимо\nавторизоваться', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18.0, // Adjusted font size slightly for better fit + fontFamily: 'Jost', // Assuming Jost font based on other screens + color: Colors.black, + ), + ), + const SizedBox(height: 20.0), // Space between text and button + // Button + ElevatedButton( + onPressed: () { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (context) => const AuthScreen(initialTabIndex: 0), + ), + (route) => false, + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), // Match button color + foregroundColor: Colors.black, + minimumSize: const Size(double.infinity, 50), // Full width button + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), // Rounded corners + ), + ), + child: const Text( + 'Перейти на вход', + style: TextStyle( + fontSize: 18.0, // Match text style + fontWeight: FontWeight.w500, + fontFamily: 'Jost', + ), + ), + ), + ], + ), + ), + // Close button in the top right corner + Positioned( + top: -10, // Adjust position to be slightly outside/on border + right: -10, // Adjust position to be slightly outside/on border + child: GestureDetector( + onTap: () => Navigator.pop(context), // Close dialog + child: Container( + width: 36, // Match size from other dialogs + height: 36, // Match size + decoration: BoxDecoration( + color: const Color(0xFFD6A067), // Match color from other dialogs + borderRadius: BorderRadius.circular(8), // Match radius + border: Border.all(color: Colors.black, width: 2), // Match border + ), + child: const Center( + child: Icon( + Icons.close, + color: Colors.black, // Match color + size: 20, // Match size + ), + ), + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/views/card_detail_screen.dart b/frontend/lib/views/card_detail_screen.dart index 8a62603..c9e7346 100644 --- a/frontend/lib/views/card_detail_screen.dart +++ b/frontend/lib/views/card_detail_screen.dart @@ -1,276 +1,184 @@ import 'package:flutter/material.dart'; +import '../models/card_model.dart'; +import 'create_exchange_screen.dart'; +import '../services/api_service.dart'; +import '../utils/error_formatter.dart'; -class CardDetailScreen extends StatelessWidget { +class CardDetailScreen extends StatefulWidget { + final CardModel card; final int cardIndex; + final bool showExchangeButton; + final bool isFromShop; + final bool showFavoriteButton; - const CardDetailScreen({Key? key, required this.cardIndex}) : super(key: key); + const CardDetailScreen({ + Key? key, + required this.card, + required this.cardIndex, + this.showExchangeButton = false, + this.isFromShop = false, + this.showFavoriteButton = true, + }) : super(key: key); @override - Widget build(BuildContext context) { - // Данные для карточек - final List> cardData = [ - { - 'image': 'assets/images/chameleon.jpg', - 'desc': 'Хамелеон — семейство ящериц, приспособленных к древесному образу жизни, способных менять окраску тела.', - 'name': 'Хамелеон', - 'type': 'Звери', - 'rarity': '4', - }, - { - 'image': 'assets/images/kiwi.jpg', - 'desc': 'Киви — семейство нелетающих птиц отряда кивиобразных.', - 'name': 'Киви', - 'type': 'Птицы', - 'rarity': '4', - }, - { - 'image': 'assets/images/platypus.jpg', - 'desc': 'Утконос — водоплавающее млекопитающее отряда однопроходных, обитающее в Австралии.', - 'name': 'Утконос', - 'type': 'Звери', - 'rarity': '4', - }, - { - 'image': 'assets/images/pantera.jpg', - 'desc': 'Пантера — крупный хищник из семейства кошачьих. В некоторых культурах черная пантера считается священным животным.', - 'name': 'Пантера', - 'type': 'Звери', - 'rarity': '4', - }, - { - 'image': 'assets/images/white_bear.jpg', - 'desc': 'Белый медведь — хищное млекопитающее семейства медвежьих, близкий родственник бурого медведя.', - 'name': 'Белый медведь', - 'type': 'Звери', - 'rarity': '3', - }, - { - 'image': 'assets/images/camel.jpg', - 'desc': 'Верблюд — млекопитающие семейства верблюдовых.', - 'name': 'Верблюд', - 'type': 'Звери', - 'rarity': '3', - }, - { - 'image': 'assets/images/zebra.jpg', - 'desc': 'Зебра — африканская дикая лошадь, отличающаяся характерным полосатым окрасом шкуры.', - 'name': 'Зебра', - 'type': 'Звери', - 'rarity': '3', - }, - { - 'image': 'assets/images/elephant.jpg', - 'desc': 'Слон — самое крупное наземное животное на Земле. Хобот слона — это многофункциональный инструмент.', - 'name': 'Слон', - 'type': 'Звери', - 'rarity': '3', - }, - ]; - final data = (cardIndex < cardData.length) ? cardData[cardIndex] : cardData[0]; - final String imagePath = data['image']!; - final String description = data['desc']!; - final String name = data['name']!; - final String type = data['type']!; - final int rarity = int.tryParse(data['rarity'] ?? '0') ?? 0; + State createState() => _CardDetailScreenState(); +} + +class _CardDetailScreenState extends State { + bool isFavorite = false; + @override + Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), - body: SafeArea( + appBar: AppBar( + title: Text(widget.card.name), + ), + body: SingleChildScrollView( child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: InkWell( - onTap: () => Navigator.pop(context), - borderRadius: BorderRadius.circular(30), - child: Container( - width: 48, - height: 48, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - shape: BoxShape.circle, - ), - child: const Icon(Icons.arrow_back, color: Colors.black, size: 32), - ), + Image.network(widget.card.imageURL), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.card.name, + style: Theme.of(context).textTheme.headlineMedium, ), - ), - ], - ), - Expanded( - child: Center( - child: Container( - width: 340, - height: 520, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(18.0), - border: Border.all(color: Colors.black, width: 6), + const SizedBox(height: 8), + Text( + 'Редкость: ${widget.card.rarity}', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 16), + Text( + widget.card.description, + style: Theme.of(context).textTheme.bodyLarge, ), - child: Container( - margin: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(14.0), - border: Border.all(color: Colors.black, width: 3), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 32.0, left: 16.0, right: 16.0, top: 8.0), + child: widget.showExchangeButton + ? ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + onPressed: () {}, + child: const Text( + 'Предложить обмен', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400, fontFamily: 'Roboto'), + ), + ) + : widget.isFromShop + ? const SizedBox.shrink() + : Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Expanded( - flex: 3, // 5% - child: Center( - child: Text( - name, - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.w500, - color: Colors.black, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), ), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), ), - ), - ), - Expanded( - flex: 36, // 60% - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 0.0, vertical: 4.0), - decoration: BoxDecoration( - border: Border.all(color: Colors.black, width: 3), - ), - child: ClipRect( - child: Image.asset( - imagePath, - fit: BoxFit.fill, - width: double.infinity, - height: double.infinity, - ), - ), - ), - ), - Expanded( - flex: 18, // 30% - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0), - child: Text( - description, - style: const TextStyle(fontSize: 17, color: Colors.black), - textAlign: TextAlign.left, - ), - ), - ), - Expanded( - flex: 3, // 5% - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 0.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, + onPressed: _disassembleCard, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ Row( - children: List.generate( - rarity, - (index) => Padding( - padding: const EdgeInsets.only(right: 4.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 24, - ), + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'Разобрать', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400, fontFamily: 'Roboto'), ), - ), + SizedBox(width: 6), + ], ), - const Spacer(), - Text( - type, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - color: Colors.black, - ), + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + '150', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w400, + fontFamily: 'Roboto', + ), + textAlign: TextAlign.center, + ), + SizedBox(width: 4), + Image.asset( + 'assets/icons/монеты.png', + height: 18, + ), + ], ), ], ), ), ), - ], - ), - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 32.0, left: 16.0, right: 16.0, top: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.0), - ), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - ), - onPressed: () {}, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text( - 'Разобрать', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400), - ), - SizedBox(width: 6), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - '150', - style: TextStyle(fontSize: 15, fontWeight: FontWeight.w400), - textAlign: TextAlign.center, - ), - SizedBox(width: 4), - Image.asset( - 'assets/icons/монеты.png', - height: 18, + SizedBox(width: 16), + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), ), - ], + padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 25), + ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CreateExchangeScreen( + cardId: widget.cardIndex, + ), + ), + ); + }, + child: const Text( + 'Обменять', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400, fontFamily: 'Roboto'), + ), ), - ], - ), - ), - ), - SizedBox(width: 16), - Expanded( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.0), ), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - ), - onPressed: () {}, - child: const Text( - 'Обменять', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w400), - ), + ], ), - ), - ], - ), ), ], ), ), ); } + + Future _disassembleCard() async { + try { + final coinsReceived = await ApiService().disassembleCard(widget.card.card_ID); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Получено монет: $coinsReceived')), + ); + Navigator.pop(context); // Возвращаемся на предыдущий экран + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } } \ No newline at end of file diff --git a/frontend/lib/views/change_password_screen.dart b/frontend/lib/views/change_password_screen.dart index e3ac38d..0c0cb02 100644 --- a/frontend/lib/views/change_password_screen.dart +++ b/frontend/lib/views/change_password_screen.dart @@ -130,6 +130,7 @@ class _ChangePasswordScreenState extends State { fontSize: 32, color: Color(0xFFD9A76A), fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), const SizedBox(height: 30), @@ -167,6 +168,7 @@ class _ChangePasswordScreenState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 20), @@ -176,6 +178,7 @@ class _ChangePasswordScreenState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -220,6 +223,7 @@ class _ChangePasswordScreenState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -286,6 +290,7 @@ class _ChangePasswordScreenState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + fontFamily: 'Roboto', ), ), ), @@ -314,6 +319,7 @@ class _ChangePasswordScreenState extends State { style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), textAlign: TextAlign.center, ), @@ -337,6 +343,7 @@ class _ChangePasswordScreenState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + fontFamily: 'Roboto', ), ), ), diff --git a/frontend/lib/views/create_card_screen.dart b/frontend/lib/views/create_card_screen.dart index 18e9caf..4e058ee 100644 --- a/frontend/lib/views/create_card_screen.dart +++ b/frontend/lib/views/create_card_screen.dart @@ -2,6 +2,10 @@ import 'package:flutter/material.dart'; import 'home_screen.dart'; import 'shop_screen.dart'; import 'exchanges_screen.dart'; +import 'inventory_screen.dart'; +import 'profile_screen.dart'; +import '../utils/auth_utils.dart'; +import '../services/analytics_service.dart'; class CreateCardScreen extends StatefulWidget { const CreateCardScreen({super.key}); @@ -12,32 +16,55 @@ class CreateCardScreen extends StatefulWidget { class _CreateCardScreenState extends State { bool _showCategories = false; - int _currentIndex = 0; // Установлено 0 для главного меню + int _currentIndex = 0; final List _categories = ['Категория 1', 'Категория 2', 'Категория 3', 'Категория 4']; - String _selectedCategory = 'Пример категории'; + String _selectedCategory = 'Выберите категорию'; + + @override + void initState() { + super.initState(); + AnalyticsService.trackScreenView('create_card_screen'); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!AuthUtils.checkGuestAccess(context, 'create_card_screen')) { + Navigator.of(context).pop(); + } + }); + } @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон + backgroundColor: const Color(0xFFFBF6EF), appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), elevation: 0, - leading: Container( - margin: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.black, width: 2), - ), - child: const Icon( - Icons.person_outline, - color: Colors.black, + leading: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Container( + width: 40.0, + height: 40.0, + child: InkWell( + borderRadius: BorderRadius.circular(20.0), + onTap: () { + if (AuthUtils.checkGuestAccess(context, 'profile_screen')) { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ProfileScreen()), + ); + } + }, + child: Image.asset('assets/icons/профиль.png', height: 22), + ), ), ), actions: [ IconButton( - icon: const Icon(Icons.search, color: Colors.black), - onPressed: () {}, + icon: Image.asset( + 'assets/icons/уведомления.png', + height: 36, + color: Colors.black, + ), + onPressed: null, ), ], ), @@ -73,7 +100,7 @@ class _CreateCardScreenState extends State { child: Container( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(8.0), ), child: Row( @@ -84,12 +111,12 @@ class _CreateCardScreenState extends State { style: const TextStyle( color: Colors.black, fontSize: 16.0, + fontFamily: 'Jost', ), ), - // Значок стрелки меняется в зависимости от состояния Icon( - _showCategories ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, - color: Colors.black + _showCategories ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, + color: Colors.black, ), ], ), @@ -100,17 +127,13 @@ class _CreateCardScreenState extends State { ), ), - // Основное содержимое - макет карточки или список категорий + // Основное содержимое Expanded( child: Stack( + alignment: Alignment.center, children: [ - // Макет карточки всегда показывается в фоне - Padding( - padding: const EdgeInsets.all(16.0), - child: _buildCardTemplate(), - ), + _buildCardTemplate(), - // Список категорий показывается поверх, если _showCategories = true if (_showCategories) Positioned( top: 16.0, @@ -119,7 +142,7 @@ class _CreateCardScreenState extends State { child: Container( padding: const EdgeInsets.all(12.0), decoration: BoxDecoration( - color: const Color(0xFFEDD6B0).withOpacity(0.95), + color: const Color(0xFFEAD7C3).withOpacity(0.95), borderRadius: BorderRadius.circular(8.0), ), child: Column( @@ -139,6 +162,7 @@ class _CreateCardScreenState extends State { width: double.infinity, child: ElevatedButton( onPressed: () { + AnalyticsService.trackCardGeneration(); // Логика создания карточки }, style: ElevatedButton.styleFrom( @@ -154,146 +178,69 @@ class _CreateCardScreenState extends State { style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, + fontFamily: 'Roboto', ), ), ), ), ), - - // Нижняя навигационная панель - Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16.0), - topRight: Radius.circular(16.0), - ), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - - // Навигация в зависимости от выбранного индекса - switch (index) { - case 0: // Главное меню - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 2: // Магазин - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: // Обменчик - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.home), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Icon(Icons.book), - label: '', - ), - BottomNavigationBarItem( - icon: Icon(Icons.storefront), - label: '', - ), - BottomNavigationBarItem( - icon: Icon(Icons.people), - label: '', - ), - ], - ), - ), ], ), + bottomNavigationBar: null, ); } - // Метод построения шаблона карточки Widget _buildCardTemplate() { return Container( + width: 300, + height: 450, decoration: BoxDecoration( color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 2), + borderRadius: BorderRadius.circular(12.0), + border: Border.all(color: Colors.black, width: 3), ), - child: Column( - children: [ - // Верхняя часть карточки - Expanded( - flex: 2, - child: Container( - width: double.infinity, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - border: Border( - bottom: BorderSide(color: Colors.black, width: 2), - ), - ), + child: Container( + margin: const EdgeInsets.all(10), + decoration: BoxDecoration( + border: Border.all(color: Colors.black, width: 3), + borderRadius: BorderRadius.circular(8.0), + ), + child: Column( + children: [ + Expanded( + flex: 7, + child: Container(), ), - ), - - // Нижняя часть карточки - Expanded( - flex: 1, - child: Container( - width: double.infinity, - color: const Color(0xFFD6A067), + Container( + height: 3, + color: Colors.black, ), - ), - ], + Expanded( + flex: 3, + child: Container(), + ), + ], + ), ), ); } - // Метод построения элемента категории Widget _buildCategoryItem(String category) { - return GestureDetector( + return InkWell( onTap: () { setState(() { _selectedCategory = category; _showCategories = false; }); }, - child: Container( - width: double.infinity, - margin: const EdgeInsets.only(bottom: 8.0), - padding: const EdgeInsets.symmetric(vertical: 12.0), - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(4.0), - ), - alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), child: Text( category, style: const TextStyle( fontSize: 16.0, - fontWeight: FontWeight.w500, color: Colors.black, + fontFamily: 'Jost', ), ), ), diff --git a/frontend/lib/views/create_exchange_screen.dart b/frontend/lib/views/create_exchange_screen.dart index a8d9a5b..7097d42 100644 --- a/frontend/lib/views/create_exchange_screen.dart +++ b/frontend/lib/views/create_exchange_screen.dart @@ -1,10 +1,23 @@ import 'package:flutter/material.dart'; +import 'dart:math' as math; import 'home_screen.dart'; import 'shop_screen.dart'; import 'exchanges_screen.dart'; +import 'inventory_screen.dart'; +import 'profile_screen.dart'; +import '../utils/auth_utils.dart'; +import '../main.dart'; +import '../services/analytics_service.dart'; class CreateExchangeScreen extends StatefulWidget { - const CreateExchangeScreen({super.key}); + final ExchangeItem? initialExchangeItem; + final int? cardId; + + const CreateExchangeScreen({ + super.key, + this.initialExchangeItem, + this.cardId, + }); @override State createState() => _CreateExchangeScreenState(); @@ -12,178 +25,575 @@ class CreateExchangeScreen extends StatefulWidget { class _CreateExchangeScreenState extends State { int _currentIndex = 3; + Map? selectedTopCard; + List> selectedExchangeCards = []; + + + @override + void initState() { + super.initState(); + AnalyticsService.trackScreenView('create_exchange_screen'); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!AuthUtils.checkGuestAccess(context, 'create_exchange_screen')) { + Navigator.of(context).pop(); + } + }); + if (widget.initialExchangeItem != null && widget.initialExchangeItem!.otherUserOfferedCardIds.isNotEmpty) { + selectedTopCard = { + 'id': widget.initialExchangeItem!.otherUserOfferedCardIds[0], + 'name': 'Карта ${widget.initialExchangeItem!.otherUserOfferedCardIds[0]}', + }; + } else if (widget.cardId != null) { + selectedTopCard = { + 'id': widget.cardId, + 'name': 'Карта ${widget.cardId}', + }; + } + } + + void _showCardSelectionDialog({ + required String title, + required Function(Map) onCardSelected, + required bool Function(int) isCardSelected, + required bool Function() canSelectMore, + }) { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + backgroundColor: const Color(0xFFFBF6EF), + child: Container( + width: MediaQuery.of(context).size.width * 0.9, + height: MediaQuery.of(context).size.height * 0.8, + child: Stack( + clipBehavior: Clip.none, + children: [ + Container( + height: MediaQuery.of(context).size.height * 0.8, + padding: const EdgeInsets.only(top: 24.0, bottom: 16.0, left: 16.0, right: 16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 20.0, + fontFamily: 'Jost', + color: Colors.black, + ), + ), + const SizedBox(height: 16.0), + Expanded( + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + childAspectRatio: 0.7, + crossAxisSpacing: 10.0, + mainAxisSpacing: 10.0, + ), + itemCount: 40, + itemBuilder: (context, index) { + bool isSelected = isCardSelected(index); + return GestureDetector( + onTap: () { + if (!isSelected && canSelectMore()) { + onCardSelected({ + 'id': index, + 'name': 'Карта $index', + }); + Navigator.pop(context); + } + }, + child: _buildExchangeCardVisual( + width: 100, + height: 140, + content: Center(child: Text('Карта $index', style: const TextStyle(color: Colors.black, fontSize: 10, fontFamily: 'Jost'),)), + ), + ); + }, + ), + ), + ], + ), + ), + // Close button in the top right corner + Positioned( + top: 0, + right: -2, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Color(0xFFD6A067), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), + ), + child: const Center( + child: Icon( + Icons.close, + color: Colors.black, + size: 20, + ), + ), + ), + ), + ), + ], + ), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + ); + }, + ); + } + + void _showTopCardSelectionDialog() { + if (!AuthUtils.checkGuestAccess(context, 'create_exchange_screen')) { + return; + } + _showCardSelectionDialog( + title: 'Выберите вашу карту', + onCardSelected: (card) { + setState(() { + selectedTopCard = card; + }); + }, + isCardSelected: (index) => selectedTopCard?['id'] == index, + canSelectMore: () => true, + ); + } + + void _showLargeCardSelectionDialog() { + if (!AuthUtils.checkGuestAccess(context, 'create_exchange_screen')) { + return; + } + _showCardSelectionDialog( + title: 'Выберите карту для обмена', + onCardSelected: (card) { + setState(() { + selectedExchangeCards.add(card); + }); + }, + isCardSelected: (index) => selectedExchangeCards.any((card) => card['id'] == index), + canSelectMore: () => selectedExchangeCards.length < 8, + ); + } + @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), elevation: 0, leading: Container( margin: const EdgeInsets.all(8.0), - decoration: BoxDecoration( + decoration: const BoxDecoration( shape: BoxShape.circle, - color: const Color(0xFFD6A067), + color: Color(0xFFD6A067), ), child: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.black), onPressed: () => Navigator.pop(context), ), ), - title: const Text( - 'Создание обмена', - style: TextStyle( - color: Colors.black, - fontSize: 18.0, - ), - ), + ), body: Column( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - childAspectRatio: 0.7, - crossAxisSpacing: 10.0, - mainAxisSpacing: 10.0, - ), - itemCount: 12, - itemBuilder: (context, index) { - return _buildCardItem(); - }, + // Заголовок "Ваша карта для обмена" + Padding( + padding: const EdgeInsets.only(top: 20.0), + child: Text( + widget.initialExchangeItem != null ? 'Вы получите' : 'Ваша карта для обмена', + style: const TextStyle( + color: Colors.black, + fontSize: 20.0, + fontFamily: 'Jost', ), ), ), - + // Карточка для выбора своей карты (одноразовый выбор) Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - onPressed: () { - Navigator.pop(context); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), + padding: const EdgeInsets.only(top: 10.0), + child: selectedTopCard == null + ? GestureDetector( + onTap: _showTopCardSelectionDialog, + child: _buildExchangeCardVisual( + width: 78, + height: 112, + content: Center( + child: Icon( + Icons.add, + size: 20, + color: Colors.black.withOpacity(0.5), + ), + ), + ), + ) + : _buildExchangeCardVisual( + width: 78, + height: 112, + content: Center(child: Text('Карта ${selectedTopCard!['id']}', style: const TextStyle(color: Colors.black, fontSize: 10, fontFamily: 'Jost'),)), + ), + ), + const SizedBox(height: 20.0), + // Текст для карт обмена + Column( + children: [ + Text( + widget.initialExchangeItem != null ? 'Вы готовы' : 'Карта, на которую вы', + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.black, + fontSize: 20.0, + fontFamily: 'Jost', ), ), - child: const Text( - 'Создать обмен', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, + Text( + widget.initialExchangeItem != null ? 'предложить взамен' : 'готовы произвести обмен', + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.black, + fontSize: 20.0, + fontFamily: 'Jost', ), ), - ), + ], ), - + const SizedBox(height: 10.0), + // Ряды карточек для обмена (многоразовый выбор) + Expanded( + child: _buildExchangeCardsRow(), + ), + // Кнопка создания обмена Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16.0), - topRight: Radius.circular(16.0), - ), + width: double.infinity, + padding: EdgeInsets.only( + left: MediaQuery.of(context).size.width * 0.06, + right: MediaQuery.of(context).size.width * 0.06, + bottom: MediaQuery.of(context).padding.bottom + 12.0, + top: 4.0, ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - - switch (index) { - case 0: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 2: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } + child: LayoutBuilder( + builder: (context, constraints) { + final screenWidth = MediaQuery.of(context).size.width; + final screenHeight = MediaQuery.of(context).size.height; + final diagonal = math.sqrt(screenWidth * screenWidth + screenHeight * screenHeight); + final buttonHeight = diagonal * 0.065; + + return SizedBox( + height: buttonHeight, + width: double.infinity, + child: ElevatedButton( + onPressed: selectedTopCard == null || ((widget.initialExchangeItem != null || widget.cardId == null) && selectedExchangeCards.isEmpty) + ? null + : () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Container( + width: 367, + height: 61, + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(15.0), + ), + padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), + child: Center( + child: Text( + widget.initialExchangeItem != null ? 'Обмен успешно отправлен' : 'Обмен создан', + style: const TextStyle( + color: Colors.black, + fontSize: 18.0, + fontWeight: FontWeight.w500, + fontFamily: 'Jost', + ), + textAlign: TextAlign.center, + ), + ), + ), + backgroundColor: Colors.transparent, + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + margin: EdgeInsets.only( + bottom: MediaQuery.of(context).size.height - 200, + left: 16, + right: 16, + ), + elevation: 0, + ), + ); + + Future.delayed(const Duration(milliseconds: 500), () { + if (context.mounted) { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => MainScreen( + initialIndex: 3, + notification: widget.initialExchangeItem != null ? 'Обмен успешно отправлен' : 'Обмен создан', + ), + ), + (route) => false, + ); + } + }); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + padding: const EdgeInsets.symmetric(horizontal: 16.0), + disabledBackgroundColor: const Color(0xFFD6A067).withOpacity(0.7), + disabledForegroundColor: Colors.black.withOpacity(0.5), + ), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + widget.initialExchangeItem != null ? 'Предложить обмен' : 'Создать обмен', + style: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.w500, + fontFamily: 'Roboto', + ), + ), + ), + ), + ); }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.home), - label: 'Гл.меню', - ), - BottomNavigationBarItem( - icon: Icon(Icons.book), - label: '', - ), - BottomNavigationBarItem( - icon: Icon(Icons.storefront), - label: 'Магазин', - ), - BottomNavigationBarItem( - icon: Icon(Icons.people), - label: 'Обменчик', - ), - ], ), ), ], ), + bottomNavigationBar: null, ); } - Widget _buildCardItem() { + Widget _buildExchangeCardVisual({Widget? content, double width = 80, double height = 120, VoidCallback? onRemove}) { + // Calculate internal heights based on the provided height + double totalNonContentHeight = 3 + 3 + 3 + 2 + 1; // Borders, padding, and line from inventory card + double availableContentHeight = height - totalNonContentHeight; + double imageHeight = availableContentHeight * 0.65; // Approximate proportion for image area + double rarityHeight = availableContentHeight * 0.2; // Approximate proportion for rarity area + + // Ensure calculated heights are non-negative + imageHeight = imageHeight > 0 ? imageHeight : 0; + rarityHeight = rarityHeight > 0 ? rarityHeight : 0; + return Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(6.0), - border: Border.all(color: Colors.black, width: 1), - ), - child: Column( + width: width, + height: height, + child: Stack( + clipBehavior: Clip.none, children: [ - Expanded( - flex: 2, + // Внешняя тонкая черная рамка + Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), + ), + ), + // Прослойка цвета карточки + Padding( + padding: const EdgeInsets.all(2.0), child: Container( - width: double.infinity, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - border: Border( - bottom: BorderSide(color: Colors.black, width: 1), - ), + decoration: BoxDecoration( + color: const Color(0xFFD6A067), // Use the same color as inventory card + borderRadius: BorderRadius.circular(7), ), ), ), - - Expanded( - flex: 1, + // Внутренняя тонкая черная рамка + Padding( + padding: const EdgeInsets.all(4.0), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.black, width: 2), + ), + ), + ), + // Основная карточка + Padding( + padding: const EdgeInsets.all(6.0), child: Container( - width: double.infinity, - color: const Color(0xFFD6A067), + decoration: BoxDecoration( + color: const Color(0xFFD6A067), // Use the same color as inventory card + borderRadius: BorderRadius.circular(5.0), + ), + child: Column( + children: [ + Expanded( + flex: 8, + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(5.0), + topRight: Radius.circular(5.0), + ), + child: Container( + color: const Color(0xFFEAD7C3), // Background color for image area + height: imageHeight, + child: content ?? const Center( // Use provided content or default placeholder + child: Text( + 'Нет изображения', + style: TextStyle(color: Colors.black45, fontSize: 10, fontFamily: 'Jost'), // Adjusted font size + textAlign: TextAlign.center, + ), + ), + ), + ), + ), + Container( + height: 3, + color: Colors.black, // Separator color + ), + Container( + height: rarityHeight, + decoration: const BoxDecoration( + color: Color(0xFFD6A067), // Rarity section color + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(5.0), + bottomRight: Radius.circular(5.0), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: List.generate(4, (i) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 1.0), + child: Image.asset( + 'assets/icons/редкость.png', // Rarity icon + height: rarityHeight > 0 ? rarityHeight * 0.5 : 0, // Adjust icon size proportionally + ), + )), + ), + ), + ], + ), ), ), + // Positioned close button - now a direct child of the outer Stack + if (onRemove != null) // Only show if onRemove callback is provided + Positioned( + top: 0, // Adjust position as needed + right: -3, // Adjust position as needed + child: GestureDetector( + onTap: onRemove, + child: Container( + width: 16, // Adjusted size + height: 16, // Adjusted size + decoration: BoxDecoration( + color: const Color(0xFFFFF4E3), + borderRadius: BorderRadius.circular(3), // Adjusted radius + border: Border.all(color: Colors.black, width: 1), // Match style + ), + child: const Center( + child: Icon( + Icons.close, + size: 12, // Adjusted icon size + color: Colors.black, // Match color + ), + ), + ), + ), + ), ], ), ); } + + Widget _buildExchangeCardsRow() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Первый ряд карточек (максимум 4) + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + ...selectedExchangeCards.take(4).map((card) => Padding( + padding: const EdgeInsets.only(right: 10.0), + child: _buildExchangeCardVisual( + width: 78, + height: 112, + content: Center(child: Text('Карта ${card['id']}', style: const TextStyle(color: Colors.black, fontSize: 10, fontFamily: 'Jost'),)), + onRemove: () { + setState(() { + selectedExchangeCards.remove(card); + }); + }, + ), + )).toList(), + if (selectedExchangeCards.length < 4) + GestureDetector( + onTap: _showLargeCardSelectionDialog, + child: _buildExchangeCardVisual( + width: 78, + height: 112, + content: Center( + child: Icon( + Icons.add, + size: 20, + color: Colors.black.withOpacity(0.5), + ), + ), + ), + ), + ], + ), + ), + ), + // Второй ряд карточек (максимум 4) + if (selectedExchangeCards.length >= 4) + Padding( + padding: const EdgeInsets.only(left: 16.0, top: 10.0), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + ...selectedExchangeCards.skip(4).take(4).map((card) => Padding( + padding: const EdgeInsets.only(right: 10.0), + child: _buildExchangeCardVisual( + width: 78, + height: 112, + content: Center(child: Text('Карта ${card['id']}', style: const TextStyle(color: Colors.black, fontSize: 10, fontFamily: 'Jost'),)), + onRemove: () { + setState(() { + selectedExchangeCards.remove(card); + }); + }, + ), + )).toList(), + if (selectedExchangeCards.length < 8) + GestureDetector( + onTap: _showLargeCardSelectionDialog, + child: _buildExchangeCardVisual( + width: 78, + height: 112, + content: Center( + child: Icon( + Icons.add, + size: 20, + color: Colors.black.withOpacity(0.5), + ), + ), + ), + ), + ], + ), + ), + ), + ], + ); + } } \ No newline at end of file diff --git a/frontend/lib/views/email_verification_screen.dart b/frontend/lib/views/email_verification_screen.dart index dd7ef13..ab2d776 100644 --- a/frontend/lib/views/email_verification_screen.dart +++ b/frontend/lib/views/email_verification_screen.dart @@ -5,10 +5,12 @@ import '../controllers/auth_controller.dart'; class EmailVerificationScreen extends StatefulWidget { final String email; + final String tempToken; const EmailVerificationScreen({ super.key, required this.email, + required this.tempToken, }); @override @@ -55,12 +57,12 @@ class _EmailVerificationScreenState extends State { if (_formKey.currentState!.validate()) { try { await Provider.of(context, listen: false) - .verifyEmail(widget.email, _codeController.text.trim(), context: context); + .verifyEmail(widget.tempToken, _codeController.text.trim(), context: context); } catch (e) { if (!mounted) return; - final errorText = e.toString().replaceAll('', ''); + final errorText = e.toString().replaceAll('Exception: ', ''); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -88,8 +90,7 @@ class _EmailVerificationScreenState extends State { } catch (e) { if (!mounted) return; - // Получаем текст ошибки без "Exception: " - final errorText = e.toString().replaceAll('', ''); + final errorText = e.toString().replaceAll('Exception: ', ''); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -106,6 +107,26 @@ class _EmailVerificationScreenState extends State { } } + String _obscureEmail(String email) { + final parts = email.split('@'); + if (parts.length != 2) return email; + + final name = parts[0]; + final domain = parts[1]; + + final obscuredName = name.length > 2 + ? '${name.substring(0, 2)}${'*' * (name.length - 2)}' + : name; + + return '$obscuredName@$domain'; + } + + String _formatTimeLeft(int seconds) { + final minutes = seconds ~/ 60; + final remainingSeconds = seconds % 60; + return '$minutes:${remainingSeconds.toString().padLeft(2, '0')}'; + } + @override Widget build(BuildContext context) { final authController = Provider.of(context); @@ -113,10 +134,10 @@ class _EmailVerificationScreenState extends State { final obscuredEmail = _obscureEmail(widget.email); return Scaffold( - + backgroundColor: const Color(0xFFFBF6EF), body: Column( children: [ - const SizedBox(height: 40), + const SizedBox(height: 80), Image.asset( 'assets/icons/карты.png', height: 80, @@ -129,6 +150,7 @@ class _EmailVerificationScreenState extends State { fontSize: 32, color: Color(0xFFD9A76A), fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), const SizedBox(height: 30), @@ -140,6 +162,7 @@ class _EmailVerificationScreenState extends State { style: TextStyle( fontSize: 24, color: Color(0xFF000000), + fontFamily: 'Jost', ), textAlign: TextAlign.center, ), @@ -160,6 +183,7 @@ class _EmailVerificationScreenState extends State { style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 16), @@ -168,18 +192,23 @@ class _EmailVerificationScreenState extends State { controller: _codeController, decoration: const InputDecoration( filled: true, - fillColor: Color(0xFFEDD6B0), + fillColor: Color(0xFFEAD7C3), border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.all(Radius.circular(8)), ), contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + labelStyle: TextStyle(fontFamily: 'Jost'), + hintStyle: TextStyle(fontFamily: 'Jost'), ), keyboardType: TextInputType.number, validator: (value) { if (value == null || value.isEmpty) { return 'Пожалуйста, введите код'; } + if (value.length != 6) { + return 'Код должен состоять из 6 цифр'; + } return null; }, ), @@ -200,6 +229,7 @@ class _EmailVerificationScreenState extends State { style: TextStyle( fontSize: 14, color: _resendTimeLeft > 0 ? Colors.grey : Colors.black, + fontFamily: 'Jost', ), ), ), @@ -234,74 +264,18 @@ class _EmailVerificationScreenState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + fontFamily: 'Roboto', ), ), ), ), - - const SizedBox(height: 16), - - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () => Navigator.pop(context), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: const Text( - 'Назад', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ), ], ), ), ), ), - - Container( - width: 100, - height: 4, - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(2), - ), - ), ], ), ); } - - String _obscureEmail(String email) { - if (email.isEmpty) return ''; - - final parts = email.split('@'); - if (parts.length != 2) return email; - - String username = parts[0]; - String domain = parts[1]; - - if (username.length <= 4) { - return '*' * username.length + '@' + domain; - } else { - return username.substring(0, 4) + '*' * (username.length - 4) + '@' + domain; - } - } - - String _formatTimeLeft(int seconds) { - int hours = seconds ~/ 3600; - int minutes = (seconds % 3600) ~/ 60; - int secs = seconds % 60; - return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}'; - } } \ No newline at end of file diff --git a/frontend/lib/views/exchange_details_screen.dart b/frontend/lib/views/exchange_details_screen.dart index 4c588c4..7879cfc 100644 --- a/frontend/lib/views/exchange_details_screen.dart +++ b/frontend/lib/views/exchange_details_screen.dart @@ -1,393 +1,385 @@ import 'package:flutter/material.dart'; -import 'home_screen.dart'; -import 'shop_screen.dart'; import 'exchanges_screen.dart'; +import '../main.dart'; -class ExchangeDetailsScreen extends StatefulWidget { - const ExchangeDetailsScreen({super.key}); +class ExchangeDetailsScreen extends StatelessWidget { + final ExchangeItem exchangeItem; - @override - State createState() => _ExchangeDetailsScreenState(); -} + const ExchangeDetailsScreen({super.key, required this.exchangeItem}); -class _ExchangeDetailsScreenState extends State { - int _currentIndex = 3; // Индекс вкладки "Обмены" в нижней навигации - @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон - appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.black), - onPressed: () => Navigator.pop(context), - ), - title: const Text( - 'Детали обмена', - style: TextStyle( - color: Colors.black, - fontSize: 20.0, - fontWeight: FontWeight.bold, - ), - ), + // Helper to get status text + String getStatusText(ExchangeStatus status) { + switch (status) { + case ExchangeStatus.pending: return 'Ожидает\nподтверждения'; + case ExchangeStatus.waiting: return 'Ожидает\nдействий'; + case ExchangeStatus.approved: return 'Завершен'; + case ExchangeStatus.rejected: return 'Отклонен'; + } + } + + return Dialog( + backgroundColor: const Color(0xFFEAD7C3), + insetPadding: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), + side: const BorderSide(color: Colors.black, width: 2), ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Информация об обмене - Card( - color: const Color(0xFFD6A067), - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 24.0), + child: SizedBox( + width: MediaQuery.of(context).size.width * 1.6, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Дата и никнейм в одной строке + Padding( + padding: const EdgeInsets.only(top: 4.0, left: 2.0, right: 54.0, bottom: 16.0), + child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - 'Обмен #12345', - style: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, + '12.05.2023', + style: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w500, + fontFamily: 'Jost', + color: Colors.black, ), ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10.0, - vertical: 4.0, - ), - decoration: BoxDecoration( - color: Colors.orange, - borderRadius: BorderRadius.circular(20.0), - ), - child: const Text( - 'В ожидании', - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - color: Colors.black, - ), + Text( + exchangeItem.nickname, + style: const TextStyle( + fontSize: 15.0, + fontWeight: FontWeight.w500, + fontFamily: 'Jost', + color: Colors.black87, ), ), ], ), - const SizedBox(height: 12.0), - const Text( - 'Дата создания: 12.05.2023', - style: TextStyle(fontSize: 14.0), - ), - const SizedBox(height: 4.0), - Row( - children: const [ - Icon(Icons.person, size: 16.0), - SizedBox(width: 4.0), - Text( - 'Пользователь: CardMaster2000', - style: TextStyle(fontSize: 14.0), + ), + // Карточки и статус как на скрине + Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Cards offered by the current user (stacked if more than one) + exchangeItem.myOfferedCardIds.length > 1 + ? Stack( + clipBehavior: Clip.none, // Allow cards to overlap + children: [ + // Show up to 3 cards in the stack (visual representation) + Positioned( + left: 12, // Adjust position for stacking + top: 0, // Adjust position for stacking + child: _buildCardItem('Card Name', 'Rarity', width: 48, height: 64), // TODO: Use actual card details from cardId + ), + Positioned( + left: 6, // Adjust position for stacking + top: 0, // Adjust position for stacking + child: _buildCardItem('Card Name', 'Rarity', width: 48, height: 64), // TODO: Use actual card details from cardId + ), + // Always show the front card if count is at least 1 + _buildCardItem('Card Name', 'Rarity', width: 48, height: 64), // TODO: Use actual card details from cardId + ], + ) + : (exchangeItem.myOfferedCardIds.isNotEmpty + ? _buildCardItem('Card Name', 'Rarity', width: 48, height: 64) // Single card + : SizedBox.shrink()), // No cards + + // Статус по центру + Expanded( + child: Center( + child: Text( + getStatusText(exchangeItem.status), // Use the exchange status + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 20, + fontFamily: 'Jost', + ), + textAlign: TextAlign.center, + ), + ), ), + // Cards offered by the other user (stacked if more than one) + exchangeItem.otherUserOfferedCardIds.length > 1 + ? Stack( + clipBehavior: Clip.none, // Allow cards to overlap + children: [ + // Show up to 3 cards in the stack (visual representation) + Positioned( + left: 12, // Adjust position for stacking + top: 0, // Adjust position for stacking + child: _buildCardItem('Card Name', 'Rarity', width: 48, height: 64), // TODO: Use actual card details from cardId + ), + Positioned( + left: 6, // Adjust position for stacking + top: 0, // Adjust position for stacking + child: _buildCardItem('Card Name', 'Rarity', width: 48, height: 64), // TODO: Use actual card details from cardId + ), + // Always show the front card if count is at least 1 + _buildCardItem('Card Name', 'Rarity', width: 48, height: 64), // TODO: Use actual card details from cardId + ], + ) + : (exchangeItem.otherUserOfferedCardIds.isNotEmpty + ? _buildCardItem('Card Name', 'Rarity', width: 48, height: 64) // Single card + : SizedBox.shrink()), // No cards ], ), - const SizedBox(height: 4.0), - Row( - children: const [ - Icon(Icons.swap_horiz, size: 16.0), - SizedBox(width: 4.0), - Text( - 'Тип: 2 карточки на 1 карточку', - style: TextStyle(fontSize: 14.0), + ), + const SizedBox(height: 12.0), + // Conditionally display buttons based on status + if (exchangeItem.status == ExchangeStatus.waiting) // Show both buttons if waiting for actions + Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: double.infinity, + height: 60, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + textStyle: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + fontFamily: 'Roboto', + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + ), + onPressed: () { + Navigator.of(context).pop(); + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => MainScreen( + initialIndex: 3, + notification: 'Обмен принят', + ), + ), + (route) => false, + ); + }, + child: const Text('Принять'), + ), + ), + const SizedBox(height: 12.0), + SizedBox( + width: double.infinity, + height: 60, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + textStyle: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + fontFamily: 'Roboto', + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + ), + onPressed: () { + Navigator.of(context).pop(); + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => MainScreen( + initialIndex: 3, + notification: 'Обмен отклонен', + ), + ), + (route) => false, + ); + }, + child: const Text('Отклонить'), + ), ), ], ), - ], - ), - ), - ), - - const SizedBox(height: 20.0), - - // Секция "Мои карточки" - const Text( - 'Мои карточки для обмена:', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12.0), - SizedBox( - height: 180, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: 2, - itemBuilder: (context, index) { - return _buildCardItem('Карточка ${index + 1}', 'Редкость: Обычная'); - }, - ), - ), - - const SizedBox(height: 20.0), - - // Секция "Карточки пользователя" - const Text( - 'Карточки пользователя для обмена:', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12.0), - SizedBox( - height: 180, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: 1, - itemBuilder: (context, index) { - return _buildCardItem('Карточка пользователя', 'Редкость: Редкая'); - }, - ), - ), - - const SizedBox(height: 24.0), - - // Кнопки действий - Row( - children: [ - Expanded( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), + ) + else if (exchangeItem.status == ExchangeStatus.pending) // Show only Cancel button if pending + Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: SizedBox( + width: double.infinity, + height: 60, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + textStyle: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + fontFamily: 'Roboto', + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + ), + onPressed: () { + Navigator.of(context).pop(); + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => MainScreen( + initialIndex: 3, + notification: 'Обмен отклонен', + ), + ), + (route) => false, + ); + }, + child: const Text('Отменить'), // Text for cancelling pending exchange + ), ), - ), - onPressed: () { - // Логика отклонения обмена - _showConfirmationDialog('Отклонить обмен?', true); - }, - child: const Text('Отклонить'), ), + ], + ), + ), + ), + // Крестик в стиле магазина с внутренним отступом + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: Color(0xFFD6A067), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.black, width: 3), ), - const SizedBox(width: 12.0), - Expanded( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - onPressed: () { - // Логика принятия обмена - _showConfirmationDialog('Принять обмен?', false); - }, - child: const Text('Принять'), + child: const Center( + child: Icon( + Icons.close, + color: Colors.black, + size: 28, ), ), - ], - ), - - const SizedBox(height: 16.0), - - // Кнопка отмены (для своих обменов) - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 12.0), - minimumSize: const Size(double.infinity, 48.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - onPressed: () { - // Логика отмены обмена - _showConfirmationDialog('Вы уверены, что хотите отменить свой обмен?', true); - }, - child: const Text( - 'Отменить обмен', - style: TextStyle(fontWeight: FontWeight.bold), ), ), - ], - ), - ), - bottomNavigationBar: Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - - // Навигация в зависимости от выбранного индекса - switch (index) { - case 0: // Главное меню - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 2: // Магазин - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: // Обмены - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - label: '', - ), - ], - ), + ), + ], ), ); } - - // Метод для создания виджета карточки - Widget _buildCardItem(String title, String rarity) { + + Widget _buildCardItem(String title, String rarity, {double width = 48, double height = 64}) { + // Precise calculation of vertical space consumed by non-content elements: + // Outer border (top + bottom): 1.5 + 1.5 = 3 + // Padding (top + bottom) within outer border: 1.5 + 1.5 = 3 + // Inner border (top + bottom) after first padding: 1.5 + 1.5 = 3 + // Padding (top + bottom) after inner border: 1.0 + 1.0 = 2 + // Separator line: 1 + double totalNonContentHeight = 3 + 3 + 3 + 2 + 1; // Total vertical space used by borders, padding, and line + + double availableContentHeight = height - totalNonContentHeight; // Height remaining for image and rarity areas + + // Distribute available height based on desired proportions (approx. 65/35 split) + double imageHeight = availableContentHeight * 0.65; + double rarityHeight = availableContentHeight * 0.2; + + // Ensure calculated heights are non-negative + imageHeight = imageHeight > 0 ? imageHeight : 0; + rarityHeight = rarityHeight > 0 ? rarityHeight : 0; + return Container( - width: 120.0, - margin: const EdgeInsets.only(right: 12.0), + width: width, + height: height, + alignment: Alignment.center, decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black54, width: 1), + color: Colors.transparent, + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.black, width: 1.5), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Изображение карточки (заглушка) - Container( - height: 100.0, - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(8.0), - topRight: Radius.circular(8.0), - ), - ), - child: const Center( - child: Icon(Icons.image, size: 40.0, color: Colors.black54), - ), + child: Padding( + padding: const EdgeInsets.all(0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(3), ), - // Информация о карточке - Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, + child: Padding( + padding: const EdgeInsets.all(1.5), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(2), + border: Border.all(color: Colors.black, width: 1.5), + ), + child: Padding( + padding: EdgeInsets.zero, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(1.0), ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4.0), - Text( - rarity, - style: const TextStyle( - fontSize: 12.0, + child: Column( + children: [ + // Image placeholder area + Container( + height: imageHeight, // Use precisely calculated height + decoration: const BoxDecoration( + color: Color(0xFFEAD7C3), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(1.0), + topRight: Radius.circular(1.0), + ), + ), + child: const Center( + child: Text( + 'Нет \n изоб', + style: TextStyle(color: Colors.black45, fontSize: 7), + textAlign: TextAlign.center, + ), + ), + ), + Container( + height: 1, + color: Colors.black, + ), + // Rarity icons area + Container( + height: rarityHeight, // Use precisely calculated height + decoration: const BoxDecoration( + color: Color(0xFFD6A067), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(1.0), + bottomRight: Radius.circular(1.0), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(4, (i) => Image.asset( + 'assets/icons/редкость.png', + height: rarityHeight > 0 ? rarityHeight * 0.7 : 0, // Adjust icon size proportionally + )), + ), + ), + ], ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), - ], + ), ), ), - ], + ), ), ); } - - // Метод для отображения диалога подтверждения - void _showConfirmationDialog(String message, bool isDecline) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - backgroundColor: const Color(0xFFFFF4E3), - title: Text(message), - content: Text( - isDecline - ? 'Это действие невозможно будет отменить.' - : 'Карточки будут переданы между участниками обмена.', - ), - actions: [ - TextButton( - style: TextButton.styleFrom( - foregroundColor: Colors.black, - ), - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Отмена'), - ), - TextButton( - style: TextButton.styleFrom( - foregroundColor: isDecline ? Colors.red : Colors.green, - ), - onPressed: () { - // Выполнить действие - Navigator.of(context).pop(); - Navigator.of(context).pop(); // Вернуться на предыдущий экран - - // Здесь можно добавить вызов API для обработки обмена - }, - child: Text(isDecline ? 'Отклонить' : 'Принять'), - ), - ], - ); - }, - ); - } } \ No newline at end of file diff --git a/frontend/lib/views/exchange_proposal_screen.dart b/frontend/lib/views/exchange_proposal_screen.dart new file mode 100644 index 0000000..95fac9c --- /dev/null +++ b/frontend/lib/views/exchange_proposal_screen.dart @@ -0,0 +1,303 @@ +import 'package:flutter/material.dart'; +import 'package:cardly/views/create_exchange_screen.dart'; // Import to reuse _buildExchangeCardVisual +import 'package:cardly/views/exchanges_screen.dart'; // Import ExchangeItem +import 'package:cardly/main.dart'; // Import MainScreen + +class ExchangeProposalScreen extends StatelessWidget { + final ExchangeItem exchangeItem; + + const ExchangeProposalScreen({super.key, required this.exchangeItem}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFFBF6EF), // Match background color + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: Container( + margin: const EdgeInsets.all(8.0), + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Color(0xFFD6A067), + ), + child: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black), + onPressed: () => Navigator.pop(context), // Back button + ), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Title: Вы получите: + const Text( + 'Вы получите:', + style: TextStyle( + fontSize: 20.0, + fontFamily: 'Jost', + color: Colors.black, + ), + ), + const SizedBox(height: 10.0), + // Other user's offered card + // TODO: Display actual card based on passed data + _buildExchangeCardVisual( // Reuse card visual + width: 80, // Approximate size from image + height: 120, // Approximate size from image + content: Center(child: Text('Карта \n${exchangeItem.otherUserOfferedCardIds.isNotEmpty ? exchangeItem.otherUserOfferedCardIds[0] : 'N/A'}', textAlign: TextAlign.center, style: const TextStyle(color: Colors.black, fontSize: 10, fontFamily: 'Jost'),)), + ), + const SizedBox(height: 20.0), + // Title: Пользователь хочет получить: + const Text( + 'Пользователь хочет получить:', + style: TextStyle( + fontSize: 20.0, + fontFamily: 'Jost', + color: Colors.black, + ), + ), + const SizedBox(height: 10.0), + // Your available cards section + SizedBox( // Fixed height container for the card section + height: 300.0, // Set a fixed height to control button position + child: Center( // Keep the Center widget to center the Wrap block horizontally within the SizedBox + child: Wrap( + alignment: WrapAlignment.center, // Center items in the wrap layout + spacing: 10.0, // Horizontal space between cards + runSpacing: 10.0, // Vertical space between rows of cards + children: exchangeItem.myOfferedCardIds.map((cardId) { + // TODO: Display your actual cards from inventory + return _buildExchangeCardVisual( + width: 80, // Match size with the one above + height: 120, // Match size + content: Center(child: Text('Ваша\nКарта $cardId', textAlign: TextAlign.center, style: const TextStyle(color: Colors.black, fontSize: 10, fontFamily: 'Jost'),)), + ); + }).toList(), + ), + ), + ), + const SizedBox(height: 100.0), // Reduced space before buttons + Padding( + padding: EdgeInsets.symmetric( + horizontal: MediaQuery.of(context).size.width * 0.06, // Reduced padding to make buttons wider + ), + child: Column( + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.065, // Match the height from create exchange screen + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.pop(context); + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => const MainScreen(initialIndex: 3, notification: 'Обмен успешно совершен'), + ), + (route) => false, + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + padding: const EdgeInsets.symmetric(horizontal: 16.0), + ), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + exchangeItem.myOfferedCardIds.length == 1 ? 'Обменяться картой из списка' : 'Обменяться картами из списка', + style: const TextStyle( + fontSize: 18.0, // Slightly smaller font size + fontWeight: FontWeight.w500, // Slightly lighter weight + fontFamily: 'Roboto', + ), + ), + ), + ), + ), + const SizedBox(height: 10.0), + SizedBox( + height: MediaQuery.of(context).size.height * 0.065, // Match the height from create exchange screen + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CreateExchangeScreen( + initialExchangeItem: exchangeItem, + ), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + padding: const EdgeInsets.symmetric(horizontal: 16.0), + ), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + exchangeItem.myOfferedCardIds.length == 1 ? 'Предложить другую карту' : 'Предложить другие карты', + style: const TextStyle( + fontSize: 18.0, // Slightly smaller font size + fontWeight: FontWeight.w500, // Slightly lighter weight + fontFamily: 'Roboto', + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +// Reuse the _buildExchangeCardVisual from create_exchange_screen.dart +Widget _buildExchangeCardVisual({Widget? content, double width = 80, double height = 120, VoidCallback? onRemove}) { + // Calculate internal heights based on the provided height + double totalNonContentHeight = 3 + 3 + 3 + 2 + 1; // Borders, padding, and line from inventory card + double availableContentHeight = height - totalNonContentHeight; + double imageHeight = availableContentHeight * 0.65; // Approximate proportion for image area + double rarityHeight = availableContentHeight * 0.2; // Approximate proportion for rarity area + + // Ensure calculated heights are non-negative + imageHeight = imageHeight > 0 ? imageHeight : 0; + rarityHeight = rarityHeight > 0 ? rarityHeight : 0; + + return Container( + width: width, + height: height, + child: Stack( + clipBehavior: Clip.none, + children: [ + // Внешняя тонкая черная рамка + Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), + ), + ), + // Прослойка цвета карточки + Padding( + padding: const EdgeInsets.all(2.0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), // Use the same color as inventory card + borderRadius: BorderRadius.circular(7), + ), + ), + ), + // Внутренняя тонкая черная рамка + Padding( + padding: const EdgeInsets.all(4.0), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.black, width: 2), + ), + ), + ), + // Основная карточка + Padding( + padding: const EdgeInsets.all(6.0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), // Use the same color as inventory card + borderRadius: BorderRadius.circular(5.0), + ), + child: Column( + children: [ + Expanded( + flex: 8, + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(5.0), + topRight: Radius.circular(5.0), + ), + child: Container( + color: const Color(0xFFEAD7C3), // Background color for image area + height: imageHeight, + child: content ?? const Center( // Use provided content or default placeholder + child: Text( + 'Нет изображения', + style: TextStyle(color: Colors.black45, fontSize: 10, fontFamily: 'Jost'), // Adjusted font size + textAlign: TextAlign.center, + ), + ), + ), + ), + ), + Container( + height: 3, + color: Colors.black, // Separator color + ), + Container( + height: rarityHeight, + decoration: const BoxDecoration( + color: Color(0xFFD6A067), // Rarity section color + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(5.0), + bottomRight: Radius.circular(5.0), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: List.generate(4, (i) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 1.0), + child: Image.asset( + 'assets/icons/редкость.png', // Rarity icon + height: rarityHeight > 0 ? rarityHeight * 0.5 : 0, // Adjust icon size proportionally + ), + )), + ), + ), + ], + ), + ), + ), + // Positioned close button - now a direct child of the outer Stack + if (onRemove != null) // Only show if onRemove callback is provided + Positioned( + top: 0, // Adjust position as needed + right: -3, // Adjust position as needed + child: GestureDetector( + onTap: onRemove, + child: Container( + width: 16, // Adjusted size + height: 16, // Adjusted size + decoration: BoxDecoration( + color: const Color(0xFFFBF6EF), + borderRadius: BorderRadius.circular(3), // Adjusted radius + border: Border.all(color: Colors.black, width: 1), // Match style + ), + child: const Center( + child: Icon( + Icons.close, + size: 12, // Adjusted icon size + color: Colors.black, // Match color + ), + ), + ), + ), + ), + ], + ), + ); + } \ No newline at end of file diff --git a/frontend/lib/views/exchanges_screen.dart b/frontend/lib/views/exchanges_screen.dart index abbad54..9a17a35 100644 --- a/frontend/lib/views/exchanges_screen.dart +++ b/frontend/lib/views/exchanges_screen.dart @@ -1,32 +1,51 @@ import 'package:flutter/material.dart'; import 'exchange_details_screen.dart'; import 'create_exchange_screen.dart'; -import 'home_screen.dart'; -import 'shop_screen.dart'; -import 'inventory_screen.dart'; import 'profile_screen.dart'; +import 'search_players_screen.dart'; +import 'exchange_proposal_screen.dart'; +import '../services/api_service.dart'; +import '../models/trade_model.dart'; +import '../utils/error_formatter.dart'; +import '../utils/auth_utils.dart'; +import 'package:provider/provider.dart'; +import '../controllers/auth_controller.dart'; +import '../services/analytics_service.dart'; class ExchangesScreen extends StatefulWidget { - const ExchangesScreen({super.key}); + final int? initialTabIndex; + const ExchangesScreen({super.key, this.initialTabIndex}); @override State createState() => _ExchangesScreenState(); } class _ExchangesScreenState extends State with SingleTickerProviderStateMixin { - int _currentIndex = 3; late TabController _tabController; String _sortOption = 'По дате'; bool _showSortOptions = false; - + bool _isLoading = false; + String? _error; + @override void initState() { super.initState(); - _tabController = TabController(length: 2, vsync: this); + AnalyticsService.trackScreenView('exchanges_screen'); + _tabController = TabController(length: 2, vsync: this, initialIndex: widget.initialTabIndex ?? 0); + _tabController.addListener(_handleTabChange); + } + + void _handleTabChange() { + if (_tabController.index == 1) { // Мои обмены + if (!AuthUtils.checkGuestAccess(context, 'my_exchanges_screen')) { + _tabController.animateTo(0); // Возвращаемся на вкладку "Обмен" + } + } } @override void dispose() { + _tabController.removeListener(_handleTabChange); _tabController.dispose(); super.dispose(); } @@ -34,10 +53,11 @@ class _ExchangesScreenState extends State with SingleTickerProv @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), elevation: 0, + automaticallyImplyLeading: false, title: Container( padding: const EdgeInsets.all(8.0), ), @@ -47,33 +67,32 @@ class _ExchangesScreenState extends State with SingleTickerProv child: Container( margin: const EdgeInsets.symmetric(horizontal: 16.0), decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(12.0), ), child: TabBar( controller: _tabController, - labelColor: Colors.black, - unselectedLabelColor: Colors.black, + dividerColor: Colors.transparent, indicator: const BoxDecoration( color: Color(0xFFD6A067), - borderRadius: BorderRadius.all(Radius.circular(12.0)), - border: Border( - bottom: BorderSide( - color: Colors.black, - width: 3.0, - ), - ), + borderRadius: BorderRadius.all(Radius.circular(10.0)), ), + labelColor: Colors.black, + unselectedLabelColor: Colors.black, indicatorSize: TabBarIndicatorSize.tab, tabs: const [ Tab(text: 'Обмен'), Tab(text: 'Мои обмены'), ], + labelStyle: TextStyle(fontFamily: 'Jost'), + unselectedLabelStyle: TextStyle(fontFamily: 'Jost'), ), ), ), ), - body: Column( + body: Stack( + children: [ + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( @@ -83,10 +102,13 @@ class _ExchangesScreenState extends State with SingleTickerProv children: [ InkWell( onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const CreateExchangeScreen()), - ); + if (AuthUtils.checkGuestAccess(context, 'create_exchange_screen')) { + AnalyticsService.trackExchange(); + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const CreateExchangeScreen()), + ); + } }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 12.0), @@ -101,6 +123,7 @@ class _ExchangesScreenState extends State with SingleTickerProv style: TextStyle( fontSize: 14.0, fontWeight: FontWeight.bold, + fontFamily: 'Roboto', ), ), SizedBox(width: 6.0), @@ -119,10 +142,15 @@ class _ExchangesScreenState extends State with SingleTickerProv ), Container( - child: IconButton( - icon: Image.asset('assets/icons/поиск.png', height: 32), + icon: Image.asset('assets/icons/поиск.png', height: 26), onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const SearchPlayersModal(), + ); }, ), ), @@ -148,6 +176,7 @@ class _ExchangesScreenState extends State with SingleTickerProv style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), const SizedBox(width: 4.0), @@ -158,14 +187,42 @@ class _ExchangesScreenState extends State with SingleTickerProv ], ), ), - + ], + ), + ), + + const SizedBox(height: 8.0), + + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildExchangesTab(), + _buildMyExchangesTab(), + ], + ), + ), + ], + ), + + // Sorting options dropdown (Positioned overlay) if (_showSortOptions) - Container( - margin: const EdgeInsets.only(top: 8.0), + Positioned( + top: 110, // Approximate position below the Sorting text + left: 16, // Align with the left padding + child: Container( + width: 120.0, // Set a fixed width to match inventory_screen dropdown size padding: const EdgeInsets.all(12.0), decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8.0), + color: const Color(0xFFEAD7C3), // Background color from inventory_screen + borderRadius: BorderRadius.circular(10.0), // Border radius from inventory_screen + boxShadow: [ // Box shadow from inventory_screen + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -173,7 +230,7 @@ class _ExchangesScreenState extends State with SingleTickerProv InkWell( onTap: () { setState(() { - _sortOption = 'По дате'; + _sortOption = 'По дате'; // Option 1 _showSortOptions = false; }); }, @@ -184,15 +241,15 @@ class _ExchangesScreenState extends State with SingleTickerProv style: TextStyle( fontSize: 14.0, fontWeight: _sortOption == 'По дате' ? FontWeight.bold : FontWeight.normal, + fontFamily: 'Jost', ), ), ), ), - const Divider(), InkWell( onTap: () { setState(() { - _sortOption = 'По редкости'; + _sortOption = 'По редкости'; // Option 2 _showSortOptions = false; }); }, @@ -203,225 +260,437 @@ class _ExchangesScreenState extends State with SingleTickerProv style: TextStyle( fontSize: 14.0, fontWeight: _sortOption == 'По редкости' ? FontWeight.bold : FontWeight.normal, + fontFamily: 'Jost', ), ), ), ), ], ), - ), - ], - ), - ), - - const SizedBox(height: 8.0), - - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - _buildExchangesTab(), - _buildMyExchangesTab(), - ], ), ), ], ), - bottomNavigationBar: Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - switch (index) { - case 0: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 1: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const InventoryScreen()), - (route) => false, - ); - break; - case 2: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - showSelectedLabels: true, - showUnselectedLabels: true, - selectedIconTheme: const IconThemeData( - size: 28, - ), - unselectedIconTheme: const IconThemeData( - size: 24, - ), - selectedLabelStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - ), - unselectedLabelStyle: const TextStyle( - fontSize: 11, - ), - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/главная.png', height: 24), - ), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/Инвентарь.png', height: 24), - ), - label: 'Инвентарь', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/магазин.png', height: 24), - ), - label: 'Магазин', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/обменник.png', height: 24), - ), - label: 'Обменник', - ), - ], - ), - ), + bottomNavigationBar: null, ); } Widget _buildExchangesTab() { return ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - itemCount: 5, + padding: const EdgeInsets.all(12.0), + itemCount: 5, itemBuilder: (context, index) { - return _buildCardExchangeItem(); + // Alternate between 1 and 3 for demonstration counts + final myCardCount = (index % 2 == 0) ? 1 : 5; + final otherUserCardCount = 1; // Modified to always be 1 + // Use dummy data for demonstration in this tab + final dummyNickname = 'User ${(index % 3) + 1}'; + final dummyExchangeItem = ExchangeItem( + date: 'N/A', // Dummy date + status: ExchangeStatus.pending, // Dummy status + myOfferedCardIds: List.generate(myCardCount, (i) => i + 1), // Dummy IDs + otherUserOfferedCardIds: List.generate(otherUserCardCount, (i) => i + 11), // Dummy IDs + nickname: dummyNickname, + ); + return _buildCardExchangeItem( + myOfferedCount: myCardCount, + otherUserOfferedCount: otherUserCardCount, + otherUserName: dummyNickname, + exchangeItem: dummyExchangeItem, + isMyExchange: false, + showLeftPlus: index == 0, // Show plus only on the left card of the first item + ); }, ); } Widget _buildMyExchangesTab() { + // Using dummy ExchangeItem data for demonstration + final List myExchanges = [ + ExchangeItem(date: '2023-10-27', status: ExchangeStatus.pending, myOfferedCardIds: [1], otherUserOfferedCardIds: [5], nickname: 'Alex'), + ExchangeItem(date: '2023-10-26', status: ExchangeStatus.approved, myOfferedCardIds: [2, 3, 4], otherUserOfferedCardIds: [6], nickname: 'Maria'), + ExchangeItem(date: '2023-10-25', status: ExchangeStatus.rejected, myOfferedCardIds: [7], otherUserOfferedCardIds: [8, 9, 10], nickname: 'Ivan'), + ExchangeItem(date: '2023-10-24', status: ExchangeStatus.waiting, myOfferedCardIds: [11, 12], otherUserOfferedCardIds: [14], nickname: 'David'), // Modified to prevent stack on right side + ]; + return ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - itemCount: 3, + padding: const EdgeInsets.all(12.0), + itemCount: myExchanges.length, itemBuilder: (context, index) { - return _buildCardExchangeItem(); + final exchange = myExchanges[index]; + return _buildCardExchangeItem( + myOfferedCount: exchange.myOfferedCardIds.length, + otherUserOfferedCount: exchange.otherUserOfferedCardIds.length, + otherUserName: exchange.nickname, + exchangeItem: exchange, + isMyExchange: true, // Set to true for this tab + ); }, ); } - Widget _buildCardExchangeItem() { + Widget _buildCardExchangeItem({ + int myOfferedCount = 1, + int otherUserOfferedCount = 1, + required String otherUserName, + required ExchangeItem exchangeItem, + bool isMyExchange = false, + bool showLeftPlus = false, + }) { + // Helper to get status text + String getStatusText(ExchangeStatus status) { + switch (status) { + case ExchangeStatus.pending: return 'Ожидает\nподтверждения'; + case ExchangeStatus.waiting: return 'Ожидает\nдействий'; + case ExchangeStatus.approved: return 'Завершен'; + case ExchangeStatus.rejected: return 'Отклонен'; + } + } + return GestureDetector( onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const ExchangeDetailsScreen()), + if (!isMyExchange) { // If in 'Обмен' tab + if (showLeftPlus) { // If the left card has a plus icon + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CreateExchangeScreen(initialExchangeItem: exchangeItem), // Pass the exchangeItem + ), + ); + } else { // If in 'Обмен' tab and no plus icon (multiple cards) + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ExchangeProposalScreen(exchangeItem: exchangeItem), // Navigate to ExchangeProposalScreen + ), + ); + } + } else { // If in 'Мои обмены' tab, show ExchangeDetailsScreen dialog + showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: ExchangeDetailsScreen(exchangeItem: exchangeItem), + ); + }, ); + } }, child: Container( - margin: const EdgeInsets.only(bottom: 16.0), + margin: const EdgeInsets.only(bottom: 12.0), padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(8.0), ), child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Container( - width: 60.0, - height: 80.0, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 1), + // First card (offered by current user) - Stacked if 2 or more + SizedBox( + width: MediaQuery.of(context).size.width * 0.2, + child: Column( + children: [ + // Offset to align with right card visual (conditional based on tab) + SizedBox(height: 24.0), // Always apply height for alignment + myOfferedCount >= 2 + ? Stack( + clipBehavior: Clip.none, + children: [ + Positioned( + left: 16, + top: 0, + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.2, + child: AspectRatio( + aspectRatio: 0.7, + child: _buildSingleCardVisual(showPlus: showLeftPlus), + ), + ), + ), + Positioned( + left: 8, + top: 0, + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.2, + child: AspectRatio( + aspectRatio: 0.7, + child: _buildSingleCardVisual(showPlus: showLeftPlus), + ), + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.2, + child: AspectRatio( + aspectRatio: 0.7, + child: _buildSingleCardVisual(showPlus: showLeftPlus), + ), + ), + ], + ) + : (myOfferedCount == 1 + ? AspectRatio( + aspectRatio: 0.7, + child: _buildSingleCardVisual(showPlus: showLeftPlus), + ) + : SizedBox.shrink()), + ], ), ), + // Swap icon or Status text Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Icon(Icons.sync_alt, size: 24.0), + children: [ + isMyExchange + ? Text( + getStatusText(exchangeItem.status), + style: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + textAlign: TextAlign.center, + ) + : Padding( + padding: EdgeInsets.only(top: isMyExchange ? 18.0 : 0.0), + child: const Icon(Icons.sync_alt, size: 24.0), + ), ], ), ), - Stack( + // Second card(s) block (offered by other user) + SizedBox( + width: MediaQuery.of(context).size.width * 0.2 + (otherUserOfferedCount > 1 ? 16 : 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + children: [ + Text( + otherUserName, + style: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4.0), + ], + ), + otherUserOfferedCount >= 2 + ? Stack( + clipBehavior: Clip.none, + children: [ + Positioned( + left: 16, + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.2, + child: AspectRatio( + aspectRatio: 0.7, + child: _buildSingleCardVisual(showPlus: false), // Always false for right side + ), + ), + ), + if (otherUserOfferedCount >= 2) + Positioned( + left: 8, + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.2, + child: AspectRatio( + aspectRatio: 0.7, + child: _buildSingleCardVisual(showPlus: false), // Always false for right side + ), + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.2, + child: AspectRatio( + aspectRatio: 0.7, + child: _buildSingleCardVisual(showPlus: false), // Always false for right side + ), + ), + ], + ) + : (otherUserOfferedCount == 1 + ? AspectRatio( + aspectRatio: 0.7, + child: _buildSingleCardVisual(showPlus: false), // Always false for right side + ) + : SizedBox.shrink()), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildSingleCardVisual({bool showPlus = false}) { + return Stack( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), + ), + ), + Padding( + padding: const EdgeInsets.all(2.0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(7), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(4.0), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.black, width: 2), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(6.0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(5.0), + ), + child: Column( children: [ - Container( - width: 60.0, - height: 80.0, - margin: const EdgeInsets.only(top: 4.0, left: 4.0), - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 1), + Expanded( + flex: 8, + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(5.0), + topRight: Radius.circular(5.0), + ), + child: Container( + color: const Color(0xFFEAD7C3), + child: showPlus + ? const Center( + child: Icon( + Icons.add, + color: Colors.black, + size: 32, + ), + ) + : const Center( + child: Text( + 'Нет изображения', + style: TextStyle(color: Colors.black45, fontSize: 12), + ), + ), + ), ), ), Container( - width: 60.0, - height: 80.0, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 1), + height: 3, + color: Colors.black, + ), + Container( + height: 32, + decoration: const BoxDecoration( + color: Color(0xFFD6A067), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(5.0), + bottomRight: Radius.circular(5.0), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: List.generate(4, (i) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 1.0), + child: Image.asset( + 'assets/icons/редкость.png', + height: 14, + ), + )), ), ), ], ), - ], + ), ), - ), + ], ); } + + Future _loadTrades() async { + try { + setState(() { + _isLoading = true; + }); + // Implement the logic to load trades from the API + // This is a placeholder and should be replaced with actual implementation + // For example, you can use a Future.delayed to simulate a delay + await Future.delayed(Duration(seconds: 2)); + setState(() { + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _error = ErrorFormatter.formatError(e); + }); + } + } + + Future _acceptTrade(Trade trade) async { + try { + await ApiService().acceptTrade(trade.trade_ID); + AnalyticsService.trackExchangeComplete(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Обмен принят')), + ); + _loadTrades(); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } + + Future _rejectTrade(Trade trade) async { + try { + await ApiService().rejectTrade(trade.trade_ID); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Обмен отклонен')), + ); + _loadTrades(); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } + + Future _cancelTrade(Trade trade) async { + try { + await ApiService().cancelTrade(trade.trade_ID); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Обмен отменен')), + ); + _loadTrades(); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } } enum ExchangeStatus { @@ -434,15 +703,15 @@ enum ExchangeStatus { class ExchangeItem { final String date; final ExchangeStatus status; - final int fromCards; - final int toCards; + final List myOfferedCardIds; + final List otherUserOfferedCardIds; final String nickname; ExchangeItem({ required this.date, required this.status, - required this.fromCards, - required this.toCards, + required this.myOfferedCardIds, + required this.otherUserOfferedCardIds, required this.nickname, }); } \ No newline at end of file diff --git a/frontend/lib/views/forgot_password_screen.dart b/frontend/lib/views/forgot_password_screen.dart index 74946dd..08f9704 100644 --- a/frontend/lib/views/forgot_password_screen.dart +++ b/frontend/lib/views/forgot_password_screen.dart @@ -69,6 +69,7 @@ class _ForgotPasswordScreenState extends State { fontSize: 32, color: Color(0xFFD9A76A), fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), const SizedBox(height: 40), @@ -86,6 +87,7 @@ class _ForgotPasswordScreenState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), const SizedBox(height: 8), @@ -99,6 +101,8 @@ class _ForgotPasswordScreenState extends State { borderRadius: BorderRadius.all(Radius.circular(8)), ), contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + labelStyle: TextStyle(fontFamily: 'Jost'), + hintStyle: TextStyle(fontFamily: 'Jost'), ), keyboardType: TextInputType.emailAddress, validator: (value) { @@ -141,6 +145,7 @@ class _ForgotPasswordScreenState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + fontFamily: 'Roboto', ), ), ), diff --git a/frontend/lib/views/forgot_password_verification_screen.dart b/frontend/lib/views/forgot_password_verification_screen.dart index c478fcb..03628e7 100644 --- a/frontend/lib/views/forgot_password_verification_screen.dart +++ b/frontend/lib/views/forgot_password_verification_screen.dart @@ -150,6 +150,7 @@ class _ForgotPasswordVerificationScreenState extends State createState() => _HomeScreenState(); -} - -class _HomeScreenState extends State { - int _currentIndex = 0; - @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), elevation: 0, leading: Padding( padding: const EdgeInsets.only(left: 16.0), @@ -33,12 +28,14 @@ class _HomeScreenState extends State { child: InkWell( borderRadius: BorderRadius.circular(20.0), onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const ProfileScreen()), - ); + if (AuthUtils.checkGuestAccess(context, 'profile_screen')) { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ProfileScreen()), + ); + } }, - child: Image.asset('assets/icons/профиль.png', height: 22), + child: Image.asset('assets/icons/профиль.png', height: 24), ), ), ), @@ -46,25 +43,32 @@ class _HomeScreenState extends State { IconButton( icon: Image.asset( 'assets/icons/поиск.png', - height: 32, + height: 26, color: Colors.black, ), onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SearchPlayersScreen(), - ), + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const SearchPlayersModal(), ); }, ), IconButton( icon: Image.asset( 'assets/icons/уведомления.png', - height: 36, + height: 30, color: Colors.black, ), - onPressed: null, + onPressed: () { + if (AuthUtils.checkGuestAccess(context, 'notifications_screen')) { + showDialog( + context: context, + builder: (context) => const NotificationsModal(), + ); + } + }, ), ], ), @@ -78,6 +82,7 @@ class _HomeScreenState extends State { style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), ), @@ -95,10 +100,45 @@ class _HomeScreenState extends State { ), itemCount: 8, itemBuilder: (context, index) { - return Container( - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(12.0), + // Dummy collection names for demonstration + final List collectionNames = [ + 'Весна', + 'Лето', + 'Осень', + 'Зима', + 'Горы', + 'Море', + 'Лес', + 'Город' + ]; + + return InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => InventoryScreen( + collectionName: collectionNames[index], + ), + ), + ); + }, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(12.0), + ), + child: Center( + child: Text( + collectionNames[index], + style: const TextStyle( + color: Colors.black, + fontSize: 14, + fontWeight: FontWeight.w500, + fontFamily: 'Jost', + ), + ), + ), ), ); }, @@ -108,31 +148,40 @@ class _HomeScreenState extends State { const SizedBox(height: 120.0), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: EdgeInsets.symmetric( + horizontal: MediaQuery.of(context).size.width * 0.06, + ), child: SizedBox( width: double.infinity, + height: MediaQuery.of(context).size.height * 0.065, child: ElevatedButton( onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const CreateCardScreen(), - ), - ); + if (AuthUtils.checkGuestAccess(context, 'create_card_screen')) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const CreateCardScreen(), + ), + ); + } }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFD6A067), foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 24.0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.0), ), + padding: const EdgeInsets.symmetric(horizontal: 16.0), ), - child: const Text( - 'Создай свою уникальную карточку', - style: TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.bold, + child: const FittedBox( + fit: BoxFit.scaleDown, + child: Text( + 'Создай свою уникальную карточку', + style: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.w500, + fontFamily: 'Roboto', + ), ), ), ), @@ -142,44 +191,47 @@ class _HomeScreenState extends State { const SizedBox(height: 32.0), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: EdgeInsets.symmetric( + horizontal: MediaQuery.of(context).size.width * 0.06, + ), child: Row( children: [ Expanded( - child: ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const QuestsScreen(), + child: SizedBox( + height: MediaQuery.of(context).size.height * 0.085, + child: ElevatedButton( + onPressed: () { + if (AuthUtils.checkGuestAccess(context, 'quests_screen')) { + showQuestsDialog(context); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 24.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset( - 'assets/icons/квесты.png', - height: 42, - color: Colors.black, - ), - const SizedBox(height: 0.0), - const Text( - 'Квесты', - style: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/icons/квесты.png', + height: 28, + color: Colors.black, ), - ), - ], + const SizedBox(height: 4.0), + const Text( + 'Квесты', + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w500, + fontFamily: 'Roboto', + ), + ), + ], + ), ), ), ), @@ -187,40 +239,44 @@ class _HomeScreenState extends State { const SizedBox(width: 20.0), Expanded( - child: ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const NewsScreen(), + child: SizedBox( + height: MediaQuery.of(context).size.height * 0.085, + child: ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const NewsScreen(), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 24.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset( - 'assets/icons/новости.png', - height: 42, - color: Colors.black, - ), - const SizedBox(height: 0.0), - const Text( - 'Новости', - style: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/icons/новости.png', + height: 28, + color: Colors.black, ), - ), - ], + const SizedBox(height: 4.0), + const Text( + 'Новости', + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w500, + fontFamily: 'Roboto', + ), + ), + ], + ), ), ), ), @@ -229,115 +285,7 @@ class _HomeScreenState extends State { ), ], ), - - bottomNavigationBar: Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - switch (index) { - case 1: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const InventoryScreen()), - (route) => false, - ); - break; - case 2: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - showSelectedLabels: true, - showUnselectedLabels: true, - selectedIconTheme: const IconThemeData( - size: 28, - ), - unselectedIconTheme: const IconThemeData( - size: 24, - ), - selectedLabelStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - ), - unselectedLabelStyle: const TextStyle( - fontSize: 11, - ), - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/главная.png', height: 24), - ), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/Инвентарь.png', height: 24), - ), - label: 'Инвентарь', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/магазин.png', height: 24), - ), - label: 'Магазин', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/обменник.png', height: 24), - ), - label: 'Обменник', - ), - ], - ), - ), + bottomNavigationBar: null, ); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/lib/views/inventory_screen.dart b/frontend/lib/views/inventory_screen.dart index dca8cab..7d9d73b 100644 --- a/frontend/lib/views/inventory_screen.dart +++ b/frontend/lib/views/inventory_screen.dart @@ -1,53 +1,220 @@ import 'package:flutter/material.dart'; -import 'home_screen.dart'; -import 'shop_screen.dart'; -import 'exchanges_screen.dart'; import 'profile_screen.dart'; import 'card_detail_screen.dart'; +import 'search_players_screen.dart'; +import '../services/api_service.dart'; +import '../models/card_model.dart'; +import '../utils/error_formatter.dart'; +import '../utils/auth_utils.dart'; +import 'package:provider/provider.dart'; +import '../controllers/auth_controller.dart'; +import '../services/analytics_service.dart'; class InventoryScreen extends StatefulWidget { - const InventoryScreen({super.key}); + final bool isOtherUser; + final String? playerName; + final int? playerId; + final String? collectionName; + final bool isFromShop; + + const InventoryScreen({ + super.key, + this.isOtherUser = false, + this.playerName, + this.playerId, + this.collectionName, + this.isFromShop = false, + }); @override State createState() => _InventoryScreenState(); } class _InventoryScreenState extends State { - int _currentIndex = 1; - String _sortOption = 'По редкости'; - bool _showSortOptions = false; - + late bool _isLoading; + late List _cards; + late String _error; + String _sortBy = 'rarity'; + + @override + void initState() { + super.initState(); + _isLoading = true; + _cards = []; + _error = ''; + AnalyticsService.trackScreenView('inventory_screen'); + _loadInventory(); + } + + Future _loadInventory() async { + try { + setState(() { + _isLoading = true; + }); + + final cards = widget.isOtherUser + ? await ApiService().getOtherProfileInventory(widget.playerId ?? 0) + : await ApiService().getUserInventory(sortBy: _sortBy); + + setState(() { + _cards = (cards as List).map((item) => CardModel.fromJson(item)).toList(); + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _error = ErrorFormatter.formatError(e); + }); + } + } + + Future _disassembleCard(CardModel card) async { + try { + final coinsReceived = await ApiService().disassembleCard(card.card_ID); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Получено монет: $coinsReceived')), + ); + await _loadInventory(); // Перезагружаем инвентарь + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } + + void _showCardDetails(CardModel card) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CardDetailScreen( + card: card, + cardIndex: 0, + showExchangeButton: false, + isFromShop: false, + ), + ), + ); + } + @override Widget build(BuildContext context) { + if (Provider.of(context).currentUser?.isGuest ?? false) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!AuthUtils.checkGuestAccess(context, 'inventory_screen')) { + Navigator.of(context).pop(); + } + }); + return Container(); // Возвращаем пустой контейнер, пока происходит проверка + } + return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), elevation: 0, - leading: Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Container( - width: 40.0, - height: 40.0, - child: InkWell( - borderRadius: BorderRadius.circular(20.0), - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const ProfileScreen()), - ); - }, - child: Image.asset('assets/icons/профиль.png', height: 22), - ), - ), - ), - title: null, - centerTitle: true, - actions: [ + automaticallyImplyLeading: false, + titleSpacing: 0, + title: widget.isOtherUser + ? Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Container( + width: 40.0, + height: 40.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFFD6A067), + ), + child: InkWell( + borderRadius: BorderRadius.circular(20.0), + onTap: () { + Navigator.pop(context); + }, + child: const Icon( + Icons.arrow_back, + color: Colors.black, + size: 29.0, + ), + ), + ), + ), + const SizedBox(width: 8), + Text( + 'Инвентарь ${widget.playerName}', + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 18.0, + fontFamily: 'Jost', + ), + ), + ], + ) + : widget.collectionName != null + ? Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Container( + width: 40.0, + height: 40.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFFD6A067), + ), + child: InkWell( + borderRadius: BorderRadius.circular(20.0), + onTap: () { + Navigator.pop(context); + }, + child: const Icon( + Icons.arrow_back, + color: Colors.black, + size: 29.0, + ), + ), + ), + ), + const SizedBox(width: 8), + Text( + widget.isFromShop + ? 'Содержимое ${widget.collectionName}' + : 'Коллекция ${widget.collectionName}', + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 18.0, + fontFamily: 'Jost', + ), + ), + ], + ) + : Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Container( + width: 40.0, + height: 40.0, + child: InkWell( + borderRadius: BorderRadius.circular(20.0), + onTap: () { + if (AuthUtils.checkGuestAccess(context, 'profile_screen')) { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ProfileScreen()), + ); + } + }, + child: Image.asset('assets/icons/профиль.png', height: 24), + ), + ), + ), + centerTitle: false, + actions: widget.isOtherUser ? null : widget.collectionName != null ? null : [ Container( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(10.0), ), child: Row( @@ -59,6 +226,7 @@ class _InventoryScreenState extends State { color: Colors.black, fontSize: 16.0, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), SizedBox(width: 6.0), @@ -67,1030 +235,66 @@ class _InventoryScreenState extends State { ), ), IconButton( - icon: Image.asset('assets/icons/поиск.png', height: 32), - onPressed: () {}, + icon: Image.asset('assets/icons/поиск.png', height: 26), + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const SearchPlayersModal(), + ); + }, ), ], ), - body: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - child: Row( - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - child: InkWell( - onTap: () { - setState(() { - _showSortOptions = !_showSortOptions; - }); - }, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Сортировка:', - style: const TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 6.0), - Icon( - _showSortOptions ? Icons.arrow_drop_up : Icons.arrow_drop_down, - color: Colors.black, - ), - ], - ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _error.isNotEmpty + ? Center(child: Text(_error)) + : _cards.isEmpty + ? const Center(child: Text('Нет карт')) + : GridView.builder( + padding: const EdgeInsets.all(16.0), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.75, + crossAxisSpacing: 16.0, + mainAxisSpacing: 16.0, ), - ), - ], - ), - ), - - Expanded( - child: GridView.builder( - padding: const EdgeInsets.all(12.0), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - childAspectRatio: 0.7, - crossAxisSpacing: 8.0, - mainAxisSpacing: 12.0, - ), - itemCount: 20, - itemBuilder: (context, index) { - return _buildCardItem(index: index); - }, - ), - ), - ], - ), - - if (_showSortOptions) - Positioned( - top: 50, - left: 16, - child: Container( - padding: const EdgeInsets.all(12.0), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8.0), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - InkWell( - onTap: () { - setState(() { - _sortOption = 'По редкости'; - _showSortOptions = false; - }); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - 'По редкости', - style: TextStyle( - fontSize: 14.0, - fontWeight: _sortOption == 'По редкости' ? FontWeight.bold : FontWeight.normal, + itemCount: _cards.length, + itemBuilder: (context, index) { + final card = _cards[index]; + return GestureDetector( + onTap: () => _showCardDetails(card), + onLongPress: widget.isOtherUser ? null : () => _disassembleCard(card), + child: Card( + child: Column( + children: [ + Expanded( + child: Image.network( + card.imageURL, + fit: BoxFit.cover, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Text( + card.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Text('Редкость: ${card.rarity}'), + ], + ), + ), + ], + ), ), - ), - ), - ), - const Divider(), - InkWell( - onTap: () { - setState(() { - _sortOption = 'По коллекциям'; - _showSortOptions = false; - }); + ); }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - 'По коллекциям', - style: TextStyle( - fontSize: 14.0, - fontWeight: _sortOption == 'По коллекциям' ? FontWeight.bold : FontWeight.normal, - ), - ), - ), ), - ], - ), - ), - ), - ], - ), - bottomNavigationBar: Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - switch (index) { - case 0: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 2: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - showSelectedLabels: true, - showUnselectedLabels: true, - selectedIconTheme: const IconThemeData( - size: 28, - ), - unselectedIconTheme: const IconThemeData( - size: 24, - ), - selectedLabelStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - ), - unselectedLabelStyle: const TextStyle( - fontSize: 11, - ), - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/главная.png', height: 24), - ), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/Инвентарь.png', height: 24), - ), - label: 'Инвентарь', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/магазин.png', height: 24), - ), - label: 'Магазин', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/обменник.png', height: 24), - ), - label: 'Обменник', - ), - ], - ), - ), + bottomNavigationBar: null, ); } - - Widget _buildCardItem({int index = 0}) { - if (index == 0) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CardDetailScreen(cardIndex: index), - ), - ); - }, - child: AspectRatio( - aspectRatio: 3/4, - child: Stack( - children: [ - // Внешняя тонкая черная рамка - Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black, width: 2), - ), - ), - // Прослойка цвета карточки - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(7), - ), - ), - ), - // Внутренняя тонкая черная рамка - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 2), - ), - ), - ), - // Основная карточка - Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(5.0), - ), - child: Column( - children: [ - Expanded( - flex: 8, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(5.0), - topRight: Radius.circular(5.0), - ), - child: Image.asset( - 'assets/images/chameleon.jpg', - fit: BoxFit.cover, - width: double.infinity, - ), - ), - ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(4, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 14, - ), - )), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } else if (index == 1) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CardDetailScreen(cardIndex: index), - ), - ); - }, - child: AspectRatio( - aspectRatio: 3/4, - child: Stack( - children: [ - // Внешняя тонкая черная рамка - Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black, width: 2), - ), - ), - // Прослойка цвета карточки - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(7), - ), - ), - ), - // Внутренняя тонкая черная рамка - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 2), - ), - ), - ), - // Основная карточка - Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(5.0), - ), - child: Column( - children: [ - Expanded( - flex: 8, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(5.0), - topRight: Radius.circular(5.0), - ), - child: Image.asset( - 'assets/images/kiwi.jpg', - fit: BoxFit.cover, - width: double.infinity, - ), - ), - ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(4, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 14, - ), - )), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } else if (index == 2) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CardDetailScreen(cardIndex: index), - ), - ); - }, - child: AspectRatio( - aspectRatio: 3/4, - child: Stack( - children: [ - // Внешняя тонкая черная рамка - Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black, width: 2), - ), - ), - // Прослойка цвета карточки - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(7), - ), - ), - ), - // Внутренняя тонкая черная рамка - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 2), - ), - ), - ), - // Основная карточка - Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(5.0), - ), - child: Column( - children: [ - Expanded( - flex: 8, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(5.0), - topRight: Radius.circular(5.0), - ), - child: Image.asset( - 'assets/images/platypus.jpg', - fit: BoxFit.cover, - width: double.infinity, - ), - ), - ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(4, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 14, - ), - )), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } else if (index == 3) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CardDetailScreen(cardIndex: index), - ), - ); - }, - child: AspectRatio( - aspectRatio: 3/4, - child: Stack( - children: [ - // Внешняя тонкая черная рамка - Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black, width: 2), - ), - ), - // Прослойка цвета карточки - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(7), - ), - ), - ), - // Внутренняя тонкая черная рамка - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 2), - ), - ), - ), - // Основная карточка - Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(5.0), - ), - child: Column( - children: [ - Expanded( - flex: 8, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(5.0), - topRight: Radius.circular(5.0), - ), - child: Image.asset( - 'assets/images/pantera.jpg', - fit: BoxFit.cover, - width: double.infinity, - ), - ), - ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(4, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 14, - ), - )), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - - } else if (index == 4) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CardDetailScreen(cardIndex: index), - ), - ); - }, - child: AspectRatio( - aspectRatio: 3/4, - child: Stack( - children: [ - Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black, width: 2), - ), - ), - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(7), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 2), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(5.0), - ), - child: Column( - children: [ - Expanded( - flex: 8, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(5.0), - topRight: Radius.circular(5.0), - ), - child: Image.asset( - 'assets/images/white_bear.jpg', - fit: BoxFit.cover, - width: double.infinity, - ), - ), - ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(3, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 14, - ), - )), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - }else if (index == 5) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CardDetailScreen(cardIndex: index), - ), - ); - }, - child: AspectRatio( - aspectRatio: 3/4, - child: Stack( - children: [ - Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black, width: 2), - ), - ), - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(7), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 2), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(5.0), - ), - child: Column( - children: [ - Expanded( - flex: 8, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(5.0), - topRight: Radius.circular(5.0), - ), - child: Image.asset( - 'assets/images/camel.jpg', - fit: BoxFit.cover, - width: double.infinity, - ), - ), - ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(3, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 14, - ), - )), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } else if (index == 6) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CardDetailScreen(cardIndex: index), - ), - ); - }, - child: AspectRatio( - aspectRatio: 3/4, - child: Stack( - children: [ - Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black, width: 2), - ), - ), - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(7), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 2), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(5.0), - ), - child: Column( - children: [ - Expanded( - flex: 8, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(5.0), - topRight: Radius.circular(5.0), - ), - child: Image.asset( - 'assets/images/zebra.jpg', - fit: BoxFit.cover, - width: double.infinity, - ), - ), - ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(3, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 14, - ), - )), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } else if (index == 7) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CardDetailScreen(cardIndex: index), - ), - ); - }, - child: AspectRatio( - aspectRatio: 3/4, - child: Stack( - children: [ - Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black, width: 2), - ), - ), - Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(7), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.black, width: 2), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(5.0), - ), - child: Column( - children: [ - Expanded( - flex: 8, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(5.0), - topRight: Radius.circular(5.0), - ), - child: Image.asset( - 'assets/images/elephant.jpg', - fit: BoxFit.cover, - width: double.infinity, - ), - ), - ), - Container( - height: 3, - color: Colors.black, - ), - Container( - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(5.0), - bottomRight: Radius.circular(5.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: List.generate(3, (i) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: Image.asset( - 'assets/icons/редкость.png', - height: 14, - ), - )), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - //};// else { - //return Container( - //decoration: BoxDecoration( - // color: const Color(0xFFD6A067), - // borderRadius: BorderRadius.circular(8.0), - // border: Border.all(color: Colors.black, width: 2), - //), - //); - } - return const SizedBox.shrink(); - } } \ No newline at end of file diff --git a/frontend/lib/views/login_screen.dart b/frontend/lib/views/login_screen.dart deleted file mode 100644 index 4333330..0000000 --- a/frontend/lib/views/login_screen.dart +++ /dev/null @@ -1,298 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:email_validator/email_validator.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'registration_screen.dart'; -import 'forgot_password_screen.dart'; -import '../controllers/auth_controller.dart'; - -class LoginScreen extends StatefulWidget { - const LoginScreen({super.key}); - - @override - State createState() => _LoginScreenState(); -} - -class _LoginScreenState extends State with SingleTickerProviderStateMixin { - final _formKey = GlobalKey(); - final _emailController = TextEditingController(); - final _passwordController = TextEditingController(); - bool _obscurePassword = true; - - late TabController _tabController; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: 2, vsync: this); - } - - @override - void dispose() { - _emailController.dispose(); - _passwordController.dispose(); - _tabController.dispose(); - super.dispose(); - } - - Future _login() async { - if (_formKey.currentState!.validate()) { - try { - await Provider.of(context, listen: false).login( - _emailController.text.trim(), - _passwordController.text, - context: context, - ); - } catch (e) { - if (!mounted) return; - - // Получаем текст ошибки без "Exception: " - final errorText = e.toString().replaceAll('', ''); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(errorText), - backgroundColor: Colors.red.shade800, - behavior: SnackBarBehavior.floating, - margin: const EdgeInsets.all(16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ); - } - } - } - - @override - Widget build(BuildContext context) { - final authController = Provider.of(context); - final isLoading = authController.isLoading; - - return Scaffold( - body: SafeArea( - child: Column( - children: [ - const SizedBox(height: 40), - Image.asset( - 'assets/icons/карты.png', - height: 80, - color: const Color(0xFFD9A76A), - ), - const SizedBox(height: 10), - const Text( - 'Cardly', - style: TextStyle( - fontSize: 24, - color: Color(0xFFD9A76A), - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 30), - - Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: TabBar( - controller: _tabController, - indicatorSize: TabBarIndicatorSize.tab, - indicator: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8), - ), - labelColor: Colors.black, - unselectedLabelColor: Colors.black, - tabs: const [ - Tab(text: 'Войти'), - Tab(text: 'Создать'), - ], - ), - ), - - const SizedBox(height: 30), - - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Электронная почта', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 8), - TextFormField( - controller: _emailController, - decoration: const InputDecoration( - filled: true, - fillColor: Color(0xFFEDD6B0), - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), - ), - keyboardType: TextInputType.emailAddress, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Пожалуйста, введите email'; - } - if (!EmailValidator.validate(value)) { - return 'Введите корректный email'; - } - return null; - }, - ), - - const SizedBox(height: 16), - - const Text( - 'Пароль', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 8), - TextFormField( - controller: _passwordController, - decoration: InputDecoration( - filled: true, - fillColor: const Color(0xFFEDD6B0), - border: const OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - suffixIcon: IconButton( - icon: Icon( - _obscurePassword ? Icons.visibility_off : Icons.visibility, - color: Colors.black54, - ), - onPressed: () { - setState(() { - _obscurePassword = !_obscurePassword; - }); - }, - ), - ), - obscureText: _obscurePassword, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Пожалуйста, введите пароль'; - } - if (value.length < 8) { - return 'Пароль должен содержать минимум 8 символов'; - } - return null; - }, - ), - - Align( - alignment: Alignment.centerRight, - child: TextButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const ForgotPasswordScreen(), - ), - ); - }, - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - minimumSize: const Size(50, 30), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: const Text( - 'Забыли пароль?', - style: TextStyle( - fontSize: 14, - color: Colors.black, - ), - ), - ), - ), - - const SizedBox(height: 40), - - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: isLoading ? null : _login, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - disabledBackgroundColor: const Color(0xFFD6A067).withOpacity(0.7), - ), - child: isLoading - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.black, - ), - ) - : const Text( - 'Вход', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - ), - ), - - GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const RegistrationScreen(), - ), - ); - }, - child: const Center( - child: Text('Нажмите, чтобы создать новый аккаунт'), - ), - ), - ], - ), - ), - - Container( - width: 100, - height: 4, - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(2), - ), - ), - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/frontend/lib/views/news_detail_screen.dart b/frontend/lib/views/news_detail_screen.dart index 3e64327..8e0d2aa 100644 --- a/frontend/lib/views/news_detail_screen.dart +++ b/frontend/lib/views/news_detail_screen.dart @@ -1,266 +1,63 @@ import 'package:flutter/material.dart'; -import 'home_screen.dart'; -import 'shop_screen.dart'; -import 'exchanges_screen.dart'; -import 'news_screen.dart'; +import '../models/news_model.dart'; +import '../utils/error_formatter.dart'; +import '../services/analytics_service.dart'; + +// Remove unused imports +// import 'home_screen.dart'; +// import 'shop_screen.dart'; +// import 'exchanges_screen.dart'; +// import 'profile_screen.dart'; +// import 'search_players_screen.dart'; + +class NewsDetailScreen extends StatelessWidget { + final News news; -class NewsDetailScreen extends StatefulWidget { - final NewsItem news; - const NewsDetailScreen({ - super.key, + Key? key, required this.news, - }); - - @override - State createState() => _NewsDetailScreenState(); -} + }) : super(key: key); -class _NewsDetailScreenState extends State { - int _currentIndex = 1; // Индекс вкладки "Новости" в нижней навигации - @override Widget build(BuildContext context) { + AnalyticsService.trackScreenView('news_detail_screen'); return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), - elevation: 0, - leading: Container( - margin: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.black, width: 2), - ), - child: Image.asset( - 'assets/icons/профиль.png', - height: 24, - color: Colors.black, - ), - ), - title: const Text( - 'Новости', - style: TextStyle( - color: Colors.black, - fontSize: 24.0, - fontWeight: FontWeight.bold, - ), - ), - centerTitle: true, - actions: [ - IconButton( - icon: Image.asset( - 'assets/icons/поиск.png', - height: 24, - color: Colors.black, - ), - onPressed: () {}, - ), - IconButton( - icon: Image.asset( - 'assets/icons/уведомления.png', - height: 24, - color: Colors.black, - ), - onPressed: () {}, - ), - ], + title: Text(news.title), ), body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Кнопка возврата - Align( - alignment: Alignment.centerLeft, - child: Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - shape: BoxShape.circle, - ), - child: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.black), - onPressed: () { - Navigator.pop(context); - }, - ), - ), - ), - - const SizedBox(height: 16.0), - - // Карточка детальной новости - Container( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (news.imageURL != null) + Image.network( + news.imageURL, width: double.infinity, - padding: const EdgeInsets.all(16.0), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8.0), - ), - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Заголовок - Center( - child: Text( - widget.news.title, - style: const TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - ), - - const SizedBox(height: 24.0), - - // Картинка - Container( - height: 180, - width: double.infinity, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - ), - alignment: Alignment.center, - child: const Text( - 'Картинка к новости', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - ), - - const SizedBox(height: 24.0), - - // Расширенное описание - Text( - widget.news.fullContent, - style: const TextStyle( - fontSize: 16.0, - ), - ), - - const SizedBox(height: 16.0), - - // Дата - Align( - alignment: Alignment.centerRight, - child: Text( - 'Дата публикации: ${widget.news.date}', - style: const TextStyle( - fontSize: 14.0, - fontStyle: FontStyle.italic, - color: Colors.black54, - ), - ), - ), - ], - ), - - // Кнопка закрытия - Positioned( - top: 0, - right: 0, - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(4.0), - ), - child: IconButton( - icon: const Icon(Icons.close, color: Colors.black), - onPressed: () { - Navigator.pop(context); - }, - constraints: const BoxConstraints( - minWidth: 36, - minHeight: 36, - ), - padding: EdgeInsets.zero, - iconSize: 20, - ), - ), - ), - ], - ), + fit: BoxFit.cover, ), - ], - ), - ), - ), - bottomNavigationBar: Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - switch (index) { - case 0: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 1: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const NewsScreen()), - (route) => false, - ); - break; - case 2: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - label: 'Гл.меню', + const SizedBox(height: 16), + Text( + news.title, + style: Theme.of(context).textTheme.headlineMedium, ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - label: '', + const SizedBox(height: 8), + Text( + 'Опубликовано: ${_formatDate(news.publishDate)}', + style: Theme.of(context).textTheme.bodySmall, ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - label: '', + const SizedBox(height: 16), + Text( + news.content, + style: Theme.of(context).textTheme.bodyLarge, ), ], ), ), ); } + + String _formatDate(DateTime date) { + return '${date.day}.${date.month}.${date.year}'; + } } \ No newline at end of file diff --git a/frontend/lib/views/news_screen.dart b/frontend/lib/views/news_screen.dart index b829e51..c78869f 100644 --- a/frontend/lib/views/news_screen.dart +++ b/frontend/lib/views/news_screen.dart @@ -1,199 +1,152 @@ import 'package:flutter/material.dart'; -import 'home_screen.dart'; -import 'shop_screen.dart'; -import 'exchanges_screen.dart'; +import 'news_detail_screen.dart'; +import 'profile_screen.dart'; +import 'search_players_screen.dart'; +import '../services/api_service.dart'; +import '../models/news_model.dart'; +import '../utils/error_formatter.dart'; +import '../services/analytics_service.dart'; class NewsScreen extends StatefulWidget { const NewsScreen({super.key}); @override - State createState() => _NewsScreenState(); + _NewsScreenState createState() => _NewsScreenState(); } class _NewsScreenState extends State { - int _currentIndex = 1; // Индекс для нижней навигации (книга) - - // Примерные данные новостей (без использования типа DateTime для избежания ошибок) - final List _newsItems = [ - NewsItem( - title: 'Заголовок к новости', - content: 'Текст новости', - date: '12.05.2023', - fullContent: 'Расширенное описание новости с дополнительными подробностями о событии или объявлении.', - ), - NewsItem( - title: 'Заголовок к новости', - content: 'Текст новости', - date: '10.05.2023', - fullContent: 'Расширенное описание новости с дополнительными подробностями о событии или объявлении.', - ), - NewsItem( - title: 'Заголовок к новости', - content: 'Текст новости', - date: '08.05.2023', - fullContent: 'Расширенное описание новости с дополнительными подробностями о событии или объявлении.', - ), - NewsItem( - title: 'Заголовок к новости', - content: 'Текст новости', - date: '05.05.2023', - fullContent: 'Расширенное описание новости с дополнительными подробностями о событии или объявлении.', - ), - ]; - + late bool _isLoading; + late List _news; + late String _error; + @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон - appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), - elevation: 0, - leading: Container( - margin: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.black, width: 2), - ), - child: Image.asset( - 'assets/icons/профиль.png', - height: 24, - color: Colors.black, + void initState() { + super.initState(); + _isLoading = true; + _news = []; + _error = ''; + AnalyticsService.trackScreenView('news_screen'); + _loadNews(); + } + + Future _loadNews() async { + try { + setState(() { + _isLoading = true; + _error = ''; + }); + + final news = await ApiService().getNews(); + + setState(() { + _news = news; + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _error = ErrorFormatter.formatError(e); + }); + } + } + + void _openNewsDetails(News news) { + try { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NewsDetailScreen( + news: news, ), ), - actions: [ - IconButton( - icon: Image.asset( - 'assets/icons/поиск.png', - height: 24, - color: Colors.black, - ), - onPressed: () {}, - ), - IconButton( - icon: Image.asset( - 'assets/icons/уведомления.png', - height: 24, - color: Colors.black, - ), - onPressed: () {}, - ), - ], - ), - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Кнопка возврата и заголовок - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFFBF6EF), + body: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // Кнопка назад - Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - shape: BoxShape.circle, - ), - child: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.black), - onPressed: () { - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - }, - ), - ), - - const SizedBox(height: 16.0), - - // Заголовок "Новости" - const Center( - child: Text( - 'Новости', - style: TextStyle( - fontSize: 24.0, - fontWeight: FontWeight.bold, - color: Colors.black, + Padding( + padding: const EdgeInsets.all(16.0), + child: InkWell( + onTap: () => Navigator.pop(context), + borderRadius: BorderRadius.circular(20.0), + child: Container( + width: 40.0, + height: 40.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFFD6A067), + ), + child: const Icon( + Icons.arrow_back, + color: Colors.black, + size: 29.0, + ), ), ), ), ], ), - ), - - const SizedBox(height: 16.0), - - // Список новостей - Expanded( - child: ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - itemCount: _newsItems.length, - itemBuilder: (context, index) { - return _buildNewsItem(_newsItems[index]); - }, - ), - ), - ], - ), - bottomNavigationBar: Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - switch (index) { - case 0: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 2: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - label: 'Гл.меню', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - label: '', + + const SizedBox(height: 16.0), + + // Заголовок "Новости" + const Center( + child: Text( + 'Новости', + style: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.bold, + color: Colors.black, + fontFamily: 'Jost', + ), + ), ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - label: '', + + const SizedBox(height: 16.0), + + // Список новостей + Expanded( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _error.isNotEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(_error), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _loadNews, + child: const Text('Повторить'), + ), + ], + ), + ) + : RefreshIndicator( + onRefresh: _loadNews, + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + itemCount: _news.length, + itemBuilder: (context, index) { + final news = _news[index]; + return _buildNewsItem(context, news); + }, + ), + ), ), ], ), @@ -202,47 +155,79 @@ class _NewsScreenState extends State { } // Метод для отображения элемента новости - Widget _buildNewsItem(NewsItem news) { - return Container( - margin: const EdgeInsets.only(bottom: 16.0), - padding: const EdgeInsets.all(16.0), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8.0), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Левая часть - заголовок - Expanded( - flex: 1, - child: Text( + Widget _buildNewsItem(BuildContext context, News news) { + return GestureDetector( + onTap: () => _openNewsDetails(news), + child: Container( + margin: const EdgeInsets.only(bottom: 16.0), + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(8.0), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Заголовок + Text( news.title, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14.0, + fontFamily: 'Jost', ), ), - ), - - const SizedBox(width: 16.0), - - // Правая часть - текст - Expanded( - flex: 1, - child: Text( + + const SizedBox(height: 8.0), + + // Текст + Text( news.content, style: const TextStyle( fontSize: 14.0, + fontFamily: 'Jost', ), ), - ), - ], + ], + ), ), ); } + + String _formatDate(String dateStr) { + final date = DateTime.parse(dateStr); + return '${date.day}.${date.month}.${date.year}'; + } } +// Примерные данные новостей +final List _newsItems = [ + NewsItem( + title: 'Заголовок к новости', + content: 'Текст новости', + date: '12.05.2023', + fullContent: 'Расширенное описание новости с дополнительными подробностями о событии или объявлении.', + ), + NewsItem( + title: 'Заголовок к новости', + content: 'Текст новости', + date: '10.05.2023', + fullContent: 'Расширенное описание новости с дополнительными подробностями о событии или объявлении.', + ), + NewsItem( + title: 'Заголовок к новости', + content: 'Текст новости', + date: '08.05.2023', + fullContent: 'Расширенное описание новости с дополнительными подробностями о событии или объявлении.', + ), + NewsItem( + title: 'Заголовок к новости', + content: 'Текст новости', + date: '05.05.2023', + fullContent: 'Расширенное описание новости с дополнительными подробностями о событии или объявлении.', + ), +]; + // Класс для хранения данных новости class NewsItem { final String title; @@ -256,4 +241,4 @@ class NewsItem { required this.date, required this.fullContent, }); -} \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/lib/views/notifications_modal.dart b/frontend/lib/views/notifications_modal.dart new file mode 100644 index 0000000..e5e3746 --- /dev/null +++ b/frontend/lib/views/notifications_modal.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import '../services/api_service.dart'; +import '../models/notification_model.dart' as app_notification; +import '../utils/error_formatter.dart'; + +class NotificationsModal extends StatefulWidget { + const NotificationsModal({super.key}); + + @override + _NotificationsModalState createState() => _NotificationsModalState(); +} + +class _NotificationsModalState extends State { + late bool _isLoading; + late List _notifications; + late String _error; + + @override + void initState() { + super.initState(); + _isLoading = true; + _notifications = []; + _error = ''; + _loadNotifications(); + } + + Future _loadNotifications() async { + try { + setState(() { + _isLoading = true; + }); + + final notifications = await ApiService().getNotifications(); + + setState(() { + _notifications = notifications; + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _error = ErrorFormatter.formatError(e); + }); + } + } + + Future _handleNotificationTap(app_notification.Notification notification) async { + try { + final redirectUrl = await ApiService().navigateToNotification(notification.notification_ID); + Navigator.pushNamed(context, redirectUrl); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: const Color(0xFFFFF4E3), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + child: Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: MediaQuery.of(context).size.width * 0.9, + height: MediaQuery.of(context).size.height * 0.8, + padding: const EdgeInsets.only(top: 24.0, bottom: 16.0, left: 16.0, right: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Уведомления', + style: TextStyle( + fontSize: 20.0, + fontFamily: 'Jost', + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16.0), + _isLoading + ? const Center(child: CircularProgressIndicator()) + : _error.isNotEmpty + ? Center(child: Text(_error)) + : _notifications.isEmpty + ? const Center(child: Text('Нет уведомлений')) + : Expanded( + child: ListView.builder( + itemCount: _notifications.length, + itemBuilder: (context, index) { + final notification = _notifications[index]; + return ListTile( + title: Text(notification.title), + subtitle: Text(_formatDate(notification.timestamp.toIso8601String())), + onTap: () => _handleNotificationTap(notification), + ); + }, + ), + ), + ], + ), + ), + // Close button in the top right corner + Positioned( + top: -1, + right: -1, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), + ), + child: const Center( + child: Icon( + Icons.close, + color: Colors.black, + size: 20, + ), + ), + ), + ), + ), + ], + ), + ); + } + + String _formatDate(String dateStr) { + final date = DateTime.parse(dateStr); + return '${date.day}.${date.month}.${date.year} ${date.hour}:${date.minute}'; + } +} \ No newline at end of file diff --git a/frontend/lib/views/pack_content_screen.dart b/frontend/lib/views/pack_content_screen.dart index c2f460a..1eb6b0f 100644 --- a/frontend/lib/views/pack_content_screen.dart +++ b/frontend/lib/views/pack_content_screen.dart @@ -1,19 +1,27 @@ import 'package:flutter/material.dart'; +import '../models/card_model.dart'; import 'shop_screen.dart'; import 'inventory_screen.dart'; class PackContentScreen extends StatelessWidget { final String setName; - const PackContentScreen({super.key, required this.setName}); + final List cards; + + const PackContentScreen({ + Key? key, + required this.setName, + required this.cards, + }) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), elevation: 0, - title: const Text('Выпавшие карты:', style: TextStyle(color: Colors.black)), + automaticallyImplyLeading: false, + title: const Text('Выпавшие карты:', style: TextStyle(color: Colors.black, fontFamily: 'Jost')), centerTitle: true, ), body: Column( @@ -22,19 +30,38 @@ class PackContentScreen extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(16.0), child: GridView.builder( + padding: const EdgeInsets.all(16.0), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - crossAxisSpacing: 10.0, - mainAxisSpacing: 10.0, - childAspectRatio: 0.7, + crossAxisCount: 2, + childAspectRatio: 0.75, + crossAxisSpacing: 16.0, + mainAxisSpacing: 16.0, ), - itemCount: 12, + itemCount: cards.length, itemBuilder: (context, index) { - return Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(6.0), - border: Border.all(color: Colors.black, width: 1), + final card = cards[index]; + return Card( + child: Column( + children: [ + Expanded( + child: Image.network( + card.imageURL, + fit: BoxFit.cover, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Text( + card.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Text('Редкость: ${card.rarity}'), + ], + ), + ), + ], ), ); }, @@ -60,10 +87,10 @@ class PackContentScreen extends StatelessWidget { foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(vertical: 16.0), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), + borderRadius: BorderRadius.circular(10.0), ), ), - child: const Text('Вернуться в магазин', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + child: const Text('Вернуться в магазин', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, fontFamily: 'Roboto')), ), ), const SizedBox(height: 12), @@ -82,13 +109,13 @@ class PackContentScreen extends StatelessWidget { foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(vertical: 16.0), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), + borderRadius: BorderRadius.circular(10.0), ), ), - child: const Text('Перейти в Инвентарь', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + child: const Text('Перейти в инвентарь', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, fontFamily: 'Roboto')), ), ), - const SizedBox(height: 16), + const SizedBox(height: 120), ], ), ), diff --git a/frontend/lib/views/pack_open_screen.dart b/frontend/lib/views/pack_open_screen.dart deleted file mode 100644 index 931f63b..0000000 --- a/frontend/lib/views/pack_open_screen.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter/material.dart'; -import 'pack_content_screen.dart'; // Экран содержимого пака - -class PackOpenScreen extends StatelessWidget { - final String setName; - const PackOpenScreen({super.key, required this.setName}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), - appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.black), - onPressed: () => Navigator.pop(context), - ), - title: Text('Открытие пака', style: const TextStyle(color: Colors.black)), - centerTitle: true, - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Картинка пака (заглушка) - Container( - width: 120, - height: 200, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(16), - ), - child: const Center( - child: Text( - '?', - style: TextStyle(fontSize: 64, color: Colors.white), - ), - ), - ), - const SizedBox(height: 32), - ElevatedButton( - onPressed: () { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => PackContentScreen(setName: setName), - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: const Text( - 'Открыть пак', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ), - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/frontend/lib/views/profile_image_dialog.dart b/frontend/lib/views/profile_image_dialog.dart new file mode 100644 index 0000000..8128217 --- /dev/null +++ b/frontend/lib/views/profile_image_dialog.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; + +class ProfileImageUploadDialog extends StatelessWidget { + const ProfileImageUploadDialog({super.key}); + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + child: Container( + width: 369, + height: 559, + decoration: BoxDecoration( + color: const Color(0xFFFBF6EF), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.black, width: 2), + ), + child: Stack( + children: [ + // Close button (X) + Positioned( + top: -1, + right: -1, + child: GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(10.0), + border: Border.all(color: Colors.black, width: 2), + ), + child: const Center( + child: Icon( + Icons.close_rounded, + color: Colors.black, + size: 48.0, + ), + ), + ), + ), + ), + // Upload button at the bottom + Positioned( + bottom: 20, + left: 0, + right: 0, + child: Center( + child: Container( + width: 289, + height: 59, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.black, width: 2), + ), + child: TextButton( + onPressed: () { + // TODO: Implement image upload functionality + }, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + 'Загрузить изображение', + style: TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w500, + fontFamily: 'Roboto', + ), + ), + ), + ), + ), + ), + // Profile icon + Center( + child: Container( + width: 140.0, + height: 140.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.black, width: 2), + ), + child: const CircleAvatar( + backgroundColor: Color(0xFFFFF4E3), + child: Icon( + Icons.person_outline, + size: 90.0, + color: Colors.black, + ), + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/views/profile_screen.dart b/frontend/lib/views/profile_screen.dart index 18f25e6..ad0786a 100644 --- a/frontend/lib/views/profile_screen.dart +++ b/frontend/lib/views/profile_screen.dart @@ -3,26 +3,131 @@ import 'home_screen.dart'; import 'shop_screen.dart'; import 'exchanges_screen.dart'; import 'inventory_screen.dart'; -import 'settings_screen.dart'; +import 'settings_screen.dart' show SettingsDialog; +import 'achievements_screen.dart'; +import 'profile_image_dialog.dart'; +import '../services/api_service.dart'; +import '../utils/error_formatter.dart'; +import '../utils/auth_utils.dart'; +import 'package:provider/provider.dart'; +import '../controllers/auth_controller.dart'; +import '../services/analytics_service.dart'; -class ProfileScreen extends StatelessWidget { - const ProfileScreen({super.key}); +class ProfileScreen extends StatefulWidget { + final bool isOtherUser; + final String? playerName; + final String? playerId; + final int? cardsCollected; + final int? collectionsCollected; + final List favoriteCardIds; + + const ProfileScreen({ + super.key, + this.isOtherUser = false, + this.playerName, + this.playerId, + this.cardsCollected, + this.collectionsCollected, + this.favoriteCardIds = const [], + }); + + @override + State createState() => _ProfileScreenState(); +} + +class _ProfileScreenState extends State { + late bool _isLoading; + late String _error; + late Map _userStats; + late List _favoriteCards; + late List _favoriteAchievements; + late List _achievements; + late Map _settings; + + @override + void initState() { + super.initState(); + _isLoading = true; + _error = ''; + _settings = {}; + AnalyticsService.trackScreenView('profile_screen'); + _loadProfileData(); + } + + Future _loadProfileData() async { + try { + setState(() { + _isLoading = true; + }); + + final profileData = await ApiService().getProfile(); + final achievements = await ApiService().getAchievements(); + + setState(() { + _userStats = profileData['userStats']; + _favoriteCards = profileData['favoriteCards']; + _favoriteAchievements = profileData['favoriteAchievements']; + _achievements = achievements; + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _error = ErrorFormatter.formatError(e); + }); + } + } + + Future _toggleAchievementFavorite(int achievementId, bool isFavorite) async { + try { + bool success; + if (isFavorite) { + success = await ApiService().removeAchievementFromFavorites(achievementId); + } else { + success = await ApiService().addAchievementToFavorites(achievementId); + } + + if (success) { + await _loadProfileData(); + } + } catch (e) { + // Показать ошибку пользователю + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } + + Future _updateSettings() async { + try { + await ApiService().updateProfileSettings(_settings); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Настройки успешно сохранены')), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } @override Widget build(BuildContext context) { + if (Provider.of(context).currentUser?.isGuest ?? false) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!AuthUtils.checkGuestAccess(context, 'profile_screen')) { + Navigator.of(context).pop(); + } + }); + return Container(); // Return empty container while checking + } + return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон + backgroundColor: const Color(0xFFFBF6EF), appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), elevation: 0, - title: const Text( - 'Профиль игрока', - style: TextStyle( - color: Colors.black45, - fontSize: 16.0, - fontWeight: FontWeight.w400, - ), - ), + title: null, centerTitle: true, leading: Padding( padding: const EdgeInsets.only(left: 16.0), @@ -41,37 +146,62 @@ class ProfileScreen extends StatelessWidget { child: const Icon( Icons.arrow_back, color: Colors.black, - size: 22.0, + size: 29.0, ), ), ), ), actions: [ - Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Container( - width: 40.0, - height: 40.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.black, width: 2), + if (widget.isOtherUser) + Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Container( + width: 40.0, + height: 40.0, + child: InkWell( + borderRadius: BorderRadius.circular(20.0), + onTap: () { + showDialog( + context: context, + barrierColor: Colors.black.withOpacity(0.2), + builder: (context) => ReportDialog(playerName: widget.playerName ?? ''), + ); + }, + child: Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY(3.14159), + child: Image.asset( + 'assets/icons/жалоба.png', + height: 22.0, + color: Colors.black, + ), + ), + ), ), - child: InkWell( - borderRadius: BorderRadius.circular(20.0), - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SettingsScreen()), - ); - }, - child: Image.asset( - 'assets/icons/настройки.png', - color: Colors.black, - height: 22.0, + ) + else + Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Container( + width: 40.0, + height: 40.0, + child: InkWell( + borderRadius: BorderRadius.circular(20.0), + onTap: () { + showDialog( + context: context, + barrierColor: Colors.black.withOpacity(0.2), + builder: (context) => const SettingsDialog(), + ); + }, + child: Image.asset( + 'assets/icons/настройки.png', + color: Colors.black, + height: 22.0, + ), ), ), ), - ), ], ), body: Column( @@ -79,46 +209,57 @@ class ProfileScreen extends StatelessWidget { const SizedBox(height: 20.0), // Аватар пользователя - Container( - width: 100.0, - height: 100.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.black, width: 2), - ), - child: const CircleAvatar( - backgroundColor: Colors.white, - child: Icon( - Icons.person_outline, - size: 60.0, - color: Colors.black, + GestureDetector( + onTap: widget.isOtherUser ? null : () { + showDialog( + context: context, + barrierColor: Colors.black.withOpacity(0.2), + builder: (context) => const ProfileImageUploadDialog(), + ); + }, + child: Container( + width: 140.0, + height: 140.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.black, width: 2), + ), + child: const CircleAvatar( + backgroundColor: Color(0xFFFBF6EF), + child: Icon( + Icons.person_outline, + size: 90.0, + color: Colors.black, + ), ), ), ), - const SizedBox(height: 12.0), + const SizedBox(height: 16.0), // Имя пользователя - const Text( - 'Ник', - style: TextStyle( + Text( + widget.isOtherUser ? widget.playerName ?? '' : 'Ник', + style: const TextStyle( fontSize: 18.0, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), const SizedBox(height: 4.0), // ID пользователя - const Text( - '######', - style: TextStyle( + Text( + widget.isOtherUser ? widget.playerId ?? '' : '######', + style: const TextStyle( fontSize: 16.0, color: Colors.black54, + fontFamily: 'Jost', ), ), - const SizedBox(height: 20.0), + const SizedBox(height: 16.0), // Коллекция карточек (5 карточек в ряд) Padding( @@ -126,45 +267,73 @@ class ProfileScreen extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate( - 5, - (index) => _buildCard(), + 5, // Always generate 5 cards + (index) => _buildCard( + index < widget.favoriteCardIds.length ? widget.favoriteCardIds[index] : -1, + ), ), ), ), - const SizedBox(height: 20.0), + const SizedBox(height: 15.0), - // Достижения - Container( - margin: const EdgeInsets.symmetric(horizontal: 16.0), - padding: const EdgeInsets.symmetric(vertical: 16.0), - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildAchievement(), - _buildAchievement(), - _buildAchievement(), - _buildAchievement(), - Container( - padding: const EdgeInsets.all(8.0), - alignment: Alignment.center, - child: const Text( - 'Все достижения →', - style: TextStyle( - fontSize: 12.0, - fontWeight: FontWeight.bold, + // Блок достижений + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Container( + height: 130, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(8.0), + ), + child: Stack( + children: [ + Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(4, (index) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Image.asset( + 'assets/icons/достижение.png', + height: 70, + width: 70, + fit: BoxFit.contain, + ), + )), ), ), - ), - ], + Positioned( + right: 12, + bottom: 8, + child: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AchievementsScreen( + isOtherUser: widget.isOtherUser, + userId: widget.isOtherUser ? int.parse(widget.playerId ?? '0') : 0, + ), + ), + ); + }, + child: Text( + widget.isOtherUser ? 'Достижения →' : 'Все достижения →', + style: const TextStyle( + fontSize: 13.0, + fontWeight: FontWeight.normal, + color: Colors.black, + fontFamily: 'Jost', + ), + ), + ), + ), + ], + ), ), ), - const SizedBox(height: 40.0), + const SizedBox(height: 90.0), // Статистика Container( @@ -173,19 +342,21 @@ class ProfileScreen extends StatelessWidget { children: [ // Собрано карт Row( - children: const [ - Text( + children: [ + const Text( 'Собрано карт: ', style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), Text( - '****', - style: TextStyle( + widget.isOtherUser ? (_userStats['cardsCollected']?.toString() ?? '0') : '****', + style: const TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), ], @@ -195,131 +366,293 @@ class ProfileScreen extends StatelessWidget { // Собрано коллекций Row( - children: const [ - Text( + children: [ + const Text( 'Собрано коллекций: ', style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.w500, + fontFamily: 'Jost', ), ), Text( - '***', - style: TextStyle( + widget.isOtherUser ? (_userStats['collectionsCollected']?.toString() ?? '0') : '***', + style: const TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), ], ), + if (widget.isOtherUser) ...[ + const SizedBox(height: 18.0), + // Кнопка посмотреть инвентарь + Align( + alignment: Alignment.centerLeft, + child: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => InventoryScreen( + isOtherUser: true, + playerName: widget.playerName ?? '', + playerId: widget.playerId != null ? int.tryParse(widget.playerId!) : null, + ), + ), + ); + }, + child: const Text( + 'Посмотреть инвентарь →', + style: TextStyle( + fontSize: 15.0, + fontWeight: FontWeight.w500, + color: Colors.black, + fontFamily: 'Jost', + ), + ), + ), + ), + ], ], ), ), const Spacer(), - - // Нижняя навигационная панель - Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), + ], + ), + bottomNavigationBar: null, + ); + } + + // Карточка в профиле + Widget _buildCard(int cardId) { + return SizedBox( + width: 74, + height: 112, + child: AspectRatio( + aspectRatio: 3/4, + child: Stack( + children: [ + // Внешняя тонкая черная рамка + Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), + ), ), - child: BottomNavigationBar( - currentIndex: 0, - onTap: (index) { - switch (index) { - case 0: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 1: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const InventoryScreen()), - (route) => false, - ); - break; - case 2: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - label: 'Гл.меню', + // Прослойка цвета карточки + Padding( + padding: const EdgeInsets.all(2.0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(7), ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - label: '', + ), + ), + // Внутренняя тонкая черная рамка + Padding( + padding: const EdgeInsets.all(4.0), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.black, width: 2), ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - label: '', + ), + ), + // Основная карточка + Padding( + padding: const EdgeInsets.all(6.0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(5.0), ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - label: '', + child: Column( + children: [ + Expanded( + flex: 8, + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(5.0), + topRight: Radius.circular(5.0), + ), + child: Container( + color: const Color(0xFFEAD7C3), + child: const Center( + child: Text( + 'Нет изображения', + style: TextStyle(color: Colors.black45, fontSize: 12), + textAlign: TextAlign.center, + ), + ), + ), + ), + ), + Container( + height: 3, + color: Colors.black, + ), + Container( + height: 20, + decoration: const BoxDecoration( + color: Color(0xFFD6A067), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(5.0), + bottomRight: Radius.circular(5.0), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: List.generate(4, (i) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 1.0), + child: Image.asset( + 'assets/icons/редкость.png', + height: 10, + ), + )), + ), + ), + ], ), - ], + ), ), - ), - ], + ], + ), ), ); } - - // Карточка в профиле - Widget _buildCard() { - return Container( - width: 60.0, - height: 84.0, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 2), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, +} + +class ReportDialog extends StatefulWidget { + final String playerName; + const ReportDialog({super.key, required this.playerName}); + + @override + State createState() => _ReportDialogState(); +} + +class _ReportDialogState extends State { + int _selectedReason = 0; + final TextEditingController _controller = TextEditingController(); + final List _reasons = [ + 'причина 1', + 'причина 2', + 'причина 3', + ]; + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: const Color(0xFFEAD7C3), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + insetPadding: EdgeInsets.zero, + child: Stack( children: [ - Expanded( - child: Container( - margin: const EdgeInsets.only(top: 4.0, left: 4.0, right: 4.0), - decoration: const BoxDecoration( - color: Color(0xFFEDD6B0), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(4.0), - topRight: Radius.circular(4.0), - ), + Container( + width: 360, + height: 600, + padding: const EdgeInsets.all(20), + child: Padding( + padding: const EdgeInsets.only(top: 50.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const SizedBox(width: 12), + Expanded( + child: Text( + 'Подать жалобу на пользователя\n${widget.playerName}', + style: const TextStyle(fontSize: 17), + textAlign: TextAlign.center, + ), + ), + ], + ), + const SizedBox(height: 18), + ...List.generate(_reasons.length, (i) => Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: Row( + children: [ + Text( + _reasons[i], + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + ), + const SizedBox(width: 12), + Radio( + value: i, + groupValue: _selectedReason, + onChanged: (val) => setState(() => _selectedReason = val!), + activeColor: Colors.black, + ), + ], + ), + )), + const SizedBox(height: 12), + TextField( + controller: _controller, + maxLines: 5, + decoration: InputDecoration( + hintText: 'Комментарий', + filled: true, + fillColor: const Color(0xFFFBF6EF), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.all(16), + ), + ), + const SizedBox(height: 18), + SizedBox( + width: double.infinity, + height: 54, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + textStyle: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + ), + onPressed: () { + Navigator.pop(context); + showTopReportSuccess(context); + }, + child: const Text('Отправить жалобу'), + ), + ), + ], ), ), ), - Container( - height: 24.0, - margin: const EdgeInsets.only(left: 4.0, right: 4.0, bottom: 4.0), - decoration: const BoxDecoration( - color: Color(0xFFEDD6B0), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(4.0), - bottomRight: Radius.circular(4.0), + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.black, width: 3), + ), + child: const Center( + child: Icon( + Icons.close, + color: Colors.black, + size: 28, + ), + ), ), ), ), @@ -327,23 +660,45 @@ class ProfileScreen extends StatelessWidget { ), ); } - - // Иконка достижения - Widget _buildAchievement() { - return Container( - padding: const EdgeInsets.all(8.0), - child: Column( - children: const [ - Icon( - Icons.emoji_events_outlined, - size: 24.0, +} + +void showTopReportSuccess(BuildContext context) { + final overlay = Overlay.of(context); + final overlayEntry = OverlayEntry( + builder: (context) => Positioned( + top: 40, + left: 16, + right: 16, + child: Material( + color: Colors.transparent, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.18), + blurRadius: 8, + offset: const Offset(0, 6), + ), + ], ), - Icon( - Icons.star_border, - size: 16.0, + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 5), + child: const Center( + child: Text( + 'Жалоба успешно отправлена', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), ), - ], + ), ), - ); - } + ), + ); + overlay.insert(overlayEntry); + Future.delayed(const Duration(seconds: 3), () => overlayEntry.remove()); } \ No newline at end of file diff --git a/frontend/lib/views/quests_screen.dart b/frontend/lib/views/quests_screen.dart index 2fb8aed..a31255d 100644 --- a/frontend/lib/views/quests_screen.dart +++ b/frontend/lib/views/quests_screen.dart @@ -3,138 +3,94 @@ import 'home_screen.dart'; import 'shop_screen.dart'; import 'exchanges_screen.dart'; import 'news_screen.dart'; +import '../services/api_service.dart'; +import '../models/quest_model.dart'; +import '../utils/error_formatter.dart'; +import '../utils/auth_utils.dart'; +import 'package:provider/provider.dart'; +import '../controllers/auth_controller.dart'; +import '../services/analytics_service.dart'; -class QuestsScreen extends StatefulWidget { - const QuestsScreen({super.key}); +class QuestsDialogContent extends StatefulWidget { + const QuestsDialogContent({super.key}); @override - State createState() => _QuestsScreenState(); + State createState() => _QuestsDialogContentState(); } -class _QuestsScreenState extends State with SingleTickerProviderStateMixin { - int _currentIndex = 0; // Индекс для нижней навигации (главное меню) +class _QuestsDialogContentState extends State with SingleTickerProviderStateMixin { late TabController _tabController; - int _selectedTab = 1; // По умолчанию открываем вкладку "Недельные квесты" - + int _selectedTab = 0; + @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this, initialIndex: _selectedTab); + AnalyticsService.trackScreenView('quests_dialog_content'); } - + @override void dispose() { _tabController.dispose(); super.dispose(); } - + @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон - appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), - elevation: 0, - leading: Container( - margin: const EdgeInsets.all(8.0), + return Center( + child: Material( + color: Colors.transparent, + child: Container( + width: 380, + constraints: const BoxConstraints(maxHeight: 450), decoration: BoxDecoration( - shape: BoxShape.circle, + color: const Color(0xFFFFF4E3), + borderRadius: BorderRadius.circular(10.0), border: Border.all(color: Colors.black, width: 2), ), - child: Image.asset( - 'assets/icons/профиль.png', - height: 24, - color: Colors.black, - ), - ), - actions: [ - IconButton( - icon: Image.asset( - 'assets/icons/поиск.png', - height: 24, - color: Colors.black, - ), - onPressed: () {}, - ), - IconButton( - icon: Image.asset( - 'assets/icons/уведомления.png', - height: 24, - color: Colors.black, - ), - onPressed: () {}, - ), - ], - ), - body: Column( - children: [ - // Основная карточка с квестами - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black54, width: 1), - ), - child: Stack( + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.only(top: 0.0, right: 0.0), + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Column( - children: [ - // Заголовок - Container( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(7.0), - topRight: Radius.circular(7.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Выполнено квестов 0/5', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 4.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - border: Border.all(color: Colors.black, width: 1), - ), - child: const Text( - 'Награда', - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - ), - ), - ), - ], + // Верхняя панель с прогрессом и наградой + Padding( + padding: const EdgeInsets.only(top: 12.0, right: 128.0), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(10.0), + ), + child: const Text( + 'Выполнено квестов 0/5', + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, ), ), - - // Вкладки - TabBar( + ), + ), + // TabBar как в магазине + Padding( + padding: const EdgeInsets.all(16.0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(12.0), + ), + child: TabBar( controller: _tabController, - labelColor: Colors.black, - unselectedLabelColor: Colors.black54, - indicatorColor: Colors.transparent, + dividerColor: Colors.transparent, indicator: const BoxDecoration( color: Color(0xFFD6A067), - border: Border( - bottom: BorderSide( - color: Colors.black, - width: 3.0, - ), - ), + borderRadius: BorderRadius.all(Radius.circular(10.0)), ), + labelColor: Colors.black, + unselectedLabelColor: Colors.black, + indicatorSize: TabBarIndicatorSize.tab, tabs: const [ Tab(text: 'Ежедневные квесты'), Tab(text: 'Недельные квесты'), @@ -145,235 +101,70 @@ class _QuestsScreenState extends State with SingleTickerProviderSt }); }, ), - - // Содержимое вкладок - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - // Вкладка с ежедневными квестами - _buildDailyQuestsTab(), - - // Вкладка с недельными квестами - _buildWeeklyQuestsTab(), - ], - ), - ), - ], + ), ), - - // Кнопка закрытия в правом верхнем углу - Positioned( - top: 12.0, - right: 12.0, - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(4.0), - ), - child: InkWell( - onTap: () { - Navigator.pop(context); - }, - child: const Icon( - Icons.close, - color: Colors.black, - size: 24.0, - ), - ), + // Содержимое вкладок + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildDailyQuestsTab(), + _buildWeeklyQuestsTab(), + ], ), ), ], ), ), - ), - ), - - const SizedBox(height: 20.0), - - // Кнопки "Квесты" и "Новости" - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0), - child: Row( - children: [ - // Кнопка "Квесты" - Expanded( - child: ElevatedButton( - onPressed: () { - // Уже на экране квестов - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 12.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - 'assets/icons/квесты.png', - height: 20.0, - color: Colors.black, - ), - const SizedBox(width: 8.0), - const Text( - 'Квесты', - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ), - - const SizedBox(width: 16.0), - - // Кнопка "Новости" - Expanded( - child: ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const NewsScreen(), - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 12.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - 'assets/icons/новости.png', - height: 20.0, - color: Colors.black, - ), - const SizedBox(width: 8.0), - const Text( - 'Новости', - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - ), - ), - ], + // Кнопка закрытия + Positioned( + top: -2.0, + right: -2.0, + child: InkWell( + onTap: () => Navigator.of(context).pop(), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(10.0), + border: Border.all(color: Colors.black, width: 2), ), + padding: const EdgeInsets.all(4.0), + child: const Icon(Icons.close, color: Colors.black, size: 32), ), ), - ], - ), - ), - - const SizedBox(height: 10.0), - - // Нижняя навигационная панель - Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - switch (index) { - case 0: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 2: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - label: 'Гл.меню', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - label: '', - ), - ], - ), + ), + ], ), - ], + ), ), ); } - - // Вкладка с ежедневными квестами + Widget _buildDailyQuestsTab() { return ListView.builder( padding: const EdgeInsets.all(16.0), - itemCount: 6, + itemCount: 5, itemBuilder: (context, index) { return _buildQuestItem('Ежедневный квест', 200); }, ); } - - // Вкладка с недельными квестами + Widget _buildWeeklyQuestsTab() { return ListView.builder( padding: const EdgeInsets.all(16.0), - itemCount: 4, + itemCount: 5, itemBuilder: (context, index) { return _buildQuestItem('Недельный квест', 1000); }, ); } - - // Элемент квеста + Widget _buildQuestItem(String title, int reward) { return Container( margin: const EdgeInsets.only(bottom: 12.0), child: Row( children: [ - // Название квеста Expanded( child: Text( title, @@ -383,16 +174,15 @@ class _QuestsScreenState extends State with SingleTickerProviderSt ), ), ), - - // Награда Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 7.0), decoration: BoxDecoration( color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(16.0), + borderRadius: BorderRadius.circular(10.0), ), child: Row( children: [ + const SizedBox(width: 12.0), Text( reward.toString(), style: const TextStyle( @@ -401,10 +191,10 @@ class _QuestsScreenState extends State with SingleTickerProviderSt ), ), const SizedBox(width: 4.0), - const Icon( - Icons.monetization_on, - color: Colors.amber, - size: 16.0, + Image.asset( + 'assets/icons/монеты.png', + height: 20, + width: 20, ), ], ), @@ -413,4 +203,165 @@ class _QuestsScreenState extends State with SingleTickerProviderSt ), ); } +} + +// Функция для показа модального окна квестов +void showQuestsDialog(BuildContext context) { + showDialog( + context: context, + barrierDismissible: true, + builder: (context) => Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 40.0), + child: const SizedBox( + width: 380, + child: QuestsDialogContent(), + ), + ), + ); +} + +class QuestsScreen extends StatefulWidget { + const QuestsScreen({super.key}); + + @override + State createState() => _QuestsScreenState(); +} + +class _QuestsScreenState extends State { + late bool _isLoading; + late Map> _quests; + late String _error; + + @override + void initState() { + super.initState(); + _isLoading = true; + _quests = { + 'dailyQuests': [], + 'weeklyQuests': [] + }; + _error = ''; + _loadQuests(); + } + + Future _loadQuests() async { + try { + setState(() { + _isLoading = true; + }); + + final quests = await ApiService().getQuests(); + + setState(() { + _quests = quests; + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _error = ErrorFormatter.formatError(e); + }); + } + } + + Future _toggleQuestStatus(Quest quest) async { + try { + final updatedQuest = await ApiService().changeQuestStatus(quest.quest_ID); + await _loadQuests(); // Перезагружаем все квесты для обновления UI + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } + + Future _claimReward(String questType) async { + try { + final result = await ApiService().claimQuestReward(questType); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Получено: ${result['receivedCoins']} монет')), + ); + await _loadQuests(); // Перезагружаем квесты после получения награды + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } + + @override + Widget build(BuildContext context) { + if (Provider.of(context).currentUser?.isGuest ?? false) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!AuthUtils.checkGuestAccess(context, 'quests_screen')) { + Navigator.of(context).pop(); + } + }); + return Container(); // Return empty container while checking + } + + return Scaffold( + appBar: AppBar( + title: const Text('Квесты'), + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _error.isNotEmpty + ? Center(child: Text(_error)) + : SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildQuestSection('Ежедневные квесты', _quests['dailyQuests']!, 'daily'), + _buildQuestSection('Еженедельные квесты', _quests['weeklyQuests']!, 'weekly'), + ], + ), + ), + ); + } + + Widget _buildQuestSection(String title, List quests, String questType) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + title, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: quests.length, + itemBuilder: (context, index) { + final quest = quests[index]; + return ListTile( + title: Text(quest.title), + subtitle: Text(quest.description), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('${quest.progress}/${quest.maxProgress}'), + Checkbox( + value: quest.isCompleted, + onChanged: (bool? value) => _toggleQuestStatus(quest), + ), + ], + ), + ); + }, + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: () => _claimReward(questType), + child: const Text('Получить награду'), + ), + ), + ], + ); + } } \ No newline at end of file diff --git a/frontend/lib/views/registration_screen.dart b/frontend/lib/views/registration_screen.dart deleted file mode 100644 index ec0531f..0000000 --- a/frontend/lib/views/registration_screen.dart +++ /dev/null @@ -1,352 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:email_validator/email_validator.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import '../controllers/auth_controller.dart'; -import 'email_verification_screen.dart'; -import 'login_screen.dart'; - -class RegistrationScreen extends StatefulWidget { - const RegistrationScreen({super.key}); - - @override - State createState() => _RegistrationScreenState(); -} - -class _RegistrationScreenState extends State with SingleTickerProviderStateMixin { - final _formKey = GlobalKey(); - final _nicknameController = TextEditingController(); - final _emailController = TextEditingController(); - final _passwordController = TextEditingController(); - final _confirmPasswordController = TextEditingController(); - bool _obscurePassword = true; - bool _obscureConfirmPassword = true; - - late TabController _tabController; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: 2, vsync: this); - _tabController.index = 1; - } - - @override - void dispose() { - _nicknameController.dispose(); - _emailController.dispose(); - _passwordController.dispose(); - _confirmPasswordController.dispose(); - _tabController.dispose(); - super.dispose(); - } - - Future _register() async { - if (_formKey.currentState!.validate()) { - try { - await Provider.of(context, listen: false).register( - _emailController.text.trim(), - _nicknameController.text.trim(), - _passwordController.text, - context: context, - ); - - if (!mounted) return; - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EmailVerificationScreen( - email: _emailController.text.trim(), - ), - ), - ); - } catch (e) { - if (!mounted) return; - - // Получаем текст ошибки без "Exception: " - final errorText = e.toString().replaceAll('', ''); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(errorText), - backgroundColor: Colors.red.shade800, - behavior: SnackBarBehavior.floating, - margin: const EdgeInsets.all(16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ); - } - } - } - - @override - Widget build(BuildContext context) { - final authController = Provider.of(context); - final isLoading = authController.isLoading; - - return Scaffold( - body: SafeArea( - child: Column( - children: [ - const SizedBox(height: 40), - Image.asset( - 'assets/icons/карты.png', - height: 80, - color: const Color(0xFFD9A76A), - ), - const SizedBox(height: 10), - const Text( - 'Cardly', - style: TextStyle( - fontSize: 24, - color: Color(0xFFD9A76A), - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 30), - - Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: TabBar( - controller: _tabController, - indicatorSize: TabBarIndicatorSize.tab, - indicator: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8), - ), - labelColor: Colors.black, - unselectedLabelColor: Colors.black, - onTap: (index) { - if (index == 0) { - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const LoginScreen()), - ); - } - }, - tabs: const [ - Tab(text: 'Войти'), - Tab(text: 'Создать'), - ], - ), - ), - - const SizedBox(height: 20), - - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Никнейм', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 8), - TextFormField( - controller: _nicknameController, - decoration: const InputDecoration( - filled: true, - fillColor: Color(0xFFEDD6B0), - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Пожалуйста, введите никнейм'; - } - return null; - }, - ), - - const SizedBox(height: 16), - - const Text( - 'Электронная почта', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 8), - TextFormField( - controller: _emailController, - decoration: const InputDecoration( - filled: true, - fillColor: Color(0xFFEDD6B0), - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), - ), - keyboardType: TextInputType.emailAddress, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Пожалуйста, введите email'; - } - if (!EmailValidator.validate(value)) { - return 'Введите корректный email'; - } - return null; - }, - ), - - const SizedBox(height: 16), - - const Text( - 'Пароль', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 8), - TextFormField( - controller: _passwordController, - decoration: InputDecoration( - filled: true, - fillColor: const Color(0xFFEDD6B0), - border: const OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - suffixIcon: IconButton( - icon: Icon( - _obscurePassword ? Icons.visibility_off : Icons.visibility, - color: Colors.black54, - ), - onPressed: () { - setState(() { - _obscurePassword = !_obscurePassword; - }); - }, - ), - ), - obscureText: _obscurePassword, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Пожалуйста, введите пароль'; - } - if (value.length < 8) { - return 'Пароль должен содержать минимум 8 символов'; - } - return null; - }, - ), - - const SizedBox(height: 16), - - const Text( - 'Повторите пароль', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 8), - TextFormField( - controller: _confirmPasswordController, - decoration: InputDecoration( - filled: true, - fillColor: const Color(0xFFEDD6B0), - border: const OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - suffixIcon: IconButton( - icon: Icon( - _obscureConfirmPassword ? Icons.visibility_off : Icons.visibility, - color: Colors.black54, - ), - onPressed: () { - setState(() { - _obscureConfirmPassword = !_obscureConfirmPassword; - }); - }, - ), - ), - obscureText: _obscureConfirmPassword, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Пожалуйста, подтвердите пароль'; - } - if (value != _passwordController.text) { - return 'Пароли не совпадают'; - } - return null; - }, - ), - - const SizedBox(height: 40), - - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: isLoading ? null : _register, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - disabledBackgroundColor: const Color(0xFFD6A067).withOpacity(0.7), - ), - child: isLoading - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.black, - ), - ) - : const Text( - 'Регистрация', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - ), - ), - ), - - Container( - width: 100, - height: 4, - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(2), - ), - ), - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/frontend/lib/views/search_players_screen.dart b/frontend/lib/views/search_players_screen.dart index 101a995..8238b6e 100644 --- a/frontend/lib/views/search_players_screen.dart +++ b/frontend/lib/views/search_players_screen.dart @@ -1,219 +1,89 @@ import 'package:flutter/material.dart'; +import 'profile_screen.dart'; -class SearchPlayersScreen extends StatefulWidget { - const SearchPlayersScreen({super.key}); - - @override - State createState() => _SearchPlayersScreenState(); -} - -class _SearchPlayersScreenState extends State { - final TextEditingController _searchController = TextEditingController(); - final List _players = [ - PlayerItem(name: 'Cardly', cards: 5), - PlayerItem(name: 'Cardly1', cards: 5), - PlayerItem(name: 'Cardly2', cards: 5), - PlayerItem(name: 'Cardly3', cards: 5), - PlayerItem(name: 'Cardly4', cards: 5), - ]; - - int _currentIndex = 0; - - @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } +class SearchPlayersModal extends StatelessWidget { + const SearchPlayersModal({super.key}); @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон - appBar: AppBar( - backgroundColor: const Color(0xFFEDD6B0), - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.black), - onPressed: () => Navigator.pop(context), - ), - title: const Text( - 'Cardly', - style: TextStyle(color: Colors.black), - ), - actions: [ - IconButton( - icon: const Icon(Icons.close, color: Colors.black), - onPressed: () => Navigator.pop(context), - ), - ], - bottom: PreferredSize( - preferredSize: const Size.fromHeight(48.0), - child: Icon( - Icons.keyboard_arrow_down, - color: Colors.black, - size: 24.0, - ), - ), - ), - body: Column( - children: [ - // Основное содержимое - список игроков - Expanded( - child: Container( - color: const Color(0xFFEDD6B0), - child: ListView.builder( - itemCount: _players.length, - itemBuilder: (context, index) { - return PlayerListItem(player: _players[index]); - }, - ), - ), - ), - - // Кнопка "Создай свою уникальную карточку" - Padding( - padding: const EdgeInsets.all(16.0), - child: SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: const Text( - 'Создай свою уникальную карточку', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - ), - ), + final List players = [ + PlayerItem(name: 'Cardly', cards: 5, id: '123456'), + PlayerItem(name: 'Cardly1', cards: 5, id: '123457'), + PlayerItem(name: 'Cardly2', cards: 5, id: '123458'), + PlayerItem(name: 'Cardly3', cards: 5, id: '123459'), + PlayerItem(name: 'Cardly4', cards: 5, id: '123460'), + ]; + final TextEditingController _searchController = TextEditingController(); + final double modalHeight = MediaQuery.of(context).size.height * 0.6; + return Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(bottom: 330), + child: Container( + height: modalHeight, + width: MediaQuery.of(context).size.width * 0.98, + decoration: const BoxDecoration( + color: Color(0xFFEAD7C3), + borderRadius: BorderRadius.all(Radius.circular(10)), ), - - // Кнопки "Квесты" и "Новости" - Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 16.0), - child: Row( - children: [ - // Кнопка "Квесты" - Expanded( - child: ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(left: 8, right: 8, top: 8, bottom: 0), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black), + onPressed: () => Navigator.of(context).pop(), ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset( - 'assets/icons/квесты.png', - height: 24, - color: Colors.black, + Expanded( + child: Container( + height: 36, + margin: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: const Color(0xFFFFF4E3), + borderRadius: BorderRadius.circular(8), ), - const SizedBox(height: 4.0), - const Text( - 'Квесты', - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, + child: TextField( + controller: _searchController, + style: const TextStyle( + fontFamily: 'Jost', + fontSize: 16, + color: Colors.black, + ), + decoration: const InputDecoration( + border: InputBorder.none, + hintText: 'Введите ник пользователя', + hintStyle: TextStyle( + fontFamily: 'Jost', + fontSize: 16, + color: Colors.black, + ), + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), ), - ], - ), - ), - ), - - const SizedBox(width: 12.0), - - // Кнопка "Новости" - Expanded( - child: ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), ), ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset( - 'assets/icons/новости.png', - height: 24, - color: Colors.black, - ), - const SizedBox(height: 4.0), - const Text( - 'Новости', - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - ), - ), - ], + IconButton( + icon: const Icon(Icons.close, color: Colors.black), + onPressed: () => _searchController.clear(), ), - ), + ], ), - ], - ), - ), - - // Нижняя навигационная панель - Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16.0), - topRight: Radius.circular(16.0), ), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - setState(() { - _currentIndex = index; - }); - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - label: 'Гл.меню', + const Divider(height: 1, color: Colors.black26), + Expanded( + child: ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 0), + itemCount: players.length, + itemBuilder: (context, index) { + return PlayerListItem(player: players[index]); + }, ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - label: '', - ), - ], - ), + ), + ], ), - ], + ), ), ); } @@ -223,66 +93,181 @@ class _SearchPlayersScreenState extends State { class PlayerItem { final String name; final int cards; - - PlayerItem({required this.name, required this.cards}); + final String id; + + PlayerItem({ + required this.name, + required this.cards, + required this.id, + }); } // Виджет элемента списка игроков class PlayerListItem extends StatelessWidget { final PlayerItem player; - + const PlayerListItem({super.key, required this.player}); - + @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + return InkWell( + onTap: () { + Navigator.pop(context); // Закрываем модальное окно поиска + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ProfileScreen( + isOtherUser: true, + playerName: player.name, + playerId: player.id, + cardsCollected: player.cards, + collectionsCollected: 2, // Примерное значение, в реальном приложении должно приходить с сервера + ), + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 10.0), child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - // Аватар пользователя + // Аватар пользователя как в profile_screen Container( - width: 40, - height: 40, + width: 44, + height: 44, decoration: BoxDecoration( shape: BoxShape.circle, + color: const Color(0xFFEAD7C3), border: Border.all(color: Colors.black, width: 2), ), - child: Image.asset( - 'assets/icons/профиль.png', - height: 24, - color: Colors.black, + child: const Center( + child: Icon(Icons.person_outline, size: 28, color: Colors.black), ), ), - - const SizedBox(width: 16.0), - + const SizedBox(width: 12.0), // Имя пользователя Text( player.name, style: const TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w500, + fontFamily: 'Jost', + fontSize: 18.0, + fontWeight: FontWeight.w400, + color: Colors.black, ), ), - const Spacer(), - - // Коллекция карточек пользователя + // Карточки пользователя (макет из инвентаря, но без фото) Row( children: List.generate( player.cards, - (index) => Container( - margin: const EdgeInsets.only(left: 4.0), - width: 32, - height: 42, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(4.0), - ), + (index) => Padding( + padding: const EdgeInsets.only(left: 6.0), + child: CardMockup(), ), ), ), ], + ), + ), + ); + } +} + +// Макет карточки как в инвентаре, но без фото +class CardMockup extends StatelessWidget { + const CardMockup({super.key}); + + @override + Widget build(BuildContext context) { + double width = 32; + double height = 44; + + // Precise calculation of vertical space consumed by non-content elements + double totalNonContentHeight = 3 + 3 + 3 + 2 + 1; // Total vertical space used by borders, padding, and line + double availableContentHeight = height - totalNonContentHeight; + double imageHeight = availableContentHeight * 0.65; + double rarityHeight = availableContentHeight * 0.2; + + return Container( + width: width, + height: height, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.black, width: 1.5), + ), + child: Padding( + padding: const EdgeInsets.all(0), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(3), + ), + child: Padding( + padding: const EdgeInsets.all(1.5), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(2), + border: Border.all(color: Colors.black, width: 1.5), + ), + child: Padding( + padding: EdgeInsets.zero, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(1.0), + ), + child: Column( + children: [ + // Image placeholder area + Container( + height: imageHeight, + decoration: const BoxDecoration( + color: Color(0xFFEAD7C3), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(1.0), + topRight: Radius.circular(1.0), + ), + ), + child: const Center( + child: Text( + 'Нет \n изоб', + style: TextStyle(color: Colors.black45, fontSize: 7, fontFamily: 'Jost'), + textAlign: TextAlign.center, + ), + ), + ), + Container( + height: 1, + color: Colors.black, + ), + // Rarity icons area + Container( + height: rarityHeight, + decoration: const BoxDecoration( + color: Color(0xFFD6A067), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(1.0), + bottomRight: Radius.circular(1.0), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(4, (i) => Image.asset( + 'assets/icons/редкость.png', + height: rarityHeight > 0 ? rarityHeight * 0.7 : 0, + )), + ), + ), + ], + ), + ), + ), + ), + ), + ), ), ); } diff --git a/frontend/lib/views/settings_screen.dart b/frontend/lib/views/settings_screen.dart index 0cdae76..6306da7 100644 --- a/frontend/lib/views/settings_screen.dart +++ b/frontend/lib/views/settings_screen.dart @@ -1,280 +1,286 @@ import 'package:flutter/material.dart'; import 'home_screen.dart'; +import 'package:provider/provider.dart'; +import '../controllers/auth_controller.dart'; +import '../services/analytics_service.dart'; -class SettingsScreen extends StatefulWidget { - const SettingsScreen({super.key}); +class SettingsDialog extends StatefulWidget { + const SettingsDialog({super.key}); @override - State createState() => _SettingsScreenState(); + State createState() => _SettingsDialogState(); } -class _SettingsScreenState extends State { +class _SettingsDialogState extends State { bool _notificationsEnabled = true; bool _exchangesDeclineEnabled = true; bool _inventoryDisplayEnabled = true; + String? _hintText; + bool _showHint = false; + + void _showHintMessage(String text) { + setState(() { + _hintText = text; + _showHint = true; + }); + } + @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон - body: Stack( - children: [ - // Основное содержимое - Container( - margin: const EdgeInsets.only(top: 40.0), + AnalyticsService.trackScreenView('settings_dialog'); + return GestureDetector( + onTapDown: (_) { + if (_showHint) { + setState(() { + _showHint = false; + }); + } + }, + child: Center( + child: Material( + color: Colors.transparent, + child: Container( + width: MediaQuery.of(context).size.width * 0.88, + height: MediaQuery.of(context).size.height * 0.75, decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(12.0), - border: Border.all(color: Colors.black, width: 1), + color: const Color(0xFFEAD7C3), + borderRadius: BorderRadius.circular(10.0), + border: Border.all(color: Colors.black, width: 2), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Stack( children: [ - // Заголовок "Настройки" - const Padding( - padding: EdgeInsets.only(top: 16.0, left: 24.0), - child: Text( - 'Настройки', - style: TextStyle( - fontSize: 22.0, - fontWeight: FontWeight.bold, - ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + const Center( + child: Text( + 'Настройки', + style: TextStyle( + fontFamily: 'Jost', + fontSize: 28.0, + fontWeight: FontWeight.w400, + ), + ), + ), + const SizedBox(height: 24.0), + _buildSettingItem( + title: 'Уведомления', + hasSwitch: true, + switchValue: _notificationsEnabled, + onChanged: (value) { + setState(() { + _notificationsEnabled = value; + }); + }, + ), + const SizedBox(height: 16.0), + _buildSettingItem( + title: 'Отклонение обменов', + hasSwitch: true, + switchValue: _exchangesDeclineEnabled, + hasInfo: true, + onInfoTap: () => _showHintMessage('Подсказка по отклонению обменов'), + onChanged: (value) { + setState(() { + _exchangesDeclineEnabled = value; + }); + }, + ), + const SizedBox(height: 16.0), + _buildSettingItem( + title: 'Показ инвентаря', + hasSwitch: true, + switchValue: _inventoryDisplayEnabled, + hasInfo: true, + onInfoTap: () => _showHintMessage('Подсказка по показу инвентаря'), + onChanged: (value) { + setState(() { + _inventoryDisplayEnabled = value; + }); + }, + ), + const Spacer(), + Padding( + padding: const EdgeInsets.only(bottom: 26.0), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () async { + try { + await Provider.of(context, listen: false).logout(); + if (!mounted) return; + Navigator.of(context).pop(); // Закрываем диалог настроек + Navigator.of(context).pushNamedAndRemoveUntil('/auth', (route) => false); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Ошибка при выходе: ${e.toString()}')), + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD6A067), + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 18.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + ), + child: const Text( + 'Выйти из аккаунта', + style: TextStyle( + fontFamily: 'Jost', + fontSize: 18.0, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ], ), ), - - // Подсказка - Align( - alignment: Alignment.centerRight, - child: Container( - margin: const EdgeInsets.only(right: 16.0, top: 8.0), - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8.0), - ), - child: const Text( - 'Текст\nподсказки', - style: TextStyle( - fontSize: 12.0, - color: Colors.black87, + Positioned( + top: -1, + right: -1, + child: GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(10.0), + border: Border.all(color: Colors.black, width: 2), + ), + child: const Center( + child: Icon( + Icons.close_rounded, + color: Colors.black, + size: 44.0, + ), ), - textAlign: TextAlign.center, ), ), ), - - const SizedBox(height: 16.0), - - // Опция "Уведомления" - _buildSettingItem( - title: 'Уведомления', - hasSwitch: false, - onTap: () { - // Действие при нажатии на опцию "Уведомления" - }, - ), - - // Опция "Отклонение обменов" - _buildSettingItem( - title: 'Отклонение обменов', - hasSwitch: true, - switchValue: _exchangesDeclineEnabled, - hasInfo: true, - onChanged: (value) { - setState(() { - _exchangesDeclineEnabled = value; - }); - }, - ), - - // Опция "Показ инвентаря" - _buildSettingItem( - title: 'Показ инвентаря', - hasSwitch: true, - switchValue: _inventoryDisplayEnabled, - hasInfo: true, - onChanged: (value) { - setState(() { - _inventoryDisplayEnabled = value; - }); - }, - ), - - const Spacer(), - - // Кнопка "Выйти из аккаунта" - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - onPressed: () { - // Логика выхода из аккаунта - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: const Text( - 'Выйти из аккаунта', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, + if (_showHint && _hintText != null) + Positioned( + left: MediaQuery.of(context).size.width * 0.35, + top: _hintText!.contains('обменов') ? 195 : 255, + child: Material( + color: Colors.transparent, + child: Container( + width: 160, + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.black, width: 1), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Text( + _hintText!, + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: 'Jost', + fontSize: 14, + color: Colors.black, + ), + ), ), ), ), - ), ], ), ), - - // Кнопка закрытия (X) - Positioned( - top: 50.0, - right: 16.0, - child: Container( - width: 40.0, - height: 40.0, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 1), - ), - child: InkWell( - borderRadius: BorderRadius.circular(8.0), - onTap: () { - Navigator.pop(context); - }, - child: const Icon( - Icons.close, - color: Colors.black, - size: 22.0, - ), - ), - ), - ), - - // Нижняя навигационная панель - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: 0, - onTap: (index) { - if (index == 0) { // Только для главного меню - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - label: 'Гл.меню', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - label: '', - ), - ], - ), - ), - ), - ], + ), ), ); } - - // Строка настройки + Widget _buildSettingItem({ required String title, bool hasSwitch = false, bool hasInfo = false, bool switchValue = false, - VoidCallback? onTap, Function(bool)? onChanged, + VoidCallback? onInfoTap, }) { - return InkWell( - onTap: hasSwitch ? null : onTap, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), - child: Row( - children: [ - // Название настройки - Text( - title, - style: const TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w500, - ), + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), + child: Row( + children: [ + Text( + title, + style: const TextStyle( + fontFamily: 'Jost', + fontSize: 18.0, + fontWeight: FontWeight.w400, ), - - // Иконка информации - if (hasInfo) - Container( + ), + if (hasInfo) + GestureDetector( + onTap: onInfoTap, + child: Container( margin: const EdgeInsets.only(left: 8.0), - width: 20.0, - height: 20.0, + width: 22.0, + height: 22.0, decoration: const BoxDecoration( color: Colors.black, shape: BoxShape.circle, ), child: const Center( child: Text( - 'i', + '!', style: TextStyle( color: Colors.white, - fontSize: 12.0, - fontWeight: FontWeight.bold, + fontFamily: 'Jost', + fontSize: 16.0, + fontWeight: FontWeight.w500, ), ), ), ), - - const Spacer(), - - // Переключатель - if (hasSwitch) - Container( - width: 26.0, - height: 26.0, + ), + const Spacer(), + if (hasSwitch) + GestureDetector( + onTap: () => onChanged?.call(!switchValue), + child: Container( + width: 56.0, + height: 32.0, decoration: BoxDecoration( - color: switchValue ? Colors.white : Colors.transparent, - shape: BoxShape.circle, - border: Border.all(color: Colors.black, width: 1), + color: const Color(0xFFD6A067), + borderRadius: BorderRadius.circular(32.0), + ), + child: Align( + alignment: switchValue ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.all(4.0), + width: 24.0, + height: 24.0, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: switchValue + ? const Icon(Icons.check, color: Colors.black, size: 16) + : null, + ), ), - child: switchValue - ? const Icon( - Icons.check, - color: Colors.green, - size: 18.0, - ) - : null, ), - ], - ), + ), + ], ), ); } diff --git a/frontend/lib/views/shop_coin_details_screen.dart b/frontend/lib/views/shop_coin_details_screen.dart deleted file mode 100644 index a70887c..0000000 --- a/frontend/lib/views/shop_coin_details_screen.dart +++ /dev/null @@ -1,305 +0,0 @@ -import 'package:flutter/material.dart'; -import 'home_screen.dart'; -import 'shop_screen.dart'; -import 'exchanges_screen.dart'; - -class ShopCoinDetailsScreen extends StatefulWidget { - final String coinName; - - const ShopCoinDetailsScreen({ - super.key, - required this.coinName, - }); - - @override - State createState() => _ShopCoinDetailsScreenState(); -} - -class _ShopCoinDetailsScreenState extends State { - int _currentIndex = 2; // Индекс вкладки "Магазин" в нижней навигации - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), - appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), - elevation: 0, - leading: Container( - margin: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.black, width: 2), - ), - child: const Icon( - Icons.person_outline, - color: Colors.black, - ), - ), - title: Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(16.0), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: const [ - Text( - '1000', - style: TextStyle( - color: Colors.black, - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(width: 6.0), - Icon( - Icons.monetization_on, - color: Colors.amber, - size: 20.0, - ), - ], - ), - ), - centerTitle: true, - actions: [ - IconButton( - icon: const Icon(Icons.search, color: Colors.black), - onPressed: () {}, - ), - ], - ), - body: Column( - children: [ - // Вкладки "Наборы" и "Монеты" - Padding( - padding: const EdgeInsets.all(16.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8.0), - ), - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: () { - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - ); - }, - child: Text( - 'Наборы', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - color: Colors.grey[600], - ), - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.all(Radius.circular(8.0)), - border: Border( - bottom: BorderSide( - color: Colors.black, - width: 3.0, - ), - ), - ), - child: const Text( - 'Монеты', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - ), - ], - ), - ), - ), - // Основное содержимое - детали монеты - Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 1), - ), - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - const SizedBox(height: 10.0), - // Превью монеты - Container( - height: 150, - width: double.infinity, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - ), - child: Center( - child: Text( - widget.coinName, - style: const TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - const SizedBox(height: 10.0), - // Название и цена - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - widget.coinName, - style: const TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - ), - ), - const Text( - 'Цена', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - const SizedBox(height: 20.0), - // Кнопка покупки - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Вы купили ${widget.coinName}')), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: const Text( - 'Купить', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - ), - // Кнопка закрытия - Positioned( - top: 8.0, - right: 8.0, - child: GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( - padding: const EdgeInsets.all(4.0), - decoration: BoxDecoration( - color: Colors.amber, - borderRadius: BorderRadius.circular(4.0), - ), - child: const Icon( - Icons.close, - color: Colors.black, - size: 18.0, - ), - ), - ), - ), - ], - ), - ), - ), - ), - // Нижняя навигационная панель - Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - switch (index) { - case 0: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 2: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - label: '', - ), - ], - ), - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/frontend/lib/views/shop_screen.dart b/frontend/lib/views/shop_screen.dart index 052d481..f19bf30 100644 --- a/frontend/lib/views/shop_screen.dart +++ b/frontend/lib/views/shop_screen.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; -import 'home_screen.dart'; -import 'exchanges_screen.dart'; -import 'inventory_screen.dart'; -import 'shop_set_details_screen.dart'; -import 'shop_coin_details_screen.dart'; import 'profile_screen.dart'; +import 'search_players_screen.dart'; +import 'inventory_screen.dart'; +import 'pack_content_screen.dart'; +import '../services/api_service.dart'; +import '../models/shop_model.dart'; +import '../models/card_model.dart'; +import '../utils/error_formatter.dart'; +import '../utils/auth_utils.dart'; +import 'package:provider/provider.dart'; +import '../controllers/auth_controller.dart'; +import '../services/analytics_service.dart'; class ShopScreen extends StatefulWidget { const ShopScreen({super.key}); @@ -13,56 +19,109 @@ class ShopScreen extends StatefulWidget { State createState() => _ShopScreenState(); } -class _ShopScreenState extends State with SingleTickerProviderStateMixin { - late TabController _tabController; - int _currentIndex = 2; - int _selectedTab = 1; - - final List _sets = [ - 'Ценник 1', - 'Ценник 2', - 'Ценник 3', - 'Ценник 4', - ]; - - final List _coins = [ - 'Ценник 1', - ]; +class _ShopScreenState extends State { + late bool _isLoading; + late List _packs; + late List _coinOffers; + late String _error; @override void initState() { super.initState(); - _tabController = TabController(length: 2, vsync: this, initialIndex: _selectedTab); + _isLoading = true; + _packs = []; + _coinOffers = []; + _error = ''; + _loadShopData(); + AnalyticsService.trackScreenView('shop_screen'); } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); + + Future _loadShopData() async { + try { + setState(() { + _isLoading = true; + }); + + final packs = await ApiService().getAllPacks(); + final coinOffers = await ApiService().getCoinOffers(); + + setState(() { + _packs = packs; + _coinOffers = coinOffers; + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _error = ErrorFormatter.formatError(e); + }); + } + } + + Future _buyPack(Pack pack) async { + try { + final result = await ApiService().buyPack(pack.pack_ID); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Набор успешно куплен!')), + ); + + // Открываем экран с содержимым набора + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PackContentScreen( + setName: pack.name, + cards: (result['receivedCards'] as List).map((item) => CardModel.fromJson(item)).toList(), + ), + ), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } + } + + Future _buyCoinOffer(CoinOffer offer) async { + try { + final paymentUrl = await ApiService().purchaseCoins( + offer.offer_ID, + 'cardly://payment-callback', // URL схема для возврата в приложение + ); + + // Здесь нужно реализовать открытие URL для оплаты + // Это может быть WebView или переход в браузер + + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(ErrorFormatter.formatError(e))), + ); + } } @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), + backgroundColor: const Color(0xFFFBF6EF), elevation: 0, leading: Padding( padding: const EdgeInsets.only(left: 16.0), child: Container( width: 40.0, height: 40.0, - child: InkWell( borderRadius: BorderRadius.circular(20.0), onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const ProfileScreen()), - ); + if (AuthUtils.checkGuestAccess(context, 'profile_screen')) { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ProfileScreen()), + ); + } }, - child: Image.asset('assets/icons/профиль.png', height: 22), + child: Image.asset('assets/icons/профиль.png', height: 24), ), ), ), @@ -72,7 +131,7 @@ class _ShopScreenState extends State with SingleTickerProviderStateM Container( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), + color: const Color(0xFFEAD7C3), borderRadius: BorderRadius.circular(10.0), ), child: Row( @@ -84,6 +143,7 @@ class _ShopScreenState extends State with SingleTickerProviderStateM color: Colors.black, fontSize: 16.0, fontWeight: FontWeight.bold, + fontFamily: 'Jost', ), ), SizedBox(width: 6.0), @@ -92,266 +152,121 @@ class _ShopScreenState extends State with SingleTickerProviderStateM ), ), IconButton( - icon: Image.asset('assets/icons/поиск.png', height: 32), - onPressed: () {}, + icon: Image.asset('assets/icons/поиск.png', height: 26), + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const SearchPlayersModal(), + ); + }, ), ], ), - body: Column( + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _error.isNotEmpty + ? Center(child: Text(_error)) + : SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(12.0), - ), - child: TabBar( - controller: _tabController, - indicator: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.all(Radius.circular(12.0)), - border: Border( - bottom: BorderSide( - color: Colors.black, - width: 3.0, - ), + _buildPacksSection(), + _buildCoinOffersSection(), + ], ), - ), - labelColor: Colors.black, - unselectedLabelColor: Colors.black, - indicatorSize: TabBarIndicatorSize.tab, - tabs: const [ - Tab(text: 'Наборы'), - Tab(text: 'Монеты'), - ], - onTap: (index) { - setState(() { - _selectedTab = index; - }); - }, - ), - ), - ), - - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - _buildSetsTab(), - - _buildCoinsTab(), - ], - ), - ), - ], - ), - bottomNavigationBar: Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - switch (index) { - case 0: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 1: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const InventoryScreen()), - (route) => false, - ); - break; - case 3: - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - showSelectedLabels: true, - showUnselectedLabels: true, - selectedIconTheme: const IconThemeData( - size: 28, - ), - unselectedIconTheme: const IconThemeData( - size: 24, - ), - selectedLabelStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - ), - unselectedLabelStyle: const TextStyle( - fontSize: 11, - ), - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/главная.png', height: 24), - ), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/Инвентарь.png', height: 24), - ), - label: 'Инвентарь', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/магазин.png', height: 24), - ), - label: 'Магазин', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - activeIcon: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8), - ), - child: Image.asset('assets/icons/обменник.png', height: 24), - ), - label: 'Обменник', - ), - ], - ), ), + bottomNavigationBar: null, ); } - Widget _buildSetsTab() { - return GridView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 16.0, - mainAxisSpacing: 16.0, - childAspectRatio: 1.0, - ), - itemCount: _sets.length, - itemBuilder: (context, index) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ShopSetDetailsScreen(setName: _sets[index]), - ), - ); - }, - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - _sets[index], - style: const TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - textAlign: TextAlign.center, - ), - ), - ], - ), + Widget _buildPacksSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.all(16.0), + child: Text( + 'Наборы карт', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), - ); - }, - ); - } - - Widget _buildCoinsTab() { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 16.0, - mainAxisSpacing: 16.0, - childAspectRatio: 1.0, ), - itemCount: _coins.length, - itemBuilder: (context, index) { - return InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ShopCoinDetailsScreen(coinName: _coins[index]), - ), - ); - }, - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - ), + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.all(16.0), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.75, + crossAxisSpacing: 16.0, + mainAxisSpacing: 16.0, + ), + itemCount: _packs.length, + itemBuilder: (context, index) { + final pack = _packs[index]; + return Card( child: Column( - mainAxisAlignment: MainAxisAlignment.end, children: [ + Expanded( + child: Image.network( + pack.imageURL, + fit: BoxFit.cover, + ), + ), Padding( padding: const EdgeInsets.all(8.0), - child: Text( - _coins[index], - style: const TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - textAlign: TextAlign.center, + child: Column( + children: [ + Text( + pack.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Text('${pack.price} монет'), + ElevatedButton( + onPressed: () => _buyPack(pack), + child: const Text('Купить'), + ), + ], ), ), ], ), - ), + ); + }, + ), + ], + ); + } + + Widget _buildCoinOffersSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.all(16.0), + child: Text( + 'Монеты', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _coinOffers.length, + itemBuilder: (context, index) { + final offer = _coinOffers[index]; + return ListTile( + title: Text('${offer.coins} монет'), + subtitle: Text('${offer.price} ₽'), + trailing: ElevatedButton( + onPressed: () => _buyCoinOffer(offer), + child: const Text('Купить'), + ), ); }, ), + ], ); } } \ No newline at end of file diff --git a/frontend/lib/views/shop_set_content_screen.dart b/frontend/lib/views/shop_set_content_screen.dart deleted file mode 100644 index 7f51b7f..0000000 --- a/frontend/lib/views/shop_set_content_screen.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'package:flutter/material.dart'; -import 'home_screen.dart'; -import 'shop_screen.dart'; -import 'exchanges_screen.dart'; - -class ShopSetContentScreen extends StatefulWidget { - final String setName; - - const ShopSetContentScreen({ - super.key, - required this.setName, - }); - - @override - State createState() => _ShopSetContentScreenState(); -} - -class _ShopSetContentScreenState extends State { - int _currentIndex = 2; // Индекс вкладки "Магазин" в нижней навигации - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон - appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), - elevation: 0, - leading: Container( - margin: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: const Color(0xFFD6A067), - ), - child: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.black), - onPressed: () => Navigator.pop(context), - ), - ), - title: const Text( - 'В наборе содержатся:', - style: TextStyle( - color: Colors.black, - fontSize: 18.0, - ), - ), - ), - body: Column( - children: [ - // Сетка карточек - Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - childAspectRatio: 0.7, - crossAxisSpacing: 10.0, - mainAxisSpacing: 10.0, - ), - itemCount: 12, // 12 карточек в сетке (3x4) - itemBuilder: (context, index) { - return _buildCardItem(); - }, - ), - ), - ), - - // Нижняя навигационная панель - Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16.0), - topRight: Radius.circular(16.0), - ), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - - // Навигация в зависимости от выбранного индекса - switch (index) { - case 0: // Главное меню - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 2: // Магазин - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: // Обменчик - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.home), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Icon(Icons.book), - label: '', - ), - BottomNavigationBarItem( - icon: Icon(Icons.storefront), - label: 'Магазин', - ), - BottomNavigationBarItem( - icon: Icon(Icons.people), - label: '', - ), - ], - ), - ), - ], - ), - ); - } - - // Метод построения элемента карточки - Widget _buildCardItem() { - return Container( - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(6.0), - border: Border.all(color: Colors.black, width: 1), - ), - child: Column( - children: [ - // Верхняя часть карточки - Expanded( - flex: 2, - child: Container( - width: double.infinity, - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - border: Border( - bottom: BorderSide(color: Colors.black, width: 1), - ), - ), - ), - ), - - // Нижняя часть карточки - Expanded( - flex: 1, - child: Container( - width: double.infinity, - color: const Color(0xFFD6A067), - ), - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/frontend/lib/views/shop_set_details_screen.dart b/frontend/lib/views/shop_set_details_screen.dart deleted file mode 100644 index 103680f..0000000 --- a/frontend/lib/views/shop_set_details_screen.dart +++ /dev/null @@ -1,335 +0,0 @@ -import 'package:flutter/material.dart'; -import 'shop_set_content_screen.dart'; -import 'home_screen.dart'; -import 'shop_screen.dart'; -import 'exchanges_screen.dart'; -import 'pack_open_screen.dart'; - -class ShopSetDetailsScreen extends StatefulWidget { - final String setName; - - const ShopSetDetailsScreen({ - super.key, - required this.setName, - }); - - @override - State createState() => _ShopSetDetailsScreenState(); -} - -class _ShopSetDetailsScreenState extends State { - int _currentIndex = 2; // Индекс вкладки "Магазин" в нижней навигации - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFFF4E3), // Бежевый фон - appBar: AppBar( - backgroundColor: const Color(0xFFFFF4E3), - elevation: 0, - leading: Container( - margin: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.black, width: 2), - ), - child: const Icon( - Icons.person_outline, - color: Colors.black, - ), - ), - title: Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(16.0), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: const [ - Text( - '1000', - style: TextStyle( - color: Colors.black, - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(width: 6.0), - Icon( - Icons.monetization_on, - color: Colors.amber, - size: 20.0, - ), - ], - ), - ), - centerTitle: true, - actions: [ - IconButton( - icon: const Icon(Icons.search, color: Colors.black), - onPressed: () {}, - ), - ], - ), - body: Column( - children: [ - // Вкладки "Наборы" и "Монеты" - Padding( - padding: const EdgeInsets.all(16.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8.0), - ), - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - borderRadius: BorderRadius.all(Radius.circular(8.0)), - border: Border( - bottom: BorderSide( - color: Colors.black, - width: 3.0, - ), - ), - ), - child: const Text( - 'Наборы', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - ), - Text( - 'Монеты', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - color: Colors.grey[600], - ), - ), - ], - ), - ), - ), - - // Основное содержимое - детали набора - Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFEDD6B0), - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.black, width: 1), - ), - child: Stack( - children: [ - // Содержимое карточки набора - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - // Превью набора - Container( - height: 150, - width: double.infinity, - decoration: BoxDecoration( - color: const Color(0xFFD6A067), - borderRadius: BorderRadius.circular(8.0), - ), - ), - - const SizedBox(height: 10.0), - - // Название и цена - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - widget.setName, - style: const TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - ), - ), - const Text( - 'Цена', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - - const SizedBox(height: 20.0), - - // Кнопка просмотра содержимого - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ShopSetContentScreen(setName: widget.setName), - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: const Text( - 'Посмотреть Содержимое', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - - const SizedBox(height: 12.0), - - // Кнопка покупки - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - // После покупки переходим на экран открытия пака - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PackOpenScreen(setName: widget.setName), - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFD6A067), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: const Text( - 'Купить', - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - ), - - // Кнопка закрытия - Positioned( - top: 8.0, - right: 8.0, - child: GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( - padding: const EdgeInsets.all(4.0), - decoration: BoxDecoration( - color: Colors.amber, - borderRadius: BorderRadius.circular(4.0), - ), - child: const Icon( - Icons.close, - color: Colors.black, - size: 18.0, - ), - ), - ), - ), - ], - ), - ), - ), - ), - - // Нижняя навигационная панель - Container( - decoration: const BoxDecoration( - color: Color(0xFFD6A067), - ), - child: BottomNavigationBar( - currentIndex: _currentIndex, - onTap: (index) { - if (index != _currentIndex) { - setState(() { - _currentIndex = index; - }); - - // Навигация в зависимости от выбранного индекса - switch (index) { - case 0: // Главное меню - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - (route) => false, - ); - break; - case 2: // Магазин - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ShopScreen()), - (route) => false, - ); - break; - case 3: // Обменчик - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ExchangesScreen()), - (route) => false, - ); - break; - } - } - }, - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black54, - items: [ - BottomNavigationBarItem( - icon: Image.asset('assets/icons/главная.png', height: 24), - label: 'Главная', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/Инвентарь.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/магазин.png', height: 24), - label: '', - ), - BottomNavigationBarItem( - icon: Image.asset('assets/icons/обменник.png', height: 24), - label: '', - ), - ], - ), - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/frontend/lib/views/splash_screen.dart b/frontend/lib/views/splash_screen.dart index afcc846..4225eed 100644 --- a/frontend/lib/views/splash_screen.dart +++ b/frontend/lib/views/splash_screen.dart @@ -1,45 +1,8 @@ import 'package:flutter/material.dart'; -import 'dart:async'; -import 'package:provider/provider.dart'; -import '../controllers/auth_controller.dart'; -import 'auth_screen.dart'; -import 'home_screen.dart'; -class SplashScreen extends StatefulWidget { +class SplashScreen extends StatelessWidget { const SplashScreen({super.key}); - @override - State createState() => _SplashScreenState(); -} - -class _SplashScreenState extends State { - @override - void initState() { - super.initState(); - - // Запускаем проверку авторизации с небольшой задержкой для отображения сплеш-скрина - Timer(const Duration(seconds: 2), () { - _checkAuth(); - }); - } - - Future _checkAuth() async { - final authController = Provider.of(context, listen: false); - final isLoggedIn = await authController.tryAutoLogin(); - - if (!mounted) return; - - if (isLoggedIn) { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (context) => const HomeScreen()), - ); - } else { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (context) => const AuthScreen()), - ); - } - } - @override Widget build(BuildContext context) { return Scaffold( @@ -63,7 +26,10 @@ class _SplashScreenState extends State { fontWeight: FontWeight.bold, ), ), - const SizedBox(height: 100), + const SizedBox(height: 20), + const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Color(0xFFD9A76A)), + ), ], ), ), diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index bd65de1..2d47809 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -42,6 +42,8 @@ dependencies: http: ^1.2.0 shared_preferences: ^2.2.2 email_validator: ^2.1.17 + # Analytics + appmetrica_plugin: ^3.2.0 dev_dependencies: flutter_test: