express-api/src/express.ts
import 'dotenv/config.js';
import express, { Application, RequestHandler } from 'express';
import cookieParser from 'cookie-parser';
import compression from 'compression';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
import { protectedRoute, sso } from '@bcgov/citz-imb-sso-express';
import router from '@/routes';
import middleware from '@/middleware';
import constants from '@/constants';
import { SSO_OPTIONS } from '@/middleware/keycloak/keycloakOptions';
import swaggerUi from 'swagger-ui-express';
import swaggerJSDoc from 'swagger-jsdoc';
import errorHandler from '@/middleware/errorHandler';
import { EndpointNotFound404 } from '@/constants/errors';
import nunjucks from 'nunjucks';
import OPENAPI_OPTIONS from '@/swagger/swaggerConfig';
import userAuthCheck from '@/middleware/userAuthCheck';
import { Roles } from '@/constants/roles';
const app: Application = express();
const { TESTING, FRONTEND_URL } = constants;
// Express Rate Limiter Configuration
export const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 1000, // Limit each IP to 1000 requests per `window` (here, per 15 minutes)
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
// Use rate limiter if not testing
if (!TESTING) app.use(limiter);
// CORS Configuration
// TODO: Does localhost need to be specified?
const corsOptions = {
origin: [
'http://localhost:3000', // Local frontend testing
FRONTEND_URL, // Frontend
],
credentials: true,
};
// Incoming CORS Filter
app.use(cors(corsOptions));
// Express Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(compression());
// Swagger service route
app.use('/api-docs/', swaggerUi.serve, swaggerUi.setup(swaggerJSDoc(OPENAPI_OPTIONS)));
// Get Custom Middleware
const { headerHandler, morganMiddleware } = middleware;
// Logging Middleware
app.use(morganMiddleware);
// SSO initialization
sso(app, SSO_OPTIONS);
// Nunjucks configuration
const nj = nunjucks.configure('src/notificationTemplates', {
autoescape: true,
express: app,
noCache: true,
});
nj.addFilter(
'filterByAttr',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function (arr: Array<Record<string, any>>, property: string, value: any) {
return arr.filter((a) => a[property] === value);
},
);
// Set headers for response
app.use(`/v2`, headerHandler as RequestHandler);
// Unprotected Routes
app.use(`/v2/health`, router.healthRouter);
// Protected Routes
// userRequestCheck applied here if same permissions throughout route
// These routes must use protectedRoute before userAuthCheck
app.use(`/v2/ltsa`, protectedRoute(), userAuthCheck(), router.ltsaRouter);
app.use(
`/v2/administrativeAreas`,
protectedRoute(),
userAuthCheck({ requiredRoles: [Roles.ADMIN] }),
router.administrativeAreasRouter,
);
app.use(
`/v2/agencies`,
protectedRoute(),
userAuthCheck({ requiredRoles: [Roles.ADMIN] }),
router.agenciesRouter,
);
app.use('/v2/lookup', protectedRoute(), router.lookupRouter);
app.use(`/v2/users`, protectedRoute(), router.usersRouter);
app.use(`/v2/properties`, protectedRoute(), router.propertiesRouter);
app.use(`/v2/parcels`, protectedRoute(), userAuthCheck(), router.parcelsRouter);
app.use(`/v2/buildings`, protectedRoute(), userAuthCheck(), router.buildingsRouter);
app.use(`/v2/notifications`, protectedRoute(), router.notificationsRouter);
app.use(`/v2/projects`, protectedRoute(), router.projectsRouter);
app.use(`/v2/reports`, protectedRoute(), userAuthCheck(), router.reportsRouter);
app.use(`/v2/tools`, protectedRoute(), router.toolsRouter);
// If a non-existent route is called. Must go after other routes.
app.use('*', (_req, _res, next) => next(EndpointNotFound404));
// Request error handler. Must go last.
app.use(errorHandler);
export default app;