You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
238 lines
8.3 KiB
TypeScript
238 lines
8.3 KiB
TypeScript
import * as compression from "compression";
|
|
import * as config from "config";
|
|
import * as cookieParser from "cookie-parser";
|
|
import * as cors from "cors";
|
|
import * as express from "express";
|
|
import {Request, Response} from "express";
|
|
import * as session from "express-session";
|
|
import sharedsession = require("express-socket.io-session");
|
|
import * as fsx from "fs-extra";
|
|
import * as http from "http";
|
|
import {IncomingMessage} from "http";
|
|
import * as httpStatus from "http-status";
|
|
import * as path from "path";
|
|
import * as redis from "redis";
|
|
import {RedisClient} from "redis";
|
|
import {Sequelize} from "sequelize-typescript";
|
|
import * as socketIo from "socket.io";
|
|
import * as socketIoRedis from "socket.io-redis";
|
|
import dataaccess from "./lib/dataAccess";
|
|
import globals from "./lib/globals";
|
|
import {GraphqlRoute} from "./routes/GraphqlRoute";
|
|
import HomeRoute from "./routes/HomeRoute";
|
|
import {UploadRoute} from "./routes/UploadRoute";
|
|
|
|
const SequelizeStore = require("connect-session-sequelize")(session.Store);
|
|
const createLimiter: (...args: any) => any = require("express-limiter");
|
|
const logger = globals.logger;
|
|
|
|
/**
|
|
* The main entry class for each cluster worker
|
|
*/
|
|
class App {
|
|
|
|
/**
|
|
* The corresponding express application
|
|
*/
|
|
public app: express.Application;
|
|
|
|
/**
|
|
* An instance of the socket.io server for websockets
|
|
*/
|
|
public io: socketIo.Server;
|
|
|
|
/**
|
|
* The instance of the redis client
|
|
*/
|
|
public redisClient: RedisClient;
|
|
|
|
/**
|
|
* The limiter for api requests
|
|
*/
|
|
public limiter: any;
|
|
|
|
/**
|
|
* An instance of the http server where the site is served
|
|
*/
|
|
public server: http.Server;
|
|
|
|
/**
|
|
* The path to the public folder that is served statically
|
|
*/
|
|
public readonly publicPath: string;
|
|
|
|
/**
|
|
* The id of the worker
|
|
*/
|
|
public readonly id?: number;
|
|
|
|
/**
|
|
* The instance of sequelize for ORM
|
|
*/
|
|
public readonly sequelize: Sequelize;
|
|
|
|
constructor(id?: number) {
|
|
this.id = id;
|
|
this.app = express();
|
|
this.redisClient = redis.createClient(null, null, {url: config.get("redis.connectionUri")});
|
|
this.limiter = createLimiter(this.app, this.redisClient);
|
|
this.server = new http.Server(this.app);
|
|
this.io = socketIo(this.server);
|
|
this.sequelize = new Sequelize(config.get("database.connectionUri"));
|
|
this.publicPath = config.get("frontend.publicPath");
|
|
if (!path.isAbsolute(this.publicPath)) {
|
|
this.publicPath = path.normalize(path.join(__dirname, this.publicPath));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* initializes everything that needs to be initialized asynchronous.
|
|
*/
|
|
public async init(): Promise<void> {
|
|
await dataaccess.init(this.sequelize);
|
|
|
|
const appSession = session({
|
|
cookie: {
|
|
maxAge: Number(config.get("session.cookieMaxAge")),
|
|
// @ts-ignore
|
|
secure: "auto",
|
|
},
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
secret: config.get("session.secret"),
|
|
store: new SequelizeStore({db: this.sequelize}),
|
|
});
|
|
|
|
await this.sequelize.sync({logging: (msg) => logger.silly(msg)});
|
|
this.sequelize.options.logging = (msg) => logger.silly(msg);
|
|
logger.info("Setting up socket.io");
|
|
try {
|
|
this.io.adapter(socketIoRedis(config.get("redis.connectionUri")));
|
|
} catch (err) {
|
|
logger.error(err.message);
|
|
logger.debug(err.stack);
|
|
}
|
|
this.io.use(sharedsession(appSession, {autoSave: true}));
|
|
|
|
logger.info("Configuring express app.");
|
|
this.server.setTimeout(config.get("server.timeout"));
|
|
this.app.set("views", path.join(__dirname, "views"));
|
|
this.app.set("view engine", "pug");
|
|
this.app.set("trust proxy", 1);
|
|
|
|
this.app.use(compression());
|
|
this.app.use(express.json());
|
|
this.app.use(express.urlencoded({extended: false}));
|
|
this.app.use(express.static(this.publicPath));
|
|
this.app.use(cookieParser());
|
|
this.app.use(appSession);
|
|
// enable cross origin requests if enabled in the config
|
|
if (config.get("server.cors")) {
|
|
this.app.use(cors());
|
|
}
|
|
// handle authentication via bearer in the Authorization header
|
|
this.app.use(async (req, res, next) => {
|
|
try {
|
|
if (!req.session.userId && req.headers.authorization) {
|
|
const bearer = req.headers.authorization.split("Bearer ")[1];
|
|
if (bearer) {
|
|
const user = await dataaccess.getUserByToken(bearer);
|
|
// @ts-ignore
|
|
req.session.userId = user.id;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
logger.error(err.message);
|
|
logger.debug(err.stack);
|
|
}
|
|
next();
|
|
});
|
|
this.app.use((req, res, next) => {
|
|
logger.verbose(`${req.method} ${req.url}`);
|
|
next();
|
|
});
|
|
|
|
// add custom routes
|
|
|
|
const uploadRoute = new UploadRoute(this.publicPath);
|
|
const homeRoute = new HomeRoute();
|
|
const graphqlRoute = new GraphqlRoute();
|
|
await uploadRoute.init();
|
|
await homeRoute.init(this.io);
|
|
await graphqlRoute.init();
|
|
|
|
this.app.use("/home", homeRoute.router);
|
|
this.limiter({
|
|
expire: config.get("api.rateLimit.upload.expire"),
|
|
lookup: ["connection.remoteAddress", "session.userId"],
|
|
method: "all",
|
|
onRateLimited: (req: IncomingMessage, res: any) => {
|
|
res.status(httpStatus.TOO_MANY_REQUESTS);
|
|
res.json({error: "Rate Limit Exceeded"});
|
|
},
|
|
path: "/upload",
|
|
total: config.get("api.rateLimit.upload.total"),
|
|
});
|
|
this.app.use("/upload", uploadRoute.router);
|
|
|
|
|
|
// listen for graphql requests
|
|
this.limiter({
|
|
expire: config.get("api.rateLimit.graphql.expire"),
|
|
lookup: ["connection.remoteAddress", "session.userId"],
|
|
method: "all",
|
|
onRateLimited: (req: IncomingMessage, res: any) => {
|
|
res.status(httpStatus.TOO_MANY_REQUESTS);
|
|
res.json({errors: [{message: "Rate Limit Exceeded"}]});
|
|
},
|
|
path: "/graphql",
|
|
total: config.get("api.rateLimit.graphql.total"),
|
|
});
|
|
this.app.use("/graphql", graphqlRoute.router);
|
|
|
|
// allow access to cluster information
|
|
this.app.use("/cluster-info", (req: Request, res: Response) => {
|
|
res.json({
|
|
id: this.id,
|
|
});
|
|
});
|
|
// redirect all request to the angular file
|
|
this.app.use((req: any, res: Response) => {
|
|
if (config.has("frontend.angularIndex")) {
|
|
const angularIndex = path.join(this.publicPath, config.get("frontend.angularIndex"));
|
|
if (fsx.existsSync(path.join(angularIndex))) {
|
|
res.sendFile(angularIndex);
|
|
} else {
|
|
res.status(httpStatus.NOT_FOUND);
|
|
res.render("errors/404.pug", {url: req.url});
|
|
}
|
|
} else {
|
|
res.status(httpStatus.NOT_FOUND);
|
|
res.render("errors/404.pug", {url: req.url});
|
|
}
|
|
});
|
|
// show an error page for internal errors
|
|
this.app.use((err, req: Request, res: Response) => {
|
|
res.status(httpStatus.INTERNAL_SERVER_ERROR);
|
|
res.render("errors/500.pug");
|
|
});
|
|
logger.info("Server configured.");
|
|
}
|
|
|
|
/**
|
|
* Starts the web server.
|
|
*/
|
|
public start(): void {
|
|
if (config.has("server.port")) {
|
|
logger.info(`Starting server...`);
|
|
this.app.listen(config.get("server.port"));
|
|
logger.info(`Server running on port ${config.get("server.port")}`);
|
|
} else {
|
|
logger.error("No port specified in the config." +
|
|
"Please configure a port in the config.yaml.");
|
|
}
|
|
}
|
|
}
|
|
|
|
export default App;
|