Files
outline/server/routes/app.ts
Apoorv Mishra 79829a3129 Ability to create share url slug (#4550)
* feat: share url slug

* feat: add col urlId

* feat: allow updating urlId

* fix: typo

* fix: migrations

* fix: urlId model validation

* fix: input label

* fix: debounce slug request

* feat: link preview

* fix: send slug variant in response if available

* fix: temporary redirect to slug variant if available

* fix: move up the custom link field

* fix: process and display backend err

* fix: reset custom link state on popover close and remove isCopied

* fix: document link preview

* fix: set urlId when available

* fix: keep unique(urlId, teamId)

* fix: codeql

* fix: get rid of preview type

* fix: width not needed for block elem

* fix: migrations

* fix: array not required

* fix: use val

* fix: validation on shareId and test

* fix: allow clearing urlId

* fix: do not escape

* fix: unique error text

* fix: keep team
2022-12-13 17:26:36 -08:00

130 lines
3.8 KiB
TypeScript

import fs from "fs";
import path from "path";
import util from "util";
import { Context, Next } from "koa";
import { escape } from "lodash";
import { Sequelize } from "sequelize";
import isUUID from "validator/lib/isUUID";
import documentLoader from "@server/commands/documentLoader";
import env from "@server/env";
import presentEnv from "@server/presenters/env";
import { getTeamFromContext } from "@server/utils/passport";
import prefetchTags from "@server/utils/prefetchTags";
const isProduction = env.ENVIRONMENT === "production";
const isTest = env.ENVIRONMENT === "test";
const readFile = util.promisify(fs.readFile);
let indexHtmlCache: Buffer | undefined;
const readIndexFile = async (ctx: Context): Promise<Buffer> => {
if (isProduction) {
return (
indexHtmlCache ??
(indexHtmlCache = await readFile(
path.join(__dirname, "../../app/index.html")
))
);
}
if (isTest) {
return (
indexHtmlCache ??
(indexHtmlCache = await readFile(
path.join(__dirname, "../static/index.html")
))
);
}
const middleware = ctx.devMiddleware;
await new Promise((resolve) => middleware.waitUntilValid(resolve));
return new Promise((resolve, reject) => {
middleware.fileSystem.readFile(
`${ctx.webpackConfig.output.path}/index.html`,
(err: Error, result: Buffer) => {
if (err) {
return reject(err);
}
resolve(result);
}
);
});
};
export const renderApp = async (
ctx: Context,
next: Next,
options: { title?: string; description?: string; canonical?: string } = {}
) => {
const {
title = "Outline",
description = "A modern team knowledge base for your internal documentation, product specs, support answers, meeting notes, onboarding, &amp; more…",
canonical = "",
} = options;
if (ctx.request.path === "/realtime/") {
return next();
}
const { shareId } = ctx.params;
const page = await readIndexFile(ctx);
const environment = `
window.env = ${JSON.stringify(presentEnv(env))};
`;
ctx.body = page
.toString()
.replace(/\/\/inject-env\/\//g, environment)
.replace(/\/\/inject-title\/\//g, escape(title))
.replace(/\/\/inject-description\/\//g, escape(description))
.replace(/\/\/inject-canonical\/\//g, canonical)
.replace(/\/\/inject-prefetch\/\//g, shareId ? "" : prefetchTags)
.replace(/\/\/inject-slack-app-id\/\//g, env.SLACK_APP_ID || "");
};
export const renderShare = async (ctx: Context, next: Next) => {
const { shareId, documentSlug } = ctx.params;
// Find the share record if publicly published so that the document title
// can be be returned in the server-rendered HTML. This allows it to appear in
// unfurls with more reliablity
let share, document;
try {
const team = await getTeamFromContext(ctx);
const result = await documentLoader({
id: documentSlug,
shareId,
teamId: team?.id,
});
share = result.share;
if (isUUID(shareId) && share && share.urlId) {
// Redirect temporarily because the url slug
// can be modified by the user at any time
ctx.redirect(`/s/${share.urlId}`);
ctx.status = 307;
}
document = result.document;
if (share && !ctx.userAgent.isBot) {
await share.update({
lastAccessedAt: new Date(),
views: Sequelize.literal("views + 1"),
});
}
} catch (err) {
// If the share or document does not exist, return a 404.
ctx.status = 404;
}
// Allow shares to be embedded in iframes on other websites
ctx.remove("X-Frame-Options");
// Inject share information in SSR HTML
return renderApp(ctx, next, {
title: document?.title,
description: document?.getSummary(),
canonical: share
? `${share.canonicalUrl}${documentSlug && document ? document.url : ""}`
: undefined,
});
};