import 'dart:convert'; import 'dart:io'; import 'package:logging/logging.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart' as shelf_io; import 'package:shelf_router/shelf_router.dart'; import 'package:cleanplate_api/src/config/env.dart'; import 'package:cleanplate_api/src/config/database.dart'; import 'package:cleanplate_api/src/config/redis_config.dart'; import 'package:cleanplate_api/src/middleware/auth_middleware.dart'; import 'package:cleanplate_api/src/middleware/cors_middleware.dart'; import 'package:cleanplate_api/src/middleware/logging_middleware.dart'; import 'package:cleanplate_api/src/middleware/error_handler.dart'; import 'package:cleanplate_api/src/middleware/rate_limit_middleware.dart'; import 'package:cleanplate_api/src/modules/auth/auth_routes.dart'; import 'package:cleanplate_api/src/modules/recipes/recipe_routes.dart'; import 'package:cleanplate_api/src/modules/ai/ai_routes.dart'; import 'package:cleanplate_api/src/modules/search/search_routes.dart'; import 'package:cleanplate_api/src/modules/media/media_routes.dart'; import 'package:cleanplate_api/src/modules/users/user_routes.dart'; import 'package:cleanplate_api/src/modules/community/community_routes.dart'; final _log = Logger('Server'); void main(List args) async { // --------------------------------------------------------------- // Logging // --------------------------------------------------------------- Logger.root.level = Env.isProduction ? Level.INFO : Level.ALL; Logger.root.onRecord.listen((record) { stderr.writeln( '${record.time} [${record.level.name}] ${record.loggerName}: ${record.message}'); if (record.error != null) stderr.writeln(' Error: ${record.error}'); if (record.stackTrace != null) { stderr.writeln(' Stack: ${record.stackTrace}'); } }); // --------------------------------------------------------------- // Infrastructure connections // --------------------------------------------------------------- try { await Database.initialize(); _log.info('Database connected'); } catch (e) { _log.warning('Could not connect to database: $e'); _log.warning('Server will start but database-dependent routes will fail'); } try { await RedisConfig.initialize(); _log.info('Redis connected'); } catch (e) { _log.warning('Could not connect to Redis: $e'); _log.warning('Server will start but rate-limiting may use in-memory fallback'); } // --------------------------------------------------------------- // Routers // --------------------------------------------------------------- final authRoutes = AuthRoutes(); final recipeRoutes = RecipeRoutes(); final aiRoutes = AiRoutes(); final searchRoutes = SearchRoutes(); final mediaRoutes = MediaRoutes(); final userRoutes = UserRoutes(); final communityRoutes = CommunityRoutes(); // Top-level router. final app = Router(); // Health check. app.get('/health', (Request request) { return Response.ok( jsonEncode({'status': 'ok'}), headers: {'content-type': 'application/json'}, ); }); // Mount module routers under /api/v1. app.mount('/api/v1/auth/', authRoutes.router.call); app.mount('/api/v1/recipes/', recipeRoutes.router.call); app.mount('/api/v1/ai/', aiRoutes.router.call); app.mount('/api/v1/search/', searchRoutes.router.call); app.mount('/api/v1/media/', mediaRoutes.router.call); app.mount('/api/v1/users/', userRoutes.router.call); // Community reviews are nested under a recipe. // We use a small adapter to inject the recipeId into the request context. app.mount('/api/v1/recipes//reviews/', (Request request) { final recipeId = request.params['recipeId']; final updated = request.change(context: {'recipeId': recipeId}); return communityRoutes.router.call(updated); }); // --------------------------------------------------------------- // Middleware pipeline // --------------------------------------------------------------- final handler = Pipeline() .addMiddleware(corsMiddleware()) .addMiddleware(loggingMiddleware()) .addMiddleware(errorHandler()) .addMiddleware(rateLimitMiddleware()) .addMiddleware(authMiddleware()) .addHandler(app.call); // --------------------------------------------------------------- // Start server // --------------------------------------------------------------- final ip = InternetAddress.anyIPv4; final port = int.parse(Env.port); final server = await shelf_io.serve(handler, ip, port); _log.info('CleanPlate API listening on http://${server.address.host}:${server.port}'); _log.info('Environment: ${Env.environment}'); // --------------------------------------------------------------- // Graceful shutdown // --------------------------------------------------------------- ProcessSignal.sigint.watch().listen((_) async { _log.info('SIGINT received — shutting down...'); await _shutdown(server); }); // SIGTERM is not available on Windows but fine on Linux/macOS. if (!Platform.isWindows) { ProcessSignal.sigterm.watch().listen((_) async { _log.info('SIGTERM received — shutting down...'); await _shutdown(server); }); } } Future _shutdown(HttpServer server) async { _log.info('Closing HTTP server...'); await server.close(force: false); _log.info('Closing database pool...'); await Database.close(); _log.info('Closing Redis connection...'); await RedisConfig.close(); _log.info('Shutdown complete'); exit(0); }