diff --git a/.gitignore b/.gitignore index 53bd674..9366864 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ dist # misc .DS_Store +.env .env.local .env.development.local .env.test.local diff --git a/README.md b/README.md index 5662627..7f14397 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ If you're a developer and want to contribute to, or want to utilize this marketp - [🏖 Remote Ocean: Pacific](#-Remote-Ocean-Pacific) - [🐳 Use with Barge](#-Use-with-Barge) - [⛵️ Environment Variables](#️-Environment-Variables) + - [Client](#Client) + - [Server](#Server) - [👩‍🔬 Testing](#-Testing) - [Unit Tests](#Unit-Tests) - [End-to-End Integration Tests](#End-to-End-Integration-Tests) @@ -41,7 +43,7 @@ If you're a developer and want to contribute to, or want to utilize this marketp This repo contains a client and a server, both written in TypeScript: - **client**: React app setup with [squid-js](https://github.com/oceanprotocol/squid-js), bootstrapped with [Create React App](https://github.com/facebook/create-react-app) -- **server**: Node.js app, utilizing [Express](https://expressjs.com). The server provides various microservices, like remote file checking. +- **server**: Node.js app, utilizing [Express](https://expressjs.com). The server provides various microservices, like remote file checking. The endpoints are documented in [server Readme](server/). To spin up both, the client and the server in a watch mode for local development, execute: @@ -79,6 +81,8 @@ Modify `./client/src/config.ts` or set environment variables to use those local ### ⛵️ Environment Variables +#### Client + The `./client/src/config.ts` file is setup to prioritize environment variables for setting each Ocean component endpoint. By setting environment variables, you can easily switch between Ocean networks the commons client connects to, without directly modifying `./client/src/config.ts`. This is helpful e.g. for local development so you don't accidentially commit changes to the config file. @@ -92,6 +96,17 @@ cp client/.env.local.example client/.env.local vi client/.env.local ``` +#### Server + +The server uses its own environment variables too: + +```bash +cp server/.env.example server/.env + +# edit variables +vi server/.env +``` + ## 👩‍🔬 Testing Test suite is setup with [Jest](https://jestjs.io) and [react-testing-library](https://github.com/kentcdodds/react-testing-library) for unit testing, and [Cypress](https://www.cypress.io) for integration testing. diff --git a/client/.env.local.example b/client/.env.local.example index e435616..29e6eba 100644 --- a/client/.env.local.example +++ b/client/.env.local.example @@ -52,3 +52,5 @@ REACT_APP_BRIZO_ADDRESS="0x008c25ed3594e094db4592f4115d5fa74c4f41ea" # REACT_APP_SECRET_STORE_URI="http://localhost:12001" # REACT_APP_FAUCET_URI="http://localhost:3001" # REACT_APP_BRIZO_ADDRESS="0x00bd138abd70e2f00903268f3db08f2d25677c9e" + +REACT_APP_REPORT_EMAIL="test@example.com" diff --git a/client/package-lock.json b/client/package-lock.json index 8226fde..db306f3 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1740,6 +1740,15 @@ "@types/react": "*" } }, + "@types/react-modal": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.8.2.tgz", + "integrity": "sha512-/Drs+XfHg9M60fy2Q63UUlhECXSNknDu3tnwFnbOhcdDjq03VD3hLCfv3X+BBzRqgu4TOu+TwEwBhgI8qdVAAQ==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-paginate": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/@types/react-paginate/-/react-paginate-6.2.1.tgz", @@ -13136,6 +13145,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "react-markdown": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-4.1.0.tgz", @@ -13150,6 +13164,17 @@ "xtend": "^4.0.1" } }, + "react-modal": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.8.2.tgz", + "integrity": "sha512-wxNk94wy/DMh2LyJa8K+LyOQDhQfhKuBrZ4SxS091p75cpW+STfY+9GpAuvl6P6Yt2r/+wxYH8Z3G5Ww/L8Tiw==", + "requires": { + "exenv": "^1.2.0", + "prop-types": "^15.5.10", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + } + }, "react-moment": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/react-moment/-/react-moment-0.9.2.tgz", diff --git a/client/package.json b/client/package.json index 7187bce..7a4ded4 100644 --- a/client/package.json +++ b/client/package.json @@ -32,6 +32,7 @@ "react-ga": "^2.6.0", "react-helmet": "^5.2.1", "react-markdown": "^4.1.0", + "react-modal": "^3.8.2", "react-moment": "^0.9.2", "react-paginate": "^6.3.0", "react-popper": "^1.3.3", @@ -51,6 +52,7 @@ "@types/react-dom": "^16.8.4", "@types/react-dotdotdot": "^1.2.0", "@types/react-helmet": "^5.0.8", + "@types/react-modal": "^3.8.2", "@types/react-paginate": "^6.2.1", "@types/react-router-dom": "^4.3.4", "@types/react-transition-group": "^2.9.2", diff --git a/client/src/components/atoms/Button.tsx b/client/src/components/atoms/Button.tsx index 4beb571..74d8eff 100644 --- a/client/src/components/atoms/Button.tsx +++ b/client/src/components/atoms/Button.tsx @@ -12,6 +12,7 @@ interface ButtonProps { onClick?: any disabled?: boolean to?: string + name?: string } export default class Button extends PureComponent { diff --git a/client/src/components/atoms/Modal.module.scss b/client/src/components/atoms/Modal.module.scss new file mode 100644 index 0000000..313ed8a --- /dev/null +++ b/client/src/components/atoms/Modal.module.scss @@ -0,0 +1,88 @@ +@import '../../styles/variables'; + +.modalOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba($brand-black, .7); + overflow-x: hidden; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + animation: fadeIn .2s ease-out backwards; +} + +.modal { + padding: $spacer; + border-radius: $border-radius; + background: $body-background; + margin: $spacer auto; + max-width: $break-point--small; + position: relative; + animation: moveUp .2s ease-out backwards; + + @media (min-width: $break-point--small) { + padding: $spacer * 2 $spacer * 1.5; + } + + &:focus { + outline: 0; + } +} + +.header { + margin-bottom: $spacer; +} + +.title { + font-size: $font-size-h3; + margin: 0; + + @media (min-width: $break-point--small) { + font-size: $font-size-h2; + } +} + +.description { + margin: 0; + margin-top: $spacer / 2; +} + +.close { + position: absolute; + cursor: pointer; + background: none; + border: 0; + box-shadow: none; + outline: 0; + top: $spacer / 4; + right: $spacer / 2; + font-size: $font-size-h2; + color: $brand-grey; + + &:hover, + &:focus { + opacity: .7; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes moveUp { + from { + transform: translate3d(0, 1rem, 0); + } + + to { + transform: translate3d(0, 0, 0); + } +} diff --git a/client/src/components/atoms/Modal.test.tsx b/client/src/components/atoms/Modal.test.tsx new file mode 100644 index 0000000..81130f8 --- /dev/null +++ b/client/src/components/atoms/Modal.test.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import { render } from '@testing-library/react' +import Modal from './Modal' +import ReactModal from 'react-modal' + +describe('Modal', () => { + it('renders without crashing', () => { + ReactModal.setAppElement(document.createElement('div')) + + render( + null}> + Hello + + ) + expect(document.querySelector('.ReactModalPortal')).toBeInTheDocument() + }) +}) diff --git a/client/src/components/atoms/Modal.tsx b/client/src/components/atoms/Modal.tsx new file mode 100644 index 0000000..d3b5c7e --- /dev/null +++ b/client/src/components/atoms/Modal.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import ReactModal from 'react-modal' +import styles from './Modal.module.scss' + +if (process.env.NODE_ENV !== 'test') ReactModal.setAppElement('#root') + +const Modal = ({ + title, + description, + isOpen, + toggleModal, + children, + onAfterOpen, + onRequestClose, + ...props +}: { + title: string + description?: string + isOpen: boolean + toggleModal: () => void + children: any + onAfterOpen?: () => void + onRequestClose?: () => void +}) => { + return ( + + + +
+

