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:
@@ -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]}.`;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user