diff --git a/CHANGELOG.md b/CHANGELOG.md index c583fcf70..0eaca55bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,21 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [v0.62.0](https://github.com/isomerpages/isomercms-backend/compare/v0.61.0...v0.62.0) + +- chore: update formsg sdk [`#1108`](https://github.com/isomerpages/isomercms-backend/pull/1108) +- Fix/privatisation quickie interaction [`#1094`](https://github.com/isomerpages/isomercms-backend/pull/1094) +- chore: copy instead of clone [`#1095`](https://github.com/isomerpages/isomercms-backend/pull/1095) +- updated GIG API swagger doc [`#1103`](https://github.com/isomerpages/isomercms-backend/pull/1103) +- fix: remove unnecessary push logs [`#1109`](https://github.com/isomerpages/isomercms-backend/pull/1109) +- fix(rr): skip checking the existence of review request [`#1102`](https://github.com/isomerpages/isomercms-backend/pull/1102) +- release/0.61.0 [`#1104`](https://github.com/isomerpages/isomercms-backend/pull/1104) +- fix(sl): include issuewild if CAA records are needed [`#1106`](https://github.com/isomerpages/isomercms-backend/pull/1106) + #### [v0.61.0](https://github.com/isomerpages/isomercms-backend/compare/v0.60.0...v0.61.0) -- fix(sl): include issuewild if CAA records are needed [`#1106`](https://github.com/isomerpages/isomercms-backend/pull/1106) +> 10 January 2024 + - chore: upgrade axios [`#1100`](https://github.com/isomerpages/isomercms-backend/pull/1100) - build(deps): bump follow-redirects from 1.15.2 to 1.15.4 [`#1101`](https://github.com/isomerpages/isomercms-backend/pull/1101) - fix(ci): reverts ci changes to allow staging updates [`#1084`](https://github.com/isomerpages/isomercms-backend/pull/1084) @@ -88,12 +100,12 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix(siteCreate): add redirect rules [`#1036`](https://github.com/isomerpages/isomercms-backend/pull/1036) - chore: remove extra and unused submodules [`#1031`](https://github.com/isomerpages/isomercms-backend/pull/1031) - release/0.54.0 [`#1033`](https://github.com/isomerpages/isomercms-backend/pull/1033) +- fix: use cTimeMs instead of birthtime due to EFS [`#1035`](https://github.com/isomerpages/isomercms-backend/pull/1035) #### [v0.54.0](https://github.com/isomerpages/isomercms-backend/compare/v0.53.0...v0.54.0) > 14 November 2023 -- fix: use cTimeMs instead of birthtime due to EFS [`#1035`](https://github.com/isomerpages/isomercms-backend/pull/1035) - fix(pagination): total length [`#1032`](https://github.com/isomerpages/isomercms-backend/pull/1032) - fix(staging-lite): apps were created for wrong br [`#1014`](https://github.com/isomerpages/isomercms-backend/pull/1014) - fix(cm): extra timeout [`#1027`](https://github.com/isomerpages/isomercms-backend/pull/1027) @@ -220,12 +232,12 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - build(deps-dev): bump @babel/traverse from 7.22.8 to 7.23.2 [`#984`](https://github.com/isomerpages/isomercms-backend/pull/984) - release/v0.48.0 [`#979`](https://github.com/isomerpages/isomercms-backend/pull/979) - feat(staging-id): add column to store the id [`#983`](https://github.com/isomerpages/isomercms-backend/pull/983) +- Fix: collaborators service tests [`#978`](https://github.com/isomerpages/isomercms-backend/pull/978) #### [v0.48.0](https://github.com/isomerpages/isomercms-backend/compare/v0.47.0...v0.48.0) > 18 October 2023 -- Fix: collaborators service tests [`#978`](https://github.com/isomerpages/isomercms-backend/pull/978) - chore(commitService): proper naming [`#975`](https://github.com/isomerpages/isomercms-backend/pull/975) - Feat/is 585 govt sgid login rollout [`#976`](https://github.com/isomerpages/isomercms-backend/pull/976) - test(quickie): unit tests [`#973`](https://github.com/isomerpages/isomercms-backend/pull/973) diff --git a/docs/gigDnsApi.yaml b/docs/gigDnsApi.yaml index 5c0b71705..d21a14fe4 100644 --- a/docs/gigDnsApi.yaml +++ b/docs/gigDnsApi.yaml @@ -35,12 +35,18 @@ paths: description: The contact email address for this DNS record. record: $ref: "#/components/schemas/DNSRecord" + rootDomain: + type: string + description: The root domain associated with this DNS record. + example: "example.com" example: email: "admin@example.com" record: type: "A" name: "example.com" content: "192.168.1.1" + rootDomain: "example.com" + responses: "201": description: DNS record successfully added. @@ -74,6 +80,81 @@ paths: message: type: string example: "Internal server error." + put: + summary: Update a DNS record + description: Update an existing DNS record in the DNS management system. + security: + - ApiKeyAuth: [] + parameters: + - in: header + name: X-API-KEY + required: true + schema: + type: string + description: API key required to authorize requests. + example: "1234567890abcdef" + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - email + - record + properties: + email: + type: string + format: email + description: The contact email address for this DNS record. + record: + $ref: "#/components/schemas/DNSRecord" + rootDomain: + type: string + description: The root domain associated with this DNS record. + example: "example.com" + example: + email: "admin@example.com" + record: + type: "A" + name: "example.com" + content: "192.168.1.1" + rootDomain: "example.com" + responses: + "200": + description: DNS record successfully updated. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "DNS record updated successfully." + "401": + description: Unauthorized. + "400": + description: Invalid request data. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "Invalid data provided." + "404": + description: DNS record not found. + "500": + description: Server error. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "Internal server error." get: summary: Retrieve DNS records description: Get a list of DNS records for a specified name. @@ -128,6 +209,19 @@ paths: "DS", ] description: The type of the DNS record to be deleted. + - in: query + name: rootDomain + required: false + schema: + type: string + description: The root domain associated with the DNS record to be deleted. + - in: query + name: email + required: true + schema: + type: string + format: email + description: The contact email address associated with the DNS record to be deleted. responses: "200": description: DNS record successfully deleted. @@ -227,4 +321,4 @@ components: description: An 8-bit integer representing the type of the digest. digest: type: string - description: The digest value, encoded in Base64. + description: The digest value, encoded in Base64. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 06f69cd87..b966e5d4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "isomercms", - "version": "0.61.0", + "version": "0.62.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "isomercms", - "version": "0.61.0", + "version": "0.62.0", "dependencies": { "@aws-sdk/client-amplify": "^3.370.0", "@aws-sdk/client-cloudwatch-logs": "^3.370.0", @@ -16,7 +16,7 @@ "@growthbook/growthbook": "^0.27.0", "@octokit/plugin-retry": "^6.0.0", "@octokit/rest": "^18.12.0", - "@opengovsg/formsg-sdk": "^0.10.0", + "@opengovsg/formsg-sdk": "^0.11.0", "@opengovsg/sgid-client": "^2.0.0", "auto-bind": "^4.0.0", "aws-lambda": "^1.0.7", @@ -3395,23 +3395,15 @@ } }, "node_modules/@opengovsg/formsg-sdk": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@opengovsg/formsg-sdk/-/formsg-sdk-0.10.0.tgz", - "integrity": "sha512-XVJzFWkpfbUVpjsjR4o4CZESSXvw4Sq7rOyD/FjAyFqtKpzlmz/X0cgja9jtaVb7+SnqZwEPt/WVmP5+TZTuVA==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@opengovsg/formsg-sdk/-/formsg-sdk-0.11.0.tgz", + "integrity": "sha512-cNSKxOWr42w9bpK9hIjcRrt3w78DAGg3gotYvzHnawvcOsG0WVMQR3Dvat59CNehioKjNH5SkJxLSSE2crhF0g==", "dependencies": { - "axios": "^0.24.0", + "axios": "^1.6.2", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1" } }, - "node_modules/@opengovsg/formsg-sdk/node_modules/axios": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", - "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", - "dependencies": { - "follow-redirects": "^1.14.4" - } - }, "node_modules/@opengovsg/sgid-client": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@opengovsg/sgid-client/-/sgid-client-2.0.0.tgz", diff --git a/package.json b/package.json index ad15c71b7..fc47bb739 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "isomercms", - "version": "0.61.0", + "version": "0.62.0", "private": true, "scripts": { "build": "tsc -p tsconfig.build.json", @@ -32,7 +32,7 @@ "@growthbook/growthbook": "^0.27.0", "@octokit/plugin-retry": "^6.0.0", "@octokit/rest": "^18.12.0", - "@opengovsg/formsg-sdk": "^0.10.0", + "@opengovsg/formsg-sdk": "^0.11.0", "@opengovsg/sgid-client": "^2.0.0", "auto-bind": "^4.0.0", "aws-lambda": "^1.0.7", diff --git a/src/routes/formsg/formsgGGsRepair.ts b/src/routes/formsg/formsgGGsRepair.ts index 869902145..2bf41a5e9 100644 --- a/src/routes/formsg/formsgGGsRepair.ts +++ b/src/routes/formsg/formsgGGsRepair.ts @@ -10,7 +10,10 @@ import { ResultAsync, errAsync, fromPromise, okAsync } from "neverthrow" import { config } from "@config/config" -import { EFS_VOL_PATH_STAGING_LITE } from "@root/constants" +import { + EFS_VOL_PATH_STAGING, + EFS_VOL_PATH_STAGING_LITE, +} from "@root/constants" import GitFileSystemError from "@root/errors/GitFileSystemError" import InitializationError from "@root/errors/InitializationError" import { consoleLogger } from "@root/logger/console.logger" @@ -147,6 +150,7 @@ export class FormsgGGsRepairRouter { .andThen(() => fromPromise( this.reposService.setUpStagingLite( + path.join(EFS_VOL_PATH_STAGING, repoName), path.join(EFS_VOL_PATH_STAGING_LITE, repoName), repoUrl ), diff --git a/src/routes/v2/authenticated/review.ts b/src/routes/v2/authenticated/review.ts index a153506fe..aa8f4250c 100644 --- a/src/routes/v2/authenticated/review.ts +++ b/src/routes/v2/authenticated/review.ts @@ -1274,7 +1274,9 @@ export class ReviewsRouter { { userWithSiteSessionData: UserWithSiteSessionData } > = async (req, res) => { // Step 1: Check that the site exists - const { siteName, requestId } = req.params + // NOTE: We don't check for the existence of the review request, as we + // assume that we are comparing between the staging and master branch + const { siteName } = req.params const { path } = req.query const { userWithSiteSessionData } = res.locals const site = await this.sitesService.getBySiteName(siteName) @@ -1308,30 +1310,7 @@ export class ReviewsRouter { }) } - // Step 2: Retrieve review request - const possibleReviewRequest = await this.reviewRequestService.getReviewRequest( - site.value, - requestId - ) - - if (isIsomerError(possibleReviewRequest)) { - logger.error({ - message: "Invalid review request requested", - method: "getBlob", - meta: { - userId: userWithSiteSessionData.isomerUserId, - email: userWithSiteSessionData.email, - siteName, - requestId, - file: path, - }, - }) - return res.status(404).send({ - message: "Please ensure that the site exists!", - }) - } - - // Step 3: Check if the user is a contributor of the site + // Step 2: Check if the user is a contributor of the site const role = await this.collaboratorsService.getRole( siteName, userWithSiteSessionData.isomerUserId diff --git a/src/services/configServices/SettingsService.js b/src/services/configServices/SettingsService.js index 66a898073..e5114ee7a 100644 --- a/src/services/configServices/SettingsService.js +++ b/src/services/configServices/SettingsService.js @@ -173,6 +173,14 @@ class SettingsService { // only when changing from public to private - not awaited as this is slow and non-blocking privatiseNetlifySite(siteName, password) } + const updatePasswordResp = await this.deploymentsService.updateAmplifyPassword( + siteName, + password, + enablePassword + ) + if (updatePasswordResp.isErr()) { + return updatePasswordResp + } if (isPrivate !== enablePassword) { // For public -> private or private -> public, we also need to update the repo privacy on github const privatiseRepoRes = await this.gitHubService.changeRepoPrivacy( @@ -189,11 +197,7 @@ class SettingsService { return errAsync(err) } } - return this.deploymentsService.updateAmplifyPassword( - siteName, - password, - enablePassword - ) + return updatePasswordResp } shouldUpdateHomepage(updatedConfigContent, configContent) { diff --git a/src/services/db/GitFileSystemService.ts b/src/services/db/GitFileSystemService.ts index 3a5e1af81..68c5b82ca 100644 --- a/src/services/db/GitFileSystemService.ts +++ b/src/services/db/GitFileSystemService.ts @@ -539,7 +539,9 @@ export default class GitFileSystemService { .cwd({ path: `${efsVolPath}/${repoName}`, root: false }) .push(gitOptions), (error) => { - logger.error(`Error when pushing ${repoName}: ${error}`) + logger.error( + `Error when pushing ${repoName}. Retrying git push operation for the first time...` + ) if (error instanceof GitError) { return new GitFileSystemError( @@ -562,7 +564,9 @@ export default class GitFileSystemService { .cwd({ path: `${efsVolPath}/${repoName}`, root: false }) .push(gitOptions), (error) => { - logger.error(`Error when pushing ${repoName}: ${error}`) + logger.error( + `Error when pushing ${repoName}. Retrying git push operation for the second time...` + ) if (error instanceof GitError) { return new GitFileSystemError( @@ -576,6 +580,7 @@ export default class GitFileSystemService { ) .orElse(() => // Retry push twice + // TODO: To eliminate duplicate code by using a backoff or retry package ResultAsync.fromPromise( isForce ? this.git @@ -585,7 +590,9 @@ export default class GitFileSystemService { .cwd({ path: `${efsVolPath}/${repoName}`, root: false }) .push(gitOptions), (error) => { - logger.error(`Error when pushing ${repoName}: ${error}`) + logger.error( + `Both retries for git push have failed. Error when pushing ${repoName}: ${error}` + ) if (error instanceof GitError) { return new GitFileSystemError( diff --git a/src/services/identity/DeploymentClient.ts b/src/services/identity/DeploymentClient.ts index eb7c8598d..df9fc8c8a 100644 --- a/src/services/identity/DeploymentClient.ts +++ b/src/services/identity/DeploymentClient.ts @@ -143,19 +143,23 @@ class DeploymentClient { }, }) - generateDeletePasswordInput = (appId: string): UpdateBranchCommandInput => ({ + generateDeletePasswordInput = ( + appId: string, + branchName = "staging" + ): UpdateBranchCommandInput => ({ appId, - branchName: "staging", + branchName, enableBasicAuth: false, basicAuthCredentials: "", }) generateUpdatePasswordInput = ( appId: string, - password: string + password: string, + branchName = "staging" ): UpdateBranchCommandInput => ({ appId, - branchName: "staging", + branchName, enableBasicAuth: true, basicAuthCredentials: Buffer.from(`user:${password}`).toString("base64"), }) diff --git a/src/services/identity/DeploymentsService.ts b/src/services/identity/DeploymentsService.ts index ee4924e9f..c03935ebe 100644 --- a/src/services/identity/DeploymentsService.ts +++ b/src/services/identity/DeploymentsService.ts @@ -202,9 +202,14 @@ class DeploymentsService { return okAsync(deploymentInfo) } - deletePassword = async (appId: string, deploymentId: number) => { + deletePassword = async ( + appId: string, + deploymentId: number, + isStagingLite: boolean + ) => { const updateAppInput = this.deploymentClient.generateDeletePasswordInput( - appId + appId, + isStagingLite ? "staging-lite" : "staging" ) const updateResp = await this.deploymentClient.sendUpdateApp(updateAppInput) @@ -249,9 +254,22 @@ class DeploymentsService { return errAsync( new NotFoundError(`Deployment for site ${repoName} does not exist`) ) - const { id, hostingId: appId } = deploymentInfo + const { + id, + hostingId: appId, + stagingLiteHostingId: stagingLiteId, + } = deploymentInfo - if (!enablePassword) return this.deletePassword(appId, id) + if (!enablePassword) { + const stagingRes = await this.deletePassword(appId, id, false) + if (!stagingLiteId || stagingRes.isErr()) return stagingRes + const stagingLiteRes = await this.deletePassword(stagingLiteId, id, true) + if (stagingLiteRes.isErr()) + logger.error( + `Privatisation adjustment failed for ${repoName} - requires manual fixing of inconsistent state` + ) + return stagingLiteRes + } const { encryptedPassword: oldEncryptedPassword, @@ -273,6 +291,24 @@ class DeploymentsService { return updateResp } + if (stagingLiteId) { + const updateStagingLiteInput = this.deploymentClient.generateUpdatePasswordInput( + stagingLiteId, + password, + "staging-lite" + ) + + const updateStagingLiteResp = await this.deploymentClient.sendUpdateApp( + updateStagingLiteInput + ) + if (updateStagingLiteResp.isErr()) { + logger.error( + `Privatisation adjustment failed for ${repoName} - requires manual fixing of inconsistent state` + ) + return updateStagingLiteResp + } + } + const { encryptedPassword, iv } = encryptPassword(password, SECRET_KEY) await this.deploymentsRepository.update( { diff --git a/src/services/identity/ReposService.ts b/src/services/identity/ReposService.ts index 8fee198cc..cb52cd30e 100644 --- a/src/services/identity/ReposService.ts +++ b/src/services/identity/ReposService.ts @@ -265,7 +265,7 @@ export default class ReposService { .checkout("staging") // reset local branch back to staging // Make sure the local path is empty, just in case dir was used on a previous attempt. - await this.setUpStagingLite(stgLiteDir, repoUrl) + await this.setUpStagingLite(stgDir, stgLiteDir, repoUrl) } createDnsIndirectionFile = ( @@ -375,23 +375,16 @@ export const createRecords = (zoneId: string): Record[] => { .map(() => undefined) } - async setUpStagingLite(stgLiteDir: string, repoUrl: string) { + async setUpStagingLite(stgDir: string, stgLiteDir: string, repoUrl: string) { fs.rmSync(`${stgLiteDir}`, { recursive: true, force: true }) // create a empty folder stgLiteDir fs.mkdirSync(stgLiteDir) + fs.cpSync(stgDir, stgLiteDir, { recursive: true }) // note: for some reason, combining below commands led to race conditions // so we have to do it separately // Create staging lite branch in other repo path - await this.simpleGit - .cwd({ path: stgLiteDir, root: false }) - .clone(repoUrl, stgLiteDir) - await this.simpleGit.cwd({ path: stgLiteDir, root: false }).pull() // some repos are large, clone takes time - await this.simpleGit - .cwd({ path: stgLiteDir, root: false }) - .checkout("staging") - if (fs.existsSync(path.join(`${stgLiteDir}`, `images`))) { await this.simpleGit .cwd({ path: stgLiteDir, root: false }) diff --git a/src/services/identity/SitesService.ts b/src/services/identity/SitesService.ts index 5acd5107a..094fb3727 100644 --- a/src/services/identity/SitesService.ts +++ b/src/services/identity/SitesService.ts @@ -327,19 +327,30 @@ class SitesService { return okAsync(site.deployment) } + // Privatisation has priority over growthbook - if private, automatically use staging + const isPrivateSiteSyncedWithDb = + site.isPrivate && site?.deployment?.stagingUrl.includes("staging.") + const featureFlagSyncedWithDb = - (isReduceBuildTimesWhitelistedRepo(sessionData.growthbook) && + !site.isPrivate && + ((isReduceBuildTimesWhitelistedRepo(sessionData.growthbook) && site?.deployment?.stagingUrl.includes("staging-lite.")) || - // useful for rollbacks - (!isReduceBuildTimesWhitelistedRepo(sessionData.growthbook) && - site?.deployment?.stagingUrl.includes("staging.")) + // useful for rollbacks + (!isReduceBuildTimesWhitelistedRepo(sessionData.growthbook) && + site?.deployment?.stagingUrl.includes("staging."))) - if (featureFlagSyncedWithDb) { + if (isPrivateSiteSyncedWithDb || featureFlagSyncedWithDb) { return okAsync(site.deployment) } let stagingUrl: StagingPermalink - if (isReduceBuildTimesWhitelistedRepo(sessionData.growthbook)) { + if (site.isPrivate) { + stagingUrl = Brand.fromString( + `https://staging.${site.deployment.hostingId}.amplifyapp.com` + ) + } else if ( + isReduceBuildTimesWhitelistedRepo(sessionData.growthbook) + ) { stagingUrl = Brand.fromString( `https://staging-lite.${site.deployment.stagingLiteHostingId}.amplifyapp.com` )