diff --git a/mnemo_cards_backend/Dockerfile b/mnemo_cards_backend/Dockerfile index 59d33b7..5d1a72e 100644 --- a/mnemo_cards_backend/Dockerfile +++ b/mnemo_cards_backend/Dockerfile @@ -57,8 +57,9 @@ ENV PORT=3000 \ EXPOSE 3000 # Healthcheck - проверка доступности сервера -HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=5 \ - CMD sh -c 'curl -sf http://127.0.0.1:${PORT:-3000}/health || exit 1' +# Increased start-period to 30s to allow server initialization (Isar, cron jobs, etc.) +HEALTHCHECK --interval=10s --timeout=5s --start-period=20s --retries=3 \ + CMD curl -f http://127.0.0.1:${PORT:-3000}/health || exit 1 # Start server - все настройки читаются из environment variables CMD ["/app/server"] diff --git a/mnemo_cards_backend/lib/api/mnemo_shelf.dart b/mnemo_cards_backend/lib/api/mnemo_shelf.dart index 09dcc9d..1224aa4 100644 --- a/mnemo_cards_backend/lib/api/mnemo_shelf.dart +++ b/mnemo_cards_backend/lib/api/mnemo_shelf.dart @@ -45,7 +45,8 @@ class MnemoShelf { final port = int.tryParse(portArg ?? '') ?? 3000; final certs = Platform.environment['CERTS_PATH']; // null = HTTP without SSL final dualMode = Platform.environment['DUAL_MODE'] == 'true' || - Platform.environment['DUAL_MODE'] == '1'; // Запуск HTTP и HTTPS одновременно + Platform.environment['DUAL_MODE'] == + '1'; // Запуск HTTP и HTTPS одновременно // V2 APIs (new RESTful API with OAuth2/JWT) final v2Routers = [ @@ -250,8 +251,8 @@ class MnemoShelf { // } return SecurityContext(withTrustedRoots: false); - // ..useCertificateChain(serverCert) - // ..usePrivateKey(key); + // ..useCertificateChain(serverCert) + // ..usePrivateKey(key); } logger(String tag) => (String msg, bool isError) { diff --git a/mnemo_cards_backend/lib/api/v2/admin_analytics_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_analytics_api_v2.dart index a05163b..77b7f65 100644 --- a/mnemo_cards_backend/lib/api/v2/admin_analytics_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/admin_analytics_api_v2.dart @@ -67,24 +67,32 @@ class AdminAnalyticsApiV2 { final paymentCount = await backend_main.isar.paymentModels.count(); // Get recent users (last 10) - final allUsers = await backend_main.isar.txn(() async => backend_main.isar.userModels.where().findAll()); + final allUsers = await backend_main.isar + .txn(() async => backend_main.isar.userModels.where().findAll()); final sortedUsers = allUsers ..sort((a, b) => (b.id ?? 0).compareTo(a.id ?? 0)); - final recentUsers = sortedUsers.take(10).map((u) => { - 'id': u.id, - 'name': u.name, - 'email': u.email, - 'createdAt': DateTime(1999).toIso8601String(), - }).toList(); + final recentUsers = sortedUsers + .take(10) + .map((u) => { + 'id': u.id, + 'name': u.name, + 'email': u.email, + 'createdAt': DateTime(1999).toIso8601String(), + }) + .toList(); // Get top packs by user count (mock data for now) - final allPacks = await backend_main.isar.txn(() async => backend_main.isar.cardPackModels.where().findAll()); - final topPacks = allPacks.take(5).map((p) => { - 'id': p.id, - 'title': p.title, - 'cards': p.cards.length, - 'enabled': p.enabled, - }).toList(); + final allPacks = await backend_main.isar + .txn(() async => backend_main.isar.cardPackModels.where().findAll()); + final topPacks = allPacks + .take(5) + .map((p) => { + 'id': p.id, + 'title': p.title, + 'cards': p.cards.length, + 'enabled': p.enabled, + }) + .toList(); return _json({ 'stats': { @@ -126,7 +134,8 @@ class AdminAnalyticsApiV2 { for (int i = 29; i >= 0; i--) { final date = now.subtract(Duration(days: i)); - final registrations = (i % 7) + 1; // Mock data: 1-7 registrations per day + final registrations = + (i % 7) + 1; // Mock data: 1-7 registrations per day chartData.add({ 'date': date.toIso8601String().split('T')[0], diff --git a/mnemo_cards_backend/lib/api/v2/admin_auth_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_auth_api_v2.dart index c56f772..8915107 100644 --- a/mnemo_cards_backend/lib/api/v2/admin_auth_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/admin_auth_api_v2.dart @@ -146,7 +146,8 @@ Use this code to access the admin panel at admin.mnemo-cards.online // Get user from database // Note: UserModel doesn't have telegramUserId field, so we need to find user differently // For now, we'll look for admin users (assuming admin field exists) - final allUsers = await backend_main.isar.txn(() async => backend_main.isar.userModels.where().findAll()); + final allUsers = await backend_main.isar + .txn(() async => backend_main.isar.userModels.where().findAll()); final user = allUsers.where((u) => u.admin).firstOrNull; if (user == null) { diff --git a/mnemo_cards_backend/lib/api/v2/admin_cards_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_cards_api_v2.dart index ee99b68..b35009c 100644 --- a/mnemo_cards_backend/lib/api/v2/admin_cards_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/admin_cards_api_v2.dart @@ -94,7 +94,8 @@ class AdminCardsApiV2 { } // Get all cards from database - final allCards = await backend_main.isar.txn(() async => backend_main.isar.gameCardModels.where().findAll()); + final allCards = await backend_main.isar + .txn(() async => backend_main.isar.gameCardModels.where().findAll()); // Apply search filter if provided List filteredCards = allCards; @@ -102,10 +103,12 @@ class AdminCardsApiV2 { final searchLower = search.toLowerCase(); filteredCards = allCards.where((card) { return card.original.toLowerCase().contains(searchLower) || - card.translation.toLowerCase().contains(searchLower) || - card.mnemo.toLowerCase().contains(searchLower) || - (card.transcription?.toLowerCase().contains(searchLower) ?? false) || - (card.transcriptionMnemo?.toLowerCase().contains(searchLower) ?? false); + card.translation.toLowerCase().contains(searchLower) || + card.mnemo.toLowerCase().contains(searchLower) || + (card.transcription?.toLowerCase().contains(searchLower) ?? + false) || + (card.transcriptionMnemo?.toLowerCase().contains(searchLower) ?? + false); }).toList(); } @@ -184,7 +187,8 @@ class AdminCardsApiV2 { late final GameCardDto cardDto; try { - cardDto = GameCardDto.fromJson(jsonDecode(body) as Map); + cardDto = + GameCardDto.fromJson(jsonDecode(body) as Map); } catch (_) { return _json( { diff --git a/mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.dart index 17bc5f8..3483f48 100644 --- a/mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.dart @@ -98,14 +98,16 @@ class AdminPacksApiV2 { } // Get all packs (admin can see disabled packs) - final allPacks = await backend_main.isar.txn(() async => backend_main.isar.cardPackModels.where().findAll()); - List filteredPacks = showDisabled ? allPacks : allPacks.where((p) => p.enabled).toList(); + final allPacks = await backend_main.isar + .txn(() async => backend_main.isar.cardPackModels.where().findAll()); + List filteredPacks = + showDisabled ? allPacks : allPacks.where((p) => p.enabled).toList(); if (search != null && search.isNotEmpty) { final searchLower = search.toLowerCase(); filteredPacks = allPacks.where((pack) { return pack.title.toLowerCase().contains(searchLower) || - (pack.subtitle?.toLowerCase().contains(searchLower) ?? false) || - pack.id.toString().contains(searchLower); + (pack.subtitle?.toLowerCase().contains(searchLower) ?? false) || + pack.id.toString().contains(searchLower); }).toList(); } @@ -119,11 +121,8 @@ class AdminPacksApiV2 { final paginatedPacks = filteredPacks.skip(offset).take(limit).toList(); // Convert to preview DTOs - final packDtos = await Future.wait( - paginatedPacks.map((pack) async => - await _packDtoConverter.toCardPackPreviewDto(pack, null) - ) - ); + final packDtos = await Future.wait(paginatedPacks.map((pack) async => + await _packDtoConverter.toCardPackPreviewDto(pack, null))); return _json({ 'items': packDtos.map((p) => p.toJson()).toList(), @@ -194,7 +193,8 @@ class AdminPacksApiV2 { late final EditCardPackDto packDto; try { - packDto = EditCardPackDto.fromJson(jsonDecode(body) as Map); + packDto = + EditCardPackDto.fromJson(jsonDecode(body) as Map); } catch (_) { return _json( { diff --git a/mnemo_cards_backend/lib/api/v2/authorize_v2.dart b/mnemo_cards_backend/lib/api/v2/authorize_v2.dart index ae11764..b596a87 100644 --- a/mnemo_cards_backend/lib/api/v2/authorize_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/authorize_v2.dart @@ -53,15 +53,16 @@ Middleware authorizeV2(UserManager userManager, JwtService jwtService) { } // Check if this is a public auth endpoint - always allow for all HTTP methods - if (normalizedPath.startsWith('/auth/') || normalizedPath.startsWith('/admin/auth/')) { + if (normalizedPath.startsWith('/auth/') || + normalizedPath.startsWith('/admin/auth/')) { if (_publicAuthPaths.contains(normalizedPath)) { // Auth endpoints in public list are accessible without token return await innerHandler(request); } // Check for dynamic auth paths (e.g., /auth/telegram/code-status/) - if (normalizedPath.startsWith('/auth/telegram/code-status') - || normalizedPath.startsWith('/admin/auth/request-code') - || normalizedPath.startsWith('/admin/auth/verify-code')) { + if (normalizedPath.startsWith('/auth/telegram/code-status') || + normalizedPath.startsWith('/admin/auth/request-code') || + normalizedPath.startsWith('/admin/auth/verify-code')) { // Code status endpoint is public (used before authentication) return await innerHandler(request); } diff --git a/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart b/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart index c9c58cc..e0e5efc 100644 --- a/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart @@ -287,7 +287,7 @@ class PacksApiV2 { // getBuyPage may throw if pack doesn't exist (via _fetchPackModel) // _fetchPackModel uses ! operator which throws on null final errorString = e.toString(); - if (errorString.contains('Null check') || + if (errorString.contains('Null check') || errorString.contains('null') || e is StateError) { return _notFound('Pack not found'); @@ -491,9 +491,7 @@ class PacksApiV2 { final items = voices .where((voice) => voice.id != null) .map( - (voice) => voice - .toDto(url: '/api/v2/voice/${voice.id}') - .toJson(), + (voice) => voice.toDto(url: '/api/v2/voice/${voice.id}').toJson(), ) .toList(); @@ -563,7 +561,8 @@ class PacksApiV2 { } String? _sanitizeVoicePath(String path) { - final normalized = path.replaceAll('\\', '/').replaceFirst(RegExp('^/'), ''); + final normalized = + path.replaceAll('\\', '/').replaceFirst(RegExp('^/'), ''); if (normalized.contains('..')) { return null; } diff --git a/mnemo_cards_backend/lib/api/v2/promocodes_api_v2.dart b/mnemo_cards_backend/lib/api/v2/promocodes_api_v2.dart index 1b319ec..ec7bc9f 100644 --- a/mnemo_cards_backend/lib/api/v2/promocodes_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/promocodes_api_v2.dart @@ -61,8 +61,7 @@ class PromocodesApiV2 { return _unauthorized(); } - final campaigns = - await _promoCodesManager.listAvailablePromocodes(user); + final campaigns = await _promoCodesManager.listAvailablePromocodes(user); return _json({ 'campaigns': campaigns.map((c) => c.toJson()).toList(), }); @@ -92,7 +91,7 @@ class PromocodesApiV2 { } final result = await _promoCodesManager.validatePromocode(code, user); - + // Return 404 if code not found if (!result['valid'] && result['message'] == 'Промокод не найден') { return _json( diff --git a/mnemo_cards_backend/lib/api/v2/subscriptions_api_v2.dart b/mnemo_cards_backend/lib/api/v2/subscriptions_api_v2.dart index c7db86e..f748068 100644 --- a/mnemo_cards_backend/lib/api/v2/subscriptions_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/subscriptions_api_v2.dart @@ -91,12 +91,15 @@ class SubscriptionsApiV2 { return _unauthorized(); } - final subscriptionDto = await _subscriptionManager.getSubscriptionDto(user); - + final subscriptionDto = + await _subscriptionManager.getSubscriptionDto(user); + return _ok({ 'active': subscriptionDto.isActive, - if (subscriptionDto.start != null) 'start': subscriptionDto.start!.toIso8601String(), - if (subscriptionDto.finish != null) 'finish': subscriptionDto.finish!.toIso8601String(), + if (subscriptionDto.start != null) + 'start': subscriptionDto.start!.toIso8601String(), + if (subscriptionDto.finish != null) + 'finish': subscriptionDto.finish!.toIso8601String(), }); } catch (e, s) { developer.log( diff --git a/mnemo_cards_backend/lib/api/v2/telegram_bot_api_v2.dart b/mnemo_cards_backend/lib/api/v2/telegram_bot_api_v2.dart index ddae8c7..e1ad551 100644 --- a/mnemo_cards_backend/lib/api/v2/telegram_bot_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/telegram_bot_api_v2.dart @@ -216,11 +216,13 @@ class TelegramBotApiV2 { if (users.length > 1) { return _ok({ 'multiple': true, - 'users': users.map((u) => { - 'id': u.id, - 'email': u.email, - 'name': u.name, - }).toList(), + 'users': users + .map((u) => { + 'id': u.id, + 'email': u.email, + 'name': u.name, + }) + .toList(), }); } @@ -235,10 +237,12 @@ class TelegramBotApiV2 { 'email': user.email, 'name': user.name, 'tags': user.userData.value?.tags.join(',') ?? '', - 'packs': user.packs.map((p) => { - 'id': p.id, - 'title': p.title, - }).toList(), + 'packs': user.packs + .map((p) => { + 'id': p.id, + 'title': p.title, + }) + .toList(), 'purchases': user.purchases.length, 'subscription': sub == null ? null @@ -260,12 +264,14 @@ class TelegramBotApiV2 { return _ok({ 'total': users.length, - 'users': users.map((user) => { - 'id': user.id, - 'email': user.email ?? '${user.id} ${user.name}', - 'lastTimeOnline': user.userData.value?.lastTimeOnline - ?.toIso8601String(), - }).toList(), + 'users': users + .map((user) => { + 'id': user.id, + 'email': user.email ?? '${user.id} ${user.name}', + 'lastTimeOnline': + user.userData.value?.lastTimeOnline?.toIso8601String(), + }) + .toList(), }); } } catch (e, s) { diff --git a/mnemo_cards_backend/lib/api/v2/telegram_bot_auth_middleware.dart b/mnemo_cards_backend/lib/api/v2/telegram_bot_auth_middleware.dart index 9002bea..eb17395 100644 --- a/mnemo_cards_backend/lib/api/v2/telegram_bot_auth_middleware.dart +++ b/mnemo_cards_backend/lib/api/v2/telegram_bot_auth_middleware.dart @@ -29,7 +29,8 @@ Middleware telegramBotAuthMiddleware() { return Response( 500, headers: {'Content-Type': 'application/json'}, - body: '{"error":"Internal Server Error","message":"API key not configured"}', + body: + '{"error":"Internal Server Error","message":"API key not configured"}', ); } diff --git a/mnemo_cards_backend/lib/api/v2/users_api_v2.dart b/mnemo_cards_backend/lib/api/v2/users_api_v2.dart index 8116098..2503b93 100644 --- a/mnemo_cards_backend/lib/api/v2/users_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/users_api_v2.dart @@ -475,7 +475,8 @@ class UsersApiV2 { studyDates.add(now); // Update total study time - final totalStudyTime = userData.totalStudyTimeMinutes + sessionDto.duration.inMinutes; + final totalStudyTime = + userData.totalStudyTimeMinutes + sessionDto.duration.inMinutes; // Recalculate streak final currentStreak = _statisticsCalculator.calculateStreak(studyDates); diff --git a/mnemo_cards_backend/lib/packs/pack_dto_converter.dart b/mnemo_cards_backend/lib/packs/pack_dto_converter.dart index 91b0217..f86bdcf 100644 --- a/mnemo_cards_backend/lib/packs/pack_dto_converter.dart +++ b/mnemo_cards_backend/lib/packs/pack_dto_converter.dart @@ -58,8 +58,10 @@ class PackDtoConverter { 'icons/ad.webp', position: PackTipPosition.bottomRight, ) - : AssetPackTip('icons/lock.webp', - position: PackTipPosition.bottomRight,), + : AssetPackTip( + 'icons/lock.webp', + position: PackTipPosition.bottomRight, + ), version: model.version, ); } diff --git a/mnemo_cards_backend/lib/packs/pack_manager.dart b/mnemo_cards_backend/lib/packs/pack_manager.dart index 48ec72d..f8eb850 100644 --- a/mnemo_cards_backend/lib/packs/pack_manager.dart +++ b/mnemo_cards_backend/lib/packs/pack_manager.dart @@ -384,11 +384,16 @@ class PackManager { String appVersion, ) async { final pack = await _fetchPackModel(packId); - final archiveFile = File( - '${assetsDirectory.path}/pack_archives/${appVersion}/${packId}_${pack.version}.zip', - ); - if (await archiveFile.exists()) { - return archiveFile.readAsBytes(); + try { + final archiveFile = File( + '${assetsDirectory.path}/pack_archives/${appVersion}/${packId}_${pack.version}.zip', + ); + if (await archiveFile.exists()) { + return await archiveFile.readAsBytes(); + } + } catch (e, s) { + log('Error when fetching pack images archive', error: e, stackTrace: s); + return []; } final cards = pack.cards; final rawData = _fetchPackImages(cards.toList()); diff --git a/mnemo_cards_backend/lib/promo_codes/promo_codes_manager.dart b/mnemo_cards_backend/lib/promo_codes/promo_codes_manager.dart index 3437e10..a7a31f9 100644 --- a/mnemo_cards_backend/lib/promo_codes/promo_codes_manager.dart +++ b/mnemo_cards_backend/lib/promo_codes/promo_codes_manager.dart @@ -226,8 +226,10 @@ class PromoCodesManager { final upperCode = code.toUpperCase(); // Check if code exists - final codeModel = - await isar.promoCodeModels.filter().codeEqualTo(upperCode).findFirst(); + final codeModel = await isar.promoCodeModels + .filter() + .codeEqualTo(upperCode) + .findFirst(); await codeModel?.campaign.load(); final campaign = codeModel?.campaign.value; if (codeModel == null || campaign == null) { diff --git a/mnemo_cards_backend/lib/tests/generators/question_generators/input_buttons_question_generator.dart b/mnemo_cards_backend/lib/tests/generators/question_generators/input_buttons_question_generator.dart index be6da48..b9da03b 100644 --- a/mnemo_cards_backend/lib/tests/generators/question_generators/input_buttons_question_generator.dart +++ b/mnemo_cards_backend/lib/tests/generators/question_generators/input_buttons_question_generator.dart @@ -53,9 +53,8 @@ class InputButtonsQuestionGenerator implements QuestionGenerator { final questionType = type ?? _getRandomQuestionType(); final questionText = questionType.translationQuestion ? answerItem.translation : null; - final questionImage = questionType.imageQuestion - ? _imageIdToUrl(answerItem.image) - : null; + final questionImage = + questionType.imageQuestion ? _imageIdToUrl(answerItem.image) : null; String? questionAudio = null; if (questionType.audioQuestion) { @@ -116,7 +115,6 @@ class InputButtonsQuestionGenerator implements QuestionGenerator { ); } - Future verbTestQuestion( TestDataItem answerCard) async { final ends = ['o', 'as', 'a', 'a', 'a', 'amos', 'ais', 'an', 'an', 'an']; diff --git a/mnemo_cards_backend/lib/tests/generators/question_generators/simple_question_generator.dart b/mnemo_cards_backend/lib/tests/generators/question_generators/simple_question_generator.dart index 91fbf31..aeae051 100644 --- a/mnemo_cards_backend/lib/tests/generators/question_generators/simple_question_generator.dart +++ b/mnemo_cards_backend/lib/tests/generators/question_generators/simple_question_generator.dart @@ -52,9 +52,8 @@ class SimpleQuestionGenerator implements QuestionGenerator { : questionType.translationQuestion ? answerCard.translation : null; - final questionImage = questionType.imageQuestion - ? _imageIdToUrl(answerCard.image) - : null; + final questionImage = + questionType.imageQuestion ? _imageIdToUrl(answerCard.image) : null; String? questionAudio = null; if (questionType.audioQuestion) { if (questionType.translationAnswers || questionType.imagesAnswers) { @@ -93,7 +92,6 @@ class SimpleQuestionGenerator implements QuestionGenerator { ); } - Future verbTestQuestion( TestDataItem answerCard, { bool withAudio = true, diff --git a/mnemo_cards_backend/lib/user/admin_ids_service.dart b/mnemo_cards_backend/lib/user/admin_ids_service.dart index 12748f4..cf9e6bf 100644 --- a/mnemo_cards_backend/lib/user/admin_ids_service.dart +++ b/mnemo_cards_backend/lib/user/admin_ids_service.dart @@ -18,4 +18,4 @@ class AdminIdsService { } return []; } -} \ No newline at end of file +} diff --git a/mnemo_cards_backend/lib/user/telegram.dart b/mnemo_cards_backend/lib/user/telegram.dart index d508fb1..1bdbc8a 100644 --- a/mnemo_cards_backend/lib/user/telegram.dart +++ b/mnemo_cards_backend/lib/user/telegram.dart @@ -76,9 +76,11 @@ class TelegramUtils { } /// Send message to admin via Telegram Bot API - static Future sendMessageToAdmin(String telegramUserId, String message) async { + static Future sendMessageToAdmin( + String telegramUserId, String message) async { try { - final url = Uri.parse('https://api.telegram.org/bot$_botToken/sendMessage'); + final url = + Uri.parse('https://api.telegram.org/bot$_botToken/sendMessage'); final response = await http.post( url,