1
0
mirror of https://github.com/oceanprotocol/commons.git synced 2023-03-15 18:03:00 +01:00

Merge pull request #172 from oceanprotocol/feature/reporting

Reporting data sets
This commit is contained in:
Matthias Kretschmann 2019-07-09 11:31:21 +02:00 committed by GitHub
commit b6770e68de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 652 additions and 53 deletions

1
.gitignore vendored
View File

@ -12,6 +12,7 @@ dist
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local

View File

@ -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.

View File

@ -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"

View File

@ -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",

View File

@ -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",

View File

@ -12,6 +12,7 @@ interface ButtonProps {
onClick?: any
disabled?: boolean
to?: string
name?: string
}
export default class Button extends PureComponent<ButtonProps, any> {

View File

@ -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);
}
}

View File

@ -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(
<Modal title="Hello" isOpen toggleModal={() => null}>
Hello
</Modal>
)
expect(document.querySelector('.ReactModalPortal')).toBeInTheDocument()
})
})

View File

@ -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 (
<ReactModal
isOpen={isOpen}
onAfterOpen={onAfterOpen}
onRequestClose={onRequestClose}
contentLabel={title}
className={styles.modal}
overlayClassName={styles.modalOverlay}
{...props}
>
<button className={styles.close} onClick={toggleModal}>
&times;
</button>
<header className={styles.header}>
<h2 className={styles.title}>{title}</h2>
{description && (
<p className={styles.description}>{description}</p>
)}
</header>
{children}
</ReactModal>
)
}
export default Modal

View File

@ -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<AssetDetailsProps> {
/>
)}
<Report did={ddo.id} title={metadata.base.name} />
<div className={styles.metaFixed}>
<h2
className={styles.metaFixedTitle}
@ -97,10 +100,6 @@ export default class AssetDetails extends PureComponent<AssetDetailsProps> {
files={base.files ? base.files : []}
ddo={ddo}
/>
{/* <pre>
<code>{JSON.stringify(metadata, null, 2)}</code>
</pre> */}
</>
)
}

View File

@ -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
</Button>

View File

@ -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;
}

View File

@ -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<HTMLInputElement>) => {
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: `<p>The following data set was reported:</p><p><strong>${this.props.title}</strong><br /><a style="color:#ff4092;text-decoration:none" href="https://commons.oceanprotocol.com/asset/${this.props.did}"><code>${this.props.did}</code></a></p><blockquote><em>${this.state.comment}</em></blockquote>`
}
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 (
<div className={styles.actions}>
<Button
link
className={styles.openLink}
onClick={this.toggleModal}
>
Report Data Set
</Button>
<Modal
title="Report Data Set"
description="Found some faulty metadata, wrongly attributed data, or anything else wrong with this data set? Tell us about it and we will take a look."
isOpen={this.state.isModalOpen}
toggleModal={this.toggleModal}
>
<div className={styles.info}>
<h3>{this.props.title}</h3>
<p>
<code>{this.props.did}</code>
</p>
{this.state.isSending ? (
<Spinner message={this.state.message} />
) : this.state.hasError ? (
<div className={styles.error}>
{this.state.message}
</div>
) : this.state.hasSuccess ? (
<div className={styles.success}>
{this.state.message}
</div>
) : (
<Form minimal>
<Input
type="textarea"
name="comment"
label="Comment"
help="Briefly describe what is wrong with this asset. If you want to get contacted by us, add your email at the end."
required
value={this.state.comment}
onChange={this.inputChange}
rows={1}
/>
<Button
primary
onClick={(e: Event) => this.sendEmail(e)}
disabled={this.state.comment === ''}
>
Report Data Set
</Button>
</Form>
)}
</div>
</Modal>
</div>
)
}
}

View File

@ -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 &,

View File

@ -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',

6
package-lock.json generated
View File

@ -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": {

View File

@ -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",

1
server/.env.example Normal file
View File

@ -0,0 +1 @@
SENDGRID_API_KEY='xxx'

118
server/README.md Normal file
View File

@ -0,0 +1,118 @@
[![banner](https://raw.githubusercontent.com/oceanprotocol/art/master/github/repo-banner%402x.png)](https://oceanprotocol.com)
<h1 align="center">Commons: Server</h1>
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": "<strong>HTML</strong>"
}
}
```
**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.
```

View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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) => {

View File

@ -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')