diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..0bef4f175 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,22 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/debian +{ + "name": "Debian", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/base:trixie", + "features": { + "ghcr.io/devcontainers/features/node:1": {} + } + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..9cd2d2b67 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,41 @@ +stages: + - build + - publish + +build_image: + stage: build + image: + name: gcr.io/kaniko-project/executor:debug + entrypoint: [""] + script: + - |- + tag=$CI_COMMIT_SHORT_SHA + if [[ -n "$CI_COMMIT_TAG" ]]; then + tag=$CI_COMMIT_TAG + fi + - mkdir -p /kaniko/.docker + - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json + - > + /kaniko/executor + --context $CI_PROJECT_DIR + --dockerfile $CI_PROJECT_DIR/Dockerfile + --destination $CI_REGISTRY_IMAGE:${tag} + --cache=true + +push_image_harbor: + stage: publish + image: + name: gcr.io/kaniko-project/executor:debug + entrypoint: [''] + script: + - mkdir -p /kaniko/.docker + - echo "{\"auths\":{\"${HARBOR_HOST}\":{\"auth\":\"$(echo -n ${HARBOR_USERNAME}:${HARBOR_PASSWORD} | base64 -w 0)\"}}}" > /kaniko/.docker/config.json + - >- + /kaniko/executor + --context "${CI_PROJECT_DIR}" + --dockerfile ${CI_PROJECT_DIR}/Dockerfile + --destination "${HARBOR_HOST}/${HARBOR_PROJECT}/${CI_PROJECT_NAME}:${CI_COMMIT_TAG}" + --cache=true + rules: + - if: $CI_COMMIT_TAG + diff --git a/nest-cli.json b/nest-cli.json index 473da2a0a..b273600d6 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -2,6 +2,9 @@ "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { + "assets": [ + { "include": "common/email-templates/*.hbs", "watchAssets": true } + ], "plugins": [{ "name": "@nestjs/swagger", "options": { diff --git a/src/common/common.module.ts b/src/common/common.module.ts index b43ed68bb..35bbb9ab7 100644 --- a/src/common/common.module.ts +++ b/src/common/common.module.ts @@ -1,5 +1,5 @@ import { Module } from "@nestjs/common"; -import { MailService } from "./mail.service"; +import { MailService } from "./services/mail.service"; @Module({ providers: [MailService], diff --git a/src/common/email-templates/doi-registered.hbs b/src/common/email-templates/doi-registered.hbs new file mode 100644 index 000000000..8ece3d055 --- /dev/null +++ b/src/common/email-templates/doi-registered.hbs @@ -0,0 +1,8 @@ +

Hello,

+ +

Your DOI has been successfully registered: {{doi}}

+ +

Please proceed to DESY's publication database PubDB.

+ +

Best regards,
+Your Service Team

\ No newline at end of file diff --git a/src/common/handlebars-helpers.ts b/src/common/handlebars-helpers.ts index d52dc4a2c..58fea4e9e 100644 --- a/src/common/handlebars-helpers.ts +++ b/src/common/handlebars-helpers.ts @@ -132,6 +132,9 @@ export const urlencode = (context: string): string => { return encodeURIComponent(context); }; +export const exchangeString = (registerDoiUri: string, search: string, replacement: string): string => { + return registerDoiUri.replace(search, replacement); +}; /** * Format a unit using mathjs. * @@ -191,6 +194,7 @@ export const handlebarsHelpers: hb.HelperDeclareSpec = { jsonify: jsonify, job_v3: job_v3, urlencode: urlencode, + exchangeString: exchangeString, base64enc: base64enc, base64dec: atob, formatUnit: formatUnit, diff --git a/src/common/mail.service.ts b/src/common/services/mail.service.ts similarity index 55% rename from src/common/mail.service.ts rename to src/common/services/mail.service.ts index 705276505..b3f0cc7d8 100644 --- a/src/common/mail.service.ts +++ b/src/common/services/mail.service.ts @@ -1,5 +1,6 @@ import { ISendMailOptions, MailerService } from "@nestjs-modules/mailer"; import { Injectable, Logger } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; import { isAxiosError } from "@nestjs/terminus/dist/utils"; import { SentMessageInfo } from "nodemailer"; @@ -10,7 +11,10 @@ import { SentMessageInfo } from "nodemailer"; */ @Injectable() export class MailService { - constructor(private readonly mailerService: MailerService) {} + constructor( + private readonly mailerService: MailerService, + private readonly configService: ConfigService, + ) {} async sendMail(options: ISendMailOptions): Promise { try { @@ -39,4 +43,32 @@ export class MailService { } } } + + async sendTemplateEmail(doi: string, userEmail: string): Promise { + const templateName = "doi-registered"; // hdb + const registerDoiUri = this.configService.get("registerDoiUri"); + Logger.debug(`registerDoiUri is ${registerDoiUri}`); + + try { + const mailOptions: ISendMailOptions = { + to: userEmail, + subject: `DOI registration confirmed: ${doi}.`, + template: templateName, + context: { + registerDoiUri: registerDoiUri, + doi: doi, + }, + }; + + if (!mailOptions.template) { + throw new Error("INTERNAL ERROR: template property is missing!"); + } + + Logger.debug(`mailOptions: ${JSON.stringify(mailOptions)}`); + await this.sendMail(mailOptions); + Logger.log(`DOI registration email sent to ${userEmail}.`); + } catch (error) { + Logger.error(`Failed to send email: ${JSON.stringify(error)}`); + } + } } diff --git a/src/config/configuration.ts b/src/config/configuration.ts index b613033ef..0e318fa3e 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -4,6 +4,7 @@ import localconfiguration from "./localconfiguration"; import { boolean } from "mathjs"; import { DEFAULT_PROPOSAL_TYPE } from "src/proposals/schemas/proposal.schema"; import { DatasetType } from "src/datasets/types/dataset-type.enum"; +import { Logger } from "@nestjs/common"; const configuration = () => { const accessGroupsStaticValues = @@ -85,12 +86,14 @@ const configuration = () => { publishedDataConfig: process.env.PUBLISHED_DATA_CONFIG_FILE || "publishedDataConfig.json", }; + Object.keys(jsonConfigFileList).forEach((key) => { const filePath = jsonConfigFileList[key]; if (fs.existsSync(filePath)) { const data = fs.readFileSync(filePath, "utf8"); try { jsonConfigMap[key] = JSON.parse(data); + Logger.debug(`From ${filePath} parsing data ${data}.`); } catch (error) { console.error( "Error json config file parsing " + filePath + " : " + error, @@ -123,6 +126,7 @@ const configuration = () => { `Example configuration file ${exampleFilePath} does not exist. Using empty configuration for ${key}`, ); jsonConfigMap[key] = {}; + Logger.debug(`jsonConfigMap[key] is ${jsonConfigMap[key]}`); } } } diff --git a/src/config/job-config/actions/emailaction/emailaction.service.ts b/src/config/job-config/actions/emailaction/emailaction.service.ts index cdc217aa3..669a8d90a 100644 --- a/src/config/job-config/actions/emailaction/emailaction.service.ts +++ b/src/config/job-config/actions/emailaction/emailaction.service.ts @@ -6,7 +6,7 @@ import { } from "../../jobconfig.interface"; import { isEmailJobActionOptions } from "./emailaction.interface"; import { EmailJobAction } from "./emailaction"; -import { MailService } from "src/common/mail.service"; +import { MailService } from "src/common/services/mail.service"; @Injectable() export class EmailJobActionCreator implements JobActionCreator { diff --git a/src/config/job-config/actions/emailaction/emailaction.spec.ts b/src/config/job-config/actions/emailaction/emailaction.spec.ts index a9c3c3b90..e1fafafc8 100644 --- a/src/config/job-config/actions/emailaction/emailaction.spec.ts +++ b/src/config/job-config/actions/emailaction/emailaction.spec.ts @@ -11,7 +11,7 @@ import { EmailJobAction } from "./emailaction"; import { EmailJobActionOptions } from "./emailaction.interface"; import { JobClass } from "../../../../jobs/schemas/job.schema"; -import { MailService } from "src/common/mail.service"; +import { MailService } from "src/common/services/mail.service"; import { MailerService } from "@nestjs-modules/mailer"; import { DatasetClass } from "src/datasets/schemas/dataset.schema"; import { CreateJobDto } from "src/jobs/dto/create-job.dto"; diff --git a/src/config/job-config/actions/emailaction/emailaction.ts b/src/config/job-config/actions/emailaction/emailaction.ts index 11a77db7b..b5efe01ce 100644 --- a/src/config/job-config/actions/emailaction/emailaction.ts +++ b/src/config/job-config/actions/emailaction/emailaction.ts @@ -13,7 +13,7 @@ import { } from "../../jobconfig.interface"; import { ISendMailOptions } from "@nestjs-modules/mailer"; import { actionType, EmailJobActionOptions } from "./emailaction.interface"; -import { MailService } from "src/common/mail.service"; +import { MailService } from "src/common/services/mail.service"; /** * Send an email following a job diff --git a/src/jobs/jobs.service.spec.ts b/src/jobs/jobs.service.spec.ts index 2016267c5..1662fd1f7 100644 --- a/src/jobs/jobs.service.spec.ts +++ b/src/jobs/jobs.service.spec.ts @@ -2,7 +2,7 @@ import { ConfigService } from "@nestjs/config"; import { getModelToken } from "@nestjs/mongoose"; import { Test, TestingModule } from "@nestjs/testing"; import { Model } from "mongoose"; -import { MailService } from "src/common/mail.service"; +import { MailService } from "src/common/services/mail.service"; import { DatasetsService } from "src/datasets/datasets.service"; import { DatasetsAccessService } from "src/datasets/datasets-access.service"; import { PoliciesService } from "src/policies/policies.service"; diff --git a/src/published-data/published-data.module.ts b/src/published-data/published-data.module.ts index 3878ac7bd..682f03c01 100644 --- a/src/published-data/published-data.module.ts +++ b/src/published-data/published-data.module.ts @@ -18,6 +18,7 @@ import { } from "./schemas/published-data.schema"; import { applyHistoryPluginOnce } from "src/common/mongoose/plugins/history.plugin.util"; import { PublishedDataV4Controller } from "./published-data.v4.controller"; +import { MailService } from "src/common/services/mail.service"; @Module({ imports: [ @@ -65,6 +66,6 @@ import { PublishedDataV4Controller } from "./published-data.v4.controller"; ProposalsModule, ], controllers: [PublishedDataController, PublishedDataV4Controller], - providers: [PublishedDataService], + providers: [PublishedDataService, MailService], }) export class PublishedDataModule {} diff --git a/src/published-data/published-data.v4.controller.ts b/src/published-data/published-data.v4.controller.ts index 6f25c91dc..dc44cb23d 100644 --- a/src/published-data/published-data.v4.controller.ts +++ b/src/published-data/published-data.v4.controller.ts @@ -57,6 +57,8 @@ import { PublishedData, PublishedDataDocument, } from "./schemas/published-data.schema"; +import { MailService } from "src/common/services/mail.service"; +import { logger } from "handlebars"; @ApiBearerAuth() @ApiTags("published data v4") @@ -76,6 +78,7 @@ export class PublishedDataV4Controller { private readonly proposalsService: ProposalsService, private readonly publishedDataService: PublishedDataService, private caslAbilityFactory: CaslAbilityFactory, + private readonly mailService: MailService, ) {} @AllowAny() @@ -617,6 +620,9 @@ export class PublishedDataV4Controller { }; try { + + console.log("registerDataciteDoiOptions is", JSON.stringify(registerDataciteDoiOptions)); + await firstValueFrom( this.httpService.request(registerDataciteDoiOptions), ); @@ -636,6 +642,12 @@ export class PublishedDataV4Controller { { status: PublishedDataStatus.REGISTERED, registeredTime: new Date() }, ); + const user = request.user as JWTUser; + await this.mailService.sendTemplateEmail( + publishedData.doi, // Replacements for the template + user.email, // Recipient email + ); + return res; }