2025-11-16 11:25:27 +00:00
|
|
|
|
import 'dart:io';
|
|
|
|
|
|
import 'package:injectable/injectable.dart';
|
|
|
|
|
|
import 'package:mnemo_cards_backend/api/di/injector.dart';
|
|
|
|
|
|
import 'package:mnemo_cards_backend/user/user_manager.dart';
|
|
|
|
|
|
import 'package:shelf/shelf.dart';
|
|
|
|
|
|
import 'package:shelf/shelf_io.dart' as shelf_io;
|
|
|
|
|
|
import 'package:shelf_router/shelf_router.dart';
|
|
|
|
|
|
|
2025-12-02 23:17:15 +00:00
|
|
|
|
import 'v2/admin_analytics_api_v2.dart';
|
|
|
|
|
|
import 'v2/admin_auth_api_v2.dart';
|
2025-12-14 01:01:44 +00:00
|
|
|
|
import 'v2/admin_cards_api_v2.dart';
|
|
|
|
|
|
import 'v2/admin_packs_api_v2.dart';
|
2025-12-14 22:27:14 +00:00
|
|
|
|
import 'v2/admin_tests_api_v2.dart';
|
|
|
|
|
|
import 'v2/admin_users_api_v2.dart';
|
2025-11-16 11:25:27 +00:00
|
|
|
|
import 'v2/auth_api_v2.dart';
|
|
|
|
|
|
import 'v2/discounts_api_v2.dart';
|
2025-12-14 18:23:30 +00:00
|
|
|
|
import 'v2/packs_api_v2.dart';
|
2025-11-16 11:25:27 +00:00
|
|
|
|
import 'v2/promocodes_api_v2.dart';
|
|
|
|
|
|
import 'v2/subscriptions_api_v2.dart';
|
|
|
|
|
|
import 'v2/tasks_api_v2.dart';
|
|
|
|
|
|
import 'v2/tests_api_v2.dart';
|
2025-12-14 18:28:26 +00:00
|
|
|
|
import 'v2/telegram_bot_api_v2.dart';
|
|
|
|
|
|
import 'v2/users_api_v2.dart';
|
2025-11-16 11:25:27 +00:00
|
|
|
|
import 'v2/authorize_v2.dart';
|
2025-12-11 17:49:15 +00:00
|
|
|
|
import 'v2/telegram_bot_auth_middleware.dart';
|
2025-11-16 11:25:27 +00:00
|
|
|
|
import 'v2/jwt_service.dart';
|
|
|
|
|
|
import 'authorize/access_middleware.dart';
|
|
|
|
|
|
import 'package:mnemo_cards_backend/packs/pack_manager.dart';
|
|
|
|
|
|
import 'package:mnemo_cards_backend/statistics/session_tracking_middleware.dart';
|
|
|
|
|
|
import 'package:mnemo_cards_backend/statistics/session_tracker.dart';
|
|
|
|
|
|
|
|
|
|
|
|
@lazySingleton
|
|
|
|
|
|
class MnemoShelf {
|
|
|
|
|
|
final UserManager userManager;
|
|
|
|
|
|
|
|
|
|
|
|
MnemoShelf(this.userManager);
|
|
|
|
|
|
|
2025-12-11 17:49:15 +00:00
|
|
|
|
Future<void> initV2() async {
|
|
|
|
|
|
// Чтение переменных окружения вместо CLI аргументов
|
|
|
|
|
|
final addressArg = Platform.environment['SERVER_ADDRESS'];
|
|
|
|
|
|
final portArg = Platform.environment['PORT'];
|
2025-12-11 19:24:22 +00:00
|
|
|
|
// Используем InternetAddress напрямую для правильной работы с bind
|
|
|
|
|
|
final address = addressArg != null && addressArg != '0.0.0.0'
|
|
|
|
|
|
? InternetAddress(addressArg)
|
|
|
|
|
|
: InternetAddress.anyIPv4;
|
2025-12-11 17:49:15 +00:00
|
|
|
|
final port = int.tryParse(portArg ?? '') ?? 3000;
|
2025-11-16 11:25:27 +00:00
|
|
|
|
|
|
|
|
|
|
// V2 APIs (new RESTful API with OAuth2/JWT)
|
2025-12-13 13:27:05 +00:00
|
|
|
|
final v2Router = Router();
|
2025-11-16 11:25:27 +00:00
|
|
|
|
|
2025-12-13 13:27:05 +00:00
|
|
|
|
// Add routers individually to avoid type issues
|
|
|
|
|
|
v2Router.mount('/', getIt.get<AuthApiV2>().router);
|
|
|
|
|
|
v2Router.mount('/', getIt.get<AdminAuthApiV2>().router);
|
|
|
|
|
|
v2Router.mount('/', getIt.get<AdminAnalyticsApiV2>().router);
|
2025-12-14 01:01:44 +00:00
|
|
|
|
v2Router.mount('/', getIt.get<AdminCardsApiV2>().router);
|
|
|
|
|
|
v2Router.mount('/', getIt.get<AdminPacksApiV2>().router);
|
2025-12-14 22:27:14 +00:00
|
|
|
|
v2Router.mount('/', getIt.get<AdminTestsApiV2>().router);
|
|
|
|
|
|
v2Router.mount('/', getIt.get<AdminUsersApiV2>().router);
|
2025-12-14 18:23:30 +00:00
|
|
|
|
v2Router.mount('/', getIt.get<PacksApiV2>().router);
|
2025-12-13 13:27:05 +00:00
|
|
|
|
v2Router.mount('/', getIt.get<TestsApiV2>().router);
|
|
|
|
|
|
v2Router.mount('/', getIt.get<PromocodesApiV2>().router);
|
|
|
|
|
|
v2Router.mount('/', getIt.get<SubscriptionsApiV2>().router);
|
|
|
|
|
|
v2Router.mount('/', getIt.get<DiscountsApiV2>().router);
|
|
|
|
|
|
v2Router.mount('/', getIt.get<TasksApiV2>().handler);
|
2025-12-14 18:28:26 +00:00
|
|
|
|
v2Router.mount('/', getIt.get<UsersApiV2>().router);
|
|
|
|
|
|
v2Router.mount('/', getIt.get<TelegramBotApiV2>().router);
|
2025-11-16 11:25:27 +00:00
|
|
|
|
|
|
|
|
|
|
// Configure a pipeline with proper CORS configuration
|
|
|
|
|
|
final corsConfig = {
|
|
|
|
|
|
'Access-Control-Allow-Origin': '*', // Разрешаем все домены для гибкости
|
|
|
|
|
|
'Access-Control-Allow-Methods':
|
|
|
|
|
|
'GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS',
|
|
|
|
|
|
'Access-Control-Allow-Headers':
|
2025-12-11 17:49:15 +00:00
|
|
|
|
'Origin, Content-Type, Accept, Authorization, user_token, request_token, app_version, X-API-Key',
|
2025-11-16 11:25:27 +00:00
|
|
|
|
'Access-Control-Expose-Headers': 'Authorization',
|
|
|
|
|
|
'Access-Control-Max-Age': '86400',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// CORS middleware that handles preflight requests
|
|
|
|
|
|
Middleware corsMiddleware() {
|
|
|
|
|
|
return (Handler innerHandler) {
|
|
|
|
|
|
return (Request request) async {
|
|
|
|
|
|
// Handle preflight requests
|
|
|
|
|
|
if (request.method == 'OPTIONS') {
|
|
|
|
|
|
return Response.ok('', headers: corsConfig);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Process the request and add CORS headers to response
|
|
|
|
|
|
final response = await innerHandler(request);
|
2025-12-14 22:10:05 +00:00
|
|
|
|
|
|
|
|
|
|
// Explicitly merge headers to ensure CORS headers are always added
|
|
|
|
|
|
// This guarantees CORS headers are present even for error responses (401, 403, etc.)
|
|
|
|
|
|
final finalHeaders = <String, String>{};
|
|
|
|
|
|
// First, add existing response headers
|
|
|
|
|
|
finalHeaders.addAll(response.headers);
|
|
|
|
|
|
// Then, add/override with CORS headers (they take priority)
|
|
|
|
|
|
corsConfig.forEach((key, value) {
|
|
|
|
|
|
finalHeaders[key] = value;
|
2025-11-16 11:25:27 +00:00
|
|
|
|
});
|
2025-12-14 22:10:05 +00:00
|
|
|
|
|
|
|
|
|
|
return response.change(headers: finalHeaders);
|
2025-11-16 11:25:27 +00:00
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
final v2Handler = Pipeline()
|
|
|
|
|
|
.addMiddleware(corsMiddleware())
|
|
|
|
|
|
.addMiddleware(logRequests(logger: logger('v2')))
|
2025-12-11 17:49:15 +00:00
|
|
|
|
.addMiddleware(telegramBotAuthMiddleware())
|
2025-11-16 11:25:27 +00:00
|
|
|
|
.addMiddleware(
|
|
|
|
|
|
authorizeV2(getIt.get<UserManager>(), getIt.get<JwtService>()))
|
|
|
|
|
|
.addMiddleware(accessMiddleware(
|
|
|
|
|
|
buildAccessService(getIt.get<PackManager>()),
|
|
|
|
|
|
getIt.get<PackManager>()))
|
|
|
|
|
|
.addMiddleware(sessionTrackingMiddleware(getIt.get<SessionTracker>()))
|
|
|
|
|
|
.addHandler(v2Router);
|
|
|
|
|
|
|
|
|
|
|
|
final rootRouter = Router()
|
2025-12-11 19:24:22 +00:00
|
|
|
|
..get('/health', (Request request) {
|
|
|
|
|
|
// Простой эндпоинт для healthcheck - всегда отвечает OK
|
|
|
|
|
|
// Не используем async для максимальной простоты и скорости
|
2025-12-11 17:59:30 +00:00
|
|
|
|
return Response.ok(
|
|
|
|
|
|
'OK',
|
|
|
|
|
|
headers: {
|
2025-12-11 19:24:22 +00:00
|
|
|
|
'Content-Type': 'text/plain; charset=utf-8',
|
2025-12-11 17:59:30 +00:00
|
|
|
|
...corsConfig,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
})
|
2025-11-16 11:25:27 +00:00
|
|
|
|
..mount('/api/v2', v2Handler)
|
|
|
|
|
|
..all('/<ignored|.*>', (Request request) async {
|
|
|
|
|
|
if (request.method == 'OPTIONS') {
|
|
|
|
|
|
return Response.ok('', headers: corsConfig);
|
|
|
|
|
|
}
|
|
|
|
|
|
return Response.notFound('Not Found', headers: corsConfig);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
final handler = rootRouter.call;
|
|
|
|
|
|
|
2025-12-11 19:34:09 +00:00
|
|
|
|
// Запуск HTTP сервера (HTTPS обрабатывается на уровне reverse proxy в Coolify)
|
|
|
|
|
|
final server = await _createServer(
|
2025-11-16 11:25:27 +00:00
|
|
|
|
handler,
|
|
|
|
|
|
address,
|
|
|
|
|
|
port,
|
|
|
|
|
|
poweredByHeader: null,
|
|
|
|
|
|
shared: true,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-12-11 19:34:09 +00:00
|
|
|
|
print('Serving app at http://${server.address.address}:${server.port}');
|
|
|
|
|
|
print('Server listening on http://${server.address.address}:${server.port}');
|
2025-11-16 11:25:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger(String tag) => (String msg, bool isError) {
|
|
|
|
|
|
if (isError) {
|
|
|
|
|
|
print('[ERROR][$tag] $msg');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
print('[$tag] $msg');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Future<HttpServer> _createServer(
|
|
|
|
|
|
Handler handler,
|
2025-12-11 19:24:22 +00:00
|
|
|
|
InternetAddress address,
|
2025-11-16 11:25:27 +00:00
|
|
|
|
int port, {
|
|
|
|
|
|
int? backlog,
|
|
|
|
|
|
bool shared = false,
|
|
|
|
|
|
String? poweredByHeader = 'Dart with package:shelf',
|
|
|
|
|
|
}) async {
|
|
|
|
|
|
backlog ??= 0;
|
2025-12-11 19:34:09 +00:00
|
|
|
|
final server = await HttpServer.bind(
|
|
|
|
|
|
address,
|
|
|
|
|
|
port,
|
|
|
|
|
|
backlog: backlog,
|
|
|
|
|
|
shared: shared,
|
|
|
|
|
|
);
|
2025-12-11 19:24:22 +00:00
|
|
|
|
// Запускаем обработку запросов (неблокирующая операция)
|
|
|
|
|
|
// serveRequests запускает обработку запросов в фоне и возвращает управление сразу
|
2025-11-16 11:25:27 +00:00
|
|
|
|
shelf_io.serveRequests(server, handler, poweredByHeader: poweredByHeader);
|
2025-12-11 19:24:22 +00:00
|
|
|
|
// Даем серверу небольшое время на инициализацию обработчика запросов
|
|
|
|
|
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
|
|
|
|
print('✅ Server bound and ready to accept connections on ${server.address.address}:${server.port}');
|
2025-11-16 11:25:27 +00:00
|
|
|
|
return server;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|