247 lines
8.9 KiB
TypeScript
247 lines
8.9 KiB
TypeScript
'use strict'
|
|
|
|
import { getLogger, Logger, } from "log4js";
|
|
import { promises as fs, mkdirSync } from "fs"
|
|
import { RPCServer } from 'rpclibrary'
|
|
import * as Path from 'path'
|
|
import * as Knex from 'knex';
|
|
import * as http from 'http';
|
|
import * as express from 'express';
|
|
import { existsSync } from "fs";
|
|
import { GuildManager } from '../Components/Guild/GuildManager';
|
|
import { ItemManager } from '../Components/Item/ItemManager';
|
|
import { RaidManager } from '../Components/Raid/RaidManager';
|
|
import { CharacterManager } from '../Components/Character/CharacterManager';
|
|
import { UserManager } from '../Components/User/UserManager';
|
|
import { RootComponent } from '../Injector/ServiceDecorator';
|
|
import { TableDefinitionExporter } from '../Types/Interfaces';
|
|
import { AdminConf, TableDefiniton, SomeOf } from '../Types/Types';
|
|
import { RPCConfigLoader } from '../Components/RPCConfigLoader';
|
|
import { FrontworkComponent } from '../Types/FrontworkComponent';
|
|
import { IAdmin } from './Interface';
|
|
import { Injector } from '../Injector/Injector';
|
|
import { PubSub } from '../Components/PubSub/PubSub';
|
|
import { BurstyRateLimiter, RateLimiterMemory, RateLimiterAbstract } from 'rate-limiter-flexible'
|
|
|
|
|
|
@RootComponent({
|
|
injectable: IAdmin,
|
|
injects: [
|
|
GuildManager,
|
|
ItemManager,
|
|
RaidManager,
|
|
CharacterManager,
|
|
UserManager,
|
|
PubSub
|
|
]
|
|
})
|
|
export class FrontworkAdmin
|
|
implements TableDefinitionExporter, IAdmin {
|
|
knex: Knex
|
|
config: RPCConfigLoader<AdminConf>
|
|
rpcServer: RPCServer
|
|
|
|
private express : express.Application
|
|
private httpServer
|
|
|
|
constructor(private frontworkComponents: FrontworkComponent[] = []) {
|
|
this.config = new RPCConfigLoader<AdminConf>({
|
|
name: "FrontworkAdminConf",
|
|
getDefaultConfig: () => {
|
|
return {
|
|
httpPort: 8080,
|
|
eventBusConf: {},
|
|
dbConf: {
|
|
client: 'sqlite3',
|
|
connection: {
|
|
filename: Path.resolve(__dirname, '../../../..', "data/frontworkAdmin.sqlite"),
|
|
},
|
|
migrations: {
|
|
directory: Path.resolve(__dirname, '../../../..', "migrations"),
|
|
extension: 'ts'
|
|
},
|
|
useNullAsDefault: true,
|
|
}
|
|
}
|
|
}
|
|
}, './config', this.configChangeHandler)
|
|
}
|
|
|
|
async start() {
|
|
const ONE_REQUEST = 15
|
|
let port: number = this.config.getConfig().httpPort
|
|
const rateLimiter = new BurstyRateLimiter(
|
|
new RateLimiterMemory({
|
|
points: 2*ONE_REQUEST,
|
|
duration: 1,
|
|
}),
|
|
new RateLimiterMemory({
|
|
keyPrefix: 'burst',
|
|
points: 5*ONE_REQUEST,
|
|
duration: 60,
|
|
})
|
|
)
|
|
|
|
await this.makeKnex()
|
|
const app = await this.makeExpress(rateLimiter)
|
|
const httpServer = new http.Server(app)
|
|
await this.startWebsocket(httpServer)
|
|
httpServer.listen(port)
|
|
await this.attachAngularSSR(app, rateLimiter)
|
|
getLogger('Admin#startWebsocket').debug("Webserver up on", port)
|
|
|
|
await Promise.all(this.frontworkComponents.map(c => c.initialize ? c.initialize() : undefined))
|
|
getLogger('Admin#start').debug(this.frontworkComponents.length + " components initialized")
|
|
}
|
|
|
|
stop() {
|
|
Promise.all([
|
|
...this.frontworkComponents.map(c => c.stop ? c.stop() : undefined),
|
|
])
|
|
.catch(e => getLogger('Admin#stop').warn(e))
|
|
.finally(() => {
|
|
this.rpcServer.close()
|
|
process.exit(0)
|
|
})
|
|
}
|
|
|
|
protected configChangeHandler = (conf: AdminConf, key?: string) => {
|
|
if (key === 'dbConf') {
|
|
this.makeKnex()
|
|
}
|
|
}
|
|
|
|
getConfigKey(key: string) {
|
|
return this.config.getConfigKey(key)
|
|
}
|
|
|
|
setConfigKey(key: string, value: any) {
|
|
return this.config.setConfigKey(key, value)
|
|
}
|
|
|
|
getTableDefinitions(): TableDefiniton[] {
|
|
return [
|
|
...this.frontworkComponents
|
|
]
|
|
.filter(exp => exp.getTableDefinitions != null)
|
|
.flatMap(exp => exp.getTableDefinitions())
|
|
}
|
|
|
|
private startWebsocket(httpServer: http.Server) {
|
|
this.rpcServer = new RPCServer([
|
|
...this.frontworkComponents,
|
|
{
|
|
name: "debug",
|
|
RPCs: () => [{
|
|
name: 'dumpDb',
|
|
call: async (table) => await this.knex(table).select('*')
|
|
}]
|
|
}
|
|
], {
|
|
errorHandler: (sock, err, rpc, args) => {
|
|
getLogger("startWebsocket#errorHandler").error("RPC", rpc)
|
|
getLogger("startWebsocket#errorHandler").error("ERR", err)
|
|
getLogger("startWebsocket#errorHandler").error("ARGS", args)
|
|
}
|
|
})
|
|
.attach(httpServer)
|
|
}
|
|
|
|
private async makeExpress(rateLimiter : SomeOf<RateLimiterAbstract> = new RateLimiterMemory({ points: 6, duration: 3 })) {
|
|
if (this.httpServer != null || this.express != null) {
|
|
getLogger('Admin#startWebserver').warn("Webserver is already running")
|
|
return
|
|
}
|
|
|
|
this.express = express()
|
|
const distFolder = "../../../../dist"
|
|
this.express.get('*.*', (req, res) => {
|
|
rateLimiter.consume(req.ip)
|
|
.then(_ => {
|
|
const filepath = Path.join(__dirname, distFolder, 'browser', decodeURIComponent(req.path))
|
|
if(!existsSync(filepath)){
|
|
getLogger('Admin#startWebserver#serveFile').error(String(new Error("404 BAD REQUEST "+filepath+ " FROM "+req.ip)))
|
|
res.send("404 Not found")
|
|
res.status(404)
|
|
return
|
|
}
|
|
res.sendFile(filepath)
|
|
res.status(200)
|
|
})
|
|
.catch(_ => {
|
|
getLogger('Admin#startWebserver#serveFile').error(String(new Error("429 BAD REQUEST "+req.path+" FROM "+req.ip)))
|
|
res.send('429 Too many requests')
|
|
res.status(429)
|
|
})
|
|
})
|
|
|
|
return this.express
|
|
}
|
|
|
|
attachAngularSSR = async (express : express.Application, rateLimiter : SomeOf<RateLimiterAbstract> = new RateLimiterMemory({ points: 6, duration: 3 })) => {
|
|
const distFolder = "../../../../dist"
|
|
const ngExpressServer = Path.join(__dirname, distFolder, 'server.js')
|
|
let port: number = this.config.getConfig().httpPort
|
|
|
|
try {
|
|
const req = require(distFolder + "/server.js")
|
|
await req.attachExpress(express, './dist', getLogger('angularSSR#'), rateLimiter)
|
|
getLogger('Admin#startWebserver').debug('Frontend from ' + ngExpressServer + " loaded")
|
|
} catch (e) {
|
|
getLogger('Admin#startWebserver').error(e)
|
|
getLogger('Admin#startWebserver').warn("No angular SSR module was provided in " + ngExpressServer)
|
|
getLogger('Admin#startWebserver').warn("This is not fatal, but your page will not render on *" + port)
|
|
}
|
|
}
|
|
|
|
private stopWebserver() {
|
|
if (this.httpServer == null || this.express == null) {
|
|
getLogger('Admin#stopWebserver').warn("Webserver is not running")
|
|
return
|
|
}
|
|
this.httpServer.close()
|
|
this.httpServer = null
|
|
this.express = null
|
|
getLogger('Admin#stopWebserver').info("Webserver stopped")
|
|
}
|
|
|
|
async makeKnex(): Promise<Knex> {
|
|
const conf: Knex.Config = this.config.getConfigKey("dbConf")
|
|
|
|
getLogger('Admin#makeKnex').debug("Making new knex:", conf)
|
|
if (conf.client === 'sqlite3') {
|
|
mkdirSync(Path.dirname((<any>conf.connection).filename), { recursive: true })
|
|
}
|
|
|
|
this.knex = Knex(conf)
|
|
if (conf.client === 'sqlite3') {
|
|
await this.knex.raw('PRAGMA foreign_keys = ON');
|
|
|
|
}
|
|
|
|
await Promise.all(
|
|
this.getTableDefinitions()
|
|
//make unique by name
|
|
.filter((other, index, self) => index === self.findIndex(
|
|
(self) => self.name === other.name)
|
|
)
|
|
//create table if not exists
|
|
.map(async (def) => {
|
|
const hasTable = await this.knex.schema.hasTable(def.name)
|
|
if (!hasTable)
|
|
return await this.knex.schema.createTable(def.name, def.tableBuilder)
|
|
})
|
|
)
|
|
await this.knex.migrate.latest().catch(e => {
|
|
getLogger('Admin#makeKnex').error(e)
|
|
process.exit(-1)
|
|
})
|
|
return this.knex
|
|
}
|
|
}
|
|
|
|
process.on('SIGINT', function () {
|
|
getLogger('process#SIGINT').info("Shutting down from SIGINT (Ctrl-C)");
|
|
Injector.resolve<FrontworkAdmin>(FrontworkAdmin).stop()
|
|
})
|