UNPKG

7.46 kBPlain TextView Raw
1/// <reference path="../typings/untyped-modules.d.ts"/>
2/// <reference path="../typings/request.d.ts"/>
3
4import {
5 ModelOpts,
6 NavigationOpts,
7 ThumbnailProvider,
8 BaseUrls,
9 ContentHooks
10} from "../typings";
11import express, {
12 Request,
13 Response,
14 NextFunction,
15 RequestHandler,
16 Express
17} from "express";
18import promiseRouter from "express-promise-router";
19import * as path from "path";
20import { resolve as resolveUrl } from "url";
21import * as fs from "fs-extra";
22import log from "./log";
23import session from "./session";
24
25import buildModels from "./model";
26import filterModels, { createModelFilter } from "./model/filterModels";
27
28import { buildInfo } from "./model/navigationBuilder";
29
30import persistence from "./persistence";
31
32import icons from "./icons";
33
34import Auth, { AnonymousPermissions } from "./auth";
35import withAuth from "./auth/withAuth";
36import Content, { getRestApiBuilder as createRestApiBuilder } from "./content";
37import Settings from "./settings";
38import Media from "./media";
39
40import apiBuilder from "./api/apiBuilder";
41import swaggerUi from "./api/swaggerUi";
42import HttpError from "./HttpError";
43import { PersistenceAdapter } from "./persistence/adapter";
44import {
45 provide as provideExternalDataSourceHelper,
46 ExternalDataSourceWithOptionalHelper
47} from "./externalDataSourceHelper";
48import ContentPersistence from "./persistence/ContentPersistence";
49import Storage from "./media/storage/Storage";
50
51type SessionOpts = CookieSessionInterfaces.CookieSessionOptions;
52
53export { Persistence } from "./persistence";
54export { default as knexAdapter } from "./persistence/adapter/knex";
55export * from "../typings";
56export { default as FsStorage } from "./media/storage/FsStorage";
57
58export * from "./utils";
59export {
60 PersistenceAdapter,
61 Storage,
62 ExternalDataSourceWithOptionalHelper,
63 SessionOpts,
64 RequestHandler,
65 AnonymousPermissions,
66 ContentPersistence,
67 log
68};
69
70export type Opts = {
71 models: ModelOpts[];
72 navigation?: NavigationOpts[];
73 storage: Storage;
74 baseUrls?: Partial<BaseUrls>;
75 basePath?: string;
76 persistenceAdapter: Promise<PersistenceAdapter>;
77 externalDataSources?: ExternalDataSourceWithOptionalHelper[];
78 sessionOpts?: SessionOpts;
79 thumbnailProvider: ThumbnailProvider;
80 clientMiddleware?: RequestHandler | RequestHandler[];
81 anonymousPermissions?: AnonymousPermissions;
82 customSetup?: (app: Express, contentPersistence: ContentPersistence) => void;
83 contentHooks?: ContentHooks;
84};
85
86const root = path.resolve(__dirname, "../dist/client");
87let index: string;
88
89function getIndexHtml(basePath: string) {
90 if (!index) index = fs.readFileSync(path.join(root, "index.html"), "utf8");
91 return index.replace(/"src\./g, `"${basePath}/static/src.`);
92}
93
94export const clientMiddleware = promiseRouter()
95 .use(
96 "/static",
97 express.static(root, {
98 maxAge: "1y", // cache all static resources for a year ...
99 immutable: true, // which is fine, as all resource URLs contain a hash
100 index: false // index.html will be served by the fallback middleware
101 }),
102 (_: Request, res: Response, next: NextFunction) => {
103 res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
104 next();
105 }
106 )
107 .use((req, res, next) => {
108 if (
109 (req.method === "GET" || req.method === "HEAD") &&
110 req.accepts("html")
111 ) {
112 const basePath = req.originalUrl.replace(/^(.*\/admin).*/, "$1");
113 res.send(getIndexHtml(basePath));
114 } else next();
115 });
116
117function addSlash(str: string) {
118 return `${str.replace(/\/$/, "")}/`;
119}
120
121function getUrls(opts: Pick<Opts, "basePath" | "baseUrls">) {
122 const basePath = (opts.basePath || "").replace(
123 new RegExp(path.posix.sep, "g"),
124 "/"
125 );
126 const baseUrls = {
127 cms: basePath,
128 ...(opts.baseUrls || {})
129 };
130
131 return {
132 basePath: addSlash(basePath),
133 baseUrls: {
134 ...baseUrls,
135 cms: addSlash(baseUrls.cms)
136 }
137 };
138}
139
140function getModels(
141 opts: Pick<Opts, "externalDataSources" | "models">,
142 baseUrls: BaseUrls
143) {
144 const externalDataSources = provideExternalDataSourceHelper(
145 opts.externalDataSources,
146 { baseUrls }
147 ).map(withAuth);
148
149 return {
150 models: buildModels(opts.models, externalDataSources),
151 externalDataSources
152 };
153}
154
155export async function getRestApiBuilder(
156 opts: Pick<Opts, "models" | "basePath" | "baseUrls" | "externalDataSources">
157) {
158 const { baseUrls } = getUrls(opts);
159 const { models } = getModels(opts, baseUrls);
160
161 return createRestApiBuilder(models, baseUrls);
162}
163
164export async function init(opts: Opts) {
165 const { baseUrls, basePath } = getUrls(opts);
166 const { models, externalDataSources } = getModels(opts, baseUrls);
167
168 const p = await persistence(models, await opts.persistenceAdapter, {
169 baseUrls,
170 contentHooks: opts.contentHooks
171 });
172 const auth = Auth(p, opts.anonymousPermissions);
173 const content = Content(p, models, externalDataSources, baseUrls);
174 const settings = Settings(p, models);
175 const media = Media(p, models, opts.storage, opts.thumbnailProvider);
176
177 const app = express();
178
179 app.use(express.json({ limit: "1mb" }));
180 app.use(session(opts.sessionOpts));
181
182 app.all("/status", (req, res) => {
183 res.json({
184 uptime: process.uptime(),
185 nodeVersion: process.version,
186 memory: process.memoryUsage(),
187 pid: process.pid
188 });
189 });
190
191 const router = promiseRouter();
192 app.use(basePath.replace(/\/$/, ""), router);
193
194 auth.routes(router); // login, principal, logout
195 media.routes(router); // static, thumbs
196 settings.routes(router); // admin/rest/settings
197 icons.routes(router); // icons
198
199 router.get("/admin/rest/info", async (req, res) => {
200 if (!req.principal || !req.principal.id) return res.json({});
201
202 const filteredModels = filterModels(models, req.principal);
203 const filter = createModelFilter(req.principal);
204 const filteredInfo = buildInfo(opts.navigation || [], models, filter);
205
206 res.json({
207 ...filteredInfo,
208 models: filteredModels,
209 baseUrls,
210 user: req.principal
211 });
212 });
213
214 router.get("/admin/rest/info/content", (req, res) => {
215 const filteredModels = filterModels(models, req.principal);
216 res.json(filteredModels.content.map(m => m.name));
217 });
218 router.get("/admin/rest/info/settings", (req, res) => {
219 const filteredModels = filterModels(models, req.principal);
220 res.json(filteredModels.settings.map(m => m.name));
221 });
222
223 auth.describe(apiBuilder);
224 media.describe(apiBuilder);
225 content.describe(apiBuilder);
226 settings.describe(apiBuilder);
227
228 router.get("/admin/rest/swagger.json", (req, res) => {
229 res.json(apiBuilder.getSpec());
230 });
231
232 router.use(
233 "/admin/rest/docs",
234 swaggerUi(
235 resolveUrl(baseUrls.cms, "admin/rest/docs/"),
236 resolveUrl(baseUrls.cms, "admin/rest/swagger.json")
237 )
238 );
239 router.get("/admin/rest", (req, res) =>
240 res.redirect(resolveUrl(baseUrls.cms, "admin/rest/docs"))
241 );
242
243 content.routes(router);
244
245 router.use("/admin", opts.clientMiddleware || clientMiddleware);
246
247 if (opts.customSetup) {
248 opts.customSetup(app, p.content);
249 }
250
251 app.get(basePath, (_, res) =>
252 res.redirect(resolveUrl(baseUrls.cms, "admin"))
253 );
254
255 app.use((err: Error, req: Request, res: Response, _: () => void) => {
256 if (err instanceof HttpError) {
257 res.status(err.status);
258 } else {
259 log.error(req.method, req.path, err);
260 res.status(500);
261 }
262 res.end(err.message);
263 return;
264 });
265
266 return { app, persistence: p };
267}