import compression from 'compression'; import cookieParser from 'cookie-parser'; import ApiGateway from 'moleculer-web'; import { Service, Errors, ServiceBroker, Context, NodeHealthStatus } from 'moleculer'; import { verify } from 'jsonwebtoken'; import CacheCleaner from '@cards-against-formality/cache-clean-mixin'; /** * AdminGatewayService exposes all access to admin users. * * @export * @class AdminGatewayService * @extends {Service} */ export default class AdminGatewayService extends Service { /** * Creates an instance of AdminGatewayService. * * @param {ServiceBroker} _broker * @memberof AdminGatewayService */ constructor(_broker: ServiceBroker) { super(_broker); this.parseServiceSchema( { name: 'admin-gateway', mixins: [ ApiGateway, CacheCleaner([ 'cache.clean.cards', 'cache.clean.decks', 'cache.clean.clients', 'cache.clean.rooms', 'cache.clean.games', ]) ], settings: { rateLimit: { limit: process.env.REQUESTS_PER_MINUTE || 100, headers: true, key: (req) => { return req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.socket.remoteAddress || req.connection.socket.remoteAddress; } }, cors: { origin: '*', methods: ['GET', 'OPTIONS', 'POST', 'PATCH', 'DELETE'], allowedHeaders: [], exposedHeaders: [], credentials: false, maxAge: 3600 }, use: [ compression(), cookieParser() ], routes: [ { path: '/admin', // Enable in prod. authorization: false, aliases: { 'GET /web/health': 'web-gateway.health', 'GET /gateway/health': 'admin-gateway.health', 'GET /cards/health': 'cards.health', 'GET /cards/:id': 'cards.get', 'GET /cards': 'cards.list', 'POST /cards/search': 'cards.find', 'POST /cards': 'cards.create', 'PATCH /cards/:id': 'cards.update', 'DELETE /cards/:id': 'cards.remove', 'GET /decks/health': 'decks.health', 'GET /decks/:id': 'decks.get', 'GET /decks': 'decks.list', 'POST /decks/search': 'decks.find', 'POST /decks': 'decks.create', 'PATCH /decks/:id': 'decks.update', 'DELETE /decks/:id': 'decks.remove', 'GET /clients/health': 'clients.health', 'GET /clients/:id': 'clients.get', 'GET /clients': 'clients.list', 'POST /clients/search': 'clients.find', 'POST /clients': 'clients.create', 'PATCH /clients/:id': 'clients.update', 'DELETE /clients/:id': 'clients.remove', 'GET /rooms/health': 'rooms.health', 'GET /rooms/:id': 'rooms.get', 'GET /rooms': 'rooms.list', 'POST /rooms/search': 'rooms.find', 'POST /rooms': 'rooms.create', 'PATCH /rooms/:id': 'rooms.update', 'DELETE /rooms/:id': 'rooms.remove', 'PUT /rooms/join/players': 'rooms.join-players', 'PUT /rooms/join/spectators': 'rooms.join-spectators', 'PUT /rooms/leave': 'rooms.leave', 'GET /games/health': 'games.health', 'PUT /games/start': 'games.start', 'POST /games/cards': 'games.submit', 'POST /games/winner': 'games.winner', 'GET /games/:id': 'games.get', 'GET /games': 'games.list', 'POST /games/search': 'games.find', 'POST /games': 'games.create', 'PATCH /games/:id': 'games.update', 'DELETE /games/:id': 'games.remove', }, mappingPolicy: 'restrict', bodyParsers: { json: { strict: false }, urlencoded: { extended: false } } }], }, methods: { authorize: this.authorize }, actions: { health: this.health } } ); } /** * Verify and Decode the JWT token using the Seret. * * @private * @param {string} token * @returns {Promise<any>} * @memberof AdminGatewayService */ private verifyAndDecode(token: string): Promise<any> { return new Promise((resolve, reject) => { verify(token, process.env.JWT_SECRET, (err, decoded) => { if (err) { reject(err); return; } resolve(decoded); return; }); }); } /** * Authorize the request. Decode the User token and add it to the ctx meta. * * @private * @param {Context<any, any>} ctx * @param {string} route * @param {*} req * @param {*} res * @returns * @memberof AdminGatewayService */ private authorize(ctx: Context<any, any>, route: string, req: any, res: any) { const auth = req.cookies['auth'] || req.headers['authorization']; if (auth === undefined || !auth?.length || !auth.startsWith('Bearer')) { return Promise.reject(new Errors.MoleculerError('No token found', 401, 'NO_TOKEN_FOUND')); } const token = auth.slice(7); return this.verifyAndDecode(token) .then(decoded => { ctx.meta.user = decoded; return ctx; }) .catch(err => { throw new Errors.MoleculerError(`Denined access: ${err.message}`, 401, 'ACCESS_DENIED'); }); } /** * Get the health data for this service. * * @private * @param {Context} ctx * @returns {Promise<NodeHealthStatus>} * @memberof AdminGatewayService */ private health(ctx: Context): Promise<NodeHealthStatus> { this.logger.info('') return ctx.call('$node.health'); } }