Merge from upstream master

This commit is contained in:
mogita 2019-06-24 11:29:21 +08:00
commit c278172290
No known key found for this signature in database
GPG Key ID: A0AA1B9C57A48ECF
44 changed files with 595 additions and 7793 deletions

View File

@ -35,11 +35,11 @@ BUGSNAG_KEY=
GITHUB_ACCESS_TOKEN=
# AWS credentials (optional in dev)
# Update the bucket URL according to your choice on AWS
AWS_ACCESS_KEY_ID=get_a_key_from_aws
AWS_SECRET_ACCESS_KEY=get_the_secret_of_above_key
AWS_S3_UPLOAD_BUCKET_URL=https://s3-ap-southeast-1.amazonaws.com
AWS_S3_UPLOAD_BUCKET_NAME=bucket-name-here
AWS_REGION=ap-xxxxx-1
AWS_S3_UPLOAD_BUCKET_URL=http://s3:4569
AWS_S3_UPLOAD_BUCKET_NAME=bucket_name_here
AWS_S3_UPLOAD_MAX_SIZE=26214400
# Emails configuration (optional)

View File

@ -1,5 +1,5 @@
// @flow
import { filter } from 'lodash';
import { filter, orderBy } from 'lodash';
import { computed, action, runInAction } from 'mobx';
import invariant from 'invariant';
import { client } from 'utils/ApiClient';
@ -22,6 +22,11 @@ export default class UsersStore extends BaseStore<User> {
return filter(this.orderedData, user => user.isAdmin);
}
@computed
get orderedData(): User[] {
return orderBy(Array.from(this.data.values()), 'name', 'asc');
}
@action
promote = (user: User) => {
return this.actionOnUser('promote', user);

File diff suppressed because it is too large Load Diff

View File

@ -141,16 +141,15 @@
"react-router-dom": "^4.3.1",
"react-waypoint": "^7.3.1",
"redis": "^2.6.2",
"redis-lock": "^0.1.0",
"rich-markdown-editor": "^9.5.1",
"rich-markdown-editor": "^9.6.1",
"safestart": "1.1.0",
"sequelize": "4.28.6",
"sequelize-cli": "^5.4.0",
"sequelize-encrypted": "0.1.0",
"sequelize": "^5.8.12",
"sequelize-cli": "^5.5.0",
"sequelize-encrypted": "^0.1.0",
"slug": "^1.0.0",
"socket.io": "^2.2.0",
"socketio-auth": "^0.1.1",
"socket.io-redis": "^5.2.0",
"socketio-auth": "^0.1.1",
"string-replace-to-array": "^1.0.3",
"style-loader": "^0.18.2",
"styled-components": "^4.2.0",
@ -187,5 +186,5 @@
"prettier": "1.8.2",
"rimraf": "^2.5.4"
},
"version": "0.17.0"
"version": "0.18.0"
}

View File

