///
///
import {
ModelOpts,
NavigationOpts,
ThumbnailProvider,
ContentHooks,
ResponseHeaders,
ExternalDataSource,
BaseUrls
} from "../typings";
import express, {
Request,
Response,
NextFunction,
RequestHandler,
Express
} from "express";
import promiseRouter from "express-promise-router";
import * as path from "path";
import urlJoin from "url-join";
import * as fs from "fs-extra";
import log from "./log";
import session from "./session";
import buildModels from "./model";
import filterModels, { createModelFilter } from "./model/filterModels";
import { buildInfo } from "./model/navigationBuilder";
import createPersistence from "./persistence";
import icons from "./icons";
import Auth, { AnonymousPermissions } from "./auth";
import withAuth from "./auth/withAuth";
import Content, { getRestApiBuilder as createRestApiBuilder } from "./content";
import Settings from "./settings";
import Media from "./media";
import apiBuilder from "./api/apiBuilder";
import swaggerUi from "./api/swaggerUi";
import HttpError from "./HttpError";
import { PersistenceAdapter } from "./persistence/adapter";
import ContentPersistence from "./persistence/ContentPersistence";
import Storage from "./media/storage/Storage";
import logResponseTime from "./responseTimeLogger";
import MigrationContext from "./persistence/MigrationContext";
import proxyMiddleware from "http-proxy-middleware";
import { spawn } from "child_process";
import SettingsPersistence from "./persistence/SettingsPersistence";
type SessionOpts = CookieSessionInterfaces.CookieSessionOptions;
export { Persistence } from "./persistence";
export { default as knexAdapter } from "./persistence/adapter/knex";
export * from "../typings";
export { default as FsStorage } from "./media/storage/FsStorage";
export * from "./utils";
export {
PersistenceAdapter,
Storage,
SessionOpts,
RequestHandler,
AnonymousPermissions,
ContentPersistence,
MigrationContext,
log
};
export type Opts = {
models: ModelOpts[];
navigation?: NavigationOpts[];
storage: Storage;
basePath?: BaseUrls | string;
persistenceAdapter: Promise;
externalDataSources?: ExternalDataSource[];
sessionOpts?: SessionOpts;
responseHeaders?: ResponseHeaders;
thumbnailProvider: ThumbnailProvider;
clientMiddleware?: (basePath: string) => RequestHandler | RequestHandler[];
anonymousPermissions?: AnonymousPermissions;
customSetup?: (
app: Express,
contentPersistence: ContentPersistence,
settingsPersistence: SettingsPersistence
) => void;
contentHooks?: ContentHooks;
migrationDir?: string;
};
const root = path.resolve(__dirname, "../dist/client");
let index: string;
function getIndexHtml(basePath: string) {
if (!index) index = fs.readFileSync(path.join(root, "index.html"), "utf8");
return index.replace(/\/admin\//g, `${urlJoin(basePath, "/admin")}/`);
}
const startDevServer = () => {
process.stdout.write("Starting development server...\n");
const child = spawn(`npm run watch`, [], {
cwd: path.resolve(__dirname, "../client/"),
shell: true
});
child.stdout.on("data", (data: Buffer) => {
const string = data.toString();
if (string.includes("Compiled successfully!")) {
process.stdout.write("Development server updated!\n");
}
if (string.includes("Compiling...\n")) {
process.stdout.write("Development server compiling...\n");
}
if (
string.includes("Failed to compile.") ||
string.includes("TypeScript error")
) {
process.stderr.write(data);
}
});
child.stderr.on("data", data => {
process.stderr.write(data);
});
child.on("exit", data => {
process.stdout.write("Stopping development server!");
process.kill(1);
});
process.on("beforeExit", code => child.kill());
};
export const clientMiddleware = (basePath: string = "/") =>
process.env.DEVCLIENT // Use Proxy to Dev Server
? [
proxyMiddleware("/static", {
target: `http://localhost:4001`,
logLevel: "error",
changeOrigin: true
}),
proxyMiddleware(urlJoin(basePath, "/admin"), {
target: `http://localhost:4001`,
logLevel: "error",
changeOrigin: true,
pathRewrite: { [`^/${basePath}/`]: "/" }
})
]
: promiseRouter()
.use(
urlJoin(basePath, "/admin"),
express.static(root, {
maxAge: "1y", // cache all static resources for a year ...
immutable: true, // which is fine, as all resource URLs contain a hash
index: false // index.html will be served by the fallback middleware
}),
(_: Request, res: Response, next: NextFunction) => {
res.setHeader(
"Cache-Control",
"no-cache, no-store, must-revalidate"
);
next();
}
)
.use(urlJoin(basePath, "/admin"), (req, res, next) => {
if (
(req.method === "GET" || req.method === "HEAD") &&
req.accepts("html")
) {
res.send(getIndexHtml(basePath));
} else next();
});
function getModels(opts: Pick) {
const externalDataSources = (opts.externalDataSources || []).map(withAuth);
return {
models: buildModels(opts.models, externalDataSources),
externalDataSources
};
}
const getBaseURLS = (
basePath?: string | BaseUrls
): { cms: string; media: string; preview: string } => {
if (!basePath) {
return {
cms: "/",
media: urlJoin("/", "/media"),
preview: "/"
};
}
if (typeof basePath === "string") {
return {
cms: basePath,
media: urlJoin(basePath, "/media"),
preview: basePath
};
}
return {
cms: basePath.cms,
media: basePath.media ? basePath.media : urlJoin(basePath.cms, "/media"),
preview: basePath.preview || basePath.cms
};
};
export async function getRestApiBuilder(
opts: Pick
) {
const { basePath } = opts;
const { models } = getModels(opts);
return createRestApiBuilder(models, getBaseURLS(basePath).cms);
}
export async function init(opts: Opts) {
const { models, externalDataSources } = getModels(opts);
const {
basePath,
storage,
thumbnailProvider,
responseHeaders,
contentHooks,
migrationDir
} = opts;
const baseURLS = getBaseURLS(basePath);
const mediaUrl = baseURLS.media;
const persistence = await createPersistence(
models,
await opts.persistenceAdapter,
{
basePath: baseURLS.cms,
mediaUrl,
contentHooks,
migrationDir
}
);
const auth = Auth(persistence, opts.anonymousPermissions);
const content = Content({
persistence,
models,
externalDataSources,
basePath: baseURLS.cms,
mediaUrl,
responseHeaders
});
const settings = Settings(persistence, models);
const media = Media(
persistence,
models,
storage,
thumbnailProvider,
baseURLS.cms
);
const app = express();
app.use(express.json({ limit: "1mb" }));
app.use(session(opts.sessionOpts));
if (process.env.PERFORMANCE_LOGGING === "true") {
app.use(logResponseTime);
}
app.all("/status", (req, res) => {
res.json({
uptime: process.uptime(),
nodeVersion: process.version,
memory: process.memoryUsage(),
pid: process.pid
});
});
const router = promiseRouter();
app.use(baseURLS.cms.replace(/\/$/, ""), router);
auth.routes(router); // login, principal, logout
media.routes(router); // static, thumbs
settings.routes(router); // admin/rest/settings
icons.routes(router); // icons
router.get("/admin/rest/info", async (req, res) => {
if (!req.principal || !req.principal.id) return res.json({});
const filteredModels = filterModels(models, req.principal);
const filter = createModelFilter(req.principal);
const filteredInfo = buildInfo(opts.navigation || [], models, filter);
res.json({
...filteredInfo,
models: filteredModels,
baseUrls: baseURLS,
user: req.principal
});
});
// This routes purpose is to provide all content options
// for the MapInput inside the built-in roles models
router.get("/admin/rest/info/content", (req, res) => {
const filteredModels = filterModels(models, req.principal);
res.json(
filteredModels.content.map(m => ({ value: m.name, label: m.singular }))
);
});
router.get("/admin/rest/info/settings", (req, res) => {
const filteredModels = filterModels(models, req.principal);
res.json(filteredModels.settings.map(m => m.name));
});
auth.describe(apiBuilder);
media.describe(apiBuilder);
content.describe(apiBuilder);
settings.describe(apiBuilder);
router.get("/admin/rest/swagger.json", (req, res) => {
res.json(apiBuilder.getSpec());
});
router.use(
"/admin/rest/docs",
swaggerUi(
urlJoin(baseURLS.cms, "admin/rest/docs/"),
urlJoin(baseURLS.cms, "admin/rest/swagger.json")
)
);
router.get("/admin/rest", (req, res) =>
res.redirect(urlJoin(baseURLS.cms, "admin/rest/docs"))
);
content.routes(router);
if (process.env.DEVCLIENT) {
startDevServer();
}
app.use(
(opts.clientMiddleware && opts.clientMiddleware(baseURLS.cms)) ||
clientMiddleware(baseURLS.cms)
);
if (opts.customSetup) {
opts.customSetup(app, persistence.content, persistence.settings);
}
app.get(baseURLS.cms, (_, res) =>
res.redirect(urlJoin(baseURLS.cms, "admin"))
);
app.use((err: Error, req: Request, res: Response, _: () => void) => {
if (err instanceof HttpError) {
res.status(err.status);
} else {
log.error(req.method, req.path, err);
res.status(500);
}
res.end(err.message);
return;
});
return { app, persistence };
}