///
import expressMod = require("express")
import morgan = require("morgan")
import jwt = require("jsonwebtoken")
import { Server as NextServer } from "next"
import rimraf = require("rimraf")
import { promisify } from "util"
import { resolve } from "path"
import { RouterBuilder } from "./router-builder"
import helmet = require("helmet")
import { setWithLanguage } from "../messages/messages"
import { fontPlugin } from "./font-plugin"
import http = require("http")
export type ExpressApp = ReturnType
declare const module: any
export class Server {
static __tag__ = "SERVER"
nodeHttpServer?: http.Server
expressApp?: ExpressApp
isProduction = process.env.NODE_ENV === "production"
constructor(public ctx: Nextpress.Context, public opts: { tag?: string } = {}) {
if (!ctx.loadedContexts.has("default.website")) {
throw Error("Server requires the default.website context to be used.")
}
setWithLanguage(ctx.website.language)
}
options = {
errorRoute: "/error",
useNextjs: true,
useHelmet: true,
jwtOptions: {
tokenHeader: "authorization",
tokenDuration: 60 * 60 * 24 * 5 //5 days
},
bundleAnalyzer: {
analyzeServer: false,
analyzeBrowser: true
}
}
static getDefaultContext(): Nextpress.Context {
throw Error(
"No default context set. Please declare a static getDefaultContext() on the server class."
)
}
useHMR() {
const hmr = require("./hmr") as typeof import("./hmr")
hmr.setServerHmr(this)
}
/**
* all set, run
*/
async run() {
// if (this.isProduction && this.options.useNextjs && !this.ctx.website.isPrebuilt) {
// await this.buildForProduction()
// }
this.expressApp = expressMod()
;(this.expressApp as any).__nextpress = true
await this.setupGlobalMiddleware(this.expressApp)
await this.setupRoutes({ app: this.expressApp })
if (!this.nodeHttpServer) {
this.nodeHttpServer = http.createServer(this.expressApp)
this.nodeHttpServer.listen(this.ctx.website.port)
console.log("Server running on " + this.ctx.website.port)
} else {
let listeners = this.nodeHttpServer!.listeners("request")
for (let x = 0; x < listeners.length; x++) {
const listener = listeners[x]
if ((listener as any).__nextpress) {
this.nodeHttpServer.removeListener("request", listener as any)
}
}
this.nodeHttpServer.addListener("request", this.expressApp)
}
}
/**
* app.use's on the express app
*/
async setupGlobalMiddleware(expressApp: expressMod.Router) {
if (this.options.useNextjs) {
this.getNextApp() //.prepare()
}
if (this.ctx.website.logRequests) {
expressApp.use(morgan("short"))
}
if (this.ctx.website.useCompression && this.isProduction) {
const compression = require("compression")
expressApp.use(compression())
}
if (this.isProduction) {
expressApp.use(
"/static",
expressMod.static(resolve(this.ctx.projectRoot, "static"), {
maxAge: "30d"
})
)
}
if (this.ctx.jwt) {
const authMw = this.createAuthMw_Jwt()
expressApp.use(authMw)
}
const robotsPath = resolve(this.ctx.projectRoot, "static", "robots.txt")
expressApp.get("/robots.txt", (_, response) => {
response.sendFile(robotsPath)
})
if (this.options.useHelmet) {
expressApp.use(helmet())
}
return expressApp
}
_nextApp?: NextServer
getNextApp() {
if (!this._nextApp) {
if (!this.options.useNextjs) {
throw Error("options.useNextJs is set to false.")
}
const nextjs = require("next") as typeof import("next")
this._nextApp = nextjs({
dev: !this.isProduction,
dir: this.ctx.projectRoot,
conf: this.getNextjsConfig()
})
this._nextApp.prepare()
}
return this._nextApp
}
async buildForProduction() {
console.log("Building for production...")
const nextBuild = require("next/dist/build").default
await promisify(rimraf)(resolve(this.ctx.projectRoot, ".next"))
await nextBuild(this.ctx.projectRoot, this.getNextjsConfig())
if (global.gc) {
global.gc()
}
}
/**
* this is meant to be overriden in order to set the server routes.
*/
async setupRoutes({ app }: { app: ExpressApp }): Promise {
const builder = new RouterBuilder(this)
app.use(await builder.createHtmlRouter())
}
/**
* the next.config.js
*/
getNextjsConfig() {
const withCSS = require("@zeit/next-css")
const withSass = require("@zeit/next-sass")
const LodashPlugin = require("lodash-webpack-plugin")
const withTypescript = require("@zeit/next-typescript")
let that = this
const opts = {
webpack(config: any, options: any) {
// Do not run type checking twice:
if (options.isServer && !that.isProduction) {
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin")
config.plugins.push(new ForkTsCheckerWebpackPlugin())
}
config.plugins.push(new LodashPlugin())
return config
}
}
let out = this.isProduction
? withTypescript(withCSS(withSass(opts)))
: withSass(withTypescript(withCSS(opts)))
if (this.ctx.website.bundleAnalyzer) {
const withBundleAnalyzer = require("@zeit/next-bundle-analyzer")
out = withBundleAnalyzer({ ...out, ...this.options.bundleAnalyzer })
}
return fontPlugin(out)
}
createAuthMw_Jwt() {
const out: expressMod.RequestHandler = (req, _, next) => {
req.nextpressAuth = new UserAuthJwt(req, {
headerKey: this.options.jwtOptions.tokenHeader,
durationSeconds: this.options.jwtOptions.tokenDuration,
secret: this.ctx.jwt.secret
})
next()
}
return out
}
}
interface User {
id: number
email: string
}
export class UserAuthSession {
constructor(public req: any) {}
async getUser(): Promise {
return (this.req.session && this.req.session.user) || undefined
}
async setUser(user: User): Promise {
if (!this.req.session) {
throw Error("Session not present.")
}
this.req.session.user = user
return ""
}
async logout() {
await new Promise(res => {
this.req.session.destroy(res)
})
}
}
export class UserAuthJwt implements UserAuthSession {
constructor(
public req: any,
private opts: { headerKey: string; secret: string; durationSeconds: number }
) {}
private _user: User | undefined
async getUser() {
if (this._user) return this._user
const token: string = this.req.headers[this.opts.headerKey.toLowerCase()]
if (!token || token === "undefined") return undefined
const decoded = await new Promise((resolve, reject) => {
jwt.verify(token, this.opts.secret, (err: any, resp) => {
if (err) {
err.statusCode = 401
return reject(err)
}
return resolve(resp)
})
})
return decoded
}
async setUser(user: User) {
if (this._user) throw Error("User already set.")
const token = await new Promise((resolve, reject) => {
jwt.sign(user, this.opts.secret, { expiresIn: this.opts.durationSeconds }, (err, token) => {
if (err) return reject(err)
return resolve(token)
})
})
this._user = user
return token
}
async logout() {
this._user = undefined
}
}
if (!process.listenerCount("unhandledRejection")) {
process.on("unhandledRejection", (...arg: any[]) => {
console.error("unhandledRejection", ...arg)
process.exit(1)
})
}
declare global {
namespace Express {
interface Request {
nextpressAuth: UserAuthSession | UserAuthJwt
}
}
}