diff --git a/.forgejo/workflows/deploy.yaml b/.forgejo/workflows/deploy.yaml index 1d27286..bc832d7 100644 --- a/.forgejo/workflows/deploy.yaml +++ b/.forgejo/workflows/deploy.yaml @@ -315,6 +315,12 @@ jobs: echo "🔄 Running pre-deploy version bump..." ./tools/deploy/bump-version.sh + - name: Push Version Bump + run: | + echo "📤 Pushing version bump to remote..." + git push + echo "✅ Version bump pushed" + - name: Configure Git run: | git config --global http.postBuffer 52428800 diff --git a/mnemo_cards_backend/build/unit_test_assets/NOTICES.Z b/mnemo_cards_backend/build/unit_test_assets/NOTICES.Z index b01b352..90f52ff 100644 Binary files a/mnemo_cards_backend/build/unit_test_assets/NOTICES.Z and b/mnemo_cards_backend/build/unit_test_assets/NOTICES.Z differ diff --git a/mnemo_cards_backend/lib/api/v2/auth_api_v2.dart b/mnemo_cards_backend/lib/api/v2/auth_api_v2.dart index 5e203cd..6e4e875 100644 --- a/mnemo_cards_backend/lib/api/v2/auth_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/auth_api_v2.dart @@ -1,10 +1,12 @@ import 'dart:convert'; +import 'dart:developer'; import 'package:injectable/injectable.dart'; import 'package:mnemo_cards_backend/api/authorize/helpers.dart'; import 'package:mnemo_cards_backend/api/v2/jwt_service.dart'; import 'package:mnemo_cards_backend/api/user/google_api.dart'; import 'package:mnemo_cards_backend/auth/telegram_auth_code_service.dart'; +import 'package:mnemo_cards_backend/user/telegram.dart'; import 'package:mnemo_cards_backend/user/user_manager.dart'; import 'package:mnemo_cards_backend/user/user_model.dart'; import 'package:shelf/shelf.dart'; @@ -228,6 +230,48 @@ class AuthApiV2 { /// POST /api/v2/auth/oauth/telegram /// Authenticate with Telegram auth code from bot /// Body: { code: string } + @Route.post('/auth/telegram/web-app') + @OpenApiRoute() + Future authenticateTelegramWebApp(Request request) async { + try { + final body = await request.readAsString(); + final json = jsonDecode(body) as Map; + final initData = json['initData'] as String?; + + if (initData == null || initData.isEmpty) { + return _badRequest('Telegram Web App init data is required'); + } + + // Use the existing telegram utils to get user ID + final telegramUtils = TelegramUtils(); + final userId = await telegramUtils.getUserId(initData); + + if (userId == null) { + return _badRequest('Invalid Telegram Web App data'); + } + + // Find or create user based on telegram user ID + var user = await _userManager.findByTelegramId(userId); + if (user == null) { + // Create new user if doesn't exist + user = await _userManager.createFromTelegramData(userId, initData); + } + + // Generate tokens + final tokens = await _jwtService.generateTokens(user); + + return _ok({ + 'user': await user.toDto(), + 'accessToken': tokens.accessToken, + 'refreshToken': tokens.refreshToken, + 'expiresIn': tokens.expiresIn, + }); + } catch (e, s) { + log('Telegram Web App auth error', error: e, stackTrace: s); + return _badRequest('Authentication failed: $e'); + } + } + @Route.post('/auth/oauth/telegram') @OpenApiRoute() Future authenticateTelegram(Request request) async { diff --git a/mnemo_cards_backend/lib/api/v2/auth_api_v2.g.dart b/mnemo_cards_backend/lib/api/v2/auth_api_v2.g.dart index 6451685..7a33342 100644 --- a/mnemo_cards_backend/lib/api/v2/auth_api_v2.g.dart +++ b/mnemo_cards_backend/lib/api/v2/auth_api_v2.g.dart @@ -33,6 +33,11 @@ Router _$AuthApiV2Router(AuthApiV2 service) { r'/auth/telegram/code-status/', service.getTelegramCodeStatus, ); + router.add( + 'POST', + r'/auth/telegram/web-app', + service.authenticateTelegramWebApp, + ); router.add( 'POST', r'/auth/oauth/telegram', diff --git a/mnemo_cards_backend/macos/Podfile b/mnemo_cards_backend/macos/Podfile index c795730..b52666a 100644 --- a/mnemo_cards_backend/macos/Podfile +++ b/mnemo_cards_backend/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/mnemo_cards_backend/macos/Runner.xcodeproj/project.pbxproj b/mnemo_cards_backend/macos/Runner.xcodeproj/project.pbxproj index 1e0832a..79a29d2 100644 --- a/mnemo_cards_backend/macos/Runner.xcodeproj/project.pbxproj +++ b/mnemo_cards_backend/macos/Runner.xcodeproj/project.pbxproj @@ -259,7 +259,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { @@ -553,7 +553,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -632,7 +632,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -679,7 +679,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/mnemo_cards_backend/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/mnemo_cards_backend/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 6b39709..cbf270c 100644 --- a/mnemo_cards_backend/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/mnemo_cards_backend/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ diff --git a/mnemo_cards_backend/macos/Runner/AppDelegate.swift b/mnemo_cards_backend/macos/Runner/AppDelegate.swift index d53ef64..b3c1761 100644 --- a/mnemo_cards_backend/macos/Runner/AppDelegate.swift +++ b/mnemo_cards_backend/macos/Runner/AppDelegate.swift @@ -1,9 +1,13 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/mnemo_cards_backend/public/open_api.yaml b/mnemo_cards_backend/public/open_api.yaml index 51591ff..e48eac8 100644 --- a/mnemo_cards_backend/public/open_api.yaml +++ b/mnemo_cards_backend/public/open_api.yaml @@ -5,1054 +5,6 @@ info: servers: - url: "http://localhost:8080" paths: - /purchases/packs/: - post: - tags: - - PurchasesApiV2 - summary: createPackPurchase - description: "POST /api/v2/purchases/packs/{packId}\nCreate purchase intent for a pack\nReturns purchase info including payment URL for YooKassa" - operationId: createPackPurchase - parameters: - - name: packId - in: path - required: true - schema: - type: string - responses: - 200: - description: "Operation completed!" - /purchases/packs//status: - get: - tags: - - PurchasesApiV2 - summary: getPackPurchaseStatus - description: "GET /api/v2/purchases/packs/{packId}/status\nCheck if pack is purchased by the authenticated user" - operationId: getPackPurchaseStatus - parameters: - - name: packId - in: path - required: true - schema: - type: string - responses: - 200: - description: "Operation completed!" - /purchases/payments: - post: - tags: - - PurchasesApiV2 - summary: createPayment - description: POST /api/v2/purchases/payments\nCreate a payment\nCurrently supports YooKassa for web payments - operationId: createPayment - responses: - 200: - description: "Operation completed!" - /purchases/payments//verify: - get: - tags: - - PurchasesApiV2 - summary: verifyPayment - description: "GET /api/v2/purchases/payments/{paymentId}/verify\nVerify payment status\nUpdates user purchases on success" - operationId: verifyPayment - parameters: - - name: paymentId - in: path - required: true - schema: - type: string - responses: - 200: - description: "Operation completed!" - /users/me: - get: - tags: - - UsersApiV2 - summary: getCurrentUser - description: GET /api/v2/users/me\nReturns current authenticated user. - operationId: getCurrentUser - responses: - 200: - description: "Operation completed!" - patch: - tags: - - UsersApiV2 - summary: updateCurrentUser - description: PATCH /api/v2/users/me\nUpdates mutable user fields (currently name and email). - operationId: updateCurrentUser - responses: - 200: - description: "Operation completed!" - /users/me/settings: - post: - tags: - - UsersApiV2 - summary: updateUserSettings - description: POST /api/v2/users/me/settings\nUpdates user settings. - operationId: updateUserSettings - responses: - 200: - description: "Operation completed!" - /users/me/statistics: - post: - tags: - - UsersApiV2 - summary: addUserTestStatistics - description: POST /api/v2/users/me/statistics\nAdds user test statistics entry. - operationId: addUserTestStatistics - responses: - 200: - description: "Operation completed!" - /users/me/purchases: - get: - tags: - - UsersApiV2 - summary: getUserPurchases - description: GET /api/v2/users/me/purchases\nReturns current user's processed purchases. - operationId: getUserPurchases - responses: - 200: - description: "Operation completed!" - /users/me/statistics/detailed: - get: - tags: - - UsersApiV2 - summary: getDetailedStatistics - description: "GET /api/v2/users/me/statistics/detailed\nReturns detailed user statistics including streaks, study time, achievements, word statistics, and pack progress." - operationId: getDetailedStatistics - security: - - bearerAuth: [] - responses: - 200: - description: Detailed user statistics - content: - application/json: - schema: - $ref: '#/components/schemas/UserDataDto' - example: - allWordsStatistics: - words: - - word: "hello" - correct: 10.0 - incorrect: 2.0 - skipped: 0.0 - questionTypes: ["translation", "pronunciation"] - correct: 150.0 - incorrect: 30.0 - skipped: 5.0 - allTestsStatistics: - tests: [] - totalAttempts: 25 - averageScore: 0.85 - lastTimeOnline: "2024-01-15T10:30:00Z" - totalStudyTimeMinutes: 1200 - currentStreak: 7 - longestStreak: 15 - packProgress: - basic_pack: - packId: "basic_pack" - totalCards: 100 - learnedCards: 45 - studyTimeMinutes: 300 - lastStudyDate: "2024-01-15T09:00:00Z" - firstStudyDate: "2024-01-01T08:00:00Z" - cardAttempts: {} - averageAccuracy: 0.82 - studyDates: - - "2024-01-15T09:00:00Z" - - "2024-01-14T10:00:00Z" - categoryMinutes: - basic: 300 - achievements: - - id: "streak_7" - title: "Week Warrior" - description: "Study for 7 consecutive days" - type: "streak7Days" - unlockedAt: "2024-01-15T09:00:00Z" - progress: 1.0 - 401: - description: Unauthorized - Authentication required - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - error: "unauthorized" - message: "Authentication required" - 404: - description: User data not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - error: "user_data_not_found" - 500: - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - error: "internal_server_error" - message: "An unexpected error occurred" - /users/me/statistics/packs: - get: - tags: - - UsersApiV2 - summary: getPacksStatistics - description: "GET /api/v2/users/me/statistics/packs\nReturns statistics for all user packs or specific pack if packId provided." - operationId: getPacksStatistics - security: - - bearerAuth: [] - parameters: - - name: packId - in: query - description: Filter by specific pack ID - required: false - schema: - type: string - example: "basic_pack" - responses: - 200: - description: List of pack progress statistics - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/PackProgressDto' - example: - - packId: "basic_pack" - totalCards: 100 - learnedCards: 45 - studyTimeMinutes: 300 - lastStudyDate: "2024-01-15T09:00:00Z" - firstStudyDate: "2024-01-01T08:00:00Z" - cardAttempts: {} - averageAccuracy: 0.82 - - packId: "advanced_pack" - totalCards: 200 - learnedCards: 120 - studyTimeMinutes: 600 - lastStudyDate: "2024-01-14T15:00:00Z" - firstStudyDate: "2023-12-01T10:00:00Z" - cardAttempts: {} - averageAccuracy: 0.75 - 401: - description: Unauthorized - Authentication required - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - error: "unauthorized" - message: "Authentication required" - 500: - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - error: "internal_server_error" - message: "An unexpected error occurred" - /users/me/statistics/words: - get: - tags: - - UsersApiV2 - summary: getWordsStatistics - description: "GET /api/v2/users/me/statistics/words\nReturns paginated word statistics with optional filtering." - operationId: getWordsStatistics - security: - - bearerAuth: [] - parameters: - - name: packId - in: query - description: Filter by specific pack ID - required: false - schema: - type: string - example: "basic_pack" - - name: limit - in: query - description: Number of results per page (default 50, max 100) - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 50 - example: 50 - - name: offset - in: query - description: Pagination offset (default 0) - required: false - schema: - type: integer - minimum: 0 - default: 0 - example: 0 - - name: sortBy - in: query - description: Sort order - 'difficulty', 'accuracy', 'recent', or 'alphabetical' (default 'difficulty') - required: false - schema: - type: string - enum: [difficulty, accuracy, recent, alphabetical] - default: difficulty - example: "difficulty" - - name: needsReview - in: query - description: Filter to show only words needing review (set to 'true') - required: false - schema: - type: string - enum: ["true", "false"] - example: "false" - responses: - 200: - description: Paginated word statistics - content: - application/json: - schema: - $ref: '#/components/schemas/WordStatisticsPaginatedResponse' - example: - words: - - word: "hello" - correct: 10.0 - incorrect: 2.0 - skipped: 0.0 - questionTypes: ["translation"] - lastReviewed: "2024-01-15T09:00:00Z" - firstLearned: "2024-01-01T08:00:00Z" - recentAttempts: [] - difficultyScore: 0.17 - needsReview: false - packId: "basic_pack" - - word: "world" - correct: 5.0 - incorrect: 8.0 - skipped: 1.0 - questionTypes: ["translation", "pronunciation"] - lastReviewed: "2024-01-14T10:00:00Z" - firstLearned: "2024-01-01T08:00:00Z" - recentAttempts: [] - difficultyScore: 0.57 - needsReview: true - packId: "basic_pack" - totalCount: 45 - page: 0 - pageSize: 50 - hasMore: false - 400: - description: Bad request - Invalid query parameters - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - error: "bad_request" - message: "Invalid limit parameter. Must be between 1 and 100." - 401: - description: Unauthorized - Authentication required - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - error: "unauthorized" - message: "Authentication required" - 500: - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - error: "internal_server_error" - message: "An unexpected error occurred" - /users/me/statistics/timeline: - get: - tags: - - UsersApiV2 - summary: getTimelineStatistics - description: "GET /api/v2/users/me/statistics/timeline\nReturns timeline statistics for study activity over a specified period." - operationId: getTimelineStatistics - security: - - bearerAuth: [] - parameters: - - name: period - in: query - description: Time period - 'day', 'week', 'month', or 'year' (default 'month') - required: false - schema: - type: string - enum: [day, week, month, year] - default: month - example: "month" - - name: from - in: query - description: Start date in ISO 8601 format (overrides period if provided) - required: false - schema: - type: string - format: date-time - example: "2024-01-01T00:00:00Z" - - name: to - in: query - description: End date in ISO 8601 format (defaults to now if not provided) - required: false - schema: - type: string - format: date-time - example: "2024-01-31T23:59:59Z" - responses: - 200: - description: Timeline statistics for the specified period - content: - application/json: - schema: - $ref: '#/components/schemas/TimelineStatisticsResponse' - example: - period: "month" - startDate: "2024-01-01T00:00:00Z" - endDate: "2024-01-31T23:59:59Z" - totalDays: 31 - activeDays: 20 - totalMinutes: 1200 - averageDailyMinutes: 60.0 - currentStreak: 7 - dailyActivity: - "2024-01-15T00:00:00Z": 60 - "2024-01-14T00:00:00Z": 45 - "2024-01-13T00:00:00Z": 30 - studyDates: - - "2024-01-15T09:00:00Z" - - "2024-01-14T10:00:00Z" - - "2024-01-13T08:00:00Z" - 400: - description: Bad request - Invalid date format - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - error: "bad_request" - message: "Invalid date format. Use ISO 8601 format (YYYY-MM-DDTHH:mm:ssZ)." - 401: - description: Unauthorized - Authentication required - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - error: "unauthorized" - message: "Authentication required" - 500: - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - error: "internal_server_error" - message: "An unexpected error occurred" - /users/me/sessions: - post: - tags: - - UsersApiV2 - summary: recordStudySession - description: "POST /api/v2/users/me/sessions\nRecords a study session for the user. Used to track study activity and update statistics." - operationId: recordStudySession - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/StudySessionDto' - example: - sessionId: "session_1234567890" - startTime: "2024-01-15T09:00:00Z" - endTime: "2024-01-15T09:30:00Z" - wordsLearned: 10 - testsCompleted: 2 - accuracy: 0.85 - packId: "basic_pack" - testId: null - responses: - 200: - description: Session recorded successfully - content: - application/json: - schema: - type: object - properties: - result: - type: boolean - example: true - sessionId: - type: string - example: "session_1234567890" - example: - result: true - sessionId: "session_1234567890" - 400: - description: Bad request - Invalid session data - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - error: "bad_request" - message: "Invalid session data: startTime is required" - 401: - description: Unauthorized - Authentication required - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - error: "unauthorized" - message: "Authentication required" - 500: - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - error: "internal_server_error" - message: "An unexpected error occurred" - /users/me/achievements: - get: - tags: - - UsersApiV2 - summary: getAchievements - description: "GET /api/v2/users/me/achievements\nReturns user's achievements and progress. Includes both unlocked and locked achievements with progress indicators." - operationId: getAchievements - security: - - bearerAuth: [] - responses: - 200: - description: List of user achievements - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/AchievementDto' - example: - - id: "streak_7" - title: "Week Warrior" - description: "Study for 7 consecutive days" - iconUrl: null - unlockedAt: "2024-01-15T09:00:00Z" - type: "streak7Days" - progress: 1.0 - - id: "words_10" - title: "Word Explorer" - description: "Learn 10 words" - iconUrl: null - unlockedAt: "2024-01-10T08:00:00Z" - type: "words10Learned" - progress: 1.0 - - id: "streak_30" - title: "Monthly Master" - description: "Study for 30 consecutive days" - iconUrl: null - unlockedAt: null - type: "streak30Days" - progress: 0.23 - 401: - description: Unauthorized - Authentication required - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - error: "unauthorized" - message: "Authentication required" - 500: - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - example: - error: "internal_server_error" - message: "An unexpected error occurred" - /admin/users: - get: - tags: - - AdminUsersApiV2 - summary: getUsers - description: GET /api/v2/admin/users\nReturns list of users by optional ids. - operationId: getUsers - responses: - 200: - description: "Operation completed!" - post: - tags: - - AdminUsersApiV2 - summary: upsertUser - description: POST /api/v2/admin/users\nCreates or updates user data (admin editing). - operationId: upsertUser - responses: - 200: - description: "Operation completed!" - /admin/users/ids: - get: - tags: - - AdminUsersApiV2 - summary: getUserIds - description: GET /api/v2/admin/users/ids\nReturns comma separated user ids. - operationId: getUserIds - responses: - 200: - description: "Operation completed!" - /admin/users//purchases: - get: - tags: - - AdminUsersApiV2 - summary: getUserPurchases - description: "GET /api/v2/admin/users//purchases\nReturns user payments history." - operationId: getUserPurchases - parameters: - - name: userId - in: path - required: true - schema: - type: string - responses: - 200: - description: "Operation completed!" - /admin/users/: - delete: - tags: - - AdminUsersApiV2 - summary: deleteUser - description: "DELETE /api/v2/admin/users/\nDeletes user." - operationId: deleteUser - parameters: - - name: userId - in: path - required: true - schema: - type: string - responses: - 200: - description: "Operation completed!" - /auth/oauth/google: - post: - tags: - - AuthApiV2 - summary: authenticateGoogle - description: POST /api/v2/auth/oauth/google\nAuthenticate with Google ID token - operationId: authenticateGoogle - responses: - 200: - description: "Operation completed!" - /auth/telegram/generate-code: - post: - tags: - - AuthApiV2 - summary: generateTelegramCode - description: "POST /api/v2/auth/telegram/generate-code\nGenerate a new Telegram authentication code\nCalled by the Telegram bot when user requests a code\nBody: { telegramUserId: string, telegramUsername?: string, firstName?: string, lastName?: string }" - operationId: generateTelegramCode - responses: - 200: - description: "Operation completed!" - /auth/telegram/web-code: - post: - tags: - - AuthApiV2 - summary: createWebTelegramCode - description: "POST /api/v2/auth/telegram/web-code\nGenerates a new Telegram authentication code initiated from the web app" - operationId: createWebTelegramCode - responses: - 200: - description: "Operation completed!" - /auth/telegram/claim-code: - post: - tags: - - AuthApiV2 - summary: claimTelegramCode - description: "POST /api/v2/auth/telegram/claim-code\nCalled by Telegram bot when user sends a code generated via the web app\nBody: { code: string, telegramUserId: string, telegramUsername?: string, firstName?: string, lastName?: string }" - operationId: claimTelegramCode - responses: - 200: - description: "Operation completed!" - /auth/telegram/code-status/: - get: - tags: - - AuthApiV2 - summary: getTelegramCodeStatus - description: "GET /api/v2/auth/telegram/code-status/\nReturns current status for a Telegram authentication code" - operationId: getTelegramCodeStatus - parameters: - - name: code - in: path - required: true - schema: - type: string - responses: - 200: - description: "Operation completed!" - /auth/oauth/telegram: - post: - tags: - - AuthApiV2 - summary: authenticateTelegram - description: "POST /api/v2/auth/oauth/telegram\nAuthenticate with Telegram auth code from bot\nBody: { code: string }" - operationId: authenticateTelegram - responses: - 200: - description: "Operation completed!" - /auth/refresh: - post: - tags: - - AuthApiV2 - summary: refreshToken - description: POST /api/v2/auth/refresh\nRefresh access token using refresh token - operationId: refreshToken - responses: - 200: - description: "Operation completed!" - /auth/me: - get: - tags: - - AuthApiV2 - summary: getCurrentUser - description: GET /api/v2/auth/me\nGet current authenticated user (requires Bearer token) - operationId: getCurrentUser - responses: - 200: - description: "Operation completed!" - /auth/logout: - post: - tags: - - AuthApiV2 - summary: logout - description: "POST /api/v2/auth/logout\nLogout and invalidate tokens\nBody (optional): { refreshToken: string }" - operationId: logout - responses: - 200: - description: "Operation completed!" - /tests/: - get: - tags: - - TestsApiV2 - summary: getTest - description: "GET /api/v2/tests/{testId}\nGet test details by ID" - operationId: getTest - parameters: - - name: testId - in: path - required: true - schema: - type: string - responses: - 200: - description: "Operation completed!" - /tests//results: - post: - tags: - - TestsApiV2 - summary: submitTestResults - description: "POST /api/v2/tests/{testId}/results\nSubmit test results" - operationId: submitTestResults - parameters: - - name: testId - in: path - required: true - schema: - type: string - responses: - 200: - description: "Operation completed!" - /tests//history: - get: - tags: - - TestsApiV2 - summary: getTestHistory - description: "GET /api/v2/tests/{testId}/history\nGet test attempt history for the authenticated user\nSupports pagination via query params: ?page=1&limit=20" - operationId: getTestHistory - parameters: - - name: testId - in: path - required: true - schema: - type: string - responses: - 200: - description: "Operation completed!" - /games: - get: - tags: - - GamesApiV2 - summary: getGames - description: GET /api/v2/games\nGet all available games\nReturns list of games with metadata - operationId: getGames - responses: - 200: - description: "Operation completed!" - /games//assets: - get: - tags: - - GamesApiV2 - summary: getGameAssets - description: "GET /api/v2/games/{gameId}/assets\nGet game assets\nReturns game assets file (zip) or asset info" - operationId: getGameAssets - parameters: - - name: gameId - in: path - required: true - schema: - type: string - responses: - 200: - description: "Operation completed!" - /admin/discounts: - get: - tags: - - DiscountsApiV2 - summary: GET /api/v2/admin/discounts - operationId: listDiscountCampaigns - responses: - 200: - description: "Operation completed!" - post: - tags: - - DiscountsApiV2 - summary: POST /api/v2/admin/discounts - operationId: addDiscountCampaign - responses: - 200: - description: "Operation completed!" - /admin/discounts/: - delete: - tags: - - DiscountsApiV2 - summary: "DELETE /api/v2/admin/discounts/{id}" - operationId: deleteDiscountCampaign - parameters: - - name: id - in: path - required: true - schema: - type: string - responses: - 200: - description: "Operation completed!" - /subscriptions/plans: - get: - tags: - - SubscriptionsApiV2 - summary: getPlans - description: GET /api/v2/subscriptions/plans\nList available subscription plans\nReturns all available subscription plans. Authentication is optional. - operationId: getPlans - responses: - 200: - description: "Operation completed!" - /subscriptions/purchase: - post: - tags: - - SubscriptionsApiV2 - summary: purchase - description: POST /api/v2/subscriptions/purchase\nPurchase a subscription - operationId: purchase - responses: - 200: - description: "Operation completed!" - /subscriptions/status: - get: - tags: - - SubscriptionsApiV2 - summary: getStatus - description: GET /api/v2/subscriptions/status\nGet current user subscription status - operationId: getStatus - responses: - 200: - description: "Operation completed!" - /subscriptions/cancel: - post: - tags: - - SubscriptionsApiV2 - summary: cancel - description: POST /api/v2/subscriptions/cancel\nCancel user’s subscription - operationId: cancel - responses: - 200: - description: "Operation completed!" - /packs: - get: - tags: - - PacksApiV2 - summary: getPacks - description: "GET /api/v2/packs\nGet all pack previews with pagination\nQuery params: ?search=term&language=lang&page=1&limit=20" - operationId: getPacks - responses: - 200: - description: "Operation completed!" - /packs/: - get: - tags: - - PacksApiV2 - summary: getPack - description: "GET /api/v2/packs/{packId}\nGet pack details by ID\nReturns full pack details with purchase status if authenticated" - operationId: getPack - parameters: - - name: packId - in: path - required: true - schema: - type: string - responses: - 200: - description: "Operation completed!" - /packs//buy: - get: - tags: - - PacksApiV2 - summary: getPackBuyPage - description: "GET /api/v2/packs/{packId}/buy\nReturns pack purchase details (includes rewarded ads offer when available)" - operationId: getPackBuyPage - parameters: - - name: packId - in: path - required: true - schema: - type: string - responses: - 200: - description: "Operation completed!" - /packs//cards: - get: - tags: - - PacksApiV2 - summary: getPackCards - description: "GET /api/v2/packs/{packId}/cards\nGet all cards in a pack\nSupports pagination via query params: ?page=1&limit=20" - operationId: getPackCards - parameters: - - name: packId - in: path - required: true - schema: - type: string - responses: - 200: - description: "Operation completed!" - /packs//cards//image: - get: - tags: - - PacksApiV2 - summary: getCardImage - description: "GET /api/v2/packs/{packId}/cards/{cardId}/image\nGet card image\nReturns PNG image file\n\nImages are accessible for enabled packs even without authentication\nto allow image preview in public pack listings" - operationId: getCardImage - parameters: - - name: packId - in: path - required: true - schema: - type: string - - name: cardId - in: path - required: true - schema: - type: string - responses: - 200: - description: "Operation completed!" - /packs//tests: - get: - tags: - - PacksApiV2 - summary: getPackTests - description: "GET /api/v2/packs/{packId}/tests\nGet tests for a pack" - operationId: getPackTests - parameters: - - name: packId - in: path - required: true - schema: - type: string - responses: - 200: - description: "Operation completed!" - /tasks: - get: - tags: - - TasksApiV2 - summary: "GET /api/v2/tasks - Get tasks with optional filtering" - operationId: getTasks - responses: - 200: - description: "Operation completed!" - /tasks/: - get: - tags: - - TasksApiV2 - summary: "GET /api/v2/tasks/{taskId} - Get specific task" - operationId: getTask - parameters: - - name: taskId - in: path - required: true - schema: - type: string - responses: - 200: - description: "Operation completed!" - /tasks//start: - post: - tags: - - TasksApiV2 - summary: "POST /api/v2/tasks/{taskId}/start - Start a task" - operationId: startTask - parameters: - - name: taskId - in: path - required: true - schema: - type: string - responses: - 200: - description: "Operation completed!" - /tasks//complete: - post: - tags: - - TasksApiV2 - summary: "POST /api/v2/tasks/{taskId}/complete - Complete a task" - operationId: completeTask - parameters: - - name: taskId - in: path - required: true - schema: - type: string - responses: - 200: - description: "Operation completed!" - /users/me/tasks/progress: - get: - tags: - - TasksApiV2 - summary: "GET /api/v2/users/me/tasks/progress - Get user task progress" - operationId: getUserProgress - responses: - 200: - description: "Operation completed!" - /tasks/categories: - get: - tags: - - TasksApiV2 - summary: "GET /api/v2/tasks/categories - Get available task categories and filters" - operationId: getTaskCategories - responses: - 200: - description: "Operation completed!" /promocodes: get: tags: @@ -1145,6 +97,38 @@ paths: responses: 200: description: "Operation completed!" + /admin/discounts: + get: + tags: + - DiscountsApiV2 + summary: GET /api/v2/admin/discounts + operationId: listDiscountCampaigns + responses: + 200: + description: "Operation completed!" + post: + tags: + - DiscountsApiV2 + summary: POST /api/v2/admin/discounts + operationId: addDiscountCampaign + responses: + 200: + description: "Operation completed!" + /admin/discounts/: + delete: + tags: + - DiscountsApiV2 + summary: "DELETE /api/v2/admin/discounts/{id}" + operationId: deleteDiscountCampaign + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + 200: + description: "Operation completed!" /ads/product/acquire/: post: tags: @@ -1171,469 +155,643 @@ paths: responses: 200: description: "Operation completed!" -components: - securitySchemes: - bearerAuth: - type: http - scheme: bearer - bearerFormat: JWT - description: JWT Bearer token authentication - schemas: - ErrorResponse: - type: object - properties: - error: - type: string - description: Error code identifier - example: "bad_request" - message: - type: string - description: Human-readable error message - example: "Invalid request parameters" - required: - - error - UserDataDto: - type: object - description: Comprehensive user statistics and data - properties: - allWordsStatistics: - $ref: '#/components/schemas/AllWordsStatisticsDto' - allTestsStatistics: - $ref: '#/components/schemas/AllTestsStatisticsDto' - lastTimeOnline: - type: string - format: date-time - nullable: true - description: When user was last online - totalStudyTimeMinutes: - type: integer - description: Total study time in minutes - example: 1200 - currentStreak: - type: integer - description: Current consecutive days streak - example: 7 - longestStreak: - type: integer - description: Longest streak ever achieved - example: 15 - packProgress: - type: object - additionalProperties: - $ref: '#/components/schemas/PackProgressDto' - description: Map of pack ID to pack progress statistics - studyDates: - type: array - items: + /subscriptions/plans: + get: + tags: + - SubscriptionsApiV2 + summary: getPlans + description: GET /api/v2/subscriptions/plans\nList available subscription plans\nReturns all available subscription plans. Authentication is optional. + operationId: getPlans + responses: + 200: + description: "Operation completed!" + /subscriptions/purchase: + post: + tags: + - SubscriptionsApiV2 + summary: purchase + description: POST /api/v2/subscriptions/purchase\nPurchase a subscription + operationId: purchase + responses: + 200: + description: "Operation completed!" + /subscriptions/status: + get: + tags: + - SubscriptionsApiV2 + summary: getStatus + description: GET /api/v2/subscriptions/status\nGet current user subscription status + operationId: getStatus + responses: + 200: + description: "Operation completed!" + /subscriptions/cancel: + post: + tags: + - SubscriptionsApiV2 + summary: cancel + description: POST /api/v2/subscriptions/cancel\nCancel user’s subscription + operationId: cancel + responses: + 200: + description: "Operation completed!" + /users/me: + get: + tags: + - UsersApiV2 + summary: getCurrentUser + description: GET /api/v2/users/me\nReturns current authenticated user. + operationId: getCurrentUser + responses: + 200: + description: "Operation completed!" + patch: + tags: + - UsersApiV2 + summary: updateCurrentUser + description: PATCH /api/v2/users/me\nUpdates mutable user fields (currently name and email). + operationId: updateCurrentUser + responses: + 200: + description: "Operation completed!" + /users/me/settings: + post: + tags: + - UsersApiV2 + summary: updateUserSettings + description: POST /api/v2/users/me/settings\nUpdates user settings. + operationId: updateUserSettings + responses: + 200: + description: "Operation completed!" + /users/me/statistics: + post: + tags: + - UsersApiV2 + summary: addUserTestStatistics + description: POST /api/v2/users/me/statistics\nAdds user test statistics entry. + operationId: addUserTestStatistics + responses: + 200: + description: "Operation completed!" + /users/me/purchases: + get: + tags: + - UsersApiV2 + summary: getUserPurchases + description: GET /api/v2/users/me/purchases\nReturns current user's processed purchases. + operationId: getUserPurchases + responses: + 200: + description: "Operation completed!" + /users/me/statistics/detailed: + get: + tags: + - UsersApiV2 + summary: getDetailedStatistics + description: "GET /api/v2/users/me/statistics/detailed\nReturns detailed user statistics including streaks, study time, achievements." + operationId: getDetailedStatistics + responses: + 200: + description: "Operation completed!" + /users/me/statistics/packs: + get: + tags: + - UsersApiV2 + summary: getPacksStatistics + description: "GET /api/v2/users/me/statistics/packs\nReturns statistics for all user packs or specific pack if packId provided.\nQuery parameters: ?packId=" + operationId: getPacksStatistics + responses: + 200: + description: "Operation completed!" + /users/me/statistics/words: + get: + tags: + - UsersApiV2 + summary: getWordsStatistics + description: "GET /api/v2/users/me/statistics/words\nReturns paginated word statistics with optional filtering.\nQuery parameters:\n- packId: filter by specific pack\n- limit: number of results (default 50, max 100)\n- offset: pagination offset (default 0)\n- sortBy: 'difficulty', 'accuracy', 'recent' (default 'difficulty')\n- needsReview: 'true' to show only words needing review" + operationId: getWordsStatistics + responses: + 200: + description: "Operation completed!" + /users/me/statistics/timeline: + get: + tags: + - UsersApiV2 + summary: getTimelineStatistics + description: "GET /api/v2/users/me/statistics/timeline\nReturns timeline statistics for study activity.\nQuery parameters:\n- period: 'day', 'week', 'month', 'year' (default 'month')\n- from: ISO date string for start date\n- to: ISO date string for end date" + operationId: getTimelineStatistics + responses: + 200: + description: "Operation completed!" + /users/me/sessions: + post: + tags: + - UsersApiV2 + summary: recordStudySession + description: POST /api/v2/users/me/sessions\nRecords a study session for the user. + operationId: recordStudySession + responses: + 200: + description: "Operation completed!" + /users/me/achievements: + get: + tags: + - UsersApiV2 + summary: getAchievements + description: GET /api/v2/users/me/achievements\nReturns user's achievements and progress. + operationId: getAchievements + responses: + 200: + description: "Operation completed!" + /admin/users: + get: + tags: + - AdminUsersApiV2 + summary: getUsers + description: GET /api/v2/admin/users\nReturns list of users by optional ids. + operationId: getUsers + responses: + 200: + description: "Operation completed!" + post: + tags: + - AdminUsersApiV2 + summary: upsertUser + description: POST /api/v2/admin/users\nCreates or updates user data (admin editing). + operationId: upsertUser + responses: + 200: + description: "Operation completed!" + /admin/users/ids: + get: + tags: + - AdminUsersApiV2 + summary: getUserIds + description: GET /api/v2/admin/users/ids\nReturns comma separated user ids. + operationId: getUserIds + responses: + 200: + description: "Operation completed!" + /admin/users//purchases: + get: + tags: + - AdminUsersApiV2 + summary: getUserPurchases + description: "GET /api/v2/admin/users//purchases\nReturns user payments history." + operationId: getUserPurchases + parameters: + - name: userId + in: path + required: true + schema: type: string - format: date-time - description: List of dates when user studied - categoryMinutes: - type: object - additionalProperties: - type: integer - description: Study time by category/language in minutes - achievements: - type: array - items: - $ref: '#/components/schemas/AchievementDto' - description: User's achievements - AllWordsStatisticsDto: - type: object - description: Aggregated word statistics - properties: - words: - type: array - items: - $ref: '#/components/schemas/WordStatisticsDto' - correct: - type: number - format: double - description: Total correct answers - incorrect: - type: number - format: double - description: Total incorrect answers - skipped: - type: number - format: double - description: Total skipped answers - WordStatisticsDto: - type: object - description: Basic word statistics - properties: - word: - type: string - description: The word being tracked - example: "hello" - correct: - type: number - format: double - description: Number of correct answers - example: 10.0 - incorrect: - type: number - format: double - description: Number of incorrect answers - example: 2.0 - skipped: - type: number - format: double - description: Number of skipped answers - example: 0.0 - questionTypes: - type: array - items: + responses: + 200: + description: "Operation completed!" + /admin/users/: + delete: + tags: + - AdminUsersApiV2 + summary: deleteUser + description: "DELETE /api/v2/admin/users/\nDeletes user." + operationId: deleteUser + parameters: + - name: userId + in: path + required: true + schema: type: string - description: Types of questions attempted - example: ["translation", "pronunciation"] - AllTestsStatisticsDto: - type: object - description: Aggregated test statistics - properties: - tests: - type: array - items: - type: object - totalAttempts: - type: integer - description: Total number of test attempts - averageScore: - type: number - format: double - description: Average test score (0.0 to 1.0) - PackProgressDto: - type: object - description: Statistics about user's progress on a specific pack - properties: - packId: - type: string - description: Pack identifier - example: "basic_pack" - totalCards: - type: integer - description: Total number of cards in the pack - example: 100 - learnedCards: - type: integer - description: Number of cards learned by the user - example: 45 - studyTimeMinutes: - type: integer - description: Total study time spent on this pack in minutes - example: 300 - lastStudyDate: - type: string - format: date-time - nullable: true - description: Date when user last studied this pack - example: "2024-01-15T09:00:00Z" - firstStudyDate: - type: string - format: date-time - nullable: true - description: Date when user first started studying this pack - example: "2024-01-01T08:00:00Z" - cardAttempts: - type: object - additionalProperties: - type: integer - description: Map of card ID to number of attempts - example: {} - averageAccuracy: - type: number - format: double - description: Average accuracy across all attempts (0.0 to 1.0) - example: 0.82 - required: - - packId - - totalCards - DetailedWordStatisticsDto: - allOf: - - $ref: '#/components/schemas/WordStatisticsDto' - - type: object - properties: - lastReviewed: - type: string - format: date-time - nullable: true - description: When this word was last reviewed - firstLearned: - type: string - format: date-time - nullable: true - description: When this word was first learned - recentAttempts: - type: array - items: - $ref: '#/components/schemas/WordAttemptDto' - description: Recent attempts (last 10) - difficultyScore: - type: number - format: double - description: Difficulty score (0.0 = easy, 1.0 = hard) - example: 0.57 - needsReview: - type: boolean - description: Whether this word needs review - example: true - packId: - type: string - nullable: true - description: Which pack this word belongs to - example: "basic_pack" - WordAttemptDto: - type: object - description: Individual word attempt data - properties: - timestamp: - type: string - format: date-time - description: When the attempt happened - wasCorrect: - type: boolean - description: Whether the answer was correct - questionType: - type: string - description: Type of question asked - example: "translation" - wasSkipped: - type: boolean - description: Whether the question was skipped - default: false - required: - - timestamp - - wasCorrect - - questionType - WordStatisticsPaginatedResponse: - type: object - description: Paginated response for word statistics - properties: - words: - type: array - items: - $ref: '#/components/schemas/DetailedWordStatisticsDto' - description: List of word statistics - totalCount: - type: integer - description: Total number of words matching the filter - example: 45 - page: - type: integer - description: Current page number (0-based) - example: 0 - pageSize: - type: integer - description: Number of results per page - example: 50 - hasMore: - type: boolean - description: Whether there are more results available - example: false - required: - - words - - totalCount - - page - - pageSize - - hasMore - TimelineStatisticsResponse: - type: object - description: Timeline statistics for study activity - properties: - period: - type: string - description: Time period used - enum: [day, week, month, year] - example: "month" - startDate: - type: string - format: date-time - description: Start date of the period - example: "2024-01-01T00:00:00Z" - endDate: - type: string - format: date-time - description: End date of the period - example: "2024-01-31T23:59:59Z" - totalDays: - type: integer - description: Total days in the period - example: 31 - activeDays: - type: integer - description: Number of days with study activity - example: 20 - totalMinutes: - type: integer - description: Total study time in minutes - example: 1200 - averageDailyMinutes: - type: number - format: double - description: Average study time per active day in minutes - example: 60.0 - currentStreak: - type: integer - description: Current streak within the period - example: 7 - dailyActivity: - type: object - additionalProperties: - type: integer - description: Map of date (ISO string) to study minutes for that day - example: - "2024-01-15T00:00:00Z": 60 - "2024-01-14T00:00:00Z": 45 - studyDates: - type: array - items: + responses: + 200: + description: "Operation completed!" + /games: + get: + tags: + - GamesApiV2 + summary: getGames + description: GET /api/v2/games\nGet all available games\nReturns list of games with metadata + operationId: getGames + responses: + 200: + description: "Operation completed!" + /games//assets: + get: + tags: + - GamesApiV2 + summary: getGameAssets + description: "GET /api/v2/games/{gameId}/assets\nGet game assets\nReturns game assets file (zip) or asset info" + operationId: getGameAssets + parameters: + - name: gameId + in: path + required: true + schema: type: string - format: date-time - description: List of dates when user studied - example: - - "2024-01-15T09:00:00Z" - - "2024-01-14T10:00:00Z" - required: - - period - - startDate - - endDate - - totalDays - - activeDays - - totalMinutes - - averageDailyMinutes - - currentStreak - - dailyActivity - - studyDates - StudySessionDto: - type: object - description: Study session data for tracking learning activity - properties: - sessionId: - type: string - nullable: true - description: Unique session identifier - example: "session_1234567890" - startTime: - type: string - format: date-time - description: When the session started - example: "2024-01-15T09:00:00Z" - endTime: - type: string - format: date-time - nullable: true - description: When the session ended (null if still active) - example: "2024-01-15T09:30:00Z" - wordsLearned: - type: integer - description: Number of words learned during this session - default: 0 - example: 10 - testsCompleted: - type: integer - description: Number of tests completed during this session - default: 0 - example: 2 - accuracy: - type: number - format: double - description: Overall accuracy during this session (0.0 to 1.0) - default: 0.0 - example: 0.85 - packId: - type: string - nullable: true - description: Pack being studied (if focused on specific pack) - example: "basic_pack" - testId: - type: string - nullable: true - description: Test being taken (if part of a test) - required: - - startTime - AchievementDto: - type: object - description: Achievement data structure - properties: - id: - type: string - description: Unique achievement identifier - example: "streak_7" - title: - type: string - description: Achievement title - example: "Week Warrior" - description: - type: string - description: Achievement description - example: "Study for 7 consecutive days" - iconUrl: - type: string - nullable: true - description: URL to achievement icon/badge image - unlockedAt: - type: string - format: date-time - nullable: true - description: When this achievement was unlocked (null if locked) - example: "2024-01-15T09:00:00Z" - type: - type: string - description: Achievement type for categorization - enum: - - firstWordLearned - - firstTestCompleted - - firstPackCompleted - - streak3Days - - streak7Days - - streak30Days - - streak100Days - - words10Learned - - words50Learned - - words100Learned - - words500Learned - - words1000Learned - - perfectTestScore - - speedLearner - - dedicatedLearner - - nightOwl - - earlyBird - - consistentLearner - - languageMaster - example: "streak7Days" - progress: - type: number - format: double - description: Progress towards unlocking (0.0 to 1.0 for locked achievements) - default: 0.0 - example: 1.0 - required: - - id - - title - - description - - type + responses: + 200: + description: "Operation completed!" + /packs: + get: + tags: + - PacksApiV2 + summary: getPacks + description: "GET /api/v2/packs\nGet all pack previews with pagination\nQuery params: ?search=term&language=lang&page=1&limit=20" + operationId: getPacks + responses: + 200: + description: "Operation completed!" + /packs/: + get: + tags: + - PacksApiV2 + summary: getPack + description: "GET /api/v2/packs/{packId}\nGet pack details by ID\nReturns full pack details with purchase status if authenticated" + operationId: getPack + parameters: + - name: packId + in: path + required: true + schema: + type: string + responses: + 200: + description: "Operation completed!" + /packs//buy: + get: + tags: + - PacksApiV2 + summary: getPackBuyPage + description: "GET /api/v2/packs/{packId}/buy\nReturns pack purchase details (includes rewarded ads offer when available)\nWorks for both authenticated and unauthenticated users" + operationId: getPackBuyPage + parameters: + - name: packId + in: path + required: true + schema: + type: string + responses: + 200: + description: "Operation completed!" + /packs//cards: + get: + tags: + - PacksApiV2 + summary: getPackCards + description: "GET /api/v2/packs/{packId}/cards\nGet all cards in a pack\nSupports pagination via query params: ?page=1&limit=20" + operationId: getPackCards + parameters: + - name: packId + in: path + required: true + schema: + type: string + responses: + 200: + description: "Operation completed!" + /packs//cards//image: + get: + tags: + - PacksApiV2 + summary: getCardImage + description: "GET /api/v2/packs/{packId}/cards/{cardId}/image\nGet card image\nReturns PNG image file\n\nImages are accessible for enabled packs even without authentication\nto allow image preview in public pack listings" + operationId: getCardImage + parameters: + - name: packId + in: path + required: true + schema: + type: string + - name: cardId + in: path + required: true + schema: + type: string + responses: + 200: + description: "Operation completed!" + /packs//tests: + get: + tags: + - PacksApiV2 + summary: getPackTests + description: "GET /api/v2/packs/{packId}/tests\nGet tests for a pack" + operationId: getPackTests + parameters: + - name: packId + in: path + required: true + schema: + type: string + responses: + 200: + description: "Operation completed!" + /purchases/packs/: + post: + tags: + - PurchasesApiV2 + summary: createPackPurchase + description: "POST /api/v2/purchases/packs/{packId}\nCreate purchase intent for a pack\nReturns purchase info including payment URL for YooKassa" + operationId: createPackPurchase + parameters: + - name: packId + in: path + required: true + schema: + type: string + responses: + 200: + description: "Operation completed!" + /purchases/packs//status: + get: + tags: + - PurchasesApiV2 + summary: getPackPurchaseStatus + description: "GET /api/v2/purchases/packs/{packId}/status\nCheck if pack is purchased by the authenticated user" + operationId: getPackPurchaseStatus + parameters: + - name: packId + in: path + required: true + schema: + type: string + responses: + 200: + description: "Operation completed!" + /purchases/payments: + post: + tags: + - PurchasesApiV2 + summary: createPayment + description: POST /api/v2/purchases/payments\nCreate a payment\nCurrently supports YooKassa for web payments + operationId: createPayment + responses: + 200: + description: "Operation completed!" + /purchases/payments//verify: + get: + tags: + - PurchasesApiV2 + summary: verifyPayment + description: "GET /api/v2/purchases/payments/{paymentId}/verify\nVerify payment status\nUpdates user purchases on success" + operationId: verifyPayment + parameters: + - name: paymentId + in: path + required: true + schema: + type: string + responses: + 200: + description: "Operation completed!" + /auth/oauth/google: + post: + tags: + - AuthApiV2 + summary: authenticateGoogle + description: POST /api/v2/auth/oauth/google\nAuthenticate with Google ID token + operationId: authenticateGoogle + responses: + 200: + description: "Operation completed!" + /auth/telegram/generate-code: + post: + tags: + - AuthApiV2 + summary: generateTelegramCode + description: "POST /api/v2/auth/telegram/generate-code\nGenerate a new Telegram authentication code\nCalled by the Telegram bot when user requests a code\nBody: { telegramUserId: string, telegramUsername?: string, firstName?: string, lastName?: string }" + operationId: generateTelegramCode + responses: + 200: + description: "Operation completed!" + /auth/telegram/web-code: + post: + tags: + - AuthApiV2 + summary: createWebTelegramCode + description: "POST /api/v2/auth/telegram/web-code\nGenerates a new Telegram authentication code initiated from the web app" + operationId: createWebTelegramCode + responses: + 200: + description: "Operation completed!" + /auth/telegram/claim-code: + post: + tags: + - AuthApiV2 + summary: claimTelegramCode + description: "POST /api/v2/auth/telegram/claim-code\nCalled by Telegram bot when user sends a code generated via the web app\nBody: { code: string, telegramUserId: string, telegramUsername?: string, firstName?: string, lastName?: string }" + operationId: claimTelegramCode + responses: + 200: + description: "Operation completed!" + /auth/telegram/code-status/: + get: + tags: + - AuthApiV2 + summary: getTelegramCodeStatus + description: "GET /api/v2/auth/telegram/code-status/\nReturns current status for a Telegram authentication code" + operationId: getTelegramCodeStatus + parameters: + - name: code + in: path + required: true + schema: + type: string + responses: + 200: + description: "Operation completed!" + /auth/telegram/web-app: + post: + tags: + - AuthApiV2 + summary: authenticateTelegramWebApp + description: "POST /api/v2/auth/oauth/telegram\nAuthenticate with Telegram auth code from bot\nBody: { code: string }" + operationId: authenticateTelegramWebApp + responses: + 200: + description: "Operation completed!" + /auth/oauth/telegram: + post: + tags: + - AuthApiV2 + summary: authenticateTelegram + operationId: authenticateTelegram + responses: + 200: + description: "Operation completed!" + /auth/refresh: + post: + tags: + - AuthApiV2 + summary: refreshToken + description: POST /api/v2/auth/refresh\nRefresh access token using refresh token + operationId: refreshToken + responses: + 200: + description: "Operation completed!" + /auth/me: + get: + tags: + - AuthApiV2 + summary: getCurrentUser + description: GET /api/v2/auth/me\nGet current authenticated user (requires Bearer token) + operationId: getCurrentUser + responses: + 200: + description: "Operation completed!" + /auth/logout: + post: + tags: + - AuthApiV2 + summary: logout + description: "POST /api/v2/auth/logout\nLogout and invalidate tokens\nBody (optional): { refreshToken: string }" + operationId: logout + responses: + 200: + description: "Operation completed!" + /tests/: + get: + tags: + - TestsApiV2 + summary: getTest + description: "GET /api/v2/tests/{testId}\nGet test details by ID" + operationId: getTest + parameters: + - name: testId + in: path + required: true + schema: + type: string + responses: + 200: + description: "Operation completed!" + /tests//results: + post: + tags: + - TestsApiV2 + summary: submitTestResults + description: "POST /api/v2/tests/{testId}/results\nSubmit test results" + operationId: submitTestResults + parameters: + - name: testId + in: path + required: true + schema: + type: string + responses: + 200: + description: "Operation completed!" + /tests//history: + get: + tags: + - TestsApiV2 + summary: getTestHistory + description: "GET /api/v2/tests/{testId}/history\nGet test attempt history for the authenticated user\nSupports pagination via query params: ?page=1&limit=20" + operationId: getTestHistory + parameters: + - name: testId + in: path + required: true + schema: + type: string + responses: + 200: + description: "Operation completed!" + /tasks: + get: + tags: + - TasksApiV2 + summary: "GET /api/v2/tasks - Get tasks with optional filtering" + operationId: getTasks + responses: + 200: + description: "Operation completed!" + /tasks/: + get: + tags: + - TasksApiV2 + summary: "GET /api/v2/tasks/{taskId} - Get specific task" + operationId: getTask + parameters: + - name: taskId + in: path + required: true + schema: + type: string + responses: + 200: + description: "Operation completed!" + /tasks//start: + post: + tags: + - TasksApiV2 + summary: "POST /api/v2/tasks/{taskId}/start - Start a task" + operationId: startTask + parameters: + - name: taskId + in: path + required: true + schema: + type: string + responses: + 200: + description: "Operation completed!" + /tasks//complete: + post: + tags: + - TasksApiV2 + summary: "POST /api/v2/tasks/{taskId}/complete - Complete a task" + operationId: completeTask + parameters: + - name: taskId + in: path + required: true + schema: + type: string + responses: + 200: + description: "Operation completed!" + /users/me/tasks/progress: + get: + tags: + - TasksApiV2 + summary: "GET /api/v2/users/me/tasks/progress - Get user task progress" + operationId: getUserProgress + responses: + 200: + description: "Operation completed!" + /tasks/categories: + get: + tags: + - TasksApiV2 + summary: "GET /api/v2/tasks/categories - Get available task categories and filters" + operationId: getTaskCategories + responses: + 200: + description: "Operation completed!" +components: { } tags: - - name: PurchasesApiV2 - description: Purchases API v2\n\nRESTful endpoints for managing purchases and payments + - name: PromocodesApiV2 + description: API v2 endpoints for promocode management and activation. + - name: DiscountsApiV2 + description: Admin endpoints for discount campaign management. + - name: AdsApiV2 + description: API v2 endpoints for rewarded ads flows. + - name: SubscriptionsApiV2 + description: "Subscriptions API v2\nRESTful endpoints for managing subscriptions & plans" - name: UsersApiV2 description: "API v2 endpoints for user profile and self-service operations." - name: AdminUsersApiV2 description: Admin endpoints for user management in API v2. + - name: GamesApiV2 + description: Games API v2\n\nRESTful endpoints for managing games and game assets + - name: PacksApiV2 + description: API v2 Packs endpoints\n\nRESTful endpoints for card packs with pagination and filtering + - name: PurchasesApiV2 + description: Purchases API v2\n\nRESTful endpoints for managing purchases and payments - name: AuthApiV2 description: API v2 Authentication endpoints\n\nImplements OAuth2/JWT Bearer token authentication - name: TestsApiV2 description: Tests API v2\n\nRESTful endpoints for managing tests and test results - - name: GamesApiV2 - description: Games API v2\n\nRESTful endpoints for managing games and game assets - - name: DiscountsApiV2 - description: Admin endpoints for discount campaign management. - - name: SubscriptionsApiV2 - description: "Subscriptions API v2\nRESTful endpoints for managing subscriptions & plans" - - name: PacksApiV2 - description: API v2 Packs endpoints\n\nRESTful endpoints for card packs with pagination and filtering - name: TasksApiV2 - description: API v2 endpoints for user tasks management - - name: PromocodesApiV2 - description: API v2 endpoints for promocode management and activation. - - name: AdsApiV2 - description: API v2 endpoints for rewarded ads flows. \ No newline at end of file + description: API v2 endpoints for user tasks management \ No newline at end of file diff --git a/mnemo_cards_web_v2/lib/domain/services/auth_service.dart b/mnemo_cards_web_v2/lib/domain/services/auth_service.dart index 933d598..6db6b16 100644 --- a/mnemo_cards_web_v2/lib/domain/services/auth_service.dart +++ b/mnemo_cards_web_v2/lib/domain/services/auth_service.dart @@ -39,13 +39,25 @@ class AuthService { // Get init data from Telegram Web App final webAppData = TelegramWebApp.instance; - final initData = webAppData.initData.toString(); - if (initData.isEmpty) { + String? initData; + + try { + // Try different ways to get init data + if (webAppData.initData != null) { + initData = webAppData.initData.toString(); + } else if (webAppData.initDataUnsafe != null) { + initData = webAppData.initDataUnsafe.toString(); + } + } catch (e) { + log('Error getting init data from Telegram Web App', error: e, name: 'AuthService'); + } + + if (initData == null || initData.isEmpty) { log('No Telegram Web App init data available', name: 'AuthService'); return null; } - log('Telegram Web App init data received, sending to backend', name: 'AuthService'); + log('Telegram Web App init data received: $initData', name: 'AuthService'); // Authenticate with backend using Telegram Web App data final authResponse = await _httpRepository.authenticateWithTelegramWebApp( diff --git a/mnemo_cards_web_v2/lib/domain/services/http_repository_v2.dart b/mnemo_cards_web_v2/lib/domain/services/http_repository_v2.dart index 6136cca..bf4768a 100644 --- a/mnemo_cards_web_v2/lib/domain/services/http_repository_v2.dart +++ b/mnemo_cards_web_v2/lib/domain/services/http_repository_v2.dart @@ -413,12 +413,8 @@ class HttpRepositoryV2 { final refreshToken = data['refreshToken'] as String; final expiresIn = data['expiresIn'] as int?; - // Save tokens - await saveTokens( - accessToken: accessToken, - refreshToken: refreshToken, - expiresIn: expiresIn, - ); + await saveTokens(accessToken: accessToken, refreshToken: refreshToken, expiresIn: expiresIn,); + final expiresAt = expiresIn != null ? DateTime.now().add(Duration(seconds: expiresIn)) diff --git a/mnemo_cards_web_v2/lib/presentation/pages/auth/auth_page.dart b/mnemo_cards_web_v2/lib/presentation/pages/auth/auth_page.dart index 41fb3d2..e677e1c 100644 --- a/mnemo_cards_web_v2/lib/presentation/pages/auth/auth_page.dart +++ b/mnemo_cards_web_v2/lib/presentation/pages/auth/auth_page.dart @@ -64,8 +64,7 @@ class _AuthPageState extends State { // Notify router about auth change appScope.userScopeHolder.notifyAuthChanged(); - // Navigate to home - router.go('/home'); + // Router will automatically redirect to /home when authentication state changes } else { setState(() { _errorMessage = 'Login cancelled'; @@ -86,9 +85,9 @@ class _AuthPageState extends State { } Future _onTelegramLoginSuccess(BuildContext context) async { - // Get router before async operations to avoid context issues - final router = GoRouter.of(context); - router.go('/home'); + // Router will automatically redirect to /home when authentication state changes + // No manual navigation needed - let the GoRouter handle the redirect + log('Telegram login successful - router will handle redirect to home', name: 'AuthPage'); } diff --git a/mnemo_cards_web_v2/lib/presentation/pages/auth/sign_in_with_telegram.dart b/mnemo_cards_web_v2/lib/presentation/pages/auth/sign_in_with_telegram.dart index 6dfb70e..ae0012a 100644 --- a/mnemo_cards_web_v2/lib/presentation/pages/auth/sign_in_with_telegram.dart +++ b/mnemo_cards_web_v2/lib/presentation/pages/auth/sign_in_with_telegram.dart @@ -81,6 +81,7 @@ class _SignInWithTelegramState extends State { isClaimed: true, expiresAt: DateTime.now(), ); + _autoLoginAttempted = true; // Mark as attempted to prevent re-attempts }); // Clear code field @@ -190,9 +191,16 @@ class _SignInWithTelegramState extends State { widget.onError('Срок действия кода истёк. Сгенерируйте новый.'); } else if (status.isConsumed) { _stopCodePolling(); - } else if (status.isReadyForLogin && !_autoLoginAttempted && mounted) { + } else if (status.isReadyForLogin && !_autoLoginAttempted && mounted) { + log('Auto-login triggered for code: ${status.code}', name: 'SignInWithTelegram'); _autoLoginAttempted = true; - await _loginWithTelegram(); + + // Small delay to ensure UI updates are processed before navigation + await Future.delayed(const Duration(milliseconds: 100)); + + if (mounted) { + await _loginWithTelegram(); + } } } catch (e, s) { log( diff --git a/mnemo_cards_web_v2/lib/presentation/pages/pack_details/pack_details_page.dart b/mnemo_cards_web_v2/lib/presentation/pages/pack_details/pack_details_page.dart index 3e5aa9a..068dd33 100644 --- a/mnemo_cards_web_v2/lib/presentation/pages/pack_details/pack_details_page.dart +++ b/mnemo_cards_web_v2/lib/presentation/pages/pack_details/pack_details_page.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'dart:math' as math; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; @@ -438,7 +439,8 @@ class _PackDetailsPageState extends State { return Column( children: [ // Модуль "проверка знаний" для мобильных - _buildMobileTestsSection(packColor), + // Hide tests section for mobile devices for now + // _buildMobileTestsSection(packColor), const SizedBox(height: 16), @@ -855,29 +857,19 @@ class _PackDetailsPageState extends State { final imageUrl = ApiConfigV2.getCardImageUrl(widget.packId, card.id); - return Image.network( - imageUrl, + return CachedNetworkImage( + imageUrl: imageUrl, fit: BoxFit.cover, - gaplessPlayback: true, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) { - return child; - } - return Center( - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, - ), + maxWidthDiskCache: 2048, + maxHeightDiskCache: 2048, + progressIndicatorBuilder: (context, child, loadingProgress) { + return const Center( + child: CircularProgressIndicator( + strokeWidth: 2, ), ); }, - errorBuilder: (context, error, stackTrace) { + errorWidget: (context, url, error) { return Center( child: Icon( Icons.broken_image, @@ -886,6 +878,9 @@ class _PackDetailsPageState extends State { ), ); }, + // Плавное появление загруженного изображения + fadeInDuration: const Duration(milliseconds: 100), + fadeOutDuration: const Duration(milliseconds: 100), ); } diff --git a/mnemo_cards_web_v2/pubspec.lock b/mnemo_cards_web_v2/pubspec.lock index 4c4889c..aad7bfa 100644 --- a/mnemo_cards_web_v2/pubspec.lock +++ b/mnemo_cards_web_v2/pubspec.lock @@ -144,6 +144,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.12.0" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" characters: dependency: transitive description: @@ -501,6 +525,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" flutter_colorpicker: dependency: transitive description: @@ -892,6 +924,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" package_config: dependency: transitive description: @@ -1176,6 +1216,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: @@ -1216,6 +1296,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" telegram_web_app: dependency: "direct main" description: diff --git a/mnemo_cards_web_v2/pubspec.yaml b/mnemo_cards_web_v2/pubspec.yaml index 1415e87..d0b9e48 100644 --- a/mnemo_cards_web_v2/pubspec.yaml +++ b/mnemo_cards_web_v2/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: shimmer: ^3.0.0 auto_size_text: ^3.0.0 fl_chart: ^0.68.0 + cached_network_image: ^3.4.1 # Utils universal_image: ^1.0.10