mnemo_cards/mnemo_cards_backend/lib/api/mnemo_shelf.dart

271 lines
9.4 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';
import 'v2/ads_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/authorize_v2.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(List<String> args) async {
final addressArg =
args.indexOf('-a') == -1 ? null : args[args.indexOf('-a') + 1];
final portArg =
args.indexOf('-p') == -1 ? null : args[args.indexOf('-p') + 1];
final address = addressArg ?? InternetAddress.anyIPv4.address;
2025-11-16 16:31:22 +00:00
final port = int.tryParse(portArg ?? '') ?? 8443;
2025-11-16 11:25:27 +00:00
final certs = args.contains('--certs')
? args[args.indexOf('--certs') + 1]
: null; // null = HTTP without SSL
final dualMode =
args.contains('--dual'); // Запуск HTTP и HTTPS одновременно
// V2 APIs (new RESTful API with OAuth2/JWT)
final v2Routers = [
getIt.get<AuthApiV2>().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,
];
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',
2025-11-16 16:31:22 +00:00
// '5492281-cf88967.twc1.net:8443'
2025-11-16 11:25:27 +00:00
// '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',
'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(
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()
..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, Object 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,
);
2025-11-16 16:31:22 +00:00
// HTTPS сервер на порту + 1 (например, 8443 -> 8081)
2025-11-16 11:25:27 +00:00
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,
Object 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,
));
shelf_io.serveRequests(server, handler, poweredByHeader: poweredByHeader);
return server;
}
}