210 lines
5.7 KiB
TypeScript
210 lines
5.7 KiB
TypeScript
import invariant from "invariant";
|
|
import { UniqueConstraintError } from "sequelize";
|
|
import WelcomeEmail from "@server/emails/templates/WelcomeEmail";
|
|
import {
|
|
AuthenticationError,
|
|
InvalidAuthenticationError,
|
|
EmailAuthenticationRequiredError,
|
|
AuthenticationProviderDisabledError,
|
|
} from "@server/errors";
|
|
import { APM } from "@server/logging/tracing";
|
|
import { AuthenticationProvider, Collection, Team, User } from "@server/models";
|
|
import teamProvisioner from "./teamProvisioner";
|
|
import userProvisioner from "./userProvisioner";
|
|
|
|
type Props = {
|
|
/** The IP address of the incoming request */
|
|
ip: string;
|
|
/** Details of the user logging in from SSO provider */
|
|
user: {
|
|
/** The displayed name of the user */
|
|
name: string;
|
|
/** The email address of the user */
|
|
email: string;
|
|
/** The public url of an image representing the user */
|
|
avatarUrl?: string | null;
|
|
/** The username of the user */
|
|
username?: string;
|
|
};
|
|
/** Details of the team the user is logging into */
|
|
team: {
|
|
/**
|
|
* The internal ID of the team that is being logged into based on the
|
|
* subdomain that the request came from, if any.
|
|
*/
|
|
teamId?: string;
|
|
/** 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 */
|
|
authenticationProvider: {
|
|
/** The name of the authentication provider, eg "google" */
|
|
name: string;
|
|
/** External identifier of the authentication provider */
|
|
providerId: string;
|
|
};
|
|
/** Details of the authentication from SSO provider */
|
|
authentication: {
|
|
/** External identifier of the user in the authentication provider */
|
|
providerId: string;
|
|
/** The scopes granted by the access token */
|
|
scopes: string[];
|
|
/** The token provided by the authentication provider */
|
|
accessToken?: string;
|
|
/** The refresh token provided by the authentication provider */
|
|
refreshToken?: string;
|
|
/** A number of seconds that the given access token expires in */
|
|
expiresIn?: number;
|
|
};
|
|
};
|
|
|
|
export type AccountProvisionerResult = {
|
|
user: User;
|
|
team: Team;
|
|
isNewTeam: boolean;
|
|
isNewUser: boolean;
|
|
};
|
|
|
|
async function accountProvisioner({
|
|
ip,
|
|
user: userParams,
|
|
team: teamParams,
|
|
authenticationProvider: authenticationProviderParams,
|
|
authentication: authenticationParams,
|
|
}: Props): Promise<AccountProvisionerResult> {
|
|
let result;
|
|
let emailMatchOnly;
|
|
|
|
try {
|
|
result = await teamProvisioner({
|
|
...teamParams,
|
|
authenticationProvider: authenticationProviderParams,
|
|
ip,
|
|
});
|
|
} catch (err) {
|
|
// The account could not be provisioned for the provided teamId
|
|
// check to see if we can try authentication using email matching only
|
|
if (err.id === "invalid_authentication") {
|
|
const authenticationProvider = await AuthenticationProvider.findOne({
|
|
where: {
|
|
name: authenticationProviderParams.name, // example: "google"
|
|
teamId: teamParams.teamId,
|
|
},
|
|
include: [
|
|
{
|
|
model: Team,
|
|
as: "team",
|
|
required: true,
|
|
},
|
|
],
|
|
});
|
|
|
|
if (authenticationProvider) {
|
|
emailMatchOnly = true;
|
|
result = {
|
|
authenticationProvider,
|
|
team: authenticationProvider.team,
|
|
isNewTeam: false,
|
|
};
|
|
}
|
|
}
|
|
|
|
if (!result) {
|
|
throw InvalidAuthenticationError(err.message);
|
|
}
|
|
}
|
|
|
|
invariant(result, "Team creator result must exist");
|
|
const { authenticationProvider, team, isNewTeam } = result;
|
|
|
|
if (!authenticationProvider.enabled) {
|
|
throw AuthenticationProviderDisabledError();
|
|
}
|
|
|
|
try {
|
|
const result = await userProvisioner({
|
|
name: userParams.name,
|
|
email: userParams.email,
|
|
username: userParams.username,
|
|
isAdmin: isNewTeam || undefined,
|
|
avatarUrl: userParams.avatarUrl,
|
|
teamId: team.id,
|
|
emailMatchOnly,
|
|
ip,
|
|
authentication: {
|
|
authenticationProviderId: authenticationProvider.id,
|
|
...authenticationParams,
|
|
expiresAt: authenticationParams.expiresIn
|
|
? new Date(Date.now() + authenticationParams.expiresIn * 1000)
|
|
: undefined,
|
|
},
|
|
});
|
|
const { isNewUser, user } = result;
|
|
|
|
if (isNewUser) {
|
|
await WelcomeEmail.schedule({
|
|
to: user.email,
|
|
teamUrl: team.url,
|
|
});
|
|
}
|
|
|
|
if (isNewUser || isNewTeam) {
|
|
let provision = isNewTeam;
|
|
|
|
// accounts for the case where a team is provisioned, but the user creation
|
|
// failed. In this case we have a valid previously created team but no
|
|
// onboarding collection.
|
|
if (!isNewTeam) {
|
|
const count = await Collection.count({
|
|
where: {
|
|
teamId: team.id,
|
|
},
|
|
});
|
|
provision = count === 0;
|
|
}
|
|
|
|
if (provision) {
|
|
await team.provisionFirstCollection(user.id);
|
|
}
|
|
}
|
|
|
|
return {
|
|
user,
|
|
team,
|
|
isNewUser,
|
|
isNewTeam,
|
|
};
|
|
} catch (err) {
|
|
if (err instanceof UniqueConstraintError) {
|
|
const exists = await User.findOne({
|
|
where: {
|
|
email: userParams.email,
|
|
teamId: team.id,
|
|
},
|
|
});
|
|
|
|
if (exists) {
|
|
throw EmailAuthenticationRequiredError(
|
|
"Email authentication required",
|
|
team.url
|
|
);
|
|
} else {
|
|
throw AuthenticationError(err.message, team.url);
|
|
}
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
export default APM.traceFunction({
|
|
serviceName: "command",
|
|
spanName: "accountProvisioner",
|
|
})(accountProvisioner);
|