chore: Remove DEPLOYMENT and SUBDOMAINS_ENABLED (#5742)

This commit is contained in:
Tom Moor
2023-08-28 20:39:58 -04:00
committed by GitHub
parent 7725f29dc7
commit 30a4303a8e
35 changed files with 136 additions and 135 deletions

View File

@@ -18,7 +18,6 @@ import InputColor from "~/components/InputColor";
import Scene from "~/components/Scene"; import Scene from "~/components/Scene";
import Switch from "~/components/Switch"; import Switch from "~/components/Switch";
import Text from "~/components/Text"; import Text from "~/components/Text";
import env from "~/env";
import useCurrentTeam from "~/hooks/useCurrentTeam"; import useCurrentTeam from "~/hooks/useCurrentTeam";
import usePolicy from "~/hooks/usePolicy"; import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores"; import useStores from "~/hooks/useStores";
@@ -255,7 +254,7 @@ function Details() {
<Heading as="h2">{t("Behavior")}</Heading> <Heading as="h2">{t("Behavior")}</Heading>
<SettingRow <SettingRow
visible={env.SUBDOMAINS_ENABLED && isCloudHosted} visible={isCloudHosted}
label={t("Subdomain")} label={t("Subdomain")}
name="subdomain" name="subdomain"
description={ description={

View File

@@ -13,6 +13,7 @@ import env from "~/env";
import { client } from "~/utils/ApiClient"; import { client } from "~/utils/ApiClient";
import Desktop from "~/utils/Desktop"; import Desktop from "~/utils/Desktop";
import Logger from "~/utils/Logger"; import Logger from "~/utils/Logger";
import isCloudHosted from "~/utils/isCloudHosted";
const AUTH_STORE = "AUTH_STORE"; const AUTH_STORE = "AUTH_STORE";
const NO_REDIRECT_PATHS = ["/", "/create", "/home", "/logout"]; const NO_REDIRECT_PATHS = ["/", "/create", "/home", "/logout"];
@@ -212,7 +213,7 @@ export default class AuthStore {
return; return;
} }
} else if ( } else if (
env.SUBDOMAINS_ENABLED && isCloudHosted &&
parseDomain(hostname).teamSubdomain !== (team.subdomain ?? "") parseDomain(hostname).teamSubdomain !== (team.subdomain ?? "")
) { ) {
window.location.href = `${team.url}${pathname}`; window.location.href = `${team.url}${pathname}`;
@@ -372,7 +373,7 @@ export default class AuthStore {
const sessions = JSON.parse(getCookie("sessions") || "{}"); const sessions = JSON.parse(getCookie("sessions") || "{}");
delete sessions[team.id]; delete sessions[team.id];
setCookie("sessions", JSON.stringify(sessions), { setCookie("sessions", JSON.stringify(sessions), {
domain: getCookieDomain(window.location.hostname), domain: getCookieDomain(window.location.hostname, isCloudHosted),
}); });
} }

View File

@@ -3,6 +3,10 @@ import env from "~/env";
/** /**
* True if the current installation is the cloud hosted version at getoutline.com * True if the current installation is the cloud hosted version at getoutline.com
*/ */
const isCloudHosted = env.DEPLOYMENT === "hosted"; const isCloudHosted = [
"https://app.getoutline.com",
"https://app.outline.dev",
"https://app.outline.dev:3000",
].includes(env.URL);
export default isCloudHosted; export default isCloudHosted;

View File

@@ -1,14 +1,14 @@
import sharedEnv from "@shared/env";
import SigninEmail from "@server/emails/templates/SigninEmail"; import SigninEmail from "@server/emails/templates/SigninEmail";
import WelcomeEmail from "@server/emails/templates/WelcomeEmail"; import WelcomeEmail from "@server/emails/templates/WelcomeEmail";
import env from "@server/env";
import { AuthenticationProvider } from "@server/models"; import { AuthenticationProvider } from "@server/models";
import { buildUser, buildGuestUser, buildTeam } from "@server/test/factories"; import { buildUser, buildGuestUser, buildTeam } from "@server/test/factories";
import { getTestServer } from "@server/test/support"; import { getTestServer, setCloudHosted } from "@server/test/support";
const server = getTestServer(); const server = getTestServer();
describe("email", () => { describe("email", () => {
beforeEach(setCloudHosted);
it("should require email param", async () => { it("should require email param", async () => {
const res = await server.post("/auth/email", { const res = await server.post("/auth/email", {
body: {}, body: {},
@@ -21,11 +21,19 @@ describe("email", () => {
it("should respond with redirect location when user is SSO enabled", async () => { it("should respond with redirect location when user is SSO enabled", async () => {
const spy = jest.spyOn(WelcomeEmail.prototype, "schedule"); const spy = jest.spyOn(WelcomeEmail.prototype, "schedule");
const user = await buildUser(); const team = await buildTeam({
subdomain: "example",
});
const user = await buildUser({
teamId: team.id,
});
const res = await server.post("/auth/email", { const res = await server.post("/auth/email", {
body: { body: {
email: user.email, email: user.email,
}, },
headers: {
host: "example.outline.dev",
},
}); });
const body = await res.json(); const body = await res.json();
expect(res.status).toEqual(200); expect(res.status).toEqual(200);
@@ -71,10 +79,6 @@ describe("email", () => {
}); });
it("should not send email when user is on another subdomain but respond with success", async () => { it("should not send email when user is on another subdomain but respond with success", async () => {
env.URL = sharedEnv.URL = "https://app.outline.dev";
env.SUBDOMAINS_ENABLED = sharedEnv.SUBDOMAINS_ENABLED = true;
env.DEPLOYMENT = "hosted";
const user = await buildUser(); const user = await buildUser();
const spy = jest.spyOn(WelcomeEmail.prototype, "schedule"); const spy = jest.spyOn(WelcomeEmail.prototype, "schedule");
await buildTeam({ await buildTeam({
@@ -138,11 +142,10 @@ describe("email", () => {
expect(spy).not.toHaveBeenCalled(); expect(spy).not.toHaveBeenCalled();
spy.mockRestore(); spy.mockRestore();
}); });
describe("with multiple users matching email", () => { describe("with multiple users matching email", () => {
it("should default to current subdomain with SSO", async () => { it("should default to current subdomain with SSO", async () => {
const spy = jest.spyOn(SigninEmail.prototype, "schedule"); const spy = jest.spyOn(SigninEmail.prototype, "schedule");
env.URL = sharedEnv.URL = "https://app.outline.dev";
env.SUBDOMAINS_ENABLED = sharedEnv.SUBDOMAINS_ENABLED = true;
const email = "sso-user@example.org"; const email = "sso-user@example.org";
const team = await buildTeam({ const team = await buildTeam({
subdomain: "example", subdomain: "example",
@@ -171,8 +174,6 @@ describe("email", () => {
it("should default to current subdomain with guest email", async () => { it("should default to current subdomain with guest email", async () => {
const spy = jest.spyOn(SigninEmail.prototype, "schedule"); const spy = jest.spyOn(SigninEmail.prototype, "schedule");
env.URL = sharedEnv.URL = "https://app.outline.dev";
env.SUBDOMAINS_ENABLED = sharedEnv.SUBDOMAINS_ENABLED = true;
const email = "guest-user@example.org"; const email = "guest-user@example.org";
const team = await buildTeam({ const team = await buildTeam({
subdomain: "example", subdomain: "example",

View File

@@ -25,13 +25,13 @@ router.post(
const domain = parseDomain(ctx.request.hostname); const domain = parseDomain(ctx.request.hostname);
let team: Team | null | undefined; let team: Team | null | undefined;
if (!env.isCloudHosted()) { if (!env.isCloudHosted) {
team = await Team.scope("withAuthenticationProviders").findOne(); team = await Team.scope("withAuthenticationProviders").findOne();
} else if (domain.custom) { } else if (domain.custom) {
team = await Team.scope("withAuthenticationProviders").findOne({ team = await Team.scope("withAuthenticationProviders").findOne({
where: { domain: domain.host }, where: { domain: domain.host },
}); });
} else if (env.SUBDOMAINS_ENABLED && domain.teamSubdomain) { } else if (domain.teamSubdomain) {
team = await Team.scope("withAuthenticationProviders").findOne({ team = await Team.scope("withAuthenticationProviders").findOne({
where: { subdomain: domain.teamSubdomain }, where: { subdomain: domain.teamSubdomain },
}); });

View File

@@ -599,7 +599,7 @@ export default class DeliverWebhookTask extends BaseTask<Props> {
}); });
status = response.ok ? "success" : "failed"; status = response.ok ? "success" : "failed";
} catch (err) { } catch (err) {
if (err instanceof FetchError && env.DEPLOYMENT === "hosted") { if (err instanceof FetchError && env.isCloudHosted) {
Logger.warn(`Failed to send webhook: ${err.message}`, { Logger.warn(`Failed to send webhook: ${err.message}`, {
event, event,
deliveryId: delivery.id, deliveryId: delivery.id,

View File

@@ -1,10 +1,14 @@
import WelcomeEmail from "@server/emails/templates/WelcomeEmail"; import WelcomeEmail from "@server/emails/templates/WelcomeEmail";
import env from "@server/env";
import { TeamDomain } from "@server/models"; import { TeamDomain } from "@server/models";
import Collection from "@server/models/Collection"; import Collection from "@server/models/Collection";
import UserAuthentication from "@server/models/UserAuthentication"; import UserAuthentication from "@server/models/UserAuthentication";
import { buildUser, buildTeam } from "@server/test/factories"; import { buildUser, buildTeam } from "@server/test/factories";
import { setupTestDatabase, seed } from "@server/test/support"; import {
setupTestDatabase,
seed,
setCloudHosted,
setSelfHosted,
} from "@server/test/support";
import accountProvisioner from "./accountProvisioner"; import accountProvisioner from "./accountProvisioner";
setupTestDatabase(); setupTestDatabase();
@@ -13,9 +17,7 @@ describe("accountProvisioner", () => {
const ip = "127.0.0.1"; const ip = "127.0.0.1";
describe("hosted", () => { describe("hosted", () => {
beforeEach(() => { beforeEach(setCloudHosted);
env.DEPLOYMENT = "hosted";
});
it("should create a new user and team", async () => { it("should create a new user and team", async () => {
const spy = jest.spyOn(WelcomeEmail.prototype, "schedule"); const spy = jest.spyOn(WelcomeEmail.prototype, "schedule");
@@ -325,9 +327,7 @@ describe("accountProvisioner", () => {
}); });
describe("self hosted", () => { describe("self hosted", () => {
beforeEach(() => { beforeEach(setSelfHosted);
env.DEPLOYMENT = undefined;
});
it("should fail if existing team and domain not in allowed list", async () => { it("should fail if existing team and domain not in allowed list", async () => {
let error; let error;

View File

@@ -1,7 +1,10 @@
import env from "@server/env";
import TeamDomain from "@server/models/TeamDomain"; import TeamDomain from "@server/models/TeamDomain";
import { buildTeam, buildUser } from "@server/test/factories"; import { buildTeam, buildUser } from "@server/test/factories";
import { setupTestDatabase } from "@server/test/support"; import {
setCloudHosted,
setSelfHosted,
setupTestDatabase,
} from "@server/test/support";
import teamProvisioner from "./teamProvisioner"; import teamProvisioner from "./teamProvisioner";
setupTestDatabase(); setupTestDatabase();
@@ -10,9 +13,7 @@ describe("teamProvisioner", () => {
const ip = "127.0.0.1"; const ip = "127.0.0.1";
describe("hosted", () => { describe("hosted", () => {
beforeEach(() => { beforeEach(setCloudHosted);
env.DEPLOYMENT = "hosted";
});
it("should create team and authentication provider", async () => { it("should create team and authentication provider", async () => {
const result = await teamProvisioner({ const result = await teamProvisioner({
@@ -127,9 +128,7 @@ describe("teamProvisioner", () => {
}); });
describe("self hosted", () => { describe("self hosted", () => {
beforeEach(() => { beforeEach(setSelfHosted);
env.DEPLOYMENT = undefined;
});
it("should allow creating first team", async () => { it("should allow creating first team", async () => {
const { team, isNewTeam } = await teamProvisioner({ const { team, isNewTeam } = await teamProvisioner({

View File

@@ -78,7 +78,7 @@ async function teamProvisioner({
}; };
} else if (teamId) { } else if (teamId) {
// The user is attempting to log into a team with an unfamiliar SSO provider // The user is attempting to log into a team with an unfamiliar SSO provider
if (env.isCloudHosted()) { if (env.isCloudHosted) {
throw InvalidAuthenticationError(); throw InvalidAuthenticationError();
} }

View File

@@ -34,7 +34,7 @@ const teamUpdater = async ({
preferences, preferences,
} = params; } = params;
if (subdomain !== undefined && env.SUBDOMAINS_ENABLED) { if (subdomain !== undefined && env.isCloudHosted) {
team.subdomain = subdomain === "" ? null : subdomain; team.subdomain = subdomain === "" ? null : subdomain;
} }

View File

@@ -164,7 +164,7 @@ export class Mailer {
}, },
} }
: undefined, : undefined,
attachments: env.isCloudHosted() attachments: env.isCloudHosted
? undefined ? undefined
: [ : [
{ {

View File

@@ -14,7 +14,7 @@ export default () => (
<img <img
alt={env.APP_NAME} alt={env.APP_NAME}
src={ src={
env.isCloudHosted() env.isCloudHosted
? `${url}/email/header-logo.png` ? `${url}/email/header-logo.png`
: "cid:header-image" : "cid:header-image"
} }

View File

@@ -11,7 +11,6 @@ import {
IsUrl, IsUrl,
IsOptional, IsOptional,
IsByteLength, IsByteLength,
Equals,
IsNumber, IsNumber,
IsIn, IsIn,
IsEmail, IsEmail,
@@ -206,13 +205,6 @@ export class Environment {
@CannotUseWithout("SSL_KEY") @CannotUseWithout("SSL_KEY")
public SSL_CERT = this.toOptionalString(process.env.SSL_CERT); public SSL_CERT = this.toOptionalString(process.env.SSL_CERT);
/**
* Should always be left unset in a self-hosted environment.
*/
@Equals("hosted")
@IsOptional()
public DEPLOYMENT = this.toOptionalString(process.env.DEPLOYMENT);
/** /**
* The default interface language. See translate.getoutline.com for a list of * The default interface language. See translate.getoutline.com for a list of
* available language codes and their percentage translated. * available language codes and their percentage translated.
@@ -240,15 +232,6 @@ export class Environment {
@IsBoolean() @IsBoolean()
public FORCE_HTTPS = this.toBoolean(process.env.FORCE_HTTPS ?? "true"); public FORCE_HTTPS = this.toBoolean(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 = this.toBoolean(
process.env.SUBDOMAINS_ENABLED ?? "false"
);
/** /**
* Should the installation send anonymized statistics to the maintainers. * Should the installation send anonymized statistics to the maintainers.
* Defaults to true. * Defaults to true.
@@ -666,8 +649,12 @@ export class Environment {
* Returns true if the current installation is the cloud hosted version at * Returns true if the current installation is the cloud hosted version at
* getoutline.com * getoutline.com
*/ */
public isCloudHosted() { public get isCloudHosted() {
return this.DEPLOYMENT === "hosted"; return [
"https://app.getoutline.com",
"https://app.outline.dev",
"https://app.outline.dev:3000",
].includes(this.URL);
} }
private toOptionalString(value: string | undefined) { private toOptionalString(value: string | undefined) {

View File

@@ -70,9 +70,9 @@ class Team extends ParanoidModel {
@Unique @Unique
@Length({ @Length({
min: 2, min: 2,
max: env.isCloudHosted() ? 32 : 255, max: env.isCloudHosted ? 32 : 255,
msg: `subdomain must be between 2 and ${ msg: `subdomain must be between 2 and ${
env.isCloudHosted() ? 32 : 255 env.isCloudHosted ? 32 : 255
} characters`, } characters`,
}) })
@Is({ @Is({
@@ -169,7 +169,7 @@ class Team extends ParanoidModel {
return `${url.protocol}//${this.domain}${url.port ? `:${url.port}` : ""}`; return `${url.protocol}//${this.domain}${url.port ? `:${url.port}` : ""}`;
} }
if (!this.subdomain || !env.SUBDOMAINS_ENABLED) { if (!this.subdomain || !env.isCloudHosted) {
return env.URL; return env.URL;
} }

View File

@@ -1,11 +1,12 @@
import env from "@server/env";
import { buildAdmin, buildTeam } from "@server/test/factories"; import { buildAdmin, buildTeam } from "@server/test/factories";
import { setupTestDatabase } from "@server/test/support"; import { setCloudHosted, setupTestDatabase } from "@server/test/support";
import TeamDomain from "./TeamDomain"; import TeamDomain from "./TeamDomain";
setupTestDatabase(); setupTestDatabase();
describe("team domain model", () => { describe("team domain model", () => {
beforeEach(setCloudHosted);
describe("create", () => { describe("create", () => {
it("should allow creation of domains", async () => { it("should allow creation of domains", async () => {
const team = await buildTeam(); const team = await buildTeam();
@@ -37,7 +38,6 @@ describe("team domain model", () => {
}); });
it("should not allow creation of domains within restricted list", async () => { it("should not allow creation of domains within restricted list", async () => {
env.DEPLOYMENT = "hosted";
const TeamDomain = await import("./TeamDomain"); const TeamDomain = await import("./TeamDomain");
const team = await buildTeam(); const team = await buildTeam();
const user = await buildAdmin({ teamId: team.id }); const user = await buildAdmin({ teamId: team.id });
@@ -57,7 +57,6 @@ describe("team domain model", () => {
}); });
it("should ignore casing and spaces when creating domains", async () => { it("should ignore casing and spaces when creating domains", async () => {
env.DEPLOYMENT = "hosted";
const TeamDomain = await import("./TeamDomain"); const TeamDomain = await import("./TeamDomain");
const team = await buildTeam(); const team = await buildTeam();
const user = await buildAdmin({ teamId: team.id }); const user = await buildAdmin({ teamId: team.id });

View File

@@ -23,7 +23,7 @@ import Length from "./validators/Length";
@Fix @Fix
class TeamDomain extends IdModel { class TeamDomain extends IdModel {
@NotIn({ @NotIn({
args: env.isCloudHosted() ? [emailProviders] : [], args: env.isCloudHosted ? [emailProviders] : [],
msg: "You chose a restricted domain, please try another.", msg: "You chose a restricted domain, please try another.",
}) })
@NotEmpty @NotEmpty

View File

@@ -74,7 +74,7 @@ export default class AuthenticationHelper {
* @returns A list of authentication providers * @returns A list of authentication providers
*/ */
public static providersForTeam(team?: Team) { public static providersForTeam(team?: Team) {
const isCloudHosted = env.isCloudHosted(); const isCloudHosted = env.isCloudHosted;
return AuthenticationHelper.providers return AuthenticationHelper.providers
.sort((config) => (config.id === "email" ? 1 : -1)) .sort((config) => (config.id === "email" ? 1 : -1))

View File

@@ -1,11 +1,16 @@
import env from "@server/env";
import { buildUser, buildTeam, buildAdmin } from "@server/test/factories"; import { buildUser, buildTeam, buildAdmin } from "@server/test/factories";
import { setupTestDatabase } from "@server/test/support"; import {
setCloudHosted,
setSelfHosted,
setupTestDatabase,
} from "@server/test/support";
import { serialize } from "./index"; import { serialize } from "./index";
setupTestDatabase(); setupTestDatabase();
it("should allow reading only", async () => { it("should allow reading only", async () => {
setSelfHosted();
const team = await buildTeam(); const team = await buildTeam();
const user = await buildUser({ const user = await buildUser({
teamId: team.id, teamId: team.id,
@@ -21,6 +26,8 @@ it("should allow reading only", async () => {
}); });
it("should allow admins to manage", async () => { it("should allow admins to manage", async () => {
setSelfHosted();
const team = await buildTeam(); const team = await buildTeam();
const admin = await buildAdmin({ const admin = await buildAdmin({
teamId: team.id, teamId: team.id,
@@ -36,7 +43,7 @@ it("should allow admins to manage", async () => {
}); });
it("should allow creation on hosted envs", async () => { it("should allow creation on hosted envs", async () => {
env.DEPLOYMENT = "hosted"; setCloudHosted();
const team = await buildTeam(); const team = await buildTeam();
const admin = await buildAdmin({ const admin = await buildAdmin({

View File

@@ -13,7 +13,7 @@ allow(User, "share", Team, (user, team) => {
}); });
allow(User, "createTeam", Team, () => { allow(User, "createTeam", Team, () => {
if (!env.isCloudHosted()) { if (!env.isCloudHosted) {
throw IncorrectEditionError("Functionality is only available on cloud"); throw IncorrectEditionError("Functionality is only available on cloud");
} }
return true; return true;
@@ -27,7 +27,7 @@ allow(User, "update", Team, (user, team) => {
}); });
allow(User, ["delete", "audit"], Team, (user, team) => { allow(User, ["delete", "audit"], Team, (user, team) => {
if (!env.isCloudHosted()) { if (!env.isCloudHosted) {
throw IncorrectEditionError("Functionality is only available on cloud"); throw IncorrectEditionError("Functionality is only available on cloud");
} }
if (!team || user.isViewer || user.teamId !== team.id) { if (!team || user.isViewer || user.teamId !== team.id) {

View File

@@ -16,14 +16,12 @@ export default function present(
COLLABORATION_URL: (env.COLLABORATION_URL || env.URL) COLLABORATION_URL: (env.COLLABORATION_URL || env.URL)
.replace(/\/$/, "") .replace(/\/$/, "")
.replace(/^http/, "ws"), .replace(/^http/, "ws"),
DEPLOYMENT: env.DEPLOYMENT,
ENVIRONMENT: env.ENVIRONMENT, ENVIRONMENT: env.ENVIRONMENT,
SENTRY_DSN: env.SENTRY_DSN, SENTRY_DSN: env.SENTRY_DSN,
SENTRY_TUNNEL: env.SENTRY_TUNNEL, SENTRY_TUNNEL: env.SENTRY_TUNNEL,
SLACK_CLIENT_ID: env.SLACK_CLIENT_ID, SLACK_CLIENT_ID: env.SLACK_CLIENT_ID,
SLACK_APP_ID: env.SLACK_APP_ID, SLACK_APP_ID: env.SLACK_APP_ID,
MAXIMUM_IMPORT_SIZE: env.MAXIMUM_IMPORT_SIZE, MAXIMUM_IMPORT_SIZE: env.MAXIMUM_IMPORT_SIZE,
SUBDOMAINS_ENABLED: env.SUBDOMAINS_ENABLED,
PDF_EXPORT_ENABLED: false, PDF_EXPORT_ENABLED: false,
DEFAULT_LANGUAGE: env.DEFAULT_LANGUAGE, DEFAULT_LANGUAGE: env.DEFAULT_LANGUAGE,
EMAIL_ENABLED: !!env.SMTP_HOST || env.ENVIRONMENT === "development", EMAIL_ENABLED: !!env.SMTP_HOST || env.ENVIRONMENT === "development",

View File

@@ -1,7 +1,9 @@
import sharedEnv from "@shared/env";
import env from "@server/env";
import { buildUser, buildTeam } from "@server/test/factories"; import { buildUser, buildTeam } from "@server/test/factories";
import { getTestServer } from "@server/test/support"; import {
getTestServer,
setCloudHosted,
setSelfHosted,
} from "@server/test/support";
const mockTeamInSessionId = "1e023d05-951c-41c6-9012-c9fa0402e1c3"; const mockTeamInSessionId = "1e023d05-951c-41c6-9012-c9fa0402e1c3";
@@ -14,6 +16,8 @@ jest.mock("@server/utils/authentication", () => ({
const server = getTestServer(); const server = getTestServer();
describe("#auth.info", () => { describe("#auth.info", () => {
beforeEach(setCloudHosted);
it("should return current authentication", async () => { it("should return current authentication", async () => {
const team = await buildTeam(); const team = await buildTeam();
const team2 = await buildTeam(); const team2 = await buildTeam();
@@ -93,8 +97,6 @@ describe("#auth.delete", () => {
describe("#auth.config", () => { describe("#auth.config", () => {
it("should return available SSO providers", async () => { it("should return available SSO providers", async () => {
env.DEPLOYMENT = "hosted";
const res = await server.post("/api/auth.config"); const res = await server.post("/api/auth.config");
const body = await res.json(); const body = await res.json();
expect(res.status).toEqual(200); expect(res.status).toEqual(200);
@@ -105,10 +107,6 @@ describe("#auth.config", () => {
}); });
it("should return available providers for team subdomain", async () => { it("should return available providers for team subdomain", async () => {
env.URL = sharedEnv.URL = "https://app.outline.dev";
env.SUBDOMAINS_ENABLED = sharedEnv.SUBDOMAINS_ENABLED = true;
env.DEPLOYMENT = "hosted";
await buildTeam({ await buildTeam({
guestSignin: false, guestSignin: false,
subdomain: "example", subdomain: "example",
@@ -131,8 +129,6 @@ describe("#auth.config", () => {
}); });
it("should return available providers for team custom domain", async () => { it("should return available providers for team custom domain", async () => {
env.DEPLOYMENT = "hosted";
await buildTeam({ await buildTeam({
guestSignin: false, guestSignin: false,
domain: "docs.mycompany.com", domain: "docs.mycompany.com",
@@ -155,9 +151,6 @@ describe("#auth.config", () => {
}); });
it("should return email provider for team when guest signin enabled", async () => { it("should return email provider for team when guest signin enabled", async () => {
env.URL = sharedEnv.URL = "https://app.outline.dev";
env.DEPLOYMENT = "hosted";
await buildTeam({ await buildTeam({
guestSignin: true, guestSignin: true,
subdomain: "example", subdomain: "example",
@@ -181,9 +174,6 @@ describe("#auth.config", () => {
}); });
it("should not return provider when disabled", async () => { it("should not return provider when disabled", async () => {
env.URL = sharedEnv.URL = "https://app.outline.dev";
env.DEPLOYMENT = "hosted";
await buildTeam({ await buildTeam({
guestSignin: false, guestSignin: false,
subdomain: "example", subdomain: "example",
@@ -206,8 +196,9 @@ describe("#auth.config", () => {
}); });
describe("self hosted", () => { describe("self hosted", () => {
beforeEach(setSelfHosted);
it("should return all configured providers but respect email setting", async () => { it("should return all configured providers but respect email setting", async () => {
env.DEPLOYMENT = "";
await buildTeam({ await buildTeam({
guestSignin: false, guestSignin: false,
authenticationProviders: [ authenticationProviders: [
@@ -227,7 +218,6 @@ describe("#auth.config", () => {
}); });
it("should return email provider for team when guest signin enabled", async () => { it("should return email provider for team when guest signin enabled", async () => {
env.DEPLOYMENT = "";
await buildTeam({ await buildTeam({
guestSignin: true, guestSignin: true,
authenticationProviders: [ authenticationProviders: [

View File

@@ -26,7 +26,7 @@ router.post("auth.config", async (ctx: APIContext<T.AuthConfigReq>) => {
// If self hosted AND there is only one team then that team becomes the // 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 // brand for the knowledge base and it's guest signin option is used for the
// root login page. // root login page.
if (!env.isCloudHosted()) { if (!env.isCloudHosted) {
const team = await Team.scope("withAuthenticationProviders").findOne(); const team = await Team.scope("withAuthenticationProviders").findOne();
if (team) { if (team) {
@@ -75,7 +75,7 @@ router.post("auth.config", async (ctx: APIContext<T.AuthConfigReq>) => {
// If subdomain signin page then we return minimal team details to allow // If subdomain signin page then we return minimal team details to allow
// for a custom screen showing only relevant signin options for that team. // for a custom screen showing only relevant signin options for that team.
else if (env.SUBDOMAINS_ENABLED && domain.teamSubdomain) { else if (env.isCloudHosted && domain.teamSubdomain) {
const team = await Team.scope("withAuthenticationProviders").findOne({ const team = await Team.scope("withAuthenticationProviders").findOne({
where: { where: {
subdomain: domain.teamSubdomain, subdomain: domain.teamSubdomain,
@@ -179,7 +179,7 @@ router.post(
ctx.cookies.set("accessToken", "", { ctx.cookies.set("accessToken", "", {
expires: subMinutes(new Date(), 1), expires: subMinutes(new Date(), 1),
domain: getCookieDomain(ctx.hostname), domain: getCookieDomain(ctx.hostname, env.isCloudHosted),
}); });
ctx.body = { ctx.body = {

View File

@@ -1,13 +1,10 @@
import env from "@server/env";
import { buildEvent, buildUser } from "@server/test/factories"; import { buildEvent, buildUser } from "@server/test/factories";
import { seed, getTestServer } from "@server/test/support"; import { seed, getTestServer, setCloudHosted } from "@server/test/support";
const server = getTestServer(); const server = getTestServer();
describe("#events.list", () => { describe("#events.list", () => {
beforeEach(() => { beforeEach(setCloudHosted);
env.DEPLOYMENT = "hosted";
});
it("should only return activity events", async () => { it("should only return activity events", async () => {
const { user, admin, document, collection } = await seed(); const { user, admin, document, collection } = await seed();

View File

@@ -1,13 +1,18 @@
import env from "@server/env";
import { TeamDomain } from "@server/models"; import { TeamDomain } from "@server/models";
import { buildAdmin, buildCollection, buildTeam } from "@server/test/factories"; import { buildAdmin, buildCollection, buildTeam } from "@server/test/factories";
import { seed, getTestServer } from "@server/test/support"; import {
seed,
getTestServer,
setCloudHosted,
setSelfHosted,
} from "@server/test/support";
const server = getTestServer(); const server = getTestServer();
describe("teams.create", () => { describe("teams.create", () => {
it("creates a team", async () => { it("creates a team", async () => {
env.DEPLOYMENT = "hosted"; setCloudHosted();
const team = await buildTeam(); const team = await buildTeam();
const user = await buildAdmin({ teamId: team.id }); const user = await buildAdmin({ teamId: team.id });
const res = await server.post("/api/teams.create", { const res = await server.post("/api/teams.create", {
@@ -23,7 +28,8 @@ describe("teams.create", () => {
}); });
it("requires a cloud hosted deployment", async () => { it("requires a cloud hosted deployment", async () => {
env.DEPLOYMENT = ""; setSelfHosted();
const team = await buildTeam(); const team = await buildTeam();
const user = await buildAdmin({ teamId: team.id }); const user = await buildAdmin({ teamId: team.id });
const res = await server.post("/api/teams.create", { const res = await server.post("/api/teams.create", {

View File

@@ -17,7 +17,6 @@ env.OIDC_TOKEN_URI = "http://localhost/token";
env.OIDC_USERINFO_URI = "http://localhost/userinfo"; env.OIDC_USERINFO_URI = "http://localhost/userinfo";
env.RATE_LIMITER_ENABLED = false; env.RATE_LIMITER_ENABLED = false;
env.DEPLOYMENT = undefined;
if (process.env.DATABASE_URL_TEST) { if (process.env.DATABASE_URL_TEST) {
env.DATABASE_URL = process.env.DATABASE_URL_TEST; env.DATABASE_URL = process.env.DATABASE_URL_TEST;

View File

@@ -1,6 +1,8 @@
import TestServer from "fetch-test-server"; import TestServer from "fetch-test-server";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import sharedEnv from "@shared/env";
import { CollectionPermission } from "@shared/types"; import { CollectionPermission } from "@shared/types";
import env from "@server/env";
import { User, Document, Collection, Team } from "@server/models"; import { User, Document, Collection, Team } from "@server/models";
import onerror from "@server/onerror"; import onerror from "@server/onerror";
import webService from "@server/services/web"; import webService from "@server/services/web";
@@ -137,3 +139,17 @@ export function setupTestDatabase() {
afterAll(disconnect); afterAll(disconnect);
beforeEach(flush); beforeEach(flush);
} }
/**
* Set the environment to be cloud hosted
*/
export function setCloudHosted() {
return (env.URL = sharedEnv.URL = "https://app.outline.dev");
}
/**
* Set the environment to be self hosted
*/
export function setSelfHosted() {
return (env.URL = sharedEnv.URL = "https://wiki.example.com");
}

View File

@@ -71,7 +71,7 @@ export async function signIn(
}, },
ip: ctx.request.ip, ip: ctx.request.ip,
}); });
const domain = getCookieDomain(ctx.request.hostname); const domain = getCookieDomain(ctx.request.hostname, env.isCloudHosted);
const expires = addMonths(new Date(), 3); const expires = addMonths(new Date(), 3);
// set a cookie for which service we last signed in with. This is // set a cookie for which service we last signed in with. This is
@@ -85,7 +85,7 @@ export async function signIn(
// set a transfer cookie for the access token itself and redirect // set a transfer cookie for the access token itself and redirect
// to the teams subdomain if subdomains are enabled // to the teams subdomain if subdomains are enabled
if (env.SUBDOMAINS_ENABLED && team.subdomain) { if (env.isCloudHosted && team.subdomain) {
// get any existing sessions (teams signed in) and add this team // get any existing sessions (teams signed in) and add this team
const existing = getSessionsInCookie(ctx); const existing = getSessionsInCookie(ctx);
const sessions = encodeURIComponent( const sessions = encodeURIComponent(

View File

@@ -22,7 +22,7 @@ export default class AzureClient extends OAuthClient {
refreshToken?: string; refreshToken?: string;
expiresAt: Date; expiresAt: Date;
}> { }> {
if (env.isCloudHosted()) { if (env.isCloudHosted) {
return super.rotateToken(accessToken, refreshToken); return super.rotateToken(accessToken, refreshToken);
} }

View File

@@ -19,10 +19,10 @@ export default function fetch(
): Promise<Response> { ): Promise<Response> {
// In self-hosted, webhooks support proxying and are also allowed to connect // In self-hosted, webhooks support proxying and are also allowed to connect
// to internal services, so use fetchWithProxy without the filtering agent. // to internal services, so use fetchWithProxy without the filtering agent.
const fetch = env.isCloudHosted() ? nodeFetch : fetchWithProxy; const fetch = env.isCloudHosted ? nodeFetch : fetchWithProxy;
return fetch(url, { return fetch(url, {
...init, ...init,
agent: env.isCloudHosted() ? useAgent(url) : undefined, agent: env.isCloudHosted ? useAgent(url) : undefined,
}); });
} }

View File

@@ -28,7 +28,7 @@ export class StateStore {
ctx.cookies.set(this.key, state, { ctx.cookies.set(this.key, state, {
expires: addMinutes(new Date(), 10), expires: addMinutes(new Date(), 10),
domain: getCookieDomain(ctx.hostname), domain: getCookieDomain(ctx.hostname, env.isCloudHosted),
}); });
callback(null, token); callback(null, token);
@@ -54,7 +54,7 @@ export class StateStore {
// Destroy the one-time pad token and ensure it matches // Destroy the one-time pad token and ensure it matches
ctx.cookies.set(this.key, "", { ctx.cookies.set(this.key, "", {
expires: subMinutes(new Date(), 1), expires: subMinutes(new Date(), 1),
domain: getCookieDomain(ctx.hostname), domain: getCookieDomain(ctx.hostname, env.isCloudHosted),
}); });
if (!token || token !== providedToken) { if (!token || token !== providedToken) {
@@ -100,11 +100,11 @@ export async function getTeamFromContext(ctx: Context) {
const domain = parseDomain(host); const domain = parseDomain(host);
let team; let team;
if (!env.isCloudHosted()) { if (!env.isCloudHosted) {
team = await Team.findOne(); team = await Team.findOne();
} else if (domain.custom) { } else if (domain.custom) {
team = await Team.findOne({ where: { domain: domain.host } }); team = await Team.findOne({ where: { domain: domain.host } });
} else if (env.SUBDOMAINS_ENABLED && domain.teamSubdomain) { } else if (domain.teamSubdomain) {
team = await Team.findOne({ team = await Team.findOne({
where: { subdomain: domain.teamSubdomain }, where: { subdomain: domain.teamSubdomain },
}); });

View File

@@ -1,7 +1,7 @@
import env from "@server/env"; import env from "@server/env";
export const robotsResponse = () => { export const robotsResponse = () => {
if (env.isCloudHosted()) { if (env.isCloudHosted) {
return ` return `
User-agent: * User-agent: *
Allow: / Allow: /

View File

@@ -10,7 +10,7 @@ export async function checkPendingMigrations() {
try { try {
const pending = await migrations.pending(); const pending = await migrations.pending();
if (!isEmpty(pending)) { if (!isEmpty(pending)) {
if (env.isCloudHosted()) { if (env.isCloudHosted) {
Logger.warn(chalk.red("Migrations are pending")); Logger.warn(chalk.red("Migrations are pending"));
process.exit(1); process.exit(1);
} else { } else {
@@ -34,7 +34,7 @@ export async function checkPendingMigrations() {
} }
export async function checkDataMigrations() { export async function checkDataMigrations() {
if (env.isCloudHosted()) { if (env.isCloudHosted) {
return; return;
} }

View File

@@ -44,14 +44,12 @@ export type PublicEnv = {
COLLABORATION_URL: string; COLLABORATION_URL: string;
AWS_S3_UPLOAD_BUCKET_URL: string; AWS_S3_UPLOAD_BUCKET_URL: string;
AWS_S3_ACCELERATE_URL: string; AWS_S3_ACCELERATE_URL: string;
DEPLOYMENT: string | undefined;
ENVIRONMENT: string; ENVIRONMENT: string;
SENTRY_DSN: string | undefined; SENTRY_DSN: string | undefined;
SENTRY_TUNNEL: string | undefined; SENTRY_TUNNEL: string | undefined;
SLACK_CLIENT_ID: string | undefined; SLACK_CLIENT_ID: string | undefined;
SLACK_APP_ID: string | undefined; SLACK_APP_ID: string | undefined;
MAXIMUM_IMPORT_SIZE: number; MAXIMUM_IMPORT_SIZE: number;
SUBDOMAINS_ENABLED: boolean;
EMAIL_ENABLED: boolean; EMAIL_ENABLED: boolean;
PDF_EXPORT_ENABLED: boolean; PDF_EXPORT_ENABLED: boolean;
DEFAULT_LANGUAGE: string; DEFAULT_LANGUAGE: string;

View File

@@ -169,27 +169,27 @@ describe("#slugifyDomain", () => {
describe("#getCookieDomain", () => { describe("#getCookieDomain", () => {
beforeEach(() => { beforeEach(() => {
env.URL = "https://example.com"; env.URL = "https://example.com";
env.SUBDOMAINS_ENABLED = true;
}); });
it("returns the normalized app host when on the host domain", () => { it("returns the normalized app host when on the host domain", () => {
expect(getCookieDomain("subdomain.example.com")).toBe("example.com"); expect(getCookieDomain("subdomain.example.com", true)).toBe("example.com");
expect(getCookieDomain("www.example.com")).toBe("example.com"); expect(getCookieDomain("www.example.com", true)).toBe("example.com");
expect(getCookieDomain("http://example.com:3000")).toBe("example.com"); expect(getCookieDomain("http://example.com:3000", true)).toBe(
expect(getCookieDomain("myteam.example.com/document/12345?q=query")).toBe(
"example.com" "example.com"
); );
expect(
getCookieDomain("myteam.example.com/document/12345?q=query", true)
).toBe("example.com");
}); });
it("returns the input if not on the host domain", () => { it("returns the input if not on the host domain", () => {
expect(getCookieDomain("www.blogspot.com")).toBe("www.blogspot.com"); expect(getCookieDomain("www.blogspot.com", true)).toBe("www.blogspot.com");
expect(getCookieDomain("anything else")).toBe("anything else"); expect(getCookieDomain("anything else", true)).toBe("anything else");
}); });
it("always returns the input when subdomains are not enabled", () => { it("always returns the input when not cloud hosted", () => {
env.SUBDOMAINS_ENABLED = false; expect(getCookieDomain("example.com", false)).toBe("example.com");
expect(getCookieDomain("example.com")).toBe("example.com"); expect(getCookieDomain("www.blogspot.com", false)).toBe("www.blogspot.com");
expect(getCookieDomain("www.blogspot.com")).toBe("www.blogspot.com"); expect(getCookieDomain("anything else", false)).toBe("anything else");
expect(getCookieDomain("anything else")).toBe("anything else");
}); });
}); });

View File

@@ -65,10 +65,10 @@ export function parseDomain(url: string): Domain {
}; };
} }
export function getCookieDomain(domain: string) { export function getCookieDomain(domain: string, isCloudHosted: boolean) {
// always use the base URL for cookies when in hosted mode // always use the base URL for cookies when in hosted mode
// and the domain is not custom // and the domain is not custom
if (env.SUBDOMAINS_ENABLED) { if (isCloudHosted) {
const parsed = parseDomain(domain); const parsed = parseDomain(domain);
if (!parsed.custom) { if (!parsed.custom) {