diff --git a/.github/workflows/system-tests.yml b/.github/workflows/system-tests.yml index ccbd06da..8c0c7618 100644 --- a/.github/workflows/system-tests.yml +++ b/.github/workflows/system-tests.yml @@ -19,5 +19,8 @@ jobs: browser: chrome headed: false start: docker compose up --build -d - wait-on: 'http://local.rosalution.cgds' + wait-on: 'https://local.rosalution.cgds' wait-on-timeout: 120 + env: + # Avoids self-signed cert error: https://github.com/cypress-io/github-action/issues/154 + NODE_TLS_REJECT_UNAUTHORIZED: 0 \ No newline at end of file diff --git a/.gitignore b/.gitignore index b89569ad..09be4a4b 100644 --- a/.gitignore +++ b/.gitignore @@ -64,4 +64,7 @@ backend/example_file_to_upload.txt media/ *.jats *.pdf -*.crossref \ No newline at end of file +*.crossref + +# SSL/TLS Certificates +*.pem \ No newline at end of file diff --git a/README.md b/README.md index 329d29d7..39f2f0bd 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,14 @@ the respective installation instructions for your target environment. - The `setup.sh` script requires sudo/admin privileges in the target development environment to update the `/etc/hosts` file to setup localhost redirection for a Rosalution deployment to redirect localhost to local.rosalution.cgds +- [mkcert](https://github.com/FiloSottile/mkcert) + - Tool to generate and install self-signed locally-trusted development certificates + - Used to make certificates for Traefik to manage HTTPS enabled services + - The `setup.sh` script requires sudo/admin privileges in the target development envrionment to install the generated + certificates in the respective browser trusts so browsers know they're valid and won't throw an insecure warning + - Default certificate location: `./etc/certificates` + - Note: Local deployment will still work without self-signed certificates. The browser will just throw an insecure + warning, the application can still be used. ### Browser Support @@ -103,6 +111,8 @@ The script will - Updates your local `/etc/hosts` to support the local DNS redirect of localhost to 'local.rosalution.cgds'. - Creates a Python virtual environment for called **"rosalution_env"** within the backend directory - Installs Python dependencies within the virtual environment +- Checks if `mkcert` is installed, if so generates self-signed certificates in `./etc/certificates` + - Generates two files, one .pem key file and one .pem cert file ```bash ./setup.sh diff --git a/docker-compose.yml b/docker-compose.yml index 8830e475..da8515b3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,17 +8,28 @@ services: - "--api.insecure=true" - "--providers.docker=true" - "--providers.docker.exposedbydefault=false" + - "--providers.file.filename=/etc/traefik/config.yml" - "--entrypoints.web.address=:80" + - "--entrypoints.web.http.redirections.entrypoint.to=websecure" + - "--entrypoints.web.http.redirections.entrypoint.scheme=https" + - "--entrypoints.websecure.address=:443" ports: - 80:80 + - 443:443 - 8080:8080 volumes: + - ./etc/traefik/config.yml:/etc/traefik/config.yml:ro + - ./etc/.certificates:/etc/certs:ro - /var/run/docker.sock:/var/run/docker.sock networks: - rosalution-network labels: - "traefik.enable=true" - "traefik.docker.network=rosalution-network" + - "traefik.http.routers.traefik.tls=true" + - "traefik.http.routers.traefik.service=api@internal" + - "traefik.http.routers.traefik.rule=Host(`localhost`)" + - "traefik.http.services.traefik.loadbalancer.server.port=8080" frontend: build: @@ -34,7 +45,7 @@ services: labels: - "traefik.enable=true" - "traefik.docker.network=rosalution-network" - - "traefik.http.routers.frontend-router.entrypoints=web" + - "traefik.http.routers.frontend-router.tls=true" - "traefik.http.routers.frontend-router.rule=Host(`local.rosalution.cgds`)" - "traefik.http.routers.frontend-router.service=frontend-web-service" - "traefik.http.services.frontend-web-service.loadbalancer.server.port=80" @@ -54,7 +65,7 @@ services: labels: - "traefik.enable=true" - "traefik.docker.network=rosalution-network" - - "traefik.http.routers.backend-router.entrypoints=web" + - "traefik.http.routers.backend-router.tls=true" - "traefik.http.routers.backend-router.rule=Host(`local.rosalution.cgds`) && PathPrefix(`/rosalution/api`)" - "traefik.http.routers.backend-router.service=backend-api-service" - "traefik.http.routers.backend-router.middlewares=backend-strip-prefix" diff --git a/etc/generate-ssl-certs.sh b/etc/generate-ssl-certs.sh new file mode 100755 index 00000000..ee2f7b54 --- /dev/null +++ b/etc/generate-ssl-certs.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# Uses mkcert to generate self-signed SSL/TLS certificates to allow Traefik to manage HTTPS +# connections for a local deployment for Rosalution. Automatically installs the generated certificate +# with the Chrome browser trust to let the browser know the certificate is valid and secure when +# accessing local.rosalutin.cgds/rosalution. + +# Also creates the certificate directory path, default is ./etc/.certificates + +# ./generate-ssl-certs.sh +# ./generate-ssl-certs.sh local.rosalution.cgds ./etc/.certificates + + +HOSTNAME=$1 +CERT_PATH=$2 + +mkdir -p "$CERT_PATH" + +mkcert -cert-file "$CERT_PATH"/local-deployment-cert.pem -key-file "$CERT_PATH"/local-deployment-key.pem "$HOSTNAME" +mkcert -install diff --git a/etc/traefik/config.yml b/etc/traefik/config.yml new file mode 100644 index 00000000..ac372ae9 --- /dev/null +++ b/etc/traefik/config.yml @@ -0,0 +1,4 @@ +tls: + certificates: + - certFile: "/etc/certs/local-deployment-cert.pem" + keyFile: "/etc/certs/local-deployment-key.pem" \ No newline at end of file diff --git a/frontend/src/components/AnalysisView/GeneBox.vue b/frontend/src/components/AnalysisView/GeneBox.vue index d3f2cdcb..f0ad57e3 100644 --- a/frontend/src/components/AnalysisView/GeneBox.vue +++ b/frontend/src/components/AnalysisView/GeneBox.vue @@ -86,30 +86,8 @@ export default { } }, copyToClipboard(textToCopy) { - /* - The below method is the updated way to copy text to clipboard. - It does not work currently because we have not added HTTPS in traffik. - This functionality for copying text will be added when HTTPS is added in traffik. - */ - // navigator.clipboard.writeText(textToCopy); - console.log(textToCopy); - - /* - The below method is marked as depreciated - Link to stack overflow article: - https://stackoverflow.com/questions/67882865/copy-datatext-to-clipboard-in-vuenuxt-js - Link to MDN: - https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand - */ - // const tmpTextField = document.createElement('input'); - // tmpTextField.value = textToCopy; - // tmpTextField.setAttribute('readonly', ''); - // tmpTextField.setAttribute('style', 'position:absolute; right:200%;'); - // document.body.appendChild(tmpTextField); - // tmpTextField.select(); - // tmpTextField.setSelectionRange(0, 99999); - // document.execCommand('copy'); - // tmpTextField.remove(); + navigator.clipboard.writeText(textToCopy); + this.$emit('clipboard-copy', textToCopy); }, getCompleteHgvsVariantName(variant) { if (variant.p_dot) { @@ -163,6 +141,14 @@ export default { padding-bottom: var(--p-1) } +.copy-icon:hover { + cursor: pointer; +} + +.copy-icon:active { + color: var(--rosalution-purple-200) +} + .genomic-build { font-size: .875rem; font-weight: 600; diff --git a/frontend/src/views/AnalysisView.vue b/frontend/src/views/AnalysisView.vue index 977599d8..147a9d2c 100644 --- a/frontend/src/views/AnalysisView.vue +++ b/frontend/src/views/AnalysisView.vue @@ -22,6 +22,7 @@ :gene="genomicUnit.gene" :transcripts="genomicUnit.transcripts" :variants="genomicUnit.variants" + @clipboard-copy="this.copyToClipboard" /> diff --git a/frontend/test/components/AnalysisView/GeneBox.spec.js b/frontend/test/components/AnalysisView/GeneBox.spec.js index ed378589..445c7e0b 100644 --- a/frontend/test/components/AnalysisView/GeneBox.spec.js +++ b/frontend/test/components/AnalysisView/GeneBox.spec.js @@ -1,4 +1,4 @@ -import {expect, describe, it, beforeAll, afterAll, vi} from 'vitest'; +import {expect, describe, it, beforeAll, afterAll} from 'vitest'; import {config, shallowMount} from '@vue/test-utils'; import GeneBox from '@/components/AnalysisView/GeneBox.vue'; @@ -105,17 +105,25 @@ describe('GeneBox.vue', () => { expect(wrapper.text()).to.contains('PS2, PS3, PM2, PP3, PP5'); }); - /* - this test will need to be modified when the copy functionality is added - copy method needs to be changed to navigator.clipboard.writeText(textToCopy); - */ it('should log text to console when copy button is clicked', async () => { + let clipboardContents = ''; + + window.__defineGetter__('navigator', function() { + return { + clipboard: { + writeText: (text) => { + clipboardContents = text; + }, + }, + }; + }); + const wrapper = getMountedComponent(); const copyButton = wrapper.find('[data-test=copy-button]'); - vi.spyOn(console, 'log'); await copyButton.trigger('click'); - expect(console.log).toHaveBeenCalled(); + + expect(clipboardContents).to.equal('NM_170707.3:c.745C>T'); }); it('should route to annotations for a gene', async () => { diff --git a/frontend/test/views/AnalysisView.spec.js b/frontend/test/views/AnalysisView.spec.js index 15148171..cd1d19ea 100644 --- a/frontend/test/views/AnalysisView.spec.js +++ b/frontend/test/views/AnalysisView.spec.js @@ -4,6 +4,7 @@ import sinon from 'sinon'; import Analyses from '@/models/analyses.js'; +import GeneBox from '@/components/AnalysisView/GeneBox.vue'; import InputDialog from '@/components/Dialogs/InputDialog.vue'; import NotificationDialog from '@/components/Dialogs/NotificationDialog.vue'; import SupplementalFormList from '@/components/AnalysisView/SupplementalFormList.vue'; @@ -129,6 +130,17 @@ describe('AnalysisView', () => { expect(appContent.exists()).to.be.true; }); + it('should display a toast when a copy text to clipboard button', async () => { + const geneBox = wrapper.getComponent(GeneBox); + + geneBox.vm.$emit('clipboard-copy', 'NM_001017980.3:c.164G>T'); + await wrapper.vm.$nextTick(); + + expect(toast.state.active).to.be.true; + expect(toast.state.type).to.equal('success'); + expect(toast.state.message).to.equal('Copied NM_001017980.3:c.164G>T to clipboard!'); + }); + describe('the header', () => { it('contains a header element', () => { const appHeader = wrapper.find('app-header'); @@ -662,19 +674,6 @@ describe('AnalysisView', () => { expect(toast.state.type).to.equal('success'); expect(toast.state.message).to.equal('Analysis updated successfully.'); }); - - it('should display info toast when canceling analysis changes', async () => { - const wrapper = getMountedComponent(); - await wrapper.setData({edit: true}); - const saveModal = wrapper.findComponent(SaveModal); - - saveModal.vm.$emit('canceledit'); - await wrapper.vm.$nextTick(); - - expect(toast.state.active).to.be.true; - expect(toast.state.type).to.equal('info'); - expect(toast.state.message).to.equal('Edit mode has been disabled and changes have not been saved.'); - }); }); }); diff --git a/setup.sh b/setup.sh index 10d22077..3ef008bd 100755 --- a/setup.sh +++ b/setup.sh @@ -28,8 +28,19 @@ fi install frontend install system-tests +# check dns entry in hosts, adds if not present ./etc/etc-hosts.sh local.rosalution.cgds +# check if mkcert is installed, generates tls certificates if found +if command -v mkcert &> /dev/null +then + echo "mkcert found, generating certificates" + ./etc/generate-ssl-certs.sh local.rosalution.cgds ./etc/.certificates +else + echo "mkcert could not be found, could not generate certificates. Browser will throw insecure warning." + echo "To generate certificates, please visit and install: https://github.com/FiloSottile/mkcert" +fi + # change to backend directory, create venv, and activate it cd backend || { echo "Failure to change to backend directory"; exit 1;} python3 -m venv rosalution_env diff --git a/system-tests/cypress.config.js b/system-tests/cypress.config.js index 76e8c253..adae0f8a 100644 --- a/system-tests/cypress.config.js +++ b/system-tests/cypress.config.js @@ -3,7 +3,7 @@ const {defineConfig} = require('cypress'); module.exports = defineConfig({ e2e: { video: false, - baseUrl: 'http://local.rosalution.cgds/rosalution', + baseUrl: 'https://local.rosalution.cgds/rosalution', fixturesFolder: './fixtures', downloadsFolder: 'cypress/downloads', screenshotsFolder: 'cypress/screenshots', diff --git a/system-tests/e2e/rosalution_analysis.cy.js b/system-tests/e2e/rosalution_analysis.cy.js index 612d1210..8099df68 100644 --- a/system-tests/e2e/rosalution_analysis.cy.js +++ b/system-tests/e2e/rosalution_analysis.cy.js @@ -36,6 +36,16 @@ describe('As a Clinical Analyst using Rosalution for analysis', () => { cy.url().should('eq', Cypress.config().baseUrl + '/'); }); + it('should click the copy button and add the variant text to the operating system clipboard to copy', () => { + cy.get('[data-test="copy-button"]').click(); + + cy.window().then((win) => { + win.navigator.clipboard.readText().then((text) => { + expect(text).to.equal('NM_001017980.3:c.164G>T'); + }); + }); + }); + it('should allow the user to navigate to a third party link after adding one', () => { cy.get('.grey-rounded-menu').invoke('attr', 'style', 'display: block; visibility: visible; opacity: 1;'); cy.get('[data-test="user-menu"] > .grey-rounded-menu').contains('Attach Monday.com').click();