2021-03-09 20:22:08 +00:00
|
|
|
|
// @flow
|
|
|
|
|
import Sequelize from "sequelize";
|
2021-10-20 03:56:11 +00:00
|
|
|
|
import { Event, Team, User, UserAuthentication } from "../models";
|
2021-03-09 20:22:08 +00:00
|
|
|
|
import { sequelize } from "../sequelize";
|
|
|
|
|
|
|
|
|
|
const Op = Sequelize.Op;
|
|
|
|
|
|
|
|
|
|
type UserCreatorResult = {|
|
|
|
|
|
user: User,
|
|
|
|
|
isNewUser: boolean,
|
|
|
|
|
authentication: UserAuthentication,
|
|
|
|
|
|};
|
|
|
|
|
|
|
|
|
|
export default async function userCreator({
|
|
|
|
|
name,
|
|
|
|
|
email,
|
2021-09-17 01:45:37 +00:00
|
|
|
|
username,
|
2021-03-09 20:22:08 +00:00
|
|
|
|
isAdmin,
|
|
|
|
|
avatarUrl,
|
|
|
|
|
teamId,
|
|
|
|
|
authentication,
|
|
|
|
|
ip,
|
|
|
|
|
}: {|
|
|
|
|
|
name: string,
|
|
|
|
|
email: string,
|
2021-09-17 01:45:37 +00:00
|
|
|
|
username?: string,
|
2021-03-09 20:22:08 +00:00
|
|
|
|
isAdmin?: boolean,
|
|
|
|
|
avatarUrl?: string,
|
|
|
|
|
teamId: string,
|
|
|
|
|
ip: string,
|
|
|
|
|
authentication: {|
|
|
|
|
|
authenticationProviderId: string,
|
|
|
|
|
providerId: string,
|
|
|
|
|
scopes: string[],
|
|
|
|
|
accessToken?: string,
|
|
|
|
|
refreshToken?: string,
|
|
|
|
|
|},
|
|
|
|
|
|}): Promise<UserCreatorResult> {
|
|
|
|
|
const { authenticationProviderId, providerId, ...rest } = authentication;
|
|
|
|
|
const auth = await UserAuthentication.findOne({
|
|
|
|
|
where: {
|
|
|
|
|
providerId,
|
|
|
|
|
},
|
|
|
|
|
include: [
|
|
|
|
|
{
|
|
|
|
|
model: User,
|
|
|
|
|
as: "user",
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Someone has signed in with this authentication before, we just
|
|
|
|
|
// want to update the details instead of creating a new record
|
|
|
|
|
if (auth) {
|
|
|
|
|
const { user } = auth;
|
|
|
|
|
|
2021-04-12 02:39:31 +00:00
|
|
|
|
// We found an authentication record that matches the user id, but it's
|
|
|
|
|
// associated with a different authentication provider, (eg a different
|
|
|
|
|
// hosted google domain). This is possible in Google Auth when moving domains.
|
|
|
|
|
// In the future we may auto-migrate these.
|
|
|
|
|
if (auth.authenticationProviderId !== authenticationProviderId) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`User authentication ${providerId} already exists for ${auth.authenticationProviderId}, tried to assign to ${authenticationProviderId}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (user) {
|
2021-09-17 01:45:37 +00:00
|
|
|
|
await user.update({ email, username });
|
2021-04-12 02:39:31 +00:00
|
|
|
|
await auth.update(rest);
|
|
|
|
|
|
|
|
|
|
return { user, authentication: auth, isNewUser: false };
|
|
|
|
|
}
|
2021-03-09 20:22:08 +00:00
|
|
|
|
|
2021-04-12 02:39:31 +00:00
|
|
|
|
// We found an authentication record, but the associated user was deleted or
|
|
|
|
|
// otherwise didn't exist. Cleanup the auth record and proceed with creating
|
|
|
|
|
// a new user. See: https://github.com/outline/outline/issues/2022
|
|
|
|
|
await auth.destroy();
|
2021-03-09 20:22:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A `user` record might exist in the form of an invite even if there is no
|
|
|
|
|
// existing authentication record that matches. In Outline an invite is a
|
|
|
|
|
// shell user record.
|
|
|
|
|
const invite = await User.findOne({
|
|
|
|
|
where: {
|
|
|
|
|
email,
|
|
|
|
|
teamId,
|
|
|
|
|
lastActiveAt: {
|
|
|
|
|
[Op.eq]: null,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
include: [
|
|
|
|
|
{
|
|
|
|
|
model: UserAuthentication,
|
|
|
|
|
as: "authentications",
|
|
|
|
|
required: false,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// We have an existing invite for his user, so we need to update it with our
|
|
|
|
|
// new details and link up the authentication method
|
|
|
|
|
if (invite && !invite.authentications.length) {
|
|
|
|
|
let transaction = await sequelize.transaction();
|
|
|
|
|
let auth;
|
|
|
|
|
try {
|
|
|
|
|
await invite.update(
|
|
|
|
|
{
|
|
|
|
|
name,
|
|
|
|
|
avatarUrl,
|
|
|
|
|
},
|
|
|
|
|
{ transaction }
|
|
|
|
|
);
|
|
|
|
|
auth = await invite.createAuthentication(authentication, {
|
|
|
|
|
transaction,
|
|
|
|
|
});
|
|
|
|
|
await transaction.commit();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-12 01:59:31 +00:00
|
|
|
|
return { user: invite, authentication: auth, isNewUser: true };
|
2021-03-09 20:22:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No auth, no user – this is an entirely new sign in.
|
|
|
|
|
let transaction = await sequelize.transaction();
|
|
|
|
|
|
|
|
|
|
try {
|
2021-10-20 03:56:11 +00:00
|
|
|
|
const { defaultUserRole } = await Team.findByPk(teamId, {
|
|
|
|
|
attributes: ["defaultUserRole"],
|
|
|
|
|
transaction,
|
|
|
|
|
});
|
|
|
|
|
|
2021-03-09 20:22:08 +00:00
|
|
|
|
const user = await User.create(
|
|
|
|
|
{
|
|
|
|
|
name,
|
|
|
|
|
email,
|
2021-09-17 01:45:37 +00:00
|
|
|
|
username,
|
2021-10-20 03:56:11 +00:00
|
|
|
|
isAdmin: typeof isAdmin === "boolean" && isAdmin,
|
|
|
|
|
isViewer: isAdmin === true ? false : defaultUserRole === "viewer",
|
2021-03-09 20:22:08 +00:00
|
|
|
|
teamId,
|
|
|
|
|
avatarUrl,
|
|
|
|
|
service: null,
|
|
|
|
|
authentications: [authentication],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
include: "authentications",
|
|
|
|
|
transaction,
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
await Event.create(
|
|
|
|
|
{
|
|
|
|
|
name: "users.create",
|
|
|
|
|
actorId: user.id,
|
|
|
|
|
userId: user.id,
|
|
|
|
|
teamId: user.teamId,
|
|
|
|
|
data: {
|
|
|
|
|
name: user.name,
|
|
|
|
|
},
|
|
|
|
|
ip,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
transaction,
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
await transaction.commit();
|
|
|
|
|
return {
|
|
|
|
|
user,
|
|
|
|
|
authentication: user.authentications[0],
|
|
|
|
|
isNewUser: true,
|
|
|
|
|
};
|
|
|
|
|
} catch (err) {
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
}
|