mnemo_cards/mnemo_cards_backend/lib/api/mnemo_shelf.dart

181 lines
7.1 KiB
Dart
Raw Normal View History

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-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 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;
}
}