Separate environment configs (#6597)

* Separate environment configs

* wip

* wip

* test

* plugins

* test

* test

* .sequelizerc, unfortunately can't go through /utils/environment due to not supporting TS

* docker-compose -> docker compose

* fix: .local wipes .development

* Add custom validation message for invalid SECRET_KEY (often confused)
This commit is contained in:
Tom Moor
2024-02-27 09:24:23 -08:00
committed by GitHub
parent 415383a1c9
commit 60e52d0423
45 changed files with 489 additions and 409 deletions

View File

@@ -13,13 +13,8 @@ defaults: &defaults
resource_class: large resource_class: large
environment: environment:
NODE_ENV: test NODE_ENV: test
SECRET_KEY: F0E5AD933D7F6FD8F4DBB3E038C501C052DC0593C686D21ACB30AE205D2F634B
DATABASE_URL_TEST: postgres://postgres:password@localhost:5432/circle_test
DATABASE_URL: postgres://postgres:password@localhost:5432/circle_test DATABASE_URL: postgres://postgres:password@localhost:5432/circle_test
URL: http://localhost:3000 URL: http://localhost:3000
SMTP_FROM_EMAIL: hello@example.com
AWS_S3_UPLOAD_BUCKET_URL: https://s3.amazonaws.com
AWS_S3_UPLOAD_BUCKET_NAME: outline-circle
NODE_OPTIONS: --max-old-space-size=8000 NODE_OPTIONS: --max-old-space-size=8000
executors: executors:
@@ -89,7 +84,7 @@ jobs:
key: dependency-cache-v1-{{ checksum "package.json" }} key: dependency-cache-v1-{{ checksum "package.json" }}
- run: - run:
name: migrate name: migrate
command: ./node_modules/.bin/sequelize db:migrate --url $DATABASE_URL_TEST command: ./node_modules/.bin/sequelize db:migrate
- run: - run:
name: test name: test
command: | command: |

10
.env.development Normal file
View File

@@ -0,0 +1,10 @@
URL=https://local.outline.dev:3000
SMTP_FROM_EMAIL=hello@example.com
# Enable unsafe-inline in script-src CSP directive
# Setting it to true allows React dev tools add-on in Firefox to successfully detect the project
DEVELOPMENT_UNSAFE_INLINE_CSP=true
# Increase the log level to debug for development
LOG_LEVEL=debug

View File

@@ -13,7 +13,6 @@ UTILS_SECRET=generate_a_new_key
# For production point these at your databases, in development the default # For production point these at your databases, in development the default
# should work out of the box. # should work out of the box.
DATABASE_URL=postgres://user:pass@localhost:5432/outline DATABASE_URL=postgres://user:pass@localhost:5432/outline
DATABASE_URL_TEST=postgres://user:pass@localhost:5432/outline-test
DATABASE_CONNECTION_POOL_MIN= DATABASE_CONNECTION_POOL_MIN=
DATABASE_CONNECTION_POOL_MAX= DATABASE_CONNECTION_POOL_MAX=
# Uncomment this to disable SSL for connecting to Postgres # Uncomment this to disable SSL for connecting to Postgres
@@ -30,7 +29,7 @@ REDIS_URL=redis://localhost:6379
# URL should point to the fully qualified, publicly accessible URL. If using a # URL should point to the fully qualified, publicly accessible URL. If using a
# proxy the port in URL and PORT may be different. # proxy the port in URL and PORT may be different.
URL=https://app.outline.dev:3000 URL=
PORT=3000 PORT=3000
# See [documentation](docs/SERVICES.md) on running a separate collaboration # See [documentation](docs/SERVICES.md) on running a separate collaboration
@@ -166,9 +165,6 @@ SLACK_VERIFICATION_TOKEN=your_token
SLACK_APP_ID=A0XXXXXXX SLACK_APP_ID=A0XXXXXXX
SLACK_MESSAGE_ACTIONS=true SLACK_MESSAGE_ACTIONS=true
# Optionally enable google analytics to track pageviews in the knowledge base
GOOGLE_ANALYTICS_ID=
# Optionally enable Sentry (sentry.io) to track errors and performance, # Optionally enable Sentry (sentry.io) to track errors and performance,
# and optionally add a Sentry proxy tunnel for bypassing ad blockers in the UI: # and optionally add a Sentry proxy tunnel for bypassing ad blockers in the UI:
# https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option) # https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option)
@@ -181,8 +177,8 @@ SMTP_HOST=
SMTP_PORT= SMTP_PORT=
SMTP_USERNAME= SMTP_USERNAME=
SMTP_PASSWORD= SMTP_PASSWORD=
SMTP_FROM_EMAIL=hello@example.com SMTP_FROM_EMAIL=
SMTP_REPLY_EMAIL=hello@example.com SMTP_REPLY_EMAIL=
SMTP_TLS_CIPHERS= SMTP_TLS_CIPHERS=
SMTP_SECURE=true SMTP_SECURE=true
@@ -198,10 +194,5 @@ RATE_LIMITER_REQUESTS=1000
RATE_LIMITER_DURATION_WINDOW=60 RATE_LIMITER_DURATION_WINDOW=60
# Iframely API config # Iframely API config
# IFRAMELY_URL= IFRAMELY_URL=
# IFRAMELY_API_KEY= IFRAMELY_API_KEY=
# Enable unsafe-inline in script-src CSP directive
# Setting it to true allows React dev tools add-on in
# Firefox to successfully detect the project
DEVELOPMENT_UNSAFE_INLINE_CSP=false

26
.env.test Normal file
View File

@@ -0,0 +1,26 @@
NODE_ENV=test
DATABASE_URL=postgres://user:pass@127.0.0.1:5432/outline-test
SECRET_KEY=F0E5AD933D7F6FD8F4DBB3E038C501C052DC0593C686D21ACB30AE205D2F634B
SMTP_HOST=smtp.example.com
SMTP_FROM_EMAIL=hello@example.com
SMTP_REPLY_EMAIL=hello@example.com
GOOGLE_CLIENT_ID=123
GOOGLE_CLIENT_SECRET=123
SLACK_CLIENT_ID=123
SLACK_CLIENT_SECRET=123
OIDC_CLIENT_ID=client-id
OIDC_CLIENT_SECRET=client-secret
OIDC_AUTH_URI=http://localhost/authorize
OIDC_TOKEN_URI=http://localhost/token
OIDC_USERINFO_URI=http://localhost/userinfo
IFRAMELY_API_KEY=123
RATE_LIMITER_ENABLED=false
FILE_STORAGE=local
FILE_STORAGE_LOCAL_ROOT_DIR=/tmp

2
.gitignore vendored
View File

@@ -2,6 +2,8 @@ dist
build build
node_modules/* node_modules/*
.env .env
.env.local
.env.production
.log .log
.vscode/* .vscode/*
npm-debug.log npm-debug.log

View File

@@ -9,7 +9,7 @@
"^@server/(.*)$": "<rootDir>/server/$1", "^@server/(.*)$": "<rootDir>/server/$1",
"^@shared/(.*)$": "<rootDir>/shared/$1" "^@shared/(.*)$": "<rootDir>/shared/$1"
}, },
"setupFiles": ["<rootDir>/__mocks__/console.js", "<rootDir>/server/test/env.ts"], "setupFiles": ["<rootDir>/__mocks__/console.js"],
"setupFilesAfterEnv": ["<rootDir>/server/test/setup.ts"], "setupFilesAfterEnv": ["<rootDir>/server/test/setup.ts"],
"globalSetup": "<rootDir>/server/test/globalSetup.js", "globalSetup": "<rootDir>/server/test/globalSetup.js",
"globalTeardown": "<rootDir>/server/test/globalTeardown.js", "globalTeardown": "<rootDir>/server/test/globalTeardown.js",

View File

@@ -1,4 +1,6 @@
require('dotenv').config({ silent: true }); require("dotenv").config({
path: process.env.NODE_ENV === "test" ? ".env.test" : ".env",
});
var path = require('path'); var path = require('path');
@@ -6,5 +8,4 @@ module.exports = {
'config': path.resolve('server/config', 'database.json'), 'config': path.resolve('server/config', 'database.json'),
'migrations-path': path.resolve('server', 'migrations'), 'migrations-path': path.resolve('server', 'migrations'),
'models-path': path.resolve('server', 'models'), 'models-path': path.resolve('server', 'models'),
'seeders-path': path.resolve('server/models', 'fixtures'),
} }

View File

@@ -1,28 +1,28 @@
up: up:
docker-compose up -d redis postgres docker compose up -d redis postgres
yarn install-local-ssl yarn install-local-ssl
yarn install --pure-lockfile yarn install --pure-lockfile
yarn dev:watch yarn dev:watch
build: build:
docker-compose build --pull outline docker compose build --pull outline
test: test:
docker-compose up -d redis postgres docker compose up -d redis postgres
yarn sequelize db:drop --env=test NODE_ENV=test yarn sequelize db:drop
yarn sequelize db:create --env=test NODE_ENV=test yarn sequelize db:create
NODE_ENV=test yarn sequelize db:migrate --env=test NODE_ENV=test yarn sequelize db:migrate
yarn test yarn test
watch: watch:
docker-compose up -d redis postgres docker compose up -d redis postgres
yarn sequelize db:drop --env=test NODE_ENV=test yarn sequelize db:drop
yarn sequelize db:create --env=test NODE_ENV=test yarn sequelize db:create
NODE_ENV=test yarn sequelize db:migrate --env=test NODE_ENV=test yarn sequelize db:migrate
yarn test:watch yarn test:watch
destroy: destroy:
docker-compose stop docker compose stop
docker-compose rm -f docker compose rm -f
.PHONY: up build destroy test watch # let's go to reserve rules names .PHONY: up build destroy test watch # let's go to reserve rules names

View File

@@ -26,7 +26,6 @@ import SearchQuery from "~/models/SearchQuery";
import KeyboardShortcuts from "~/scenes/KeyboardShortcuts"; import KeyboardShortcuts from "~/scenes/KeyboardShortcuts";
import { createAction } from "~/actions"; import { createAction } from "~/actions";
import { NavigationSection, RecentSearchesSection } from "~/actions/sections"; import { NavigationSection, RecentSearchesSection } from "~/actions/sections";
import env from "~/env";
import Desktop from "~/utils/Desktop"; import Desktop from "~/utils/Desktop";
import history from "~/utils/history"; import history from "~/utils/history";
import isCloudHosted from "~/utils/isCloudHosted"; import isCloudHosted from "~/utils/isCloudHosted";
@@ -212,9 +211,6 @@ export const logout = createAction({
icon: <LogoutIcon />, icon: <LogoutIcon />,
perform: () => { perform: () => {
void stores.auth.logout(); void stores.auth.logout();
if (env.OIDC_LOGOUT_URI) {
window.location.replace(env.OIDC_LOGOUT_URI);
}
}, },
}); });

View File

@@ -1,15 +1,10 @@
import * as React from "react"; import * as React from "react";
import { Redirect } from "react-router-dom"; import { Redirect } from "react-router-dom";
import env from "~/env";
import useStores from "~/hooks/useStores"; import useStores from "~/hooks/useStores";
const Logout = () => { const Logout = () => {
const { auth } = useStores(); const { auth } = useStores();
void auth.logout(); void auth.logout();
if (env.OIDC_LOGOUT_URI) {
window.location.replace(env.OIDC_LOGOUT_URI);
return null;
}
return <Redirect to="/" />; return <Redirect to="/" />;
}; };

View File

@@ -14,6 +14,7 @@ import { PartialWithId } from "~/types";
import { client } from "~/utils/ApiClient"; import { client } from "~/utils/ApiClient";
import Desktop from "~/utils/Desktop"; import Desktop from "~/utils/Desktop";
import Logger from "~/utils/Logger"; import Logger from "~/utils/Logger";
import history from "~/utils/history";
import isCloudHosted from "~/utils/isCloudHosted"; import isCloudHosted from "~/utils/isCloudHosted";
import Store from "./base/Store"; import Store from "./base/Store";
@@ -304,16 +305,15 @@ export default class AuthStore extends Store<Team> {
} }
}; };
/**
* Logs the user out and optionally revokes the authentication token.
*
* @param savePath Whether the current path should be saved and returned to after login.
* @param tryRevokingToken Whether the auth token should attempt to be revoked, this should be
* disabled with requests from ApiClient to prevent infinite loops.
*/
@action @action
logout = async ( logout = async (savePath = false, tryRevokingToken = true) => {
/** Whether the current path should be saved and returned to after login */
savePath = false,
/**
* Whether the auth token should attempt to be revoked, this should be disabled
* with requests from ApiClient to prevent infinite loops.
*/
tryRevokingToken = true
) => {
// if this logout was forced from an authenticated route then // if this logout was forced from an authenticated route then
// save the current path so we can go back there once signed in // save the current path so we can go back there once signed in
if (savePath) { if (savePath) {
@@ -348,9 +348,16 @@ export default class AuthStore extends Store<Team> {
this.currentUserId = null; this.currentUserId = null;
this.currentTeamId = null; this.currentTeamId = null;
this.collaborationToken = null; this.collaborationToken = null;
this.rootStore.clear();
// Tell the host application we logged out, if any allows window cleanup. // Tell the host application we logged out, if any allows window cleanup.
void Desktop.bridge?.onLogout?.(); if (Desktop.isElectron()) {
this.rootStore.clear(); void Desktop.bridge?.onLogout?.();
} else if (env.OIDC_LOGOUT_URI) {
window.location.replace(env.OIDC_LOGOUT_URI);
return;
}
history.replace("/");
}; };
} }

View File

@@ -99,7 +99,7 @@
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"dd-trace": "^3.33.0", "dd-trace": "^3.33.0",
"diff": "^5.1.0", "diff": "^5.1.0",
"dotenv": "^4.0.0", "dotenv": "^16.4.5",
"email-providers": "^1.14.0", "email-providers": "^1.14.0",
"emoji-mart": "^5.5.2", "emoji-mart": "^5.5.2",
"emoji-regex": "^10.3.0", "emoji-regex": "^10.3.0",
@@ -247,6 +247,7 @@
"@types/body-scroll-lock": "^3.1.0", "@types/body-scroll-lock": "^3.1.0",
"@types/crypto-js": "^4.2.1", "@types/crypto-js": "^4.2.1",
"@types/diff": "^5.0.4", "@types/diff": "^5.0.4",
"@types/dotenv": "^8.2.0",
"@types/emoji-regex": "^9.2.0", "@types/emoji-regex": "^9.2.0",
"@types/express-useragent": "^1.0.2", "@types/express-useragent": "^1.0.2",
"@types/formidable": "^2.0.6", "@types/formidable": "^2.0.6",

View File

@@ -6,7 +6,6 @@ import Router from "koa-router";
import { Profile } from "passport"; import { Profile } from "passport";
import { slugifyDomain } from "@shared/utils/domains"; import { slugifyDomain } from "@shared/utils/domains";
import accountProvisioner from "@server/commands/accountProvisioner"; import accountProvisioner from "@server/commands/accountProvisioner";
import env from "@server/env";
import { MicrosoftGraphError } from "@server/errors"; import { MicrosoftGraphError } from "@server/errors";
import passportMiddleware from "@server/middlewares/passport"; import passportMiddleware from "@server/middlewares/passport";
import { User } from "@server/models"; import { User } from "@server/models";
@@ -17,6 +16,7 @@ import {
getTeamFromContext, getTeamFromContext,
getClientFromContext, getClientFromContext,
} from "@server/utils/passport"; } from "@server/utils/passport";
import env from "../env";
const router = new Router(); const router = new Router();
const providerName = "azure"; const providerName = "azure";

View File

@@ -1,6 +1,7 @@
import invariant from "invariant";
import JWT from "jsonwebtoken"; import JWT from "jsonwebtoken";
import env from "@server/env"; import OAuthClient from "@server/utils/oauth";
import OAuthClient from "./oauth"; import env from "./env";
type AzurePayload = { type AzurePayload = {
/** A GUID that represents the Azure AD tenant that the user is from */ /** A GUID that represents the Azure AD tenant that the user is from */
@@ -14,6 +15,13 @@ export default class AzureClient extends OAuthClient {
userinfo: "https://graph.microsoft.com/v1.0/me", userinfo: "https://graph.microsoft.com/v1.0/me",
}; };
constructor() {
invariant(env.AZURE_CLIENT_ID, "AZURE_CLIENT_ID is required");
invariant(env.AZURE_CLIENT_SECRET, "AZURE_CLIENT_SECRET is required");
super(env.AZURE_CLIENT_ID, env.AZURE_CLIENT_SECRET);
}
async rotateToken( async rotateToken(
accessToken: string, accessToken: string,
refreshToken: string refreshToken: string

View File

@@ -0,0 +1,27 @@
import { IsOptional } from "class-validator";
import { Environment } from "@server/env";
import environment from "@server/utils/environment";
import { CannotUseWithout } from "@server/utils/validators";
class AzurePluginEnvironment extends Environment {
/**
* Azure OAuth2 client credentials. To enable authentication with Azure.
*/
@IsOptional()
@CannotUseWithout("AZURE_CLIENT_SECRET")
public AZURE_CLIENT_ID = this.toOptionalString(environment.AZURE_CLIENT_ID);
@IsOptional()
@CannotUseWithout("AZURE_CLIENT_ID")
public AZURE_CLIENT_SECRET = this.toOptionalString(
environment.AZURE_CLIENT_SECRET
);
@IsOptional()
@CannotUseWithout("AZURE_CLIENT_ID")
public AZURE_RESOURCE_APP_ID = this.toOptionalString(
environment.AZURE_RESOURCE_APP_ID
);
}
export default new AzurePluginEnvironment();

View File

@@ -6,7 +6,6 @@ import { Profile } from "passport";
import { Strategy as GoogleStrategy } from "passport-google-oauth2"; import { Strategy as GoogleStrategy } from "passport-google-oauth2";
import { slugifyDomain } from "@shared/utils/domains"; import { slugifyDomain } from "@shared/utils/domains";
import accountProvisioner from "@server/commands/accountProvisioner"; import accountProvisioner from "@server/commands/accountProvisioner";
import env from "@server/env";
import { import {
GmailAccountCreationError, GmailAccountCreationError,
TeamDomainRequiredError, TeamDomainRequiredError,
@@ -19,6 +18,7 @@ import {
getTeamFromContext, getTeamFromContext,
getClientFromContext, getClientFromContext,
} from "@server/utils/passport"; } from "@server/utils/passport";
import env from "../env";
const router = new Router(); const router = new Router();
const providerName = "google"; const providerName = "google";

View File

@@ -0,0 +1,21 @@
import { IsOptional } from "class-validator";
import { Environment } from "@server/env";
import environment from "@server/utils/environment";
import { CannotUseWithout } from "@server/utils/validators";
class GooglePluginEnvironment extends Environment {
/**
* Google OAuth2 client credentials. To enable authentication with Google.
*/
@IsOptional()
@CannotUseWithout("GOOGLE_CLIENT_SECRET")
public GOOGLE_CLIENT_ID = this.toOptionalString(environment.GOOGLE_CLIENT_ID);
@IsOptional()
@CannotUseWithout("GOOGLE_CLIENT_ID")
public GOOGLE_CLIENT_SECRET = this.toOptionalString(
environment.GOOGLE_CLIENT_SECRET
);
}
export default new GooglePluginEnvironment();

View File

@@ -0,0 +1,18 @@
import invariant from "invariant";
import OAuthClient from "@server/utils/oauth";
import env from "./env";
export default class GoogleClient extends OAuthClient {
endpoints = {
authorize: "https://accounts.google.com/o/oauth2/auth",
token: "https://accounts.google.com/o/oauth2/token",
userinfo: "https://www.googleapis.com/oauth2/v3/userinfo",
};
constructor() {
invariant(env.GOOGLE_CLIENT_ID, "GOOGLE_CLIENT_ID is required");
invariant(env.GOOGLE_CLIENT_SECRET, "GOOGLE_CLIENT_SECRET is required");
super(env.GOOGLE_CLIENT_ID, env.GOOGLE_CLIENT_SECRET);
}
}

View File

@@ -1,5 +1,5 @@
{ {
"name": "Iframely", "name": "Iframely",
"description": "Integrate Iframely to enable unfurling of arbitrary urls", "description": "Integrate Iframely to enable unfurling of arbitrary urls",
"requiredEnvVars": ["IFRAMELY_URL", "IFRAMELY_API_KEY"] "requiredEnvVars": ["IFRAMELY_API_KEY"]
} }

View File

@@ -0,0 +1,27 @@
import { IsOptional, IsUrl } from "class-validator";
import { Environment } from "@server/env";
import environment from "@server/utils/environment";
import { CannotUseWithout } from "@server/utils/validators";
class IframelyPluginEnvironment extends Environment {
/**
* Iframely url
*/
@IsOptional()
@IsUrl({
require_tld: false,
require_protocol: true,
allow_underscores: true,
protocols: ["http", "https"],
})
public IFRAMELY_URL = environment.IFRAMELY_URL ?? "https://iframe.ly";
/**
* Iframely API key
*/
@IsOptional()
@CannotUseWithout("IFRAMELY_URL")
public IFRAMELY_API_KEY = this.toOptionalString(environment.IFRAMELY_API_KEY);
}
export default new IframelyPluginEnvironment();

View File

@@ -1,9 +1,9 @@
import { Day } from "@shared/utils/time"; import { Day } from "@shared/utils/time";
import env from "@server/env";
import { InternalError } from "@server/errors"; import { InternalError } from "@server/errors";
import Logger from "@server/logging/Logger"; import Logger from "@server/logging/Logger";
import Redis from "@server/storage/redis"; import Redis from "@server/storage/redis";
import fetch from "@server/utils/fetch"; import fetch from "@server/utils/fetch";
import env from "./env";
class Iframely { class Iframely {
private static apiUrl = `${env.IFRAMELY_URL}/api`; private static apiUrl = `${env.IFRAMELY_URL}/api`;

View File

@@ -1,12 +1,5 @@
{ {
"name": "OIDC", "name": "OIDC",
"description": "Adds an OpenID compatible authentication provider.", "description": "Adds an OpenID compatible authentication provider.",
"requiredEnvVars": [ "requiredEnvVars": ["OIDC_CLIENT_ID", "OIDC_CLIENT_SECRET", "OIDC_AUTH_URI", "OIDC_TOKEN_URI", "OIDC_USERINFO_URI"]
"OIDC_CLIENT_ID",
"OIDC_CLIENT_SECRET",
"OIDC_AUTH_URI",
"OIDC_TOKEN_URI",
"OIDC_USERINFO_URI",
"OIDC_DISPLAY_NAME"
]
} }

View File

@@ -5,7 +5,6 @@ import get from "lodash/get";
import { Strategy } from "passport-oauth2"; import { Strategy } from "passport-oauth2";
import { slugifyDomain } from "@shared/utils/domains"; import { slugifyDomain } from "@shared/utils/domains";
import accountProvisioner from "@server/commands/accountProvisioner"; import accountProvisioner from "@server/commands/accountProvisioner";
import env from "@server/env";
import { import {
OIDCMalformedUserInfoError, OIDCMalformedUserInfoError,
AuthenticationError, AuthenticationError,
@@ -19,6 +18,7 @@ import {
getTeamFromContext, getTeamFromContext,
getClientFromContext, getClientFromContext,
} from "@server/utils/passport"; } from "@server/utils/passport";
import env from "../env";
const router = new Router(); const router = new Router();
const providerName = "oidc"; const providerName = "oidc";

View File

@@ -0,0 +1,79 @@
import { IsOptional, IsUrl, MaxLength } from "class-validator";
import { Environment } from "@server/env";
import environment from "@server/utils/environment";
import { CannotUseWithout } from "@server/utils/validators";
class OIDCPluginEnvironment extends Environment {
/**
* OIDC client credentials. To enable authentication with any
* compatible provider.
*/
@IsOptional()
@CannotUseWithout("OIDC_CLIENT_SECRET")
@CannotUseWithout("OIDC_AUTH_URI")
@CannotUseWithout("OIDC_TOKEN_URI")
@CannotUseWithout("OIDC_USERINFO_URI")
@CannotUseWithout("OIDC_DISPLAY_NAME")
public OIDC_CLIENT_ID = this.toOptionalString(environment.OIDC_CLIENT_ID);
@IsOptional()
@CannotUseWithout("OIDC_CLIENT_ID")
public OIDC_CLIENT_SECRET = this.toOptionalString(
environment.OIDC_CLIENT_SECRET
);
/**
* The name of the OIDC provider, eg "GitLab" this will be displayed on the
* sign-in button and other places in the UI. The default value is:
* "OpenID Connect".
*/
@MaxLength(50)
public OIDC_DISPLAY_NAME = environment.OIDC_DISPLAY_NAME ?? "OpenID Connect";
/**
* The OIDC authorization endpoint.
*/
@IsOptional()
@IsUrl({
require_tld: false,
allow_underscores: true,
})
public OIDC_AUTH_URI = this.toOptionalString(environment.OIDC_AUTH_URI);
/**
* The OIDC token endpoint.
*/
@IsOptional()
@IsUrl({
require_tld: false,
allow_underscores: true,
})
public OIDC_TOKEN_URI = this.toOptionalString(environment.OIDC_TOKEN_URI);
/**
* The OIDC userinfo endpoint.
*/
@IsOptional()
@IsUrl({
require_tld: false,
allow_underscores: true,
})
public OIDC_USERINFO_URI = this.toOptionalString(
environment.OIDC_USERINFO_URI
);
/**
* The OIDC profile field to use as the username. The default value is
* "preferred_username".
*/
public OIDC_USERNAME_CLAIM =
environment.OIDC_USERNAME_CLAIM ?? "preferred_username";
/**
* A space separated list of OIDC scopes to request. Defaults to "openid
* profile email".
*/
public OIDC_SCOPES = environment.OIDC_SCOPES ?? "openid profile email";
}
export default new OIDCPluginEnvironment();

View File

@@ -0,0 +1,18 @@
import invariant from "invariant";
import OAuthClient from "@server/utils/oauth";
import env from "./env";
export default class OIDCClient extends OAuthClient {
endpoints = {
authorize: env.OIDC_AUTH_URI || "",
token: env.OIDC_TOKEN_URI || "",
userinfo: env.OIDC_USERINFO_URI || "",
};
constructor() {
invariant(env.OIDC_CLIENT_ID, "OIDC_CLIENT_ID is required");
invariant(env.OIDC_CLIENT_SECRET, "OIDC_CLIENT_SECRET is required");
super(env.OIDC_CLIENT_ID, env.OIDC_CLIENT_SECRET);
}
}

View File

@@ -1,6 +1,5 @@
import randomstring from "randomstring"; import randomstring from "randomstring";
import { IntegrationService } from "@shared/types"; import { IntegrationService } from "@shared/types";
import env from "@server/env";
import { IntegrationAuthentication, SearchQuery } from "@server/models"; import { IntegrationAuthentication, SearchQuery } from "@server/models";
import { import {
buildDocument, buildDocument,
@@ -9,6 +8,7 @@ import {
buildUser, buildUser,
} from "@server/test/factories"; } from "@server/test/factories";
import { getTestServer } from "@server/test/support"; import { getTestServer } from "@server/test/support";
import env from "../env";
import * as Slack from "../slack"; import * as Slack from "../slack";
jest.mock("../slack", () => ({ jest.mock("../slack", () => ({

View File

@@ -4,7 +4,6 @@ import escapeRegExp from "lodash/escapeRegExp";
import { Op } from "sequelize"; import { Op } from "sequelize";
import { z } from "zod"; import { z } from "zod";
import { IntegrationService } from "@shared/types"; import { IntegrationService } from "@shared/types";
import env from "@server/env";
import { import {
AuthenticationError, AuthenticationError,
InvalidRequestError, InvalidRequestError,
@@ -26,6 +25,7 @@ import SearchHelper from "@server/models/helpers/SearchHelper";
import { APIContext } from "@server/types"; import { APIContext } from "@server/types";
import { safeEqual } from "@server/utils/crypto"; import { safeEqual } from "@server/utils/crypto";
import { opts } from "@server/utils/i18n"; import { opts } from "@server/utils/i18n";
import env from "../env";
import presentMessageAttachment from "../presenters/messageAttachment"; import presentMessageAttachment from "../presenters/messageAttachment";
import * as Slack from "../slack"; import * as Slack from "../slack";
import * as T from "./schema"; import * as T from "./schema";

View File

@@ -6,7 +6,6 @@ import { Strategy as SlackStrategy } from "passport-slack-oauth2";
import { IntegrationService, IntegrationType } from "@shared/types"; import { IntegrationService, IntegrationType } from "@shared/types";
import { integrationSettingsPath } from "@shared/utils/routeHelpers"; import { integrationSettingsPath } from "@shared/utils/routeHelpers";
import accountProvisioner from "@server/commands/accountProvisioner"; import accountProvisioner from "@server/commands/accountProvisioner";
import env from "@server/env";
import auth from "@server/middlewares/authentication"; import auth from "@server/middlewares/authentication";
import passportMiddleware from "@server/middlewares/passport"; import passportMiddleware from "@server/middlewares/passport";
import validate from "@server/middlewares/validate"; import validate from "@server/middlewares/validate";
@@ -23,6 +22,7 @@ import {
getTeamFromContext, getTeamFromContext,
StateStore, StateStore,
} from "@server/utils/passport"; } from "@server/utils/passport";
import env from "../env";
import * as Slack from "../slack"; import * as Slack from "../slack";
import * as T from "./schema"; import * as T from "./schema";

View File

@@ -0,0 +1,44 @@
import { IsBoolean, IsOptional } from "class-validator";
import { Environment } from "@server/env";
import Deprecated from "@server/models/decorators/Deprecated";
import environment from "@server/utils/environment";
import { CannotUseWithout } from "@server/utils/validators";
class SlackPluginEnvironment extends Environment {
/**
* Slack OAuth2 client credentials. To enable authentication with Slack.
*/
@IsOptional()
@Deprecated("Use SLACK_CLIENT_SECRET instead")
public SLACK_SECRET = this.toOptionalString(environment.SLACK_SECRET);
@IsOptional()
@Deprecated("Use SLACK_CLIENT_ID instead")
public SLACK_KEY = this.toOptionalString(environment.SLACK_KEY);
@IsOptional()
@CannotUseWithout("SLACK_CLIENT_ID")
public SLACK_CLIENT_SECRET = this.toOptionalString(
environment.SLACK_CLIENT_SECRET ?? environment.SLACK_SECRET
);
/**
* Secret to verify webhook requests received from Slack.
*/
@IsOptional()
public SLACK_VERIFICATION_TOKEN = this.toOptionalString(
environment.SLACK_VERIFICATION_TOKEN
);
/**
* If enabled a "Post to Channel" button will be added to search result
* messages inside of Slack. This also requires setup in Slack UI.
*/
@IsOptional()
@IsBoolean()
public SLACK_MESSAGE_ACTIONS = this.toBoolean(
environment.SLACK_MESSAGE_ACTIONS ?? "false"
);
}
export default new SlackPluginEnvironment();

View File

@@ -2,7 +2,6 @@ import { differenceInMilliseconds } from "date-fns";
import { Op } from "sequelize"; import { Op } from "sequelize";
import { IntegrationService, IntegrationType } from "@shared/types"; import { IntegrationService, IntegrationType } from "@shared/types";
import { Minute } from "@shared/utils/time"; import { Minute } from "@shared/utils/time";
import env from "@server/env";
import { Document, Integration, Collection, Team } from "@server/models"; import { Document, Integration, Collection, Team } from "@server/models";
import BaseProcessor from "@server/queues/processors/BaseProcessor"; import BaseProcessor from "@server/queues/processors/BaseProcessor";
import { import {
@@ -12,6 +11,7 @@ import {
Event, Event,
} from "@server/types"; } from "@server/types";
import fetch from "@server/utils/fetch"; import fetch from "@server/utils/fetch";
import env from "../env";
import presentMessageAttachment from "../presenters/messageAttachment"; import presentMessageAttachment from "../presenters/messageAttachment";
export default class SlackProcessor extends BaseProcessor { export default class SlackProcessor extends BaseProcessor {

View File

@@ -1,7 +1,7 @@
import querystring from "querystring"; import querystring from "querystring";
import env from "@server/env";
import { InvalidRequestError } from "@server/errors"; import { InvalidRequestError } from "@server/errors";
import fetch from "@server/utils/fetch"; import fetch from "@server/utils/fetch";
import env from "./env";
const SLACK_API_URL = "https://slack.com/api"; const SLACK_API_URL = "https://slack.com/api";

View File

@@ -5,7 +5,6 @@ import FormData from "form-data";
import { ensureDirSync } from "fs-extra"; import { ensureDirSync } from "fs-extra";
import { v4 as uuidV4 } from "uuid"; import { v4 as uuidV4 } from "uuid";
import env from "@server/env"; import env from "@server/env";
import "@server/test/env";
import FileStorage from "@server/storage/files"; import FileStorage from "@server/storage/files";
import { buildAttachment, buildUser } from "@server/test/factories"; import { buildAttachment, buildUser } from "@server/test/factories";
import { getTestServer } from "@server/test/support"; import { getTestServer } from "@server/test/support";

View File

@@ -4,7 +4,7 @@
"dialect": "postgres" "dialect": "postgres"
}, },
"test": { "test": {
"use_env_variable": "DATABASE_URL_TEST", "use_env_variable": "DATABASE_URL",
"dialect": "postgres" "dialect": "postgres"
}, },
"production": { "production": {
@@ -20,4 +20,4 @@
"use_env_variable": "DATABASE_URL", "use_env_variable": "DATABASE_URL",
"dialect": "postgres" "dialect": "postgres"
} }
} }

View File

@@ -1,10 +1,5 @@
/* eslint-disable @typescript-eslint/no-var-requires */ // eslint-disable-next-line import/order
import environment from "./utils/environment";
// Load the process environment variables
require("dotenv").config({
silent: true,
});
import os from "os"; import os from "os";
import { import {
validate, validate,
@@ -16,7 +11,6 @@ import {
IsIn, IsIn,
IsEmail, IsEmail,
IsBoolean, IsBoolean,
MaxLength,
} from "class-validator"; } from "class-validator";
import uniq from "lodash/uniq"; import uniq from "lodash/uniq";
import { languages } from "@shared/i18n"; import { languages } from "@shared/i18n";
@@ -25,7 +19,7 @@ import Deprecated from "./models/decorators/Deprecated";
import { getArg } from "./utils/args"; import { getArg } from "./utils/args";
export class Environment { export class Environment {
private validationPromise; protected validationPromise;
constructor() { constructor() {
this.validationPromise = validate(this); this.validationPromise = validate(this);
@@ -44,21 +38,23 @@ export class Environment {
* The current environment name. * The current environment name.
*/ */
@IsIn(["development", "production", "staging", "test"]) @IsIn(["development", "production", "staging", "test"])
public ENVIRONMENT = process.env.NODE_ENV ?? "production"; public ENVIRONMENT = environment.NODE_ENV ?? "production";
/** /**
* The secret key is used for encrypting data. Do not change this value once * The secret key is used for encrypting data. Do not change this value once
* set or your users will be unable to login. * set or your users will be unable to login.
*/ */
@IsByteLength(32, 64) @IsByteLength(32, 64, {
public SECRET_KEY = process.env.SECRET_KEY ?? ""; message: `The SECRET_KEY environment variable is invalid (Use \`openssl rand -hex 32\` to generate a value).`,
})
public SECRET_KEY = environment.SECRET_KEY ?? "";
/** /**
* The secret that should be passed to the cron utility endpoint to enable * The secret that should be passed to the cron utility endpoint to enable
* triggering of scheduled tasks. * triggering of scheduled tasks.
*/ */
@IsNotEmpty() @IsNotEmpty()
public UTILS_SECRET = process.env.UTILS_SECRET ?? ""; public UTILS_SECRET = environment.UTILS_SECRET ?? "";
/** /**
* The url of the database. * The url of the database.
@@ -69,7 +65,7 @@ export class Environment {
allow_underscores: true, allow_underscores: true,
protocols: ["postgres", "postgresql"], protocols: ["postgres", "postgresql"],
}) })
public DATABASE_URL = process.env.DATABASE_URL ?? ""; public DATABASE_URL = environment.DATABASE_URL ?? "";
/** /**
* The url of the database pool. * The url of the database pool.
@@ -81,7 +77,7 @@ export class Environment {
protocols: ["postgres", "postgresql"], protocols: ["postgres", "postgresql"],
}) })
public DATABASE_CONNECTION_POOL_URL = this.toOptionalString( public DATABASE_CONNECTION_POOL_URL = this.toOptionalString(
process.env.DATABASE_CONNECTION_POOL_URL environment.DATABASE_CONNECTION_POOL_URL
); );
/** /**
@@ -90,7 +86,7 @@ export class Environment {
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()
public DATABASE_CONNECTION_POOL_MIN = this.toOptionalNumber( public DATABASE_CONNECTION_POOL_MIN = this.toOptionalNumber(
process.env.DATABASE_CONNECTION_POOL_MIN environment.DATABASE_CONNECTION_POOL_MIN
); );
/** /**
@@ -99,7 +95,7 @@ export class Environment {
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()
public DATABASE_CONNECTION_POOL_MAX = this.toOptionalNumber( public DATABASE_CONNECTION_POOL_MAX = this.toOptionalNumber(
process.env.DATABASE_CONNECTION_POOL_MAX environment.DATABASE_CONNECTION_POOL_MAX
); );
/** /**
@@ -110,7 +106,7 @@ export class Environment {
*/ */
@IsIn(["disable", "allow", "require", "prefer", "verify-ca", "verify-full"]) @IsIn(["disable", "allow", "require", "prefer", "verify-ca", "verify-full"])
@IsOptional() @IsOptional()
public PGSSLMODE = process.env.PGSSLMODE; public PGSSLMODE = environment.PGSSLMODE;
/** /**
* The url of redis. Note that redis does not have a database after the port. * The url of redis. Note that redis does not have a database after the port.
@@ -118,7 +114,7 @@ export class Environment {
* base64-encoded configuration. * base64-encoded configuration.
*/ */
@IsNotEmpty() @IsNotEmpty()
public REDIS_URL = process.env.REDIS_URL; public REDIS_URL = environment.REDIS_URL;
/** /**
* The fully qualified, external facing domain name of the server. * The fully qualified, external facing domain name of the server.
@@ -129,7 +125,7 @@ export class Environment {
require_protocol: true, require_protocol: true,
require_tld: false, require_tld: false,
}) })
public URL = process.env.URL || ""; public URL = environment.URL || "";
/** /**
* If using a Cloudfront/Cloudflare distribution or similar it can be set below. * If using a Cloudfront/Cloudflare distribution or similar it can be set below.
@@ -143,7 +139,7 @@ export class Environment {
require_protocol: true, require_protocol: true,
require_tld: false, require_tld: false,
}) })
public CDN_URL = this.toOptionalString(process.env.CDN_URL); public CDN_URL = this.toOptionalString(environment.CDN_URL);
/** /**
* The fully qualified, external facing domain name of the collaboration * The fully qualified, external facing domain name of the collaboration
@@ -156,7 +152,7 @@ export class Environment {
}) })
@IsOptional() @IsOptional()
public COLLABORATION_URL = this.toOptionalString( public COLLABORATION_URL = this.toOptionalString(
process.env.COLLABORATION_URL environment.COLLABORATION_URL
); );
/** /**
@@ -166,7 +162,7 @@ export class Environment {
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
public COLLABORATION_MAX_CLIENTS_PER_DOCUMENT = parseInt( public COLLABORATION_MAX_CLIENTS_PER_DOCUMENT = parseInt(
process.env.COLLABORATION_MAX_CLIENTS_PER_DOCUMENT || "100", environment.COLLABORATION_MAX_CLIENTS_PER_DOCUMENT || "100",
10 10
); );
@@ -175,18 +171,18 @@ export class Environment {
*/ */
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()
public PORT = this.toOptionalNumber(process.env.PORT) ?? 3000; public PORT = this.toOptionalNumber(environment.PORT) ?? 3000;
/** /**
* Optional extra debugging. Comma separated * Optional extra debugging. Comma separated
*/ */
public DEBUG = process.env.DEBUG || ""; public DEBUG = environment.DEBUG || "";
/** /**
* Configure lowest severity level for server logs * Configure lowest severity level for server logs
*/ */
@IsIn(["error", "warn", "info", "http", "verbose", "debug", "silly"]) @IsIn(["error", "warn", "info", "http", "verbose", "debug", "silly"])
public LOG_LEVEL = process.env.LOG_LEVEL || "info"; public LOG_LEVEL = environment.LOG_LEVEL || "info";
/** /**
* How many processes should be spawned. As a reasonable rule divide your * How many processes should be spawned. As a reasonable rule divide your
@@ -194,7 +190,7 @@ export class Environment {
*/ */
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()
public WEB_CONCURRENCY = this.toOptionalNumber(process.env.WEB_CONCURRENCY); public WEB_CONCURRENCY = this.toOptionalNumber(environment.WEB_CONCURRENCY);
/** /**
* How long a request should be processed before giving up and returning an * How long a request should be processed before giving up and returning an
@@ -203,28 +199,28 @@ export class Environment {
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()
public REQUEST_TIMEOUT = public REQUEST_TIMEOUT =
this.toOptionalNumber(process.env.REQUEST_TIMEOUT) ?? 10 * 1000; this.toOptionalNumber(environment.REQUEST_TIMEOUT) ?? 10 * 1000;
/** /**
* Base64 encoded private key if Outline is to perform SSL termination. * Base64 encoded protected key if Outline is to perform SSL termination.
*/ */
@IsOptional() @IsOptional()
@CannotUseWithout("SSL_CERT") @CannotUseWithout("SSL_CERT")
public SSL_KEY = this.toOptionalString(process.env.SSL_KEY); public SSL_KEY = this.toOptionalString(environment.SSL_KEY);
/** /**
* Base64 encoded public certificate if Outline is to perform SSL termination. * Base64 encoded public certificate if Outline is to perform SSL termination.
*/ */
@IsOptional() @IsOptional()
@CannotUseWithout("SSL_KEY") @CannotUseWithout("SSL_KEY")
public SSL_CERT = this.toOptionalString(process.env.SSL_CERT); public SSL_CERT = this.toOptionalString(environment.SSL_CERT);
/** /**
* The default interface language. See translate.getoutline.com for a list of * The default interface language. See translate.getoutline.com for a list of
* available language codes and their percentage translated. * available language codes and their percentage translated.
*/ */
@IsIn(languages) @IsIn(languages)
public DEFAULT_LANGUAGE = process.env.DEFAULT_LANGUAGE ?? "en_US"; public DEFAULT_LANGUAGE = environment.DEFAULT_LANGUAGE ?? "en_US";
/** /**
* A comma list of which services should be enabled on this instance defaults to all. * A comma list of which services should be enabled on this instance defaults to all.
@@ -235,7 +231,7 @@ export class Environment {
public SERVICES = uniq( public SERVICES = uniq(
( (
getArg("services") ?? getArg("services") ??
process.env.SERVICES ?? environment.SERVICES ??
"collaboration,websockets,worker,web" "collaboration,websockets,worker,web"
) )
.split(",") .split(",")
@@ -248,7 +244,7 @@ export class Environment {
* loadbalancer. * loadbalancer.
*/ */
@IsBoolean() @IsBoolean()
public FORCE_HTTPS = this.toBoolean(process.env.FORCE_HTTPS ?? "true"); public FORCE_HTTPS = this.toBoolean(environment.FORCE_HTTPS ?? "true");
/** /**
* Should the installation send anonymized statistics to the maintainers. * Should the installation send anonymized statistics to the maintainers.
@@ -256,51 +252,51 @@ export class Environment {
*/ */
@IsBoolean() @IsBoolean()
public TELEMETRY = this.toBoolean( public TELEMETRY = this.toBoolean(
process.env.ENABLE_UPDATES ?? process.env.TELEMETRY ?? "true" environment.ENABLE_UPDATES ?? environment.TELEMETRY ?? "true"
); );
/** /**
* An optional comma separated list of allowed domains. * An optional comma separated list of allowed domains.
*/ */
public ALLOWED_DOMAINS = public ALLOWED_DOMAINS =
process.env.ALLOWED_DOMAINS ?? process.env.GOOGLE_ALLOWED_DOMAINS; environment.ALLOWED_DOMAINS ?? environment.GOOGLE_ALLOWED_DOMAINS;
// Third-party services // Third-party services
/** /**
* The host of your SMTP server for enabling emails. * The host of your SMTP server for enabling emails.
*/ */
public SMTP_HOST = process.env.SMTP_HOST; public SMTP_HOST = environment.SMTP_HOST;
/** /**
* Optional hostname of the client, used for identifying to the server * Optional hostname of the client, used for identifying to the server
* defaults to hostname of the machine. * defaults to hostname of the machine.
*/ */
public SMTP_NAME = process.env.SMTP_NAME; public SMTP_NAME = environment.SMTP_NAME;
/** /**
* The port of your SMTP server. * The port of your SMTP server.
*/ */
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()
public SMTP_PORT = this.toOptionalNumber(process.env.SMTP_PORT); public SMTP_PORT = this.toOptionalNumber(environment.SMTP_PORT);
/** /**
* The username of your SMTP server, if any. * The username of your SMTP server, if any.
*/ */
public SMTP_USERNAME = process.env.SMTP_USERNAME; public SMTP_USERNAME = environment.SMTP_USERNAME;
/** /**
* The password for the SMTP username, if any. * The password for the SMTP username, if any.
*/ */
public SMTP_PASSWORD = process.env.SMTP_PASSWORD; public SMTP_PASSWORD = environment.SMTP_PASSWORD;
/** /**
* The email address from which emails are sent. * The email address from which emails are sent.
*/ */
@IsEmail({ allow_display_name: true, allow_ip_domain: true }) @IsEmail({ allow_display_name: true, allow_ip_domain: true })
@IsOptional() @IsOptional()
public SMTP_FROM_EMAIL = this.toOptionalString(process.env.SMTP_FROM_EMAIL); public SMTP_FROM_EMAIL = this.toOptionalString(environment.SMTP_FROM_EMAIL);
/** /**
* The reply-to address for emails sent from Outline. If unset the from * The reply-to address for emails sent from Outline. If unset the from
@@ -308,12 +304,12 @@ export class Environment {
*/ */
@IsEmail({ allow_display_name: true, allow_ip_domain: true }) @IsEmail({ allow_display_name: true, allow_ip_domain: true })
@IsOptional() @IsOptional()
public SMTP_REPLY_EMAIL = this.toOptionalString(process.env.SMTP_REPLY_EMAIL); public SMTP_REPLY_EMAIL = this.toOptionalString(environment.SMTP_REPLY_EMAIL);
/** /**
* Override the cipher used for SMTP SSL connections. * Override the cipher used for SMTP SSL connections.
*/ */
public SMTP_TLS_CIPHERS = this.toOptionalString(process.env.SMTP_TLS_CIPHERS); public SMTP_TLS_CIPHERS = this.toOptionalString(environment.SMTP_TLS_CIPHERS);
/** /**
* If true (the default) the connection will use TLS when connecting to server. * If true (the default) the connection will use TLS when connecting to server.
@@ -322,182 +318,56 @@ export class Environment {
* Setting secure to false therefore does not mean that you would not use an * Setting secure to false therefore does not mean that you would not use an
* encrypted connection. * encrypted connection.
*/ */
public SMTP_SECURE = this.toBoolean(process.env.SMTP_SECURE ?? "true"); public SMTP_SECURE = this.toBoolean(environment.SMTP_SECURE ?? "true");
/** /**
* Sentry DSN for capturing errors and frontend performance. * Sentry DSN for capturing errors and frontend performance.
*/ */
@IsUrl() @IsUrl()
@IsOptional() @IsOptional()
public SENTRY_DSN = this.toOptionalString(process.env.SENTRY_DSN); public SENTRY_DSN = this.toOptionalString(environment.SENTRY_DSN);
/** /**
* Sentry tunnel URL for bypassing ad blockers * Sentry tunnel URL for bypassing ad blockers
*/ */
@IsUrl() @IsUrl()
@IsOptional() @IsOptional()
public SENTRY_TUNNEL = this.toOptionalString(process.env.SENTRY_TUNNEL); public SENTRY_TUNNEL = this.toOptionalString(environment.SENTRY_TUNNEL);
/** /**
* A release SHA or other identifier for Sentry. * A release SHA or other identifier for Sentry.
*/ */
public RELEASE = this.toOptionalString(process.env.RELEASE); public RELEASE = this.toOptionalString(environment.RELEASE);
/** /**
* A Google Analytics tracking ID, supports v3 or v4 properties. * A Google Analytics tracking ID, supports v3 or v4 properties.
*/ */
@IsOptional() @IsOptional()
public GOOGLE_ANALYTICS_ID = this.toOptionalString( public GOOGLE_ANALYTICS_ID = this.toOptionalString(
process.env.GOOGLE_ANALYTICS_ID environment.GOOGLE_ANALYTICS_ID
); );
/** /**
* A DataDog API key for tracking server metrics. * A DataDog API key for tracking server metrics.
*/ */
public DD_API_KEY = process.env.DD_API_KEY; public DD_API_KEY = environment.DD_API_KEY;
/** /**
* The name of the service to use in DataDog. * The name of the service to use in DataDog.
*/ */
public DD_SERVICE = process.env.DD_SERVICE ?? "outline"; public DD_SERVICE = environment.DD_SERVICE ?? "outline";
/**
* Google OAuth2 client credentials. To enable authentication with Google.
*/
@IsOptional()
@CannotUseWithout("GOOGLE_CLIENT_SECRET")
public GOOGLE_CLIENT_ID = this.toOptionalString(process.env.GOOGLE_CLIENT_ID);
@IsOptional() @IsOptional()
@CannotUseWithout("GOOGLE_CLIENT_ID")
public GOOGLE_CLIENT_SECRET = this.toOptionalString(
process.env.GOOGLE_CLIENT_SECRET
);
/**
* Slack OAuth2 client credentials. To enable authentication with Slack.
*/
@IsOptional()
@Deprecated("Use SLACK_CLIENT_SECRET instead")
public SLACK_SECRET = this.toOptionalString(process.env.SLACK_SECRET);
@IsOptional()
@Deprecated("Use SLACK_CLIENT_ID instead")
public SLACK_KEY = this.toOptionalString(process.env.SLACK_KEY);
@IsOptional()
@CannotUseWithout("SLACK_CLIENT_SECRET")
public SLACK_CLIENT_ID = this.toOptionalString( public SLACK_CLIENT_ID = this.toOptionalString(
process.env.SLACK_CLIENT_ID ?? process.env.SLACK_KEY environment.SLACK_CLIENT_ID ?? environment.SLACK_KEY
);
@IsOptional()
@CannotUseWithout("SLACK_CLIENT_ID")
public SLACK_CLIENT_SECRET = this.toOptionalString(
process.env.SLACK_CLIENT_SECRET ?? process.env.SLACK_SECRET
); );
/** /**
* This is used to verify webhook requests received from Slack. * Injected into the `slack-app-id` header meta tag if provided.
*/
@IsOptional()
public SLACK_VERIFICATION_TOKEN = this.toOptionalString(
process.env.SLACK_VERIFICATION_TOKEN
);
/**
* This is injected into the slack-app-id header meta tag if provided.
*/ */
@IsOptional() @IsOptional()
@CannotUseWithout("SLACK_CLIENT_ID") @CannotUseWithout("SLACK_CLIENT_ID")
public SLACK_APP_ID = this.toOptionalString(process.env.SLACK_APP_ID); public SLACK_APP_ID = this.toOptionalString(environment.SLACK_APP_ID);
/**
* If enabled a "Post to Channel" button will be added to search result
* messages inside of Slack. This also requires setup in Slack UI.
*/
@IsOptional()
@IsBoolean()
public SLACK_MESSAGE_ACTIONS = this.toBoolean(
process.env.SLACK_MESSAGE_ACTIONS ?? "false"
);
/**
* Azure OAuth2 client credentials. To enable authentication with Azure.
*/
@IsOptional()
@CannotUseWithout("AZURE_CLIENT_SECRET")
public AZURE_CLIENT_ID = this.toOptionalString(process.env.AZURE_CLIENT_ID);
@IsOptional()
@CannotUseWithout("AZURE_CLIENT_ID")
public AZURE_CLIENT_SECRET = this.toOptionalString(
process.env.AZURE_CLIENT_SECRET
);
@IsOptional()
@CannotUseWithout("AZURE_CLIENT_ID")
public AZURE_RESOURCE_APP_ID = this.toOptionalString(
process.env.AZURE_RESOURCE_APP_ID
);
/**
* OIDC client credentials. To enable authentication with any
* compatible provider.
*/
@IsOptional()
@CannotUseWithout("OIDC_CLIENT_SECRET")
@CannotUseWithout("OIDC_AUTH_URI")
@CannotUseWithout("OIDC_TOKEN_URI")
@CannotUseWithout("OIDC_USERINFO_URI")
@CannotUseWithout("OIDC_DISPLAY_NAME")
public OIDC_CLIENT_ID = this.toOptionalString(process.env.OIDC_CLIENT_ID);
@IsOptional()
@CannotUseWithout("OIDC_CLIENT_ID")
public OIDC_CLIENT_SECRET = this.toOptionalString(
process.env.OIDC_CLIENT_SECRET
);
/**
* The name of the OIDC provider, eg "GitLab" this will be displayed on the
* sign-in button and other places in the UI. The default value is:
* "OpenID Connect".
*/
@MaxLength(50)
public OIDC_DISPLAY_NAME = process.env.OIDC_DISPLAY_NAME ?? "OpenID Connect";
/**
* The OIDC authorization endpoint.
*/
@IsOptional()
@IsUrl({
require_tld: false,
allow_underscores: true,
})
public OIDC_AUTH_URI = this.toOptionalString(process.env.OIDC_AUTH_URI);
/**
* The OIDC token endpoint.
*/
@IsOptional()
@IsUrl({
require_tld: false,
allow_underscores: true,
})
public OIDC_TOKEN_URI = this.toOptionalString(process.env.OIDC_TOKEN_URI);
/**
* The OIDC userinfo endpoint.
*/
@IsOptional()
@IsUrl({
require_tld: false,
allow_underscores: true,
})
public OIDC_USERINFO_URI = this.toOptionalString(
process.env.OIDC_USERINFO_URI
);
/** /**
* Disable autoredirect to the OIDC login page if there is only one * Disable autoredirect to the OIDC login page if there is only one
@@ -506,7 +376,7 @@ export class Environment {
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
public OIDC_DISABLE_REDIRECT = this.toOptionalBoolean( public OIDC_DISABLE_REDIRECT = this.toOptionalBoolean(
process.env.OIDC_DISABLE_REDIRECT environment.OIDC_DISABLE_REDIRECT
); );
/** /**
@@ -517,20 +387,7 @@ export class Environment {
require_tld: false, require_tld: false,
allow_underscores: true, allow_underscores: true,
}) })
public OIDC_LOGOUT_URI = this.toOptionalString(process.env.OIDC_LOGOUT_URI); public OIDC_LOGOUT_URI = this.toOptionalString(environment.OIDC_LOGOUT_URI);
/**
* The OIDC profile field to use as the username. The default value is
* "preferred_username".
*/
public OIDC_USERNAME_CLAIM =
process.env.OIDC_USERNAME_CLAIM ?? "preferred_username";
/**
* A space separated list of OIDC scopes to request. Defaults to "openid
* profile email".
*/
public OIDC_SCOPES = process.env.OIDC_SCOPES ?? "openid profile email";
/** /**
* A string representing the version of the software. * A string representing the version of the software.
@@ -539,7 +396,7 @@ export class Environment {
* SOURCE_VERSION is used by Heroku * SOURCE_VERSION is used by Heroku
*/ */
public VERSION = this.toOptionalString( public VERSION = this.toOptionalString(
process.env.SOURCE_COMMIT || process.env.SOURCE_VERSION environment.SOURCE_COMMIT || environment.SOURCE_VERSION
); );
/** /**
@@ -548,7 +405,7 @@ export class Environment {
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
public RATE_LIMITER_ENABLED = this.toBoolean( public RATE_LIMITER_ENABLED = this.toBoolean(
process.env.RATE_LIMITER_ENABLED ?? "false" environment.RATE_LIMITER_ENABLED ?? "false"
); );
/** /**
@@ -559,7 +416,7 @@ export class Environment {
@IsNumber() @IsNumber()
@CannotUseWithout("RATE_LIMITER_ENABLED") @CannotUseWithout("RATE_LIMITER_ENABLED")
public RATE_LIMITER_REQUESTS = public RATE_LIMITER_REQUESTS =
this.toOptionalNumber(process.env.RATE_LIMITER_REQUESTS) ?? 1000; this.toOptionalNumber(environment.RATE_LIMITER_REQUESTS) ?? 1000;
/** /**
* Set max allowed realtime connections before throttling. Defaults to 50 * Set max allowed realtime connections before throttling. Defaults to 50
@@ -568,7 +425,7 @@ export class Environment {
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
public RATE_LIMITER_COLLABORATION_REQUESTS = public RATE_LIMITER_COLLABORATION_REQUESTS =
this.toOptionalNumber(process.env.RATE_LIMITER_COLLABORATION_REQUESTS) ?? this.toOptionalNumber(environment.RATE_LIMITER_COLLABORATION_REQUESTS) ??
50; 50;
/** /**
@@ -579,7 +436,7 @@ export class Environment {
@IsNumber() @IsNumber()
@CannotUseWithout("RATE_LIMITER_ENABLED") @CannotUseWithout("RATE_LIMITER_ENABLED")
public RATE_LIMITER_DURATION_WINDOW = public RATE_LIMITER_DURATION_WINDOW =
this.toOptionalNumber(process.env.RATE_LIMITER_DURATION_WINDOW) ?? 60; this.toOptionalNumber(environment.RATE_LIMITER_DURATION_WINDOW) ?? 60;
/** /**
* Set max allowed upload size for file attachments. * Set max allowed upload size for file attachments.
@@ -589,7 +446,7 @@ export class Environment {
@IsNumber() @IsNumber()
@Deprecated("Use FILE_STORAGE_UPLOAD_MAX_SIZE instead") @Deprecated("Use FILE_STORAGE_UPLOAD_MAX_SIZE instead")
public AWS_S3_UPLOAD_MAX_SIZE = this.toOptionalNumber( public AWS_S3_UPLOAD_MAX_SIZE = this.toOptionalNumber(
process.env.AWS_S3_UPLOAD_MAX_SIZE environment.AWS_S3_UPLOAD_MAX_SIZE
); );
/** /**
@@ -597,7 +454,7 @@ export class Environment {
*/ */
@IsOptional() @IsOptional()
public AWS_ACCESS_KEY_ID = this.toOptionalString( public AWS_ACCESS_KEY_ID = this.toOptionalString(
process.env.AWS_ACCESS_KEY_ID environment.AWS_ACCESS_KEY_ID
); );
/** /**
@@ -606,35 +463,35 @@ export class Environment {
@IsOptional() @IsOptional()
@CannotUseWithout("AWS_ACCESS_KEY_ID") @CannotUseWithout("AWS_ACCESS_KEY_ID")
public AWS_SECRET_ACCESS_KEY = this.toOptionalString( public AWS_SECRET_ACCESS_KEY = this.toOptionalString(
process.env.AWS_SECRET_ACCESS_KEY environment.AWS_SECRET_ACCESS_KEY
); );
/** /**
* The name of the AWS S3 region to use. * The name of the AWS S3 region to use.
*/ */
@IsOptional() @IsOptional()
public AWS_REGION = this.toOptionalString(process.env.AWS_REGION); public AWS_REGION = this.toOptionalString(environment.AWS_REGION);
/** /**
* Optional AWS S3 endpoint URL for file attachments. * Optional AWS S3 endpoint URL for file attachments.
*/ */
@IsOptional() @IsOptional()
public AWS_S3_ACCELERATE_URL = this.toOptionalString( public AWS_S3_ACCELERATE_URL = this.toOptionalString(
process.env.AWS_S3_ACCELERATE_URL environment.AWS_S3_ACCELERATE_URL
); );
/** /**
* Optional AWS S3 endpoint URL for file attachments. * Optional AWS S3 endpoint URL for file attachments.
*/ */
@IsOptional() @IsOptional()
public AWS_S3_UPLOAD_BUCKET_URL = process.env.AWS_S3_UPLOAD_BUCKET_URL ?? ""; public AWS_S3_UPLOAD_BUCKET_URL = environment.AWS_S3_UPLOAD_BUCKET_URL ?? "";
/** /**
* The bucket name to store file attachments in. * The bucket name to store file attachments in.
*/ */
@IsOptional() @IsOptional()
public AWS_S3_UPLOAD_BUCKET_NAME = this.toOptionalString( public AWS_S3_UPLOAD_BUCKET_NAME = this.toOptionalString(
process.env.AWS_S3_UPLOAD_BUCKET_NAME environment.AWS_S3_UPLOAD_BUCKET_NAME
); );
/** /**
@@ -643,26 +500,26 @@ export class Environment {
*/ */
@IsOptional() @IsOptional()
public AWS_S3_FORCE_PATH_STYLE = this.toBoolean( public AWS_S3_FORCE_PATH_STYLE = this.toBoolean(
process.env.AWS_S3_FORCE_PATH_STYLE ?? "true" environment.AWS_S3_FORCE_PATH_STYLE ?? "true"
); );
/** /**
* Set default AWS S3 ACL for file attachments. * Set default AWS S3 ACL for file attachments.
*/ */
@IsOptional() @IsOptional()
public AWS_S3_ACL = process.env.AWS_S3_ACL ?? "private"; public AWS_S3_ACL = environment.AWS_S3_ACL ?? "private";
/** /**
* Which file storage system to use * Which file storage system to use
*/ */
@IsIn(["local", "s3"]) @IsIn(["local", "s3"])
public FILE_STORAGE = this.toOptionalString(process.env.FILE_STORAGE) ?? "s3"; public FILE_STORAGE = this.toOptionalString(environment.FILE_STORAGE) ?? "s3";
/** /**
* Set default root dir path for local file storage * Set default root dir path for local file storage
*/ */
public FILE_STORAGE_LOCAL_ROOT_DIR = public FILE_STORAGE_LOCAL_ROOT_DIR =
this.toOptionalString(process.env.FILE_STORAGE_LOCAL_ROOT_DIR) ?? this.toOptionalString(environment.FILE_STORAGE_LOCAL_ROOT_DIR) ??
"/var/lib/outline/data"; "/var/lib/outline/data";
/** /**
@@ -670,8 +527,8 @@ export class Environment {
*/ */
@IsNumber() @IsNumber()
public FILE_STORAGE_UPLOAD_MAX_SIZE = public FILE_STORAGE_UPLOAD_MAX_SIZE =
this.toOptionalNumber(process.env.FILE_STORAGE_UPLOAD_MAX_SIZE) ?? this.toOptionalNumber(environment.FILE_STORAGE_UPLOAD_MAX_SIZE) ??
this.toOptionalNumber(process.env.AWS_S3_UPLOAD_MAX_SIZE) ?? this.toOptionalNumber(environment.AWS_S3_UPLOAD_MAX_SIZE) ??
1000000; 1000000;
/** /**
@@ -679,9 +536,9 @@ export class Environment {
*/ */
@IsNumber() @IsNumber()
public FILE_STORAGE_IMPORT_MAX_SIZE = public FILE_STORAGE_IMPORT_MAX_SIZE =
this.toOptionalNumber(process.env.FILE_STORAGE_IMPORT_MAX_SIZE) ?? this.toOptionalNumber(environment.FILE_STORAGE_IMPORT_MAX_SIZE) ??
this.toOptionalNumber(process.env.MAXIMUM_IMPORT_SIZE) ?? this.toOptionalNumber(environment.MAXIMUM_IMPORT_SIZE) ??
this.toOptionalNumber(process.env.FILE_STORAGE_UPLOAD_MAX_SIZE) ?? this.toOptionalNumber(environment.FILE_STORAGE_UPLOAD_MAX_SIZE) ??
1000000; 1000000;
/** /**
@@ -689,9 +546,9 @@ export class Environment {
*/ */
@IsNumber() @IsNumber()
public FILE_STORAGE_WORKSPACE_IMPORT_MAX_SIZE = public FILE_STORAGE_WORKSPACE_IMPORT_MAX_SIZE =
this.toOptionalNumber(process.env.FILE_STORAGE_WORKSPACE_IMPORT_MAX_SIZE) ?? this.toOptionalNumber(environment.FILE_STORAGE_WORKSPACE_IMPORT_MAX_SIZE) ??
this.toOptionalNumber(process.env.MAXIMUM_IMPORT_SIZE) ?? this.toOptionalNumber(environment.MAXIMUM_IMPORT_SIZE) ??
this.toOptionalNumber(process.env.FILE_STORAGE_UPLOAD_MAX_SIZE) ?? this.toOptionalNumber(environment.FILE_STORAGE_UPLOAD_MAX_SIZE) ??
1000000; 1000000;
/** /**
@@ -705,7 +562,7 @@ export class Environment {
@IsNumber() @IsNumber()
@Deprecated("Use FILE_STORAGE_IMPORT_MAX_SIZE instead") @Deprecated("Use FILE_STORAGE_IMPORT_MAX_SIZE instead")
public MAXIMUM_IMPORT_SIZE = this.toOptionalNumber( public MAXIMUM_IMPORT_SIZE = this.toOptionalNumber(
process.env.MAXIMUM_IMPORT_SIZE environment.MAXIMUM_IMPORT_SIZE
); );
/** /**
@@ -714,33 +571,14 @@ export class Environment {
*/ */
@IsNumber() @IsNumber()
public MAXIMUM_EXPORT_SIZE = public MAXIMUM_EXPORT_SIZE =
this.toOptionalNumber(process.env.MAXIMUM_EXPORT_SIZE) ?? os.totalmem(); this.toOptionalNumber(environment.MAXIMUM_EXPORT_SIZE) ?? os.totalmem();
/**
* Iframely url
*/
@IsOptional()
@IsUrl({
require_tld: false,
require_protocol: true,
allow_underscores: true,
protocols: ["http", "https"],
})
public IFRAMELY_URL = process.env.IFRAMELY_URL ?? "https://iframe.ly";
/**
* Iframely API key
*/
@IsOptional()
@CannotUseWithout("IFRAMELY_URL")
public IFRAMELY_API_KEY = this.toOptionalString(process.env.IFRAMELY_API_KEY);
/** /**
* Enable unsafe-inline in script-src CSP directive * Enable unsafe-inline in script-src CSP directive
*/ */
@IsBoolean() @IsBoolean()
public DEVELOPMENT_UNSAFE_INLINE_CSP = this.toBoolean( public DEVELOPMENT_UNSAFE_INLINE_CSP = this.toBoolean(
process.env.DEVELOPMENT_UNSAFE_INLINE_CSP ?? "false" environment.DEVELOPMENT_UNSAFE_INLINE_CSP ?? "false"
); );
/** /**
@@ -781,11 +619,11 @@ export class Environment {
return this.ENVIRONMENT === "test"; return this.ENVIRONMENT === "test";
} }
private toOptionalString(value: string | undefined) { protected toOptionalString(value: string | undefined) {
return value ? value : undefined; return value ? value : undefined;
} }
private toOptionalNumber(value: string | undefined) { protected toOptionalNumber(value: string | undefined) {
return value ? parseInt(value, 10) : undefined; return value ? parseInt(value, 10) : undefined;
} }
@@ -801,7 +639,7 @@ export class Environment {
* @param value The string to convert * @param value The string to convert
* @returns A boolean * @returns A boolean
*/ */
private toBoolean(value: string) { protected toBoolean(value: string) {
try { try {
return value ? !!JSON.parse(value) : false; return value ? !!JSON.parse(value) : false;
} catch (err) { } catch (err) {
@@ -823,7 +661,7 @@ export class Environment {
* @param value The string to convert * @param value The string to convert
* @returns A boolean or undefined * @returns A boolean or undefined
*/ */
private toOptionalBoolean(value: string | undefined) { protected toOptionalBoolean(value: string | undefined) {
try { try {
return value ? !!JSON.parse(value) : undefined; return value ? !!JSON.parse(value) : undefined;
} catch (err) { } catch (err) {
@@ -832,6 +670,4 @@ export class Environment {
} }
} }
const env = new Environment(); export default new Environment();
export default env;

View File

@@ -16,17 +16,18 @@ import {
IsUUID, IsUUID,
PrimaryKey, PrimaryKey,
} from "sequelize-typescript"; } from "sequelize-typescript";
import env from "@server/env";
import Model from "@server/models/base/Model"; import Model from "@server/models/base/Model";
import AzureClient from "@server/utils/azure";
import GoogleClient from "@server/utils/google";
import OIDCClient from "@server/utils/oidc";
import { ValidationError } from "../errors"; import { ValidationError } from "../errors";
import Team from "./Team"; import Team from "./Team";
import UserAuthentication from "./UserAuthentication"; import UserAuthentication from "./UserAuthentication";
import Fix from "./decorators/Fix"; import Fix from "./decorators/Fix";
import Length from "./validators/Length"; import Length from "./validators/Length";
// TODO: Avoid this hardcoding of plugins
import AzureClient from "plugins/azure/server/azure";
import GoogleClient from "plugins/google/server/google";
import OIDCClient from "plugins/oidc/server/oidc";
@Table({ @Table({
tableName: "authentication_providers", tableName: "authentication_providers",
modelName: "authentication_provider", modelName: "authentication_provider",
@@ -86,20 +87,11 @@ class AuthenticationProvider extends Model<
get oauthClient() { get oauthClient() {
switch (this.name) { switch (this.name) {
case "google": case "google":
return new GoogleClient( return new GoogleClient();
env.GOOGLE_CLIENT_ID || "",
env.GOOGLE_CLIENT_SECRET || ""
);
case "azure": case "azure":
return new AzureClient( return new AzureClient();
env.AZURE_CLIENT_ID || "",
env.AZURE_CLIENT_SECRET || ""
);
case "oidc": case "oidc":
return new OIDCClient( return new OIDCClient();
env.OIDC_CLIENT_ID || "",
env.OIDC_CLIENT_SECRET || ""
);
default: default:
return undefined; return undefined;
} }

View File

@@ -6,6 +6,7 @@ import find from "lodash/find";
import sortBy from "lodash/sortBy"; import sortBy from "lodash/sortBy";
import env from "@server/env"; import env from "@server/env";
import Team from "@server/models/Team"; import Team from "@server/models/Team";
import environment from "@server/utils/environment";
export type AuthenticationProviderConfig = { export type AuthenticationProviderConfig = {
id: string; id: string;
@@ -49,7 +50,7 @@ export default class AuthenticationHelper {
// Test the all required env vars are set for the auth provider // Test the all required env vars are set for the auth provider
const enabled = (config.requiredEnvVars ?? []).every( const enabled = (config.requiredEnvVars ?? []).every(
(name: string) => !!env[name] (name: string) => !!environment[name]
); );
if (enabled) { if (enabled) {

View File

@@ -1,27 +0,0 @@
import env from "../env";
// test environment variables
env.SMTP_HOST = "smtp.example.com";
env.ENVIRONMENT = "test";
env.GOOGLE_CLIENT_ID = "123";
env.GOOGLE_CLIENT_SECRET = "123";
env.SLACK_CLIENT_ID = "123";
env.SLACK_CLIENT_SECRET = "123";
env.AZURE_CLIENT_ID = undefined;
env.AZURE_CLIENT_SECRET = undefined;
env.OIDC_CLIENT_ID = "client-id";
env.OIDC_CLIENT_SECRET = "client-secret";
env.OIDC_AUTH_URI = "http://localhost/authorize";
env.OIDC_TOKEN_URI = "http://localhost/token";
env.OIDC_USERINFO_URI = "http://localhost/userinfo";
env.RATE_LIMITER_ENABLED = false;
env.FILE_STORAGE = "local";
env.FILE_STORAGE_LOCAL_ROOT_DIR = "/tmp";
env.IFRAMELY_API_KEY = "123";
if (process.env.DATABASE_URL_TEST) {
env.DATABASE_URL = process.env.DATABASE_URL_TEST;
}

View File

@@ -1,4 +1,3 @@
import "./env";
import { sequelize } from "@server/storage/database"; import { sequelize } from "@server/storage/database";
module.exports = async function () { module.exports = async function () {

View File

@@ -6,8 +6,6 @@ declare module "formidable/lib/file";
declare module "oy-vey"; declare module "oy-vey";
declare module "dotenv";
declare module "email-providers" { declare module "email-providers" {
const list: string[]; const list: string[];
export default list; export default list;

View File

@@ -0,0 +1,39 @@
import fs from "fs";
import path from "path";
import dotenv from "dotenv";
let environment: Record<string, string> = {};
const envPath = path.resolve(process.cwd(), `.env`);
const envDefault = fs.existsSync(envPath)
? dotenv.parse(fs.readFileSync(envPath, "utf8"))
: {};
// Load environment specific variables, in reverse order of precedence
const environments = ["production", "development", "local", "test"];
for (const env of environments) {
const isEnv = process.env.NODE_ENV === env || envDefault.NODE_ENV === env;
const isLocalDevelopment =
env === "local" &&
(process.env.NODE_ENV === "development" ||
envDefault.NODE_ENV === "development");
if (isEnv || isLocalDevelopment) {
const resolvedPath = path.resolve(process.cwd(), `.env.${env}`);
if (fs.existsSync(resolvedPath)) {
environment = {
...environment,
...dotenv.parse(fs.readFileSync(resolvedPath, "utf8")),
};
}
}
}
process.env = {
...envDefault,
...environment,
...process.env,
};
export default process.env;

View File

@@ -1,9 +0,0 @@
import OAuthClient from "./oauth";
export default class GoogleClient extends OAuthClient {
endpoints = {
authorize: "https://accounts.google.com/o/oauth2/auth",
token: "https://accounts.google.com/o/oauth2/token",
userinfo: "https://www.googleapis.com/oauth2/v3/userinfo",
};
}

View File

@@ -1,10 +0,0 @@
import env from "@server/env";
import OAuthClient from "./oauth";
export default class OIDCClient extends OAuthClient {
endpoints = {
authorize: env.OIDC_AUTH_URI || "",
token: env.OIDC_TOKEN_URI || "",
userinfo: env.OIDC_USERINFO_URI || "",
};
}

View File

@@ -4,6 +4,7 @@ import glob from "glob";
import env from "@server/env"; import env from "@server/env";
import Logger from "@server/logging/Logger"; import Logger from "@server/logging/Logger";
import { UnfurlResolver } from "@server/types"; import { UnfurlResolver } from "@server/types";
import environment from "./environment";
const rootDir = env.ENVIRONMENT === "test" ? "" : "build"; const rootDir = env.ENVIRONMENT === "test" ? "" : "build";
@@ -25,7 +26,7 @@ const resolvers: Record<string, UnfurlResolver> = plugins.reduce(
// Test the all required env vars are set for the resolver // Test the all required env vars are set for the resolver
const enabled = (config.requiredEnvVars ?? []).every( const enabled = (config.requiredEnvVars ?? []).every(
(name: string) => !!env[name] (name: string) => !!environment[name]
); );
if (!enabled) { if (!enabled) {
return resolvers; return resolvers;

View File

@@ -2,20 +2,15 @@ import fs from "fs";
import path from "path"; import path from "path";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import browserslistToEsbuild from "browserslist-to-esbuild"; import browserslistToEsbuild from "browserslist-to-esbuild";
import dotenv from "dotenv";
import { webpackStats } from "rollup-plugin-webpack-stats"; import { webpackStats } from "rollup-plugin-webpack-stats";
import { CommonServerOptions, defineConfig } from "vite"; import { CommonServerOptions, defineConfig } from "vite";
import { VitePWA } from "vite-plugin-pwa"; import { VitePWA } from "vite-plugin-pwa";
import { viteStaticCopy } from "vite-plugin-static-copy"; import { viteStaticCopy } from "vite-plugin-static-copy";
import environment from "./server/utils/environment";
// Load the process environment variables
dotenv.config({
silent: true,
});
let httpsConfig: CommonServerOptions["https"] | undefined; let httpsConfig: CommonServerOptions["https"] | undefined;
if (process.env.NODE_ENV === "development") { if (environment.NODE_ENV === "development") {
try { try {
httpsConfig = { httpsConfig = {
key: fs.readFileSync("./server/config/certs/private.key"), key: fs.readFileSync("./server/config/certs/private.key"),
@@ -31,13 +26,13 @@ export default () =>
defineConfig({ defineConfig({
root: "./", root: "./",
publicDir: "./server/static", publicDir: "./server/static",
base: (process.env.CDN_URL ?? "") + "/static/", base: (environment.CDN_URL ?? "") + "/static/",
server: { server: {
port: 3001, port: 3001,
host: true, host: true,
https: httpsConfig, https: httpsConfig,
fs: fs:
process.env.NODE_ENV === "development" environment.NODE_ENV === "development"
? { ? {
// Allow serving files from one level up to the project root // Allow serving files from one level up to the project root
allow: [".."], allow: [".."],
@@ -91,7 +86,7 @@ export default () =>
globPatterns: ["**/*.{js,css,ico,png,svg}"], globPatterns: ["**/*.{js,css,ico,png,svg}"],
navigateFallback: null, navigateFallback: null,
modifyURLPrefix: { modifyURLPrefix: {
"": `${process.env.CDN_URL ?? ""}/static/`, "": `${environment.CDN_URL ?? ""}/static/`,
}, },
runtimeCaching: [ runtimeCaching: [
{ {

View File

@@ -2864,6 +2864,13 @@
resolved "https://registry.yarnpkg.com/@types/diff/-/diff-5.0.4.tgz#ba774c225ee68ce13a090fec16cf34b97a78537b" resolved "https://registry.yarnpkg.com/@types/diff/-/diff-5.0.4.tgz#ba774c225ee68ce13a090fec16cf34b97a78537b"
integrity "sha1-undMIl7mjOE6CQ/sFs80uXp4U3s= sha512-d7489/WO4B65k0SIqxXtviR9+MrPDipWQF6w+5D7YPrqgu6Qb87JsTdWQaNZo7itcdbViQSev3Jaz7dtKO0+Dg==" integrity "sha1-undMIl7mjOE6CQ/sFs80uXp4U3s= sha512-d7489/WO4B65k0SIqxXtviR9+MrPDipWQF6w+5D7YPrqgu6Qb87JsTdWQaNZo7itcdbViQSev3Jaz7dtKO0+Dg=="
"@types/dotenv@^8.2.0":
version "8.2.0"
resolved "https://registry.yarnpkg.com/@types/dotenv/-/dotenv-8.2.0.tgz#5cd64710c3c98e82d9d15844375a33bf1b45d053"
integrity sha512-ylSC9GhfRH7m1EUXBXofhgx4lUWmFeQDINW5oLuS+gxWdfUeW4zJdeVTYVkexEW+e2VUvlZR2kGnGGipAWR7kw==
dependencies:
dotenv "*"
"@types/emoji-regex@^9.2.0": "@types/emoji-regex@^9.2.0":
version "9.2.0" version "9.2.0"
resolved "https://registry.yarnpkg.com/@types/emoji-regex/-/emoji-regex-9.2.0.tgz#2e117de04f5fa561c5dcbe43a860ecd856517525" resolved "https://registry.yarnpkg.com/@types/emoji-regex/-/emoji-regex-9.2.0.tgz#2e117de04f5fa561c5dcbe43a860ecd856517525"
@@ -5895,16 +5902,16 @@ dot-prop@^5.2.0:
dependencies: dependencies:
is-obj "^2.0.0" is-obj "^2.0.0"
dotenv@*, dotenv@^16.4.5:
version "16.4.5"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
dotenv@16.3.1: dotenv@16.3.1:
version "16.3.1" version "16.3.1"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
integrity "sha1-NpA03n1+WxIJcmkzUqO/ESFyzD4= sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==" integrity "sha1-NpA03n1+WxIJcmkzUqO/ESFyzD4= sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ=="
dotenv@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d"
integrity "sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0= sha512-XcaMACOr3JMVcEv0Y/iUM2XaOsATRZ3U1In41/1jjK6vJZ2PZbQ1bzCG8uvaByfaBpl9gqc9QWJovpUGBXLLYQ=="
dottie@^2.0.6: dottie@^2.0.6:
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/dottie/-/dottie-2.0.6.tgz#34564ebfc6ec5e5772272d466424ad5b696484d4" resolved "https://registry.yarnpkg.com/dottie/-/dottie-2.0.6.tgz#34564ebfc6ec5e5772272d466424ad5b696484d4"