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
299 lines
11 KiB
Dart
299 lines
11 KiB
Dart
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;
|
||
}
|
||
}
|