chore: Remove env variables in webpack bundle (#1353)

* chore: Remove env variables in webpack bundle

* remove unused globals

* refactor: consolidate window.env calls to single file

* fix: Slack client side integration auth

* fix: developers url
This commit is contained in:
Tom Moor 2020-07-18 11:02:40 -07:00 committed by GitHub
parent 24448c7504
commit 67981a351e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 186 additions and 206 deletions

View File

@ -58,14 +58,5 @@
},
"env": {
"jest": true
},
"globals": {
"__DEV__": true,
"SLACK_KEY": true,
"DEPLOYMENT": true,
"BASE_URL": true,
"SENTRY_DSN": true,
"afterAll": true,
"Sentry": true
}
}

View File

@ -9,8 +9,9 @@ WORKDIR $APP_PATH
COPY . $APP_PATH
RUN yarn install --pure-lockfile
RUN yarn build
RUN cp -r /opt/outline/node_modules /opt/node_modules
CMD yarn build && yarn start
CMD yarn start
EXPOSE 3000

View File

@ -1,6 +1,7 @@
// @flow
/* global ga */
import * as React from "react";
import env from "env";
type Props = {
children?: React.Node,
@ -8,7 +9,7 @@ type Props = {
export default class Analytics extends React.Component<Props> {
componentDidMount() {
if (!process.env.GOOGLE_ANALYTICS_ID) return;
if (!env.GOOGLE_ANALYTICS_ID) return;
// standard Google Analytics script
window.ga =
@ -20,7 +21,7 @@ export default class Analytics extends React.Component<Props> {
// $FlowIssue
ga.l = +new Date();
ga("create", process.env.GOOGLE_ANALYTICS_ID, "auto");
ga("create", env.GOOGLE_ANALYTICS_ID, "auto");
ga("set", { dimension1: "true" });
ga("send", "pageview");

View File

@ -5,6 +5,7 @@ import { Redirect } from "react-router-dom";
import AuthStore from "stores/AuthStore";
import LoadingIndicator from "components/LoadingIndicator";
import { isCustomSubdomain } from "shared/utils/domains";
import env from "env";
type Props = {
auth: AuthStore,
@ -23,7 +24,7 @@ const Authenticated = observer(({ auth, children }: Props) => {
// If we're authenticated but viewing a subdomain that doesn't match the
// currently authenticated team then kick the user to the teams subdomain.
if (
process.env.SUBDOMAINS_ENABLED &&
env.SUBDOMAINS_ENABLED &&
team.subdomain &&
isCustomSubdomain(hostname) &&
!hostname.startsWith(`${team.subdomain}.`)

View File

@ -14,7 +14,7 @@ class CopyToClipboard extends React.PureComponent<Props> {
const { text, onCopy, children } = this.props;
const elem = React.Children.only(children);
copy(text, {
debug: !!__DEV__,
debug: process.env.NODE_ENV !== "production",
});
if (onCopy) onCopy();

View File

@ -23,7 +23,7 @@ class ErrorBoundary extends React.Component<Props> {
console.error(error);
if (window.Sentry) {
Sentry.captureException(error);
window.Sentry.captureException(error);
}
}

View File

@ -29,6 +29,7 @@ import HeaderBlock from "./components/HeaderBlock";
import Version from "./components/Version";
import PoliciesStore from "stores/PoliciesStore";
import AuthStore from "stores/AuthStore";
import env from "env";
type Props = {
history: RouterHistory,
@ -146,7 +147,7 @@ class SettingsSidebar extends React.Component<Props> {
</Section>
)}
{can.update &&
process.env.DEPLOYMENT !== "hosted" && (
env.DEPLOYMENT !== "hosted" && (
<Section>
<Header>Installation</Header>
<Version />

View File

@ -13,6 +13,7 @@ import PoliciesStore from "stores/PoliciesStore";
import ViewsStore from "stores/ViewsStore";
import AuthStore from "stores/AuthStore";
import UiStore from "stores/UiStore";
import env from "env";
export const SocketContext: any = React.createContext();
@ -34,7 +35,7 @@ class SocketProvider extends React.Component<Props> {
@observable socket;
componentDidMount() {
if (!process.env.WEBSOCKETS_ENABLED) return;
if (!env.WEBSOCKETS_ENABLED) return;
this.socket = io(window.location.origin, {
path: "/realtime",

3
app/env.js Normal file
View File

@ -0,0 +1,3 @@
// @flow
const env = window.env;
export default env;

View File

@ -10,9 +10,10 @@ import ScrollToTop from "components/ScrollToTop";
import Toasts from "components/Toasts";
import Theme from "components/Theme";
import Routes from "./routes";
import env from "env";
let DevTools;
if (__DEV__) {
if (process.env.NODE_ENV !== "production") {
DevTools = require("mobx-react-devtools").default; // eslint-disable-line global-require
}
@ -44,7 +45,7 @@ if (element) {
window.addEventListener("load", async () => {
// installation does not use Google Analytics, or tracking is blocked on client
// no point loading the rest of the analytics bundles
if (!process.env.GOOGLE_ANALYTICS_ID || !window.ga) return;
if (!env.GOOGLE_ANALYTICS_ID || !window.ga) return;
// https://github.com/googleanalytics/autotrack/issues/137#issuecomment-305890099
await import("autotrack/autotrack.js");

View File

@ -17,6 +17,7 @@ import Service from "./Service";
import Notices from "./Notices";
import AuthStore from "stores/AuthStore";
import getQueryVariable from "shared/utils/getQueryVariable";
import env from "env";
type Props = {
auth: AuthStore,
@ -62,7 +63,7 @@ class Login extends React.Component<Props, State> {
);
const header =
process.env.DEPLOYMENT === "hosted" &&
env.DEPLOYMENT === "hosted" &&
(config.hostname ? (
<Back href={process.env.URL}>
<BackIcon color="currentColor" /> Back to home
@ -101,8 +102,8 @@ class Login extends React.Component<Props, State> {
<Centered align="center" justify="center" column auto>
<PageTitle title="Login" />
<Logo>
{process.env.TEAM_LOGO && process.env.DEPLOYMENT !== "hosted" ? (
<TeamLogo src={process.env.TEAM_LOGO} />
{env.TEAM_LOGO && env.DEPLOYMENT !== "hosted" ? (
<TeamLogo src={env.TEAM_LOGO} />
) : (
<OutlineLogo size={38} fill="currentColor" />
)}

View File

@ -13,6 +13,7 @@ import CenteredContent from "components/CenteredContent";
import PageTitle from "components/PageTitle";
import HelpText from "components/HelpText";
import Flex from "components/Flex";
import env from "env";
type Props = {
auth: AuthStore,
@ -115,7 +116,7 @@ class Details extends React.Component<Props> {
required
short
/>
{process.env.SUBDOMAINS_ENABLED && (
{env.SUBDOMAINS_ENABLED && (
<React.Fragment>
<Input
label="Subdomain"
@ -129,7 +130,7 @@ class Details extends React.Component<Props> {
/>
{this.subdomain && (
<HelpText small>
Your knowledgebase will be accessible at{" "}
Your knowledge base will be accessible at{" "}
<strong>{this.subdomain}.getoutline.com</strong>
</HelpText>
)}

View File

@ -14,6 +14,7 @@ import IntegrationsStore from "stores/IntegrationsStore";
import AuthStore from "stores/AuthStore";
import Notice from "components/Notice";
import getQueryVariable from "shared/utils/getQueryVariable";
import env from "env";
type Props = {
collections: CollectionsStore,
@ -68,7 +69,7 @@ class Slack extends React.Component<Props> {
) : (
<SlackButton
scopes={["commands", "links:read", "links:write"]}
redirectUri={`${BASE_URL}/auth/slack.commands`}
redirectUri={`${env.URL}/auth/slack.commands`}
state={teamId}
/>
)}
@ -105,7 +106,7 @@ class Slack extends React.Component<Props> {
<strong>{collection.name}</strong>
<SlackButton
scopes={["incoming-webhook"]}
redirectUri={`${BASE_URL}/auth/slack.post`}
redirectUri={`${env.URL}/auth/slack.post`}
state={collection.id}
label="Connect"
/>

View File

@ -4,6 +4,7 @@ import styled from "styled-components";
import { slackAuth } from "shared/utils/routeHelpers";
import SlackLogo from "components/SlackLogo";
import Button from "components/Button";
import env from "env";
type Props = {
scopes?: string[],
@ -14,7 +15,12 @@ type Props = {
function SlackButton({ state, scopes, redirectUri, label }: Props) {
const handleClick = () =>
(window.location.href = slackAuth(state, scopes, redirectUri));
(window.location.href = slackAuth(
state,
scopes,
env.SLACK_KEY,
redirectUri
));
return (
<Button

View File

@ -3,7 +3,7 @@ import { observable, action, computed, autorun, runInAction } from "mobx";
import invariant from "invariant";
import { getCookie, setCookie, removeCookie } from "tiny-cookie";
import { client } from "utils/ApiClient";
import { getCookieDomain } from "shared/utils/domains";
import { getCookieDomain } from "utils/domains";
import RootStore from "stores/RootStore";
import User from "models/User";
import Team from "models/Team";
@ -102,7 +102,7 @@ export default class AuthStore {
this.team = new Team(team);
if (window.Sentry) {
Sentry.configureScope(function(scope) {
window.Sentry.configureScope(function(scope) {
scope.setUser({ id: user.id });
scope.setExtra("team", team.name);
scope.setExtra("teamId", team.id);

7
app/utils/domains.js Normal file
View File

@ -0,0 +1,7 @@
// @flow
import { stripSubdomain } from "shared/utils/domains";
import env from "env";
export function getCookieDomain(domain: string) {
return env.SUBDOMAINS_ENABLED ? stripSubdomain(domain) : domain;
}

View File

@ -1,11 +1,4 @@
// @flow
declare var __DEV__: string;
declare var SLACK_KEY: string;
declare var SLACK_APP_ID: string;
declare var BASE_URL: string;
declare var SENTRY_DSN: ?string;
declare var DEPLOYMENT: string;
declare var Sentry: any;
declare var process: {
env: {
[string]: string,

View File

@ -51,7 +51,7 @@ function filterServices(team) {
router.post("auth.config", async ctx => {
// If self hosted AND there is only one team then that team becomes the
// brand for the knowledgebase and it's guest signin option is used for the
// brand for the knowledge base and it's guest signin option is used for the
// root login page.
if (process.env.DEPLOYMENT !== "hosted") {
const teams = await Team.findAll();

View File

@ -33,30 +33,33 @@ if (process.env.NODE_ENV === "development") {
const compile = webpack(config);
/* eslint-enable global-require */
app.use(
convert(
devMiddleware(compile, {
// display no info to console (only warnings and errors)
noInfo: true,
const middleware = devMiddleware(compile, {
// display no info to console (only warnings and errors)
noInfo: true,
// display nothing to the console
quiet: false,
// display nothing to the console
quiet: false,
// switch into lazy mode
// that means no watching, but recompilation on every request
lazy: false,
// switch into lazy mode
// that means no watching, but recompilation on every request
lazy: false,
// public path to bind the middleware to
// use the same as in webpack
publicPath: config.output.publicPath,
// public path to bind the middleware to
// use the same as in webpack
publicPath: config.output.publicPath,
// options for formatting the statistics
stats: {
colors: true,
},
})
)
);
// options for formatting the statistics
stats: {
colors: true,
},
});
app.use(async (ctx, next) => {
ctx.webpackConfig = config;
ctx.devMiddleware = middleware;
await next();
});
app.use(convert(middleware));
app.use(
convert(
hotMiddleware(compile, {

View File

@ -2,11 +2,11 @@
import bodyParser from "koa-bodyparser";
import Koa from "koa";
import Router from "koa-router";
import addMonths from "date-fns/add_months";
import validation from "../middlewares/validation";
import auth from "../middlewares/authentication";
import addMonths from "date-fns/add_months";
import { getCookieDomain } from "../utils/domains";
import { Team } from "../models";
import { getCookieDomain } from "../../shared/utils/domains";
import slack from "./slack";
import google from "./google";

View File

@ -1,9 +1,9 @@
// @flow
import Sequelize from "sequelize";
import Router from "koa-router";
import auth from "../middlewares/authentication";
import addHours from "date-fns/add_hours";
import { getCookieDomain } from "../../shared/utils/domains";
import auth from "../middlewares/authentication";
import { getCookieDomain } from "../utils/domains";
import { slackAuth } from "../../shared/utils/routeHelpers";
import {
Authentication,

12
server/env.js Normal file
View File

@ -0,0 +1,12 @@
// @flow
export default {
URL: process.env.URL,
DEPLOYMENT: process.env.DEPLOYMENT,
SENTRY_DSN: process.env.SENTRY_DSN,
TEAM_LOGO: process.env.TEAM_LOGO,
SLACK_KEY: process.env.SLACK_KEY,
SLACK_APP_ID: process.env.SLACK_APP_ID,
SUBDOMAINS_ENABLED: process.env.SUBDOMAINS_ENABLED === "true",
WEBSOCKETS_ENABLED: process.env.WEBSOCKETS_ENABLED === "true",
GOOGLE_ANALYTICS_ID: process.env.GOOGLE_ANALYTICS_ID,
};

View File

@ -3,10 +3,10 @@ import JWT from "jsonwebtoken";
import { type Context } from "koa";
import { User, ApiKey } from "../models";
import { getUserForJWT } from "../utils/jwt";
import { getCookieDomain } from "../utils/domains";
import { AuthenticationError, UserSuspendedError } from "../errors";
import addMonths from "date-fns/add_months";
import addMinutes from "date-fns/add_minutes";
import { getCookieDomain } from "../../shared/utils/domains";
export default function auth(options?: { required?: boolean } = {}) {
return async function authMiddleware(ctx: Context, next: () => Promise<*>) {

View File

@ -2,15 +2,40 @@
import path from "path";
import Koa from "koa";
import Router from "koa-router";
import fs from "fs";
import util from "util";
import sendfile from "koa-sendfile";
import serve from "koa-static";
import apexRedirect from "./middlewares/apexRedirect";
import { robotsResponse } from "./utils/robots";
import { opensearchResponse } from "./utils/opensearch";
import environment from "./env";
const isProduction = process.env.NODE_ENV === "production";
const koa = new Koa();
const router = new Router();
const readFile = util.promisify(fs.readFile);
const readIndexFile = async ctx => {
if (isProduction) {
return readFile(path.join(__dirname, "../dist/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, result) => {
if (err) {
return reject(err);
}
resolve(result);
}
);
});
};
// serve static assets
koa.use(
@ -49,11 +74,15 @@ router.get("*", async (ctx, next) => {
return next();
}
if (isProduction) {
await sendfile(ctx, path.join(__dirname, "../dist/index.html"));
} else {
await sendfile(ctx, path.join(__dirname, "./static/dev.html"));
}
const page = await readIndexFile(ctx);
const env = `
window.env = ${JSON.stringify(environment)};
`;
ctx.body = page
.toString()
.replace(/\/\/inject-env\/\//g, env)
.replace(/\/\/inject-sentry-dsn\/\//g, process.env.SENTRY_DSN || "")
.replace(/\/\/inject-slack-app-id\/\//g, process.env.SLACK_APP_ID || "");
});
// middleware

View File

@ -1,42 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Outline</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="shortcut icon" type="image/png" href="favicon-32.png" sizes="32x32" />
<link rel="manifest" href="/manifest.json" />
<link
rel="search"
type="application/opensearchdescription+xml"
href="/opensearch.xml"
title="Outline"
/>
<style>
body,
html {
margin: 0;
padding: 0;
}
body {
display: flex;
width: 100%;
height: 100%;
}
#root {
flex: 1;
min-height: 100vh;
}
</style>
</head>
<body>
<div id="root"></div>
<script>
if (window.localStorage.getItem("theme") === "dark") {
window.document.querySelector('#root').style.background = "#111319";
}
</script>
<script src="/static/bundle.js"></script>
</body>
</html>

View File

@ -1,47 +1,43 @@
<!doctype html>
<html lang="en">
<head>
<title>Outline</title>
<meta name="slack-app-id" content="<%= SLACK_APP_ID %>" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="shortcut icon" type="image/png" href="favicon-32.png" sizes="32x32" />
<link rel="manifest" href="/manifest.json" />
<link
rel="search"
type="application/opensearchdescription+xml"
href="/opensearch.xml"
title="Outline"
/>
<style>
body,
html {
margin: 0;
padding: 0;
}
body {
display: flex;
width: 100%;
height: 100%;
}
<head>
<title>Outline</title>
<meta name="slack-app-id" content="//inject-slack-app-id//" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="shortcut icon" type="image/png" href="favicon-32.png" sizes="32x32" />
<link rel="manifest" href="/manifest.json" />
<link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="Outline" />
<style>
body,
html {
margin: 0;
padding: 0;
}
#root {
flex: 1;
min-height: 100vh;
}
</style>
</head>
<body>
<div id="root"></div>
<script
src="https://browser.sentry-cdn.com/5.12.1/bundle.min.js"
integrity="sha384-y+an4eARFKvjzOivf/Z7JtMJhaN6b+lLQ5oFbBbUwZNNVir39cYtkjW1r6Xjbxg3"
crossorigin="anonymous"
>
body {
display: flex;
width: 100%;
height: 100%;
}
#root {
flex: 1;
min-height: 100vh;
}
</style>
</head>
<body>
<div id="root"></div>
<script>//inject-env//</script>
<script src="https://browser.sentry-cdn.com/5.12.1/bundle.min.js"
integrity="sha384-y+an4eARFKvjzOivf/Z7JtMJhaN6b+lLQ5oFbBbUwZNNVir39cYtkjW1r6Xjbxg3" crossorigin="anonymous">
</script>
<script>
<script>
if ('//inject-sentry-dsn//') {
Sentry.init({
dsn: '<%= SENTRY_DSN %>',
dsn: '//inject-sentry-dsn//',
ignoreErrors: [
'AuthorizationError',
'NetworkError',
@ -50,10 +46,12 @@
'UpdateRequiredError',
],
});
}
if (window.localStorage.getItem("theme") === "dark") {
window.document.querySelector('#root').style.background = "#111319";
}
</script>
</body>
</html>
if (window.localStorage.getItem("theme") === "dark") {
window.document.querySelector('#root').style.background = "#111319";
}
</script>
</body>
</html>

8
server/utils/domains.js Normal file
View File

@ -0,0 +1,8 @@
// @flow
import { stripSubdomain } from "../../shared/utils/domains";
export function getCookieDomain(domain: string) {
return process.env.SUBDOMAINS_ENABLED === "true"
? stripSubdomain(domain)
: domain;
}

View File

@ -52,14 +52,6 @@ export function parseDomain(url: string): ?Domain {
return null;
}
export function getCookieDomain(domain: string) {
// TODO: All the process.env parsing needs centralizing
return process.env.SUBDOMAINS_ENABLED === "true" ||
process.env.SUBDOMAINS_ENABLED === true
? stripSubdomain(domain)
: domain;
}
export function stripSubdomain(hostname: string) {
const parsed = parseDomain(hostname);
if (!parsed) return hostname;

View File

@ -8,11 +8,12 @@ export function slackAuth(
"identity.avatar",
"identity.team",
],
clientId: string = process.env.SLACK_KEY,
redirectUri: string = `${process.env.URL}/auth/slack.callback`
): string {
const baseUrl = "https://slack.com/oauth/authorize";
const params = {
client_id: process.env.SLACK_KEY,
client_id: clientId,
scope: scopes ? scopes.join(" ") : "",
redirect_uri: redirectUri,
state,
@ -53,12 +54,8 @@ export function mailToUrl(): string {
return "mailto:hello@getoutline.com";
}
export function features(): string {
return `${process.env.URL}/#features`;
}
export function developers(): string {
return `${process.env.URL}/developers`;
return `https://www.getoutline.com/developers`;
}
export function changelog(): string {

View File

@ -1,24 +1,23 @@
/* eslint-disable */
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const commonWebpackConfig = require('./webpack.config');
const webpack = require("webpack");
const commonWebpackConfig = require("./webpack.config");
const developmentWebpackConfig = Object.assign(commonWebpackConfig, {
cache: true,
devtool: 'eval-source-map',
devtool: "eval-source-map",
entry: [
'babel-polyfill',
'babel-regenerator-runtime',
'webpack-hot-middleware/client',
'./app/index',
"babel-polyfill",
"babel-regenerator-runtime",
"webpack-hot-middleware/client",
"./app/index",
],
});
developmentWebpackConfig.plugins = [
...developmentWebpackConfig.plugins,
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
title: 'Outline',
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("development"),
}),
];

View File

@ -1,27 +1,10 @@
/* eslint-disable */
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
require('dotenv').config({ silent: true });
const definePlugin = new webpack.DefinePlugin({
__DEV__: JSON.stringify(JSON.parse(process.env.NODE_ENV !== 'production')),
__PRERELEASE__: JSON.stringify(
JSON.parse(process.env.BUILD_PRERELEASE || 'false')
),
SLACK_APP_ID: JSON.stringify(process.env.SLACK_APP_ID),
BASE_URL: JSON.stringify(process.env.URL),
SENTRY_DSN: JSON.stringify(process.env.SENTRY_DSN),
'process.env': {
DEPLOYMENT: JSON.stringify(process.env.DEPLOYMENT),
URL: JSON.stringify(process.env.URL),
TEAM_LOGO: JSON.stringify(process.env.TEAM_LOGO),
SLACK_KEY: JSON.stringify(process.env.SLACK_KEY),
SUBDOMAINS_ENABLED: JSON.stringify(process.env.SUBDOMAINS_ENABLED === 'true'),
WEBSOCKETS_ENABLED: JSON.stringify(process.env.WEBSOCKETS_ENABLED === 'true')
}
});
module.exports = {
output: {
path: path.join(__dirname, 'dist'),
@ -65,11 +48,13 @@ module.exports = {
}
},
plugins: [
definePlugin,
new webpack.ProvidePlugin({
fetch: 'imports-loader?this=>global!exports-loader?global.fetch!isomorphic-fetch',
}),
new webpack.IgnorePlugin(/unicode\/category\/So/),
new HtmlWebpackPlugin({
template: 'server/static/index.html',
}),
],
stats: {
assets: false,

View File

@ -1,7 +1,6 @@
/* eslint-disable */
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
@ -21,9 +20,6 @@ productionWebpackConfig = Object.assign(commonWebpackConfig, {
productionWebpackConfig.plugins = [
...productionWebpackConfig.plugins,
new ManifestPlugin(),
new HtmlWebpackPlugin({
template: 'server/static/index.html',
}),
new UglifyJsPlugin({
sourceMap: true,
uglifyOptions: {
@ -32,13 +28,7 @@ productionWebpackConfig.plugins = [
}
}),
new webpack.DefinePlugin({
'process.env.DEPLOYMENT': JSON.stringify(process.env.DEPLOYMENT),
'process.env.URL': JSON.stringify(process.env.URL),
'process.env.TEAM_LOGO': JSON.stringify(process.env.TEAM_LOGO),
'process.env.NODE_ENV': JSON.stringify('production'),
'process.env.GOOGLE_ANALYTICS_ID': JSON.stringify(process.env.GOOGLE_ANALYTICS_ID),
'process.env.SUBDOMAINS_ENABLED': JSON.stringify(process.env.SUBDOMAINS_ENABLED === 'true'),
'process.env.WEBSOCKETS_ENABLED': JSON.stringify(process.env.WEBSOCKETS_ENABLED === 'true'),
}),
];