@ -107,6 +107,15 @@ Object {
}
`;
exports[`#documents.update should require text while appending 1`] = `
Object {
"error": "param_required",
"message": "Text is required while appending",
"ok": false,
"status": 400,
}
`;
exports[`#documents.viewed should require authentication 1`] = `
Object {
"error": "authentication_required",

View File

@ -4,7 +4,7 @@ exports[`#users.activate should activate a suspended user 1`] = `
Object {
"data": Object {
"avatarUrl": "http://example.com/avatar.png",
"createdAt": "2018-01-01T00:00:00.000Z",
"createdAt": "2018-01-02T00:00:00.000Z",
"email": "user1@example.com",
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
"isAdmin": false,
@ -38,7 +38,7 @@ exports[`#users.demote should demote an admin 1`] = `
Object {
"data": Object {
"avatarUrl": "http://example.com/avatar.png",
"createdAt": "2018-01-01T00:00:00.000Z",
"createdAt": "2018-01-02T00:00:00.000Z",
"email": "user1@example.com",
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
"isAdmin": false,
@ -71,6 +71,14 @@ Object {
exports[`#users.list should require admin for detailed info 1`] = `
Object {
"data": Array [
Object {
"avatarUrl": "http://example.com/avatar.png",
"createdAt": "2018-01-02T00:00:00.000Z",
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
"isAdmin": false,
"isSuspended": false,
"name": "User 1",
},
Object {
"avatarUrl": "http://example.com/avatar.png",
"createdAt": "2018-01-01T00:00:00.000Z",
@ -79,14 +87,6 @@ Object {
"isSuspended": false,
"name": "Admin User",
},
Object {
"avatarUrl": "http://example.com/avatar.png",
"createdAt": "2018-01-01T00:00:00.000Z",
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
"isAdmin": false,
"isSuspended": false,
"name": "User 1",
},
],
"ok": true,
"pagination": Object {
@ -103,7 +103,7 @@ Object {
"data": Array [
Object {
"avatarUrl": "http://example.com/avatar.png",
"createdAt": "2018-01-01T00:00:00.000Z",
"createdAt": "2018-01-02T00:00:00.000Z",
"email": "user1@example.com",
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
"isAdmin": false,
@ -134,7 +134,7 @@ exports[`#users.promote should promote a new admin 1`] = `
Object {
"data": Object {
"avatarUrl": "http://example.com/avatar.png",
"createdAt": "2018-01-01T00:00:00.000Z",
"createdAt": "2018-01-02T00:00:00.000Z",
"email": "user1@example.com",
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
"isAdmin": true,
@ -168,7 +168,7 @@ exports[`#users.suspend should suspend an user 1`] = `
Object {
"data": Object {
"avatarUrl": "http://example.com/avatar.png",
"createdAt": "2018-01-01T00:00:00.000Z",
"createdAt": "2018-01-02T00:00:00.000Z",
"email": "user1@example.com",
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
"isAdmin": false,
@ -202,7 +202,7 @@ exports[`#users.update should update user profile information 1`] = `
Object {
"data": Object {
"avatarUrl": "http://example.com/avatar.png",
"createdAt": "2018-01-01T00:00:00.000Z",
"createdAt": "2018-01-02T00:00:00.000Z",
"email": "user1@example.com",
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
"isAdmin": false,

View File

@ -49,7 +49,7 @@ router.post('apiKeys.delete', auth(), async ctx => {
ctx.assertUuid(id, 'id is required');
const user = ctx.state.user;
const key = await ApiKey.findById(id);
const key = await ApiKey.findByPk(id);
authorize(user, 'delete', key);
await key.destroy();

View File

@ -8,7 +8,7 @@ const router = new Router();
router.post('auth.info', auth(), async ctx => {
const user = ctx.state.user;
const team = await Team.findById(user.teamId);
const team = await Team.findByPk(user.teamId);
ctx.body = {
data: {

View File

@ -49,7 +49,7 @@ router.post('collections.info', auth(), async ctx => {
const { id } = ctx.body;
ctx.assertUuid(id, 'id is required');
const collection = await Collection.findById(id);
const collection = await Collection.findByPk(id);
authorize(ctx.state.user, 'read', collection);
ctx.body = {
@ -62,14 +62,14 @@ router.post('collections.add_user', auth(), async ctx => {
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(userId, 'userId is required');
const collection = await Collection.findById(id);
const collection = await Collection.findByPk(id);
authorize(ctx.state.user, 'update', collection);
if (!collection.private) {
throw new InvalidRequestError('Collection must be private to add users');
}
const user = await User.findById(userId);
const user = await User.findByPk(userId);
authorize(ctx.state.user, 'read', user);
await CollectionUser.create({
@ -97,14 +97,14 @@ router.post('collections.remove_user', auth(), async ctx => {
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(userId, 'userId is required');
const collection = await Collection.findById(id);
const collection = await Collection.findByPk(id);
authorize(ctx.state.user, 'update', collection);
if (!collection.private) {
throw new InvalidRequestError('Collection must be private to remove users');
}
const user = await User.findById(userId);
const user = await User.findByPk(userId);
authorize(ctx.state.user, 'read', user);
await collection.removeUser(user);
@ -126,7 +126,7 @@ router.post('collections.users', auth(), async ctx => {
const { id } = ctx.body;
ctx.assertUuid(id, 'id is required');
const collection = await Collection.findById(id);
const collection = await Collection.findByPk(id);
authorize(ctx.state.user, 'read', collection);
const users = await collection.getUsers();
@ -141,7 +141,7 @@ router.post('collections.export', auth(), async ctx => {
ctx.assertUuid(id, 'id is required');
const user = ctx.state.user;
const collection = await Collection.findById(id);
const collection = await Collection.findByPk(id);
authorize(user, 'export', collection);
// async operation to create zip archive and email user
@ -154,7 +154,7 @@ router.post('collections.export', auth(), async ctx => {
router.post('collections.exportAll', auth(), async ctx => {
const user = ctx.state.user;
const team = await Team.findById(user.teamId);
const team = await Team.findByPk(user.teamId);
authorize(user, 'export', team);
// async operation to create zip archive and email user
@ -174,7 +174,7 @@ router.post('collections.update', auth(), async ctx => {
ctx.assertHexColor(color, 'Invalid hex value (please use format #FFFFFF)');
const user = ctx.state.user;
const collection = await Collection.findById(id);
const collection = await Collection.findByPk(id);
authorize(user, 'update', collection);
if (isPrivate && !collection.private) {
@ -237,7 +237,7 @@ router.post('collections.delete', auth(), async ctx => {
const user = ctx.state.user;
ctx.assertUuid(id, 'id is required');
const collection = await Collection.findById(id);
const collection = await Collection.findByPk(id);
authorize(user, 'delete', collection);
const total = await Collection.count();

View File

@ -41,7 +41,7 @@ router.post('documents.list', auth(), pagination(), async ctx => {
ctx.assertUuid(collectionId, 'collection must be a UUID');
where = { ...where, collectionId };
const collection = await Collection.findById(collectionId);
const collection = await Collection.findByPk(collectionId);
authorize(user, 'read', collection);
// otherwise, filter by all collections the user has access to
@ -77,7 +77,7 @@ router.post('documents.pinned', auth(), pagination(), async ctx => {
ctx.assertUuid(collectionId, 'collection is required');
const user = ctx.state.user;
const collection = await Collection.findById(collectionId);
const collection = await Collection.findByPk(collectionId);
authorize(user, 'read', collection);
const starredScope = { method: ['withStarred', user.id] };
@ -86,7 +86,6 @@ router.post('documents.pinned', auth(), pagination(), async ctx => {
teamId: user.teamId,
collectionId,
pinnedById: {
// $FlowFixMe
[Op.ne]: null,
},
},
@ -118,7 +117,6 @@ router.post('documents.archived', auth(), pagination(), async ctx => {
teamId: user.teamId,
collectionId: collectionIds,
archivedAt: {
// $FlowFixMe
[Op.ne]: null,
},
},
@ -232,7 +230,6 @@ router.post('documents.drafts', auth(), pagination(), async ctx => {
where: {
userId: user.id,
collectionId: collectionIds,
// $FlowFixMe
publishedAt: { [Op.eq]: null },
},
order: [[sort, direction]],
@ -258,9 +255,8 @@ router.post('documents.info', auth({ required: false }), async ctx => {
let document;
if (shareId) {
const share = await Share.find({
const share = await Share.findOne({
where: {
// $FlowFixMe
revokedAt: { [Op.eq]: null },
id: shareId,
},
@ -277,7 +273,7 @@ router.post('documents.info', auth({ required: false }), async ctx => {
}
document = share.document;
} else {
document = await Document.findById(id);
document = await Document.findByPk(id);
authorize(user, 'read', document);
}
@ -293,7 +289,7 @@ router.post('documents.revision', auth(), async ctx => {
ctx.assertPresent(id, 'id is required');
ctx.assertPresent(revisionId, 'revisionId is required');
const document = await Document.findById(id);
const document = await Document.findByPk(id);
authorize(ctx.state.user, 'read', document);
const revision = await Revision.findOne({
@ -313,7 +309,7 @@ router.post('documents.revisions', auth(), pagination(), async ctx => {
let { id, sort = 'updatedAt', direction } = ctx.body;
if (direction !== 'ASC') direction = 'DESC';
ctx.assertPresent(id, 'id is required');
const document = await Document.findById(id);
const document = await Document.findByPk(id);
authorize(ctx.state.user, 'read', document);
@ -335,7 +331,7 @@ router.post('documents.restore', auth(), async ctx => {
ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const document = await Document.findById(id);
const document = await Document.findByPk(id);
if (document.archivedAt) {
authorize(user, 'unarchive', document);
@ -354,7 +350,7 @@ router.post('documents.restore', auth(), async ctx => {
// restore a document to a specific revision
authorize(user, 'update', document);
const revision = await Revision.findById(revisionId);
const revision = await Revision.findByPk(revisionId);
authorize(document, 'restore', revision);
document.text = revision.text;
@ -386,7 +382,7 @@ router.post('documents.search', auth(), pagination(), async ctx => {
if (collectionId) {
ctx.assertUuid(collectionId, 'collectionId must be a UUID');
const collection = await Collection.findById(collectionId);
const collection = await Collection.findByPk(collectionId);
authorize(user, 'read', collection);
}
@ -430,7 +426,7 @@ router.post('documents.pin', auth(), async ctx => {
const { id } = ctx.body;
ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const document = await Document.findById(id);
const document = await Document.findByPk(id);
authorize(user, 'update', document);
@ -454,7 +450,7 @@ router.post('documents.unpin', auth(), async ctx => {
const { id } = ctx.body;
ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const document = await Document.findById(id);
const document = await Document.findByPk(id);
authorize(user, 'update', document);
@ -478,7 +474,7 @@ router.post('documents.star', auth(), async ctx => {
const { id } = ctx.body;
ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const document = await Document.findById(id);
const document = await Document.findByPk(id);
authorize(user, 'read', document);
@ -499,7 +495,7 @@ router.post('documents.unstar', auth(), async ctx => {
const { id } = ctx.body;
ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const document = await Document.findById(id);
const document = await Document.findByPk(id);
authorize(user, 'read', document);
@ -590,7 +586,7 @@ router.post('documents.create', auth(), async ctx => {
// reload to get all of the data needed to present (user, collection etc)
// we need to specify publishedAt to bypass default scope that only returns
// published documents
document = await Document.find({
document = await Document.findOne({
where: { id: document.id, publishedAt: document.publishedAt },
});
@ -600,12 +596,22 @@ router.post('documents.create', auth(), async ctx => {
});
router.post('documents.update', auth(), async ctx => {
const { id, title, text, publish, autosave, done, lastRevision } = ctx.body;
const {
id,
title,
text,
publish,
autosave,
done,
lastRevision,
append,
} = ctx.body;
ctx.assertPresent(id, 'id is required');
ctx.assertPresent(title || text, 'title or text is required');
if (append) ctx.assertPresent(text, 'Text is required while appending');
const user = ctx.state.user;
const document = await Document.findById(id);
const document = await Document.findByPk(id);
authorize(ctx.state.user, 'update', document);
@ -615,7 +621,12 @@ router.post('documents.update', auth(), async ctx => {
// Update document
if (title) document.title = title;
if (text) document.text = text;
//append to document
if (append) {
document.text += text;
} else if (text) {
document.text = text;
}
document.lastModifiedById = user.id;
if (publish) {
@ -665,10 +676,10 @@ router.post('documents.move', auth(), async ctx => {
}
const user = ctx.state.user;
const document = await Document.findById(id);
const document = await Document.findByPk(id);
authorize(user, 'move', document);
const collection = await Collection.findById(collectionId);
const collection = await Collection.findByPk(collectionId);
authorize(user, 'update', collection);
if (collection.type !== 'atlas' && parentDocumentId) {
@ -678,7 +689,7 @@ router.post('documents.move', auth(), async ctx => {
}
if (parentDocumentId) {
const parent = await Document.findById(parentDocumentId);
const parent = await Document.findByPk(parentDocumentId);
authorize(user, 'update', parent);
}
@ -706,7 +717,7 @@ router.post('documents.archive', auth(), async ctx => {
ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const document = await Document.findById(id);
const document = await Document.findByPk(id);
authorize(user, 'archive', document);
await document.archive(user.id);
@ -729,7 +740,7 @@ router.post('documents.delete', auth(), async ctx => {
ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const document = await Document.findById(id);
const document = await Document.findByPk(id);
authorize(user, 'delete', document);
await document.delete();

View File

@ -66,10 +66,11 @@ describe('#documents.info', async () => {
});
it('should return document from shareId without token', async () => {
const { document } = await seed();
const { document, user } = await seed();
const share = await buildShare({
documentId: document.id,
teamId: document.teamId,
userId: user.id,
});
const res = await server.post('/api/documents.info', {
@ -88,6 +89,7 @@ describe('#documents.info', async () => {
const share = await buildShare({
documentId: document.id,
teamId: document.teamId,
userId: user.id,
});
await share.revoke(user.id);
@ -102,6 +104,7 @@ describe('#documents.info', async () => {
const share = await buildShare({
documentId: document.id,
teamId: document.teamId,
userId: user.id,
});
await document.archive(user.id);
@ -116,6 +119,7 @@ describe('#documents.info', async () => {
const share = await buildShare({
documentId: document.id,
teamId: document.teamId,
userId: user.id,
});
const res = await server.post('/api/documents.info', {
@ -134,6 +138,7 @@ describe('#documents.info', async () => {
const share = await buildShare({
documentId: document.id,
teamId: document.teamId,
userId: user.id,
});
collection.private = true;
@ -974,7 +979,7 @@ describe('#documents.create', async () => {
},
});
const body = await res.json();
const newDocument = await Document.findById(body.data.id);
const newDocument = await Document.findByPk(body.data.id);
expect(res.status).toEqual(200);
expect(newDocument.parentDocumentId).toBe(null);
expect(newDocument.collection.id).toBe(collection.id);
@ -1212,6 +1217,42 @@ describe('#documents.update', async () => {
});
expect(res.status).toEqual(403);
});
it('should append document with text', async () => {
const { user, document } = await seed();
const res = await server.post('/api/documents.update', {
body: {
token: user.getJwtToken(),
id: document.id,
text: 'Additional text',
lastRevision: document.revision,
append: true,
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.text).toBe(document.text + 'Additional text');
});
it('should require text while appending', async () => {
const { user, document } = await seed();
const res = await server.post('/api/documents.update', {
body: {
token: user.getJwtToken(),
id: document.id,
lastRevision: document.revision,
title: 'Updated Title',
append: true,
},
});
const body = await res.json();
expect(res.status).toEqual(400);
expect(body).toMatchSnapshot();
});
});
describe('#documents.archive', async () => {

View File

@ -12,15 +12,16 @@ router.post('hooks.unfurl', async ctx => {
const { challenge, token, event } = ctx.body;
if (challenge) return (ctx.body = ctx.body.challenge);
if (token !== process.env.SLACK_VERIFICATION_TOKEN)
if (token !== process.env.SLACK_VERIFICATION_TOKEN) {
throw new AuthenticationError('Invalid token');
}
const user = await User.find({
const user = await User.findOne({
where: { service: 'slack', serviceId: event.user },
});
if (!user) return;
const auth = await Authentication.find({
const auth = await Authentication.findOne({
where: { service: 'slack', teamId: user.teamId },
});
if (!auth) return;
@ -29,7 +30,7 @@ router.post('hooks.unfurl', async ctx => {
let unfurls = {};
for (let link of event.links) {
const id = link.url.substr(link.url.lastIndexOf('/') + 1);
const doc = await Document.findById(id);
const doc = await Document.findByPk(id);
if (!doc || doc.teamId !== user.teamId) continue;
unfurls[link.url] = {
@ -60,7 +61,7 @@ router.post('hooks.interactive', async ctx => {
if (token !== process.env.SLACK_VERIFICATION_TOKEN)
throw new AuthenticationError('Invalid verification token');
const user = await User.find({
const user = await User.findOne({
where: { service: 'slack', serviceId: data.user.id },
});
if (!user) {
@ -73,12 +74,12 @@ router.post('hooks.interactive', async ctx => {
}
// we find the document based on the users teamId to ensure access
const document = await Document.find({
const document = await Document.findOne({
where: { id: data.callback_id, teamId: user.teamId },
});
if (!document) throw new InvalidRequestError('Invalid document');
const team = await Team.findById(user.teamId);
const team = await Team.findByPk(user.teamId);
// respond with a public message that will be posted in the original channel
ctx.body = {
@ -100,7 +101,7 @@ router.post('hooks.slack', async ctx => {
if (token !== process.env.SLACK_VERIFICATION_TOKEN)
throw new AuthenticationError('Invalid verification token');
const user = await User.find({
const user = await User.findOne({
where: {
service: 'slack',
serviceId: user_id,
@ -113,7 +114,7 @@ router.post('hooks.slack', async ctx => {
return;
}
const team = await Team.findById(user.teamId);
const team = await Team.findByPk(user.teamId);
const results = await Document.searchForUser(user, text, {
limit: 5,
});

View File

@ -33,7 +33,7 @@ router.post('integrations.delete', auth(), async ctx => {
ctx.assertUuid(id, 'id is required');
const user = ctx.state.user;
const integration = await Integration.findById(id);
const integration = await Integration.findByPk(id);
authorize(user, 'delete', integration);
await integration.destroy();

View File

@ -10,24 +10,39 @@ export default function pagination(options?: Object) {
) {
const opts = {
defaultLimit: 15,
defaultOffset: 0,
maxLimit: 100,
...options,
};
let query = ctx.request.query;
let body = ctx.request.body;
// $FlowFixMe
let limit = parseInt(query.limit || body.limit, 10);
// $FlowFixMe
let offset = parseInt(query.offset || body.offset, 10);
limit = isNaN(limit) ? opts.defaultLimit : limit;
offset = isNaN(offset) ? 0 : offset;
let body: Object = ctx.request.body;
let limit = query.limit || body.limit;
let offset = query.offset || body.offset;
if (limit && isNaN(limit)) {
throw new InvalidRequestError(`Pagination limit must be a valid number`);
}
if (offset && isNaN(offset)) {
throw new InvalidRequestError(`Pagination offset must be a valid number`);
}
limit = parseInt(limit || opts.defaultLimit, 10);
offset = parseInt(offset || opts.defaultOffset, 10);
if (limit > opts.maxLimit) {
throw new InvalidRequestError(
`Pagination limit is too large (max ${opts.maxLimit})`
);
}
if (limit <= 0) {
throw new InvalidRequestError(`Pagination limit must be greater than 0`);
}
if (offset < 0) {
throw new InvalidRequestError(
`Pagination offset must be greater than or equal to 0`
);
}
ctx.state.pagination = {
limit: limit,

View File

@ -0,0 +1,56 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from 'fetch-test-server';
import app from '../../app';
import { flushdb, seed } from '../../test/support';
const server = new TestServer(app.callback());
beforeEach(flushdb);
afterAll(server.close);
describe('#pagination', async () => {
it('should allow offset and limit', async () => {
const { user } = await seed();
const res = await server.post('/api/users.list', {
body: { token: user.getJwtToken(), limit: 1, offset: 1 },
});
expect(res.status).toEqual(200);
});
it('should not allow negative limit', async () => {
const { user } = await seed();
const res = await server.post('/api/users.list', {
body: { token: user.getJwtToken(), limit: -1 },
});
expect(res.status).toEqual(400);
});
it('should not allow non-integer limit', async () => {
const { user } = await seed();
const res = await server.post('/api/users.list', {
body: { token: user.getJwtToken(), limit: 'blah' },
});
expect(res.status).toEqual(400);
});
it('should not allow negative offset', async () => {
const { user } = await seed();
const res = await server.post('/api/users.list', {
body: { token: user.getJwtToken(), offset: -1 },
});
expect(res.status).toEqual(400);
});
it('should not allow non-integer offset', async () => {
const { user } = await seed();
const res = await server.post('/api/users.list', {
body: { token: user.getJwtToken(), offset: 'blah' },
});
expect(res.status).toEqual(400);
});
});

View File

@ -47,7 +47,7 @@ router.post('notificationSettings.delete', auth(), async ctx => {
ctx.assertUuid(id, 'id is required');
const user = ctx.state.user;
const setting = await NotificationSetting.findById(id);
const setting = await NotificationSetting.findByPk(id);
authorize(user, 'delete', setting);
await setting.destroy();
@ -62,7 +62,7 @@ router.post('notificationSettings.unsubscribe', async ctx => {
ctx.assertUuid(id, 'id is required');
ctx.assertPresent(token, 'token is required');
const setting = await NotificationSetting.findById(id);
const setting = await NotificationSetting.findByPk(id);
if (setting) {
if (token !== setting.unsubscribeToken) {
ctx.redirect(`${process.env.URL}?notice=invalid-auth`);

View File

@ -19,11 +19,12 @@ router.post('shares.list', auth(), pagination(), async ctx => {
const where = {
teamId: user.teamId,
userId: user.id,
// $FlowFixMe
revokedAt: { [Op.eq]: null },
};
if (user.isAdmin) delete where.userId;
if (user.isAdmin) {
delete where.userId;
}
const collectionIds = await user.collectionIds();
const shares = await Share.findAll({
@ -58,8 +59,8 @@ router.post('shares.create', auth(), async ctx => {
ctx.assertPresent(documentId, 'documentId is required');
const user = ctx.state.user;
const document = await Document.findById(documentId);
const team = await Team.findById(user.teamId);
const document = await Document.findByPk(documentId);
const team = await Team.findByPk(user.teamId);
authorize(user, 'share', document);
authorize(user, 'share', team);
@ -85,7 +86,7 @@ router.post('shares.revoke', auth(), async ctx => {
ctx.assertUuid(id, 'id is required');
const user = ctx.state.user;
const share = await Share.findById(id);
const share = await Share.findByPk(id);
authorize(user, 'revoke', share);
await share.revoke(user.id);

View File

@ -11,10 +11,11 @@ afterAll(server.close);
describe('#shares.list', async () => {
it('should only return shares created by user', async () => {
const { user, document } = await seed();
const { user, admin, document } = await seed();
await buildShare({
documentId: document.id,
teamId: user.teamId,
userId: admin.id,
});
const share = await buildShare({
documentId: document.id,
@ -51,10 +52,11 @@ describe('#shares.list', async () => {
});
it('admins should return shares created by all users', async () => {
const { admin, document } = await seed();
const { user, admin, document } = await seed();
const share = await buildShare({
documentId: document.id,
teamId: admin.teamId,
userId: user.id,
});
const res = await server.post('/api/shares.list', {
body: { token: admin.getJwtToken() },
@ -72,6 +74,7 @@ describe('#shares.list', async () => {
await buildShare({
documentId: document.id,
teamId: admin.teamId,
userId: admin.id,
});
collection.private = true;

View File

@ -15,7 +15,7 @@ router.post('team.update', auth(), async ctx => {
const endpoint = publicS3Endpoint();
const user = ctx.state.user;
const team = await Team.findById(user.teamId);
const team = await Team.findByPk(user.teamId);
authorize(user, 'update', team);
if (process.env.SUBDOMAINS_ENABLED === 'true') {

View File

@ -1,7 +1,13 @@
// @flow
import uuid from 'uuid';
import Router from 'koa-router';
import { makePolicy, signPolicy, publicS3Endpoint } from '../utils/s3';
import format from 'date-fns/format';
import {
makePolicy,
getSignature,
publicS3Endpoint,
makeCredential,
} from '../utils/s3';
import { ValidationError } from '../errors';
import { Event, User, Team } from '../models';
import auth from '../middlewares/authentication';
@ -63,7 +69,9 @@ router.post('users.s3Upload', auth(), async ctx => {
const s3Key = uuid.v4();
const key = `uploads/${ctx.state.user.id}/${s3Key}/${filename}`;
const policy = makePolicy();
const credential = makeCredential();
const longDate = format(new Date(), 'YYYYMMDDTHHmmss\\Z');
const policy = makePolicy(credential, longDate);
const endpoint = publicS3Endpoint();
const url = `${endpoint}/${key}`;
@ -84,13 +92,15 @@ router.post('users.s3Upload', auth(), async ctx => {
maxUploadSize: process.env.AWS_S3_UPLOAD_MAX_SIZE,
uploadUrl: endpoint,
form: {
AWSAccessKeyId: process.env.AWS_ACCESS_KEY_ID,
'Cache-Control': 'max-age=31557600',
'Content-Type': kind,
key,
acl: 'public-read',
signature: signPolicy(policy),
key,
policy,
'x-amz-algorithm': 'AWS4-HMAC-SHA256',
'x-amz-credential': credential,
'x-amz-date': longDate,
'x-amz-signature': getSignature(policy),
},
asset: {
contentType: kind,
@ -109,10 +119,10 @@ router.post('users.promote', auth(), async ctx => {
const teamId = ctx.state.user.teamId;
ctx.assertPresent(userId, 'id is required');
const user = await User.findById(userId);
const user = await User.findByPk(userId);
authorize(ctx.state.user, 'promote', user);
const team = await Team.findById(teamId);
const team = await Team.findByPk(teamId);
await team.addAdmin(user);
ctx.body = {
@ -125,10 +135,10 @@ router.post('users.demote', auth(), async ctx => {
const teamId = ctx.state.user.teamId;
ctx.assertPresent(userId, 'id is required');
const user = await User.findById(userId);
const user = await User.findByPk(userId);
authorize(ctx.state.user, 'demote', user);
const team = await Team.findById(teamId);
const team = await Team.findByPk(teamId);
try {
await team.removeAdmin(user);
} catch (err) {
@ -151,10 +161,10 @@ router.post('users.suspend', auth(), async ctx => {
const teamId = ctx.state.user.teamId;
ctx.assertPresent(userId, 'id is required');
const user = await User.findById(userId);
const user = await User.findByPk(userId);
authorize(ctx.state.user, 'suspend', user);
const team = await Team.findById(teamId);
const team = await Team.findByPk(teamId);
try {
await team.suspendUser(user, admin);
} catch (err) {
@ -178,10 +188,10 @@ router.post('users.activate', auth(), async ctx => {
const teamId = ctx.state.user.teamId;
ctx.assertPresent(userId, 'id is required');
const user = await User.findById(userId);
const user = await User.findByPk(userId);
authorize(ctx.state.user, 'activate', user);
const team = await Team.findById(teamId);
const team = await Team.findByPk(teamId);
await team.activateUser(user, admin);
ctx.body = {

View File

@ -13,7 +13,7 @@ router.post('views.list', auth(), async ctx => {
ctx.assertUuid(documentId, 'documentId is required');
const user = ctx.state.user;
const document = await Document.findById(documentId);
const document = await Document.findByPk(documentId);
authorize(user, 'read', document);
const views = await View.findAll({
@ -37,7 +37,7 @@ router.post('views.create', auth(), async ctx => {
ctx.assertUuid(documentId, 'documentId is required');
const user = ctx.state.user;
const document = await Document.findById(documentId);
const document = await Document.findByPk(documentId);
authorize(user, 'read', document);
await View.increment({ documentId, userId: user.id });

View File

@ -31,7 +31,7 @@ router.get('/redirect', auth(), async ctx => {
expires: addMonths(new Date(), 3),
});
const team = await Team.findById(user.teamId);
const team = await Team.findByPk(user.teamId);
ctx.redirect(`${team.url}/dashboard`);
});

View File

@ -89,7 +89,7 @@ router.get('slack.commands', auth({ required: false }), async ctx => {
if (!user) {
if (state) {
try {
const team = await Team.findById(state);
const team = await Team.findByPk(state);
return ctx.redirect(
`${team.url}/auth${ctx.request.path}?${ctx.request.querystring}`
);
@ -143,8 +143,8 @@ router.get('slack.post', auth({ required: false }), async ctx => {
// appropriate subdomain to complete the oauth flow
if (!user) {
try {
const collection = await Collection.findById(state);
const team = await Team.findById(collection.teamId);
const collection = await Collection.findByPk(state);
const team = await Team.findByPk(collection.teamId);
return ctx.redirect(
`${team.url}/auth${ctx.request.path}?${ctx.request.querystring}`
);

View File

@ -36,7 +36,7 @@ export default async function documentMover({
document.parentDocumentId = parentDocumentId;
const newCollection: Collection = collectionChanged
? await Collection.findById(collectionId, { transaction })
? await Collection.findByPk(collectionId, { transaction })
: collection;
await newCollection.addDocumentToStructure(document, index, {
documentJson,

View File

@ -53,7 +53,7 @@ if (process.env.WEBSOCKETS_ENABLED === 'true') {
// allow the client to request to join rooms based on
// new collections being created.
socket.on('join', async event => {
const collection = await Collection.findById(event.roomId);
const collection = await Collection.findByPk(event.roomId);
if (can(user, 'read', collection)) {
socket.join(`collection-${event.roomId}`);

View File

@ -18,7 +18,7 @@ const queueOptions = {
async function exportAndEmailCollection(collectionId: string, email: string) {
log('Archiving collection', collectionId);
const collection = await Collection.findById(collectionId);
const collection = await Collection.findByPk(collectionId);
const filePath = await archiveCollection(collection);
log('Archive path', filePath);
@ -36,7 +36,7 @@ async function exportAndEmailCollection(collectionId: string, email: string) {
async function exportAndEmailCollections(teamId: string, email: string) {
log('Archiving team', teamId);
const team = await Team.findById(teamId);
const team = await Team.findByPk(teamId);
const collections = await Collection.findAll({
where: { teamId },
order: [['name', 'ASC']],

View File

@ -57,7 +57,7 @@ export default function auth(options?: { required?: boolean } = {}) {
if (!apiKey) throw new AuthenticationError('Invalid API key');
user = await User.findById(apiKey.userId);
user = await User.findByPk(apiKey.userId);
if (!user) throw new AuthenticationError('Invalid API key');
} else {
// JWT

View File

@ -1,7 +1,15 @@
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.removeConstraint('users', 'email_unique_idx');
await queryInterface.removeConstraint('users', 'username_unique_idx');
await queryInterface.changeColumn('users', 'email', {
type: Sequelize.STRING,
unique: false,
allowNull: false,
});
await queryInterface.changeColumn('users', 'username', {
type: Sequelize.STRING,
unique: false,
allowNull: false,
});
},
down: async (queryInterface, Sequelize) => {

View File

@ -3,7 +3,6 @@ import { find, remove } from 'lodash';
import slug from 'slug';
import randomstring from 'randomstring';
import { DataTypes, sequelize } from '../sequelize';
import { asyncLock } from '../redis';
import Document from './Document';
import CollectionUser from './CollectionUser';
import { welcomeMessage } from '../utils/onboarding';
@ -143,53 +142,65 @@ Collection.prototype.addDocumentToStructure = async function(
) {
if (!this.documentStructure) return;
let unlock;
let transaction;
// documentStructure can only be updated by one request at a time
if (options.save !== false) {
unlock = await asyncLock(`collection-${this.id}`);
}
try {
// documentStructure can only be updated by one request at a time
if (options.save !== false) {
transaction = await sequelize.transaction();
}
// If moving existing document with children, use existing structure
const documentJson = {
...document.toJSON(),
...options.documentJson,
};
if (!document.parentDocumentId) {
// Note: Index is supported on DB level but it's being ignored
// by the API presentation until we build product support for it.
this.documentStructure.splice(
index !== undefined ? index : this.documentStructure.length,
0,
documentJson
);
} else {
// Recursively place document
const placeDocument = documentList => {
return documentList.map(childDocument => {
if (document.parentDocumentId === childDocument.id) {
childDocument.children.splice(
index !== undefined ? index : childDocument.children.length,
0,
documentJson
);
} else {
childDocument.children = placeDocument(childDocument.children);
}
return childDocument;
});
// If moving existing document with children, use existing structure
const documentJson = {
...document.toJSON(),
...options.documentJson,
};
this.documentStructure = placeDocument(this.documentStructure);
}
// Sequelize doesn't seem to set the value with splice on JSONB field
this.documentStructure = this.documentStructure;
if (!document.parentDocumentId) {
// Note: Index is supported on DB level but it's being ignored
// by the API presentation until we build product support for it.
this.documentStructure.splice(
index !== undefined ? index : this.documentStructure.length,
0,
documentJson
);
} else {
// Recursively place document
const placeDocument = documentList => {
return documentList.map(childDocument => {
if (document.parentDocumentId === childDocument.id) {
childDocument.children.splice(
index !== undefined ? index : childDocument.children.length,
0,
documentJson
);
} else {
childDocument.children = placeDocument(childDocument.children);
}
if (options.save !== false) {
await this.save(options);
if (unlock) unlock();
return childDocument;
});
};
this.documentStructure = placeDocument(this.documentStructure);
}
// Sequelize doesn't seem to set the value with splice on JSONB field
this.documentStructure = this.documentStructure;
if (options.save !== false) {
await this.save({
...options,
transaction,
});
if (transaction) {
await transaction.commit();
}
}
} catch (err) {
if (transaction) {
await transaction.rollback();
}
throw err;
}
return this;
@ -203,28 +214,39 @@ Collection.prototype.updateDocument = async function(
) {
if (!this.documentStructure) return;
// documentStructure can only be updated by one request at the time
const unlock = await asyncLock(`collection-${this.id}`);
let transaction;
const { id } = updatedDocument;
try {
// documentStructure can only be updated by one request at the time
transaction = await sequelize.transaction();
const updateChildren = documents => {
return documents.map(document => {
if (document.id === id) {
document = {
...updatedDocument.toJSON(),
children: document.children,
};
} else {
document.children = updateChildren(document.children);
}
return document;
});
};
const { id } = updatedDocument;
const updateChildren = documents => {
return documents.map(document => {
if (document.id === id) {
document = {
...updatedDocument.toJSON(),
children: document.children,
};
} else {
document.children = updateChildren(document.children);
}
return document;
});
};
this.documentStructure = updateChildren(this.documentStructure);
await this.save({ transaction });
await transaction.commit();
} catch (err) {
if (transaction) {
await transaction.rollback();
}
throw err;
}
this.documentStructure = updateChildren(this.documentStructure);
await this.save();
unlock();
return this;
};
@ -239,37 +261,48 @@ Collection.prototype.removeDocumentInStructure = async function(
) {
if (!this.documentStructure) return;
let returnValue;
let unlock;
let transaction;
// documentStructure can only be updated by one request at the time
unlock = await asyncLock(`collection-${this.id}`);
try {
// documentStructure can only be updated by one request at the time
transaction = await sequelize.transaction();
const removeFromChildren = async (children, id) => {
children = await Promise.all(
children.map(async childDocument => {
return {
...childDocument,
children: await removeFromChildren(childDocument.children, id),
};
})
const removeFromChildren = async (children, id) => {
children = await Promise.all(
children.map(async childDocument => {
return {
...childDocument,
children: await removeFromChildren(childDocument.children, id),
};
})
);
const match = find(children, { id });
if (match) {
if (!returnValue) returnValue = match;
remove(children, { id });
}
return children;
};
this.documentStructure = await removeFromChildren(
this.documentStructure,
document.id
);
const match = find(children, { id });
if (match) {
if (!returnValue) returnValue = match;
remove(children, { id });
await this.save({
...options,
transaction,
});
await transaction.commit();
} catch (err) {
if (transaction) {
await transaction.rollback();
}
return children;
};
this.documentStructure = await removeFromChildren(
this.documentStructure,
document.id
);
await this.save(options);
if (unlock) await unlock();
throw err;
}
return returnValue;
};

View File

@ -156,7 +156,6 @@ Document.associate = models => {
],
where: {
publishedAt: {
// $FlowFixMe
[Op.ne]: null,
},
},
@ -182,7 +181,7 @@ Document.associate = models => {
}));
};
Document.findById = async (id, options) => {
Document.findByPk = async (id, options) => {
const scope = Document.scope('withUnpublished');
if (isUUID(id)) {
@ -300,7 +299,7 @@ Document.searchForUser = async (
Document.addHook('beforeSave', async model => {
if (!model.publishedAt) return;
const collection = await Collection.findById(model.collectionId);
const collection = await Collection.findByPk(model.collectionId);
if (!collection || collection.type !== 'atlas') return;
await collection.updateDocument(model);
@ -310,7 +309,7 @@ Document.addHook('beforeSave', async model => {
Document.addHook('afterCreate', async model => {
if (!model.publishedAt) return;
const collection = await Collection.findById(model.collectionId);
const collection = await Collection.findByPk(model.collectionId);
if (!collection || collection.type !== 'atlas') return;
await collection.addDocumentToStructure(model);
@ -366,7 +365,7 @@ Document.prototype.archiveWithChildren = async function(userId, options) {
Document.prototype.publish = async function() {
if (this.publishedAt) return this.save();
const collection = await Collection.findById(this.collectionId);
const collection = await Collection.findByPk(this.collectionId);
if (collection.type !== 'atlas') return this.save();
await collection.addDocumentToStructure(this);
@ -402,7 +401,6 @@ Document.prototype.unarchive = async function(userId) {
where: {
id: this.parentDocumentId,
archivedAt: {
// $FlowFixMe
[Op.eq]: null,
},
},

View File

@ -131,7 +131,6 @@ Team.prototype.removeAdmin = async function(user: User) {
teamId: this.id,
isAdmin: true,
id: {
// $FlowFixMe
[Op.ne]: user.id,
},
},

View File

@ -159,7 +159,7 @@ User.beforeDestroy(removeIdentifyingInfo);
User.beforeSave(uploadAvatar);
User.beforeCreate(setRandomJwtSecret);
User.afterCreate(async user => {
const team = await Team.findById(user.teamId);
const team = await Team.findByPk(user.teamId);
sendEmail('welcome', user.email, { teamUrl: team.url });
});

View File

@ -337,6 +337,15 @@ export default function Api() {
</span>
}
/>
<Argument
id="append"
description={
<span>
Pass <Code>true</Code> to append the text parameter to the
end of the document rather than replace.
</span>
}
/>
<Argument
id="autosave"
description={
@ -351,7 +360,7 @@ export default function Api() {
description={
<span>
Pass <Code>true</Code> to signify the end of an editing
session. This will trigger documents.update hooks.
session. This will trigger update notifications.
</span>
}
/>

View File

@ -1,11 +1,6 @@
// @flow
import redis from 'redis';
import redisLock from 'redis-lock';
const client = redis.createClient(process.env.REDIS_URL);
const lock = redisLock(client);
const asyncLock = (lockName: string): * =>
new Promise(resolve => lock(lockName, unlock => resolve(unlock)));
export { client, asyncLock };
export { client };

View File

@ -113,7 +113,7 @@ router.get('/', async ctx => {
) {
const domain = parseDomain(ctx.request.hostname);
const subdomain = domain ? domain.subdomain : undefined;
const team = await Team.find({
const team = await Team.findOne({
where: { subdomain },
});
if (team) {

View File

@ -3,9 +3,10 @@ import Sequelize from 'sequelize';
import EncryptedField from 'sequelize-encrypted';
import debug from 'debug';
const secretKey = process.env.SECRET_KEY;
export const encryptedFields = EncryptedField(Sequelize, secretKey);
export const encryptedFields = EncryptedField(
Sequelize,
process.env.SECRET_KEY
);
export const DataTypes = Sequelize;
export const Op = Sequelize.Op;
@ -13,5 +14,4 @@ export const Op = Sequelize.Op;
export const sequelize = new Sequelize(process.env.DATABASE_URL, {
logging: debug('sql'),
typeValidation: true,
operatorsAliases: false,
});

View File

@ -23,7 +23,7 @@ export default class Notifications {
// wait until the user has finished editing
if (!event.done) return;
const document = await Document.findById(event.modelId);
const document = await Document.findByPk(event.modelId);
if (!document) return;
const { collection } = document;
@ -32,7 +32,6 @@ export default class Notifications {
const notificationSettings = await NotificationSetting.findAll({
where: {
userId: {
// $FlowFixMe
[Op.ne]: document.lastModifiedById,
},
teamId: document.teamId,
@ -73,7 +72,7 @@ export default class Notifications {
}
async collectionCreated(event: Event) {
const collection = await Collection.findById(event.modelId, {
const collection = await Collection.findByPk(event.modelId, {
include: [
{
model: User,
@ -88,7 +87,6 @@ export default class Notifications {
const notificationSettings = await NotificationSetting.findAll({
where: {
userId: {
// $FlowFixMe
[Op.ne]: collection.createdById,
},
teamId: collection.teamId,

View File

@ -60,7 +60,7 @@ export default class Slack {
// lets not send a notification on every autosave update
if (event.autosave) return;
const document = await Document.findById(event.modelId);
const document = await Document.findByPk(event.modelId);
if (!document) return;
// never send information on draft documents
@ -76,7 +76,7 @@ export default class Slack {
});
if (!integration) return;
const team = await Team.findById(document.teamId);
const team = await Team.findByPk(document.teamId);
let text = `${document.createdBy.name} published a new document`;

View File

@ -17,7 +17,7 @@ export default class Websockets {
case 'documents.unpin':
case 'documents.update':
case 'documents.delete': {
const document = await Document.findById(event.modelId, {
const document = await Document.findByPk(event.modelId, {
paranoid: false,
});
const documents = [await presentDocument(document)];
@ -32,7 +32,7 @@ export default class Websockets {
});
}
case 'documents.create': {
const document = await Document.findById(event.modelId);
const document = await Document.findByPk(event.modelId);
const documents = [await presentDocument(document)];
const collections = [await presentCollection(document.collection)];
@ -78,7 +78,7 @@ export default class Websockets {
return;
}
case 'collections.create': {
const collection = await Collection.findById(event.modelId, {
const collection = await Collection.findByPk(event.modelId, {
paranoid: false,
});
const collections = [await presentCollection(collection)];
@ -106,7 +106,7 @@ export default class Websockets {
}
case 'collections.update':
case 'collections.delete': {
const collection = await Collection.findById(event.modelId, {
const collection = await Collection.findByPk(event.modelId, {
paranoid: false,
});
const collections = [await presentCollection(collection)];

View File

@ -23,21 +23,6 @@ const seed = async () => {
},
});
const user = await User.create({
id: '46fde1d4-0050-428f-9f0b-0bf77f4bdf61',
email: 'user1@example.com',
username: 'user1',
name: 'User 1',
teamId: team.id,
service: 'slack',
serviceId: 'U2399UF2P',
slackData: {
id: 'U2399UF2P',
image_192: 'http://example.com/avatar.png',
},
createdAt: new Date('2018-01-01T00:00:00.000Z'),
});
const admin = await User.create({
id: 'fa952cff-fa64-4d42-a6ea-6955c9689046',
email: 'admin@example.com',
@ -54,6 +39,21 @@ const seed = async () => {
createdAt: new Date('2018-01-01T00:00:00.000Z'),
});
const user = await User.create({
id: '46fde1d4-0050-428f-9f0b-0bf77f4bdf61',
email: 'user1@example.com',
username: 'user1',
name: 'User 1',
teamId: team.id,
service: 'slack',
serviceId: 'U2399UF2P',
slackData: {
id: 'U2399UF2P',
image_192: 'http://example.com/avatar.png',
},
createdAt: new Date('2018-01-02T00:00:00.000Z'),
});
const collection = await Collection.create({
id: '26fde1d4-0050-428f-9f0b-0bf77f8bdf62',
name: 'Collection',

View File

@ -13,7 +13,7 @@ export async function getUserForJWT(token: string) {
if (!payload) throw new AuthenticationError('Invalid token');
const user = await User.findById(payload.id);
const user = await User.findByPk(payload.id);
try {
JWT.verify(token, user.jwtSecret);

View File

@ -8,9 +8,29 @@ import fetch from 'isomorphic-fetch';
import bugsnag from 'bugsnag';
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY;
const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID;
const AWS_REGION = process.env.AWS_REGION;
const AWS_S3_UPLOAD_BUCKET_NAME = process.env.AWS_S3_UPLOAD_BUCKET_NAME;
export const makePolicy = () => {
const hmac = (key: string, message: string, encoding: any) => {
return crypto
.createHmac('sha256', key)
.update(message, 'utf8')
.digest(encoding);
};
export const makeCredential = () => {
const credential =
AWS_ACCESS_KEY_ID +
'/' +
format(new Date(), 'YYYYMMDD') +
'/' +
AWS_REGION +
'/s3/aws4_request';
return credential;
};
export const makePolicy = (credential: string, longDate: string) => {
const tomorrow = addHours(new Date(), 24);
const policy = {
conditions: [
@ -20,6 +40,9 @@ export const makePolicy = () => {
['content-length-range', 0, +process.env.AWS_S3_UPLOAD_MAX_SIZE],
['starts-with', '$Content-Type', 'image'],
['starts-with', '$Cache-Control', ''],
{ 'x-amz-algorithm': 'AWS4-HMAC-SHA256' },
{ 'x-amz-credential': credential },
{ 'x-amz-date': longDate },
],
expiration: format(tomorrow, 'YYYY-MM-DDTHH:mm:ss\\Z'),
};
@ -27,13 +50,16 @@ export const makePolicy = () => {
return new Buffer(JSON.stringify(policy)).toString('base64');
};
export const signPolicy = (policy: any) => {
invariant(AWS_SECRET_ACCESS_KEY, 'AWS_SECRET_ACCESS_KEY not set');
const signature = crypto
.createHmac('sha1', AWS_SECRET_ACCESS_KEY)
.update(policy)
.digest('base64');
export const getSignature = (policy: any) => {
const kDate = hmac(
'AWS4' + AWS_SECRET_ACCESS_KEY,
format(new Date(), 'YYYYMMDD')
);
const kRegion = hmac(kDate, AWS_REGION);
const kService = hmac(kRegion, 's3');
const kCredentials = hmac(kService, 'aws4_request');
const signature = hmac(kCredentials, policy, 'hex');
return signature;
};

View File

@ -7,7 +7,7 @@ import { Collection, Document } from '../models';
async function addToArchive(zip, documents) {
for (const doc of documents) {
const document = await Document.findById(doc.id);
const document = await Document.findByPk(doc.id);
zip.file(`${document.title}.md`, unescape(document.text));

205
yarn.lock
View File

@ -132,10 +132,6 @@
version "0.19.0-0"
resolved "https://registry.yarnpkg.com/@tommoor/slate-edit-list/-/slate-edit-list-0.19.0-0.tgz#972a714e9ea4cdf47a530d5702a904f5547be2dd"
"@types/geojson@^1.0.0":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-1.0.6.tgz#3e02972728c69248c2af08d60a48cbb8680fffdf"
"@types/node@*":
version "11.10.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.10.4.tgz#3f5fc4f0f322805f009e00ab35a2ff3d6b778e42"
@ -304,6 +300,10 @@ ansi-regex@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.0.0.tgz#70de791edf021404c3fd615aa89118ae0432e5a9"
ansi-regex@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
ansi-styles@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
@ -314,7 +314,7 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1:
dependencies:
color-convert "^1.9.0"
any-promise@^1.0.0, any-promise@^1.1.0:
any-promise@^1.0.0, any-promise@^1.1.0, any-promise@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
@ -1287,10 +1287,14 @@ blob@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683"
bluebird@^3.3.5, bluebird@^3.4.6, bluebird@^3.5.1, bluebird@^3.5.3:
bluebird@^3.3.5, bluebird@^3.5.1, bluebird@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7"
bluebird@^3.5.0:
version "3.5.5"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
bluebird@~3.4.1:
version "3.4.7"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
@ -1877,6 +1881,14 @@ cliui@^4.0.0:
strip-ansi "^4.0.0"
wrap-ansi "^2.0.0"
cliui@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
dependencies:
string-width "^3.1.0"
strip-ansi "^5.2.0"
wrap-ansi "^5.1.0"
clone-buffer@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
@ -1901,7 +1913,7 @@ cloneable-readable@^1.0.0:
process-nextick-args "^2.0.0"
readable-stream "^2.3.5"
cls-bluebird@^2.0.1:
cls-bluebird@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cls-bluebird/-/cls-bluebird-2.1.0.tgz#37ef1e080a8ffb55c2f4164f536f1919e7968aee"
dependencies:
@ -2437,7 +2449,7 @@ debug@^2.1.2, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.1, debug@^2.6
dependencies:
ms "2.0.0"
debug@^3.0.0, debug@^3.1.0, debug@^3.2.6:
debug@^3.1.0, debug@^3.2.6:
version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
dependencies:
@ -2528,7 +2540,7 @@ depd@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
depd@^1.1.0, depd@^1.1.2, depd@~1.1.2:
depd@^1.1.2, depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
@ -3671,14 +3683,14 @@ generic-pool@2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-2.4.3.tgz#780c36f69dfad05a5a045dd37be7adca11a4f6ff"
generic-pool@^3.1.8:
version "3.6.1"
resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.6.1.tgz#a51a8439ee86f0bbcf100fc1db3f45c86289deb4"
get-caller-file@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
get-caller-file@^2.0.1:
version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
get-document@1:
version "1.0.0"
resolved "https://registry.yarnpkg.com/get-document/-/get-document-1.0.0.tgz#4821bce66f1c24cb0331602be6cb6b12c4f01c4b"
@ -5665,7 +5677,7 @@ lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
"lodash@>=3.5 <5", lodash@^4.1.1, lodash@^4.11.1, lodash@^4.17.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.6.1:
"lodash@>=3.5 <5", lodash@^4.1.1, lodash@^4.11.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.6.1:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
@ -5993,13 +6005,19 @@ mobx@4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/mobx/-/mobx-4.6.0.tgz#88a8ed21ff81b8861778c4b0d38e3dcdd1a7ddde"
moment-timezone@^0.5.23, moment-timezone@^0.5.4:
moment-timezone@^0.5.21:
version "0.5.25"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.25.tgz#a11bfa2f74e088327f2cd4c08b3e7bdf55957810"
dependencies:
moment ">= 2.9.0"
moment-timezone@^0.5.23:
version "0.5.23"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.23.tgz#7cbb00db2c14c71b19303cb47b0fb0a6d8651463"
dependencies:
moment ">= 2.9.0"
"moment@>= 2.9.0", moment@^2.13.0:
"moment@>= 2.9.0", moment@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
@ -6415,7 +6433,7 @@ os-locale@^2.0.0:
lcid "^1.0.0"
mem "^1.1.0"
os-locale@^3.0.0:
os-locale@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
dependencies:
@ -7572,10 +7590,6 @@ redis-errors@^1.0.0, redis-errors@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
redis-lock@^0.1.0:
version "0.1.4"
resolved "https://registry.yarnpkg.com/redis-lock/-/redis-lock-0.1.4.tgz#e83590bee22b5f01cdb65bfbd88d988045356272"
redis-parser@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b"
@ -7791,6 +7805,10 @@ require-main-filename@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
require-main-filename@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
require-package-name@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/require-package-name/-/require-package-name-2.0.1.tgz#c11e97276b65b8e2923f75dabf5fb2ef0c3841b9"
@ -7845,20 +7863,19 @@ ret@~0.1.10:
version "0.1.15"
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
retry-as-promised@^2.3.1:
version "2.3.2"
resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-2.3.2.tgz#cd974ee4fd9b5fe03cbf31871ee48221c07737b7"
retry-as-promised@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-3.2.0.tgz#769f63d536bec4783549db0777cb56dadd9d8543"
dependencies:
bluebird "^3.4.6"
debug "^2.6.9"
any-promise "^1.3.0"
retry-axios@0.3.2, retry-axios@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/retry-axios/-/retry-axios-0.3.2.tgz#5757c80f585b4cc4c4986aa2ffd47a60c6d35e13"
rich-markdown-editor@^9.5.1:
version "9.5.1"
resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-9.5.1.tgz#c595f35e3cb73bff97a28102a277004a8629c157"
rich-markdown-editor@^9.6.1:
version "9.6.1"
resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-9.6.1.tgz#1f72f06fcc324fb66b8670dfffba99895c1dd47d"
dependencies:
"@domoinc/slate-edit-table" "^0.22.2"
"@tommoor/slate-edit-list" "0.19.0-0"
@ -8030,7 +8047,7 @@ semver-diff@^2.0.0:
dependencies:
semver "^5.0.3"
"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
@ -8042,6 +8059,10 @@ semver@5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
semver@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.1.tgz#53f53da9b30b2103cd4f15eab3a18ecbcb210c9b"
sentence-case@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-2.1.1.tgz#1f6e2dda39c168bf92d13f86d4a918933f667ed4"
@ -8049,9 +8070,9 @@ sentence-case@^2.1.0:
no-case "^2.2.0"
upper-case-first "^1.1.2"
sequelize-cli@^5.4.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/sequelize-cli/-/sequelize-cli-5.4.0.tgz#6a2c2af331466414d8b2ecb6912e24d2de0d04b5"
sequelize-cli@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/sequelize-cli/-/sequelize-cli-5.5.0.tgz#b0570352f70eaa489a25dccf55cf316675d6ff06"
dependencies:
bluebird "^3.5.3"
cli-color "^1.4.0"
@ -8060,33 +8081,35 @@ sequelize-cli@^5.4.0:
lodash "^4.17.5"
resolve "^1.5.0"
umzug "^2.1.0"
yargs "^12.0.5"
yargs "^13.1.0"
sequelize-encrypted@0.1.0:
sequelize-encrypted@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/sequelize-encrypted/-/sequelize-encrypted-0.1.0.tgz#f9c7a94dc1b4413e1347a49f06cd07b7f3bf9916"
sequelize@4.28.6:
version "4.28.6"
resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-4.28.6.tgz#44b4b69f550bc53f41135bf8db73c5d492cb7e64"
sequelize-pool@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-2.2.0.tgz#fd4eb05ccefb5df5c23d2cc6fd934c20fd9c5dab"
sequelize@^5.8.12:
version "5.8.12"
resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-5.8.12.tgz#91f46f16789307d40c68f8c039c10d1d0f230ad2"
dependencies:
bluebird "^3.4.6"
cls-bluebird "^2.0.1"
debug "^3.0.0"
depd "^1.1.0"
bluebird "^3.5.0"
cls-bluebird "^2.1.0"
debug "^4.1.1"
dottie "^2.0.0"
generic-pool "^3.1.8"
inflection "1.12.0"
lodash "^4.17.1"
moment "^2.13.0"
moment-timezone "^0.5.4"
retry-as-promised "^2.3.1"
semver "^5.0.1"
terraformer-wkt-parser "^1.1.2"
lodash "^4.17.11"
moment "^2.24.0"
moment-timezone "^0.5.21"
retry-as-promised "^3.1.0"
semver "^6.1.1"
sequelize-pool "^2.2.0"
toposort-class "^1.0.1"
uuid "^3.0.0"
validator "^9.1.0"
wkx "^0.4.1"
uuid "^3.2.1"
validator "^10.11.0"
wkx "^0.4.6"
serialize-javascript@^1.4.0:
version "1.6.1"
@ -8623,6 +8646,14 @@ string-width@^3.0.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.0.0"
string-width@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
dependencies:
emoji-regex "^7.0.1"
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0"
string_decoder@^1.0.0, string_decoder@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
@ -8657,6 +8688,12 @@ strip-ansi@^5.0.0:
dependencies:
ansi-regex "^4.0.0"
strip-ansi@^5.1.0, strip-ansi@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
dependencies:
ansi-regex "^4.1.0"
strip-bom@3.0.0, strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
@ -8808,19 +8845,6 @@ term-size@^1.2.0:
dependencies:
execa "^0.7.0"
terraformer-wkt-parser@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/terraformer-wkt-parser/-/terraformer-wkt-parser-1.2.0.tgz#c9d6ac3dff25f4c0bd344e961f42694961834c34"
dependencies:
"@types/geojson" "^1.0.0"
terraformer "~1.0.5"
terraformer@~1.0.5:
version "1.0.9"
resolved "https://registry.yarnpkg.com/terraformer/-/terraformer-1.0.9.tgz#77851fef4a49c90b345dc53cf26809fdf29dcda6"
optionalDependencies:
"@types/geojson" "^1.0.0"
test-exclude@^4.2.1:
version "4.2.3"
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.3.tgz#a9a5e64474e4398339245a0a769ad7c2f4a97c20"
@ -9385,7 +9409,7 @@ uuid@2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.2.tgz#48bd5698f0677e3c7901a1c46ef15b1643794726"
uuid@3.3.2, uuid@^3.0.0, uuid@^3.2.1, uuid@^3.3.2:
uuid@3.3.2, uuid@^3.2.1, uuid@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
@ -9400,9 +9424,9 @@ validator@5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-5.2.0.tgz#e66fb3ec352348c1f7232512328738d8d66a9689"
validator@^9.1.0:
version "9.4.1"
resolved "https://registry.yarnpkg.com/validator/-/validator-9.4.1.tgz#abf466d398b561cd243050112c6ff1de6cc12663"
validator@^10.11.0:
version "10.11.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228"
value-equal@^0.4.0:
version "0.4.0"
@ -9644,9 +9668,9 @@ windows-release@^3.1.0:
dependencies:
execa "^0.10.0"
wkx@^0.4.1:
version "0.4.6"
resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.4.6.tgz#228ab592e6457382ea6fb79fc825058d07fce523"
wkx@^0.4.6:
version "0.4.7"
resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.4.7.tgz#ba0e4f9e785e95c9975856c1834f19a95c65cfb5"
dependencies:
"@types/node" "*"
@ -9675,6 +9699,14 @@ wrap-ansi@^2.0.0:
string-width "^1.0.1"
strip-ansi "^3.0.1"
wrap-ansi@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
dependencies:
ansi-styles "^3.2.0"
string-width "^3.0.0"
strip-ansi "^5.0.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@ -9748,7 +9780,7 @@ y18n@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0:
y18n@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
@ -9760,9 +9792,9 @@ yallist@^3.0.0, yallist@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9"
yargs-parser@^11.1.1:
version "11.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
yargs-parser@^13.1.0:
version "13.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0"
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"
@ -9803,22 +9835,21 @@ yargs@^10.0.3:
y18n "^3.2.1"
yargs-parser "^8.1.0"
yargs@^12.0.5:
version "12.0.5"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
yargs@^13.1.0:
version "13.2.4"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83"
dependencies:
cliui "^4.0.0"
decamelize "^1.2.0"
cliui "^5.0.0"
find-up "^3.0.0"
get-caller-file "^1.0.1"
os-locale "^3.0.0"
get-caller-file "^2.0.1"
os-locale "^3.1.0"
require-directory "^2.1.1"
require-main-filename "^1.0.1"
require-main-filename "^2.0.0"
set-blocking "^2.0.0"
string-width "^2.0.0"
string-width "^3.0.0"
which-module "^2.0.0"
y18n "^3.2.1 || ^4.0.0"
yargs-parser "^11.1.1"
y18n "^4.0.0"
yargs-parser "^13.1.0"
yargs@^4.2.0:
version "4.8.1"