├── .dockerignore ├── src ├── registry │ └── registry.json ├── util │ ├── logger.ts │ ├── ping.ts │ ├── loadbalancer.ts │ ├── config.ts │ ├── axiosCaller.ts │ └── auth.ts ├── gateway.ts └── routes │ └── index.ts ├── .prettierrc.js ├── .gitignore ├── .env.example ├── Dockerfile ├── .eslintrc.js ├── README.md ├── package.json └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/registry/registry.json: -------------------------------------------------------------------------------- 1 | { 2 | "services": { 3 | }, 4 | "auth": { 5 | "users": {} 6 | } 7 | } -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2, 7 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | coverage/ 4 | .nyc_output/ 5 | .vscode/launch.json 6 | .vscode/settings.json 7 | .idea 8 | */dist 9 | *.log 10 | dist/ 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP='API Gateway' 2 | PORT=5001 3 | NODE_ENV=development 4 | APP_SECRET= 5 | RATE_LIMIT_PER_HOUR=100 6 | AUTH_POLICY=auth0 or custom 7 | AUTH0_AUDIENCE='#' 8 | AUTH0_ISSUERER='#' 9 | AUTH0_JWKS_URI='#' 10 | AUTH0_CLIENT_ID='#' 11 | AUTH0_CLIENT_SECRET='#' 12 | CUSTOM_AUTH_SECRET='#' -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | # Create app directory 4 | WORKDIR /usr/src/app 5 | 6 | # Add package file 7 | COPY package*.json ./ 8 | 9 | # Install deps 10 | RUN npm i 11 | RUN npm audit fix --force 12 | 13 | # Copy source 14 | COPY . . 15 | 16 | # Build dist 17 | RUN npm run build-ts 18 | 19 | # Expose port 20 | EXPOSE ${PORT} 21 | 22 | CMD npm run start 23 | -------------------------------------------------------------------------------- /src/util/logger.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, format, transports } from 'winston'; 2 | 3 | const logger = createLogger({ 4 | format: format.combine( 5 | format.colorize(), 6 | format.timestamp(), 7 | format.printf(({ level, message, timestamp }) => `${timestamp} ${level}: ${message}`), 8 | ), 9 | transports: [new transports.Console()], 10 | }); 11 | 12 | export { logger }; 13 | -------------------------------------------------------------------------------- /src/util/ping.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | 4 | type IPing = { 5 | isAlive: Function; 6 | } 7 | 8 | const ping = {} as IPing; 9 | ping.isAlive = async (url: any) => { 10 | try { 11 | const response = await axios.get(url); 12 | return response.status === 200 ? true: false; 13 | } catch (error) { 14 | return false; 15 | 16 | } 17 | } 18 | 19 | export { ping } -------------------------------------------------------------------------------- /src/util/loadbalancer.ts: -------------------------------------------------------------------------------- 1 | type ILoadBalancer = { 2 | ROUND_ROBIN: Function; 3 | isEnabled: Function; 4 | } 5 | 6 | const loadbalancer = {} as ILoadBalancer; 7 | 8 | loadbalancer.ROUND_ROBIN = (service: { index: number; instances: string | any[]; }) => { 9 | try { 10 | const newIndex = ++service.index >= service.instances.length ? 0 : service.index 11 | service.index = newIndex 12 | return loadbalancer.isEnabled(service, newIndex, loadbalancer.ROUND_ROBIN) 13 | } catch (err) { 14 | return err 15 | } 16 | } 17 | 18 | loadbalancer.isEnabled = (service: { instances: { [x: string]: { enabled: any; }; }; }, index: string | number, loadBalanceStrategy: (arg0: any) => any) => { 19 | return service.instances[index].enabled ? index : loadBalanceStrategy(service) 20 | } 21 | 22 | export { loadbalancer } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | extends: [ 4 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 5 | 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 6 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 7 | ], 8 | parserOptions: { 9 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 10 | sourceType: 'module', // Allows for the use of imports 11 | }, 12 | rules: { 13 | // disable the rule for all file 14 | '@typescript-eslint/interface-name-prefix': [2, { 'prefixWithI': 'always' }], 15 | '@typescript-eslint/explicit-function-return-type': 'off', 16 | }, 17 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # API-Gateway 2 | 3 | A simple but robust API Gateway written in [TypeScript](https://www.typescriptlang.org/). 4 | 5 | API Gateway is a developer friendly tool that sits at the heart of your microservices architecture and securely expose them through APIs using Nodejs. 6 | 7 | ## Features 8 | 9 | - `Authentication` (Auth0) 10 | - `Service Registry` 11 | - `Service Management` 12 | - `Load Balancing` 13 | - `API Health & PING` 14 | - `REST API` 15 | - `Logging` 16 | 17 | 18 | ## Installation 19 | 20 | 1. Clone the repository to your project root directory using this command: (replace `api-gateway` with a folder name for the gateway) 21 | ```sh 22 | git clone https://github.com/AdebsAlert/api-gateway.git api-gateway 23 | ``` 24 | 25 | 2. CD into the gateway directory and install all dependencies: 26 | ```sh 27 | cd api-gateway 28 | npm install -g typescript 29 | npm install 30 | ``` 31 | 32 | 3. Create a .env file from the .env.example file and enter your key values 33 | ```sh 34 | cp .env.example .env 35 | ``` 36 | 37 | 4. Set your AUTH policy in your .env file and your policy params 38 | ```sh 39 | AUTH_POLICY='auth0' or 'custom' 40 | ``` 41 | 42 | 5. Build and start the API Gateway 43 | ```sh 44 | npm run build-ts 45 | npm run watch-serve 46 | ``` 47 | 48 | ## Service Management 49 | 50 | https://documenter.getpostman.com/view/4644005/UVkvKYZS 51 | 52 | 53 | ## License 54 | [MIT](LICENSE) 55 | -------------------------------------------------------------------------------- /src/util/config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { logger } from './logger'; 3 | 4 | dotenv.config({}); 5 | 6 | function throwIfUndefined(secret: T | undefined, name?: string): T { 7 | if (!secret) { 8 | logger.error(`${name} must not be undefined`); 9 | return process.exit(1); 10 | } 11 | return secret; 12 | } 13 | 14 | export const PORT = throwIfUndefined(process.env.PORT, 'PORT'); 15 | export const APP = throwIfUndefined(process.env.APP, 'APP'); 16 | export const NODE_ENV = throwIfUndefined(process.env.NODE_ENV, 'NODE_ENV'); 17 | export const RATE_LIMIT_PER_HOUR = throwIfUndefined(process.env.RATE_LIMIT_PER_HOUR, 'RATE_LIMIT_PER_HOUR'); 18 | export const APP_SECRET = throwIfUndefined(process.env.APP_SECRET, 'APP_SECRET'); 19 | export const AUTH_POLICY = throwIfUndefined(process.env.AUTH_POLICY, 'AUTH_POLICY'); 20 | export const AUTH0_AUDIENCE = throwIfUndefined(process.env.AUTH0_AUDIENCE, 'AUTH0_AUDIENCE').replace(/\/$/, ''); 21 | export const AUTH0_ISSUERER = throwIfUndefined(process.env.AUTH0_ISSUERER, 'AUTH0_ISSUERER').replace(/\/$/, ''); 22 | export const AUTH0_JWKS_URI = throwIfUndefined(process.env.AUTH0_JWKS_URI, 'AUTH0_JWKS_URI').replace(/\/$/, ''); 23 | export const AUTH0_CLIENT_ID = throwIfUndefined(process.env.AUTH0_CLIENT_ID, 'AUTH0_CLIENT_ID'); 24 | export const AUTH0_CLIENT_SECRET = throwIfUndefined(process.env.AUTH0_CLIENT_SECRET, 'AUTH0_CLIENT_SECRET'); 25 | export const CUSTOM_AUTH_SECRET = throwIfUndefined(process.env.CUSTOM_AUTH_SECRET, 'CUSTOM_AUTH_SECRET'); 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-api-gateway", 3 | "version": "1.0.0", 4 | "description": "API Gateway in TypeScript", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node dist/gateway.js", 8 | "watch-serve": "nodemon dist/gateway.js", 9 | "serve": "node dist/gateway.js", 10 | "build-ts": "tsc", 11 | "watch-ts": "tsc -w", 12 | "test": "jest --forceExit --verbose --detectOpenHandles --colors --coverage", 13 | "watch-test": "npm run test -- --watchAll", 14 | "heroku-build": "npm run build-ts && npm run start" 15 | }, 16 | "keywords": [], 17 | "author": "Adebayo Mustafa ", 18 | "license": "ISC", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/AdebsAlert/api-gateway" 22 | }, 23 | "devDependencies": { 24 | "@types/append-query": "^2.0.1", 25 | "@types/cors": "^2.8.12", 26 | "@types/express": "^4.17.13", 27 | "@types/hapi__joi": "^17.1.8", 28 | "@types/jsonwebtoken": "^8.5.6", 29 | "nodemon": "^2.0.4" 30 | }, 31 | "dependencies": { 32 | "@hapi/joi": "^17.1.1", 33 | "append-query": "^2.1.1", 34 | "axios": "^0.24.0", 35 | "cors": "^2.8.5", 36 | "dotenv": "^10.0.0", 37 | "ejs": "^3.1.6", 38 | "express": "^4.17.2", 39 | "express-jwt": "^6.1.0", 40 | "express-openid-connect": "^2.7.0", 41 | "express-rate-limit": "^6.0.3", 42 | "helmet": "^4.6.0", 43 | "jsonwebtoken": "^8.5.1", 44 | "jwks-rsa": "^2.0.5", 45 | "typescript": "^4.5.4", 46 | "winston": "^3.3.3" 47 | }, 48 | "engines": { 49 | "node": ">=16.0.0", 50 | "npm": ">=8.0.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/util/axiosCaller.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, AxiosResponse} from 'axios'; 2 | import { authenticateAuth0, authenticateCustom } from '../util/auth' 3 | import { AUTH_POLICY } from './config'; 4 | import { logger } from './logger'; 5 | 6 | export const axiosCall = async (method: any, url: any, data: any, headers: any) => { 7 | // extract the bearer token from the header and validate it 8 | if(!url.includes('/auth/') && AUTH_POLICY === 'auth0') { 9 | logger.info('Gateway - request is being authenticated with Auth0') 10 | const authUser = await authenticateAuth0(headers) 11 | headers.user = JSON.stringify(authUser); 12 | }else if(!url.includes('/auth/') && AUTH_POLICY === 'custom'){ 13 | logger.info('Gateway - request is being authenticated with Custom Auth') 14 | const authUser = await authenticateCustom(headers) 15 | headers.user = JSON.stringify(authUser); 16 | }else{ 17 | logger.info('Gateway - request is not being authenticated') 18 | 19 | } 20 | 21 | const config: AxiosRequestConfig = { 22 | method, 23 | url, 24 | data, 25 | }; 26 | 27 | delete headers['authorization']; 28 | 29 | logger.info('Gateway - routing incoming request') 30 | 31 | try { 32 | const response: AxiosResponse = await axios(config); 33 | return response 34 | 35 | } catch (error: any) { 36 | if (error.response) { 37 | logger.error(`Gateway - response error: ${error.response.data.error}`); 38 | return error.response; 39 | } else { 40 | logger.error(`Gateway - routing error: ${error.message}`) 41 | return error.message; 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/gateway.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import helmet from 'helmet' 3 | const app = express() 4 | import { router } from './routes' 5 | import { APP_SECRET, AUTH0_AUDIENCE, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_ISSUERER, PORT, RATE_LIMIT_PER_HOUR, AUTH_POLICY } from './util/config' 6 | import rateLimit from 'express-rate-limit' 7 | import { auth } from 'express-openid-connect' 8 | import bodyParser from 'body-parser' 9 | import cors from 'cors' 10 | 11 | const limiter = rateLimit({ 12 | windowMs: 60 * 60 * 1000, // 60 minutes 13 | max: parseInt(RATE_LIMIT_PER_HOUR), // Limit each IP to 100 requests per `window` (here, per 60 minutes) 14 | standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers 15 | legacyHeaders: false, // Disable the `X-RateLimit-*` headers 16 | }) 17 | 18 | // Apply the rate limiting middleware to all requests 19 | app.use(limiter) 20 | app.use(express.json()) 21 | app.use(helmet()) 22 | app.use(bodyParser.json()); 23 | app.use(bodyParser.urlencoded({ extended: true })); 24 | app.use( 25 | cors({ 26 | origin: '*', 27 | methods: ['GET', 'POST', 'DELETE', 'UPDATE', 'PUT', 'PATCH'], 28 | allowedHeaders: ['Content-Type', 'Authorization'], 29 | preflightContinue: false, 30 | }), 31 | ); 32 | 33 | if(AUTH_POLICY === 'auth0') { 34 | const config: any = { 35 | authRequired: false, 36 | auth0Logout: true, 37 | secret: APP_SECRET, 38 | baseURL: AUTH0_AUDIENCE, 39 | clientID: AUTH0_CLIENT_ID, 40 | issuerBaseURL: AUTH0_ISSUERER, 41 | clientSecret: AUTH0_CLIENT_SECRET, 42 | authorizationParams: { 43 | response_type: 'code', 44 | audience: `${AUTH0_AUDIENCE}/`, 45 | scope: 'openid profile email', 46 | }, 47 | routes: { 48 | login: false, 49 | logout: false, 50 | } 51 | }; 52 | 53 | app.use(auth(config)) 54 | 55 | app.get('/', (req, res) => { 56 | res.json({user: req.oidc.user, token: req.oidc.accessToken?.access_token}); 57 | }) 58 | } else { 59 | 60 | } 61 | 62 | 63 | app.use('/', router) 64 | 65 | app.listen(PORT, () => { 66 | console.log('Gateway has started on port ' + PORT) 67 | }) 68 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 5 | "lib": ["ESNext"], 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | "sourceMap": true, /* Generates corresponding '.map' file. */ 8 | "outDir": "./dist", /* Redirect output structure to the directory. */ 9 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 10 | 11 | /* Strict Type-Checking Options */ 12 | "strict": true, /* Enable all strict type-checking options. */ 13 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 14 | "strictNullChecks": true, /* Enable strict null checks. */ 15 | "strictFunctionTypes": false, /* Enable strict checking of function types. */ 16 | "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 17 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 18 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 19 | 20 | /* Additional Checks */ 21 | "noUnusedLocals": true, /* Report errors on unused locals. */ 22 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 23 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 24 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 25 | 26 | /* Module Resolution Options */ 27 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 28 | "resolveJsonModule": true, 29 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 30 | }, 31 | "exclude": ["test", "registry/*.json"] 32 | } 33 | -------------------------------------------------------------------------------- /src/util/auth.ts: -------------------------------------------------------------------------------- 1 | import { AUTH0_JWKS_URI, AUTH0_AUDIENCE, AUTH0_ISSUERER, CUSTOM_AUTH_SECRET } from './config'; 2 | 3 | import jwt from 'express-jwt'; 4 | import jwks from 'jwks-rsa'; 5 | import Joi from '@hapi/joi'; 6 | import axios from 'axios'; 7 | import jwtWeb from 'jsonwebtoken'; 8 | 9 | 10 | export const jwtCheck = jwt({ 11 | secret: jwks.expressJwtSecret({ 12 | cache: true, 13 | rateLimit: true, 14 | jwksRequestsPerMinute: 5, 15 | jwksUri: AUTH0_JWKS_URI 16 | }), 17 | audience: `${AUTH0_AUDIENCE}/`, 18 | issuer: `${AUTH0_ISSUERER}/`, 19 | algorithms: ['RS256'] 20 | }); 21 | 22 | export async function authenticateAuth0(headers: { authorization: string; }) { 23 | const { authorization } = headers; 24 | 25 | const schema = Joi.object() 26 | .keys({ 27 | authorization: Joi.string() 28 | .regex(/^Bearer [A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/) 29 | .required() 30 | .error(new Error('Invalid bearer token, you need authenticate again')), 31 | }) 32 | .unknown(true); 33 | 34 | const validation = schema.validate(headers); 35 | if (validation.error) { 36 | throw new Error(validation.error.message); 37 | } 38 | 39 | const [, token] = authorization!.split('Bearer '); 40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 41 | try { 42 | const response = await axios.get( 43 | `${AUTH0_ISSUERER}/userinfo`, 44 | { 45 | headers: { 46 | authorization: `Bearer ${token}`, 47 | } 48 | } 49 | ) 50 | 51 | return response.data; 52 | } catch (error) { 53 | throw new Error('Invalid authorization token, you need to authenticate again'); 54 | } 55 | } 56 | 57 | export async function authenticateCustom(headers: { authorization: string; }) { 58 | const { authorization } = headers; 59 | 60 | const schema = Joi.object() 61 | .keys({ 62 | authorization: Joi.string() 63 | .regex(/^Bearer [A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/) 64 | .required() 65 | .error(new Error('Invalid bearer token, you need authenticate again')), 66 | }) 67 | .unknown(true); 68 | 69 | const validation = schema.validate(headers); 70 | if (validation.error) { 71 | throw new Error(validation.error.message); 72 | } 73 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 74 | const [, token] = authorization!.split('Bearer '); 75 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 76 | let decoded: any; 77 | try { 78 | decoded = jwtWeb.verify(token, CUSTOM_AUTH_SECRET); 79 | 80 | return decoded; 81 | } catch (error) { 82 | throw new Error('Invalid authorization token, you need to authenticate again'); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | const router = express.Router() 3 | import registry from '../registry/registry.json' 4 | import fs from 'fs' 5 | import { loadbalancer } from '../util/loadbalancer' 6 | import { logger } from '../util/logger'; 7 | import joi from '@hapi/joi'; 8 | import { axiosCall } from '../util/axiosCaller' 9 | import { ping } from '../util/ping' 10 | import { AUTH_POLICY } from '../util/config' 11 | // init the registry 12 | const registryData: {[index: string]:any} = registry; 13 | const loadbalancerData: {[index: string]:any} = loadbalancer; 14 | import appendQuery from 'append-query' 15 | 16 | 17 | /******** Begin Auth0 Authentication routes */ 18 | if(AUTH_POLICY === 'auth0') { 19 | router.get('/auth', (req, res) => { 20 | logger.info(`authSignin - signin process ongoing`); 21 | const redirectUrl = req.query.redirect_url ? req.query.redirect_url as string: '/'; 22 | res.oidc.login({ returnTo: '/auth/redirect?url='+redirectUrl }); 23 | }) 24 | 25 | router.get('/auth/signin', (req, res) => { 26 | logger.info(`authSignin - signin process ongoing`); 27 | const redirectUrl = req.query.redirect_url ? req.query.redirect_url as string: '/'; 28 | res.oidc.login({ returnTo: '/auth/redirect?url='+redirectUrl }); 29 | }) 30 | 31 | router.get('/auth/me', (req, res) => { 32 | logger.info(`authMe - getting authenticated user processing ongoing`); 33 | if(!req.oidc.accessToken) { // noaccess token to refresh, redirect to login 34 | res.status(401).json({user: null, token: null}); 35 | return 36 | } 37 | res.status(200).json({user: req.oidc.user, token: req.oidc.accessToken?.access_token}); 38 | }) 39 | 40 | 41 | router.get('/auth/refresh-token', async (req, res) => { 42 | logger.info(`authrefreshToken - refresh token process ongoing`); 43 | if(!req.oidc.accessToken) { // noaccess token to refresh, redirect to login 44 | logger.info(`authrefreshToken - no token found, login in`); 45 | res.oidc.login({ returnTo: '/auth/refresh-token' }); 46 | return 47 | } 48 | 49 | let { access_token, isExpired, refresh }: any = req.oidc.accessToken; 50 | if (isExpired()) { 51 | logger.info(`authrefreshToken - token is expired, refreshing token`); 52 | ({ access_token } = await refresh()); 53 | } 54 | 55 | res.json({token: access_token}); 56 | }) 57 | 58 | router.get('/auth/logout', (req, res) => { 59 | logger.info(`authLogout - logout process ongoing`); 60 | const redirectUrl = req.query.redirect_url ? req.query.redirect_url as string: '/'; 61 | res.oidc.logout({ returnTo: '/auth/redirect?url='+redirectUrl }); 62 | }) 63 | 64 | router.get('/auth/redirect', (req, res) => { 65 | logger.info(`authRedirect - auth redirection process ongoing`); 66 | const redirectUrl = req.query.url ? req.query.url as string: '/'; 67 | let token = '0', auth = false; 68 | 69 | if(req.oidc.accessToken) { 70 | token = req.oidc.accessToken?.access_token; 71 | auth = true; 72 | } 73 | 74 | const fullRedirectUrl = `${redirectUrl.replace(/\/$/, '')}/?webauth=${auth}&x-oidc-token=${token}` 75 | 76 | return res.redirect(301, fullRedirectUrl); 77 | }) 78 | } 79 | 80 | /*********** End Auth0 Authentication routes */ 81 | 82 | 83 | router.post('/gateway/instance/enable/:serviceName', (req, res) => { 84 | const serviceName = req.params.serviceName; 85 | const requestBody = req.body; 86 | 87 | const schema = joi.object().keys({ 88 | url: joi.string().required(), 89 | enabled: joi.boolean().required(), 90 | }); 91 | 92 | const validation = schema.validate(requestBody); 93 | if (validation.error) { 94 | logger.error(`enableInstance - Enabling instance failed due to validation error - ${validation.error.details[0].message}`); 95 | 96 | return res.status(400).json({ 97 | message: validation.error.details[0].message, 98 | }); 99 | } 100 | 101 | let instances; 102 | 103 | if (registry.services.hasOwnProperty(serviceName) == true) { 104 | instances = registryData.services[serviceName].instances 105 | } else { 106 | logger.error(`enableInstance - Service name not found`); 107 | 108 | return res.status(404).json({ 109 | message: 'Service name not found', 110 | }); 111 | } 112 | const index = instances.findIndex((srv: { url: any }) => { return srv.url === requestBody.url }) 113 | if(index == -1){ 114 | logger.error(`enableInstance - Could not find ${requestBody.url} for service ${serviceName}`); 115 | 116 | return res.status(404).json({ message: `Could not find ${requestBody.url} for service ${serviceName}`}) 117 | } else { 118 | instances[index].enabled = requestBody.enabled 119 | fs.writeFile('src/registry/registry.json', JSON.stringify(registry), (error) => { 120 | if (error) { 121 | logger.error(`enableInstance - Could not enable/disable ${requestBody.url} for service ${serviceName} : ${error}`); 122 | 123 | return res.status(400).json({ message: `Could not enable/disable ${requestBody.url} for service ${serviceName} : ${error}`}) 124 | } else { 125 | // write to dist folder too 126 | fs.writeFile('dist/registry/registry.json', JSON.stringify(registry), (_error) => { 127 | }) 128 | logger.info(`enableInstance - Succeessfully enabled/disabled ${requestBody.url} for service ${serviceName}`); 129 | 130 | return res.status(200).json({ message: `Succeessfully ${requestBody.enabled == true ? 'enabled' : 'disabled'} ${requestBody.url} for service ${serviceName}`, data: registryData.services[serviceName].instances[index] }) 131 | } 132 | }) 133 | } 134 | return 135 | }) 136 | 137 | router.post('/gateway/service/register', async (req, res) => { 138 | const requestBody = req.body 139 | const schema = joi.object().keys({ 140 | serviceName: joi.string().required(), 141 | protocol: joi.string().required(), 142 | host: joi.string().required(), 143 | port: joi.number().required(), 144 | healthUrl: joi.string().required(), 145 | }); 146 | 147 | const validation = schema.validate(requestBody); 148 | if (validation.error) { 149 | logger.error(`registerService - registering service failed due to validation error - ${validation.error.details[0].message}`); 150 | 151 | return res.status(400).json({ 152 | message: validation.error.details[0].message, 153 | }); 154 | } 155 | 156 | 157 | requestBody.url = requestBody.protocol + "://" + requestBody.host + ":" + requestBody.port + "/" 158 | requestBody.enabled = true 159 | 160 | // check if healthUrl contains http 161 | if(requestBody.healthUrl.search('http') != 0) { 162 | 163 | logger.error(`registerService - Please input full path health url for ${requestBody.url}`); 164 | 165 | return res.status(400).json({ message: `Please input full path health url for ${requestBody.url}`}) 166 | } 167 | 168 | // ping health endpoint to see if it is available 169 | const pingHealth = await ping.isAlive(requestBody.healthUrl) 170 | 171 | if(pingHealth === false) { 172 | logger.error(`registerService - Health url is not reachable for ${requestBody.url}`); 173 | 174 | return res.status(400).json({ message: `Health url is not reachable for ${requestBody.url}`}) 175 | } 176 | 177 | if (serviceAlreadyExists(requestBody) == true) { 178 | logger.error(`registerService - Configuration already exists for ${requestBody.serviceName} at ${requestBody.url}`); 179 | 180 | return res.status(400).json({ message: `Configuration already exists for ${requestBody.serviceName} at ${requestBody.url}`}) 181 | } else { 182 | const currentServicesData = registryData.services 183 | if (registry.services.hasOwnProperty(requestBody.serviceName) == false) { 184 | const newService = { 185 | [requestBody.serviceName.toLowerCase()]: { 186 | index: 0, 187 | loadBalanceStrategy: "ROUND_ROBIN", 188 | instances: [] 189 | } 190 | } 191 | registryData.services = Object.assign(currentServicesData, newService) 192 | registryData.services[requestBody.serviceName.toLowerCase()].instances.push({ ...requestBody }) 193 | }else{ 194 | registryData.services[requestBody.serviceName.toLowerCase()].instances.push({ ...requestBody }) 195 | 196 | } 197 | 198 | fs.writeFile('src/registry/registry.json', JSON.stringify(registryData), (error) => { 199 | if (error) { 200 | logger.error(`registerService - Could not register ${requestBody.serviceName}: ${error}`); 201 | 202 | return res.status(400).json({ message: `Could not register ${requestBody.serviceName}: ${error}`}) 203 | } else { 204 | // write to dist folder too 205 | fs.writeFile('dist/registry/registry.json', JSON.stringify(registryData), (_error) => { 206 | }) 207 | logger.info(`registerService - Successfully registered ${requestBody.serviceName}`); 208 | 209 | return res.status(200).json({ 210 | message: `Succeessfully registered ${requestBody.serviceName}`, 211 | data: registryData.services[requestBody.serviceName.toLowerCase()] }) 212 | } 213 | }) 214 | } 215 | return 216 | }) 217 | 218 | router.post('/gateway/service/unregister', (req, res) => { 219 | const requestBody = req.body 220 | 221 | const schema = joi.object().keys({ 222 | serviceName: joi.string().required(), 223 | url: joi.string().required(), 224 | }); 225 | 226 | const validation = schema.validate(requestBody); 227 | if (validation.error) { 228 | logger.error(`unregisterService - registering service failed due to validation error - ${validation.error.details[0].message}`); 229 | 230 | return res.status(400).json({ 231 | message: validation.error.details[0].message, 232 | }); 233 | } 234 | 235 | if (serviceAlreadyExists(requestBody)) { 236 | const index = registryData.services[requestBody.serviceName.toLowerCase()].instances.findIndex((instance: { url: any }) => { 237 | return requestBody.url === instance.url 238 | }) 239 | registryData.services[requestBody.serviceName.toLowerCase()].instances.splice(index, 1) 240 | fs.writeFile('src/registry/registry.json', JSON.stringify(registryData), (error) => { 241 | if (error) { 242 | logger.error(`unregisterService - Could not unregister ${requestBody.serviceName}: ${error}`); 243 | 244 | return res.status(400).json({ message: `Could not unregister ${requestBody.serviceName}: ${error}`}) 245 | } else { 246 | // write to dist folder too 247 | fs.writeFile('dist/registry/registry.json', JSON.stringify(registryData), (_error) => { 248 | }) 249 | 250 | logger.info(`unregisterService - Successfully unregistered ${requestBody.serviceName}`); 251 | 252 | return res.status(200).json({ message: `Successfully unregistered ${requestBody.serviceName}`}) 253 | } 254 | }) 255 | } else { 256 | logger.error(`unregisterService - Service does not exist for ${requestBody.serviceName} at ${requestBody.url}`); 257 | 258 | return res.status(400).json({ message: `Service does not exist for ${requestBody.serviceName} at ${requestBody.url}`}) 259 | } 260 | return 261 | }) 262 | 263 | router.get('/gateway/service/all', (_req, res) => { 264 | logger.info(`getAllServices - All services returned successfully`); 265 | 266 | return res.status(200).json({ message: `All services returned successfully`, data: registryData.services }) 267 | }) 268 | 269 | router.get('/gateway/service/:serviceName', (req, res) => { 270 | if (registry.services.hasOwnProperty(req.params.serviceName.toLowerCase()) == true ) { 271 | const instances = registryData.services[req.params.serviceName.toLowerCase()].instances 272 | logger.info(`getServiceInstances - Instances fetced successfully`); 273 | 274 | return res.status(200).json({ message: `Service instances returned successfully`, data: instances }) 275 | } else { 276 | logger.error(`getServiceInstances - Service name not found`); 277 | 278 | return res.status(404).json({ 279 | message: 'Service name not found', 280 | }); 281 | } 282 | }) 283 | 284 | // router.post('/gateway/user/authenticate/refresh-token', async (req, res) => { 285 | // const token = req.body.token 286 | 287 | // // call axios 288 | // try { 289 | // const newToken = await refreshToken(token) 290 | 291 | // logger.info(`refreshToken - Successfully refreshed token`); 292 | 293 | // return res.status(200).json({ 294 | // message: 'Token refreshed successfully', 295 | // token: newToken, 296 | // }) 297 | 298 | // } catch (error: any) { 299 | // logger.error(`refreshToken - ${error.message}`); 300 | 301 | // return res.status(400).json({message: `${error.message}`}) 302 | // } 303 | // }) 304 | 305 | 306 | router.all('/:serviceName/:path/:sl1?/:sl2?/:sl3?/:sl4?/:sl5?/:sl6?/:sl7?/:sl8?', async (req, res) => { 307 | if (registry.services.hasOwnProperty(req.params.serviceName.toLowerCase()) == false) { 308 | logger.error(`${req.params.serviceName.toLowerCase()} service not found in registry`); 309 | 310 | return res.status(404).json({ 311 | message: `${req.params.serviceName.toLowerCase()} service not found in registry`, 312 | }); 313 | } 314 | const service = registryData.services[req.params.serviceName] 315 | 316 | // check if any active instance for this server 317 | let servInstances = service.instances.find( (instance: { [x: string]: boolean }) => instance['enabled'] === true ); 318 | 319 | if(!servInstances) { 320 | logger.error(`gatewayRouting - No active instance for service ${req.params.serviceName}`); 321 | 322 | return res.status(400).json({message: `No active instance for service ${req.params.serviceName}`}) 323 | 324 | } 325 | 326 | if (service) { 327 | if (!service.loadBalanceStrategy) { 328 | service.loadBalanceStrategy = 'ROUND_ROBIN' 329 | fs.writeFile('src/registry/registry.json', JSON.stringify(registryData), (error) => { 330 | if (error) { 331 | logger.error(`gatewayRouting - Couldn't write load balance strategy: ${error}`); 332 | 333 | return res.status(400).json({message: `Couldn't write load balance strategy: ${error}`}) 334 | } 335 | 336 | // write to dist folder too 337 | fs.writeFile('dist/registry/registry.json', JSON.stringify(registryData), (_error) => { 338 | }) 339 | return 340 | }) 341 | } 342 | 343 | const trailingUrlChain1 = req.params.sl1 ? '/' + req.params.sl1 : '' 344 | const trailingUrlChain2 = req.params.sl2 ? '/' + req.params.sl2 : '' 345 | const trailingUrlChain3 = req.params.sl3 ? '/' + req.params.sl3 : '' 346 | const trailingUrlChain4 = req.params.sl4 ? '/' + req.params.sl4 : '' 347 | const trailingUrlChain5 = req.params.sl5 ? '/' + req.params.sl5 : '' 348 | const trailingUrlChain6 = req.params.sl6 ? '/' + req.params.sl6 : '' 349 | const trailingUrlChain7 = req.params.sl7 ? '/' + req.params.sl7 : '' 350 | const trailingUrlChain8 = req.params.sl8 ? '/' + req.params.sl8 : '' 351 | 352 | const newIndex = loadbalancerData[service.loadBalanceStrategy](service) 353 | const url = `${service.instances[newIndex].url}${req.params.serviceName.toLowerCase()}/` 354 | const method = req.method 355 | const query = req.query as unknown as string 356 | const apiUrl = `${url}${req.params.path}${trailingUrlChain1}${trailingUrlChain2}${trailingUrlChain3}${trailingUrlChain4}${trailingUrlChain5}${trailingUrlChain6}${trailingUrlChain7}${trailingUrlChain8}` 357 | const apiBody = req.body 358 | const headers = req.headers 359 | const apiUrlQuery = appendQuery(apiUrl, query) 360 | 361 | // call axios 362 | try { 363 | const response = await axiosCall(method, apiUrlQuery, apiBody, headers) 364 | logger.info(`gatewayRouting - Successfully routed request: method ${method}, url: ${apiUrl}`); 365 | 366 | return res.status(response.status).json(response.data) 367 | } catch (error: any) { 368 | if(error.code == 'ECONNREFUSED') { 369 | // turn the service inactive as it is unreachable 370 | service.instances[newIndex].enabled = false 371 | fs.writeFile('src/registry/registry.json', JSON.stringify(registry), (error) => { 372 | if (error) { 373 | logger.error(`gatewayRouting - Could not disable ${url} for service ${service.instances[newIndex].serviceName} : ${error}`); 374 | } else { 375 | // write to dist folder too 376 | fs.writeFile('dist/registry/registry.json', JSON.stringify(registry), (_error) => { 377 | }) 378 | logger.info(`gatewayRouting - Successfully disabled ${url} for service ${service.instances[newIndex].serviceName}`); 379 | } 380 | }) 381 | logger.error(`gatewayRouting - An error occured: service ${url} is unreachable`); 382 | 383 | return res.status(400).json({message: `An error occured: service ${url} is unreachable`}) 384 | 385 | } 386 | logger.error(`gatewayRouting - ${error.message}`); 387 | 388 | return res.status(400).json({message: `${error.message}`}) 389 | } 390 | } else { 391 | logger.error(`gatewayRouting - Service name ${req.params.serviceName} does not exist`); 392 | 393 | return res.status(400).json({message: `Service name ${req.params.serviceName} does not exist`}) 394 | } 395 | }) 396 | 397 | const serviceAlreadyExists = (checkInfo: { serviceName: string | number; url: any; }) => { 398 | let exists = false 399 | 400 | if (registry.services.hasOwnProperty(checkInfo.serviceName) == false) { 401 | exists = false 402 | }else{ 403 | registryData.services[checkInfo.serviceName].instances.forEach((instance: { url: any; enabled: boolean }) => { 404 | if (instance.url === checkInfo.url && instance.enabled === true) { 405 | exists = true 406 | return 407 | }else{ 408 | exists = false 409 | } 410 | }) 411 | } 412 | return exists 413 | } 414 | 415 | export { router }; --------------------------------------------------------------------------------