From d67b00d820b843022b705a5c7f26d44b4b68d10a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 15:38:21 +0000 Subject: [PATCH 1/8] build(deps): bump nodemailer from 6.9.16 to 6.10.0 Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 6.9.16 to 6.10.0. - [Release notes](https://github.com/nodemailer/nodemailer/releases) - [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodemailer/nodemailer/compare/v6.9.16...v6.10.0) --- updated-dependencies: - dependency-name: nodemailer dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2d85c4c29..a5c7a7f38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10275,6 +10275,16 @@ "node": ">=0.10.0" } }, + "node_modules/mailparser/node_modules/nodemailer": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", + "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", + "license": "MIT-0", + "optional": true, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/mailsplit": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.2.tgz", @@ -11441,9 +11451,10 @@ "dev": true }, "node_modules/nodemailer": { - "version": "6.9.16", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", - "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", + "integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==", + "license": "MIT-0", "engines": { "node": ">=6.0.0" } From 894bb4d4be7e05042452db207e27d86dad8c0d4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 15:50:03 +0000 Subject: [PATCH 2/8] build(deps-dev): bump eslint from 9.18.0 to 9.19.0 Bumps [eslint](https://github.com/eslint/eslint) from 9.18.0 to 9.19.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v9.18.0...v9.19.0) --- updated-dependencies: - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index a5c7a7f38..726806b5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1381,10 +1381,11 @@ "dev": true }, "node_modules/@eslint/js": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz", - "integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==", + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz", + "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -6850,17 +6851,18 @@ } }, "node_modules/eslint": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz", - "integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==", + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz", + "integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.10.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.18.0", + "@eslint/js": "9.19.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", From 2a4c2ca5085a84a36489ec453aa38376d398b290 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:01:59 +0000 Subject: [PATCH 3/8] build(deps-dev): bump mocha from 11.0.1 to 11.1.0 Bumps [mocha](https://github.com/mochajs/mocha) from 11.0.1 to 11.1.0. - [Release notes](https://github.com/mochajs/mocha/releases) - [Changelog](https://github.com/mochajs/mocha/blob/main/CHANGELOG.md) - [Commits](https://github.com/mochajs/mocha/compare/v11.0.1...v11.1.0) --- updated-dependencies: - dependency-name: mocha dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 66 +++++------------------------------------------ 1 file changed, 6 insertions(+), 60 deletions(-) diff --git a/package-lock.json b/package-lock.json index 726806b5d..e466ecbcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11018,10 +11018,11 @@ } }, "node_modules/mocha": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.0.1.tgz", - "integrity": "sha512-+3GkODfsDG71KSCQhc4IekSW+ItCK/kiez1Z28ksWvYhKXV/syxMlerR/sC7whDp7IyreZ4YxceMLdTs5hQE8A==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", + "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", "dev": true, + "license": "MIT", "dependencies": { "ansi-colors": "^4.1.3", "browser-stdout": "^1.3.1", @@ -11040,8 +11041,8 @@ "strip-json-comments": "^3.1.1", "supports-color": "^8.1.1", "workerpool": "^6.5.1", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", "yargs-unparser": "^2.0.0" }, "bin": { @@ -11061,17 +11062,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/mocha/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, "node_modules/mocha/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -11105,50 +11095,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/mocha/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/mocha/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/mongodb": { "version": "6.12.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz", From f589253c1570a2eb19bd1abff761c9aa4458aa2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:13:51 +0000 Subject: [PATCH 4/8] build(deps-dev): bump @types/node from 22.10.7 to 22.10.10 Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.10.7 to 22.10.10. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e466ecbcb..4c634636f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3820,9 +3820,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.10.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", - "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", + "version": "22.10.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz", + "integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==", "license": "MIT", "dependencies": { "undici-types": "~6.20.0" From 93d818cfdd7f1ab916b1a122b7c08efd72592c3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:25:42 +0000 Subject: [PATCH 5/8] build(deps): bump mathjs from 14.0.1 to 14.1.0 Bumps [mathjs](https://github.com/josdejong/mathjs) from 14.0.1 to 14.1.0. - [Changelog](https://github.com/josdejong/mathjs/blob/develop/HISTORY.md) - [Commits](https://github.com/josdejong/mathjs/compare/v14.0.1...v14.1.0) --- updated-dependencies: - dependency-name: mathjs dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 60 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4c634636f..66091e9c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2334,6 +2334,31 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lambdatest/node-tunnel": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@lambdatest/node-tunnel/-/node-tunnel-4.0.8.tgz", + "integrity": "sha512-IY42aDD4Ryqjug9V4wpCjckKpHjC2zrU/XhhorR5ztX088XITRFKUo8U6+gOjy/V8kAB+EgDuIXfK0izXbt9Ow==", + "license": "ISC", + "dependencies": { + "adm-zip": "^0.5.10", + "axios": "^1.6.2", + "get-port": "^1.0.0", + "https-proxy-agent": "^5.0.0", + "split": "^1.0.1" + } + }, + "node_modules/@lambdatest/node-tunnel/node_modules/get-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-1.0.0.tgz", + "integrity": "sha512-vg59F3kcXBOtcIijwtdAyCxFocyv/fVkGQvw1kVGrxFO1U4SSGkGjrbASg5DN3TVekVle/jltwOjYRnZWc1YdA==", + "license": "MIT", + "bin": { + "get-port": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -4527,6 +4552,15 @@ "node": ">=0.4.0" } }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -10336,11 +10370,13 @@ } }, "node_modules/mathjs": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.0.1.tgz", - "integrity": "sha512-yyJgLwC6UXuve724np8tHRMYaTtb5UqiOGQkjwbSXgH8y1C/LcJ0pvdNDZLI2LT7r+iExh2Y5HwfAY+oZFtGIQ==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.1.0.tgz", + "integrity": "sha512-W/aOnRs7YxoUfUe0si968BJ41Q07tApNNZL3JRpgnPdDO+qQO4YXbVdT8muP0vCrYBkmKGn/izseXlrAITgYqg==", + "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.25.7", + "@lambdatest/node-tunnel": "^4.0.8", "complex.js": "^2.2.5", "decimal.js": "^10.4.3", "escape-latex": "^1.2.0", @@ -13331,6 +13367,18 @@ "memory-pager": "^1.0.2" } }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "license": "MIT", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -13827,6 +13875,12 @@ "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==", "dev": true }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, "node_modules/time-span": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", From 1a82a2cf50a4d56eb749f080765627bdb6d1d9f1 Mon Sep 17 00:00:00 2001 From: Spencer Bliven Date: Tue, 28 Jan 2025 09:02:10 +0100 Subject: [PATCH 6/8] feat: support Microsoft Graph API for emails (#1628) Support Microsoft Graph API for emails Co-authored-by: Max Novelli --- .env.example | 8 +- README.md | 11 ++- src/app.module.ts | 57 +++++++++--- src/common/graph-mail.ts | 168 ++++++++++++++++++++++++++++++++++++ src/config/configuration.ts | 18 ++-- 5 files changed, 242 insertions(+), 20 deletions(-) create mode 100644 src/common/graph-mail.ts diff --git a/.env.example b/.env.example index 3a7455a4a..2778d109b 100644 --- a/.env.example +++ b/.env.example @@ -30,10 +30,14 @@ REGISTER_METADATA_URI="https://mds.test.datacite.org/metadata" DOI_USERNAME="username" DOI_PASSWORD="password" SITE= +EMAIL_TYPE=<"smtp"|"ms365"> +EMAIL_FROM= SMTP_HOST= -SMTP_MESSAGE_FROM= SMTP_PORT= -SMTP_SECURE= +SMTP_SECURE=<"yes"|"no"> +MS365_TENANT_ID= +MS365_CLIENT_ID= +MS365_CLIENT_SECRET= DATASET_CREATION_VALIDATION_ENABLED=true DATASET_CREATION_VALIDATION_REGEX="^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$" diff --git a/README.md b/README.md index 1c4105bff..f0fc67a96 100644 --- a/README.md +++ b/README.md @@ -169,10 +169,15 @@ Valid environment variables for the .env file. See [.env.example](/.env.example) | `REGISTER_DOI_URI` | string | | URI to the organization that registers the facility's DOIs. | | | `REGISTER_METADATA_URI` | string | | URI to the organization that registers the facility's published data metadata. | | | `SITE` | string | | The name of your site. | | +| `EMAIL_TYPE` | string | Yes | The type of your email provider. Options are "smtp" or "ms365". | "smtp" | +| `EMAIL_FROM` | string | Yes | Email address that emails should be sent from. | | | `SMTP_HOST` | string | Yes | Host of SMTP server. | | -| `SMTP_MESSAGE_FROM` | string | Yes | Email address that emails should be sent from. | | -| `SMTP_PORT` | string | Yes | Port of SMTP server. | | -| `SMTP_SECURE` | string | Yes | Secure of SMTP server. | | +| `SMTP_MESSAGE_FROM` | string | Yes | (Deprecated) Alternate spelling of EMAIL_FROM.| | +| `SMTP_PORT` | number | Yes | Port of SMTP server. | 587 | +| `SMTP_SECURE` | bool | Yes | Use encrypted SMTPS. | "no" | +| `MS365_TENANT_ID` | string | Yes | Tenant ID for sending emails over Microsoft Graph API. | | +| `MS365_CLIENT_ID` | string | Yes | Client ID for sending emails over Microsoft Graph API | | +| `MS365_CLIENT_SECRET` | string | Yes | Client Secret for sending emails over Microsoft Graph API | | | `POLICY_PUBLICATION_SHIFT` | integer | Yes | Embargo period expressed in years. | 3 years | | `POLICY_RETENTION_SHIFT` | integer | Yes | Retention period (how long the facility will hold on to data) expressed in years. | -1 (indefinitely) | | `ELASTICSEARCH_ENABLED` | string | | Flag to enable/disable the Elasticsearch endpoints. Values "yes" or "no". | "no" | diff --git a/src/app.module.ts b/src/app.module.ts index 585824733..0898968b5 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -32,6 +32,9 @@ import { EventEmitterModule } from "@nestjs/event-emitter"; import { AdminModule } from "./admin/admin.module"; import { HealthModule } from "./health/health.module"; import { LoggerModule } from "./loggers/logger.module"; +import { HttpModule, HttpService } from "@nestjs/axios"; +import { MSGraphMailTransport } from "./common/graph-mail"; +import { TransportType } from "@nestjs-modules/mailer/dist/interfaces/mailer-options.interface"; import { MetricsModule } from "./metrics/metrics.module"; @Module({ @@ -61,17 +64,51 @@ import { MetricsModule } from "./metrics/metrics.module"; LogbooksModule, EventEmitterModule.forRoot(), MailerModule.forRootAsync({ - useFactory: async (configService: ConfigService) => { - const port = configService.get("smtp.port"); + imports: [ConfigModule, HttpModule], + useFactory: async ( + configService: ConfigService, + httpService: HttpService, + ) => { + let transport: TransportType; + const transportType = configService + .get("email.type") + ?.toLowerCase(); + if (transportType === "smtp") { + transport = { + host: configService.get("email.smtp.host"), + port: configService.get("email.smtp.port"), + secure: configService.get("email.smtp.secure"), + }; + } else if (transportType === "ms365") { + const tenantId = configService.get("email.ms365.tenantId"), + clientId = configService.get("email.ms365.clientId"), + clientSecret = configService.get( + "email.ms365.clientSecret", + ); + if (tenantId === undefined) { + throw new Error("Missing MS365_TENANT_ID"); + } + if (clientId === undefined) { + throw new Error("Missing MS365_CLIENT_ID"); + } + if (clientSecret === undefined) { + throw new Error("Missing MS365_CLIENT_SECRET"); + } + transport = new MSGraphMailTransport(httpService, { + tenantId, + clientId, + clientSecret, + }); + } else { + throw new Error( + `Invalid EMAIL_TYPE: ${transportType}. Expect on of "smtp" or "ms365"`, + ); + } + return { - transport: { - host: configService.get("smtp.host"), - port: port ? parseInt(port) : undefined, - secure: - configService.get("smtp.secure") === "yes" ? true : false, - }, + transport: transport, defaults: { - from: configService.get("smtp.messageFrom"), + from: configService.get("email.from"), }, template: { dir: join(__dirname, "./common/email-templates"), @@ -86,7 +123,7 @@ import { MetricsModule } from "./metrics/metrics.module"; }, }; }, - inject: [ConfigService], + inject: [ConfigService, HttpService], }), MongooseModule.forRootAsync({ useFactory: async (configService: ConfigService) => ({ diff --git a/src/common/graph-mail.ts b/src/common/graph-mail.ts new file mode 100644 index 000000000..f811bf325 --- /dev/null +++ b/src/common/graph-mail.ts @@ -0,0 +1,168 @@ +/** + * This defines a nodemailer transport implementing the MS365 Graph API. + * + * https://learn.microsoft.com/en-us/graph/api/resources/mail-api-overview + */ +import { SentMessageInfo, Transport } from "nodemailer"; +import MailMessage from "nodemailer/lib/mailer/mail-message"; +import { HttpService } from "@nestjs/axios"; +import { Address } from "nodemailer/lib/mailer"; +import { firstValueFrom } from "rxjs"; +import { Injectable, Logger } from "@nestjs/common"; + +// Define interface for access token response +interface TokenResponse { + access_token: string; + expires_in: number; +} + +interface MSGraphMailTransportOptions { + clientId: string; + clientSecret: string; + refreshToken?: string; + tenantId: string; +} + +function getAddress(address: string | Address): { + name?: string; + address: string; +} { + return typeof address === "object" ? address : { address }; +} + +// Define the Microsoft Graph Transport class +@Injectable() +export class MSGraphMailTransport implements Transport { + name: string; + version: string; + private clientId: string; + private clientSecret: string; + private refreshToken?: string; + private tenantId: string; + private cachedAccessToken: string | null = null; + private tokenExpiry: number | null = null; + + constructor( + private readonly httpService: HttpService, + options: MSGraphMailTransportOptions, + ) { + this.httpService.axiosRef.defaults.headers["Content-Type"] = + "application/json"; + this.name = "Microsoft Graph API Transport"; + this.version = "1.0.0"; + this.clientId = options.clientId; + this.clientSecret = options.clientSecret; + this.refreshToken = options.refreshToken; + this.tenantId = options.tenantId; + } + + // Method to send email using Microsoft Graph API + send( + mail: MailMessage, + callback: (err: Error | null, info?: SentMessageInfo) => void, + ): void { + this.getAccessToken().then( + (accessToken) => { + this.sendEmail(accessToken, mail).then( + (info) => { + callback(null, info); + }, + (err) => { + callback(err, undefined); + }, + ); + }, + (err) => { + callback(err, undefined); + }, + ); + } + + // Method to fetch or return cached access token + private getAccessToken(): Promise { + if ( + this.cachedAccessToken != null && + Date.now() < (this.tokenExpiry ?? 0) + ) { + return ((token: string) => + new Promise((resolve) => resolve(token)))( + this.cachedAccessToken, + ); + } + + const body: Record = { + client_id: this.clientId, + client_secret: this.clientSecret, + }; + if (this.refreshToken) { + body["refresh_token"] = this.refreshToken; + body["grant_type"] = "refresh_token"; + } else { + body["grant_type"] = "client_credentials"; + body["scope"] = "https://graph.microsoft.com/.default"; + } + + return firstValueFrom( + this.httpService.post( + `https://login.microsoftonline.com/${this.tenantId}/oauth2/v2.0/token`, + body, + { headers: { "Content-Type": "application/x-www-form-urlencoded" } }, + ), + ).then((response) => { + this.cachedAccessToken = response.data.access_token; + this.tokenExpiry = Date.now() + response.data.expires_in * 1000; + + return this.cachedAccessToken; + }); + } + + private sendEmail( + accessToken: string, + mail: MailMessage, + ): Promise { + const { to, subject, text, html, from } = mail.data; + + // Construct email payload for Microsoft Graph API + const emailPayload = { + message: { + subject: subject, + body: { + contentType: html ? "HTML" : "Text", + content: html || text, + }, + toRecipients: Array.isArray(to) + ? to.map((recipient: string | Address) => getAddress(recipient)) + : [{ emailAddress: { address: to } }], + }, + }; + + // Send the email using Microsoft Graph API + return firstValueFrom( + this.httpService.post( + `https://graph.microsoft.com/v1.0/users/${from}/sendMail`, + emailPayload, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }, + ), + ).then( + (response) => { + if (response.status === 202) { + return { + envelope: mail.message.getEnvelope(), + messageId: mail.message.messageId, + }; + } + + throw new Error("Failed to send email: " + response.statusText); + }, + (err) => { + Logger.error(err); + throw err; + }, + ); + } +} diff --git a/src/config/configuration.ts b/src/config/configuration.ts index a8056d972..1386e012f 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -216,11 +216,19 @@ const configuration = () => { doiUsername: process.env.DOI_USERNAME, doiPassword: process.env.DOI_PASSWORD, site: process.env.SITE, - smtp: { - host: process.env.SMTP_HOST, - messageFrom: process.env.SMTP_MESSAGE_FROM, - port: process.env.SMTP_PORT, - secure: process.env.SMTP_SECURE, + email: { + type: process.env.EMAIL_TYPE || "smtp", + from: process.env.EMAIL_FROM || process.env.SMTP_MESSAGE_FROM, + smtp: { + host: process.env.SMTP_HOST, + port: parseInt(process.env.SMTP_PORT || "587"), + secure: boolean(process.env?.SMTP_SECURE || false), + }, + ms365: { + tenantId: process.env.MS365_TENANT_ID, + clientId: process.env.MS365_CLIENT_ID, + clientSecret: process.env.MS365_CLIENT_SECRET, + }, }, policyTimes: { policyPublicationShiftInYears: process.env.POLICY_PUBLICATION_SHIFT ?? 3, From 8ac01ba4abeb49b814f2ab944daa19badfb8b8ea Mon Sep 17 00:00:00 2001 From: Jay Date: Tue, 28 Jan 2025 15:57:28 +0100 Subject: [PATCH 7/8] fix: add missing ApiProperty decorator to patchExternalSettings endpoint (#1670) --- src/samples/schemas/sample.schema.ts | 8 ++++---- src/users/users.controller.ts | 13 ++++++++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/samples/schemas/sample.schema.ts b/src/samples/schemas/sample.schema.ts index 857dbf7af..75db3c432 100644 --- a/src/samples/schemas/sample.schema.ts +++ b/src/samples/schemas/sample.schema.ts @@ -16,16 +16,16 @@ export type SampleDocument = SampleClass & Document; timestamps: true, }) export class SampleClass extends OwnableClass { - @ApiHideProperty() - @Prop({ type: String }) - _id: string; - /** * Globally unique identifier of a sample. This could be provided as an input value or generated by the system. */ @Prop({ type: String, unique: true, required: true, default: () => uuidv4() }) sampleId: string; + @ApiHideProperty() + @Prop({ type: String }) + _id: string; + /** * The owner of the sample. */ diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 0b63b8367..de2ef0ec8 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -16,6 +16,7 @@ import { ApiBearerAuth, ApiBody, ApiOperation, + ApiParam, ApiResponse, ApiTags, } from "@nestjs/swagger"; @@ -304,11 +305,21 @@ export class UsersController { ability.can(Action.UserUpdateOwn, User) || ability.can(Action.UserUpdateAny, User), ) + @ApiParam({ name: "id", type: String, description: "User ID" }) + @ApiBody({ + schema: { + type: "object", + additionalProperties: true, + example: { field: "setting" }, + description: + "External settings to update. This should be a key-value pair object containing the settings to modify.", + }, + }) @Patch("/:id/settings/external") async patchExternalSettings( @Req() request: Request, @Param("id") id: string, - @Body() externalSettings: Record, + @Body() externalSettings = {}, ): Promise { await this.checkUserAuthorization( request, From 819e123eeb7456a48a651380d28bb05397227591 Mon Sep 17 00:00:00 2001 From: martintrajanovski Date: Thu, 30 Jan 2025 08:45:54 +0100 Subject: [PATCH 8/8] refactor: proposal count endpoint --- src/common/types.ts | 5 ++++ src/proposals/proposals.controller.ts | 43 +++++++++++++++++++++++---- src/proposals/proposals.service.ts | 9 ++++-- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/common/types.ts b/src/common/types.ts index 4b3f1a08e..1eb1e1fee 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -29,6 +29,11 @@ export class FullFacetResponse implements IFullFacets { [key: string]: object; } +export class ProposalCountFilters { + @ApiPropertyOptional() + fields?: string; +} + export class CountApiResponse { @ApiProperty({ type: Number }) count: number; diff --git a/src/proposals/proposals.controller.ts b/src/proposals/proposals.controller.ts index 494bc60d9..bda8e93c7 100644 --- a/src/proposals/proposals.controller.ts +++ b/src/proposals/proposals.controller.ts @@ -68,6 +68,7 @@ import { FullQueryFilters, CountApiResponse, FullFacetFilters, + ProposalCountFilters, } from "src/common/types"; @ApiBearerAuth() @@ -388,8 +389,8 @@ export class ProposalsController { description: "Database filters to apply when retrieving count for proposals", required: false, - type: String, - example: '{"where": {"proposalId": "189691"}}', + type: ProposalCountFilters, + example: `{ fields: ${proposalsFullQueryExampleFields}}`, }) @ApiResponse({ status: 200, @@ -397,11 +398,41 @@ export class ProposalsController { description: "Return the number of proposals in the following format: { count: integer }", }) - async count(@Req() request: Request, @Query("filters") filters?: string) { - const proposalFilters: IFilters = - this.updateFiltersForList(request, JSON.parse(filters ?? "{}")); + async count(@Req() request: Request, @Query() filters: { fields?: string }) { + const user: JWTUser = request.user as JWTUser; + const fields: IProposalFields = JSON.parse(filters.fields ?? "{}"); + + if (user) { + const ability = this.caslAbilityFactory.proposalsInstanceAccess(user); + const canViewAll = ability.can(Action.ProposalsReadAny, ProposalClass); + + if (!canViewAll) { + const canViewAccess = ability.can( + Action.ProposalsReadManyAccess, + ProposalClass, + ); + const canViewOwner = ability.can( + Action.ProposalsReadManyOwner, + ProposalClass, + ); + const canViewPublic = ability.can( + Action.ProposalsReadManyPublic, + ProposalClass, + ); + if (canViewAccess) { + fields.userGroups = fields.userGroups ?? []; + fields.userGroups.push(...user.currentGroups); + // fields.sharedWith = user.email; + } else if (canViewOwner) { + fields.ownerGroup = fields.ownerGroup ?? []; + fields.ownerGroup.push(...user.currentGroups); + } else if (canViewPublic) { + fields.isPublished = true; + } + } + } - return this.proposalsService.count(proposalFilters); + return this.proposalsService.count({ fields }); } // GET /proposals/fullquery diff --git a/src/proposals/proposals.service.ts b/src/proposals/proposals.service.ts index 7509798df..51ca2e75e 100644 --- a/src/proposals/proposals.service.ts +++ b/src/proposals/proposals.service.ts @@ -60,9 +60,14 @@ export class ProposalsService { async count( filter: IFilters, ): Promise<{ count: number }> { - const whereFilter: FilterQuery = filter.where ?? {}; + const filterQuery: FilterQuery = + createFullqueryFilter( + this.proposalModel, + "proposalId", + filter.fields, + ); - const count = await this.proposalModel.countDocuments(whereFilter).exec(); + const count = await this.proposalModel.countDocuments(filterQuery).exec(); return { count }; }