feat: allow ad-hoc creation of new teams (#3964)

Co-authored-by: Tom Moor <tom@getoutline.com>
This commit is contained in:
Nan Yu
2022-10-16 08:57:27 -04:00
committed by GitHub
parent 1fbc000e03
commit 39fc8d5c14
33 changed files with 529 additions and 186 deletions

View File

@@ -0,0 +1,110 @@
import { Transaction } from "sequelize";
import slugify from "slugify";
import { RESERVED_SUBDOMAINS } from "@shared/utils/domains";
import { APM } from "@server/logging/tracing";
import { Team, Event } from "@server/models";
import { generateAvatarUrl } from "@server/utils/avatars";
type Props = {
/** The displayed name of the team */
name: string;
/** The domain name from the email of the user logging in */
domain?: string;
/** The preferred subdomain to provision for the team if not yet created */
subdomain: string;
/** The public url of an image representing the team */
avatarUrl?: string | null;
/** Details of the authentication provider being used */
authenticationProviders: {
/** The name of the authentication provider, eg "google" */
name: string;
/** External identifier of the authentication provider */
providerId: string;
}[];
/** The IP address of the incoming request */
ip: string;
/** Optional transaction to be chained from outside */
transaction: Transaction;
};
async function teamCreator({
name,
domain,
subdomain,
avatarUrl,
authenticationProviders,
ip,
transaction,
}: Props): Promise<Team> {
// If the service did not provide a logo/avatar then we attempt to generate
// one via ClearBit, or fallback to colored initials in worst case scenario
if (!avatarUrl) {
avatarUrl = await generateAvatarUrl({
name,
domain,
id: subdomain,
});
}
const team = await Team.create(
{
name,
avatarUrl,
authenticationProviders,
},
{
include: ["authenticationProviders"],
transaction,
}
);
await Event.create(
{
name: "teams.create",
teamId: team.id,
ip,
},
{
transaction,
}
);
const availableSubdomain = await findAvailableSubdomain(team, subdomain);
await team.update({ subdomain: availableSubdomain }, { transaction });
return team;
}
async function findAvailableSubdomain(team: Team, requestedSubdomain: string) {
// filter subdomain to only valid characters
// if there are less than the minimum length, use a default subdomain
const normalizedSubdomain = slugify(requestedSubdomain, {
lower: true,
strict: true,
});
let subdomain =
normalizedSubdomain.length < 3 ||
RESERVED_SUBDOMAINS.includes(normalizedSubdomain)
? "team"
: normalizedSubdomain;
let append = 0;
for (;;) {
const existing = await Team.findOne({ where: { subdomain } });
if (existing) {
// subdomain was invalid or already used, try another
subdomain = `${normalizedSubdomain}${++append}`;
} else {
break;
}
}
return subdomain;
}
export default APM.traceFunction({
serviceName: "command",
spanName: "teamCreator",
})(teamCreator);