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
environment:
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
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
executors:
@ -89,7 +84,7 @@ jobs:
key: dependency-cache-v1-{{ checksum "package.json" }}
- run:
name: migrate
command: ./node_modules/.bin/sequelize db:migrate --url $DATABASE_URL_TEST
command: ./node_modules/.bin/sequelize db:migrate
- run:
name: test
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
# should work out of the box.
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_MAX=
# 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
# proxy the port in URL and PORT may be different.
URL=https://app.outline.dev:3000
URL=
PORT=3000
# See [documentation](docs/SERVICES.md) on running a separate collaboration
@ -166,9 +165,6 @@ SLACK_VERIFICATION_TOKEN=your_token
SLACK_APP_ID=A0XXXXXXX
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,
# 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)
@ -181,8 +177,8 @@ SMTP_HOST=
SMTP_PORT=
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM_EMAIL=hello@example.com
SMTP_REPLY_EMAIL=hello@example.com
SMTP_FROM_EMAIL=
SMTP_REPLY_EMAIL=
SMTP_TLS_CIPHERS=
SMTP_SECURE=true
@ -198,10 +194,5 @@ RATE_LIMITER_REQUESTS=1000
RATE_LIMITER_DURATION_WINDOW=60
# Iframely API config
# IFRAMELY_URL=
# 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
IFRAMELY_URL=
IFRAMELY_API_KEY=

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
node_modules/*
.env
.env.local
.env.production
.log
.vscode/*
npm-debug.log

View File

@ -9,7 +9,7 @@
"^@server/(.*)$": "<rootDir>/server/$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"],
"globalSetup": "<rootDir>/server/test/globalSetup.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');
@ -6,5 +8,4 @@ module.exports = {
'config': path.resolve('server/config', 'database.json'),
'migrations-path': path.resolve('server', 'migrations'),
'models-path': path.resolve('server', 'models'),
'seeders-path': path.resolve('server/models', 'fixtures'),
}

View File

@ -1,28 +1,28 @@
up:
docker-compose up -d redis postgres
docker compose up -d redis postgres
yarn install-local-ssl
yarn install --pure-lockfile
yarn dev:watch
build:
docker-compose build --pull outline
docker compose build --pull outline
test:
docker-compose up -d redis postgres
yarn sequelize db:drop --env=test
yarn sequelize db:create --env=test
NODE_ENV=test yarn sequelize db:migrate --env=test
docker compose up -d redis postgres
NODE_ENV=test yarn sequelize db:drop
NODE_ENV=test yarn sequelize db:create
NODE_ENV=test yarn sequelize db:migrate
yarn test
watch:
docker-compose up -d redis postgres
yarn sequelize db:drop --env=test
yarn sequelize db:create --env=test
NODE_ENV=test yarn sequelize db:migrate --env=test
docker compose up -d redis postgres
NODE_ENV=test yarn sequelize db:drop
NODE_ENV=test yarn sequelize db:create
NODE_ENV=test yarn sequelize db:migrate
yarn test:watch
destroy:
docker-compose stop
docker-compose rm -f
docker compose stop
docker compose rm -f
.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 { createAction } from "~/actions";
import { NavigationSection, RecentSearchesSection } from "~/actions/sections";
import env from "~/env";
import Desktop from "~/utils/Desktop";
import history from "~/utils/history";
import isCloudHosted from "~/utils/isCloudHosted";
@ -212,9 +211,6 @@ export const logout = createAction({
icon: <LogoutIcon />,
perform: () => {
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 { Redirect } from "react-router-dom";
import env from "~/env";
import useStores from "~/hooks/useStores";
const Logout = () => {
const { auth } = useStores();
void auth.logout();
if (env.OIDC_LOGOUT_URI) {
window.location.replace(env.OIDC_LOGOUT_URI);
return null;
}
return <Redirect to="/" />;
};

View File

@ -14,6 +14,7 @@ import { PartialWithId } from "~/types";
import { client } from "~/utils/ApiClient";
import Desktop from "~/utils/Desktop";
import Logger from "~/utils/Logger";
import history from "~/utils/history";
import isCloudHosted from "~/utils/isCloudHosted";
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
logout = async (
/** 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
) => {
logout = async (savePath = false, tryRevokingToken = true) => {
// if this logout was forced from an authenticated route then
// save the current path so we can go back there once signed in
if (savePath) {
@ -348,9 +348,16 @@ export default class AuthStore extends Store<Team> {
this.currentUserId = null;
this.currentTeamId = null;
this.collaborationToken = null;
this.rootStore.clear();
// Tell the host application we logged out, if any allows window cleanup.
void Desktop.bridge?.onLogout?.();
this.rootStore.clear();
if (Desktop.isElectron()) {
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",
"dd-trace": "^3.33.0",
"diff": "^5.1.0",
"dotenv": "^4.0.0",
"dotenv": "^16.4.5",
"email-providers": "^1.14.0",
"emoji-mart": "^5.5.2",
"emoji-regex": "^10.3.0",
@ -247,6 +247,7 @@
"@types/body-scroll-lock": "^3.1.0",
"@types/crypto-js": "^4.2.1",
"@types/diff": "^5.0.4",
"@types/dotenv": "^8.2.0",
"@types/emoji-regex": "^9.2.0",
"@types/express-useragent": "^1.0.2",
"@types/formidable": "^2.0.6",

View File

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

View File

@ -1,6 +1,7 @@
import invariant from "invariant";
import JWT from "jsonwebtoken";
import env from "@server/env";
import OAuthClient from "./oauth";
import OAuthClient from "@server/utils/oauth";
import env from "./env";
type AzurePayload = {
/** 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",
};
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(
accessToken: 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 { slugifyDomain } from "@shared/utils/domains";
import accountProvisioner from "@server/commands/accountProvisioner";
import env from "@server/env";
import {
GmailAccountCreationError,
TeamDomainRequiredError,
@ -19,6 +18,7 @@ import {
getTeamFromContext,
getClientFromContext,
} from "@server/utils/passport";
import env from "../env";
const router = new Router();
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",
"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 env from "@server/env";
import { InternalError } from "@server/errors";
import Logger from "@server/logging/Logger";
import Redis from "@server/storage/redis";
import fetch from "@server/utils/fetch";
import env from "./env";
class Iframely {
private static apiUrl = `${env.IFRAMELY_URL}/api`;

View File

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

View File

@ -5,7 +5,6 @@ import get from "lodash/get";
import { Strategy } from "passport-oauth2";
import { slugifyDomain } from "@shared/utils/domains";
import accountProvisioner from "@server/commands/accountProvisioner";
import env from "@server/env";
import {
OIDCMalformedUserInfoError,
AuthenticationError,
@ -19,6 +18,7 @@ import {
getTeamFromContext,
getClientFromContext,
} from "@server/utils/passport";
import env from "../env";
const router = new Router();
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 { IntegrationService } from "@shared/types";
import env from "@server/env";
import { IntegrationAuthentication, SearchQuery } from "@server/models";
import {
buildDocument,
@ -9,6 +8,7 @@ import {
buildUser,
} from "@server/test/factories";
import { getTestServer } from "@server/test/support";
import env from "../env";
import * as Slack from "../slack";
jest.mock("../slack", () => ({

View File

@ -4,7 +4,6 @@ import escapeRegExp from "lodash/escapeRegExp";
import { Op } from "sequelize";
import { z } from "zod";
import { IntegrationService } from "@shared/types";
import env from "@server/env";
import {
AuthenticationError,
InvalidRequestError,
@ -26,6 +25,7 @@ import SearchHelper from "@server/models/helpers/SearchHelper";
import { APIContext } from "@server/types";
import { safeEqual } from "@server/utils/crypto";
import { opts } from "@server/utils/i18n";
import env from "../env";
import presentMessageAttachment from "../presenters/messageAttachment";
import * as Slack from "../slack";
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 { integrationSettingsPath } from "@shared/utils/routeHelpers";
import accountProvisioner from "@server/commands/accountProvisioner";
import env from "@server/env";
import auth from "@server/middlewares/authentication";
import passportMiddleware from "@server/middlewares/passport";
import validate from "@server/middlewares/validate";
@ -23,6 +22,7 @@ import {
getTeamFromContext,
StateStore,
} from "@server/utils/passport";
import env from "../env";
import * as Slack from "../slack";
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 { IntegrationService, IntegrationType } from "@shared/types";
import { Minute } from "@shared/utils/time";
import env from "@server/env";
import { Document, Integration, Collection, Team } from "@server/models";
import BaseProcessor from "@server/queues/processors/BaseProcessor";
import {
@ -12,6 +11,7 @@ import {
Event,
} from "@server/types";
import fetch from "@server/utils/fetch";
import env from "../env";
import presentMessageAttachment from "../presenters/messageAttachment";
export default class SlackProcessor extends BaseProcessor {

View File

@ -1,7 +1,7 @@
import querystring from "querystring";
import env from "@server/env";
import { InvalidRequestError } from "@server/errors";
import fetch from "@server/utils/fetch";
import env from "./env";
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 { v4 as uuidV4 } from "uuid";
import env from "@server/env";
import "@server/test/env";
import FileStorage from "@server/storage/files";
import { buildAttachment, buildUser } from "@server/test/factories";
import { getTestServer } from "@server/test/support";

View File

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

View File

@ -1,10 +1,5 @@
/* eslint-disable @typescript-eslint/no-var-requires */
// Load the process environment variables
require("dotenv").config({
silent: true,
});
// eslint-disable-next-line import/order
import environment from "./utils/environment";
import os from "os";
import {
validate,
@ -16,7 +11,6 @@ import {
IsIn,
IsEmail,
IsBoolean,
MaxLength,
} from "class-validator";
import uniq from "lodash/uniq";
import { languages } from "@shared/i18n";
@ -25,7 +19,7 @@ import Deprecated from "./models/decorators/Deprecated";
import { getArg } from "./utils/args";
export class Environment {
private validationPromise;
protected validationPromise;
constructor() {
this.validationPromise = validate(this);
@ -44,21 +38,23 @@ export class Environment {
* The current environment name.
*/
@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
* set or your users will be unable to login.
*/
@IsByteLength(32, 64)
public SECRET_KEY = process.env.SECRET_KEY ?? "";
@IsByteLength(32, 64, {
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
* triggering of scheduled tasks.
*/
@IsNotEmpty()
public UTILS_SECRET = process.env.UTILS_SECRET ?? "";
public UTILS_SECRET = environment.UTILS_SECRET ?? "";
/**
* The url of the database.
@ -69,7 +65,7 @@ export class Environment {
allow_underscores: true,
protocols: ["postgres", "postgresql"],
})
public DATABASE_URL = process.env.DATABASE_URL ?? "";
public DATABASE_URL = environment.DATABASE_URL ?? "";
/**
* The url of the database pool.
@ -81,7 +77,7 @@ export class Environment {
protocols: ["postgres", "postgresql"],
})
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()
@IsOptional()
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()
@IsOptional()
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"])
@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.
@ -118,7 +114,7 @@ export class Environment {
* base64-encoded configuration.
*/
@IsNotEmpty()
public REDIS_URL = process.env.REDIS_URL;
public REDIS_URL = environment.REDIS_URL;
/**
* The fully qualified, external facing domain name of the server.
@ -129,7 +125,7 @@ export class Environment {
require_protocol: true,
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.
@ -143,7 +139,7 @@ export class Environment {
require_protocol: true,
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
@ -156,7 +152,7 @@ export class Environment {
})
@IsOptional()
public COLLABORATION_URL = this.toOptionalString(
process.env.COLLABORATION_URL
environment.COLLABORATION_URL
);
/**
@ -166,7 +162,7 @@ export class Environment {
@IsOptional()
@IsNumber()
public COLLABORATION_MAX_CLIENTS_PER_DOCUMENT = parseInt(
process.env.COLLABORATION_MAX_CLIENTS_PER_DOCUMENT || "100",
environment.COLLABORATION_MAX_CLIENTS_PER_DOCUMENT || "100",
10
);
@ -175,18 +171,18 @@ export class Environment {
*/
@IsNumber()
@IsOptional()
public PORT = this.toOptionalNumber(process.env.PORT) ?? 3000;
public PORT = this.toOptionalNumber(environment.PORT) ?? 3000;
/**
* Optional extra debugging. Comma separated
*/
public DEBUG = process.env.DEBUG || "";
public DEBUG = environment.DEBUG || "";
/**
* Configure lowest severity level for server logs
*/
@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
@ -194,7 +190,7 @@ export class Environment {
*/
@IsNumber()
@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
@ -203,28 +199,28 @@ export class Environment {
@IsNumber()
@IsOptional()
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()
@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.
*/
@IsOptional()
@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
* available language codes and their percentage translated.
*/
@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.
@ -235,7 +231,7 @@ export class Environment {
public SERVICES = uniq(
(
getArg("services") ??
process.env.SERVICES ??
environment.SERVICES ??
"collaboration,websockets,worker,web"
)
.split(",")
@ -248,7 +244,7 @@ export class Environment {
* loadbalancer.
*/
@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.
@ -256,51 +252,51 @@ export class Environment {
*/
@IsBoolean()
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.
*/
public ALLOWED_DOMAINS =
process.env.ALLOWED_DOMAINS ?? process.env.GOOGLE_ALLOWED_DOMAINS;
environment.ALLOWED_DOMAINS ?? environment.GOOGLE_ALLOWED_DOMAINS;
// Third-party services
/**
* 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
* 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.
*/
@IsNumber()
@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.
*/
public SMTP_USERNAME = process.env.SMTP_USERNAME;
public SMTP_USERNAME = environment.SMTP_USERNAME;
/**
* 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.
*/
@IsEmail({ allow_display_name: true, allow_ip_domain: true })
@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
@ -308,12 +304,12 @@ export class Environment {
*/
@IsEmail({ allow_display_name: true, allow_ip_domain: true })
@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.
*/
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.
@ -322,182 +318,56 @@ export class Environment {
* Setting secure to false therefore does not mean that you would not use an
* 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.
*/
@IsUrl()
@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
*/
@IsUrl()
@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.
*/
public RELEASE = this.toOptionalString(process.env.RELEASE);
public RELEASE = this.toOptionalString(environment.RELEASE);
/**
* A Google Analytics tracking ID, supports v3 or v4 properties.
*/
@IsOptional()
public GOOGLE_ANALYTICS_ID = this.toOptionalString(
process.env.GOOGLE_ANALYTICS_ID
environment.GOOGLE_ANALYTICS_ID
);
/**
* 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.
*/
public DD_SERVICE = process.env.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);
public DD_SERVICE = environment.DD_SERVICE ?? "outline";
@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(
process.env.SLACK_CLIENT_ID ?? process.env.SLACK_KEY
);
@IsOptional()
@CannotUseWithout("SLACK_CLIENT_ID")
public SLACK_CLIENT_SECRET = this.toOptionalString(
process.env.SLACK_CLIENT_SECRET ?? process.env.SLACK_SECRET
environment.SLACK_CLIENT_ID ?? environment.SLACK_KEY
);
/**
* This is used to verify webhook requests received from Slack.
*/
@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.
* Injected into the `slack-app-id` header meta tag if provided.
*/
@IsOptional()
@CannotUseWithout("SLACK_CLIENT_ID")
public SLACK_APP_ID = this.toOptionalString(process.env.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
);
public SLACK_APP_ID = this.toOptionalString(environment.SLACK_APP_ID);
/**
* Disable autoredirect to the OIDC login page if there is only one
@ -506,7 +376,7 @@ export class Environment {
@IsOptional()
@IsBoolean()
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,
allow_underscores: true,
})
public OIDC_LOGOUT_URI = this.toOptionalString(process.env.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";
public OIDC_LOGOUT_URI = this.toOptionalString(environment.OIDC_LOGOUT_URI);
/**
* A string representing the version of the software.
@ -539,7 +396,7 @@ export class Environment {
* SOURCE_VERSION is used by Heroku
*/
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()
@IsBoolean()
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()
@CannotUseWithout("RATE_LIMITER_ENABLED")
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
@ -568,7 +425,7 @@ export class Environment {
@IsOptional()
@IsNumber()
public RATE_LIMITER_COLLABORATION_REQUESTS =
this.toOptionalNumber(process.env.RATE_LIMITER_COLLABORATION_REQUESTS) ??
this.toOptionalNumber(environment.RATE_LIMITER_COLLABORATION_REQUESTS) ??
50;
/**
@ -579,7 +436,7 @@ export class Environment {
@IsNumber()
@CannotUseWithout("RATE_LIMITER_ENABLED")
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.
@ -589,7 +446,7 @@ export class Environment {
@IsNumber()
@Deprecated("Use FILE_STORAGE_UPLOAD_MAX_SIZE instead")
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()
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()
@CannotUseWithout("AWS_ACCESS_KEY_ID")
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.
*/
@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.
*/
@IsOptional()
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.
*/
@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.
*/
@IsOptional()
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()
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.
*/
@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
*/
@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
*/
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";
/**
@ -670,8 +527,8 @@ export class Environment {
*/
@IsNumber()
public FILE_STORAGE_UPLOAD_MAX_SIZE =
this.toOptionalNumber(process.env.FILE_STORAGE_UPLOAD_MAX_SIZE) ??
this.toOptionalNumber(process.env.AWS_S3_UPLOAD_MAX_SIZE) ??
this.toOptionalNumber(environment.FILE_STORAGE_UPLOAD_MAX_SIZE) ??
this.toOptionalNumber(environment.AWS_S3_UPLOAD_MAX_SIZE) ??
1000000;
/**
@ -679,9 +536,9 @@ export class Environment {
*/
@IsNumber()
public FILE_STORAGE_IMPORT_MAX_SIZE =
this.toOptionalNumber(process.env.FILE_STORAGE_IMPORT_MAX_SIZE) ??
this.toOptionalNumber(process.env.MAXIMUM_IMPORT_SIZE) ??
this.toOptionalNumber(process.env.FILE_STORAGE_UPLOAD_MAX_SIZE) ??
this.toOptionalNumber(environment.FILE_STORAGE_IMPORT_MAX_SIZE) ??
this.toOptionalNumber(environment.MAXIMUM_IMPORT_SIZE) ??
this.toOptionalNumber(environment.FILE_STORAGE_UPLOAD_MAX_SIZE) ??
1000000;
/**
@ -689,9 +546,9 @@ export class Environment {
*/
@IsNumber()
public FILE_STORAGE_WORKSPACE_IMPORT_MAX_SIZE =
this.toOptionalNumber(process.env.FILE_STORAGE_WORKSPACE_IMPORT_MAX_SIZE) ??
this.toOptionalNumber(process.env.MAXIMUM_IMPORT_SIZE) ??
this.toOptionalNumber(process.env.FILE_STORAGE_UPLOAD_MAX_SIZE) ??
this.toOptionalNumber(environment.FILE_STORAGE_WORKSPACE_IMPORT_MAX_SIZE) ??
this.toOptionalNumber(environment.MAXIMUM_IMPORT_SIZE) ??
this.toOptionalNumber(environment.FILE_STORAGE_UPLOAD_MAX_SIZE) ??
1000000;
/**
@ -705,7 +562,7 @@ export class Environment {
@IsNumber()
@Deprecated("Use FILE_STORAGE_IMPORT_MAX_SIZE instead")
public MAXIMUM_IMPORT_SIZE = this.toOptionalNumber(
process.env.MAXIMUM_IMPORT_SIZE
environment.MAXIMUM_IMPORT_SIZE
);
/**
@ -714,33 +571,14 @@ export class Environment {
*/
@IsNumber()
public MAXIMUM_EXPORT_SIZE =
this.toOptionalNumber(process.env.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);
this.toOptionalNumber(environment.MAXIMUM_EXPORT_SIZE) ?? os.totalmem();
/**
* Enable unsafe-inline in script-src CSP directive
*/
@IsBoolean()
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";
}
private toOptionalString(value: string | undefined) {
protected toOptionalString(value: string | undefined) {
return value ? value : undefined;
}
private toOptionalNumber(value: string | undefined) {
protected toOptionalNumber(value: string | undefined) {
return value ? parseInt(value, 10) : undefined;
}
@ -801,7 +639,7 @@ export class Environment {
* @param value The string to convert
* @returns A boolean
*/
private toBoolean(value: string) {
protected toBoolean(value: string) {
try {
return value ? !!JSON.parse(value) : false;
} catch (err) {
@ -823,7 +661,7 @@ export class Environment {
* @param value The string to convert
* @returns A boolean or undefined
*/
private toOptionalBoolean(value: string | undefined) {
protected toOptionalBoolean(value: string | undefined) {
try {
return value ? !!JSON.parse(value) : undefined;
} catch (err) {
@ -832,6 +670,4 @@ export class Environment {
}
}
const env = new Environment();
export default env;
export default new Environment();

View File

@ -16,17 +16,18 @@ import {
IsUUID,
PrimaryKey,
} from "sequelize-typescript";
import env from "@server/env";
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 Team from "./Team";
import UserAuthentication from "./UserAuthentication";
import Fix from "./decorators/Fix";
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({
tableName: "authentication_providers",
modelName: "authentication_provider",
@ -86,20 +87,11 @@ class AuthenticationProvider extends Model<
get oauthClient() {
switch (this.name) {
case "google":
return new GoogleClient(
env.GOOGLE_CLIENT_ID || "",
env.GOOGLE_CLIENT_SECRET || ""
);
return new GoogleClient();
case "azure":
return new AzureClient(
env.AZURE_CLIENT_ID || "",
env.AZURE_CLIENT_SECRET || ""
);
return new AzureClient();
case "oidc":
return new OIDCClient(
env.OIDC_CLIENT_ID || "",
env.OIDC_CLIENT_SECRET || ""
);
return new OIDCClient();
default:
return undefined;
}

View File

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

View File

@ -6,8 +6,6 @@ declare module "formidable/lib/file";
declare module "oy-vey";
declare module "dotenv";
declare module "email-providers" {
const list: string[];
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 Logger from "@server/logging/Logger";
import { UnfurlResolver } from "@server/types";
import environment from "./environment";
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
const enabled = (config.requiredEnvVars ?? []).every(
(name: string) => !!env[name]
(name: string) => !!environment[name]
);
if (!enabled) {
return resolvers;

View File

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

View File

@ -2864,6 +2864,13 @@
resolved "https://registry.yarnpkg.com/@types/diff/-/diff-5.0.4.tgz#ba774c225ee68ce13a090fec16cf34b97a78537b"
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":
version "9.2.0"
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:
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:
version "16.3.1"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
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:
version "2.0.6"
resolved "https://registry.yarnpkg.com/dottie/-/dottie-2.0.6.tgz#34564ebfc6ec5e5772272d466424ad5b696484d4"