Skip to content

Commit

Permalink
feat: Remove attachments from public access
Browse files Browse the repository at this point in the history
Closes #219
  • Loading branch information
meltyshev committed Apr 26, 2022
1 parent 486e663 commit 36e4bef
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 19 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ COPY --from=client-builder /app/build/index.html views

VOLUME /app/public/user-avatars
VOLUME /app/public/project-background-images
VOLUME /app/public/attachments
VOLUME /app/private/attachments

EXPOSE 1337

Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ services:
volumes:
- user-avatars:/app/public/user-avatars
- project-background-images:/app/public/project-background-images
- attachments:/app/public/attachments
- attachments:/app/private/attachments
ports:
- 3000:1337
environment:
Expand Down
8 changes: 5 additions & 3 deletions server/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,11 @@ public/user-avatars/*
!public/project-background-images
public/project-background-images/*
!public/project-background-images/.gitkeep
!public/attachments
public/attachments/*
!public/attachments/.gitkeep

private/*
!private/attachments
private/attachments/*
!private/attachments/.gitkeep

views/*
!views/.gitkeep
67 changes: 67 additions & 0 deletions server/api/controllers/attachments/download-thumbnail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const fs = require('fs');
const path = require('path');

const Errors = {
ATTACHMENT_NOT_FOUND: {
attachmentNotFound: 'Attachment not found',
},
};

module.exports = {
inputs: {
id: {
type: 'string',
regex: /^[0-9]+$/,
required: true,
},
filename: {
type: 'string',
required: true,
},
},

exits: {
attachmentNotFound: {
responseType: 'notFound',
},
},

async fn(inputs, exits) {
const { currentUser } = this.req;

const { attachment, card, project } = await sails.helpers.attachments
.getProjectPath(inputs.id)
.intercept('pathNotFound', () => Errors.ATTACHMENT_NOT_FOUND);

const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId);

if (!isBoardMember) {
const isProjectManager = await sails.helpers.users.isProjectManager(
currentUser.id,
project.id,
);

if (!isProjectManager) {
throw Errors.ATTACHMENT_NOT_FOUND; // Forbidden
}
}

if (!attachment.isImage) {
throw Errors.ATTACHMENT_NOT_FOUND;
}

const filePath = path.join(
sails.config.custom.attachmentsPath,
attachment.dirname,
'thumbnails',
inputs.filename,
);

if (!fs.existsSync(filePath)) {
throw Errors.ATTACHMENT_NOT_FOUND;
}

this.res.setHeader('Content-Disposition', `inline; ${inputs.filename}`);
return exits.success(fs.createReadStream(filePath));
},
};
69 changes: 69 additions & 0 deletions server/api/controllers/attachments/download.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
const fs = require('fs');
const path = require('path');

const Errors = {
ATTACHMENT_NOT_FOUND: {
attachmentNotFound: 'Attachment not found',
},
};

module.exports = {
inputs: {
id: {
type: 'string',
regex: /^[0-9]+$/,
required: true,
},
filename: {
type: 'string',
required: true,
},
},

exits: {
attachmentNotFound: {
responseType: 'notFound',
},
},

async fn(inputs, exits) {
const { currentUser } = this.req;

const { attachment, card, project } = await sails.helpers.attachments
.getProjectPath(inputs.id)
.intercept('pathNotFound', () => Errors.ATTACHMENT_NOT_FOUND);

const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId);

if (!isBoardMember) {
const isProjectManager = await sails.helpers.users.isProjectManager(
currentUser.id,
project.id,
);

if (!isProjectManager) {
throw Errors.ATTACHMENT_NOT_FOUND; // Forbidden
}
}

const filePath = path.join(
sails.config.custom.attachmentsPath,
attachment.dirname,
attachment.filename,
);

if (!fs.existsSync(filePath)) {
throw Errors.ATTACHMENT_NOT_FOUND;
}

let contentDisposition;
if (attachment.isImage || path.extname(attachment.filename) === '.pdf') {
contentDisposition = 'inline';
} else {
contentDisposition = `attachment; ${inputs.filename}`;
}

this.res.setHeader('Content-Disposition', contentDisposition);
return exits.success(fs.createReadStream(filePath));
},
};
4 changes: 2 additions & 2 deletions server/api/models/Attachment.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ module.exports = {
customToJSON() {
return {
..._.omit(this, ['dirname', 'filename', 'isImage']),
url: `${sails.config.custom.attachmentsUrl}/${this.dirname}/${this.filename}`,
url: `${sails.config.custom.attachmentsUrl}/${this.id}/download/${this.filename}`,
coverUrl: this.isImage
? `${sails.config.custom.attachmentsUrl}/${this.dirname}/thumbnails/cover-256.jpg`
? `${sails.config.custom.attachmentsUrl}/${this.id}/download/thumbnails/cover-256.jpg`
: null,
};
},
Expand Down
2 changes: 1 addition & 1 deletion server/config/custom.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ module.exports.custom = {
projectBackgroundImagesPath: path.join(sails.config.paths.public, 'project-background-images'),
projectBackgroundImagesUrl: `${process.env.BASE_URL}/project-background-images`,

attachmentsPath: path.join(sails.config.paths.public, 'attachments'),
attachmentsPath: path.join(sails.config.appPath, 'private', 'attachments'),
attachmentsUrl: `${process.env.BASE_URL}/attachments`,
};
11 changes: 0 additions & 11 deletions server/config/policies.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,10 @@ module.exports.policies = {

'*': 'is-authenticated',

// 'users/index': ['is-authenticated', 'is-admin'],
'users/create': ['is-authenticated', 'is-admin'],
'users/delete': ['is-authenticated', 'is-admin'],

'projects/create': ['is-authenticated', 'is-admin'],
// 'projects/update': ['is-authenticated', 'is-admin'],
// 'projects/update-background-image': ['is-authenticated', 'is-admin'],
// 'projects/delete': ['is-authenticated', 'is-admin'],

// 'project-memberships/create': ['is-authenticated', 'is-admin'],
// 'project-memberships/delete': ['is-authenticated', 'is-admin'],

// 'boards/create': ['is-authenticated', 'is-admin'],
// 'boards/update': ['is-authenticated', 'is-admin'],
// 'boards/delete': ['is-authenticated', 'is-admin'],

'access-tokens/create': true,
};
10 changes: 10 additions & 0 deletions server/config/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ module.exports.routes = {
'GET /api/notifications/:id': 'notifications/show',
'PATCH /api/notifications/:ids': 'notifications/update',

'GET /attachments/:id/download/:filename': {
action: 'attachments/download',
skipAssets: false,
},

'GET /attachments/:id/download/thumbnails/:filename': {
action: 'attachments/download-thumbnail',
skipAssets: false,
},

'GET /*': {
view: 'index',
skipAssets: true,
Expand Down
File renamed without changes.

0 comments on commit 36e4bef

Please sign in to comment.