chore: API Consistency (#1304)
* chore: Addressing API inconsistencies * lint * add: Missing sort to groups.list fix: Documention issues * test: fix * feat: Add missing shares.info endpoint * feat: Add sorting to users.list endpoint * fix: Incorrect pagination parameters listed on user endpoints * users.s3Upload -> attachments.create * chore: exportAll -> export_all
This commit is contained in:
@ -42,7 +42,7 @@ class DocumentHistory extends React.Component<Props> {
|
|||||||
const results = await this.props.revisions.fetchPage({
|
const results = await this.props.revisions.fetchPage({
|
||||||
limit,
|
limit,
|
||||||
offset: this.offset,
|
offset: this.offset,
|
||||||
id: this.props.match.params.documentSlug,
|
documentId: this.props.match.params.documentSlug,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -267,7 +267,7 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
collection.id
|
collection.id
|
||||||
)}
|
)}
|
||||||
fetch={documents.fetchAlphabetical}
|
fetch={documents.fetchAlphabetical}
|
||||||
options={{ collection: collection.id }}
|
options={{ collectionId: collection.id }}
|
||||||
showPin
|
showPin
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
@ -278,7 +278,7 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
collection.id
|
collection.id
|
||||||
)}
|
)}
|
||||||
fetch={documents.fetchLeastRecentlyUpdated}
|
fetch={documents.fetchLeastRecentlyUpdated}
|
||||||
options={{ collection: collection.id }}
|
options={{ collectionId: collection.id }}
|
||||||
showPin
|
showPin
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
@ -289,7 +289,7 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
collection.id
|
collection.id
|
||||||
)}
|
)}
|
||||||
fetch={documents.fetchRecentlyPublished}
|
fetch={documents.fetchRecentlyPublished}
|
||||||
options={{ collection: collection.id }}
|
options={{ collectionId: collection.id }}
|
||||||
showPublished
|
showPublished
|
||||||
showPin
|
showPin
|
||||||
/>
|
/>
|
||||||
@ -300,7 +300,7 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
collection.id
|
collection.id
|
||||||
)}
|
)}
|
||||||
fetch={documents.fetchRecentlyUpdated}
|
fetch={documents.fetchRecentlyUpdated}
|
||||||
options={{ collection: collection.id }}
|
options={{ collectionId: collection.id }}
|
||||||
showPin
|
showPin
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
@ -97,11 +97,8 @@ class DataLoader extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadRevision = async () => {
|
loadRevision = async () => {
|
||||||
const { documentSlug, revisionId } = this.props.match.params;
|
const { revisionId } = this.props.match.params;
|
||||||
|
this.revision = await this.props.revisions.fetch(revisionId);
|
||||||
this.revision = await this.props.revisions.fetch(documentSlug, {
|
|
||||||
revisionId,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
loadDocument = async () => {
|
loadDocument = async () => {
|
||||||
|
@ -105,6 +105,6 @@ export default class CollectionsStore extends BaseStore<Collection> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export = () => {
|
export = () => {
|
||||||
return client.post('/collections.exportAll');
|
return client.post('/collections.export_all');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -20,21 +20,16 @@ export default class RevisionsStore extends BaseStore<Revision> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
fetch = async (
|
fetch = async (id: string, options?: FetchOptions): Promise<?Revision> => {
|
||||||
documentId: string,
|
|
||||||
options?: FetchOptions
|
|
||||||
): Promise<?Revision> => {
|
|
||||||
this.isFetching = true;
|
this.isFetching = true;
|
||||||
const id = options && options.revisionId;
|
invariant(id, 'Id is required');
|
||||||
if (!id) throw new Error('revisionId is required');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rev = this.data.get(id);
|
const rev = this.data.get(id);
|
||||||
if (rev) return rev;
|
if (rev) return rev;
|
||||||
|
|
||||||
const res = await client.post('/documents.revision', {
|
const res = await client.post('/revisions.info', {
|
||||||
id: documentId,
|
id,
|
||||||
revisionId: id,
|
|
||||||
});
|
});
|
||||||
invariant(res && res.data, 'Revision not available');
|
invariant(res && res.data, 'Revision not available');
|
||||||
this.add(res.data);
|
this.add(res.data);
|
||||||
@ -54,7 +49,7 @@ export default class RevisionsStore extends BaseStore<Revision> {
|
|||||||
this.isFetching = true;
|
this.isFetching = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await client.post('/documents.revisions', options);
|
const res = await client.post('/revisions.list', options);
|
||||||
invariant(res && res.data, 'Document revisions not available');
|
invariant(res && res.data, 'Document revisions not available');
|
||||||
runInAction('RevisionsStore#fetchPage', () => {
|
runInAction('RevisionsStore#fetchPage', () => {
|
||||||
res.data.forEach(revision => this.add(revision));
|
res.data.forEach(revision => this.add(revision));
|
||||||
|
@ -13,7 +13,7 @@ export const uploadFile = async (
|
|||||||
options?: Options = { name: '' }
|
options?: Options = { name: '' }
|
||||||
) => {
|
) => {
|
||||||
const name = file instanceof File ? file.name : options.name;
|
const name = file instanceof File ? file.name : options.name;
|
||||||
const response = await client.post('/users.s3Upload', {
|
const response = await client.post('/attachments.create', {
|
||||||
public: options.public,
|
public: options.public,
|
||||||
documentId: options.documentId,
|
documentId: options.documentId,
|
||||||
contentType: file.type,
|
contentType: file.type,
|
||||||
@ -24,7 +24,7 @@ export const uploadFile = async (
|
|||||||
invariant(response, 'Response should be available');
|
invariant(response, 'Response should be available');
|
||||||
|
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
const asset = data.asset;
|
const attachment = data.attachment;
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
for (const key in data.form) {
|
for (const key in data.form) {
|
||||||
@ -44,7 +44,7 @@ export const uploadFile = async (
|
|||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
return asset;
|
return attachment;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dataUrlToBlob = (dataURL: string) => {
|
export const dataUrlToBlob = (dataURL: string) => {
|
||||||
|
@ -43,7 +43,7 @@ Object {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#collections.exportAll should require authentication 1`] = `
|
exports[`#collections.export_all should require authentication 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"error": "authentication_required",
|
"error": "authentication_required",
|
||||||
"message": "Authentication required",
|
"message": "Authentication required",
|
||||||
|
@ -1,13 +1,92 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import Router from 'koa-router';
|
import Router from 'koa-router';
|
||||||
|
import uuid from 'uuid';
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
import { Attachment, Document, Event } from '../models';
|
||||||
|
import {
|
||||||
|
makePolicy,
|
||||||
|
getSignature,
|
||||||
|
publicS3Endpoint,
|
||||||
|
makeCredential,
|
||||||
|
getSignedImageUrl,
|
||||||
|
} from '../utils/s3';
|
||||||
import auth from '../middlewares/authentication';
|
import auth from '../middlewares/authentication';
|
||||||
import { Attachment, Document } from '../models';
|
|
||||||
import { getSignedImageUrl } from '../utils/s3';
|
|
||||||
import { NotFoundError } from '../errors';
|
import { NotFoundError } from '../errors';
|
||||||
import policy from '../policies';
|
import policy from '../policies';
|
||||||
|
|
||||||
const { authorize } = policy;
|
const { authorize } = policy;
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
const AWS_S3_ACL = process.env.AWS_S3_ACL || 'private';
|
||||||
|
|
||||||
|
router.post('attachments.create', auth(), async ctx => {
|
||||||
|
let { name, documentId, contentType, size } = ctx.body;
|
||||||
|
|
||||||
|
ctx.assertPresent(name, 'name is required');
|
||||||
|
ctx.assertPresent(contentType, 'contentType is required');
|
||||||
|
ctx.assertPresent(size, 'size is required');
|
||||||
|
|
||||||
|
const { user } = ctx.state;
|
||||||
|
const s3Key = uuid.v4();
|
||||||
|
const key = `uploads/${user.id}/${s3Key}/${name}`;
|
||||||
|
const acl =
|
||||||
|
ctx.body.public === undefined
|
||||||
|
? AWS_S3_ACL
|
||||||
|
: ctx.body.public ? 'public-read' : 'private';
|
||||||
|
const credential = makeCredential();
|
||||||
|
const longDate = format(new Date(), 'YYYYMMDDTHHmmss\\Z');
|
||||||
|
const policy = makePolicy(credential, longDate, acl);
|
||||||
|
const endpoint = publicS3Endpoint();
|
||||||
|
const url = `${endpoint}/${key}`;
|
||||||
|
|
||||||
|
if (documentId) {
|
||||||
|
const document = await Document.findByPk(documentId, { userId: user.id });
|
||||||
|
authorize(user, 'update', document);
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachment = await Attachment.create({
|
||||||
|
key,
|
||||||
|
acl,
|
||||||
|
size,
|
||||||
|
url,
|
||||||
|
contentType,
|
||||||
|
documentId,
|
||||||
|
teamId: user.teamId,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Event.create({
|
||||||
|
name: 'attachments.create',
|
||||||
|
data: { name },
|
||||||
|
teamId: user.teamId,
|
||||||
|
userId: user.id,
|
||||||
|
ip: ctx.request.ip,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
data: {
|
||||||
|
maxUploadSize: process.env.AWS_S3_UPLOAD_MAX_SIZE,
|
||||||
|
uploadUrl: endpoint,
|
||||||
|
form: {
|
||||||
|
'Cache-Control': 'max-age=31557600',
|
||||||
|
'Content-Type': contentType,
|
||||||
|
acl,
|
||||||
|
key,
|
||||||
|
policy,
|
||||||
|
'x-amz-algorithm': 'AWS4-HMAC-SHA256',
|
||||||
|
'x-amz-credential': credential,
|
||||||
|
'x-amz-date': longDate,
|
||||||
|
'x-amz-signature': getSignature(policy),
|
||||||
|
},
|
||||||
|
attachment: {
|
||||||
|
documentId,
|
||||||
|
contentType,
|
||||||
|
name,
|
||||||
|
url: attachment.redirectUrl,
|
||||||
|
size,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
router.post('attachments.redirect', auth(), async ctx => {
|
router.post('attachments.redirect', auth(), async ctx => {
|
||||||
const { id } = ctx.body;
|
const { id } = ctx.body;
|
||||||
|
@ -410,7 +410,7 @@ router.post('collections.export', auth(), async ctx => {
|
|||||||
ctx.body = fs.createReadStream(filePath);
|
ctx.body = fs.createReadStream(filePath);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('collections.exportAll', auth(), async ctx => {
|
router.post('collections.export_all', auth(), async ctx => {
|
||||||
const { download = false } = ctx.body;
|
const { download = false } = ctx.body;
|
||||||
|
|
||||||
const user = ctx.state.user;
|
const user = ctx.state.user;
|
||||||
|
@ -176,9 +176,9 @@ describe('#collections.export', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#collections.exportAll', async () => {
|
describe('#collections.export_all', async () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const res = await server.post('/api/collections.exportAll');
|
const res = await server.post('/api/collections.export_all');
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
|
|
||||||
expect(res.status).toEqual(401);
|
expect(res.status).toEqual(401);
|
||||||
@ -187,7 +187,7 @@ describe('#collections.exportAll', async () => {
|
|||||||
|
|
||||||
it('should require authorization', async () => {
|
it('should require authorization', async () => {
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const res = await server.post('/api/collections.exportAll', {
|
const res = await server.post('/api/collections.export_all', {
|
||||||
body: { token: user.getJwtToken() },
|
body: { token: user.getJwtToken() },
|
||||||
});
|
});
|
||||||
expect(res.status).toEqual(403);
|
expect(res.status).toEqual(403);
|
||||||
@ -195,7 +195,7 @@ describe('#collections.exportAll', async () => {
|
|||||||
|
|
||||||
it('should return success', async () => {
|
it('should return success', async () => {
|
||||||
const { admin } = await seed();
|
const { admin } = await seed();
|
||||||
const res = await server.post('/api/collections.exportAll', {
|
const res = await server.post('/api/collections.export_all', {
|
||||||
body: { token: admin.getJwtToken() },
|
body: { token: admin.getJwtToken() },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -204,7 +204,7 @@ describe('#collections.exportAll', async () => {
|
|||||||
|
|
||||||
it('should allow downloading directly', async () => {
|
it('should allow downloading directly', async () => {
|
||||||
const { admin } = await seed();
|
const { admin } = await seed();
|
||||||
const res = await server.post('/api/collections.exportAll', {
|
const res = await server.post('/api/collections.export_all', {
|
||||||
body: { token: admin.getJwtToken(), download: true },
|
body: { token: admin.getJwtToken(), download: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import documentMover from '../commands/documentMover';
|
|||||||
import {
|
import {
|
||||||
presentDocument,
|
presentDocument,
|
||||||
presentCollection,
|
presentCollection,
|
||||||
presentRevision,
|
|
||||||
presentPolicies,
|
presentPolicies,
|
||||||
} from '../presenters';
|
} from '../presenters';
|
||||||
import {
|
import {
|
||||||
@ -31,8 +30,10 @@ const router = new Router();
|
|||||||
|
|
||||||
router.post('documents.list', auth(), pagination(), async ctx => {
|
router.post('documents.list', auth(), pagination(), async ctx => {
|
||||||
const { sort = 'updatedAt', backlinkDocumentId, parentDocumentId } = ctx.body;
|
const { sort = 'updatedAt', backlinkDocumentId, parentDocumentId } = ctx.body;
|
||||||
const collectionId = ctx.body.collection;
|
|
||||||
const createdById = ctx.body.user;
|
// collection and user are here for backwards compatablity
|
||||||
|
const collectionId = ctx.body.collectionId || ctx.body.collection;
|
||||||
|
const createdById = ctx.body.userId || ctx.body.user;
|
||||||
let direction = ctx.body.direction;
|
let direction = ctx.body.direction;
|
||||||
if (direction !== 'ASC') direction = 'DESC';
|
if (direction !== 'ASC') direction = 'DESC';
|
||||||
|
|
||||||
@ -411,54 +412,6 @@ router.post('documents.info', auth({ required: false }), async ctx => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('documents.revision', auth(), async ctx => {
|
|
||||||
let { id, revisionId } = ctx.body;
|
|
||||||
ctx.assertPresent(id, 'id is required');
|
|
||||||
ctx.assertPresent(revisionId, 'revisionId is required');
|
|
||||||
|
|
||||||
const user = ctx.state.user;
|
|
||||||
const document = await Document.findByPk(id, { userId: user.id });
|
|
||||||
authorize(user, 'read', document);
|
|
||||||
|
|
||||||
const revision = await Revision.findOne({
|
|
||||||
where: {
|
|
||||||
id: revisionId,
|
|
||||||
documentId: document.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.body = {
|
|
||||||
pagination: ctx.state.pagination,
|
|
||||||
data: await presentRevision(revision),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
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 user = ctx.state.user;
|
|
||||||
const document = await Document.findByPk(id, { userId: user.id });
|
|
||||||
authorize(user, 'read', document);
|
|
||||||
|
|
||||||
const revisions = await Revision.findAll({
|
|
||||||
where: { documentId: document.id },
|
|
||||||
order: [[sort, direction]],
|
|
||||||
offset: ctx.state.pagination.offset,
|
|
||||||
limit: ctx.state.pagination.limit,
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await Promise.all(
|
|
||||||
revisions.map(revision => presentRevision(revision))
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.body = {
|
|
||||||
pagination: ctx.state.pagination,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('documents.restore', auth(), async ctx => {
|
router.post('documents.restore', auth(), async ctx => {
|
||||||
const { id, revisionId } = ctx.body;
|
const { id, revisionId } = ctx.body;
|
||||||
ctx.assertPresent(id, 'id is required');
|
ctx.assertPresent(id, 'id is required');
|
||||||
@ -667,6 +620,10 @@ router.post('documents.star', auth(), async ctx => {
|
|||||||
data: { title: document.title },
|
data: { title: document.title },
|
||||||
ip: ctx.request.ip,
|
ip: ctx.request.ip,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('documents.unstar', auth(), async ctx => {
|
router.post('documents.unstar', auth(), async ctx => {
|
||||||
@ -690,6 +647,10 @@ router.post('documents.unstar', auth(), async ctx => {
|
|||||||
data: { title: document.title },
|
data: { title: document.title },
|
||||||
ip: ctx.request.ip,
|
ip: ctx.request.ip,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('documents.create', auth(), async ctx => {
|
router.post('documents.create', auth(), async ctx => {
|
||||||
@ -925,8 +886,8 @@ router.post('documents.move', auth(), async ctx => {
|
|||||||
collections: await Promise.all(
|
collections: await Promise.all(
|
||||||
collections.map(collection => presentCollection(collection))
|
collections.map(collection => presentCollection(collection))
|
||||||
),
|
),
|
||||||
policies: presentPolicies(user, documents),
|
|
||||||
},
|
},
|
||||||
|
policies: presentPolicies(user, documents),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -436,48 +436,6 @@ describe('#documents.drafts', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#documents.revision', async () => {
|
|
||||||
it("should return a document's revisions", async () => {
|
|
||||||
const { user, document } = await seed();
|
|
||||||
const res = await server.post('/api/documents.revisions', {
|
|
||||||
body: {
|
|
||||||
token: user.getJwtToken(),
|
|
||||||
id: document.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const body = await res.json();
|
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
|
||||||
expect(body.data.length).toEqual(1);
|
|
||||||
expect(body.data[0].id).not.toEqual(document.id);
|
|
||||||
expect(body.data[0].title).toEqual(document.title);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not return revisions for document in collection not a member of', async () => {
|
|
||||||
const { user, document, collection } = await seed();
|
|
||||||
collection.private = true;
|
|
||||||
await collection.save();
|
|
||||||
|
|
||||||
const res = await server.post('/api/documents.revisions', {
|
|
||||||
body: { token: user.getJwtToken(), id: document.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.status).toEqual(403);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require authorization', async () => {
|
|
||||||
const { document } = await seed();
|
|
||||||
const user = await buildUser();
|
|
||||||
const res = await server.post('/api/documents.revisions', {
|
|
||||||
body: {
|
|
||||||
token: user.getJwtToken(),
|
|
||||||
id: document.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(res.status).toEqual(403);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#documents.search', async () => {
|
describe('#documents.search', async () => {
|
||||||
it('should return results', async () => {
|
it('should return results', async () => {
|
||||||
const { user } = await seed();
|
const { user } = await seed();
|
||||||
|
@ -18,13 +18,16 @@ const { authorize } = policy;
|
|||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
router.post('groups.list', auth(), pagination(), async ctx => {
|
router.post('groups.list', auth(), pagination(), async ctx => {
|
||||||
|
const { sort = 'updatedAt' } = ctx.body;
|
||||||
|
let direction = ctx.body.direction;
|
||||||
|
if (direction !== 'ASC') direction = 'DESC';
|
||||||
const user = ctx.state.user;
|
const user = ctx.state.user;
|
||||||
|
|
||||||
let groups = await Group.findAll({
|
let groups = await Group.findAll({
|
||||||
where: {
|
where: {
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
},
|
},
|
||||||
order: [['updatedAt', 'DESC']],
|
order: [[sort, direction]],
|
||||||
offset: ctx.state.pagination.offset,
|
offset: ctx.state.pagination.offset,
|
||||||
limit: ctx.state.pagination.limit,
|
limit: ctx.state.pagination.limit,
|
||||||
});
|
});
|
||||||
|
@ -8,6 +8,7 @@ import events from './events';
|
|||||||
import users from './users';
|
import users from './users';
|
||||||
import collections from './collections';
|
import collections from './collections';
|
||||||
import documents from './documents';
|
import documents from './documents';
|
||||||
|
import revisions from './revisions';
|
||||||
import views from './views';
|
import views from './views';
|
||||||
import hooks from './hooks';
|
import hooks from './hooks';
|
||||||
import apiKeys from './apiKeys';
|
import apiKeys from './apiKeys';
|
||||||
@ -45,6 +46,7 @@ router.use('/', events.routes());
|
|||||||
router.use('/', users.routes());
|
router.use('/', users.routes());
|
||||||
router.use('/', collections.routes());
|
router.use('/', collections.routes());
|
||||||
router.use('/', documents.routes());
|
router.use('/', documents.routes());
|
||||||
|
router.use('/', revisions.routes());
|
||||||
router.use('/', views.routes());
|
router.use('/', views.routes());
|
||||||
router.use('/', hooks.routes());
|
router.use('/', hooks.routes());
|
||||||
router.use('/', apiKeys.routes());
|
router.use('/', apiKeys.routes());
|
||||||
|
60
server/api/revisions.js
Normal file
60
server/api/revisions.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// @flow
|
||||||
|
import Router from 'koa-router';
|
||||||
|
import auth from '../middlewares/authentication';
|
||||||
|
import pagination from './middlewares/pagination';
|
||||||
|
import { presentRevision } from '../presenters';
|
||||||
|
import { Document, Revision } from '../models';
|
||||||
|
import { NotFoundError } from '../errors';
|
||||||
|
import policy from '../policies';
|
||||||
|
|
||||||
|
const { authorize } = policy;
|
||||||
|
const router = new Router();
|
||||||
|
|
||||||
|
router.post('revisions.info', auth(), async ctx => {
|
||||||
|
let { id } = ctx.body;
|
||||||
|
ctx.assertPresent(id, 'id is required');
|
||||||
|
|
||||||
|
const user = ctx.state.user;
|
||||||
|
const revision = await Revision.findByPk(id);
|
||||||
|
if (!revision) {
|
||||||
|
throw new NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const document = await Document.findByPk(revision.documentId, {
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
authorize(user, 'read', document);
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
pagination: ctx.state.pagination,
|
||||||
|
data: await presentRevision(revision),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('revisions.list', auth(), pagination(), async ctx => {
|
||||||
|
let { documentId, sort = 'updatedAt', direction } = ctx.body;
|
||||||
|
if (direction !== 'ASC') direction = 'DESC';
|
||||||
|
ctx.assertPresent(documentId, 'documentId is required');
|
||||||
|
|
||||||
|
const user = ctx.state.user;
|
||||||
|
const document = await Document.findByPk(documentId, { userId: user.id });
|
||||||
|
authorize(user, 'read', document);
|
||||||
|
|
||||||
|
const revisions = await Revision.findAll({
|
||||||
|
where: { documentId: document.id },
|
||||||
|
order: [[sort, direction]],
|
||||||
|
offset: ctx.state.pagination.offset,
|
||||||
|
limit: ctx.state.pagination.limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await Promise.all(
|
||||||
|
revisions.map(revision => presentRevision(revision))
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
pagination: ctx.state.pagination,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
92
server/api/revisions.test.js
Normal file
92
server/api/revisions.test.js
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||||
|
import TestServer from 'fetch-test-server';
|
||||||
|
import app from '../app';
|
||||||
|
import { flushdb, seed } from '../test/support';
|
||||||
|
import { buildDocument, buildUser } from '../test/factories';
|
||||||
|
import Revision from '../models/Revision';
|
||||||
|
|
||||||
|
const server = new TestServer(app.callback());
|
||||||
|
|
||||||
|
beforeEach(flushdb);
|
||||||
|
afterAll(server.close);
|
||||||
|
|
||||||
|
describe('#revisions.info', async () => {
|
||||||
|
it('should return a document revision', async () => {
|
||||||
|
const { user, document } = await seed();
|
||||||
|
const revision = await Revision.findOne({
|
||||||
|
where: {
|
||||||
|
documentId: document.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const res = await server.post('/api/revisions.info', {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
id: revision.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.id).not.toEqual(document.id);
|
||||||
|
expect(body.data.title).toEqual(document.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require authorization', async () => {
|
||||||
|
const document = await buildDocument();
|
||||||
|
const revision = await Revision.findOne({
|
||||||
|
where: {
|
||||||
|
documentId: document.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const user = await buildUser();
|
||||||
|
const res = await server.post('/api/revisions.info', {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
id: revision.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#revisions.list', async () => {
|
||||||
|
it("should return a document's revisions", async () => {
|
||||||
|
const { user, document } = await seed();
|
||||||
|
const res = await server.post('/api/revisions.list', {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
documentId: document.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.length).toEqual(1);
|
||||||
|
expect(body.data[0].id).not.toEqual(document.id);
|
||||||
|
expect(body.data[0].title).toEqual(document.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return revisions for document in collection not a member of', async () => {
|
||||||
|
const { user, document, collection } = await seed();
|
||||||
|
collection.private = true;
|
||||||
|
await collection.save();
|
||||||
|
|
||||||
|
const res = await server.post('/api/revisions.list', {
|
||||||
|
body: { token: user.getJwtToken(), documentId: document.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toEqual(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require authorization', async () => {
|
||||||
|
const document = await buildDocument();
|
||||||
|
const user = await buildUser();
|
||||||
|
const res = await server.post('/api/revisions.list', {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
documentId: document.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(403);
|
||||||
|
});
|
||||||
|
});
|
@ -11,6 +11,19 @@ const Op = Sequelize.Op;
|
|||||||
const { authorize } = policy;
|
const { authorize } = policy;
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
|
router.post('shares.info', auth(), async ctx => {
|
||||||
|
const { id } = ctx.body;
|
||||||
|
ctx.assertUuid(id, 'id is required');
|
||||||
|
|
||||||
|
const user = ctx.state.user;
|
||||||
|
const share = await Share.findByPk(id);
|
||||||
|
authorize(user, 'read', share);
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
data: presentShare(share),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
router.post('shares.list', auth(), pagination(), async ctx => {
|
router.post('shares.list', auth(), pagination(), async ctx => {
|
||||||
let { sort = 'updatedAt', direction } = ctx.body;
|
let { sort = 'updatedAt', direction } = ctx.body;
|
||||||
if (direction !== 'ASC') direction = 'DESC';
|
if (direction !== 'ASC') direction = 'DESC';
|
||||||
|
@ -1,27 +1,20 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import uuid from 'uuid';
|
|
||||||
import Router from 'koa-router';
|
import Router from 'koa-router';
|
||||||
import format from 'date-fns/format';
|
|
||||||
import { Op } from '../sequelize';
|
import { Op } from '../sequelize';
|
||||||
import {
|
import { Event, User, Team } from '../models';
|
||||||
makePolicy,
|
|
||||||
getSignature,
|
|
||||||
publicS3Endpoint,
|
|
||||||
makeCredential,
|
|
||||||
} from '../utils/s3';
|
|
||||||
import { Document, Attachment, Event, User, Team } from '../models';
|
|
||||||
import auth from '../middlewares/authentication';
|
import auth from '../middlewares/authentication';
|
||||||
import pagination from './middlewares/pagination';
|
import pagination from './middlewares/pagination';
|
||||||
import userInviter from '../commands/userInviter';
|
import userInviter from '../commands/userInviter';
|
||||||
import { presentUser } from '../presenters';
|
import { presentUser } from '../presenters';
|
||||||
import policy from '../policies';
|
import policy from '../policies';
|
||||||
|
|
||||||
const AWS_S3_ACL = process.env.AWS_S3_ACL || 'private';
|
|
||||||
const { authorize } = policy;
|
const { authorize } = policy;
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
router.post('users.list', auth(), pagination(), async ctx => {
|
router.post('users.list', auth(), pagination(), async ctx => {
|
||||||
const { query, includeSuspended = false } = ctx.body;
|
const { sort = 'createdAt', query, includeSuspended = false } = ctx.body;
|
||||||
|
let direction = ctx.body.direction;
|
||||||
|
if (direction !== 'ASC') direction = 'DESC';
|
||||||
const user = ctx.state.user;
|
const user = ctx.state.user;
|
||||||
|
|
||||||
let where = {
|
let where = {
|
||||||
@ -48,7 +41,7 @@ router.post('users.list', auth(), pagination(), async ctx => {
|
|||||||
|
|
||||||
const users = await User.findAll({
|
const users = await User.findAll({
|
||||||
where,
|
where,
|
||||||
order: [['createdAt', 'DESC']],
|
order: [[sort, direction]],
|
||||||
offset: ctx.state.pagination.offset,
|
offset: ctx.state.pagination.offset,
|
||||||
limit: ctx.state.pagination.limit,
|
limit: ctx.state.pagination.limit,
|
||||||
});
|
});
|
||||||
@ -81,79 +74,6 @@ router.post('users.update', auth(), async ctx => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('users.s3Upload', auth(), async ctx => {
|
|
||||||
let { name, filename, documentId, contentType, kind, size } = ctx.body;
|
|
||||||
|
|
||||||
// backwards compatability
|
|
||||||
name = name || filename;
|
|
||||||
contentType = contentType || kind;
|
|
||||||
|
|
||||||
ctx.assertPresent(name, 'name is required');
|
|
||||||
ctx.assertPresent(contentType, 'contentType is required');
|
|
||||||
ctx.assertPresent(size, 'size is required');
|
|
||||||
|
|
||||||
const { user } = ctx.state;
|
|
||||||
const s3Key = uuid.v4();
|
|
||||||
const key = `uploads/${user.id}/${s3Key}/${name}`;
|
|
||||||
const acl =
|
|
||||||
ctx.body.public === undefined
|
|
||||||
? AWS_S3_ACL
|
|
||||||
: ctx.body.public ? 'public-read' : 'private';
|
|
||||||
const credential = makeCredential();
|
|
||||||
const longDate = format(new Date(), 'YYYYMMDDTHHmmss\\Z');
|
|
||||||
const policy = makePolicy(credential, longDate, acl);
|
|
||||||
const endpoint = publicS3Endpoint();
|
|
||||||
const url = `${endpoint}/${key}`;
|
|
||||||
|
|
||||||
if (documentId) {
|
|
||||||
const document = await Document.findByPk(documentId, { userId: user.id });
|
|
||||||
authorize(user, 'update', document);
|
|
||||||
}
|
|
||||||
|
|
||||||
const attachment = await Attachment.create({
|
|
||||||
key,
|
|
||||||
acl,
|
|
||||||
size,
|
|
||||||
url,
|
|
||||||
contentType,
|
|
||||||
documentId,
|
|
||||||
teamId: user.teamId,
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
await Event.create({
|
|
||||||
name: 'user.s3Upload',
|
|
||||||
data: { name },
|
|
||||||
teamId: user.teamId,
|
|
||||||
userId: user.id,
|
|
||||||
ip: ctx.request.ip,
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.body = {
|
|
||||||
data: {
|
|
||||||
maxUploadSize: process.env.AWS_S3_UPLOAD_MAX_SIZE,
|
|
||||||
uploadUrl: endpoint,
|
|
||||||
form: {
|
|
||||||
'Cache-Control': 'max-age=31557600',
|
|
||||||
'Content-Type': contentType,
|
|
||||||
acl,
|
|
||||||
key,
|
|
||||||
policy,
|
|
||||||
'x-amz-algorithm': 'AWS4-HMAC-SHA256',
|
|
||||||
'x-amz-credential': credential,
|
|
||||||
'x-amz-date': longDate,
|
|
||||||
'x-amz-signature': getSignature(policy),
|
|
||||||
},
|
|
||||||
asset: {
|
|
||||||
contentType,
|
|
||||||
name,
|
|
||||||
url: attachment.redirectUrl,
|
|
||||||
size,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Admin specific
|
// Admin specific
|
||||||
|
|
||||||
router.post('users.promote', auth(), async ctx => {
|
router.post('users.promote', auth(), async ctx => {
|
||||||
|
@ -49,7 +49,7 @@ export default function Api() {
|
|||||||
</Arguments>
|
</Arguments>
|
||||||
</Method>
|
</Method>
|
||||||
|
|
||||||
<Method method="users.s3Upload" label="Get S3 upload credentials">
|
<Method method="attachments.create" label="Get S3 upload credentials">
|
||||||
<Description>
|
<Description>
|
||||||
You can upload small files and images as part of your documents.
|
You can upload small files and images as part of your documents.
|
||||||
All files are stored using Amazon S3. Instead of uploading files
|
All files are stored using Amazon S3. Instead of uploading files
|
||||||
@ -84,7 +84,7 @@ export default function Api() {
|
|||||||
Promote a user to be a team admin. This endpoint is only available
|
Promote a user to be a team admin. This endpoint is only available
|
||||||
for admin users.
|
for admin users.
|
||||||
</Description>
|
</Description>
|
||||||
<Arguments pagination>
|
<Arguments>
|
||||||
<Argument id="id" description="User ID to be promoted" required />
|
<Argument id="id" description="User ID to be promoted" required />
|
||||||
</Arguments>
|
</Arguments>
|
||||||
</Method>
|
</Method>
|
||||||
@ -95,7 +95,7 @@ export default function Api() {
|
|||||||
is always required. This endpoint is only available for admin
|
is always required. This endpoint is only available for admin
|
||||||
users.
|
users.
|
||||||
</Description>
|
</Description>
|
||||||
<Arguments pagination>
|
<Arguments>
|
||||||
<Argument id="id" description="User ID to be demoted" required />
|
<Argument id="id" description="User ID to be demoted" required />
|
||||||
</Arguments>
|
</Arguments>
|
||||||
</Method>
|
</Method>
|
||||||
@ -105,7 +105,7 @@ export default function Api() {
|
|||||||
Admin can suspend users to reduce the number of accounts on their
|
Admin can suspend users to reduce the number of accounts on their
|
||||||
billing plan or prevent them from accessing documention.
|
billing plan or prevent them from accessing documention.
|
||||||
</Description>
|
</Description>
|
||||||
<Arguments pagination>
|
<Arguments>
|
||||||
<Argument
|
<Argument
|
||||||
id="id"
|
id="id"
|
||||||
description="User ID to be suspended"
|
description="User ID to be suspended"
|
||||||
@ -122,7 +122,7 @@ export default function Api() {
|
|||||||
Admin can re-active a suspended user. This will update the billing
|
Admin can re-active a suspended user. This will update the billing
|
||||||
plan and re-enable their access to the documention.
|
plan and re-enable their access to the documention.
|
||||||
</Description>
|
</Description>
|
||||||
<Arguments pagination>
|
<Arguments>
|
||||||
<Argument
|
<Argument
|
||||||
id="id"
|
id="id"
|
||||||
description="User ID to be activated"
|
description="User ID to be activated"
|
||||||
@ -173,7 +173,10 @@ export default function Api() {
|
|||||||
</Arguments>
|
</Arguments>
|
||||||
</Method>
|
</Method>
|
||||||
|
|
||||||
<Method method="collections.exportAll" label="Export all collections">
|
<Method
|
||||||
|
method="collections.export_all"
|
||||||
|
label="Export all collections"
|
||||||
|
>
|
||||||
<Description>
|
<Description>
|
||||||
Returns a zip file of all the collections or creates an async job
|
Returns a zip file of all the collections or creates an async job
|
||||||
to send a zip file via email to the authenticated user. If
|
to send a zip file via email to the authenticated user. If
|
||||||
@ -527,20 +530,6 @@ export default function Api() {
|
|||||||
</Arguments>
|
</Arguments>
|
||||||
</Method>
|
</Method>
|
||||||
|
|
||||||
<Method method="documents.info" label="Get a document">
|
|
||||||
<Description>
|
|
||||||
Get a document with its ID or URL identifier from user’s
|
|
||||||
collections.
|
|
||||||
</Description>
|
|
||||||
<Arguments>
|
|
||||||
<Argument
|
|
||||||
id="id"
|
|
||||||
description="Document ID or URI identifier"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Arguments>
|
|
||||||
</Method>
|
|
||||||
|
|
||||||
<Method
|
<Method
|
||||||
method="documents.restore"
|
method="documents.restore"
|
||||||
label="Restore a previous revision"
|
label="Restore a previous revision"
|
||||||
@ -651,32 +640,21 @@ export default function Api() {
|
|||||||
</Arguments>
|
</Arguments>
|
||||||
</Method>
|
</Method>
|
||||||
|
|
||||||
<Method
|
<Method method="revisions.info" label="Get revision for a document">
|
||||||
method="documents.revision"
|
|
||||||
label="Get revision for a document"
|
|
||||||
>
|
|
||||||
<Description>Return a specific revision of a document.</Description>
|
<Description>Return a specific revision of a document.</Description>
|
||||||
<Arguments>
|
<Arguments>
|
||||||
<Argument
|
<Argument id="id" description="Revision ID" required />
|
||||||
id="id"
|
|
||||||
description="Document ID or URI identifier"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Argument id="revisionId" description="Revision ID" required />
|
|
||||||
</Arguments>
|
</Arguments>
|
||||||
</Method>
|
</Method>
|
||||||
|
|
||||||
<Method
|
<Method method="revisions.list" label="Get revisions for a document">
|
||||||
method="documents.revisions"
|
|
||||||
label="Get revisions for a document"
|
|
||||||
>
|
|
||||||
<Description>
|
<Description>
|
||||||
Return revisions for a document. Upon each edit, a new revision is
|
Return revisions for a document. Upon each edit, a new revision is
|
||||||
stored.
|
stored.
|
||||||
</Description>
|
</Description>
|
||||||
<Arguments pagination>
|
<Arguments pagination>
|
||||||
<Argument
|
<Argument
|
||||||
id="id"
|
id="documentId"
|
||||||
description="Document ID or URI identifier"
|
description="Document ID or URI identifier"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@ -688,7 +666,7 @@ export default function Api() {
|
|||||||
This method allows you to create a new group to organize people in
|
This method allows you to create a new group to organize people in
|
||||||
the team.
|
the team.
|
||||||
</Description>
|
</Description>
|
||||||
<Arguments pagination>
|
<Arguments>
|
||||||
<Argument
|
<Argument
|
||||||
id="name"
|
id="name"
|
||||||
description="The name of the group"
|
description="The name of the group"
|
||||||
@ -702,7 +680,7 @@ export default function Api() {
|
|||||||
This method allows you to update an existing group. At this time
|
This method allows you to update an existing group. At this time
|
||||||
the only field that can be edited is the name.
|
the only field that can be edited is the name.
|
||||||
</Description>
|
</Description>
|
||||||
<Arguments pagination>
|
<Arguments>
|
||||||
<Argument id="id" description="Group ID" required />
|
<Argument id="id" description="Group ID" required />
|
||||||
<Argument
|
<Argument
|
||||||
id="name"
|
id="name"
|
||||||
|
@ -6,6 +6,7 @@ export default function present(group: Group) {
|
|||||||
id: group.id,
|
id: group.id,
|
||||||
name: group.name,
|
name: group.name,
|
||||||
memberCount: group.groupMemberships.length,
|
memberCount: group.groupMemberships.length,
|
||||||
|
createdAt: group.createdAt,
|
||||||
updatedAt: group.updatedAt,
|
updatedAt: group.updatedAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user