mnemo_cards/mnemo_cards_backend/lib/api/mnemo_shelf.dart
Dmitry ea1baedb67
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
server
2025-12-11 22:24:22 +03:00

299 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
import 'v2/ads_api_v2.dart';
import 'v2/admin_analytics_api_v2.dart';
import 'v2/admin_auth_api_v2.dart';
import 'v2/admin_cards_api_v2.dart';
import 'v2/admin_packs_api_v2.dart';
import 'v2/admin_users_api_v2.dart';
import 'v2/auth_api_v2.dart';
import 'v2/discounts_api_v2.dart';
import 'v2/games_api_v2.dart';
import 'v2/packs_api_v2.dart';
import 'v2/purchases_api_v2.dart';
import 'v2/promocodes_api_v2.dart';
import 'v2/subscriptions_api_v2.dart';
import 'v2/tasks_api_v2.dart';
import 'v2/tests_api_v2.dart';
import 'v2/users_api_v2.dart';
import 'v2/telegram_bot_api_v2.dart';
import 'v2/authorize_v2.dart';
import 'v2/telegram_bot_auth_middleware.dart';
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);
Future<void> initV2() async {
// Чтение переменных окружения вместо CLI аргументов
final addressArg = Platform.environment['SERVER_ADDRESS'];
final portArg = Platform.environment['PORT'];
// Используем InternetAddress напрямую для правильной работы с bind
final address = addressArg != null && addressArg != '0.0.0.0'
? InternetAddress(addressArg)
: InternetAddress.anyIPv4;
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 одновременно
// V2 APIs (new RESTful API with OAuth2/JWT)
final v2Routers = [
getIt.get<AuthApiV2>().router,
getIt.get<AdminAuthApiV2>().router,
getIt.get<AdminAnalyticsApiV2>().router,
getIt.get<AdminCardsApiV2>().router,
getIt.get<AdminPacksApiV2>().router,
getIt.get<PacksApiV2>().router,
getIt.get<TestsApiV2>().router,
getIt.get<GamesApiV2>().router,
getIt.get<PurchasesApiV2>().router,
getIt.get<AdsApiV2>().router,
getIt.get<UsersApiV2>().router,
getIt.get<PromocodesApiV2>().router,
getIt.get<SubscriptionsApiV2>().router,
getIt.get<AdminUsersApiV2>().router,
getIt.get<DiscountsApiV2>().router,
getIt.get<TasksApiV2>().router,
getIt.get<TelegramBotApiV2>().router,
];
final v2Router = v2Routers.fold<Router>(
Router(),
(router, child) => router..mount('/', child),
);
// Note: rootRouter is not used because we manually route to v1/v2 handlers
// for different authentication middleware. Keeping for reference.
// final rootRouter = Router()
// ..mount('/', v1Router) // Legacy v1 APIs
// ..mount('/api/v2', v2Router); // New v2 APIs
// ..mount(
// '/swagger',
// SwaggerUI('public/open_api.yaml', title: 'Api'),
// )
// ..mount('/', createStaticHandler('public'));
// Configure a pipeline with proper CORS configuration
// Поддерживаем как HTTP (разработка), так и HTTPS (продакшен)
// Note: allowedOrigins is kept for reference but not used since we allow all origins
// final allowedOrigins = [
// 'https://1592725-cf88967.twc1.net',
// 'https://1592725-cf88967.twc1.net:443',
// '5492281-cf88967.twc1.net',
// '5492281-cf88967.twc1.net:8443'
// 'http://localhost:*',
// 'http://localhost:51717', // для разработки
// 'http://localhost:3000', // для разработки
// 'http://localhost:8000', // для разработки
// 'http://127.0.0.1:8000', // для разработки
// ];
final corsConfig = {
'Access-Control-Allow-Origin': '*', // Разрешаем все домены для гибкости
'Access-Control-Allow-Methods':
'GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS',
'Access-Control-Allow-Headers':
'Origin, Content-Type, Accept, Authorization, user_token, request_token, app_version, X-API-Key',
'Access-Control-Expose-Headers': 'Authorization',
'Access-Control-Max-Age': '86400',
};
// Добавляем HSTS только для HTTPS соединений
if (certs != null) {
corsConfig['Strict-Transport-Security'] =
'max-age=31536000; includeSubDomains';
}
// 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);
return response.change(headers: {
...response.headers,
...corsConfig,
});
};
};
}
final v2Handler = Pipeline()
.addMiddleware(corsMiddleware())
.addMiddleware(logRequests(logger: logger('v2')))
.addMiddleware(telegramBotAuthMiddleware())
.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()
..get('/health', (Request request) {
// Простой эндпоинт для healthcheck - всегда отвечает OK
// Не используем async для максимальной простоты и скорости
return Response.ok(
'OK',
headers: {
'Content-Type': 'text/plain; charset=utf-8',
...corsConfig,
},
);
})
..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;
if (dualMode) {
// Запуск HTTP и HTTPS серверов одновременно
await _startDualServers(handler, address, port, certs);
} else {
// Обычный запуск одного сервера
final server = await _createServer(
handler,
address,
port,
securityContext: certs != null ? getSecurityContext(certs) : null,
poweredByHeader: null,
shared: true,
);
final protocol = certs != null ? 'https' : 'http';
print(
'Serving app at $protocol://${server.address.address}:${server.port}');
final url = '$protocol://${server.address.address}:${server.port}';
print('Server listening on $url');
if (certs != null) {
print('HTTPS enabled with SSL certificates from: $certs');
print('Domain: 1592725-cf88967.twc1.net');
}
}
}
Future<void> _startDualServers(
Handler handler, InternetAddress address, int port, String? certs) async {
print('🚀 Starting DUAL mode: HTTP + HTTPS servers');
// HTTP сервер на указанном порту
final httpServer = await _createServer(
handler,
address,
port,
securityContext: null, // HTTP без SSL
poweredByHeader: null,
shared: true,
);
// HTTPS сервер на порту + 1 (например, 8443 -> 8081)
final httpsPort = port + 1;
final httpsServer = await _createServer(
handler,
address,
httpsPort,
securityContext: certs != null ? getSecurityContext(certs) : null,
poweredByHeader: null,
shared: true,
);
print('✅ Both servers started successfully!');
print(
'📡 HTTP server: http://${httpServer.address.address}:${httpServer.port}');
print(
'🔒 HTTPS server: https://${httpsServer.address.address}:${httpsServer.port}');
if (certs != null) {
print('🔐 HTTPS enabled with SSL certificates from: $certs');
print('🌐 Domain: 1592725-cf88967.twc1.net');
}
}
SecurityContext getSecurityContext(String path) {
// Для Let's Encrypt используем fullchain.pem и privkey.pem
// Для самоподписанных сертификатов используем server.crt и server.key
String serverCert, key;
// if (path.contains('letsencrypt') || path.contains('/etc/letsencrypt/')) {
// // Let's Encrypt сертификаты
// // serverCert = path + '/fullchain.pem';
// // key = path + '/privkey.pem';
// } else {
// // // Самоподписанные сертификаты
// // serverCert = path.startsWith('/')
// // ? path + '/server.crt'
// // : Platform.script.resolve('$path/server.crt').toFilePath();
// // key = path.startsWith('/')
// // ? path + '/server.key'
// // : Platform.script.resolve('$path/server.key').toFilePath();
// }
return SecurityContext(withTrustedRoots: false);
// ..useCertificateChain(serverCert)
// ..usePrivateKey(key);
}
logger(String tag) => (String msg, bool isError) {
if (isError) {
print('[ERROR][$tag] $msg');
} else {
print('[$tag] $msg');
}
};
Future<HttpServer> _createServer(
Handler handler,
InternetAddress address,
int port, {
SecurityContext? securityContext,
int? backlog,
bool shared = false,
String? poweredByHeader = 'Dart with package:shelf',
}) async {
backlog ??= 0;
var server = await (securityContext == null
? HttpServer.bind(address, port, backlog: backlog, shared: shared)
: HttpServer.bindSecure(
address,
port,
securityContext,
backlog: backlog,
shared: shared,
requestClientCertificate: true,
));
// Запускаем обработку запросов (неблокирующая операция)
// serveRequests запускает обработку запросов в фоне и возвращает управление сразу
shelf_io.serveRequests(server, handler, poweredByHeader: poweredByHeader);
// Даем серверу небольшое время на инициализацию обработчика запросов
await Future.delayed(const Duration(milliseconds: 100));
print('✅ Server bound and ready to accept connections on ${server.address.address}:${server.port}');
return server;
}
}