1 |
|
2 |
|
3 |
|
4 | import {
|
5 | ModelOpts,
|
6 | NavigationOpts,
|
7 | ThumbnailProvider,
|
8 | BaseUrls,
|
9 | ContentHooks
|
10 | } from "../typings";
|
11 | import express, {
|
12 | Request,
|
13 | Response,
|
14 | NextFunction,
|
15 | RequestHandler,
|
16 | Express
|
17 | } from "express";
|
18 | import promiseRouter from "express-promise-router";
|
19 | import * as path from "path";
|
20 | import { resolve as resolveUrl } from "url";
|
21 | import * as fs from "fs-extra";
|
22 | import log from "./log";
|
23 | import session from "./session";
|
24 |
|
25 | import buildModels from "./model";
|
26 | import filterModels, { createModelFilter } from "./model/filterModels";
|
27 |
|
28 | import { buildInfo } from "./model/navigationBuilder";
|
29 |
|
30 | import persistence from "./persistence";
|
31 |
|
32 | import icons from "./icons";
|
33 |
|
34 | import Auth, { AnonymousPermissions } from "./auth";
|
35 | import withAuth from "./auth/withAuth";
|
36 | import Content, { getRestApiBuilder as createRestApiBuilder } from "./content";
|
37 | import Settings from "./settings";
|
38 | import Media from "./media";
|
39 |
|
40 | import apiBuilder from "./api/apiBuilder";
|
41 | import swaggerUi from "./api/swaggerUi";
|
42 | import HttpError from "./HttpError";
|
43 | import { PersistenceAdapter } from "./persistence/adapter";
|
44 | import {
|
45 | provide as provideExternalDataSourceHelper,
|
46 | ExternalDataSourceWithOptionalHelper
|
47 | } from "./externalDataSourceHelper";
|
48 | import ContentPersistence from "./persistence/ContentPersistence";
|
49 | import Storage from "./media/storage/Storage";
|
50 |
|
51 | type SessionOpts = CookieSessionInterfaces.CookieSessionOptions;
|
52 |
|
53 | export { Persistence } from "./persistence";
|
54 | export { default as knexAdapter } from "./persistence/adapter/knex";
|
55 | export * from "../typings";
|
56 | export { default as FsStorage } from "./media/storage/FsStorage";
|
57 |
|
58 | export * from "./utils";
|
59 | export {
|
60 | PersistenceAdapter,
|
61 | Storage,
|
62 | ExternalDataSourceWithOptionalHelper,
|
63 | SessionOpts,
|
64 | RequestHandler,
|
65 | AnonymousPermissions,
|
66 | ContentPersistence,
|
67 | log
|
68 | };
|
69 |
|
70 | export 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 |
|
86 | const root = path.resolve(__dirname, "../dist/client");
|
87 | let index: string;
|
88 |
|
89 | function 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 |
|
94 | export const clientMiddleware = promiseRouter()
|
95 | .use(
|
96 | "/static",
|
97 | express.static(root, {
|
98 | maxAge: "1y",
|
99 | immutable: true,
|
100 | index: false
|
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 |
|
117 | function addSlash(str: string) {
|
118 | return `${str.replace(/\/$/, "")}/`;
|
119 | }
|
120 |
|
121 | function 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 |
|
140 | function 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 |
|
155 | export 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 |
|
164 | export 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);
|
195 | media.routes(router);
|
196 | settings.routes(router);
|
197 | icons.routes(router);
|
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 | }
|