{title}

+ {description && ( +

{description}

+ )} +
+ + {children} +
+ ) +} + +export default Modal diff --git a/client/src/components/templates/Asset/AssetDetails.tsx b/client/src/components/templates/Asset/AssetDetails.tsx index 222143f..03ffc49 100644 --- a/client/src/components/templates/Asset/AssetDetails.tsx +++ b/client/src/components/templates/Asset/AssetDetails.tsx @@ -5,6 +5,7 @@ import Markdown from '../../atoms/Markdown' import CategoryLink from '../../atoms/CategoryLink' import styles from './AssetDetails.module.scss' import AssetFilesDetails from './AssetFilesDetails' +import Report from './Report' interface AssetDetailsProps { metadata: MetaData @@ -58,6 +59,8 @@ export default class AssetDetails extends PureComponent { /> )} + +

{ files={base.files ? base.files : []} ddo={ddo} /> - - {/*
-                    {JSON.stringify(metadata, null, 2)}
-                
*/} ) } diff --git a/client/src/components/templates/Asset/AssetFile.tsx b/client/src/components/templates/Asset/AssetFile.tsx index d014d62..7ffdf06 100644 --- a/client/src/components/templates/Asset/AssetFile.tsx +++ b/client/src/components/templates/Asset/AssetFile.tsx @@ -146,6 +146,7 @@ export default class AssetFile extends PureComponent< // weird 0 hack so TypeScript is happy onClick={() => this.purchaseAsset(ddo, index || 0)} disabled={!isLogged || !isOceanNetwork} + name="Download" > Get file diff --git a/client/src/components/templates/Asset/Report.module.scss b/client/src/components/templates/Asset/Report.module.scss new file mode 100644 index 0000000..83996e0 --- /dev/null +++ b/client/src/components/templates/Asset/Report.module.scss @@ -0,0 +1,49 @@ +@import '../../../styles/variables'; + +.actions { + text-align: right; + margin-top: $spacer; +} + +.openLink { + margin: 0; + font-size: $font-size-small; +} + +.info { + background: $brand-white; + padding: $spacer; + border: 1px solid $brand-grey-lighter; + border-radius: $border-radius; + + h3 { + font-size: $font-size-base; + margin-top: 0; + margin-bottom: $spacer / 8; + } + + p { + border-bottom: 1px solid $brand-grey-lighter; + padding-bottom: $spacer / 2; + } + + code { + padding: 0; + color: $brand-grey-light; + } +} + +.error { + background: $red; + padding: $spacer / 2; + text-align: center; + color: $brand-white; + border-radius: $border-radius; + font-weight: $font-weight-bold; + font-size: $font-size-small; +} + +.success { + composes: error; + background: $green; +} diff --git a/client/src/components/templates/Asset/Report.tsx b/client/src/components/templates/Asset/Report.tsx new file mode 100644 index 0000000..5d167b7 --- /dev/null +++ b/client/src/components/templates/Asset/Report.tsx @@ -0,0 +1,144 @@ +import React, { PureComponent, ChangeEvent } from 'react' +import axios from 'axios' +import { Logger } from '@oceanprotocol/squid' +import Modal from '../../atoms/Modal' +import styles from './Report.module.scss' +import Button from '../../atoms/Button' +import Input from '../../atoms/Form/Input' +import Form from '../../atoms/Form/Form' +import { serviceUri } from '../../../config' +import Spinner from '../../atoms/Spinner' + +export default class Report extends PureComponent< + { did: string; title: string }, + { + isModalOpen: boolean + comment: string + message: string + isSending: boolean + hasError?: boolean + hasSuccess?: boolean + } +> { + public state = { + isModalOpen: false, + comment: '', + message: 'Sending...', + isSending: false, + hasError: false, + hasSuccess: false + } + + // for canceling axios requests + public signal = axios.CancelToken.source() + + public componentWillUnmount() { + this.signal.cancel() + } + + private inputChange = (event: ChangeEvent) => { + this.setState({ + comment: event.target.value + }) + } + + private toggleModal = () => { + this.setState({ isModalOpen: !this.state.isModalOpen }) + } + + private sendEmail = async (event: Event) => { + event.preventDefault() + this.setState({ isSending: true }) + + const msg = { + to: process.env.REACT_APP_REPORT_EMAIL, + from: 'info@oceanprotocol.com', + subject: `[Report] ${this.props.title}`, + html: `

