mirror of
https://github.com/outline/outline.git
synced 2026-02-20 19:39:24 -06:00
feat: Redirect to previous subdomains (#8477)
* Migration * Store previous subdomains * Redirect previous subdomains at service layer * refactor * refactor * change index * Guard logic to hosted only
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
"use strict";
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.transaction(async transaction => {
|
||||
await queryInterface.addColumn("teams", "previousSubdomains", {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
allowNull: true,
|
||||
}, { transaction });
|
||||
await queryInterface.sequelize.query(
|
||||
`CREATE INDEX teams_previous_subdomains ON teams USING GIN ("previousSubdomains");`,
|
||||
{ transaction }
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.sequelize.transaction(async transaction => {
|
||||
await queryInterface.sequelize.query(
|
||||
`DROP INDEX teams_previous_subdomains;`,
|
||||
{ transaction }
|
||||
);
|
||||
await queryInterface.removeColumn("teams", "previousSubdomains", { transaction });
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,20 +1,43 @@
|
||||
import { buildTeam, buildCollection } from "@server/test/factories";
|
||||
|
||||
describe("collectionIds", () => {
|
||||
it("should return non-private collection ids", async () => {
|
||||
const team = await buildTeam();
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
describe("Team", () => {
|
||||
describe("collectionIds", () => {
|
||||
it("should return non-private collection ids", async () => {
|
||||
const team = await buildTeam();
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
});
|
||||
// build a collection in another team
|
||||
await buildCollection();
|
||||
// build a private collection
|
||||
await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: null,
|
||||
});
|
||||
const response = await team.collectionIds();
|
||||
expect(response.length).toEqual(1);
|
||||
expect(response[0]).toEqual(collection.id);
|
||||
});
|
||||
// build a collection in another team
|
||||
await buildCollection();
|
||||
// build a private collection
|
||||
await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: null,
|
||||
});
|
||||
|
||||
describe("previousSubdomains", () => {
|
||||
it("should list the previous subdomains", async () => {
|
||||
const team = await buildTeam({
|
||||
subdomain: "example",
|
||||
});
|
||||
const subdomain = "updated";
|
||||
|
||||
await team.update({ subdomain });
|
||||
expect(team.subdomain).toEqual(subdomain);
|
||||
expect(team.previousSubdomains?.length).toEqual(1);
|
||||
expect(team.previousSubdomains?.[0]).toEqual("example");
|
||||
|
||||
const subdomain2 = "another";
|
||||
await team.update({ subdomain: subdomain2 });
|
||||
expect(team.subdomain).toEqual(subdomain2);
|
||||
expect(team.previousSubdomains?.length).toEqual(2);
|
||||
expect(team.previousSubdomains?.[0]).toEqual("example");
|
||||
expect(team.previousSubdomains?.[1]).toEqual(subdomain);
|
||||
});
|
||||
const response = await team.collectionIds();
|
||||
expect(response.length).toEqual(1);
|
||||
expect(response[0]).toEqual(collection.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -171,6 +171,9 @@ class Team extends ParanoidModel<
|
||||
@Column
|
||||
lastActiveAt: Date | null;
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
previousSubdomains: string[] | null;
|
||||
|
||||
// getters
|
||||
|
||||
/**
|
||||
@@ -369,6 +372,25 @@ class Team extends ParanoidModel<
|
||||
return model;
|
||||
}
|
||||
|
||||
@BeforeUpdate
|
||||
static async savePreviousSubdomain(model: Team) {
|
||||
const previousSubdomain = model.previous("subdomain");
|
||||
if (previousSubdomain && previousSubdomain !== model.subdomain) {
|
||||
model.previousSubdomains = model.previousSubdomains || [];
|
||||
|
||||
if (!model.previousSubdomains.includes(previousSubdomain)) {
|
||||
// Add the previous subdomain to the list of previous subdomains
|
||||
// upto a maximum of 3 previous subdomains
|
||||
model.previousSubdomains.push(previousSubdomain);
|
||||
if (model.previousSubdomains.length > 3) {
|
||||
model.previousSubdomains.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
@AfterUpdate
|
||||
static deletePreviousAvatar = async (model: Team) => {
|
||||
const previousAvatarUrl = model.previous("avatarUrl");
|
||||
@@ -393,6 +415,41 @@ class Team extends ParanoidModel<
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Find a team by its current or previous subdomain.
|
||||
*
|
||||
* @param subdomain - The subdomain to search for.
|
||||
* @returns The team with the given or previous subdomain, or null if not found.
|
||||
*/
|
||||
static async findBySubdomain(subdomain: string) {
|
||||
// Preference is always given to the team with the subdomain currently
|
||||
// otherwise we can try and find a team that previously used the subdomain.
|
||||
return (
|
||||
(await this.findOne({
|
||||
where: {
|
||||
subdomain,
|
||||
},
|
||||
})) || (await this.findByPreviousSubdomain(subdomain))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a team by its previous subdomain.
|
||||
*
|
||||
* @param previousSubdomain - The previous subdomain to search for.
|
||||
* @returns The team with the given previous subdomain, or null if not found.
|
||||
*/
|
||||
static async findByPreviousSubdomain(previousSubdomain: string) {
|
||||
return this.findOne({
|
||||
where: {
|
||||
previousSubdomains: {
|
||||
[Op.contains]: [previousSubdomain],
|
||||
},
|
||||
},
|
||||
order: [["updatedAt", "DESC"]],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Team;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { buildAdmin, buildTeam } from "@server/test/factories";
|
||||
import TeamDomain from "./TeamDomain";
|
||||
|
||||
describe("team domain model", () => {
|
||||
describe("TeamDomain", () => {
|
||||
describe("create", () => {
|
||||
it("should allow creation of domains", async () => {
|
||||
const team = await buildTeam();
|
||||
|
||||
@@ -7,6 +7,7 @@ import send from "koa-send";
|
||||
import userAgent, { UserAgentContext } from "koa-useragent";
|
||||
import { languages } from "@shared/i18n";
|
||||
import { IntegrationType, TeamPreference } from "@shared/types";
|
||||
import { parseDomain } from "@shared/utils/domains";
|
||||
import { Day } from "@shared/utils/time";
|
||||
import env from "@server/env";
|
||||
import { NotFoundError } from "@server/errors";
|
||||
@@ -138,11 +139,29 @@ router.get("*", shareDomains(), async (ctx, next) => {
|
||||
}
|
||||
|
||||
const team = await getTeamFromContext(ctx);
|
||||
let redirectUrl;
|
||||
|
||||
// Redirect all requests to custom domain if one is set
|
||||
if (team?.domain && team.domain !== ctx.hostname) {
|
||||
ctx.redirect(ctx.href.replace(ctx.hostname, team.domain));
|
||||
return;
|
||||
if (env.isCloudHosted) {
|
||||
// Redirect all requests to custom domain if one is set
|
||||
if (team?.domain && team.domain !== ctx.hostname) {
|
||||
redirectUrl = ctx.href.replace(ctx.hostname, team.domain);
|
||||
}
|
||||
|
||||
// Redirect if subdomain is not the current team's subdomain
|
||||
else if (team?.subdomain) {
|
||||
const { teamSubdomain } = parseDomain(ctx.href);
|
||||
if (team?.subdomain !== teamSubdomain) {
|
||||
redirectUrl = ctx.href.replace(
|
||||
`//${teamSubdomain}.`,
|
||||
`//${team.subdomain}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (redirectUrl) {
|
||||
ctx.redirect(redirectUrl);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const analytics = team
|
||||
|
||||
@@ -127,9 +127,7 @@ export async function getTeamFromContext(ctx: Context) {
|
||||
} else if (domain.custom) {
|
||||
team = await Team.findOne({ where: { domain: domain.host } });
|
||||
} else if (domain.teamSubdomain) {
|
||||
team = await Team.findOne({
|
||||
where: { subdomain: domain.teamSubdomain },
|
||||
});
|
||||
team = await Team.findBySubdomain(domain.teamSubdomain);
|
||||
}
|
||||
|
||||
return team;
|
||||
|
||||
Reference in New Issue
Block a user