Add support for SSL in development (#5668)
This commit is contained in:
@@ -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
|
||||
|
||||
1
Makefile
1
Makefile
@@ -1,5 +1,6 @@
|
||||
up:
|
||||
docker-compose up -d redis postgres s3
|
||||
yarn install-local-ssl
|
||||
yarn install --pure-lockfile
|
||||
yarn dev:watch
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
0
server/config/certs/.gitkeep
Normal file
0
server/config/certs/.gitkeep
Normal 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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
23
server/scripts/install-local-ssl.js
Normal file
23
server/scripts/install-local-ssl.js
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user