Skip to content

Commit

Permalink
Implement HTTPS in Rosalution, complete with copying and system test (#…
Browse files Browse the repository at this point in the history
…148)

* Added HTTPS to the Rosalution project. Updated all the necessary files to generate and manage certificates. Updated the variant copy button to work properly along with a system test

* Fixed the unit test in GeneBox testing the copy to clipboard functionality

* broke out the certificate generation to be its own script, added reactivity to the copy text button, also the button now toasts with a success explaining the text copied

* Frontend linting

* Added a unit test for copied text toast, any copy text can use this function

* Updated the readme to include mkcert as a prerequisite

* Added -p option to mkdir in ./etc/generate-ssl-certs.sh to ensure parent directories are made during creation time
  • Loading branch information
JmScherer authored Nov 14, 2023
1 parent 56116c3 commit 26aa97a
Show file tree
Hide file tree
Showing 13 changed files with 119 additions and 49 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/system-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,7 @@ backend/example_file_to_upload.txt
media/
*.jats
*.pdf
*.crossref
*.crossref

# SSL/TLS Certificates
*.pem
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
15 changes: 13 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"
Expand All @@ -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"
Expand Down
20 changes: 20 additions & 0 deletions etc/generate-ssl-certs.sh
Original file line number Diff line number Diff line change
@@ -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 <hostname> <certificate path>
# ./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
4 changes: 4 additions & 0 deletions etc/traefik/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
tls:
certificates:
- certFile: "/etc/certs/local-deployment-cert.pem"
keyFile: "/etc/certs/local-deployment-key.pem"
34 changes: 10 additions & 24 deletions frontend/src/components/AnalysisView/GeneBox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/views/AnalysisView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
:gene="genomicUnit.gene"
:transcripts="genomicUnit.transcripts"
:variants="genomicUnit.variants"
@clipboard-copy="this.copyToClipboard"
/>
<SectionBox
v-for="(section) in sectionsList"
Expand Down Expand Up @@ -598,6 +599,10 @@ export default {
await notificationDialog.title('Failure').confirmText('Ok').alert(error);
}
},
copyToClipboard(copiedText) {
toast.success(`Copied ${copiedText} to clipboard!`);
},
},
};
</script>
Expand Down
22 changes: 15 additions & 7 deletions frontend/test/components/AnalysisView/GeneBox.spec.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 () => {
Expand Down
25 changes: 12 additions & 13 deletions frontend/test/views/AnalysisView.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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.');
});
});
});

Expand Down
11 changes: 11 additions & 0 deletions setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion system-tests/cypress.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
10 changes: 10 additions & 0 deletions system-tests/e2e/rosalution_analysis.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit 26aa97a

Please sign in to comment.