Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
32165a8
See if this helps with schema usage
jmgasper Jun 22, 2025
0994e2d
Merge
jmgasper Jun 22, 2025
5c39688
Allow non admins to see Applications
himaniraghav3 Aug 25, 2025
f2b6f56
deploy PM-1612
himaniraghav3 Aug 25, 2025
eff16df
deploy PM-1612
himaniraghav3 Aug 25, 2025
8f091ff
Merge branch 'develop' into deploy-pm-1612
himaniraghav3 Aug 25, 2025
3a68fd7
Merge pull request #853 from topcoder-platform/deploy-pm-1612
himaniraghav3 Aug 25, 2025
429dc5a
PM-1612 AI review feedback
himaniraghav3 Aug 26, 2025
036aa45
Merge pull request #854 from topcoder-platform/PM-1612
himaniraghav3 Aug 26, 2025
158e8d9
fix: send project info only for PM and admin role users
hentrymartin Sep 2, 2025
def3f5c
fix: send project info only for PM and admin role users
hentrymartin Sep 2, 2025
344671c
fix: send project info only for PM and admin role users
hentrymartin Sep 2, 2025
11ae715
fix: project object optional
hentrymartin Sep 2, 2025
3d2216a
fix: project object optional
hentrymartin Sep 2, 2025
6fb8f34
fix: project object optional
hentrymartin Sep 2, 2025
f8f52e0
fix: project object optional
hentrymartin Sep 2, 2025
d81d763
fix: removed debug log
hentrymartin Sep 2, 2025
ada92eb
Merge pull request #855 from topcoder-platform/fix-project-exposing
hentrymartin Sep 2, 2025
56de058
Updates for returning large attachments immediately and continuing th…
jmgasper Sep 23, 2025
3ad202c
Merge branch 'develop' of github.com:topcoder-platform/tc-project-ser…
jmgasper Sep 23, 2025
ccfcc6e
Merge branch 'master' into develop
jmgasper Sep 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ workflows:
context : org-global
filters:
branches:
only: ['develop', 'migration-setup', 'pm-1611_1', 'pm-1836_prod']
only: ['develop', 'migration-setup', 'PM-1612', 'fix-project-exposing']
- deployProd:
context : org-global
filters:
Expand Down
174 changes: 78 additions & 96 deletions src/routes/attachments/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ module.exports = [
* Add project attachment
* In development mode we have to mock the ec2 file transfer and file service calls
*/
(req, res, next) => {
async (req, res, next) => {
const data = req.body;
// default values
const projectId = req.params.projectId;
Expand All @@ -53,64 +53,22 @@ module.exports = [
// extract file name
const fileName = Path.parse(data.path).base;
// create file path
const path = _.join([
const attachmentPath = _.join([
config.get('projectAttachmentPathPrefix'),
data.projectId,
config.get('projectAttachmentPathPrefix'),
fileName,
], '/');
let newAttachment = null;

const sourceBucket = data.s3Bucket;
const sourceKey = data.path;
const destBucket = config.get('attachmentsS3Bucket');
const destKey = path;
const destKey = attachmentPath;

if (data.type === ATTACHMENT_TYPES.LINK) {
// We create the record in the db and return (i.e. no need to handle transferring file between S3 buckets)
Promise.resolve(models.ProjectAttachment.create({
projectId,
allowedUsers,
createdBy: req.authUser.userId,
updatedBy: req.authUser.userId,
title: data.title,
size: data.size,
category: data.category || null,
description: data.description,
contentType: data.contentType,
path: data.path,
type: data.type,
tags: data.tags,
})).then((_link) => {
const link = _link.get({ plain: true });
req.log.debug('New Link Attachment record: ', link);

// emit the Kafka event
util.sendResourceToKafkaBus(
req,
EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_ADDED,
RESOURCES.ATTACHMENT,
link);

res.status(201).json(link);
return Promise.resolve();
})
.catch((error) => {
req.log.error('Error adding link attachment', error);
const rerr = error;
rerr.status = rerr.status || 500;
next(rerr);
});
} else {
// don't actually transfer file in development mode if file uploading is disabled, so we can test this endpoint
const fileTransferPromise = (process.env.NODE_ENV !== 'development' || config.get('enableFileUpload') === 'true')
? util.s3FileTransfer(req, sourceBucket, sourceKey, destBucket, destKey)
: Promise.resolve();

fileTransferPromise.then(() => {
// file copied to final destination, create DB record
req.log.debug('creating db file record');
return models.ProjectAttachment.create({
try {
if (data.type === ATTACHMENT_TYPES.LINK) {
// Create the record and return immediately (no file transfer needed)
const linkInstance = await models.ProjectAttachment.create({
projectId,
allowedUsers,
createdBy: req.authUser.userId,
Expand All @@ -120,60 +78,84 @@ module.exports = [
category: data.category || null,
description: data.description,
contentType: data.contentType,
path,
path: data.path,
type: data.type,
tags: data.tags,
});
}).then((_newAttachment) => {
newAttachment = _newAttachment.get({ plain: true });
req.log.debug('New Attachment record: ', newAttachment);
if (process.env.NODE_ENV !== 'development' || config.get('enableFileUpload') === 'true') {
// retrieve download url for the response
req.log.debug('retrieving download url');
return getDownloadUrl(destBucket, path);
}
return Promise.resolve();
}).then((url) => {
if (
process.env.NODE_ENV !== 'development' ||
config.get('enableFileUpload') === 'true'
) {
req.log.debug('Retreiving Presigned Url resp: ', url);
let response = _.cloneDeep(newAttachment);
response = _.omit(response, ['path', 'deletedAt']);

response.downloadUrl = url;

// emit the event
util.sendResourceToKafkaBus(
req,
EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_ADDED,
RESOURCES.ATTACHMENT,
newAttachment,
);
res.status(201).json(response);
return Promise.resolve();
}
let response = _.cloneDeep(newAttachment);
response = _.omit(response, ['path', 'deletedAt']);
// only in development mode
response.downloadUrl = path;
// emit the event
const link = linkInstance.get({ plain: true });
req.log.debug('New Link Attachment record: ', link);

util.sendResourceToKafkaBus(
req,
EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_ADDED,
RESOURCES.ATTACHMENT,
newAttachment);

res.status(201).json(response);
return Promise.resolve();
})
.catch((error) => {
req.log.error('Error adding file attachment', error);
const rerr = error;
rerr.status = rerr.status || 500;
next(rerr);
});
link,
);

res.status(201).json(link);
return;
}

const shouldTransfer = process.env.NODE_ENV !== 'development' || config.get('enableFileUpload') === 'true';
const downloadUrlPromise = shouldTransfer
? getDownloadUrl(destBucket, destKey)
: Promise.resolve(destKey);

req.log.debug('creating db file record');
const attachmentInstance = await models.ProjectAttachment.create({
projectId,
allowedUsers,
createdBy: req.authUser.userId,
updatedBy: req.authUser.userId,
title: data.title,
size: data.size,
category: data.category || null,
description: data.description,
contentType: data.contentType,
path: destKey,
type: data.type,
tags: data.tags,
});

const newAttachment = attachmentInstance.get({ plain: true });
req.log.debug('New Attachment record: ', newAttachment);

const downloadUrl = await downloadUrlPromise;
req.log.debug('Retrieved presigned url for new attachment');

let response = _.cloneDeep(newAttachment);
response = _.omit(response, ['path', 'deletedAt']);
response.downloadUrl = downloadUrl;

util.sendResourceToKafkaBus(
req,
EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_ADDED,
RESOURCES.ATTACHMENT,
newAttachment,
);

res.status(201).json(response);

if (shouldTransfer) {
util.s3FileTransfer(req, sourceBucket, sourceKey, destBucket, destKey)
.then(() => {
req.log.debug('File attachment copied asynchronously', { attachmentId: newAttachment.id });
})
.catch((error) => {
req.log.error('Async S3 file transfer failed', {
error: error.message,
stack: error.stack,
attachmentId: newAttachment.id,
source: `${sourceBucket}/${sourceKey}`,
destination: `${destBucket}/${destKey}`,
});
});
}
} catch (error) {
req.log.error('Error adding attachment', error);
const rerr = error;
rerr.status = rerr.status || 500;
next(rerr);
}
},
];
2 changes: 2 additions & 0 deletions src/routes/copilotOpportunity/get.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module.exports = [
}

const isAdminOrManager = util.hasRoles(req, [USER_ROLE.CONNECT_ADMIN, USER_ROLE.TOPCODER_ADMIN, USER_ROLE.PROJECT_MANAGER]);

return models.CopilotOpportunity.findOne({
where: { id },
include: isAdminOrManager ? [
Expand Down Expand Up @@ -43,6 +44,7 @@ module.exports = [
if (req.authUser) {
canApplyAsCopilot = !memberIds.includes(req.authUser.userId)
}

if (plainOpportunity.project) {
// This shouldn't be exposed to the clientside
delete plainOpportunity.project.members;
Expand Down
4 changes: 2 additions & 2 deletions src/routes/copilotOpportunity/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ module.exports = [
baseOrder.push([sortParams[0], sortParams[1]]);

return models.CopilotOpportunity.findAll({
include: isAdminOrManager ? [
include: isAdminOrManager ?[
{
model: models.CopilotRequest,
as: 'copilotRequest',
Expand All @@ -57,7 +57,7 @@ module.exports = [
{
model: models.CopilotRequest,
as: 'copilotRequest',
},
}
],
order: baseOrder,
limit,
Expand Down
39 changes: 20 additions & 19 deletions src/routes/copilotOpportunityApply/list.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import _ from 'lodash';
import { middleware as tcMiddleware } from 'tc-core-library-js';

import models from '../../models';
import { ADMIN_ROLES } from '../../constants';
import util from '../../util';

const permissions = tcMiddleware.permissions;

module.exports = [
permissions('copilotApplications.view'),
(req, res, next) => {
const canAccessAllApplications = util.hasRoles(req, ADMIN_ROLES) || util.hasProjectManagerRole(req);
const userId = req.authUser.userId;
const isAdminOrPM = util.hasRoles(req, ADMIN_ROLES) || util.hasProjectManagerRole(req);
const opportunityId = _.parseInt(req.params.id);

let sort = req.query.sort ? decodeURIComponent(req.query.sort) : 'createdAt desc';
Expand All @@ -24,17 +19,15 @@ module.exports = [
}
const sortParams = sort.split(' ');

// Admin can see all requests and the PM can only see requests created by them
const whereCondition = _.assign({
opportunityId,
},
canAccessAllApplications ? {} : { createdBy: userId },
);

return models.CopilotOpportunity.findOne({
where: {
id: opportunityId,
}
},
}).then((opportunity) => {
if (!opportunity) {
const err = new Error('No opportunity found');
Expand All @@ -51,13 +44,13 @@ module.exports = [
],
order: [[sortParams[0], sortParams[1]]],
})
.then(copilotApplications => {
.then((copilotApplications) => {
req.log.debug(`CopilotApplications ${JSON.stringify(copilotApplications)}`);
return models.ProjectMember.getActiveProjectMembers(opportunity.projectId).then((members) => {
req.log.debug(`Fetched existing active members ${JSON.stringify(members)}`);
req.log.debug(`Applications ${JSON.stringify(copilotApplications)}`);
const enrichedApplications = copilotApplications.map(application => {
const m = members.find(m => m.userId === application.userId);
const enrichedApplications = copilotApplications.map((application) => {
const member = members.find(memberItem => memberItem.userId === application.userId);

// Using spread operator fails in lint check
// While Object.assign fails silently during run time
Expand All @@ -77,22 +70,30 @@ module.exports = [
copilotOpportunity: application.copilotOpportunity,
};

if (m) {
enriched.existingMembership = m;
if (member) {
enriched.existingMembership = member;
}

req.log.debug(`Existing member to application ${JSON.stringify(enriched)}`);

return enriched;
});

const response = isAdminOrPM
? enrichedApplications
: enrichedApplications.map(app => ({
userId: app.userId,
status: app.status,
createdAt: app.createdAt,
}));

req.log.debug(`Enriched Applications ${JSON.stringify(enrichedApplications)}`);
res.status(200).send(enrichedApplications);
res.status(200).send(response);
});
})
});
})
.catch((err) => {
util.handleError('Error fetching copilot applications', err, req, next);
});
.catch((err) => {
util.handleError('Error fetching copilot applications', err, req, next);
});
},
];