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
Nan Yu 142303b3de feat: Add groups and group permissions (#1204)
* WIP - got one API test to pass yay

* adds group update endpoint

* added group policies

* adds groups.list API

* adds groups.info

* remove comment

* WIP

* tests for delete

* adds group membership list

* adds tests for groups list

* add and remove user endpoints for group

* ask some questions

* fix up some issues around primary keys

* remove export from group permissions

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* remove random file

* only create events on actual updates, add tests to ensure

* adds uniqueness validation to group name

* throw validation errors on model and let it pass through the controller

* fix linting

* WIP

* WIP

* WIP

* WIP

* WIP basic edit and delete

* basic CRUD for groups and memberships in place

* got member counts working

* add member count and limit the number of users sent over teh wire to 6

* factor avatar with AvatarWithPresence into its own class

* wip

* WIP avatars in group lists

* WIP collection groups

* add and remove group endpoints

* wip add collection groups

* wip get group adding to collections to work

* wip get updating collection group memberships to work

* wip get new group modal working

* add tests for collection index

* include collection groups in the withmemberships scope

* tie permissions to group memberships

* remove unused import

* Update app/components/GroupListItem.js

update title copy

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update server/migrations/20191211044318-create-groups.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update server/api/groups.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update server/api/groups.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/menus/CollectionMenu.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update server/models/Group.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* minor fixes

* Update app/scenes/CollectionMembers/AddGroupsToCollection.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/menus/GroupMenu.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/menus/GroupMenu.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/menus/GroupMenu.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/scenes/Collection.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/scenes/CollectionMembers/CollectionMembers.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/scenes/GroupNew.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/scenes/GroupNew.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/scenes/Settings/Groups.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update server/api/documents.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/scenes/CollectionMembers/components/CollectionGroupMemberListItem.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* address comments

* WIP - getting websocket stuff up and running

* socket event for group deletion

* wrapped up cascading deletes

* lint

* flow

* fix: UI feedback

* fix: Facepile size

* fix: Lots of missing await's

* Allow clicking facepile on group list item to open members

* remove unused route push, grammar

* fix: Remove bad analytics events
feat: Add group events to audit log

* collection. -> collections.

* Add groups to entity websocket events (sync create/update/delete) between clients

* fix: Users should not be able to see groups they are not a member of

* fix: Not caching errors in UI when changing group memberships

* fix: Hide unusable UI

* test

* fix: Tweak language

* feat: Automatically open 'add member' modal after creating group

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2020-03-14 20:48:32 -07:00

217 lines
5.6 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 { 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', '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 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;