diff --git a/README.md b/README.md index fecd2bd9..dc66b0dd 100644 --- a/README.md +++ b/README.md @@ -61,21 +61,21 @@ - first it would be waiting for `kafka-client` to create all the required topics and exit, you would see: ``` - tc-taas-es-procesor | Waiting for kafka-client to exit.... + tc-taas-es-processor | Waiting for kafka-client to exit.... ``` - after that, `taas-es-processor` would be started itself. Make sure it successfully connected to Kafka, you should see 9 lines with text `Subscribed to taas.`: ``` - tc-taas-es-procesor | 2021-01-22T14:27:48.971Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.create:0 offset 0 leader kafka:9093 - tc-taas-es-procesor | 2021-01-22T14:27:48.972Z DEBUG no-kafka-client Subscribed to taas.job.create:0 offset 0 leader kafka:9093 - tc-taas-es-procesor | 2021-01-22T14:27:48.972Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.delete:0 offset 0 leader kafka:9093 - tc-taas-es-procesor | 2021-01-22T14:27:48.973Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.delete:0 offset 0 leader kafka:9093 - tc-taas-es-procesor | 2021-01-22T14:27:48.974Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.update:0 offset 0 leader kafka:9093 - tc-taas-es-procesor | 2021-01-22T14:27:48.975Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.create:0 offset 0 leader kafka:9093 - tc-taas-es-procesor | 2021-01-22T14:27:48.976Z DEBUG no-kafka-client Subscribed to taas.job.delete:0 offset 0 leader kafka:9093 - tc-taas-es-procesor | 2021-01-22T14:27:48.977Z DEBUG no-kafka-client Subscribed to taas.job.update:0 offset 0 leader kafka:9093 - tc-taas-es-procesor | 2021-01-22T14:27:48.978Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.update:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-01-22T14:27:48.971Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.create:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-01-22T14:27:48.972Z DEBUG no-kafka-client Subscribed to taas.job.create:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-01-22T14:27:48.972Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.delete:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-01-22T14:27:48.973Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.delete:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-01-22T14:27:48.974Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.update:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-01-22T14:27:48.975Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.create:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-01-22T14:27:48.976Z DEBUG no-kafka-client Subscribed to taas.job.delete:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-01-22T14:27:48.977Z DEBUG no-kafka-client Subscribed to taas.job.update:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-01-22T14:27:48.978Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.update:0 offset 0 leader kafka:9093 ``` diff --git a/config/default.js b/config/default.js index 7831923c..e475a677 100644 --- a/config/default.js +++ b/config/default.js @@ -116,8 +116,13 @@ module.exports = { // the emails address for receiving the issue report // REPORT_ISSUE_EMAILS may contain comma-separated list of email which is converted to array REPORT_ISSUE_EMAILS: (process.env.REPORT_ISSUE_EMAILS || '').split(','), + // the emails address for receiving the issue report + // REPORT_ISSUE_EMAILS may contain comma-separated list of email which is converted to array + REQUEST_EXTENSION_EMAILS: (process.env.REQUEST_EXTENSION_EMAILS || '').split(','), // SendGrid email template ID for reporting issue REPORT_ISSUE_SENDGRID_TEMPLATE_ID: process.env.REPORT_ISSUE_SENDGRID_TEMPLATE_ID, + // SendGrid email template ID for requesting extension + REQUEST_EXTENSION_SENDGRID_TEMPLATE_ID: process.env.REQUEST_EXTENSION_SENDGRID_TEMPLATE_ID, // the URL where TaaS App is hosted TAAS_APP_URL: process.env.TAAS_APP_URL || 'https://platform.topcoder-dev.com/taas/myteams' } diff --git a/config/email_template.config.js b/config/email_template.config.js index ea4375f7..bc2d803a 100644 --- a/config/email_template.config.js +++ b/config/email_template.config.js @@ -22,6 +22,7 @@ module.exports = { recipients: config.REPORT_ISSUE_EMAILS, sendgridTemplateId: config.REPORT_ISSUE_SENDGRID_TEMPLATE_ID }, + /* Report issue for a particular member * * - userHandle: the user handle. Example: "bili_2021" @@ -39,5 +40,24 @@ module.exports = { '{{reportText}}', recipients: config.REPORT_ISSUE_EMAILS, sendgridTemplateId: config.REPORT_ISSUE_SENDGRID_TEMPLATE_ID + }, + + /* Request extension for a particular member + * + * - userHandle: the user handle. Example: "bili_2021" + * - projectId: the project ID. Example: 123412 + * - projectName: the project name. Example: "TaaS API Misc Updates" + * - text: comment for the request. Example: "I would like to keep working with this member for 2 months..." + */ + 'extension-request': { + subject: 'Extension Requested for member {{userHandle}} on TaaS Team {{projectName}} ({{projectId}}).', + body: 'User Handle: {{userHandle}}' + '\n' + + 'Project Name: {{projectName}}' + '\n' + + 'Project ID: {{projectId}}' + '\n' + + `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '\n' + + '\n' + + '{{text}}', + recipients: config.REPORT_ISSUE_EMAILS, + sendgridTemplateId: config.REQUEST_EXTENSION_SENDGRID_TEMPLATE_ID } } diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 6daa5e1c..ec79947b 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -4407,7 +4407,7 @@ }, "response": [] }, - { + { "name": "POST /taas-teams/email - team-issue-report", "request": { "method": "POST", @@ -4483,51 +4483,89 @@ }, "response": [] }, - { - "name": "POST /taas-teams/:id/members", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_administrator}}" - }, - { - "key": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"handles\": [\n \"tester1234\",\n \"non-existing\"\n ],\n \"emails\": [\n \"non-existing@domain.com\",\n \"email@domain.com\"\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{URL}}/taas-teams/:id/members", - "host": [ - "{{URL}}" - ], - "path": [ - "taas-teams", - ":id", - "members" - ], - "variable": [ - { - "key": "id", - "value": "16705" - } - ] - } - }, - "response": [] - }, + { + "name": "POST /taas-teams/email - extension-request", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"template\": \"extension-request\",\n \"data\": {\n \"projectName\": \"TaaS Project Name\",\n \"projectId\": 12345,\n \"userHandle\": \"pshah_manager\",\n \"reportText\": \"I would like to keep working with this member for 2 months...\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/email", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "email" + ] + } + }, + "response": [] + }, + { + "name": "POST /taas-teams/:id/members", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"handles\": [\n \"tester1234\",\n \"non-existing\"\n ],\n \"emails\": [\n \"non-existing@domain.com\",\n \"email@domain.com\"\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/:id/members", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + ":id", + "members" + ], + "variable": [ + { + "key": "id", + "value": "16705" + } + ] + } + }, + "response": [] + }, { "name": "GET /taas-teams/:id/members", "request": { @@ -9772,4 +9810,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 16f11034..5b8f7d2b 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -23,6 +23,11 @@ paths: Create job. **Authorization** All topcoder members are allowed + + Permission rules on field `isApplicationPageActive`: + - M2M user is allowed to set the value of the field + - Other users are not allowed to set the value of the field + security: - bearerAuth: [] requestBody: @@ -352,6 +357,10 @@ paths: Update the job. **Authorization** Every topcoder member can update the job he/she created. bookingmanager and connectmember can update all jobs. + + Permission rules on field `isApplicationPageActive`: + - M2M user is allowed to update the value of the field + - Other users are not allowed to update the value of the field security: - bearerAuth: [] parameters: @@ -413,6 +422,10 @@ paths: Update job. **Authorization** Topcoder token with patch job scope is allowed + + Permission rules on field `isApplicationPageActive`: + - M2M user is allowed to update the value of the field + - Other users are not allowed to update the value of the field security: - bearerAuth: [] parameters: @@ -1980,6 +1993,9 @@ components: description: "The job candidates." items: $ref: '#/components/schemas/JobCandidate' + isApplicationPageActive: + type: boolean + default: false createdAt: type: string format: date-time @@ -2057,6 +2073,9 @@ components: type: string format: uuid description: "The skill id." + isApplicationPageActive: + type: boolean + default: false JobCandidate: required: - id @@ -2186,6 +2205,9 @@ components: type: string format: uuid description: "The skill id." + isApplicationPageActive: + type: boolean + default: false ResourceBooking: required: - id diff --git a/local/docker-compose.yml b/local/docker-compose.yml index 7c7104b8..3894d100 100644 --- a/local/docker-compose.yml +++ b/local/docker-compose.yml @@ -45,7 +45,7 @@ services: - 9200:9200 taas-es-processor: - container_name: tc-taas-es-procesor + container_name: tc-taas-es-processor build: context: ./generic-tc-service args: diff --git a/migrations/2021-02-27-job-add-is-application-page-active-field.js b/migrations/2021-02-27-job-add-is-application-page-active-field.js new file mode 100644 index 00000000..84e548a4 --- /dev/null +++ b/migrations/2021-02-27-job-add-is-application-page-active-field.js @@ -0,0 +1,19 @@ +/* + * Add isApplicationPageActive field to the Job model. + */ + +module.exports = { + up: queryInterface => { + return Promise.all([ + queryInterface.sequelize.query('ALTER TABLE bookings.jobs ADD is_application_page_active BOOLEAN NOT NULL DEFAULT false'), + // this command looks like does nothing, because we already set default value to `null` and this column cannot be `NULL` + // but we keep it as it was tested this way, and it looks harmful + queryInterface.sequelize.query('UPDATE bookings.jobs SET is_application_page_active=false WHERE is_application_page_active is NULL'), + ]) + }, + down: queryInterface => { + return Promise.all([ + queryInterface.sequelize.query('ALTER TABLE bookings.jobs DROP is_application_page_active') + ]) + } +} diff --git a/src/common/helper.js b/src/common/helper.js index cb018ac6..50adc94b 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -70,6 +70,7 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { workload: { type: 'keyword' }, skills: { type: 'keyword' }, status: { type: 'keyword' }, + isApplicationPageActive: { type: 'boolean' }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, diff --git a/src/models/Job.js b/src/models/Job.js index c124caec..49d34ff7 100644 --- a/src/models/Job.js +++ b/src/models/Job.js @@ -98,6 +98,12 @@ module.exports = (sequelize) => { type: Sequelize.STRING(255), allowNull: false }, + isApplicationPageActive: { + field: 'is_application_page_active', + type: Sequelize.BOOLEAN, + defaultValue: false, + allowNull: false + }, createdBy: { field: 'created_by', type: Sequelize.UUID, diff --git a/src/services/JobService.js b/src/services/JobService.js index bea40b45..7d855bd0 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -148,6 +148,11 @@ async function createJob (currentUser, job) { await helper.checkIsMemberOfProject(currentUser.userId, job.projectId) } + // the "isApplicationPageActive" field can be set/updated only by M2M user + if (!_.isUndefined(job.isApplicationPageActive) && !currentUser.isMachine) { + throw new errors.ForbiddenError('You are not allowed to set/update the value of field "isApplicationPageActive".') + } + await _validateSkills(job.skills) job.id = uuid() job.createdBy = await helper.getUserId(currentUser.userId) @@ -171,7 +176,8 @@ createJob.schema = Joi.object().keys({ resourceType: Joi.stringAllowEmpty().allow(null), rateType: Joi.rateType().allow(null), workload: Joi.workload().allow(null), - skills: Joi.array().items(Joi.string().uuid()).required() + skills: Joi.array().items(Joi.string().uuid()).required(), + isApplicationPageActive: Joi.boolean() }).required() }).required() @@ -188,6 +194,12 @@ async function updateJob (currentUser, id, data) { } let job = await Job.findById(id) const oldValue = job.toJSON() + + // the "isApplicationPageActive" field can be set/updated only by M2M user + if (!_.isUndefined(data.isApplicationPageActive) && !currentUser.isMachine) { + throw new errors.ForbiddenError('You are not allowed to set/update the value of field "isApplicationPageActive".') + } + const ubahnUserId = await helper.getUserId(currentUser.userId) if (!currentUser.hasManagePermission && !currentUser.isMachine) { // Check whether user can update the job. @@ -232,7 +244,8 @@ partiallyUpdateJob.schema = Joi.object().keys({ resourceType: Joi.stringAllowEmpty().allow(null), rateType: Joi.rateType().allow(null), workload: Joi.workload().allow(null), - skills: Joi.array().items(Joi.string().uuid()) + skills: Joi.array().items(Joi.string().uuid()), + isApplicationPageActive: Joi.boolean() }).required() }).required() @@ -262,7 +275,8 @@ fullyUpdateJob.schema = Joi.object().keys({ rateType: Joi.rateType().allow(null).default(null), workload: Joi.workload().allow(null).default(null), skills: Joi.array().items(Joi.string().uuid()).required(), - status: Joi.jobStatus().default('sourcing') + status: Joi.jobStatus().default('sourcing'), + isApplicationPageActive: Joi.boolean() }).required() }).required()