Add support for SSL in development (#5668)

This commit is contained in:
Tom Moor
2023-08-09 07:21:41 -04:00
committed by GitHub
parent 454a4e9a8d
commit c32cec7bff
12 changed files with 75 additions and 27 deletions

View File

@@ -30,7 +30,7 @@ REDIS_URL=redis://localhost:6379
# URL should point to the fully qualified, publicly accessible URL. If using a
# proxy the port in URL and PORT may be different.
URL=http://localhost:3000
URL=https://app.outline.dev:3000
PORT=3000
# See [documentation](docs/SERVICES.md) on running a separate collaboration

View File

@@ -1,5 +1,6 @@
up:
docker-compose up -d redis postgres s3
yarn install-local-ssl
yarn install --pure-lockfile
yarn dev:watch

View File

@@ -16,6 +16,7 @@
"lint": "eslint app server shared plugins",
"prepare": "husky install",
"postinstall": "yarn patch-package",
"install-local-ssl": "node ./server/scripts/install-local-ssl.js",
"heroku-postbuild": "yarn build && yarn db:migrate",
"db:create-migration": "sequelize migration:create",
"db:create": "sequelize db:create",

View File

@@ -60,7 +60,7 @@ describe("email", () => {
email: user.email,
},
headers: {
host: "example.localoutline.com",
host: "example.outline.dev",
},
});
const body = await res.json();
@@ -71,7 +71,7 @@ describe("email", () => {
});
it("should not send email when user is on another subdomain but respond with success", async () => {
env.URL = sharedEnv.URL = "http://localoutline.com";
env.URL = sharedEnv.URL = "https://app.outline.dev";
env.SUBDOMAINS_ENABLED = sharedEnv.SUBDOMAINS_ENABLED = true;
env.DEPLOYMENT = "hosted";
@@ -85,7 +85,7 @@ describe("email", () => {
email: user.email,
},
headers: {
host: "example.localoutline.com",
host: "example.outline.dev",
},
});
@@ -109,7 +109,7 @@ describe("email", () => {
email: user.email,
},
headers: {
host: "example.localoutline.com",
host: "example.outline.dev",
},
});
const body = await res.json();
@@ -129,7 +129,7 @@ describe("email", () => {
email: "user@example.com",
},
headers: {
host: "example.localoutline.com",
host: "example.outline.dev",
},
});
const body = await res.json();
@@ -141,7 +141,7 @@ describe("email", () => {
describe("with multiple users matching email", () => {
it("should default to current subdomain with SSO", async () => {
const spy = jest.spyOn(SigninEmail.prototype, "schedule");
env.URL = sharedEnv.URL = "http://localoutline.com";
env.URL = sharedEnv.URL = "https://app.outline.dev";
env.SUBDOMAINS_ENABLED = sharedEnv.SUBDOMAINS_ENABLED = true;
const email = "sso-user@example.org";
const team = await buildTeam({
@@ -159,7 +159,7 @@ describe("email", () => {
email,
},
headers: {
host: "example.localoutline.com",
host: "example.outline.dev",
},
});
const body = await res.json();
@@ -171,7 +171,7 @@ describe("email", () => {
it("should default to current subdomain with guest email", async () => {
const spy = jest.spyOn(SigninEmail.prototype, "schedule");
env.URL = sharedEnv.URL = "http://localoutline.com";
env.URL = sharedEnv.URL = "https://app.outline.dev";
env.SUBDOMAINS_ENABLED = sharedEnv.SUBDOMAINS_ENABLED = true;
const email = "guest-user@example.org";
const team = await buildTeam({
@@ -189,7 +189,7 @@ describe("email", () => {
email,
},
headers: {
host: "example.localoutline.com",
host: "example.outline.dev",
},
});
const body = await res.json();

View File

View File

@@ -162,7 +162,7 @@ export class Environment {
*/
@IsNumber()
@IsOptional()
public PORT = this.toOptionalNumber(process.env.PORT);
public PORT = this.toOptionalNumber(process.env.PORT) ?? 3000;
/**
* Optional extra debugging. Comma separated

View File

@@ -128,16 +128,17 @@ async function start(id: number, disconnect: () => void) {
});
server.on("listening", () => {
const address = server.address();
const port = (address as AddressInfo).port;
Logger.info(
"lifecycle",
`Listening on ${useHTTPS ? "https" : "http"}://localhost:${
(address as AddressInfo).port
`Listening on ${useHTTPS ? "https" : "http"}://localhost:${port} / ${
env.URL
}`
);
});
server.listen(normalizedPortFlag || env.PORT || "3000");
server.listen(normalizedPortFlag || env.PORT);
server.setTimeout(env.REQUEST_TIMEOUT);
ShutdownHelper.add(

View File

@@ -105,7 +105,7 @@ describe("#auth.config", () => {
});
it("should return available providers for team subdomain", async () => {
env.URL = sharedEnv.URL = "http://localoutline.com";
env.URL = sharedEnv.URL = "https://app.outline.dev";
env.SUBDOMAINS_ENABLED = sharedEnv.SUBDOMAINS_ENABLED = true;
env.DEPLOYMENT = "hosted";
@@ -121,7 +121,7 @@ describe("#auth.config", () => {
});
const res = await server.post("/api/auth.config", {
headers: {
host: `example.localoutline.com`,
host: `example.outline.dev`,
},
});
const body = await res.json();
@@ -155,7 +155,7 @@ describe("#auth.config", () => {
});
it("should return email provider for team when guest signin enabled", async () => {
env.URL = sharedEnv.URL = "http://localoutline.com";
env.URL = sharedEnv.URL = "https://app.outline.dev";
env.DEPLOYMENT = "hosted";
await buildTeam({
@@ -170,7 +170,7 @@ describe("#auth.config", () => {
});
const res = await server.post("/api/auth.config", {
headers: {
host: "example.localoutline.com",
host: "example.outline.dev",
},
});
const body = await res.json();
@@ -181,7 +181,7 @@ describe("#auth.config", () => {
});
it("should not return provider when disabled", async () => {
env.URL = sharedEnv.URL = "http://localoutline.com";
env.URL = sharedEnv.URL = "https://app.outline.dev";
env.DEPLOYMENT = "hosted";
await buildTeam({
@@ -197,7 +197,7 @@ describe("#auth.config", () => {
});
const res = await server.post("/api/auth.config", {
headers: {
host: "example.localoutline.com",
host: "example.outline.dev",
},
});
const body = await res.json();

View File

@@ -17,7 +17,11 @@ import readManifestFile from "@server/utils/readManifestFile";
const isProduction = env.ENVIRONMENT === "production";
const isDevelopment = env.ENVIRONMENT === "development";
const isTest = env.ENVIRONMENT === "test";
const readFile = util.promisify(fs.readFile);
const entry = "app/index.tsx";
const viteHost = env.URL.replace(`:${env.PORT}`, ":3001");
let indexHtmlCache: Buffer | undefined;
const readIndexFile = async (): Promise<Buffer> => {
@@ -71,20 +75,20 @@ export const renderApp = async (
window.env = ${JSON.stringify(presentEnv(env, options.analytics))};
</script>
`;
const entry = "app/index.tsx";
const scriptTags = isProduction
? `<script type="module" nonce="${ctx.state.cspNonce}" src="${
env.CDN_URL || ""
}/static/${readManifestFile()[entry]["file"]}"></script>`
: `<script type="module" nonce="${ctx.state.cspNonce}">
import RefreshRuntime from 'http://localhost:3001/static/@react-refresh'
import RefreshRuntime from "${viteHost}/static/@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => { }
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
<script type="module" nonce="${ctx.state.cspNonce}" src="http://localhost:3001/static/@vite/client"></script>
<script type="module" nonce="${ctx.state.cspNonce}" src="http://localhost:3001/static/${entry}"></script>
<script type="module" nonce="${ctx.state.cspNonce}" src="${viteHost}/static/@vite/client"></script>
<script type="module" nonce="${ctx.state.cspNonce}" src="${viteHost}/static/${entry}"></script>
`;
ctx.body = page

View File

@@ -0,0 +1,23 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const exec = require("child_process").execSync;
const fs = require("fs");
const path = require("path");
const sslDir = path.join(__dirname, "..", "config", "certs");
const sslCert = path.join(sslDir, "public.cert");
const sslKey = path.join(sslDir, "private.key");
if (!fs.existsSync(sslKey) || !fs.existsSync(sslCert)) {
try {
exec(
`mkcert -cert-file ${sslDir}/public.cert -key-file ${sslDir}/private.key "*.outline.dev" && mkcert -install`
);
console.log("🔒 Local SSL certificate created");
} catch (e) {
console.log(
"SSL certificates could not be generated. Ensure mkcert is installed and in your PATH"
);
console.log(e.message);
}
}

View File

@@ -25,13 +25,15 @@ export function getSSLOptions() {
? Buffer.from(env.SSL_KEY, "base64").toString("ascii")
: undefined) ||
safeReadFile("private.key") ||
safeReadFile("private.pem"),
safeReadFile("private.pem") ||
safeReadFile("server/config/certs/private.key"),
cert:
(env.SSL_CERT
? Buffer.from(env.SSL_CERT, "base64").toString("ascii")
: undefined) ||
safeReadFile("public.cert") ||
safeReadFile("public.pem"),
safeReadFile("public.pem") ||
safeReadFile("server/config/certs/public.cert"),
};
} catch (err) {
return {

View File

@@ -1,3 +1,4 @@
import fs from "fs";
import path from "path";
// eslint-disable-next-line import/no-unresolved
import { optimizeLodashImports } from "@optimize-lodash/rollup-plugin";
@@ -5,7 +6,7 @@ import react from "@vitejs/plugin-react";
import browserslistToEsbuild from "browserslist-to-esbuild";
import dotenv from "dotenv";
import { webpackStats } from "rollup-plugin-webpack-stats";
import { defineConfig } from "vite";
import { CommonServerOptions, defineConfig } from "vite";
import { VitePWA } from "vite-plugin-pwa";
import { viteStaticCopy } from "vite-plugin-static-copy";
@@ -14,6 +15,20 @@ dotenv.config({
silent: true,
});
let httpsConfig: CommonServerOptions["https"] | undefined;
if (process.env.NODE_ENV === "development") {
try {
httpsConfig = {
key: fs.readFileSync("./server/config/certs/private.key"),
cert: fs.readFileSync("./server/config/certs/public.cert"),
};
} catch (err) {
// eslint-disable-next-line no-console
console.warn("No local SSL certs found, HTTPS will not be available");
}
}
export default () =>
defineConfig({
root: "./",
@@ -22,6 +37,7 @@ export default () =>
server: {
port: 3001,
host: true,
https: httpsConfig,
},
plugins: [
// https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#readme