UNPKG

@bitzonegaming/roleplay-engine-framework

Version:
156 lines (155 loc) 6.82 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ApiServer = void 0; require("reflect-metadata"); const fastify_1 = __importDefault(require("fastify")); const cors_1 = __importDefault(require("@fastify/cors")); const types_1 = require("./types"); const error_handler_1 = require("./middleware/error-handler"); const auth_1 = require("./middleware/auth"); const decorators_1 = require("./decorators"); /** * API Server that manages HTTP endpoints using Fastify and decorators */ class ApiServer { constructor(context, config) { this.controllers = new Map(); this.context = context; this.config = config; this.logger = context.logger; this.fastify = (0, fastify_1.default)({ logger: false, // We use our own logger }); this.fastify.setErrorHandler((0, error_handler_1.createErrorHandler)(context)); this.fastify.register(cors_1.default, { origin: config.cors?.origin ?? '*', methods: config.cors?.methods ?? ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], credentials: config.cors?.credentials ?? true, allowedHeaders: config.cors?.allowedHeaders ?? '*', }); } /** * Registers a controller with the API server */ registerController(ControllerCtor) { const controllerMetadata = Reflect.getMetadata(types_1.METADATA_KEYS.CONTROLLER, ControllerCtor); if (!controllerMetadata) { throw new Error(`${ControllerCtor.name} is not decorated with @Controller`); } const controller = new ControllerCtor(this.context); this.controllers.set(ControllerCtor, controller); const routes = Reflect.getMetadata(types_1.METADATA_KEYS.ROUTES, Object.getPrototypeOf(controller)) || []; for (const route of routes) { const fullPath = this.joinPaths(controllerMetadata.path, route.path); const methodName = Reflect.getMetadata(`route:${route.method}:${route.path}`, Object.getPrototypeOf(controller)); if (!methodName) { this.logger.warn(`No method found for route ${route.method} ${fullPath}`); continue; } const handler = controller[methodName]; if (typeof handler !== 'function') { this.logger.warn(`Method ${methodName} is not a function on controller`); continue; } const authMetadata = (0, decorators_1.getAuthorizationMetadata)(Object.getPrototypeOf(controller), methodName); this.fastify.route({ method: route.method, url: fullPath, handler: async (request, reply) => { const authorizedRequest = request; if (authMetadata?.apiKey) { await (0, auth_1.validateApiKey)(request, this.config.gamemodeApiKeyHash); } if (authMetadata?.sessionToken) { await (0, auth_1.validateSessionToken)(authorizedRequest, this.context, authMetadata.sessionToken.scope, authMetadata.sessionToken.accessPolicy); } const paramMetadata = (0, decorators_1.getParamMetadata)(Object.getPrototypeOf(controller), methodName); const args = []; for (const param of paramMetadata.sort((a, b) => a.index - b.index)) { switch (param.type) { case decorators_1.ParamType.BODY: args[param.index] = request.body; break; case decorators_1.ParamType.QUERY: args[param.index] = param.property ? request.query[param.property] : request.query; break; case decorators_1.ParamType.PARAMS: args[param.index] = param.property ? request.params[param.property] : request.params; break; case decorators_1.ParamType.HEADERS: args[param.index] = param.property ? request.headers[param.property.toLowerCase()] : request.headers; break; case decorators_1.ParamType.REQUEST: args[param.index] = authorizedRequest; break; case decorators_1.ParamType.REPLY: args[param.index] = reply; break; } } const result = await handler.call(controller, ...args); if (route.statusCode) { reply.status(route.statusCode); } return result; }, }); this.logger.info(`Registered route: ${route.method} ${fullPath}`); } return this; } /** * Starts the API server */ async start() { const port = this.config.port ?? 3000; const host = this.config.host || '0.0.0.0'; await this.fastify.listen({ port, host }); this.logger.info(`API server listening on ${host}:${port}`); } /** * Stops the API server */ async stop() { for (const controller of this.controllers.values()) { if (controller.dispose) { try { await controller.dispose(); } catch (error) { this.logger.error(`Error disposing controller:`, error); } } } await this.fastify.close(); this.logger.info('API server stopped'); } /** * Gets the Fastify instance (for advanced configuration) */ getFastify() { return this.fastify; } /** * Joins paths ensuring proper slashes */ joinPaths(base, path) { if (!base) return path || '/'; if (!path || path === '/') return base || '/'; const cleanBase = base.endsWith('/') ? base.slice(0, -1) : base; const cleanPath = path.startsWith('/') ? path : `/${path}`; return `${cleanBase}${cleanPath}`; } } exports.ApiServer = ApiServer;