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({
|
||||
limit,
|
||||
offset: this.offset,
|
||||
id: this.props.match.params.documentSlug,
|
||||
documentId: this.props.match.params.documentSlug,
|
||||
});
|
||||
|
||||
if (
|
||||
|
@ -267,7 +267,7 @@ class CollectionScene extends React.Component<Props> {
|
||||
collection.id
|
||||
)}
|
||||
fetch={documents.fetchAlphabetical}
|
||||
options={{ collection: collection.id }}
|
||||
options={{ collectionId: collection.id }}
|
||||
showPin
|
||||
/>
|
||||
</Route>
|
||||
@ -278,7 +278,7 @@ class CollectionScene extends React.Component<Props> {
|
||||
collection.id
|
||||
)}
|
||||
fetch={documents.fetchLeastRecentlyUpdated}
|
||||
options={{ collection: collection.id }}
|
||||
options={{ collectionId: collection.id }}
|
||||
showPin
|
||||
/>
|
||||
</Route>
|
||||
@ -289,7 +289,7 @@ class CollectionScene extends React.Component<Props> {
|
||||
collection.id
|
||||
)}
|
||||
fetch={documents.fetchRecentlyPublished}
|
||||
options={{ collection: collection.id }}
|
||||
options={{ collectionId: collection.id }}
|
||||
showPublished
|
||||
showPin
|
||||
/>
|
||||
@ -300,7 +300,7 @@ class CollectionScene extends React.Component<Props> {
|
||||
collection.id
|
||||
)}
|
||||
fetch={documents.fetchRecentlyUpdated}
|
||||
options={{ collection: collection.id }}
|
||||
options={{ collectionId: collection.id }}
|
||||
showPin
|
||||
/>
|
||||
</Route>
|
||||
|
@ -97,11 +97,8 @@ class DataLoader extends React.Component<Props> {
|
||||
};
|
||||
|
||||
loadRevision = async () => {
|
||||
const { documentSlug, revisionId } = this.props.match.params;
|
||||
|
||||
this.revision = await this.props.revisions.fetch(documentSlug, {
|
||||
revisionId,
|
||||
});
|
||||
const { revisionId } = this.props.match.params;
|
||||
this.revision = await this.props.revisions.fetch(revisionId);
|
||||
};
|
||||
|
||||
loadDocument = async () => {
|
||||
|
@ -105,6 +105,6 @@ export default class CollectionsStore extends BaseStore<Collection> {
|
||||
}
|
||||
|
||||
export = () => {
|
||||
return client.post('/collections.exportAll');
|
||||
return client.post('/collections.export_all');
|
||||
};
|
||||
}
|
||||
|
@ -20,21 +20,16 @@ export default class RevisionsStore extends BaseStore<Revision> {
|
||||
}
|
||||
|
||||
@action
|
||||
fetch = async (
|
||||
documentId: string,
|
||||
options?: FetchOptions
|
||||
): Promise<?Revision> => {
|
||||
fetch = async (id: string, options?: FetchOptions): Promise<?Revision> => {
|
||||
this.isFetching = true;
|
||||
const id = options && options.revisionId;
|
||||
if (!id) throw new Error('revisionId is required');
|
||||
invariant(id, 'Id is required');
|
||||
|
||||
try {
|
||||
const rev = this.data.get(id);
|
||||
if (rev) return rev;
|
||||
|
||||
const res = await client.post('/documents.revision', {
|
||||
id: documentId,
|
||||
revisionId: id,
|
||||
const res = await client.post('/revisions.info', {
|
||||
id,
|
||||
});
|
||||
invariant(res && res.data, 'Revision not available');
|
||||
this.add(res.data);
|
||||
@ -54,7 +49,7 @@ export default class RevisionsStore extends BaseStore<Revision> {
|
||||
this.isFetching = true;
|
||||
|
||||
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');
|
||||
runInAction('RevisionsStore#fetchPage', () => {
|
||||
res.data.forEach(revision => this.add(revision));
|
||||
|
@ -13,7 +13,7 @@ export const uploadFile = async (
|
||||
options?: 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,
|
||||
documentId: options.documentId,
|
||||
contentType: file.type,
|
||||
@ -24,7 +24,7 @@ export const uploadFile = async (
|
||||
invariant(response, 'Response should be available');
|
||||
|
||||
const data = response.data;
|
||||
const asset = data.asset;
|
||||
const attachment = data.attachment;
|
||||
const formData = new FormData();
|
||||
|
||||
for (const key in data.form) {
|
||||
@ -44,7 +44,7 @@ export const uploadFile = async (
|
||||
body: formData,
|
||||
});
|
||||
|
||||
return asset;
|
||||
return attachment;
|
||||
};
|
||||
|
||||
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 {
|
||||
"error": "authentication_required",
|
||||
"message": "Authentication required",
|
||||
|
@ -1,13 +1,92 @@
|
||||
// @flow
|
||||
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 { Attachment, Document } from '../models';
|
||||
import { getSignedImageUrl } from '../utils/s3';
|
||||
import { NotFoundError } from '../errors';
|
||||
import policy from '../policies';
|
||||
|
||||
const { authorize } = policy;
|
||||
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 => {
|
||||
const { id } = ctx.body;
|
||||
|
@ -410,7 +410,7 @@ router.post('collections.export', auth(), async ctx => {
|
||||
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 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 () => {
|
||||
const res = await server.post('/api/collections.exportAll');
|
||||
const res = await server.post('/api/collections.export_all');
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
@ -187,7 +187,7 @@ describe('#collections.exportAll', async () => {
|
||||
|
||||
it('should require authorization', async () => {
|
||||
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() },
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
@ -195,7 +195,7 @@ describe('#collections.exportAll', async () => {
|
||||
|
||||
it('should return success', async () => {
|
||||
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() },
|
||||
});
|
||||
|
||||
@ -204,7 +204,7 @@ describe('#collections.exportAll', async () => {
|
||||
|
||||
it('should allow downloading directly', async () => {
|
||||
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 },
|
||||
});
|
||||
|
||||
|
@ -7,7 +7,6 @@ import documentMover from '../commands/documentMover';
|
||||
import {
|
||||
presentDocument,
|
||||
presentCollection,
|
||||
presentRevision,
|
||||
presentPolicies,
|
||||
} from '../presenters';
|
||||
import {
|
||||
@ -31,8 +30,10 @@ const router = new Router();
|
||||
|
||||
router.post('documents.list', auth(), pagination(), async ctx => {
|
||||
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;
|
||||
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 => {
|
||||
const { id, revisionId } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
@ -667,6 +620,10 @@ router.post('documents.star', auth(), async ctx => {
|
||||
data: { title: document.title },
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
|
||||
router.post('documents.unstar', auth(), async ctx => {
|
||||
@ -690,6 +647,10 @@ router.post('documents.unstar', auth(), async ctx => {
|
||||
data: { title: document.title },
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
|
||||
router.post('documents.create', auth(), async ctx => {
|
||||
@ -925,8 +886,8 @@ router.post('documents.move', auth(), async ctx => {
|
||||
collections: await Promise.all(
|
||||
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 () => {
|
||||
it('should return results', async () => {
|
||||
const { user } = await seed();
|
||||
|
@ -18,13 +18,16 @@ const { authorize } = policy;
|
||||
const router = new Router();
|
||||
|
||||
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;
|
||||
|
||||
let groups = await Group.findAll({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
},
|
||||
order: [['updatedAt', 'DESC']],
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
|
@ -8,6 +8,7 @@ import events from './events';
|
||||
import users from './users';
|
||||
import collections from './collections';
|
||||
import documents from './documents';
|
||||
import revisions from './revisions';
|
||||
import views from './views';
|
||||
import hooks from './hooks';
|
||||
import apiKeys from './apiKeys';
|
||||
@ -45,6 +46,7 @@ router.use('/', events.routes());
|
||||
router.use('/', users.routes());
|
||||
router.use('/', collections.routes());
|
||||
router.use('/', documents.routes());
|
||||
router.use('/', revisions.routes());
|
||||
router.use('/', views.routes());
|
||||
router.use('/', hooks.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 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 => {
|
||||
let { sort = 'updatedAt', direction } = ctx.body;
|
||||
if (direction !== 'ASC') direction = 'DESC';
|
||||
|
@ -1,27 +1,20 @@
|
||||
// @flow
|
||||
import uuid from 'uuid';
|
||||
import Router from 'koa-router';
|
||||
import format from 'date-fns/format';
|
||||
import { Op } from '../sequelize';
|
||||
import {
|
||||
makePolicy,
|
||||
getSignature,
|
||||
publicS3Endpoint,
|
||||
makeCredential,
|
||||
} from '../utils/s3';
|
||||
import { Document, Attachment, Event, User, Team } from '../models';
|
||||
import { Event, User, Team } from '../models';
|
||||
import auth from '../middlewares/authentication';
|
||||
import pagination from './middlewares/pagination';
|
||||
import userInviter from '../commands/userInviter';
|
||||
import { presentUser } from '../presenters';
|
||||
import policy from '../policies';
|
||||
|
||||
const AWS_S3_ACL = process.env.AWS_S3_ACL || 'private';
|
||||
const { authorize } = policy;
|
||||
const router = new Router();
|
||||
|
||||
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;
|
||||
|
||||
let where = {
|
||||
@ -48,7 +41,7 @@ router.post('users.list', auth(), pagination(), async ctx => {
|
||||
|
||||
const users = await User.findAll({
|
||||
where,
|
||||
order: [['createdAt', 'DESC']],
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
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
|
||||
|
||||
router.post('users.promote', auth(), async ctx => {
|
||||
|
@ -49,7 +49,7 @@ export default function Api() {
|
||||
</Arguments>
|
||||
</Method>
|
||||
|
||||
<Method method="users.s3Upload" label="Get S3 upload credentials">
|
||||
<Method method="attachments.create" label="Get S3 upload credentials">
|
||||
<Description>
|
||||
You can upload small files and images as part of your documents.
|
||||
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
|
||||
for admin users.
|
||||
</Description>
|
||||
<Arguments pagination>
|
||||
<Arguments>
|
||||
<Argument id="id" description="User ID to be promoted" required />
|
||||
</Arguments>
|
||||
</Method>
|
||||
@ -95,7 +95,7 @@ export default function Api() {
|
||||
is always required. This endpoint is only available for admin
|
||||
users.
|
||||
</Description>
|
||||
<Arguments pagination>
|
||||
<Arguments>
|
||||
<Argument id="id" description="User ID to be demoted" required />
|
||||
</Arguments>
|
||||
</Method>
|
||||
@ -105,7 +105,7 @@ export default function Api() {
|
||||
Admin can suspend users to reduce the number of accounts on their
|
||||
billing plan or prevent them from accessing documention.
|
||||
</Description>
|
||||
<Arguments pagination>
|
||||
<Arguments>
|
||||
<Argument
|
||||
id="id"
|
||||
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
|
||||
plan and re-enable their access to the documention.
|
||||
</Description>
|
||||
<Arguments pagination>
|
||||
<Arguments>
|
||||
<Argument
|
||||
id="id"
|
||||
description="User ID to be activated"
|
||||
@ -173,7 +173,10 @@ export default function Api() {
|
||||
</Arguments>
|
||||
</Method>
|
||||
|
||||
<Method method="collections.exportAll" label="Export all collections">
|
||||
<Method
|
||||
method="collections.export_all"
|
||||
label="Export all collections"
|
||||
>
|
||||
<Description>
|
||||
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
|
||||
@ -527,20 +530,6 @@ export default function Api() {
|
||||
</Arguments>
|
||||
</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="documents.restore"
|
||||
label="Restore a previous revision"
|
||||
@ -651,32 +640,21 @@ export default function Api() {
|
||||
</Arguments>
|
||||
</Method>
|
||||
|
||||
<Method
|
||||
method="documents.revision"
|
||||
label="Get revision for a document"
|
||||
>
|
||||
<Method method="revisions.info" label="Get revision for a document">
|
||||
<Description>Return a specific revision of a document.</Description>
|
||||
<Arguments>
|
||||
<Argument
|
||||
id="id"
|
||||
description="Document ID or URI identifier"
|
||||
required
|
||||
/>
|
||||
<Argument id="revisionId" description="Revision ID" required />
|
||||
<Argument id="id" description="Revision ID" required />
|
||||
</Arguments>
|
||||
</Method>
|
||||
|
||||
<Method
|
||||
method="documents.revisions"
|
||||
label="Get revisions for a document"
|
||||
>
|
||||
<Method method="revisions.list" label="Get revisions for a document">
|
||||
<Description>
|
||||
Return revisions for a document. Upon each edit, a new revision is
|
||||
stored.
|
||||
</Description>
|
||||
<Arguments pagination>
|
||||
<Argument
|
||||
id="id"
|
||||
id="documentId"
|
||||
description="Document ID or URI identifier"
|
||||
required
|
||||
/>
|
||||
@ -688,7 +666,7 @@ export default function Api() {
|
||||
This method allows you to create a new group to organize people in
|
||||
the team.
|
||||
</Description>
|
||||
<Arguments pagination>
|
||||
<Arguments>
|
||||
<Argument
|
||||
id="name"
|
||||
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
|
||||
the only field that can be edited is the name.
|
||||
</Description>
|
||||
<Arguments pagination>
|
||||
<Arguments>
|
||||
<Argument id="id" description="Group ID" required />
|
||||
<Argument
|
||||
id="name"
|
||||
|
@ -6,6 +6,7 @@ export default function present(group: Group) {
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
memberCount: group.groupMemberships.length,
|
||||
createdAt: group.createdAt,
|
||||
updatedAt: group.updatedAt,
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user