feat: allow ad-hoc creation of new teams (#3964)
Co-authored-by: Tom Moor <tom@getoutline.com>
This commit is contained in:
110
server/commands/teamCreator.ts
Normal file
110
server/commands/teamCreator.ts
Normal 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);
|
||||
Reference in New Issue
Block a user