chore: Centralize env parsing, validation, defaults, and deprecation notices (#3487)
* chore: Centralize env parsing, defaults, deprecation * wip * test * test * tsc * docs, more validation * fix: Allow empty REDIS_URL (defaults to localhost) * test * fix: SLACK_MESSAGE_ACTIONS not bool * fix: Add SMTP port validation
This commit is contained in:
@@ -65,8 +65,8 @@ AWS_S3_ACL=private
|
||||
#
|
||||
# When configuring the Client ID, add a redirect URL under "OAuth & Permissions":
|
||||
# https://<URL>/auth/slack.callback
|
||||
SLACK_KEY=get_a_key_from_slack
|
||||
SLACK_SECRET=get_the_secret_of_above_key
|
||||
SLACK_CLIENT_ID=get_a_key_from_slack
|
||||
SLACK_CLIENT_SECRET=get_the_secret_of_above_key
|
||||
|
||||
# To configure Google auth, you'll need to create an OAuth Client ID at
|
||||
# => https://console.cloud.google.com/apis/credentials
|
||||
|
||||
4
app.json
4
app.json
@@ -102,11 +102,11 @@
|
||||
"value": "openid profile email",
|
||||
"required": false
|
||||
},
|
||||
"SLACK_KEY": {
|
||||
"SLACK_CLIENT_ID": {
|
||||
"description": "See https://api.slack.com/apps to create a new Slack app. You must configure at least one of Slack or Google to control login.",
|
||||
"required": false
|
||||
},
|
||||
"SLACK_SECRET": {
|
||||
"SLACK_CLIENT_SECRET": {
|
||||
"description": "Your Slack client secret - d2dc414f9953226bad0a356cXXXXYYYY",
|
||||
"required": false
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import copy from "copy-to-clipboard";
|
||||
import * as React from "react";
|
||||
import env from "~/env";
|
||||
|
||||
type Props = {
|
||||
text: string;
|
||||
@@ -14,7 +15,7 @@ class CopyToClipboard extends React.PureComponent<Props> {
|
||||
const elem = React.Children.only(children);
|
||||
|
||||
copy(text, {
|
||||
debug: process.env.NODE_ENV !== "production",
|
||||
debug: env.ENVIRONMENT !== "production",
|
||||
format: "text/plain",
|
||||
});
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ const useAuthorizedSettingsConfig = () => {
|
||||
name: "Slack",
|
||||
path: "/settings/integrations/slack",
|
||||
component: Slack,
|
||||
enabled: can.update && (!!env.SLACK_KEY || isHosted),
|
||||
enabled: can.update && (!!env.SLACK_CLIENT_ID || isHosted),
|
||||
group: t("Integrations"),
|
||||
icon: SlackIcon,
|
||||
},
|
||||
|
||||
@@ -83,7 +83,7 @@ function Slack() {
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
{env.SLACK_KEY ? (
|
||||
{env.SLACK_CLIENT_ID ? (
|
||||
<>
|
||||
<p>
|
||||
{commandIntegration ? (
|
||||
|
||||
@@ -15,13 +15,18 @@ type Props = {
|
||||
function SlackButton({ state = "", scopes, redirectUri, label, icon }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClick = () =>
|
||||
(window.location.href = slackAuth(
|
||||
const handleClick = () => {
|
||||
if (!env.SLACK_CLIENT_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = slackAuth(
|
||||
state,
|
||||
scopes,
|
||||
env.SLACK_KEY,
|
||||
env.SLACK_CLIENT_ID,
|
||||
redirectUri
|
||||
));
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleClick} icon={icon} neutral>
|
||||
|
||||
@@ -13,7 +13,7 @@ export function initSentry(history: History) {
|
||||
routingInstrumentation: Sentry.reactRouterV5Instrumentation(history),
|
||||
}),
|
||||
],
|
||||
tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1,
|
||||
tracesSampleRate: env.ENVIRONMENT === "production" ? 0.1 : 1,
|
||||
ignoreErrors: [
|
||||
"ResizeObserver loop completed with undelivered notifications",
|
||||
"ResizeObserver loop limit exceeded",
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"bull": "^3.29.0",
|
||||
"cancan": "3.1.0",
|
||||
"chalk": "^4.1.0",
|
||||
"class-validator": "^0.13.2",
|
||||
"compressorjs": "^1.0.7",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"core-js": "^3.10.2",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import env from "@server/env";
|
||||
import TeamDomain from "@server/models/TeamDomain";
|
||||
import { buildTeam, buildUser } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
@@ -25,7 +26,7 @@ describe("teamCreator", () => {
|
||||
});
|
||||
|
||||
it("should not allow creating multiple teams in installation", async () => {
|
||||
delete process.env.DEPLOYMENT;
|
||||
env.DEPLOYMENT = undefined;
|
||||
await buildTeam();
|
||||
let error;
|
||||
|
||||
@@ -47,7 +48,7 @@ describe("teamCreator", () => {
|
||||
});
|
||||
|
||||
it("should return existing team when within allowed domains", async () => {
|
||||
delete process.env.DEPLOYMENT;
|
||||
env.DEPLOYMENT = undefined;
|
||||
const existing = await buildTeam();
|
||||
const user = await buildUser({
|
||||
teamId: existing.id,
|
||||
@@ -106,7 +107,7 @@ describe("teamCreator", () => {
|
||||
});
|
||||
|
||||
it("should return exising team", async () => {
|
||||
delete process.env.DEPLOYMENT;
|
||||
env.DEPLOYMENT = undefined;
|
||||
const authenticationProvider = {
|
||||
name: "google",
|
||||
providerId: "example.com",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import invariant from "invariant";
|
||||
import env from "@server/env";
|
||||
import { DomainNotAllowedError, MaximumTeamsError } from "@server/errors";
|
||||
import Logger from "@server/logging/logger";
|
||||
import { APM } from "@server/logging/tracing";
|
||||
@@ -53,7 +54,7 @@ async function teamCreator({
|
||||
// This team has never been seen before, if self hosted the logic is different
|
||||
// to the multi-tenant version, we want to restrict to a single team that MAY
|
||||
// have multiple authentication providers
|
||||
if (process.env.DEPLOYMENT !== "hosted") {
|
||||
if (env.DEPLOYMENT !== "hosted") {
|
||||
const teamCount = await Team.count();
|
||||
|
||||
// If the self-hosted installation has a single team and the domain for the
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Transaction } from "sequelize";
|
||||
import { sequelize } from "@server/database/sequelize";
|
||||
import env from "@server/env";
|
||||
import { Event, Team, TeamDomain, User } from "@server/models";
|
||||
|
||||
type TeamUpdaterProps = {
|
||||
@@ -27,7 +28,7 @@ const teamUpdater = async ({ params, user, team, ip }: TeamUpdaterProps) => {
|
||||
|
||||
const transaction: Transaction = await sequelize.transaction();
|
||||
|
||||
if (subdomain !== undefined && process.env.SUBDOMAINS_ENABLED === "true") {
|
||||
if (subdomain !== undefined && env.SUBDOMAINS_ENABLED) {
|
||||
team.subdomain = subdomain === "" ? null : subdomain;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import invariant from "invariant";
|
||||
import { uniqBy } from "lodash";
|
||||
import { Role } from "@shared/types";
|
||||
import InviteEmail from "@server/emails/templates/InviteEmail";
|
||||
import env from "@server/env";
|
||||
import Logger from "@server/logging/logger";
|
||||
import { User, Event, Team } from "@server/models";
|
||||
import { UserFlag } from "@server/models/User";
|
||||
@@ -89,11 +90,11 @@ export default async function userInviter({
|
||||
teamUrl: team.url,
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
if (env.ENVIRONMENT === "development") {
|
||||
Logger.info(
|
||||
"email",
|
||||
`Sign in immediately: ${
|
||||
process.env.URL
|
||||
env.URL
|
||||
}/auth/email.callback?token=${newUser.getEmailSigninToken()}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Sequelize } from "sequelize-typescript";
|
||||
import env from "@server/env";
|
||||
import Logger from "../logging/logger";
|
||||
import * as models from "../models";
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
const isSSLDisabled = process.env.PGSSLMODE === "disable";
|
||||
const poolMax = parseInt(process.env.DATABASE_CONNECTION_POOL_MAX || "5", 10);
|
||||
const poolMin = parseInt(process.env.DATABASE_CONNECTION_POOL_MIN || "0", 10);
|
||||
const isProduction = env.ENVIRONMENT === "production";
|
||||
const isSSLDisabled = env.PGSSLMODE === "disable";
|
||||
const poolMax = env.DATABASE_CONNECTION_POOL_MAX ?? 5;
|
||||
const poolMin = env.DATABASE_CONNECTION_POOL_MIN ?? 0;
|
||||
|
||||
export const sequelize = new Sequelize(
|
||||
process.env.DATABASE_URL || process.env.DATABASE_CONNECTION_POOL_URL || "",
|
||||
env.DATABASE_URL ?? env.DATABASE_CONNECTION_POOL_URL ?? "",
|
||||
{
|
||||
logging: (msg) => Logger.debug("database", msg),
|
||||
typeValidation: true,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import SequelizeEncrypted from "sequelize-encrypted";
|
||||
import { Sequelize } from "sequelize-typescript";
|
||||
import env from "@server/env";
|
||||
|
||||
/**
|
||||
* Encrypted field storage, use via the Encrypted decorator, not directly.
|
||||
*/
|
||||
export default function vaults() {
|
||||
return SequelizeEncrypted(Sequelize, process.env.SECRET_KEY);
|
||||
return SequelizeEncrypted(Sequelize, env.SECRET_KEY);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import nodemailer, { Transporter } from "nodemailer";
|
||||
import Oy from "oy-vey";
|
||||
import * as React from "react";
|
||||
import env from "@server/env";
|
||||
import Logger from "@server/logging/logger";
|
||||
import { APM } from "@server/logging/tracing";
|
||||
import { baseStyles } from "./templates/components/EmailLayout";
|
||||
|
||||
const useTestEmailService =
|
||||
process.env.NODE_ENV === "development" && !process.env.SMTP_USERNAME;
|
||||
env.ENVIRONMENT === "development" && !env.SMTP_USERNAME;
|
||||
|
||||
type SendMailOptions = {
|
||||
to: string;
|
||||
@@ -27,7 +28,7 @@ export class Mailer {
|
||||
transporter: Transporter | undefined;
|
||||
|
||||
constructor() {
|
||||
if (process.env.SMTP_HOST) {
|
||||
if (env.SMTP_HOST) {
|
||||
this.transporter = nodemailer.createTransport(this.getOptions());
|
||||
}
|
||||
if (useTestEmailService) {
|
||||
@@ -69,8 +70,8 @@ export class Mailer {
|
||||
try {
|
||||
Logger.info("email", `Sending email "${data.subject}" to ${data.to}`);
|
||||
const info = await transporter.sendMail({
|
||||
from: process.env.SMTP_FROM_EMAIL,
|
||||
replyTo: process.env.SMTP_REPLY_EMAIL || process.env.SMTP_FROM_EMAIL,
|
||||
from: env.SMTP_FROM_EMAIL,
|
||||
replyTo: env.SMTP_REPLY_EMAIL ?? env.SMTP_FROM_EMAIL,
|
||||
to: data.to,
|
||||
subject: data.subject,
|
||||
html,
|
||||
@@ -91,24 +92,20 @@ export class Mailer {
|
||||
|
||||
private getOptions() {
|
||||
return {
|
||||
host: process.env.SMTP_HOST || "",
|
||||
port: parseInt(process.env.SMTP_PORT || "", 10),
|
||||
secure:
|
||||
"SMTP_SECURE" in process.env
|
||||
? process.env.SMTP_SECURE === "true"
|
||||
: process.env.NODE_ENV === "production",
|
||||
auth: process.env.SMTP_USERNAME
|
||||
host: env.SMTP_HOST,
|
||||
port: env.SMTP_PORT,
|
||||
secure: env.SMTP_SECURE ?? env.ENVIRONMENT === "production",
|
||||
auth: env.SMTP_USERNAME
|
||||
? {
|
||||
user: process.env.SMTP_USERNAME || "",
|
||||
pass: process.env.SMTP_PASSWORD,
|
||||
user: env.SMTP_USERNAME,
|
||||
pass: env.SMTP_PASSWORD,
|
||||
}
|
||||
: undefined,
|
||||
tls: env.SMTP_TLS_CIPHERS
|
||||
? {
|
||||
ciphers: env.SMTP_TLS_CIPHERS,
|
||||
}
|
||||
: undefined,
|
||||
tls:
|
||||
"SMTP_TLS_CIPHERS" in process.env
|
||||
? {
|
||||
ciphers: process.env.SMTP_TLS_CIPHERS,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import invariant from "invariant";
|
||||
import * as React from "react";
|
||||
import env from "@server/env";
|
||||
import { Collection } from "@server/models";
|
||||
import BaseEmail from "./BaseEmail";
|
||||
import Body from "./components/Body";
|
||||
@@ -54,7 +55,7 @@ ${collection.name}
|
||||
|
||||
${collection.user.name} ${eventName} the collection "${collection.name}"
|
||||
|
||||
Open Collection: ${process.env.URL}${collection.url}
|
||||
Open Collection: ${env.URL}${collection.url}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -75,7 +76,7 @@ Open Collection: ${process.env.URL}${collection.url}
|
||||
</p>
|
||||
<EmptySpace height={10} />
|
||||
<p>
|
||||
<Button href={`${process.env.URL}${collection.url}`}>
|
||||
<Button href={`${env.URL}${collection.url}`}>
|
||||
Open Collection
|
||||
</Button>
|
||||
</p>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from "react";
|
||||
import env from "@server/env";
|
||||
import logger from "@server/logging/logger";
|
||||
import BaseEmail from "./BaseEmail";
|
||||
import Body from "./components/Body";
|
||||
@@ -39,7 +40,7 @@ signin page at: ${teamUrl}
|
||||
}
|
||||
|
||||
protected render({ token, teamUrl }: Props) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
if (env.ENVIRONMENT === "development") {
|
||||
logger.debug("email", `Sign-In link: ${this.signinLink(token)}`);
|
||||
}
|
||||
|
||||
@@ -67,6 +68,6 @@ signin page at: ${teamUrl}
|
||||
}
|
||||
|
||||
private signinLink(token: string): string {
|
||||
return `${process.env.URL}/auth/email.callback?token=${token}`;
|
||||
return `${env.URL}/auth/email.callback?token=${token}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Table, TBody, TR, TD } from "oy-vey";
|
||||
import * as React from "react";
|
||||
import theme from "@shared/styles/theme";
|
||||
import { twitterUrl } from "@shared/utils/urlHelpers";
|
||||
import env from "@server/env";
|
||||
|
||||
type Props = {
|
||||
unsubscribeUrl?: string;
|
||||
@@ -35,7 +36,7 @@ export default ({ unsubscribeUrl }: Props) => {
|
||||
<TBody>
|
||||
<TR>
|
||||
<TD style={footerStyle}>
|
||||
<a href={process.env.URL} style={linkStyle}>
|
||||
<a href={env.URL} style={linkStyle}>
|
||||
Outline
|
||||
</a>
|
||||
<a href={twitterUrl()} style={externalLinkStyle}>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Table, TBody, TR, TD } from "oy-vey";
|
||||
import * as React from "react";
|
||||
import env from "@server/env";
|
||||
import EmptySpace from "./EmptySpace";
|
||||
|
||||
const url = process.env.CDN_URL || process.env.URL;
|
||||
const url = env.CDN_URL ?? env.URL;
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
|
||||
447
server/env.ts
447
server/env.ts
@@ -1,6 +1,449 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
// Load the process environment variables
|
||||
require("dotenv").config({
|
||||
silent: true,
|
||||
});
|
||||
|
||||
export default process.env;
|
||||
import {
|
||||
validate,
|
||||
IsNotEmpty,
|
||||
IsUrl,
|
||||
IsOptional,
|
||||
IsByteLength,
|
||||
Equals,
|
||||
IsNumber,
|
||||
IsIn,
|
||||
IsBoolean,
|
||||
IsEmail,
|
||||
Contains,
|
||||
MaxLength,
|
||||
} from "class-validator";
|
||||
import { languages } from "@shared/i18n";
|
||||
import { CannotUseWithout } from "@server/utils/validators";
|
||||
import Deprecated from "./models/decorators/Deprecated";
|
||||
|
||||
export class Environment {
|
||||
private validationPromise;
|
||||
|
||||
constructor() {
|
||||
this.validationPromise = validate(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows waiting on the environment to be validated.
|
||||
*
|
||||
* @returns A promise that resolves when the environment is validated.
|
||||
*/
|
||||
public validate() {
|
||||
return this.validationPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current envionment name.
|
||||
*/
|
||||
@IsIn(["development", "production", "staging", "test"])
|
||||
public ENVIRONMENT = process.env.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}`;
|
||||
|
||||
/**
|
||||
* 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}`;
|
||||
|
||||
/**
|
||||
* The url of the database.
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@IsUrl({ require_tld: false, protocols: ["postgres"] })
|
||||
public DATABASE_URL = `${process.env.DATABASE_URL}`;
|
||||
|
||||
/**
|
||||
* The url of the database pool.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsUrl({ require_tld: false, protocols: ["postgres"] })
|
||||
public DATABASE_CONNECTION_POOL_URL = `${process.env.DATABASE_CONNECTION_POOL_URL}`;
|
||||
|
||||
/**
|
||||
* Database connection pool configuration.
|
||||
*/
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
public DATABASE_CONNECTION_POOL_MIN = this.toOptionalNumber(
|
||||
process.env.DATABASE_CONNECTION_POOL_MIN
|
||||
);
|
||||
|
||||
/**
|
||||
* Database connection pool configuration.
|
||||
*/
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
public DATABASE_CONNECTION_POOL_MAX = this.toOptionalNumber(
|
||||
process.env.DATABASE_CONNECTION_POOL_MAX
|
||||
);
|
||||
|
||||
/**
|
||||
* Set to "disable" to disable SSL connection to the database. This option is
|
||||
* passed through to Postgres. See:
|
||||
*
|
||||
* https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLMODE
|
||||
*/
|
||||
@IsIn(["disable", "allow", "require", "prefer", "verify-ca", "verify-full"])
|
||||
@IsOptional()
|
||||
public PGSSLMODE = process.env.PGSSLMODE;
|
||||
|
||||
/**
|
||||
* The url of redis. Note that redis does not have a database after the port.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsUrl({ require_tld: false, protocols: ["redis", "rediss", "ioredis"] })
|
||||
public REDIS_URL = process.env.REDIS_URL;
|
||||
|
||||
/**
|
||||
* The fully qualified, external facing domain name of the server.
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@IsUrl({ require_tld: false })
|
||||
public URL = `${process.env.URL}`;
|
||||
|
||||
/**
|
||||
* If using a Cloudfront/Cloudflare distribution or similar it can be set below.
|
||||
* This will cause paths to javascript, stylesheets, and images to be updated to
|
||||
* the hostname defined in CDN_URL. In your CDN configuration the origin server
|
||||
* should be set to the same as URL.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
public CDN_URL = process.env.CDN_URL;
|
||||
|
||||
/**
|
||||
* The fully qualified, external facing domain name of the collaboration
|
||||
* service, if different (unlikely)
|
||||
*/
|
||||
@IsUrl({ require_tld: false })
|
||||
@IsOptional()
|
||||
public COLLABORATION_URL = process.env.COLLABORATION_URL;
|
||||
|
||||
/**
|
||||
* The port that the server will listen on, defaults to 3000.
|
||||
*/
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
public PORT = this.toOptionalNumber(process.env.PORT);
|
||||
|
||||
/**
|
||||
* Optional extra debugging. Comma separated
|
||||
*/
|
||||
public DEBUG = `${process.env.DEBUG}`;
|
||||
|
||||
/**
|
||||
* How many processes should be spawned. As a reasonable rule divide your
|
||||
* server's available memory by 512 for a rough estimate
|
||||
*/
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
public WEB_CONCURRENCY = this.toOptionalNumber(process.env.WEB_CONCURRENCY);
|
||||
|
||||
/**
|
||||
* Base64 encoded private key if Outline is to perform SSL termination.
|
||||
*/
|
||||
@IsOptional()
|
||||
@CannotUseWithout("SSL_CERT")
|
||||
public SSL_KEY = process.env.SSL_KEY;
|
||||
|
||||
/**
|
||||
* Base64 encoded public certificate if Outline is to perform SSL termination.
|
||||
*/
|
||||
@IsOptional()
|
||||
@CannotUseWithout("SSL_KEY")
|
||||
public SSL_CERT = process.env.SSL_CERT;
|
||||
|
||||
/**
|
||||
* Should always be left unset in a self-hosted environment.
|
||||
*/
|
||||
@Equals("hosted")
|
||||
@IsOptional()
|
||||
public DEPLOYMENT = process.env.DEPLOYMENT;
|
||||
|
||||
/**
|
||||
* Custom company logo that displays on the authentication screen.
|
||||
*/
|
||||
public TEAM_LOGO = process.env.TEAM_LOGO;
|
||||
|
||||
/**
|
||||
* 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";
|
||||
|
||||
/**
|
||||
* A comma separated list of which services should be enabled on this
|
||||
* instance – defaults to all.
|
||||
*/
|
||||
public SERVICES =
|
||||
process.env.SERVICES ?? "collaboration,websockets,worker,web";
|
||||
|
||||
/**
|
||||
* Auto-redirect to https in production. The default is true but you may set
|
||||
* to false if you can be sure that SSL is terminated at an external
|
||||
* loadbalancer.
|
||||
*/
|
||||
@IsBoolean()
|
||||
public FORCE_HTTPS = Boolean(process.env.FORCE_HTTPS ?? "true");
|
||||
|
||||
/**
|
||||
* Whether to support multiple subdomains in a single instance.
|
||||
*/
|
||||
@IsBoolean()
|
||||
@Deprecated("The community edition of Outline does not support subdomains")
|
||||
public SUBDOMAINS_ENABLED = Boolean(
|
||||
process.env.SUBDOMAINS_ENABLED ?? "false"
|
||||
);
|
||||
|
||||
/**
|
||||
* Should the installation send anonymized statistics to the maintainers.
|
||||
* Defaults to true.
|
||||
*/
|
||||
@IsBoolean()
|
||||
public TELEMETRY = Boolean(
|
||||
process.env.ENABLE_UPDATES ?? process.env.TELEMETRY ?? "true"
|
||||
);
|
||||
|
||||
/**
|
||||
* Because imports can be much larger than regular file attachments and are
|
||||
* deleted automatically we allow an optional separate limit on the size of
|
||||
* imports.
|
||||
*/
|
||||
@IsNumber()
|
||||
public MAXIMUM_IMPORT_SIZE =
|
||||
this.toOptionalNumber(process.env.MAXIMUM_IMPORT_SIZE) ?? 5120000;
|
||||
|
||||
/**
|
||||
* An optional comma separated list of allowed domains.
|
||||
*/
|
||||
public ALLOWED_DOMAINS =
|
||||
process.env.ALLOWED_DOMAINS ?? process.env.GOOGLE_ALLOWED_DOMAINS;
|
||||
|
||||
// Third-party services
|
||||
|
||||
/**
|
||||
* The host of your SMTP server for enabling emails.
|
||||
*/
|
||||
public SMTP_HOST = process.env.SMTP_HOST;
|
||||
|
||||
/**
|
||||
* The port of your SMTP server.
|
||||
*/
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
public SMTP_PORT = this.toOptionalNumber(process.env.SMTP_PORT);
|
||||
|
||||
/**
|
||||
* The username of your SMTP server, if any.
|
||||
*/
|
||||
public SMTP_USERNAME = process.env.SMTP_USERNAME;
|
||||
|
||||
/**
|
||||
* The password for the SMTP username, if any.
|
||||
*/
|
||||
public SMTP_PASSWORD = process.env.SMTP_PASSWORD;
|
||||
|
||||
/**
|
||||
* The email address from which emails are sent.
|
||||
*/
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
public SMTP_FROM_EMAIL = process.env.SMTP_FROM_EMAIL;
|
||||
|
||||
/**
|
||||
* The reply-to address for emails sent from Outline. If unset the from
|
||||
* address is used by default.
|
||||
*/
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
public SMTP_REPLY_EMAIL = process.env.SMTP_REPLY_EMAIL;
|
||||
|
||||
/**
|
||||
* Override the cipher used for SMTP SSL connections.
|
||||
*/
|
||||
public SMTP_TLS_CIPHERS = process.env.SMTP_TLS_CIPHERS;
|
||||
|
||||
/**
|
||||
* If true (the default) the connection will use TLS when connecting to server.
|
||||
* If false then TLS is used only if server supports the STARTTLS extension.
|
||||
*
|
||||
* Setting secure to false therefore does not mean that you would not use an
|
||||
* encrypted connection.
|
||||
*/
|
||||
public SMTP_SECURE = Boolean(process.env.SMTP_SECURE ?? "true");
|
||||
|
||||
/**
|
||||
* Sentry DSN for capturing errors and frontend performance.
|
||||
*/
|
||||
@IsUrl()
|
||||
@IsOptional()
|
||||
public SENTRY_DSN = process.env.SENTRY_DSN;
|
||||
|
||||
/**
|
||||
* A release SHA or other identifier for Sentry.
|
||||
*/
|
||||
public RELEASE = process.env.RELEASE;
|
||||
|
||||
/**
|
||||
* An optional host from which to load default avatars.
|
||||
*/
|
||||
@IsUrl()
|
||||
public DEFAULT_AVATAR_HOST =
|
||||
process.env.DEFAULT_AVATAR_HOST ?? "https://tiley.herokuapp.com";
|
||||
|
||||
/**
|
||||
* A Google Analytics tracking ID, only v3 supported at this time.
|
||||
*/
|
||||
@Contains("UA-")
|
||||
@IsOptional()
|
||||
public GOOGLE_ANALYTICS_ID = process.env.GOOGLE_ANALYTICS_ID;
|
||||
|
||||
/**
|
||||
* A DataDog API key for tracking server metrics.
|
||||
*/
|
||||
public DD_API_KEY = process.env.DD_API_KEY;
|
||||
|
||||
/**
|
||||
* Google OAuth2 client credentials. To enable authentication with Google.
|
||||
*/
|
||||
@IsOptional()
|
||||
@CannotUseWithout("GOOGLE_CLIENT_SECRET")
|
||||
public GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
|
||||
|
||||
@IsOptional()
|
||||
@CannotUseWithout("GOOGLE_CLIENT_ID")
|
||||
public GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
|
||||
|
||||
/**
|
||||
* Slack OAuth2 client credentials. To enable authentication with Slack.
|
||||
*/
|
||||
@IsOptional()
|
||||
@CannotUseWithout("SLACK_CLIENT_SECRET")
|
||||
public SLACK_CLIENT_ID = process.env.SLACK_CLIENT_ID ?? process.env.SLACK_KEY;
|
||||
|
||||
@IsOptional()
|
||||
@CannotUseWithout("SLACK_CLIENT_ID")
|
||||
public SLACK_CLIENT_SECRET =
|
||||
process.env.SLACK_CLIENT_SECRET ?? process.env.SLACK_SECRET;
|
||||
|
||||
/**
|
||||
* This is injected into the HTML page headers for Slack.
|
||||
*/
|
||||
@IsOptional()
|
||||
@CannotUseWithout("SLACK_CLIENT_ID")
|
||||
public SLACK_VERIFICATION_TOKEN = process.env.SLACK_VERIFICATION_TOKEN;
|
||||
|
||||
/**
|
||||
* This is injected into the slack-app-id header meta tag if provided.
|
||||
*/
|
||||
@IsOptional()
|
||||
@CannotUseWithout("SLACK_CLIENT_ID")
|
||||
public SLACK_APP_ID = 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 = Boolean(
|
||||
process.env.SLACK_MESSAGE_ACTIONS ?? "false"
|
||||
);
|
||||
|
||||
/**
|
||||
* Azure OAuth2 client credentials. To enable authentication with Azure.
|
||||
*/
|
||||
@IsOptional()
|
||||
@CannotUseWithout("AZURE_CLIENT_SECRET")
|
||||
public AZURE_CLIENT_ID = process.env.AZURE_CLIENT_ID;
|
||||
|
||||
@IsOptional()
|
||||
@CannotUseWithout("AZURE_CLIENT_ID")
|
||||
public AZURE_CLIENT_SECRET = process.env.AZURE_CLIENT_SECRET;
|
||||
|
||||
@IsOptional()
|
||||
@CannotUseWithout("AZURE_CLIENT_ID")
|
||||
public AZURE_RESOURCE_APP_ID = process.env.AZURE_RESOURCE_APP_ID;
|
||||
|
||||
/**
|
||||
* OICD 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 = process.env.OIDC_CLIENT_ID;
|
||||
|
||||
@IsOptional()
|
||||
@CannotUseWithout("OIDC_CLIENT_ID")
|
||||
public OIDC_CLIENT_SECRET = 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()
|
||||
public OIDC_AUTH_URI = process.env.OIDC_AUTH_URI;
|
||||
|
||||
/**
|
||||
* The OIDC token endpoint.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
public OIDC_TOKEN_URI = process.env.OIDC_TOKEN_URI;
|
||||
|
||||
/**
|
||||
* The OIDC userinfo endpoint.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
public OIDC_USERINFO_URI = process.env.OIDC_USERINFO_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";
|
||||
|
||||
private toOptionalNumber(value: string | undefined) {
|
||||
return value ? parseInt(value, 10) : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const env = new Environment();
|
||||
|
||||
export default env;
|
||||
|
||||
@@ -3,8 +3,7 @@ import env from "./env";
|
||||
|
||||
export function AuthenticationError(
|
||||
message = "Invalid authentication",
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string | undefined' is not assignable to typ... Remove this comment to see the full error message
|
||||
redirectUrl: string = env.URL
|
||||
redirectUrl = env.URL
|
||||
) {
|
||||
return httpErrors(401, message, {
|
||||
redirectUrl,
|
||||
@@ -113,8 +112,7 @@ export function MaximumTeamsError(
|
||||
|
||||
export function EmailAuthenticationRequiredError(
|
||||
message = "User must authenticate with email",
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string | undefined' is not assignable to typ... Remove this comment to see the full error message
|
||||
redirectUrl: string = env.URL
|
||||
redirectUrl = env.URL
|
||||
) {
|
||||
return httpErrors(400, message, {
|
||||
redirectUrl,
|
||||
@@ -148,8 +146,7 @@ export function OIDCMalformedUserInfoError(
|
||||
|
||||
export function AuthenticationProviderDisabledError(
|
||||
message = "Authentication method has been disabled by an admin",
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string | undefined' is not assignable to typ... Remove this comment to see the full error message
|
||||
redirectUrl: string = env.URL
|
||||
redirectUrl = env.URL
|
||||
) {
|
||||
return httpErrors(400, message, {
|
||||
redirectUrl,
|
||||
|
||||
@@ -23,27 +23,21 @@ import { getSSLOptions } from "./utils/ssl";
|
||||
import { checkEnv, checkMigrations } from "./utils/startup";
|
||||
import { checkUpdates } from "./utils/updates";
|
||||
|
||||
// If a services flag is passed it takes priority over the enviroment variable
|
||||
// If a services flag is passed it takes priority over the environment variable
|
||||
// for example: --services=web,worker
|
||||
const normalizedServiceFlag = getArg("services");
|
||||
|
||||
// The default is to run all services to make development and OSS installations
|
||||
// easier to deal with. Separate services are only needed at scale.
|
||||
const serviceNames = uniq(
|
||||
(
|
||||
normalizedServiceFlag ||
|
||||
env.SERVICES ||
|
||||
"collaboration,websockets,worker,web"
|
||||
)
|
||||
(normalizedServiceFlag || env.SERVICES)
|
||||
.split(",")
|
||||
.map((service) => service.trim())
|
||||
);
|
||||
|
||||
// The number of processes to run, defaults to the number of CPU's available
|
||||
// for the web service, and 1 for collaboration during the beta period.
|
||||
let processCount = env.WEB_CONCURRENCY
|
||||
? parseInt(env.WEB_CONCURRENCY, 10)
|
||||
: undefined;
|
||||
let processCount = env.WEB_CONCURRENCY;
|
||||
|
||||
if (serviceNames.includes("collaboration")) {
|
||||
if (processCount !== 1) {
|
||||
@@ -57,11 +51,11 @@ if (serviceNames.includes("collaboration")) {
|
||||
}
|
||||
|
||||
// This function will only be called once in the original process
|
||||
function master() {
|
||||
checkEnv();
|
||||
checkMigrations();
|
||||
async function master() {
|
||||
await checkEnv();
|
||||
await checkMigrations();
|
||||
|
||||
if (env.ENABLE_UPDATES !== "false" && process.env.NODE_ENV === "production") {
|
||||
if (env.TELEMETRY && env.ENVIRONMENT === "production") {
|
||||
checkUpdates();
|
||||
setInterval(checkUpdates, 24 * 3600 * 1000);
|
||||
}
|
||||
@@ -84,7 +78,7 @@ async function start(id: number, disconnect: () => void) {
|
||||
const router = new Router();
|
||||
|
||||
// install basic middleware shared by all services
|
||||
if ((env.DEBUG || "").includes("http")) {
|
||||
if (env.DEBUG.includes("http")) {
|
||||
app.use(logger((str) => Logger.info("http", str)));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ import Metrics from "@server/logging/metrics";
|
||||
import Sentry from "@server/logging/sentry";
|
||||
import * as Tracing from "./tracing";
|
||||
|
||||
const isProduction = env.NODE_ENV === "production";
|
||||
const isProduction = env.ENVIRONMENT === "production";
|
||||
|
||||
type LogCategory =
|
||||
| "lifecycle"
|
||||
| "hocuspocus"
|
||||
@@ -72,7 +73,7 @@ class Logger {
|
||||
warn(message: string, extra?: Extra) {
|
||||
Metrics.increment("logger.warning");
|
||||
|
||||
if (process.env.SENTRY_DSN) {
|
||||
if (env.SENTRY_DSN) {
|
||||
Sentry.withScope(function (scope) {
|
||||
scope.setLevel(Sentry.Severity.Warning);
|
||||
|
||||
@@ -104,7 +105,7 @@ class Logger {
|
||||
Metrics.increment("logger.error");
|
||||
Tracing.setError(error);
|
||||
|
||||
if (process.env.SENTRY_DSN) {
|
||||
if (env.SENTRY_DSN) {
|
||||
Sentry.withScope(function (scope) {
|
||||
scope.setLevel(Sentry.Severity.Error);
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import ddMetrics from "datadog-metrics";
|
||||
import env from "@server/env";
|
||||
|
||||
class Metrics {
|
||||
enabled = !!process.env.DD_API_KEY;
|
||||
enabled = !!env.DD_API_KEY;
|
||||
|
||||
constructor() {
|
||||
if (!this.enabled) {
|
||||
@@ -9,9 +10,9 @@ class Metrics {
|
||||
}
|
||||
|
||||
ddMetrics.init({
|
||||
apiKey: process.env.DD_API_KEY,
|
||||
apiKey: env.DD_API_KEY,
|
||||
prefix: "outline.",
|
||||
defaultTags: [`env:${process.env.DD_ENV || process.env.NODE_ENV}`],
|
||||
defaultTags: [`env:${process.env.DD_ENV ?? env.ENVIRONMENT}`],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export function requestErrorHandler(error: any, ctx: ContextWithState) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.SENTRY_DSN) {
|
||||
if (env.SENTRY_DSN) {
|
||||
Sentry.withScope(function (scope) {
|
||||
const requestId = ctx.headers["x-request-id"];
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { init, tracer, addTags, markAsError } from "@theo.gravity/datadog-apm";
|
||||
import env from "@server/env";
|
||||
|
||||
export * as APM from "@theo.gravity/datadog-apm";
|
||||
|
||||
// If the DataDog agent is installed and the DD_API_KEY environment variable is
|
||||
// in the environment then we can safely attempt to start the DD tracer
|
||||
if (process.env.DD_API_KEY) {
|
||||
if (env.DD_API_KEY) {
|
||||
init(
|
||||
{
|
||||
// SOURCE_COMMIT is used by Docker Hub
|
||||
@@ -13,7 +14,7 @@ if (process.env.DD_API_KEY) {
|
||||
service: process.env.DD_SERVICE || "outline",
|
||||
},
|
||||
{
|
||||
useMock: process.env.NODE_ENV === "test",
|
||||
useMock: env.ENVIRONMENT === "test",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import { Context } from "koa";
|
||||
import env from "@server/env";
|
||||
import Logger from "@server/logging/logger";
|
||||
import { signIn } from "@server/utils/authentication";
|
||||
import { AccountProvisionerResult } from "../commands/accountProvisioner";
|
||||
@@ -20,7 +21,7 @@ export default function createMiddleware(providerName: string) {
|
||||
return ctx.redirect(`${err.redirectUrl || "/"}?notice=${notice}`);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
if (env.ENVIRONMENT === "development") {
|
||||
throw err;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Default,
|
||||
DataType,
|
||||
} from "sequelize-typescript";
|
||||
import env from "@server/env";
|
||||
import Team from "./Team";
|
||||
import User from "./User";
|
||||
import Fix from "./decorators/Fix";
|
||||
@@ -48,7 +49,7 @@ class NotificationSetting extends Model {
|
||||
|
||||
get unsubscribeUrl() {
|
||||
const token = NotificationSetting.getUnsubscribeToken(this.userId);
|
||||
return `${process.env.URL}/api/notificationSettings.unsubscribe?token=${token}&id=${this.id}`;
|
||||
return `${env.URL}/api/notificationSettings.unsubscribe?token=${token}&id=${this.id}`;
|
||||
}
|
||||
|
||||
get unsubscribeToken() {
|
||||
@@ -73,7 +74,7 @@ class NotificationSetting extends Model {
|
||||
|
||||
static getUnsubscribeToken = (userId: string) => {
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(`${userId}-${process.env.SECRET_KEY}`);
|
||||
hash.update(`${userId}-${env.SECRET_KEY}`);
|
||||
return hash.digest("hex");
|
||||
};
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ class Team extends ParanoidModel {
|
||||
*/
|
||||
get emailSigninEnabled(): boolean {
|
||||
return (
|
||||
this.guestSignin && (!!env.SMTP_HOST || env.NODE_ENV === "development")
|
||||
this.guestSignin && (!!env.SMTP_HOST || env.ENVIRONMENT === "development")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -126,11 +126,11 @@ class Team extends ParanoidModel {
|
||||
return `https://${this.domain}`;
|
||||
}
|
||||
|
||||
if (!this.subdomain || process.env.SUBDOMAINS_ENABLED !== "true") {
|
||||
return process.env.URL;
|
||||
if (!this.subdomain || !env.SUBDOMAINS_ENABLED) {
|
||||
return env.URL;
|
||||
}
|
||||
|
||||
const url = new URL(process.env.URL || "");
|
||||
const url = new URL(env.URL);
|
||||
url.host = `${this.subdomain}.${stripSubdomain(url.host)}`;
|
||||
return url.href.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ import {
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { languages } from "@shared/i18n";
|
||||
import { stringToColor } from "@shared/utils/color";
|
||||
import env from "@server/env";
|
||||
import Logger from "@server/logging/logger";
|
||||
import { DEFAULT_AVATAR_HOST } from "@server/utils/avatars";
|
||||
import { publicS3Endpoint, uploadToS3FromUrl } from "@server/utils/s3";
|
||||
import { ValidationError } from "../errors";
|
||||
import ApiKey from "./ApiKey";
|
||||
@@ -137,7 +137,7 @@ class User extends ParanoidModel {
|
||||
@Column(DataType.JSONB)
|
||||
flags: { [key in UserFlag]?: number } | null;
|
||||
|
||||
@Default(process.env.DEFAULT_LANGUAGE)
|
||||
@Default(env.DEFAULT_LANGUAGE)
|
||||
@IsIn([languages])
|
||||
@Column
|
||||
language: string;
|
||||
@@ -156,7 +156,7 @@ class User extends ParanoidModel {
|
||||
.createHash("md5")
|
||||
.update(this.email || "")
|
||||
.digest("hex");
|
||||
return `${DEFAULT_AVATAR_HOST}/avatar/${hash}/${initial}.png?c=${color}`;
|
||||
return `${env.DEFAULT_AVATAR_HOST}/avatar/${hash}/${initial}.png?c=${color}`;
|
||||
}
|
||||
|
||||
set avatarUrl(value: string | null) {
|
||||
@@ -439,7 +439,7 @@ class User extends ParanoidModel {
|
||||
avatarUrl &&
|
||||
!avatarUrl.startsWith("/api") &&
|
||||
!avatarUrl.startsWith(endpoint) &&
|
||||
!avatarUrl.startsWith(DEFAULT_AVATAR_HOST)
|
||||
!avatarUrl.startsWith(env.DEFAULT_AVATAR_HOST)
|
||||
) {
|
||||
try {
|
||||
const newUrl = await uploadToS3FromUrl(
|
||||
|
||||
14
server/models/decorators/Deprecated.ts
Normal file
14
server/models/decorators/Deprecated.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
|
||||
const Deprecated = (message?: string) => (
|
||||
target: Object,
|
||||
propertyKey: string
|
||||
) => {
|
||||
if (process.env[propertyKey]) {
|
||||
console.warn(
|
||||
`The environment variable ${propertyKey} is deprecated and will be removed in a future release. ${message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Deprecated;
|
||||
@@ -1,26 +1,29 @@
|
||||
import { PublicEnv } from "@shared/types";
|
||||
import { Environment } from "@server/env";
|
||||
|
||||
// Note: This entire object is stringified in the HTML exposed to the client
|
||||
// do not add anything here that should be a secret or password
|
||||
export default function present(env: Record<string, any>): PublicEnv {
|
||||
export default function present(env: Environment): PublicEnv {
|
||||
return {
|
||||
URL: env.URL.replace(/\/$/, ""),
|
||||
AWS_S3_UPLOAD_BUCKET_URL: env.AWS_S3_UPLOAD_BUCKET_URL,
|
||||
AWS_S3_ACCELERATE_URL: env.AWS_S3_ACCELERATE_URL,
|
||||
AWS_S3_UPLOAD_BUCKET_URL: process.env.AWS_S3_UPLOAD_BUCKET_URL || "",
|
||||
AWS_S3_ACCELERATE_URL: process.env.AWS_S3_ACCELERATE_URL || "",
|
||||
CDN_URL: (env.CDN_URL || "").replace(/\/$/, ""),
|
||||
COLLABORATION_URL: (env.COLLABORATION_URL || env.URL)
|
||||
.replace(/\/$/, "")
|
||||
.replace(/^http/, "ws"),
|
||||
DEPLOYMENT: env.DEPLOYMENT,
|
||||
ENVIRONMENT: env.NODE_ENV,
|
||||
ENVIRONMENT: env.ENVIRONMENT,
|
||||
SENTRY_DSN: env.SENTRY_DSN,
|
||||
TEAM_LOGO: env.TEAM_LOGO,
|
||||
SLACK_KEY: env.SLACK_KEY,
|
||||
SLACK_CLIENT_ID: env.SLACK_CLIENT_ID,
|
||||
SLACK_APP_ID: env.SLACK_APP_ID,
|
||||
MAXIMUM_IMPORT_SIZE: env.MAXIMUM_IMPORT_SIZE || 1024 * 1000 * 5,
|
||||
SUBDOMAINS_ENABLED: env.SUBDOMAINS_ENABLED === "true",
|
||||
EMAIL_ENABLED: !!env.SMTP_HOST || env.NODE_ENV === "development",
|
||||
MAXIMUM_IMPORT_SIZE: env.MAXIMUM_IMPORT_SIZE,
|
||||
SUBDOMAINS_ENABLED: env.SUBDOMAINS_ENABLED,
|
||||
DEFAULT_LANGUAGE: env.DEFAULT_LANGUAGE,
|
||||
EMAIL_ENABLED: !!env.SMTP_HOST || env.ENVIRONMENT === "development",
|
||||
GOOGLE_ANALYTICS_ID: env.GOOGLE_ANALYTICS_ID,
|
||||
RELEASE: env.SOURCE_COMMIT || env.SOURCE_VERSION || undefined,
|
||||
RELEASE:
|
||||
process.env.SOURCE_COMMIT || process.env.SOURCE_VERSION || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import env from "@server/env";
|
||||
import { User } from "@server/models";
|
||||
|
||||
type Options = {
|
||||
@@ -38,8 +39,7 @@ export default (
|
||||
|
||||
if (options.includeDetails) {
|
||||
userData.email = user.email;
|
||||
userData.language =
|
||||
user.language || process.env.DEFAULT_LANGUAGE || "en_US";
|
||||
userData.language = user.language || env.DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
||||
return userData;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fetch from "fetch-with-proxy";
|
||||
import { Op } from "sequelize";
|
||||
import env from "@server/env";
|
||||
import { Document, Integration, Collection, Team } from "@server/models";
|
||||
import { presentSlackAttachment } from "@server/presenters";
|
||||
import {
|
||||
@@ -65,7 +66,7 @@ export default class SlackProcessor extends BaseProcessor {
|
||||
{
|
||||
color: collection.color,
|
||||
title: collection.name,
|
||||
title_link: `${process.env.URL}${collection.url}`,
|
||||
title_link: `${env.URL}${collection.url}`,
|
||||
text: collection.description,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Redis from "ioredis";
|
||||
import { defaults } from "lodash";
|
||||
import env from "@server/env";
|
||||
import Logger from "./logging/logger";
|
||||
|
||||
const defaultOptions = {
|
||||
@@ -12,23 +13,21 @@ const defaultOptions = {
|
||||
|
||||
// support Heroku Redis, see:
|
||||
// https://devcenter.heroku.com/articles/heroku-redis#ioredis-module
|
||||
tls:
|
||||
process.env.REDIS_URL && process.env.REDIS_URL.startsWith("rediss://")
|
||||
? {
|
||||
rejectUnauthorized: false,
|
||||
}
|
||||
: undefined,
|
||||
tls: (env.REDIS_URL || "").startsWith("rediss://")
|
||||
? {
|
||||
rejectUnauthorized: false,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
export default class RedisAdapter extends Redis {
|
||||
constructor(url: string | undefined) {
|
||||
if (!(url || "").startsWith("ioredis://")) {
|
||||
super(process.env.REDIS_URL, defaultOptions);
|
||||
if (!url || !url.startsWith("ioredis://")) {
|
||||
super(env.REDIS_URL, defaultOptions);
|
||||
} else {
|
||||
let customOptions = {};
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const decodedString = Buffer.from(url!.slice(10), "base64").toString();
|
||||
const decodedString = Buffer.from(url.slice(10), "base64").toString();
|
||||
customOptions = JSON.parse(decodedString);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to decode redis adapter options: ${error}`);
|
||||
@@ -52,12 +51,10 @@ export default class RedisAdapter extends Redis {
|
||||
private static _subscriber: RedisAdapter;
|
||||
|
||||
public static get defaultClient(): RedisAdapter {
|
||||
return this._client || (this._client = new this(process.env.REDIS_URL));
|
||||
return this._client || (this._client = new this(env.REDIS_URL));
|
||||
}
|
||||
|
||||
public static get defaultSubscriber(): RedisAdapter {
|
||||
return (
|
||||
this._subscriber || (this._subscriber = new this(process.env.REDIS_URL))
|
||||
);
|
||||
return this._subscriber || (this._subscriber = new this(env.REDIS_URL));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import TestServer from "fetch-test-server";
|
||||
import env from "@server/env";
|
||||
import webService from "@server/services/web";
|
||||
import { buildUser, buildTeam } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
@@ -57,7 +58,7 @@ describe("#auth.config", () => {
|
||||
});
|
||||
|
||||
it("should return available providers for team subdomain", async () => {
|
||||
process.env.URL = "http://localoutline.com";
|
||||
env.URL = "http://localoutline.com";
|
||||
await buildTeam({
|
||||
guestSignin: false,
|
||||
subdomain: "example",
|
||||
@@ -102,7 +103,7 @@ describe("#auth.config", () => {
|
||||
});
|
||||
|
||||
it("should return email provider for team when guest signin enabled", async () => {
|
||||
process.env.URL = "http://localoutline.com";
|
||||
env.URL = "http://localoutline.com";
|
||||
await buildTeam({
|
||||
guestSignin: true,
|
||||
subdomain: "example",
|
||||
@@ -126,7 +127,7 @@ describe("#auth.config", () => {
|
||||
});
|
||||
|
||||
it("should not return provider when disabled", async () => {
|
||||
process.env.URL = "http://localoutline.com";
|
||||
env.URL = "http://localoutline.com";
|
||||
await buildTeam({
|
||||
guestSignin: false,
|
||||
subdomain: "example",
|
||||
@@ -149,7 +150,7 @@ describe("#auth.config", () => {
|
||||
});
|
||||
describe("self hosted", () => {
|
||||
it("should return available providers for team", async () => {
|
||||
process.env.DEPLOYMENT = "";
|
||||
env.DEPLOYMENT = "";
|
||||
await buildTeam({
|
||||
guestSignin: false,
|
||||
authenticationProviders: [
|
||||
@@ -166,7 +167,7 @@ describe("#auth.config", () => {
|
||||
expect(body.data.providers[0].name).toBe("Slack");
|
||||
});
|
||||
it("should return email provider for team when guest signin enabled", async () => {
|
||||
process.env.DEPLOYMENT = "";
|
||||
env.DEPLOYMENT = "";
|
||||
await buildTeam({
|
||||
guestSignin: true,
|
||||
authenticationProviders: [
|
||||
|
||||
@@ -2,6 +2,7 @@ import invariant from "invariant";
|
||||
import Router from "koa-router";
|
||||
import { find } from "lodash";
|
||||
import { parseDomain, isCustomSubdomain } from "@shared/utils/domains";
|
||||
import env from "@server/env";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { Team, TeamDomain } from "@server/models";
|
||||
import { presentUser, presentTeam, presentPolicies } from "@server/presenters";
|
||||
@@ -10,7 +11,7 @@ import providers from "../auth/providers";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
function filterProviders(team: Team) {
|
||||
function filterProviders(team?: Team) {
|
||||
return providers
|
||||
.sort((provider) => (provider.id === "email" ? 1 : -1))
|
||||
.filter((provider) => {
|
||||
@@ -39,7 +40,7 @@ router.post("auth.config", async (ctx) => {
|
||||
// If self hosted AND there is only one team then that team becomes the
|
||||
// brand for the knowledge base and it's guest signin option is used for the
|
||||
// root login page.
|
||||
if (process.env.DEPLOYMENT !== "hosted") {
|
||||
if (env.DEPLOYMENT !== "hosted") {
|
||||
const teams = await Team.scope("withAuthenticationProviders").findAll();
|
||||
|
||||
if (teams.length === 1) {
|
||||
@@ -76,7 +77,7 @@ router.post("auth.config", async (ctx) => {
|
||||
// If subdomain signin page then we return minimal team details to allow
|
||||
// for a custom screen showing only relevant signin options for that team.
|
||||
if (
|
||||
process.env.SUBDOMAINS_ENABLED === "true" &&
|
||||
env.SUBDOMAINS_ENABLED &&
|
||||
isCustomSubdomain(ctx.request.hostname) &&
|
||||
!isCustomDomain(ctx.request.hostname)
|
||||
) {
|
||||
@@ -103,7 +104,6 @@ router.post("auth.config", async (ctx) => {
|
||||
// Otherwise, we're requesting from the standard root signin page
|
||||
ctx.body = {
|
||||
data: {
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 1 arguments, but got 0.
|
||||
providers: filterProviders(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Context } from "koa";
|
||||
import Router from "koa-router";
|
||||
import env from "@server/env";
|
||||
import { AuthenticationError } from "@server/errors";
|
||||
import CleanupDeletedDocumentsTask from "@server/queues/tasks/CleanupDeletedDocumentsTask";
|
||||
import CleanupDeletedTeamsTask from "@server/queues/tasks/CleanupDeletedTeamsTask";
|
||||
@@ -11,7 +12,7 @@ const router = new Router();
|
||||
const cronHandler = async (ctx: Context) => {
|
||||
const { token, limit = 500 } = ctx.body as { token?: string; limit: number };
|
||||
|
||||
if (process.env.UTILS_SECRET !== token) {
|
||||
if (env.UTILS_SECRET !== token) {
|
||||
throw AuthenticationError("Invalid secret token");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import TestServer from "fetch-test-server";
|
||||
import env from "@server/env";
|
||||
import { IntegrationAuthentication, SearchQuery } from "@server/models";
|
||||
import webService from "@server/services/web";
|
||||
import { buildDocument, buildIntegration } from "@server/test/factories";
|
||||
@@ -24,7 +25,7 @@ describe("#hooks.unfurl", () => {
|
||||
});
|
||||
const res = await server.post("/api/hooks.unfurl", {
|
||||
body: {
|
||||
token: process.env.SLACK_VERIFICATION_TOKEN,
|
||||
token: env.SLACK_VERIFICATION_TOKEN,
|
||||
team_id: "TXXXXXXXX",
|
||||
api_app_id: "AXXXXXXXXX",
|
||||
event: {
|
||||
@@ -51,7 +52,7 @@ describe("#hooks.slack", () => {
|
||||
const { user, team } = await seed();
|
||||
const res = await server.post("/api/hooks.slack", {
|
||||
body: {
|
||||
token: process.env.SLACK_VERIFICATION_TOKEN,
|
||||
token: env.SLACK_VERIFICATION_TOKEN,
|
||||
user_id: user.authentications[0].providerId,
|
||||
team_id: team.authenticationProviders[0].providerId,
|
||||
text: "dsfkndfskndsfkn",
|
||||
@@ -71,7 +72,7 @@ describe("#hooks.slack", () => {
|
||||
});
|
||||
const res = await server.post("/api/hooks.slack", {
|
||||
body: {
|
||||
token: process.env.SLACK_VERIFICATION_TOKEN,
|
||||
token: env.SLACK_VERIFICATION_TOKEN,
|
||||
user_id: user.authentications[0].providerId,
|
||||
team_id: team.authenticationProviders[0].providerId,
|
||||
text: "contains",
|
||||
@@ -93,7 +94,7 @@ describe("#hooks.slack", () => {
|
||||
});
|
||||
const res = await server.post("/api/hooks.slack", {
|
||||
body: {
|
||||
token: process.env.SLACK_VERIFICATION_TOKEN,
|
||||
token: env.SLACK_VERIFICATION_TOKEN,
|
||||
user_id: user.authentications[0].providerId,
|
||||
team_id: team.authenticationProviders[0].providerId,
|
||||
text: "*contains",
|
||||
@@ -113,7 +114,7 @@ describe("#hooks.slack", () => {
|
||||
});
|
||||
const res = await server.post("/api/hooks.slack", {
|
||||
body: {
|
||||
token: process.env.SLACK_VERIFICATION_TOKEN,
|
||||
token: env.SLACK_VERIFICATION_TOKEN,
|
||||
user_id: user.authentications[0].providerId,
|
||||
team_id: team.authenticationProviders[0].providerId,
|
||||
text: "contains",
|
||||
@@ -132,7 +133,7 @@ describe("#hooks.slack", () => {
|
||||
const { user, team } = await seed();
|
||||
await server.post("/api/hooks.slack", {
|
||||
body: {
|
||||
token: process.env.SLACK_VERIFICATION_TOKEN,
|
||||
token: env.SLACK_VERIFICATION_TOKEN,
|
||||
user_id: user.authentications[0].providerId,
|
||||
team_id: team.authenticationProviders[0].providerId,
|
||||
text: "contains",
|
||||
@@ -160,7 +161,7 @@ describe("#hooks.slack", () => {
|
||||
const { user, team } = await seed();
|
||||
const res = await server.post("/api/hooks.slack", {
|
||||
body: {
|
||||
token: process.env.SLACK_VERIFICATION_TOKEN,
|
||||
token: env.SLACK_VERIFICATION_TOKEN,
|
||||
user_id: user.authentications[0].providerId,
|
||||
team_id: team.authenticationProviders[0].providerId,
|
||||
text: "help",
|
||||
@@ -175,7 +176,7 @@ describe("#hooks.slack", () => {
|
||||
const { user, team } = await seed();
|
||||
const res = await server.post("/api/hooks.slack", {
|
||||
body: {
|
||||
token: process.env.SLACK_VERIFICATION_TOKEN,
|
||||
token: env.SLACK_VERIFICATION_TOKEN,
|
||||
user_id: user.authentications[0].providerId,
|
||||
team_id: team.authenticationProviders[0].providerId,
|
||||
text: "",
|
||||
@@ -202,7 +203,7 @@ describe("#hooks.slack", () => {
|
||||
});
|
||||
const res = await server.post("/api/hooks.slack", {
|
||||
body: {
|
||||
token: process.env.SLACK_VERIFICATION_TOKEN,
|
||||
token: env.SLACK_VERIFICATION_TOKEN,
|
||||
user_id: "unknown-slack-user-id",
|
||||
team_id: team.authenticationProviders[0].providerId,
|
||||
text: "contains",
|
||||
@@ -234,7 +235,7 @@ describe("#hooks.slack", () => {
|
||||
});
|
||||
const res = await server.post("/api/hooks.slack", {
|
||||
body: {
|
||||
token: process.env.SLACK_VERIFICATION_TOKEN,
|
||||
token: env.SLACK_VERIFICATION_TOKEN,
|
||||
user_id: "unknown-slack-user-id",
|
||||
team_id: serviceTeamId,
|
||||
text: "contains",
|
||||
@@ -273,7 +274,7 @@ describe("#hooks.interactive", () => {
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const payload = JSON.stringify({
|
||||
token: process.env.SLACK_VERIFICATION_TOKEN,
|
||||
token: env.SLACK_VERIFICATION_TOKEN,
|
||||
user: {
|
||||
id: user.authentications[0].providerId,
|
||||
},
|
||||
@@ -302,7 +303,7 @@ describe("#hooks.interactive", () => {
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const payload = JSON.stringify({
|
||||
token: process.env.SLACK_VERIFICATION_TOKEN,
|
||||
token: env.SLACK_VERIFICATION_TOKEN,
|
||||
user: {
|
||||
id: "unknown-slack-user-id",
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import invariant from "invariant";
|
||||
import Router from "koa-router";
|
||||
import { escapeRegExp } from "lodash";
|
||||
import env from "@server/env";
|
||||
import { AuthenticationError, InvalidRequestError } from "@server/errors";
|
||||
import Logger from "@server/logging/logger";
|
||||
import {
|
||||
@@ -26,7 +27,7 @@ router.post("hooks.unfurl", async (ctx) => {
|
||||
return (ctx.body = ctx.body.challenge);
|
||||
}
|
||||
|
||||
if (token !== process.env.SLACK_VERIFICATION_TOKEN) {
|
||||
if (token !== env.SLACK_VERIFICATION_TOKEN) {
|
||||
throw AuthenticationError("Invalid token");
|
||||
}
|
||||
|
||||
@@ -89,7 +90,7 @@ router.post("hooks.interactive", async (ctx) => {
|
||||
assertPresent(token, "token is required");
|
||||
assertPresent(callback_id, "callback_id is required");
|
||||
|
||||
if (token !== process.env.SLACK_VERIFICATION_TOKEN) {
|
||||
if (token !== env.SLACK_VERIFICATION_TOKEN) {
|
||||
throw AuthenticationError("Invalid verification token");
|
||||
}
|
||||
|
||||
@@ -127,7 +128,7 @@ router.post("hooks.slack", async (ctx) => {
|
||||
assertPresent(team_id, "team_id is required");
|
||||
assertPresent(user_id, "user_id is required");
|
||||
|
||||
if (token !== process.env.SLACK_VERIFICATION_TOKEN) {
|
||||
if (token !== env.SLACK_VERIFICATION_TOKEN) {
|
||||
throw AuthenticationError("Invalid verification token");
|
||||
}
|
||||
|
||||
@@ -289,7 +290,7 @@ router.post("hooks.slack", async (ctx) => {
|
||||
result.document.collection,
|
||||
team,
|
||||
queryIsInTitle ? undefined : result.context,
|
||||
process.env.SLACK_MESSAGE_ACTIONS
|
||||
env.SLACK_MESSAGE_ACTIONS
|
||||
? [
|
||||
{
|
||||
name: "post",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Router from "koa-router";
|
||||
import env from "@server/env";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { Team, NotificationSetting } from "@server/models";
|
||||
import { authorize } from "@server/policies";
|
||||
@@ -75,7 +76,7 @@ router.post("notificationSettings.unsubscribe", async (ctx) => {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.redirect(`${process.env.URL}?notice=invalid-auth`);
|
||||
ctx.redirect(`${env.URL}?notice=invalid-auth`);
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -5,6 +5,7 @@ import userInviter from "@server/commands/userInviter";
|
||||
import userSuspender from "@server/commands/userSuspender";
|
||||
import { sequelize } from "@server/database/sequelize";
|
||||
import InviteEmail from "@server/emails/templates/InviteEmail";
|
||||
import env from "@server/env";
|
||||
import { ValidationError } from "@server/errors";
|
||||
import logger from "@server/logging/logger";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
@@ -338,11 +339,11 @@ router.post("users.resendInvite", auth(), async (ctx) => {
|
||||
user.incrementFlag(UserFlag.InviteSent);
|
||||
await user.save({ transaction });
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
if (env.ENVIRONMENT === "development") {
|
||||
logger.info(
|
||||
"email",
|
||||
`Sign in immediately: ${
|
||||
process.env.URL
|
||||
env.URL
|
||||
}/auth/email.callback?token=${user.getEmailSigninToken()}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,25 +15,22 @@ import { StateStore, request } from "@server/utils/passport";
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "azure";
|
||||
const AZURE_CLIENT_ID = process.env.AZURE_CLIENT_ID;
|
||||
const AZURE_CLIENT_SECRET = process.env.AZURE_CLIENT_SECRET;
|
||||
const AZURE_RESOURCE_APP_ID = process.env.AZURE_RESOURCE_APP_ID;
|
||||
const scopes: string[] = [];
|
||||
|
||||
export const config = {
|
||||
name: "Microsoft",
|
||||
enabled: !!AZURE_CLIENT_ID,
|
||||
enabled: !!env.AZURE_CLIENT_ID,
|
||||
};
|
||||
|
||||
if (AZURE_CLIENT_ID && AZURE_CLIENT_SECRET) {
|
||||
if (env.AZURE_CLIENT_ID && env.AZURE_CLIENT_SECRET) {
|
||||
const strategy = new AzureStrategy(
|
||||
{
|
||||
clientID: AZURE_CLIENT_ID,
|
||||
clientSecret: AZURE_CLIENT_SECRET,
|
||||
clientID: env.AZURE_CLIENT_ID,
|
||||
clientSecret: env.AZURE_CLIENT_SECRET,
|
||||
callbackURL: `${env.URL}/auth/azure.callback`,
|
||||
useCommonEndpoint: true,
|
||||
passReqToCallback: true,
|
||||
resource: AZURE_RESOURCE_APP_ID,
|
||||
resource: env.AZURE_RESOURCE_APP_ID,
|
||||
// @ts-expect-error StateStore
|
||||
store: new StateStore(),
|
||||
scope: scopes,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import TestServer from "fetch-test-server";
|
||||
import SigninEmail from "@server/emails/templates/SigninEmail";
|
||||
import WelcomeEmail from "@server/emails/templates/WelcomeEmail";
|
||||
import env from "@server/env";
|
||||
import webService from "@server/services/web";
|
||||
import { buildUser, buildGuestUser, buildTeam } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
@@ -40,8 +41,8 @@ describe("email", () => {
|
||||
});
|
||||
|
||||
it("should respond with redirect location when user is SSO enabled on another subdomain", async () => {
|
||||
process.env.URL = "http://localoutline.com";
|
||||
process.env.SUBDOMAINS_ENABLED = "true";
|
||||
env.URL = "http://localoutline.com";
|
||||
env.SUBDOMAINS_ENABLED = true;
|
||||
const user = await buildUser();
|
||||
const spy = jest.spyOn(WelcomeEmail, "schedule");
|
||||
await buildTeam({
|
||||
@@ -93,8 +94,8 @@ describe("email", () => {
|
||||
describe("with multiple users matching email", () => {
|
||||
it("should default to current subdomain with SSO", async () => {
|
||||
const spy = jest.spyOn(SigninEmail, "schedule");
|
||||
process.env.URL = "http://localoutline.com";
|
||||
process.env.SUBDOMAINS_ENABLED = "true";
|
||||
env.URL = "http://localoutline.com";
|
||||
env.SUBDOMAINS_ENABLED = true;
|
||||
const email = "sso-user@example.org";
|
||||
const team = await buildTeam({
|
||||
subdomain: "example",
|
||||
@@ -123,8 +124,8 @@ describe("email", () => {
|
||||
|
||||
it("should default to current subdomain with guest email", async () => {
|
||||
const spy = jest.spyOn(SigninEmail, "schedule");
|
||||
process.env.URL = "http://localoutline.com";
|
||||
process.env.SUBDOMAINS_ENABLED = "true";
|
||||
env.URL = "http://localoutline.com";
|
||||
env.SUBDOMAINS_ENABLED = true;
|
||||
const email = "guest-user@example.org";
|
||||
const team = await buildTeam({
|
||||
subdomain: "example",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { find } from "lodash";
|
||||
import { parseDomain, isCustomSubdomain } from "@shared/utils/domains";
|
||||
import SigninEmail from "@server/emails/templates/SigninEmail";
|
||||
import WelcomeEmail from "@server/emails/templates/WelcomeEmail";
|
||||
import env from "@server/env";
|
||||
import { AuthorizationError } from "@server/errors";
|
||||
import errorHandling from "@server/middlewares/errorHandling";
|
||||
import methodOverride from "@server/middlewares/methodOverride";
|
||||
@@ -43,7 +44,7 @@ router.post("email", errorHandling(), async (ctx) => {
|
||||
}
|
||||
|
||||
if (
|
||||
process.env.SUBDOMAINS_ENABLED === "true" &&
|
||||
env.SUBDOMAINS_ENABLED &&
|
||||
isCustomSubdomain(ctx.request.hostname) &&
|
||||
!isCustomDomain(ctx.request.hostname)
|
||||
) {
|
||||
|
||||
@@ -15,8 +15,6 @@ import { StateStore } from "@server/utils/passport";
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "google";
|
||||
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
|
||||
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
|
||||
const scopes = [
|
||||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
@@ -24,7 +22,7 @@ const scopes = [
|
||||
|
||||
export const config = {
|
||||
name: "Google",
|
||||
enabled: !!GOOGLE_CLIENT_ID,
|
||||
enabled: !!env.GOOGLE_CLIENT_ID,
|
||||
};
|
||||
|
||||
type GoogleProfile = Profile & {
|
||||
@@ -35,12 +33,12 @@ type GoogleProfile = Profile & {
|
||||
};
|
||||
};
|
||||
|
||||
if (GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET) {
|
||||
if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
|
||||
passport.use(
|
||||
new GoogleStrategy(
|
||||
{
|
||||
clientID: GOOGLE_CLIENT_ID,
|
||||
clientSecret: GOOGLE_CLIENT_SECRET,
|
||||
clientID: env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET,
|
||||
callbackURL: `${env.URL}/auth/google.callback`,
|
||||
passReqToCallback: true,
|
||||
// @ts-expect-error StateStore
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import { Request } from "koa";
|
||||
import Router from "koa-router";
|
||||
import { get } from "lodash";
|
||||
import { Strategy } from "passport-oauth2";
|
||||
@@ -13,21 +14,15 @@ import { StateStore, request } from "@server/utils/passport";
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "oidc";
|
||||
const OIDC_DISPLAY_NAME = process.env.OIDC_DISPLAY_NAME || "OpenID Connect";
|
||||
const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID || "";
|
||||
const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET || "";
|
||||
const OIDC_AUTH_URI = process.env.OIDC_AUTH_URI || "";
|
||||
const OIDC_TOKEN_URI = process.env.OIDC_TOKEN_URI || "";
|
||||
const OIDC_USERINFO_URI = process.env.OIDC_USERINFO_URI || "";
|
||||
const OIDC_SCOPES = process.env.OIDC_SCOPES || "";
|
||||
const OIDC_USERNAME_CLAIM =
|
||||
process.env.OIDC_USERNAME_CLAIM || "preferred_username";
|
||||
const OIDC_AUTH_URI = env.OIDC_AUTH_URI || "";
|
||||
const OIDC_TOKEN_URI = env.OIDC_TOKEN_URI || "";
|
||||
const OIDC_USERINFO_URI = env.OIDC_USERINFO_URI || "";
|
||||
|
||||
export const config = {
|
||||
name: OIDC_DISPLAY_NAME,
|
||||
enabled: !!OIDC_CLIENT_ID,
|
||||
name: env.OIDC_DISPLAY_NAME,
|
||||
enabled: !!env.OIDC_CLIENT_ID,
|
||||
};
|
||||
const scopes = OIDC_SCOPES.split(" ");
|
||||
const scopes = env.OIDC_SCOPES.split(" ");
|
||||
|
||||
Strategy.prototype.userProfile = async function (accessToken, done) {
|
||||
try {
|
||||
@@ -38,18 +33,18 @@ Strategy.prototype.userProfile = async function (accessToken, done) {
|
||||
}
|
||||
};
|
||||
|
||||
if (OIDC_CLIENT_ID) {
|
||||
if (env.OIDC_CLIENT_ID && env.OIDC_CLIENT_SECRET) {
|
||||
passport.use(
|
||||
providerName,
|
||||
new Strategy(
|
||||
{
|
||||
authorizationURL: OIDC_AUTH_URI,
|
||||
tokenURL: OIDC_TOKEN_URI,
|
||||
clientID: OIDC_CLIENT_ID,
|
||||
clientSecret: OIDC_CLIENT_SECRET,
|
||||
clientID: env.OIDC_CLIENT_ID,
|
||||
clientSecret: env.OIDC_CLIENT_SECRET,
|
||||
callbackURL: `${env.URL}/auth/${providerName}.callback`,
|
||||
passReqToCallback: true,
|
||||
scope: OIDC_SCOPES,
|
||||
scope: env.OIDC_SCOPES,
|
||||
// @ts-expect-error custom state store
|
||||
store: new StateStore(),
|
||||
state: true,
|
||||
@@ -62,7 +57,7 @@ if (OIDC_CLIENT_ID) {
|
||||
// Any claim supplied in response to the userinfo request will be
|
||||
// available on the `profile` parameter
|
||||
async function (
|
||||
req: any,
|
||||
req: Request,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
profile: Record<string, string>,
|
||||
@@ -97,7 +92,7 @@ if (OIDC_CLIENT_ID) {
|
||||
avatarUrl: profile.picture,
|
||||
// Claim name can be overriden using an env variable.
|
||||
// Default is 'preferred_username' as per OIDC spec.
|
||||
username: get(profile, OIDC_USERNAME_CLAIM),
|
||||
username: get(profile, env.OIDC_USERNAME_CLAIM),
|
||||
},
|
||||
authenticationProvider: {
|
||||
name: providerName,
|
||||
|
||||
@@ -39,8 +39,6 @@ type SlackProfile = Profile & {
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "slack";
|
||||
const SLACK_CLIENT_ID = process.env.SLACK_KEY;
|
||||
const SLACK_CLIENT_SECRET = process.env.SLACK_SECRET;
|
||||
const scopes = [
|
||||
"identity.email",
|
||||
"identity.basic",
|
||||
@@ -50,14 +48,14 @@ const scopes = [
|
||||
|
||||
export const config = {
|
||||
name: "Slack",
|
||||
enabled: !!SLACK_CLIENT_ID,
|
||||
enabled: !!env.SLACK_CLIENT_ID,
|
||||
};
|
||||
|
||||
if (SLACK_CLIENT_ID && SLACK_CLIENT_SECRET) {
|
||||
if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
const strategy = new SlackStrategy(
|
||||
{
|
||||
clientID: SLACK_CLIENT_ID,
|
||||
clientSecret: SLACK_CLIENT_SECRET,
|
||||
clientID: env.SLACK_CLIENT_ID,
|
||||
clientSecret: env.SLACK_CLIENT_SECRET,
|
||||
callbackURL: `${env.URL}/auth/slack.callback`,
|
||||
passReqToCallback: true,
|
||||
// @ts-expect-error StateStore
|
||||
@@ -151,7 +149,7 @@ if (SLACK_CLIENT_ID && SLACK_CLIENT_SECRET) {
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint = `${process.env.URL || ""}/auth/slack.commands`;
|
||||
const endpoint = `${env.URL}/auth/slack.commands`;
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string | string[] | undefined' i... Remove this comment to see the full error message
|
||||
const data = await Slack.oauthAccess(code, endpoint);
|
||||
const authentication = await IntegrationAuthentication.create({
|
||||
@@ -210,7 +208,7 @@ if (SLACK_CLIENT_ID && SLACK_CLIENT_SECRET) {
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint = `${process.env.URL || ""}/auth/slack.post`;
|
||||
const endpoint = `${env.URL}/auth/slack.post`;
|
||||
const data = await Slack.oauthAccess(code as string, endpoint);
|
||||
const authentication = await IntegrationAuthentication.create({
|
||||
service: "slack",
|
||||
|
||||
@@ -16,8 +16,8 @@ import { robotsResponse } from "@server/utils/robots";
|
||||
import apexRedirect from "../middlewares/apexRedirect";
|
||||
import presentEnv from "../presenters/env";
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
const isTest = process.env.NODE_ENV === "test";
|
||||
const isProduction = env.ENVIRONMENT === "production";
|
||||
const isTest = env.ENVIRONMENT === "test";
|
||||
const koa = new Koa();
|
||||
const router = new Router();
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
@@ -62,7 +62,7 @@ const renderApp = async (ctx: Context, next: Next, title = "Outline") => {
|
||||
.replace(/\/\/inject-env\/\//g, environment)
|
||||
.replace(/\/\/inject-title\/\//g, title)
|
||||
.replace(/\/\/inject-prefetch\/\//g, shareId ? "" : prefetchTags)
|
||||
.replace(/\/\/inject-slack-app-id\/\//g, process.env.SLACK_APP_ID || "");
|
||||
.replace(/\/\/inject-slack-app-id\/\//g, env.SLACK_APP_ID || "");
|
||||
};
|
||||
|
||||
const renderShare = async (ctx: Context, next: Next) => {
|
||||
@@ -93,7 +93,7 @@ koa.use(
|
||||
})
|
||||
);
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
if (isProduction) {
|
||||
router.get("/static/*", async (ctx) => {
|
||||
try {
|
||||
const pathname = ctx.path.substring(8);
|
||||
@@ -135,9 +135,7 @@ router.get("/locales/:lng.json", async (ctx) => {
|
||||
setHeaders: (res) => {
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
process.env.NODE_ENV === "production"
|
||||
? `max-age=${7 * 24 * 60 * 60}`
|
||||
: "no-cache"
|
||||
isProduction ? `max-age=${7 * 24 * 60 * 60}` : "no-cache"
|
||||
);
|
||||
},
|
||||
root: path.join(__dirname, "../../shared/i18n/locales"),
|
||||
|
||||
@@ -13,8 +13,9 @@ import routes from "../routes";
|
||||
import api from "../routes/api";
|
||||
import auth from "../routes/auth";
|
||||
|
||||
const isProduction = env.NODE_ENV === "production";
|
||||
const isTest = env.NODE_ENV === "test";
|
||||
const isProduction = env.ENVIRONMENT === "production";
|
||||
const isTest = env.ENVIRONMENT === "test";
|
||||
|
||||
// Construct scripts CSP based on services in use by this installation
|
||||
const defaultSrc = ["'self'"];
|
||||
const scriptSrc = [
|
||||
@@ -36,7 +37,7 @@ if (env.CDN_URL) {
|
||||
export default function init(app: Koa = new Koa()): Koa {
|
||||
if (isProduction) {
|
||||
// Force redirect to HTTPS protocol unless explicitly disabled
|
||||
if (process.env.FORCE_HTTPS !== "false") {
|
||||
if (env.FORCE_HTTPS) {
|
||||
app.use(
|
||||
enforceHttps({
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ trustProtoHeader: boolean; }' ... Remove this comment to see the full error message
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import "../env";
|
||||
import env from "../env";
|
||||
|
||||
// test environment variables
|
||||
process.env.SMTP_HOST = "smtp.example.com";
|
||||
process.env.DATABASE_URL = process.env.DATABASE_URL_TEST;
|
||||
process.env.NODE_ENV = "test";
|
||||
process.env.GOOGLE_CLIENT_ID = "123";
|
||||
process.env.SLACK_KEY = "123";
|
||||
process.env.DEPLOYMENT = "";
|
||||
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.DEPLOYMENT = undefined;
|
||||
|
||||
if (process.env.DATABASE_URL_TEST) {
|
||||
env.DATABASE_URL = process.env.DATABASE_URL_TEST;
|
||||
}
|
||||
|
||||
// NOTE: this require must come after the ENV var override above
|
||||
// so that sequelize uses the test config variables
|
||||
|
||||
@@ -2,6 +2,7 @@ import querystring from "querystring";
|
||||
import { addMonths } from "date-fns";
|
||||
import { Context } from "koa";
|
||||
import { pick } from "lodash";
|
||||
import env from "@server/env";
|
||||
import Logger from "@server/logging/logger";
|
||||
import { User, Event, Team, Collection, View } from "@server/models";
|
||||
import { getCookieDomain } from "@server/utils/domains";
|
||||
@@ -64,7 +65,7 @@ export async function signIn(
|
||||
|
||||
// set a transfer cookie for the access token itself and redirect
|
||||
// to the teams subdomain if subdomains are enabled
|
||||
if (process.env.SUBDOMAINS_ENABLED === "true" && team.subdomain) {
|
||||
if (env.SUBDOMAINS_ENABLED && team.subdomain) {
|
||||
// get any existing sessions (teams signed in) and add this team
|
||||
const existing = JSON.parse(
|
||||
decodeURIComponent(ctx.cookies.get("sessions") || "") || "{}"
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import crypto from "crypto";
|
||||
import fetch from "fetch-with-proxy";
|
||||
|
||||
export const DEFAULT_AVATAR_HOST =
|
||||
process.env.DEFAULT_AVATAR_HOST || "https://tiley.herokuapp.com";
|
||||
import env from "@server/env";
|
||||
|
||||
export async function generateAvatarUrl({
|
||||
id,
|
||||
@@ -30,8 +28,8 @@ export async function generateAvatarUrl({
|
||||
}
|
||||
}
|
||||
|
||||
const tileyUrl = `${DEFAULT_AVATAR_HOST}/avatar/${hashedId}/${encodeURIComponent(
|
||||
name[0]
|
||||
)}.png`;
|
||||
const tileyUrl = `${
|
||||
env.DEFAULT_AVATAR_HOST
|
||||
}/avatar/${hashedId}/${encodeURIComponent(name[0])}.png`;
|
||||
return cbUrl && cbResponse && cbResponse.status === 200 ? cbUrl : tileyUrl;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { parseDomain, stripSubdomain } from "@shared/utils/domains";
|
||||
import env from "@server/env";
|
||||
|
||||
export function getCookieDomain(domain: string) {
|
||||
return process.env.SUBDOMAINS_ENABLED === "true"
|
||||
? stripSubdomain(domain)
|
||||
: domain;
|
||||
return env.SUBDOMAINS_ENABLED ? stripSubdomain(domain) : domain;
|
||||
}
|
||||
|
||||
export function isCustomDomain(hostname: string) {
|
||||
const parsed = parseDomain(hostname);
|
||||
const main = parseDomain(process.env.URL);
|
||||
const main = parseDomain(env.URL);
|
||||
|
||||
return (
|
||||
parsed && main && (main.domain !== parsed.domain || main.tld !== parsed.tld)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect } from "@jest/globals";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import env from "@server/env";
|
||||
import parseAttachmentIds from "./parseAttachmentIds";
|
||||
|
||||
it("should return an empty array with no matches", () => {
|
||||
@@ -32,9 +33,8 @@ it("should parse attachment ID from markdown with additional query params", () =
|
||||
it("should parse attachment ID from markdown with fully qualified url", () => {
|
||||
const uuid = uuidv4();
|
||||
const results = parseAttachmentIds(
|
||||
``
|
||||
``
|
||||
);
|
||||
expect(process.env.URL).toBeTruthy();
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0]).toBe(uuid);
|
||||
});
|
||||
@@ -69,9 +69,8 @@ it("should parse attachment ID from html", () => {
|
||||
it("should parse attachment ID from html with fully qualified url", () => {
|
||||
const uuid = uuidv4();
|
||||
const results = parseAttachmentIds(
|
||||
`<img src="${process.env.URL}/api/attachments.redirect?id=${uuid}" />`
|
||||
`<img src="${env.URL}/api/attachments.redirect?id=${uuid}" />`
|
||||
);
|
||||
expect(process.env.URL).toBeTruthy();
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0]).toBe(uuid);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Queue from "bull";
|
||||
import { snakeCase } from "lodash";
|
||||
import env from "@server/env";
|
||||
import Metrics from "@server/logging/metrics";
|
||||
import Redis from "../redis";
|
||||
|
||||
@@ -18,7 +19,7 @@ export function createQueue(
|
||||
return Redis.defaultSubscriber;
|
||||
|
||||
default:
|
||||
return new Redis(process.env.REDIS_URL);
|
||||
return new Redis(env.REDIS_URL);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import env from "@server/env";
|
||||
|
||||
const DISALLOW_ROBOTS = `User-agent: *
|
||||
Disallow: /`;
|
||||
|
||||
export const robotsResponse = () => {
|
||||
if (process.env.DEPLOYMENT !== "hosted") {
|
||||
if (env.DEPLOYMENT !== "hosted") {
|
||||
return DISALLOW_ROBOTS;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import querystring from "querystring";
|
||||
import fetch from "fetch-with-proxy";
|
||||
import env from "@server/env";
|
||||
import { InvalidRequestError } from "../errors";
|
||||
|
||||
const SLACK_API_URL = "https://slack.com/api";
|
||||
@@ -48,11 +49,11 @@ export async function request(endpoint: string, body: Record<string, any>) {
|
||||
|
||||
export async function oauthAccess(
|
||||
code: string,
|
||||
redirect_uri = `${process.env.URL || ""}/auth/slack.callback`
|
||||
redirect_uri = `${env.URL}/auth/slack.callback`
|
||||
) {
|
||||
return request("oauth.access", {
|
||||
client_id: process.env.SLACK_KEY,
|
||||
client_secret: process.env.SLACK_SECRET,
|
||||
client_id: env.SLACK_CLIENT_ID,
|
||||
client_secret: env.SLACK_CLIENT_SECRET,
|
||||
redirect_uri,
|
||||
code,
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import chalk from "chalk";
|
||||
import env from "@server/env";
|
||||
import Logger from "@server/logging/logger";
|
||||
import AuthenticationProvider from "@server/models/AuthenticationProvider";
|
||||
import Team from "@server/models/Team";
|
||||
|
||||
export async function checkMigrations() {
|
||||
if (process.env.DEPLOYMENT === "hosted") {
|
||||
if (env.DEPLOYMENT === "hosted") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -22,92 +23,27 @@ $ node ./build/server/scripts/20210226232041-migrate-authentication.js
|
||||
}
|
||||
}
|
||||
|
||||
export function checkEnv() {
|
||||
const errors = [];
|
||||
|
||||
if (
|
||||
!process.env.SECRET_KEY ||
|
||||
process.env.SECRET_KEY === "generate_a_new_key"
|
||||
) {
|
||||
errors.push(
|
||||
`The ${chalk.bold(
|
||||
"SECRET_KEY"
|
||||
)} env variable must be set with the output of ${chalk.bold(
|
||||
"$ openssl rand -hex 32"
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!process.env.UTILS_SECRET ||
|
||||
process.env.UTILS_SECRET === "generate_a_new_key"
|
||||
) {
|
||||
errors.push(
|
||||
`The ${chalk.bold(
|
||||
"UTILS_SECRET"
|
||||
)} env variable must be set with a secret value, it is recommended to use the output of ${chalk.bold(
|
||||
"$ openssl rand -hex 32"
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.AWS_ACCESS_KEY_ID) {
|
||||
[
|
||||
"AWS_REGION",
|
||||
"AWS_SECRET_ACCESS_KEY",
|
||||
"AWS_S3_UPLOAD_BUCKET_URL",
|
||||
"AWS_S3_UPLOAD_MAX_SIZE",
|
||||
].forEach((key) => {
|
||||
if (!process.env[key]) {
|
||||
errors.push(
|
||||
`The ${chalk.bold(
|
||||
key
|
||||
)} env variable must be set when using S3 compatible storage`
|
||||
);
|
||||
export async function checkEnv() {
|
||||
await env.validate().then((errors) => {
|
||||
if (errors.length > 0) {
|
||||
Logger.warn(
|
||||
"Environment configuration is invalid, please check the following:\n\n"
|
||||
);
|
||||
for (const error of errors) {
|
||||
Logger.warn("- " + Object.values(error.constraints ?? {}).join(", "));
|
||||
}
|
||||
});
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
if (!process.env.URL) {
|
||||
errors.push(
|
||||
`The ${chalk.bold(
|
||||
"URL"
|
||||
)} env variable must be set to the fully qualified, externally accessible URL, e.g https://wiki.mycompany.com`
|
||||
);
|
||||
}
|
||||
|
||||
if (!process.env.DATABASE_URL && !process.env.DATABASE_CONNECTION_POOL_URL) {
|
||||
errors.push(
|
||||
`The ${chalk.bold(
|
||||
"DATABASE_URL"
|
||||
)} env variable must be set to the location of your postgres server, including username, password, and port`
|
||||
);
|
||||
}
|
||||
|
||||
if (!process.env.REDIS_URL) {
|
||||
errors.push(
|
||||
`The ${chalk.bold(
|
||||
"REDIS_URL"
|
||||
)} env variable must be set to the location of your redis server, including username, password, and port`
|
||||
);
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
Logger.warn(
|
||||
"\n\nThe server could not start, please fix the following configuration errors and try again:\n" +
|
||||
errors.map((e) => `- ${e}`).join("\n")
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
if (env.ENVIRONMENT === "production") {
|
||||
Logger.info(
|
||||
"lifecycle",
|
||||
chalk.green(`
|
||||
Is your team enjoying Outline? Consider supporting future development by sponsoring the project:\n\nhttps://github.com/sponsors/outline
|
||||
`)
|
||||
);
|
||||
} else if (process.env.NODE_ENV === "development") {
|
||||
} else if (env.ENVIRONMENT === "development") {
|
||||
Logger.warn(
|
||||
`Running Outline in ${chalk.bold(
|
||||
"development mode"
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import crypto from "crypto";
|
||||
import fetch from "fetch-with-proxy";
|
||||
import invariant from "invariant";
|
||||
import env from "@server/env";
|
||||
import Collection from "@server/models/Collection";
|
||||
import Document from "@server/models/Document";
|
||||
import Team from "@server/models/Team";
|
||||
import User from "@server/models/User";
|
||||
import Redis from "@server/redis";
|
||||
import packageInfo from "../../package.json";
|
||||
import Redis from "../redis";
|
||||
|
||||
const UPDATES_URL = "https://updates.getoutline.com";
|
||||
const UPDATES_KEY = "UPDATES_KEY";
|
||||
|
||||
export async function checkUpdates() {
|
||||
invariant(
|
||||
process.env.SECRET_KEY && process.env.URL,
|
||||
"SECRET_KEY or URL env var is not set"
|
||||
);
|
||||
const secret = process.env.SECRET_KEY.slice(0, 6) + process.env.URL;
|
||||
const secret = env.SECRET_KEY.slice(0, 6) + env.URL;
|
||||
const id = crypto.createHash("sha256").update(secret).digest("hex");
|
||||
const [
|
||||
userCount,
|
||||
|
||||
31
server/utils/validators.ts
Normal file
31
server/utils/validators.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationArguments,
|
||||
ValidationOptions,
|
||||
} from "class-validator";
|
||||
|
||||
export function CannotUseWithout(
|
||||
property: string,
|
||||
validationOptions?: ValidationOptions
|
||||
) {
|
||||
return function (object: Object, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: "cannotUseWithout",
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
constraints: [property],
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate<T>(value: T, args: ValidationArguments) {
|
||||
const object = (args.object as unknown) as T;
|
||||
const required = args.constraints[0] as string;
|
||||
return object[required] !== undefined;
|
||||
},
|
||||
defaultMessage(args: ValidationArguments) {
|
||||
return `${propertyName} cannot be used without ${args.constraints[0]}.`;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -4,9 +4,8 @@ import en_US from "./locales/en_US/translation.json";
|
||||
import pt_PT from "./locales/pt_PT/translation.json";
|
||||
import { initI18n } from ".";
|
||||
|
||||
describe("i18n process.env is unset", () => {
|
||||
describe("i18n env is unset", () => {
|
||||
beforeEach(() => {
|
||||
delete process.env.DEFAULT_LANGUAGE;
|
||||
initI18n()
|
||||
.addResources("en-US", "translation", en_US)
|
||||
.addResources("de-DE", "translation", de_DE)
|
||||
@@ -26,10 +25,9 @@ describe("i18n process.env is unset", () => {
|
||||
expect(i18n.t("Saving")).toBe("A guardar");
|
||||
});
|
||||
});
|
||||
describe("i18n process.env is en-US", () => {
|
||||
describe("i18n env is en-US", () => {
|
||||
beforeEach(() => {
|
||||
process.env.DEFAULT_LANGUAGE = "en-US";
|
||||
initI18n()
|
||||
initI18n("en-US")
|
||||
.addResources("en-US", "translation", en_US)
|
||||
.addResources("de-DE", "translation", de_DE)
|
||||
.addResources("pt-PT", "translation", pt_PT);
|
||||
@@ -48,10 +46,10 @@ describe("i18n process.env is en-US", () => {
|
||||
expect(i18n.t("Saving")).toBe("A guardar");
|
||||
});
|
||||
});
|
||||
describe("i18n process.env is de-DE", () => {
|
||||
|
||||
describe("i18n env is de-DE", () => {
|
||||
beforeEach(() => {
|
||||
process.env.DEFAULT_LANGUAGE = "de-DE";
|
||||
initI18n()
|
||||
initI18n("de-DE")
|
||||
.addResources("en-US", "translation", en_US)
|
||||
.addResources("de-DE", "translation", de_DE)
|
||||
.addResources("pt-PT", "translation", pt_PT);
|
||||
@@ -70,10 +68,10 @@ describe("i18n process.env is de-DE", () => {
|
||||
expect(i18n.t("Saving")).toBe("A guardar");
|
||||
});
|
||||
});
|
||||
describe("i18n process.env is pt-PT", () => {
|
||||
|
||||
describe("i18n env is pt-PT", () => {
|
||||
beforeEach(() => {
|
||||
process.env.DEFAULT_LANGUAGE = "pt-PT";
|
||||
initI18n()
|
||||
initI18n("pt-PT")
|
||||
.addResources("en-US", "translation", en_US)
|
||||
.addResources("de-DE", "translation", de_DE)
|
||||
.addResources("pt-PT", "translation", pt_PT);
|
||||
|
||||
@@ -79,10 +79,8 @@ const underscoreToDash = (text: string) => text.replace("_", "-");
|
||||
|
||||
const dashToUnderscore = (text: string) => text.replace("-", "_");
|
||||
|
||||
export const initI18n = () => {
|
||||
const lng = underscoreToDash(
|
||||
"DEFAULT_LANGUAGE" in process.env ? process.env.DEFAULT_LANGUAGE! : "en_US"
|
||||
);
|
||||
export const initI18n = (defaultLanguage = "en_US") => {
|
||||
const lng = underscoreToDash(defaultLanguage);
|
||||
i18n
|
||||
.use(backend)
|
||||
.use(initReactI18next)
|
||||
@@ -104,7 +102,6 @@ export const initI18n = () => {
|
||||
fallbackLng: lng,
|
||||
supportedLngs: languages.map(underscoreToDash),
|
||||
// Uncomment when debugging translation framework, otherwise it's noisy
|
||||
// debug: process.env.NODE_ENV === "development",
|
||||
keySeparator: false,
|
||||
});
|
||||
return i18n;
|
||||
|
||||
@@ -8,15 +8,16 @@ export type PublicEnv = {
|
||||
COLLABORATION_URL: string;
|
||||
AWS_S3_UPLOAD_BUCKET_URL: string;
|
||||
AWS_S3_ACCELERATE_URL: string;
|
||||
DEPLOYMENT: "hosted" | "";
|
||||
ENVIRONMENT: "production" | "development";
|
||||
DEPLOYMENT: string | undefined;
|
||||
ENVIRONMENT: string;
|
||||
SENTRY_DSN: string | undefined;
|
||||
TEAM_LOGO: string | undefined;
|
||||
SLACK_KEY: string | undefined;
|
||||
SLACK_CLIENT_ID: string | undefined;
|
||||
SLACK_APP_ID: string | undefined;
|
||||
MAXIMUM_IMPORT_SIZE: number;
|
||||
SUBDOMAINS_ENABLED: boolean;
|
||||
EMAIL_ENABLED: boolean;
|
||||
DEFAULT_LANGUAGE: string;
|
||||
GOOGLE_ANALYTICS_ID: string | undefined;
|
||||
RELEASE: string | undefined;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import env from "../env";
|
||||
|
||||
export function slackAuth(
|
||||
state: string,
|
||||
scopes: string[] = [
|
||||
@@ -6,9 +8,8 @@ export function slackAuth(
|
||||
"identity.avatar",
|
||||
"identity.team",
|
||||
],
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string | undefined' is not assignable to typ... Remove this comment to see the full error message
|
||||
clientId: string = process.env.SLACK_KEY,
|
||||
redirectUri = `${process.env.URL}/auth/slack.callback`
|
||||
clientId: string,
|
||||
redirectUri = `${env.URL}/auth/slack.callback`
|
||||
): string {
|
||||
const baseUrl = "https://slack.com/oauth/authorize";
|
||||
const params = {
|
||||
@@ -48,7 +49,7 @@ export function changelogUrl(): string {
|
||||
}
|
||||
|
||||
export function signin(service = "slack"): string {
|
||||
return `${process.env.URL}/auth/${service}`;
|
||||
return `${env.URL}/auth/${service}`;
|
||||
}
|
||||
|
||||
export const SLUG_URL_REGEX = /^(?:[0-9a-zA-Z-_~]*-)?([a-zA-Z0-9]{10,15})$/;
|
||||
|
||||
15
yarn.lock
15
yarn.lock
@@ -5189,6 +5189,14 @@ class-utils@^0.3.5:
|
||||
isobject "^3.0.0"
|
||||
static-extend "^0.1.1"
|
||||
|
||||
class-validator@^0.13.2:
|
||||
version "0.13.2"
|
||||
resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.13.2.tgz#64b031e9f3f81a1e1dcd04a5d604734608b24143"
|
||||
integrity sha512-yBUcQy07FPlGzUjoLuUfIOXzgynnQPPruyK1Ge2B74k9ROwnle1E+NxLWnUv5OLU8hA/qL5leAE9XnXq3byaBw==
|
||||
dependencies:
|
||||
libphonenumber-js "^1.9.43"
|
||||
validator "^13.7.0"
|
||||
|
||||
clean-css@^4.0.12, clean-css@^4.2.3:
|
||||
version "4.2.4"
|
||||
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178"
|
||||
@@ -10080,6 +10088,11 @@ lib0@^0.2.35, lib0@^0.2.42, lib0@^0.2.46, lib0@^0.2.47, lib0@^0.2.49:
|
||||
dependencies:
|
||||
isomorphic.js "^0.2.4"
|
||||
|
||||
libphonenumber-js@^1.9.43:
|
||||
version "1.9.52"
|
||||
resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.52.tgz#662ea92dcb6761ceb2dc9a8cd036aadd355bc999"
|
||||
integrity sha512-8k83chc+zMj+J/RkaBxi0PpSTAdzHmpqzCMqquSJVRfbZFr8DCp6vPC7ms2PIPGxeqajZLI6CBLW5nLCJCJrYg==
|
||||
|
||||
lie@~3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
|
||||
@@ -15002,7 +15015,7 @@ validate-npm-package-license@^3.0.1:
|
||||
spdx-correct "^3.0.0"
|
||||
spdx-expression-parse "^3.0.0"
|
||||
|
||||
validator@13.7.0, validator@^13.6.0:
|
||||
validator@13.7.0, validator@^13.6.0, validator@^13.7.0:
|
||||
version "13.7.0"
|
||||
resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857"
|
||||
integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==
|
||||
|
||||
Reference in New Issue
Block a user