diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 933df99f58..cc3489709e 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -7,6 +7,8 @@ on: env: IMAGE_REPOSITORY: 'gcr.io/the-coral-project/coral' + IMAGE_CACHE_REPOSITORY: 'coralproject/ci' + DOCKERHUB_USERNAME: 'coralproject' jobs: @@ -16,7 +18,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 - uses: webfactory/ssh-agent@v0.7.0 with: @@ -28,6 +33,12 @@ jobs: registry: gcr.io username: _json_key password: ${{ secrets.GCR_JSON_KEY }} + - + name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ env.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Define SHORT_SHA with commit short sha run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV @@ -90,20 +101,24 @@ jobs: # Build tag push the image after a merge to develop - name: Build, Tag, Push - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 if: github.ref == 'refs/heads/develop' with: push: true tags: ${{ env.IMAGE_REPOSITORY }}:develop-latest build-args: | REVISION_HASH=${GITHUB_SHA} + cache-from: type=registry,ref=${{ env.IMAGE_CACHE_REPOSITORY }}:cache-develop + cache-to: type=registry,ref=${{ env.IMAGE_CACHE_REPOSITORY }}:cache-develop # Build tag push the release candidate image when the branch name begins with release- - name: Build, Tag, Push RC - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 if: startsWith( github.ref, 'refs/heads/release-') with: push: true tags: ${{ env.IMAGE_REPOSITORY }}:${{ github.ref_name }}-${{ env.SHORT_SHA }} build-args: | REVISION_HASH=${GITHUB_SHA} + cache-from: type=registry,ref=${{ env.IMAGE_CACHE_REPOSITORY }}:cache-release + cache-to: type=registry,ref=${{ env.IMAGE_CACHE_REPOSITORY }}:cache-release diff --git a/.github/workflows/build-test-deploy.yml b/.github/workflows/build-test-deploy.yml index abb146928c..552a540e01 100644 --- a/.github/workflows/build-test-deploy.yml +++ b/.github/workflows/build-test-deploy.yml @@ -10,6 +10,7 @@ env: DOCKERHUB_USERNAME: 'coralproject' GOOGLE_CLOUD_BUCKET: 'coral-cdn' IMAGE_REPOSITORY: 'coralproject/talk' + IMAGE_CACHE_REPOSITORY: 'coralproject/ci' SENTRY_ORG: 'voxmedia' SENTRY_PROJECT: 'coral' @@ -20,7 +21,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 - uses: webfactory/ssh-agent@v0.7.0 with: @@ -118,28 +122,34 @@ jobs: echo "PATCH_TAG=${MAJOR}.${MINOR}.${PATCH}" >> $GITHUB_ENV - name: Build, Tag, Push Major Tag - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: push: true tags: ${{ env.IMAGE_REPOSITORY }}:${{ env.MAJOR_TAG }} build-args: | REVISION_HASH=${{ env.GITHUB_SHA }} + cache-from: type=registry,ref=${{ env.IMAGE_CACHE_REPOSITORY }}:cache-major + cache-to: type=registry,ref=${{ env.IMAGE_CACHE_REPOSITORY }}:cache-major - name: Build, Tag, Push Minor Tag - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: push: true tags: ${{ env.IMAGE_REPOSITORY }}:${{ env.MINOR_TAG }} build-args: | REVISION_HASH=${{ env.GITHUB_SHA }} + cache-from: type=registry,ref=${{ env.IMAGE_CACHE_REPOSITORY }}:cache-minor + cache-to: type=registry,ref=${{ env.IMAGE_CACHE_REPOSITORY }}:cache-minor - name: Build, Tag, Push Patch Tag - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: push: true tags: ${{ env.IMAGE_REPOSITORY }}:${{ env.PATCH_TAG }} build-args: | REVISION_HASH=${{ env.GITHUB_SHA }} + cache-from: type=registry,ref=${{ env.IMAGE_CACHE_REPOSITORY }}:cache-patch + cache-to: type=registry,ref=${{ env.IMAGE_CACHE_REPOSITORY }}:cache-patch - name: Deploy Static Assets to GCS Bucket run: | diff --git a/docs/docs/migrate-data.md b/docs/docs/migrate-data.md index 80b4fa2fce..57a07b8ca8 100644 --- a/docs/docs/migrate-data.md +++ b/docs/docs/migrate-data.md @@ -10,10 +10,7 @@ Before you start the data migration process, make sure you have the following: - a site ID for the new v7 coral instance (inspect the one record in the `sites` collection) - a mongo instance with a replica of your v4 production data set - a local machine or virtual machine with sufficient RAM and processing power to run `mongoexport` and `mongoimport` on your data set. In our experience, large data sets (10+ GB) will require a minimum of 16GB RAM and 8 cores. - <<<<<<< HEAD -- # _before_ you start the migration process, it is a good idea to create several test users (one commenter, one moderator, one admin). Following the data migration, attempt to log in and comment/moderate as these users to verify that account and login related data was migrated correctly, and that authentication has been configured correctly. - _before_ you start the migration process, it is a good idea to create several test users (one commenter, one moderator, one admin). Following the data migration, attempt to log in and comment/moderate as these users to verify that account and login related data was migrated correctly, and that authentication has bee configured correctly. - > > > > > > > main ## 1. Obtain JSON files for v4 data diff --git a/importBlocker.json b/importBlocker.json new file mode 100644 index 0000000000..5f9074e1ad --- /dev/null +++ b/importBlocker.json @@ -0,0 +1,22 @@ +{ + "sources": [ + { + "name": "client", + "directory": "src/core/client", + "blockedImports": [ + "coral-server/" + ], + "extensions": [ ".ts", ".tsx" ] + }, + { + "name": "server", + "directory": "src/core/server", + "blockedImports": [ + "coral-client/", + "coral-stream/", + "coral-framework/" + ], + "extensions": [ ".ts" ] + } + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e92d9f145a..0080820ebc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@coralproject/talk", - "version": "8.1.0", + "version": "8.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@coralproject/talk", - "version": "8.1.0", + "version": "8.2.0", "license": "Apache-2.0", "dependencies": { "@ampproject/toolbox-cache-url": "^2.9.0", diff --git a/package.json b/package.json index daa5af5122..a4014580c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@coralproject/talk", - "version": "8.1.0", + "version": "8.2.0", "author": "The Coral Project", "homepage": "https://coralproject.net/", "sideEffects": [ @@ -51,6 +51,7 @@ "lint:graphql": "graphql-schema-linter src/core/server/graph/schema/schema.graphql", "lint:scripts": "eslint 'scripts/**/*.{js,ts,tsx}'", "lint:server": "eslint 'src/**/*.{js,ts,tsx}' --ignore-pattern 'src/core/client/**'", + "lint:imports": "ts-node --transpile-only ./scripts/importBlocker.ts", "lint": "npm-run-all --parallel lint:* tscheck:*", "migration:create": "ts-node --transpile-only ./scripts/migration/create.ts", "start:development": "NODE_ENV=development TS_NODE_PROJECT=./src/tsconfig.json DISABLE_JOB_PROCESSORS=true ts-node-dev --inspect --transpile-only --no-notify -r tsconfig-paths/register --ignore-watch ./docs/ ./src/index.ts", diff --git a/scripts/importBlocker.ts b/scripts/importBlocker.ts new file mode 100644 index 0000000000..fc4b7b1cc9 --- /dev/null +++ b/scripts/importBlocker.ts @@ -0,0 +1,107 @@ +/* eslint-disable no-console */ + +import fs from "fs"; +import path from "path"; +import readLine from "readline"; + +const CONFIG_FILE = "importBlocker.json"; + +interface Source { + name: string; + directory: string; + blockedImports: string[]; + extensions: string[]; +} + +interface Config { + sources: Source[]; +} + +interface Error { + path: string; + lineNumber: number; + line: string; +} + +const loadConfig = (): Config => { + const configRaw = fs.readFileSync(CONFIG_FILE).toString(); + const config = JSON.parse(configRaw) as Config; + + return config; +}; + +const errors: Error[] = []; + +const processFile = async ( + source: Source, + filePath: string, + filters: string[] +) => { + const stream = fs.createReadStream(filePath); + const reader = readLine.createInterface({ + input: stream, + crlfDelay: Infinity, + }); + + let lineCounter = 0; + for await (const line of reader) { + for (const filter of filters) { + if (line.includes(filter)) { + errors.push({ + path: filePath, + lineNumber: lineCounter, + line, + }); + } + } + + lineCounter++; + } +}; + +const processDir = async (source: Source, dir: string, filters: string[]) => { + const items = fs.readdirSync(dir); + + for (const item of items) { + const fullPath = path.join(dir, item); + const stats = fs.lstatSync(fullPath); + + if (stats.isDirectory()) { + await processDir(source, fullPath, filters); + } else if (stats.isFile()) { + const ext = path.extname(item); + + if (source.extensions.includes(ext)) { + await processFile(source, fullPath, filters); + } + } + } +}; + +const run = async () => { + const config = loadConfig(); + + for (const source of config.sources) { + const filters = source.blockedImports.map( + (blockedImport) => `from "${blockedImport}` + ); + + await processDir(source, source.directory, filters); + } + + if (errors.length > 0) { + console.error("Blocked imports linter found the following errors:"); + + for (const error of errors) { + console.error( + ` Blocked import found in '${error.path}' on line ${error.lineNumber}: ${error.line}` + ); + } + + process.exit(1); + } else { + console.log("Blocked imports linter found 0 errors."); + } +}; + +run().finally(() => {}); diff --git a/src/core/client/admin/components/BanModal.css b/src/core/client/admin/components/BanModal.css index 2a2439b7b7..426d838b68 100644 --- a/src/core/client/admin/components/BanModal.css +++ b/src/core/client/admin/components/BanModal.css @@ -12,6 +12,26 @@ $ban-modal-text: var(--palette-text-500); height: calc(12 * var(--mini-unit)); } -.sitesToggle { +.domainBanHeader { + margin-bottom: var(--spacing-2); +} + +.form { margin-bottom: var(--spacing-3); } + +.banFromHeader { + margin-bottom: var(--spacing-2); +} + +.sitesOptions { + margin-bottom: var(--spacing-1); +} + +.banDomainOption { + margin: var(--spacing-1) 0; +} + +.customizeMessage { + align-self: flex-start; +} diff --git a/src/core/client/admin/components/BanModal.spec.tsx b/src/core/client/admin/components/BanModal.spec.tsx index f899b14e61..41c9a98731 100644 --- a/src/core/client/admin/components/BanModal.spec.tsx +++ b/src/core/client/admin/components/BanModal.spec.tsx @@ -137,7 +137,7 @@ it("creates domain ban for unmoderated domain while updating user ban status", a const modal = getBanModal(container, user); const banDomainButton = within(modal).getByLabelText( - `Ban all new accounts on test.com` + `Ban all new commenter accounts from test.com` ); userEvent.click(banDomainButton); screen.debug(banDomainButton); @@ -173,7 +173,7 @@ test.each(gteOrgMods)( const modal = getBanModal(container, commenterUser); const banDomainButton = within(modal).getByLabelText( - `Ban all new accounts on test.com` + `Ban all new commenter accounts from test.com` ); expect(banDomainButton).toBeInTheDocument(); @@ -199,7 +199,7 @@ test.each(siteMods)( const modal = getBanModal(container, commenterUser); const banDomainButton = within(modal).queryByText( - `Ban all new accounts on test.com` + `Ban all new commenter accounts from test.com` ); expect(banDomainButton).toBeNull(); @@ -234,7 +234,7 @@ it("does not display ban domain option for moderated domain", async () => { const modal = getBanModal(container, user); const banDomainButton = within(modal).queryByText( - `Ban all new accounts on test.com` + `Ban all new commenter accounts from test.com` ); expect(banDomainButton).not.toBeInTheDocument(); @@ -280,7 +280,7 @@ test.each([...PROTECTED_EMAIL_DOMAINS.values()])( const modal = getBanModal(container, user); const banDomainButton = within(modal).queryByText( - `Ban all new accounts on test.com` + `Ban all new commenter accounts from test.com` ); expect(banDomainButton).not.toBeInTheDocument(); diff --git a/src/core/client/admin/components/BanModal.tsx b/src/core/client/admin/components/BanModal.tsx index 70e3e3d669..092e8fe238 100644 --- a/src/core/client/admin/components/BanModal.tsx +++ b/src/core/client/admin/components/BanModal.tsx @@ -16,10 +16,12 @@ import { useMutation } from "coral-framework/lib/relay"; import { GQLUSER_ROLE } from "coral-framework/schema"; import { Button, + ButtonIcon, CheckBox, Flex, FormField, HorizontalGutter, + Label, RadioButton, Textarea, } from "coral-ui/components/v2"; @@ -39,7 +41,10 @@ import ChangeStatusModal from "./UserStatus/ChangeStatusModal"; import { getTextForUpdateType } from "./UserStatus/helpers"; import UserStatusSitesList from "./UserStatus/UserStatusSitesList"; -import { isSiteModerator } from "coral-common/permissions/types"; +import { + isOrgModerator, + isSiteModerator, +} from "coral-common/permissions/types"; import styles from "./BanModal.css"; export enum UpdateType { @@ -141,29 +146,22 @@ const BanModal: FunctionComponent = ({ ); }, [getMessage, username]); - const viewerIsScoped = - !!viewer.moderationScopes?.sites && - viewer.moderationScopes?.sites.length > 0; + const viewerIsScoped = isSiteModerator(viewer); const viewerIsSiteMod = !!isMultisite && viewer.role === GQLUSER_ROLE.MODERATOR && !!viewer.moderationScopes?.sites && - viewer.moderationScopes?.sites?.length > 0; + viewer.moderationScopes.sites?.length > 0; const viewerIsSingleSiteMod = !!( viewerIsSiteMod && viewer.moderationScopes?.sites && - viewer.moderationScopes?.sites.length === 1 + viewer.moderationScopes.sites.length === 1 ); const viewerIsAdmin = viewer.role === GQLUSER_ROLE.ADMIN; - const viewerIsOrgAdmin = - viewer.role === GQLUSER_ROLE.MODERATOR && - !!( - !viewer.moderationScopes?.sites || - viewer.moderationScopes?.sites?.length === 0 - ); + const viewerIsOrgMod = isOrgModerator(viewer); const userIsBlanketBanned = !!userBanStatus?.active; const userIsSingleSiteBanned = !!userBanStatus?.sites?.length; @@ -181,6 +179,12 @@ const BanModal: FunctionComponent = ({ : UpdateType.ALL_SITES; }); + const showAllSitesOption = + userRole !== GQLUSER_ROLE.MODERATOR && + (viewerIsAdmin || + viewerIsOrgMod || + (viewerIsScoped && !viewerIsSingleSiteMod && isMultisite)); + const [customizeMessage, setCustomizeMessage] = useState(false); const [emailMessage, setEmailMessage] = useState(getDefaultMessage); const [rejectExistingComments, setRejectExistingComments] = useState(false); @@ -208,7 +212,7 @@ const BanModal: FunctionComponent = ({ viewer.moderationScopes!.sites!.map((scopeSite) => scopeSite.id) ); } - }, [viewerIsSingleSiteMod, viewer.moderationScopes?.sites]); + }, [viewerIsSingleSiteMod, viewer.moderationScopes]); const onFormSubmit = useCallback(async () => { switch (updateType) { @@ -218,7 +222,7 @@ const BanModal: FunctionComponent = ({ message: customizeMessage ? emailMessage : getDefaultMessage, rejectExistingComments, siteIDs: viewerIsScoped - ? viewer.moderationScopes?.sites!.map(({ id }) => id) + ? viewer.moderationScopes!.sites!.map(({ id }) => id) : [], }); break; @@ -252,7 +256,7 @@ const BanModal: FunctionComponent = ({ getDefaultMessage, rejectExistingComments, viewerIsScoped, - viewer.moderationScopes?.sites, + viewer.moderationScopes, updateUserBan, banSiteIDs, unbanSiteIDs, @@ -309,54 +313,137 @@ const BanModal: FunctionComponent = ({
{({ handleSubmit, submitError }) => ( - - {updateType !== UpdateType.NO_SITES && ( - + {/* BAN FROM/REJECT COMMENTS */} + + {/* ban from header */} + + + + - - setRejectExistingComments(event.target.checked) + {/* sites options */} + {showAllSitesOption && ( + + + + setUpdateType(UpdateType.ALL_SITES) + } + disabled={userBanStatus?.active} + > + All sites + + + + )} + + + + setUpdateType(UpdateType.SPECIFIC_SITES) + } + > + Specific Sites + + + + {!viewerIsScoped && userHasAnyBan && ( + + + + setUpdateType(UpdateType.NO_SITES) + } + > + No Sites + + + + )} + + {/* reject comments option */} + {updateType !== UpdateType.NO_SITES && ( + - {viewerIsSingleSiteMod - ? "Reject all comments on this site" - : rejectExistingCommentsMessage} - - - )} + + setRejectExistingComments(event.target.checked) + } + > + {viewerIsSingleSiteMod + ? "Reject all comments on this site" + : rejectExistingCommentsMessage} + + + )} + + {/* EMAIL BAN */} {canBanDomain && ( - - { - setBanDomain(target.checked); - }} - > - Ban all new accounts on {emailDomain} - - + + + {/* domain ban header */} + + + + {/* domain ban checkbox */} + + { + setBanDomain(target.checked); + }} + > + Ban all new commenter accounts from{" "} + {emailDomain} + + + + )} + {/* customize message button*/} {updateType !== UpdateType.NO_SITES && ( - - - setCustomizeMessage(event.target.checked) - } - > - Customize ban email message - - + )} + {/* optional custom message field */} {updateType !== UpdateType.NO_SITES && customizeMessage && (