This repository has been archived on 2022-08-14. You can view files and clone it, but cannot push or open issues or pull requests.
outline/server/models/Team.js

216 lines
5.5 KiB
JavaScript

// @flow
import uuid from 'uuid';
import { URL } from 'url';
import fs from 'fs';
import util from 'util';
import path from 'path';
import { DataTypes, sequelize, Op } from '../sequelize';
import { publicS3Endpoint, uploadToS3FromUrl } from '../utils/s3';
import {
stripSubdomain,
RESERVED_SUBDOMAINS,
} from '../../shared/utils/domains';
import parseTitle from '../../shared/utils/parseTitle';
import Collection from './Collection';
import Document from './Document';
import User from './User';
const readFile = util.promisify(fs.readFile);
const Team = sequelize.define(
'team',
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
name: DataTypes.STRING,
subdomain: {
type: DataTypes.STRING,
allowNull: true,
validate: {
isLowercase: true,
is: {
args: [/^[a-z\d-]+$/, 'i'],
msg: 'Must be only alphanumeric and dashes',
},
len: {
args: [4, 32],
msg: 'Must be between 4 and 32 characters',
},
notIn: {
args: [RESERVED_SUBDOMAINS],
msg: 'You chose a restricted word, please try another.',
},
},
unique: true,
},
slackId: { type: DataTypes.STRING, allowNull: true },
googleId: { type: DataTypes.STRING, allowNull: true },
avatarUrl: { type: DataTypes.STRING, allowNull: true },
sharing: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true },
guestSignin: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
},
documentEmbeds: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
},
slackData: DataTypes.JSONB,
},
{
getterMethods: {
url() {
if (!this.subdomain || process.env.SUBDOMAINS_ENABLED !== 'true') {
return process.env.URL;
}
const url = new URL(process.env.URL);
url.host = `${this.subdomain}.${stripSubdomain(url.host)}`;
return url.href.replace(/\/$/, '');
},
logoUrl() {
return (
this.avatarUrl || (this.slackData ? this.slackData.image_88 : null)
);
},
},
}
);
Team.associate = models => {
Team.hasMany(models.Collection, { as: 'collections' });
Team.hasMany(models.Document, { as: 'documents' });
Team.hasMany(models.User, { as: 'users' });
};
const uploadAvatar = async model => {
const endpoint = publicS3Endpoint();
const { avatarUrl } = model;
if (
avatarUrl &&
!avatarUrl.startsWith('/api') &&
!avatarUrl.startsWith(endpoint)
) {
try {
const newUrl = await uploadToS3FromUrl(
avatarUrl,
`avatars/${model.id}/${uuid.v4()}`,
'public-read'
);
if (newUrl) model.avatarUrl = newUrl;
} catch (err) {
// we can try again next time
console.error(err);
}
}
};
Team.prototype.provisionSubdomain = async function(subdomain) {
if (this.subdomain) return this.subdomain;
let append = 0;
while (true) {
try {
await this.update({ subdomain });
break;
} catch (err) {
// subdomain was invalid or already used, try again
subdomain = `${subdomain}${++append}`;
}
}
return subdomain;
};
Team.prototype.provisionFirstCollection = async function(userId) {
const collection = await Collection.create({
name: 'Welcome',
description:
'This collection is a quick guide to what Outline is all about. Feel free to delete this collection once your team is up to speed with the basics!',
type: 'atlas',
teamId: this.id,
creatorId: userId,
});
// For the first collection we go ahead and create some intitial documents to get
// the team started. You can edit these in /server/onboarding/x.md
const onboardingDocs = ['support', 'integrations', 'editor', 'philosophy'];
for (const name of onboardingDocs) {
const text = await readFile(
path.join(__dirname, '..', 'onboarding', `${name}.md`),
'utf8'
);
const { title } = parseTitle(text);
const document = await Document.create({
isWelcome: true,
parentDocumentId: null,
collectionId: collection.id,
teamId: collection.teamId,
userId: collection.creatorId,
lastModifiedById: collection.creatorId,
createdById: collection.creatorId,
title,
text,
});
await document.publish();
}
};
Team.prototype.addAdmin = async function(user: User) {
return user.update({ isAdmin: true });
};
Team.prototype.removeAdmin = async function(user: User) {
const res = await User.findAndCountAll({
where: {
teamId: this.id,
isAdmin: true,
id: {
[Op.ne]: user.id,
},
},
limit: 1,
});
if (res.count >= 1) {
return user.update({ isAdmin: false });
} else {
throw new Error('At least one admin is required');
}
};
Team.prototype.suspendUser = async function(user: User, admin: User) {
if (user.id === admin.id)
throw new Error('Unable to suspend the current user');
return user.update({
suspendedById: admin.id,
suspendedAt: new Date(),
});
};
Team.prototype.activateUser = async function(user: User, admin: User) {
return user.update({
suspendedById: null,
suspendedAt: null,
});
};
Team.prototype.collectionIds = async function(paranoid: boolean = true) {
let models = await Collection.findAll({
attributes: ['id', 'private'],
where: { teamId: this.id, private: false },
paranoid,
});
return models.map(c => c.id);
};
Team.beforeSave(uploadAvatar);
export default Team;