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.
Files
outline/server/models/Team.js
Tom Moor 9274005cbb feat: Upgrade editor (#1227)
* WIP

* document migration

* fix: Handle clashing keyboard events

* fix: convert getSummary

* fix: parseDocumentIds

* lint

* fix: Remove unused plugin

* Move editor version to header
Add editor version check for API endpoints

* fix: Editor update auto-reload
Bump RME

* test

* bump rme

* Remove slate flow types, improve themeing, bump rme

* bump rme

* fix: parseDocumentIds returning duplicate ID's, improved regression tests

* test

* fix: Missing code styles

* lint

* chore: Upgrade v2 migration to use AST

* Bump RME

* Update welcome doc

* add highlight to keyboard shortcuts ref

* theming improvements

* fix: Code comments show as headings, closes #1255

* loop

* fix: TOC highlighting

* lint

* add: Automated backup of docs before migration

* Update embeds to new format

* fix: React warning

* bump to final editor version 10.0.0

* test
2020-05-19 20:39:34 -07:00

221 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 { ValidationError } from '../errors';
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 & API',
'📝 Our Editor',
'👋 What is Outline',
];
for (const title of onboardingDocs) {
const text = await readFile(
path.join(__dirname, '..', 'onboarding', `${title}.md`),
'utf8'
);
const document = await Document.create({
version: 1,
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 ValidationError('At least one admin is required');
}
};
Team.prototype.suspendUser = async function(user: User, admin: User) {
if (user.id === admin.id)
throw new ValidationError('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;