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

229 lines
5.6 KiB
JavaScript
Raw Normal View History

// @flow
import fs from "fs";
import path from "path";
import { URL } from "url";
import util from "util";
import uuid from "uuid";
import {
stripSubdomain,
RESERVED_SUBDOMAINS,
} from "../../shared/utils/domains";
import { ValidationError } from "../errors";
import { DataTypes, sequelize, Op } from "../sequelize";
import { publicS3Endpoint, uploadToS3FromUrl } from "../utils/s3";
import Collection from "./Collection";
import Document from "./Document";
import User from "./User";
2016-04-29 05:25:37 +00:00
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,
2018-11-04 04:47:46 +00:00
is: {
args: [/^[a-z\d-]+$/, "i"],
msg: "Must be only alphanumeric and dashes",
2018-11-04 04:47:46 +00:00
},
len: {
args: [4, 32],
msg: "Must be between 4 and 32 characters",
2018-11-04 04:47:46 +00:00
},
notIn: {
args: [RESERVED_SUBDOMAINS],
msg: "You chose a restricted word, please try another.",
2018-11-04 04:47:46 +00:00
},
},
unique: true,
},
domain: {
type: DataTypes.STRING,
allowNull: true,
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,
2016-06-20 07:18:03 +00:00
},
{
paranoid: true,
getterMethods: {
url() {
if (this.domain) {
return `https://${this.domain}`;
}
if (!this.subdomain || process.env.SUBDOMAINS_ENABLED !== "true") {
2018-11-12 05:17:03 +00:00
return process.env.URL;
}
const url = new URL(process.env.URL);
url.host = `${this.subdomain}.${stripSubdomain(url.host)}`;
return url.href.replace(/\/$/, "");
},
2018-11-12 05:17:03 +00:00
logoUrl() {
2018-11-10 07:40:33 +00:00
return (
this.avatarUrl || (this.slackData ? this.slackData.image_88 : null)
);
},
},
}
);
2016-04-29 05:25:37 +00:00
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) {
2018-11-12 22:53:32 +00:00
if (this.subdomain) return this.subdomain;
2018-11-12 22:53:32 +00:00
let append = 0;
while (true) {
try {
await this.update({ subdomain });
break;
} catch (err) {
// subdomain was invalid or already used, try again
subdomain = `${subdomain}${++append}`;
}
}
2018-11-12 22:53:32 +00:00
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!",
teamId: this.id,
createdById: userId,
sort: Collection.DEFAULT_SORT,
});
// 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 = [
2020-07-21 02:14:15 +00:00
"Support",
"Integrations & API",
"Our Editor",
"What is Outline",
];
for (const title of onboardingDocs) {
const text = await readFile(
path.join(
__dirname,
"..",
"..",
"..",
"server",
"onboarding",
`${title}.md`
),
"utf8"
);
const document = await Document.create({
version: 1,
isWelcome: true,
parentDocumentId: null,
collectionId: collection.id,
teamId: collection.teamId,
userId: collection.createdById,
lastModifiedById: collection.createdById,
createdById: collection.createdById,
title,
text,
});
await document.publish(collection.createdById);
}
};
Team.prototype.addAdmin = async function (user: User) {
return user.update({ isAdmin: true });
2017-12-26 13:02:26 +00:00
};
Team.prototype.removeAdmin = async function (user: User) {
2017-12-26 13:02:26 +00:00
const res = await User.findAndCountAll({
where: {
teamId: this.id,
2017-12-26 13:02:26 +00:00
isAdmin: true,
id: {
[Op.ne]: user.id,
},
},
limit: 1,
});
if (res.count >= 1) {
return user.update({ isAdmin: false });
2017-12-26 13:02:26 +00:00
} else {
throw new ValidationError("At least one admin is required");
2017-12-26 13:02:26 +00:00
}
};
Team.prototype.activateUser = async function (user: User, admin: User) {
2018-03-04 23:38:51 +00:00
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);
2016-04-29 05:25:37 +00:00
export default Team;