The following data set was reported:

${this.props.title}
${this.props.did}

${this.state.comment}
` + } + + try { + const response = await axios({ + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + url: `${serviceUri}/api/v1/report`, + data: { msg }, + cancelToken: this.signal.token + }) + + this.setState({ + isSending: false, + hasSuccess: true, + message: 'Thanks for the report! We will take a look soon.' + }) + return response.data.result + } catch (error) { + !axios.isCancel(error) && + this.setState({ + message: error.message, + isSending: false, + hasError: true + }) && + Logger.error(error.message) + } + } + + public render() { + return ( +
+ + +
+

{this.props.title}

+

+ {this.props.did} +

+ + {this.state.isSending ? ( + + ) : this.state.hasError ? ( +
+ {this.state.message} +
+ ) : this.state.hasSuccess ? ( +
+ {this.state.message} +
+ ) : ( +
+ + +
+ )} +
+
+
+ ) + } +} diff --git a/client/src/styles/global.scss b/client/src/styles/global.scss index b6379f3..7d6e100 100644 --- a/client/src/styles/global.scss +++ b/client/src/styles/global.scss @@ -274,6 +274,9 @@ samp { font-size: $font-size-small; border-radius: $border-radius; text-shadow: none; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-all; h1 &, h2 &, diff --git a/cypress/integration/consume.spec.js b/cypress/integration/consume.spec.js index d266b08..09be8cb 100644 --- a/cypress/integration/consume.spec.js +++ b/cypress/integration/consume.spec.js @@ -3,19 +3,27 @@ context('Consume', () => { before(() => { cy.visit(`/asset/${Cypress.env('CONSUME_ASSET')}`) - // Wait for end of loading - cy.get('button', { timeout: 60000 }).should('have.length', 1) + // Alias button selector & wait for end of loading + cy.get('button[name="Download"]', { timeout: 60000 }) + .first() + .should('have.length', 1) + }) + + beforeEach(() => { + cy.get('button[name="Download"]') + .first() + .as('button') }) it('Download button is clickable when user is connected.', () => { - cy.get('button').should('not.be.disabled') + cy.get('@button').should('not.be.disabled') }) it('Consume asset and check if there is no error', () => { // Click consume button - cy.get('button').click() + cy.get('@button').click() // Wait consume process to end - cy.get('button', { timeout: 150000 }).should('contain', 'Get file') + cy.get('@button', { timeout: 150000 }).should('contain', 'Get file') // check if there is no error cy.get('article>div').should( 'not.contain', diff --git a/package-lock.json b/package-lock.json index c61ae66..1dcdb9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9943,9 +9943,9 @@ } }, "typescript": { - "version": "3.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", - "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz", + "integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==", "dev": true }, "uglify-js": { diff --git a/package.json b/package.json index 90bf1f7..537e8b1 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "stylelint-config-bigchaindb": "^1.2.2", "stylelint-config-css-modules": "^1.4.0", "stylelint-config-standard": "^18.3.0", - "typescript": "3.4.5" + "typescript": "3.5.2" }, "repository": { "type": "git", diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 0000000..28c38a0 --- /dev/null +++ b/server/.env.example @@ -0,0 +1 @@ +SENDGRID_API_KEY='xxx' diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..521b971 --- /dev/null +++ b/server/README.md @@ -0,0 +1,118 @@ +[![banner](https://raw.githubusercontent.com/oceanprotocol/art/master/github/repo-banner%402x.png)](https://oceanprotocol.com) + +

Commons: Server

+ +This folder contains server component written in TypeScript using [Express](https://expressjs.com). The server provides various microservices. + +- [Get Started](#Get-Started) +- [✨ API Documentation](#-API-Documentation) + - [Url Checker](#Url-Checker) + - [Report](#Report) +- [🎁 Contributing](#-Contributing) +- [🏛 License](#-License) + +## Get Started + +To spin up the server in a watch mode for local development, execute: + +```bash +npm install +npm start +``` + +## ✨ API Documentation + +### Url Checker + +Url Checker returns size and additional information about requested file. This service is used as a solution to frontend CORS restrictions. + +**Endpoint:** POST `/api/v1/urlcheck` + +**Request Parameters** + +```json +{ + "url": "https://oceanprotocol.com/tech-whitepaper.pdf" +} +``` + +**Response: Success** + +```json +{ + "status": "success", + "result": { + "found": true, + "contentLength": "2989228", + "contentType": "application/pdf" + } +} +``` + +**Response: Error** + +```json +{ + "status": "error", + "message": null +} +``` + +### Report + +Report endpoints sends an email via SendGrid. Requires `SENDGRID_API_KEY` set as environment variables. + +**Endpoint:** POST `/api/v1/report` + +**Request Parameters** + +```json +{ + "msg": { + "to": "test@example.com", + "from": "test@example.com", + "subject": "My Subject", + "text": "Text", + "html": "HTML" + } +} +``` + +**Response: Success** + +```json +{ + "status": "success" +} +``` + +**Response: Error** + +```json +{ + "status": "error", + "message": "Error message" +} +``` + +## 🎁 Contributing + +See the page titled "[Ways to Contribute](https://docs.oceanprotocol.com/concepts/contributing/)" in the Ocean Protocol documentation. + +## 🏛 License + +```text +Copyright 2019 Ocean Protocol Foundation Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` diff --git a/server/package-lock.json b/server/package-lock.json index 37c4112..410b715 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -351,6 +351,34 @@ "@types/yargs": "^12.0.9" } }, + "@sendgrid/client": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-6.4.0.tgz", + "integrity": "sha512-GcO+hKXMQiwN0xMGfPITArlj4Nab1vZsrsRLmsJlcXGZV1V1zQC6XuAWJv6MGDd0hr/jKaXmCJ1XMYkxIRQHFw==", + "requires": { + "@sendgrid/helpers": "^6.4.0", + "@types/request": "^2.0.3", + "request": "^2.88.0" + } + }, + "@sendgrid/helpers": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-6.4.0.tgz", + "integrity": "sha512-1dDDXauArHyxwTKFFfWvQpsijmwalyLgwoQJ3FRCssFq1RfqYDgFhRg0Xs3v/IXS2jkKWePSWiPORSR4Sysdpw==", + "requires": { + "chalk": "^2.0.1", + "deepmerge": "^2.1.1" + } + }, + "@sendgrid/mail": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-6.4.0.tgz", + "integrity": "sha512-pVzbqbxhZ4FUN6iSIksRLtyXRPurrcee1i0noPDStDCLlHVwUR+TofeeKIFWGpIvbbk5UR6S6iV/U5ie8Kdblw==", + "requires": { + "@sendgrid/client": "^6.4.0", + "@sendgrid/helpers": "^6.4.0" + } + }, "@types/babel__core": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.2.tgz", @@ -405,8 +433,7 @@ "@types/caseless": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", - "dev": true + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" }, "@types/compression": { "version": "0.0.36", @@ -463,7 +490,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", - "dev": true, "requires": { "@types/node": "*" } @@ -524,10 +550,9 @@ } }, "@types/node": { - "version": "11.13.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.15.tgz", - "integrity": "sha512-x6ypl5Uzly+j23hbxmMzf12Eb4lOhIEqQz0HuczpTUa1KIx1GpbN/o4E3aAED20UoEsdK0wvyY8QcffuWSLDkw==", - "dev": true + "version": "12.0.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.12.tgz", + "integrity": "sha512-Uy0PN4R5vgBUXFoJrKryf5aTk3kJ8Rv3PdlHjl6UaX+Cqp1QE0yPQ68MPXGrZOfG7gZVNDIJZYyot0B9ubXUrQ==" }, "@types/range-parser": { "version": "1.2.3", @@ -539,7 +564,6 @@ "version": "2.48.1", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.1.tgz", "integrity": "sha512-ZgEZ1TiD+KGA9LiAAPPJL68Id2UWfeSO62ijSXZjFJArVV+2pKcsVHmrcu+1oiE3q6eDGiFiSolRc4JHoerBBg==", - "dev": true, "requires": { "@types/caseless": "*", "@types/form-data": "*", @@ -564,9 +588,9 @@ "dev": true }, "@types/superagent": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.1.tgz", - "integrity": "sha512-NetXrraTWPcdGG6IwYJhJ5esUGx8AYNiozbc1ENWEsF6BsD4JmNODJczI6Rm1xFPVp6HZESds9YCfqz4zIsM6A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.2.tgz", + "integrity": "sha512-GISrJnl+eZSzkVdsP2bXARXaroe/qKTwl/7v/d7bHP4OhlZKKIExcvQexwTDWHGtalHSLVuM78/Ri54laoOFfQ==", "dev": true, "requires": { "@types/cookiejar": "*", @@ -574,9 +598,9 @@ } }, "@types/supertest": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.7.tgz", - "integrity": "sha512-GibTh4OTkal71btYe2fpZP/rVHIPnnUsYphEaoywVHo+mo2a/LhlOFkIm5wdN0H0DA0Hx8x+tKgCYMD9elHu5w==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.8.tgz", + "integrity": "sha512-wcax7/ip4XSSJRLbNzEIUVy2xjcBIZZAuSd2vtltQfRK7kxhx5WMHbLHkYdxN3wuQCrwpYrg86/9byDjPXoGMA==", "dev": true, "requires": { "@types/superagent": "*" @@ -585,8 +609,7 @@ "@types/tough-cookie": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz", - "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==", - "dev": true + "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==" }, "@types/yargs": { "version": "12.0.12", @@ -681,7 +704,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -1117,7 +1139,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -1241,7 +1262,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -1249,8 +1269,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "combined-stream": { "version": "1.0.8", @@ -1504,6 +1523,11 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==" + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -1605,6 +1629,11 @@ "is-obj": "^1.0.0" } }, + "dotenv": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.0.0.tgz", + "integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==" + }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -1681,8 +1710,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { "version": "1.11.1", @@ -1853,9 +1881,9 @@ } }, "express-validator": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.0.1.tgz", - "integrity": "sha512-hrvN512QBs8zKm2vu33a0AxomZBiDl/0jAYxoq3lnGdXrbWBvObGYugt8jmdfZbzoFDlT2ZF8jhYYszyjtdOjw==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.1.1.tgz", + "integrity": "sha512-AF6YOhdDiCU7tUOO/OHp2W++I3qpYX7EInMmEEcRGOjs+qoubwgc5s6Wo3OQgxwsWRGCxXlrF73SIDEmY4y3wg==", "requires": { "lodash": "^4.17.11", "validator": "^11.0.0" @@ -2800,8 +2828,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { "version": "1.0.0", @@ -5537,7 +5564,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -5793,9 +5819,9 @@ } }, "typescript": { - "version": "3.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", - "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz", + "integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==", "dev": true }, "uglify-js": { @@ -6031,9 +6057,9 @@ } }, "validator": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-11.0.0.tgz", - "integrity": "sha512-+wnGLYqaKV2++nUv60uGzUJyJQwYVOin6pn1tgEiFCeCQO60yeu3Og9/yPccbBX574kxIcEJicogkzx6s6eyag==" + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-11.1.0.tgz", + "integrity": "sha512-qiQ5ktdO7CD6C/5/mYV4jku/7qnqzjrxb3C/Q5wR3vGGinHTgJZN/TdFT3ZX4vXhX2R1PXx42fB1cn5W+uJ4lg==" }, "vary": { "version": "1.1.2", diff --git a/server/package.json b/server/package.json index 103b58d..d822518 100644 --- a/server/package.json +++ b/server/package.json @@ -13,11 +13,13 @@ "coverage": "cat coverage/lcov.info | codacy-coverage --token 8801f827fe1144ffa85cd7da94f2bbf7" }, "dependencies": { + "@sendgrid/mail": "^6.4.0", "body-parser": "^1.18.3", "compression": "^1.7.4", "debug": "^4.1.1", + "dotenv": "^8.0.0", "express": "^4.17.1", - "express-validator": "^6.0.1", + "express-validator": "^6.1.1", "morgan": "^1.9.1", "request": "^2.88.0" }, @@ -28,15 +30,15 @@ "@types/express": "^4.17.0", "@types/jest": "^24.0.15", "@types/morgan": "^1.7.35", - "@types/node": "^11.13.15", + "@types/node": "^12.0.12", "@types/request": "^2.48.1", - "@types/supertest": "^2.0.7", + "@types/supertest": "^2.0.8", "jest": "^24.8.0", "nodemon": "^1.19.1", "supertest": "^4.0.2", "ts-jest": "^24.0.2", "ts-node": "^8.3.0", - "typescript": "3.4.5" + "typescript": "3.5.2" }, "repository": { "type": "git", diff --git a/server/src/routes/ReportRouter.ts b/server/src/routes/ReportRouter.ts new file mode 100644 index 0000000..9751da2 --- /dev/null +++ b/server/src/routes/ReportRouter.ts @@ -0,0 +1,36 @@ +import { Router, Request, Response } from 'express' +import SendgridMail from '@sendgrid/mail' +import 'dotenv/config' + +SendgridMail.setApiKey(process.env.SENDGRID_API_KEY) + +export class ReportRouter { + public router: Router + + public constructor() { + this.router = Router() + } + + public async sendMessage(req: Request, res: Response) { + if (!req.body.msg) { + return res.send({ status: 'error', message: 'missing message' }) + } + + try { + await SendgridMail.send(req.body.msg) + return res.send({ status: 'success' }) + } catch (error) { + console.error(`${error.code} - ${error.message}`) // eslint-disable-line + res.send(`${error.code} - ${error.message}`) + } + } + + public init() { + this.router.post('/', this.sendMessage) + } +} + +const reportRoutes = new ReportRouter() +reportRoutes.init() + +export default reportRoutes.router diff --git a/server/src/server.ts b/server/src/server.ts index 7062d4b..2b85536 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -7,6 +7,7 @@ import pkg from '../../package.json' // routes import UrlCheckRouter from './routes/UrlCheckRouter' +import ReportRouter from './routes/ReportRouter' // config import config from './config/config' @@ -62,6 +63,7 @@ app.get('/', (req, res) => { ) }) app.use('/api/v1/urlcheck', UrlCheckRouter) +app.use('/api/v1/report', ReportRouter) /// catch 404 app.use((req, res) => { diff --git a/server/test/api.test.ts b/server/test/api.test.ts index a43f0b9..b319df1 100644 --- a/server/test/api.test.ts +++ b/server/test/api.test.ts @@ -25,6 +25,14 @@ describe('POST /api/v1/urlcheck', () => { }) }) +describe('POST /api/v1/report', () => { + it('responds with error message when message is missing', async () => { + const response = await request(server).post('/api/v1/report') + const text = await JSON.parse(response.text) + expect(text.message).toBe('missing message') + }) +}) + describe('Errors', () => { it('responds with 404 on unknown path', async () => { const response = await request(server).post('/whatever')