diff options
author | poeti8 <ezzati.upt@gmail.com> | 2019-07-06 09:07:39 +0300 |
---|---|---|
committer | poeti8 <ezzati.upt@gmail.com> | 2019-07-06 09:07:39 +0300 |
commit | 5f434c59f29ad676c078c8d37ec9f266d3432638 (patch) | |
tree | f17c0468be95159c96ae81e4d0e01f2fd3aff163 | |
parent | bacb93b24a14b38cc7a3c2923b37232c293ec128 (diff) | |
parent | 1de09a388a520aa1502e71201c80c7f695ffb19d (diff) |
Merge branch 'develop' into feature/pricing-tablefeature/pricing-table
-rw-r--r-- | Dockerfile | 47 | ||||
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | client/actions/__test__/url.js | 2 | ||||
-rw-r--r-- | client/store/index.js | 15 | ||||
-rw-r--r-- | docker-compose.yml | 27 | ||||
-rw-r--r-- | docker-examples/README.md | 63 | ||||
-rw-r--r-- | docker-examples/server-config.example.js | 36 | ||||
-rw-r--r-- | package.json | 4 | ||||
-rwxr-xr-x | run.sh | 94 | ||||
-rw-r--r-- | server/controllers/urlController.js | 2 | ||||
-rw-r--r-- | server/db/url.js | 510 | ||||
-rw-r--r-- | server/db/user.js | 385 | ||||
-rw-r--r-- | server/offline/sw.js | 20 | ||||
-rw-r--r-- | server/server.js | 9 |
14 files changed, 452 insertions, 763 deletions
@@ -1,40 +1,17 @@ -FROM node:9.5.0-alpine +FROM node:10-alpine -ADD . /code -WORKDIR /code -RUN apk add --no-cache bash -RUN npm install -RUN npm run build - -ENV KUTT_PORT 3000 -ENV KUTT_DOMAIN 'kutt.it' - -ENV NEO4J_HOST '127.0.0.1' -ENV NEO4J_USER '' -ENV NEO4J_PASS '' - -ENV REDIS_HOST '127.0.0.1' -ENV REDIS_PORT 6379 -ENV REDIS_PASSWORD '' +# Setting working directory. +WORKDIR /usr/src/app -ENV USER_LIMIT_PER_DAY 50 -ENV JWT_SECRET 'mysecurekey' -ENV ADMIN_EMAILS '[]' - -ENV RECAPTCHA_SECRET_KEY '' -ENV RECAPTCHA_SITE_KEY '' - -ENV GOOGLE_SAFE_BROWSING_KEY '' -ENV GOOGLE_ANALYTICS '' +# Installing dependencies +COPY package*.json ./ +RUN npm install -ENV MAIL_HOST '' -ENV MAIL_PORT '' -ENV MAIL_SECURE 'false' -ENV MAIL_USER '' -ENV MAIL_FROM '' -ENV MAIL_PASSWORD '' -ENV MAIL_REPORT '' -ENV CONTACT_EMAIL '' +# Copying source files +COPY . . -CMD /code/run.sh +# Building app +RUN npm run build +# Running the app +CMD [ "npm", "start" ]
\ No newline at end of file @@ -132,6 +132,7 @@ Download Kutt's official workflow for [Alfred](https://www.alfredapp.com/) app f | C# (.NET) | [KuttSharp](https://github.com/0xaryan/KuttSharp) | .NET package for Kutt.it url shortener | | Python | [kutt-cli](https://github.com/univa64/kutt-cli) | Command-line client for Kutt written in Python | | Ruby | [kutt.rb](https://github.com/univa64/kutt.rb) | Kutt library written in Ruby | +| Rust | [kutt-rs](https://github.com/robatipoor/kutt-rs) | Command line tool written in Rust | | Node.js | [node-kutt](https://github.com/ardalanamini/node-kutt) | Node.js client for Kutt.it url shortener | | Bash | [kutt-bash](https://git.nixnet.xyz/caltlgin/kutt-bash) | Simple command line program for Kutt | diff --git a/client/actions/__test__/url.js b/client/actions/__test__/url.js index 24d4674..29ee62d 100644 --- a/client/actions/__test__/url.js +++ b/client/actions/__test__/url.js @@ -138,7 +138,7 @@ describe('url actions', () => { } }) .post('/api/url/deleteurl') - .reply(200, { message: 'Sort URL deleted successfully' }); + .reply(200, { message: 'Short URL deleted successfully' }); const store = mockStore({ url: { list: mockedItems } }); diff --git a/client/store/index.js b/client/store/index.js index dc9be51..9278e14 100644 --- a/client/store/index.js +++ b/client/store/index.js @@ -1,9 +1,6 @@ -import { createStore, applyMiddleware } from 'redux'; -import { composeWithDevTools } from 'redux-devtools-extension'; -import thunk from 'redux-thunk'; -import rootReducer from '../reducers'; - -const store = initialState => - createStore(rootReducer, initialState, composeWithDevTools(applyMiddleware(thunk))); - -export default store; +/* eslint-disable global-require */ +if (process.env.NODE_ENV === 'production') { + module.exports = require('./store.prod'); +} else { + module.exports = require('./store.dev'); +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3419f04 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +version: '3' + +services: + kutt: + build: . + container_name: kutt + links: + - neo4j + - redis + ports: + - "3000:3000" + env_file: + - .env + + redis: + image: "redis:alpine" + container_name: kutt-redis + expose: + - 6379 + + neo4j: + image: neo4j:3.5 + container_name: kutt-neo4j + environment: + - NEO4J_AUTH=neo4j/test + expose: + - 7687
\ No newline at end of file diff --git a/docker-examples/README.md b/docker-examples/README.md deleted file mode 100644 index 9d61ff1..0000000 --- a/docker-examples/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# Running Kutt in Docker - -Assumptions: -The domain in this example is `kutt.local`. This needs to be configured in your hosts file for this example to work _as written_. You should, of course, modify this domain to suit your needs. - -### Configure Kutt - -server/config.js -``` -module.exports = { - - PORT: process.env.KUTT_PORT, # Or whatever you want to name the env var - - /* The domain that this website is on */ - DEFAULT_DOMAIN: process.env.KUTT_DOMAIN, # Or whatever.. - - ... - -} -``` - -No docker-relevant modifications are necessary for client/config.js. However, you will still need to configure this as part of the standard kutt setup. - -### Neo4j in a container - -You can run neo4j in a container and link it to the kutt container in Docker. - -Properly installing and running neo4j is outside of the scope of this document. But here's a simple one-liner to get neo4j running on docker for dev/test: - -``` -docker run \ - --publish=7474:7474 --publish=7687:7687 \ - --name neo4j \ - neo4j -``` -**This is not a production-ready setup. There is no data persistence, nor proper security. Use for test/dev only.** - -Then, configure Kutt: -server/config.js -``` -... -/* Neo4j database credential details */ -DB_URI: 'bolt://neo4j', -DB_USERNAME: 'neo4j', # Or pass this in via env var as before -DB_PASSWORD: 'neo4j', # Or via env var.. -... -``` - -Once you have neo4j running in a container, you'll link your Kutt container to it. This will be documented below. - -### Build Kutt Image - -First you'll need to build Kutt. -From the root directory of Kutt, execute the following: -`docker build -t kutt .` - -### Run Kutt - -Once you've built the image, then all that is left to do is run Kutt. - -`docker run -d -p80:3000 -e KUTT_PORT=3000 -e KUTT_DOMAIN=kutt.local --link=neo4j kutt` - -Direct your browser to http://kutt.local/ and begin kutting URLs! diff --git a/docker-examples/server-config.example.js b/docker-examples/server-config.example.js deleted file mode 100644 index 1a62739..0000000 --- a/docker-examples/server-config.example.js +++ /dev/null @@ -1,36 +0,0 @@ -module.exports = { - PORT: process.env.KUTT_PORT, - - /* The domain that this website is on */ - DEFAULT_DOMAIN: process.env.KUTT_DOMAIN, - - /* Neo4j database credential details */ - DB_URI: 'bolt://localhost', - DB_USERNAME: '', - DB_PASSWORD: '', - - /* A passphrase to encrypt JWT. Use a long and secure key. */ - JWT_SECRET: 'securekey', - - /* - Invisible reCaptcha secret key - Create one in https://www.google.com/recaptcha/intro/ - */ - RECAPTCHA_SECRET_KEY: '', - - /* - Google Cloud API to prevent from users from submitting malware URLs. - Get it from https://developers.google.com/safe-browsing/v4/get-started - */ - GOOGLE_SAFE_BROWSING_KEY: '', - - /* - Your email host details to use to send verification emails. - More info on http://nodemailer.com/ - */ - MAIL_HOST: '', - MAIL_PORT: 587, - MAIL_SECURE: false, - MAIL_USER: '', - MAIL_PASSWORD: '', -}; diff --git a/package.json b/package.json index d565219..472d771 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "scripts": { "test": "mocha --compilers js:@babel/register ./client/**/__test__/*.js", "dev": "nodemon ./server/server.js", + "docker:build": "docker build -t kutt .", + "docker:run": "docker run -p 3000:3000 --env-file .env -d kutt:latest", "build": "next build ./client", "start": "NODE_ENV=production node ./server/server.js", "lint": "./node_modules/.bin/eslint . --fix", @@ -51,7 +53,7 @@ "ms": "^2.1.1", "nanoid": "^1.3.4", "natives": "^1.1.6", - "neo4j-driver": "^1.7.2", + "neo4j-driver": "^1.7.5", "next": "^7.0.3", "next-redux-wrapper": "^2.1.0", "node-cron": "^2.0.3", @@ -1,94 +0,0 @@ -#!/bin/bash - - -echo "Configuring client" - -cat <<EOF > client/config.js -module.exports = { - /* - Invisible reCaptcha site key - Create one in https://www.google.com/recaptcha/intro/ - */ - RECAPTCHA_SITE_KEY: "${RECAPTCHA_SITE_KEY}", - - // Google analytics tracking ID - GOOGLE_ANALYTICS_ID: "${GOOGLE_ANALYTICS}", - - // Contact email address - CONTACT_EMAIL: "${CONTACT_EMAIL}", - - // Report email address - REPORT_EMAIL: "${MAIL_REPORT}", -}; -EOF - -cat <<EOF > server/config.js -module.exports = { - PORT: process.env.KUTT_PORT, - - /* The domain that this website is on */ - DEFAULT_DOMAIN: process.env.KUTT_DOMAIN, - - /* Neo4j database credential details */ - DB_URI: 'bolt://' + process.env.NEO4J_HOST, - DB_USERNAME: process.env.NEO4J_USER, - DB_PASSWORD: process.env.NEO4J_PASS, - - /* Redis host and port */ - REDIS_HOST: process.env.REDIS_HOST, - REDIS_PORT: process.env.REDIS_PORT, - REDIS_PASSWORD: process.env.REDIS_PASS, - - /* The daily limit for each user */ - USER_LIMIT_PER_DAY: parseInt(process.env.USER_LIMIT_PER_DAY || 50, 10), - - /* A passphrase to encrypt JWT. Use a long and secure key. */ - JWT_SECRET: process.env.JWT_SECRET, - - /* - Admin emails so they can access admin actions on settings page - Array of strings - */ - ADMIN_EMAILS: JSON.parse(process.env.ADMIN_EMAILS || '[]'), - - /* - Invisible reCaptcha secret key - Create one in https://www.google.com/recaptcha/intro/ - */ - RECAPTCHA_SECRET_KEY: process.env.RECAPTCHA_SECRET_KEY, - - /* - Google Cloud API to prevent from users from submitting malware URLs. - Get it from https://developers.google.com/safe-browsing/v4/get-started - */ - GOOGLE_SAFE_BROWSING_KEY: process.env.GOOGLE_SAFE_BROWSING_KEY, - - /* - Google Analytics tracking ID for universal analytics. - Example: UA-XXXX-XX - */ - GOOGLE_ANALYTICS: process.env.GOOGLE_ANALYTICS, - - /* - Your email host details to use to send verification emails. - More info on http://nodemailer.com/ - */ - MAIL_HOST: process.env.MAIL_HOST, - MAIL_PORT: process.env.MAIL_PORT, - MAIL_SECURE: process.env.MAIL_SECURE == 'true', - MAIL_USER: process.env.MAIL_USER, - MAIL_FROM: process.env.MAIL_FROM, // Example: "Kutt <support@kutt.it>". Leave empty to use MAIL_USER - MAIL_PASSWORD: process.env.MAIL_PASS, - - /* - The email address that will receive submitted reports. - */ - REPORT_MAIL: process.env.MAIL_REPORT, -}; -EOF - -echo "Building Client" -./node_modules/.bin/next build ./client - -echo "Starting" -npm start diff --git a/server/controllers/urlController.js b/server/controllers/urlController.js index 847bc01..a3996e9 100644 --- a/server/controllers/urlController.js +++ b/server/controllers/urlController.js @@ -276,7 +276,7 @@ exports.deleteUrl = async ({ body: { id, domain }, user }, res) => { if (!urls && !urls.length) return res.status(400).json({ error: "Couldn't find the short URL." }); redis.del(id + (customDomain || '')); const response = await deleteUrl({ id, domain: customDomain, user }); - if (response) return res.status(200).json({ message: 'Sort URL deleted successfully' }); + if (response) return res.status(200).json({ message: 'Short URL deleted successfully' }); return res.status(400).json({ error: "Couldn't delete short URL." }); }; diff --git a/server/db/url.js b/server/db/url.js index bfbc849..1eaae48 100644 --- a/server/db/url.js +++ b/server/db/url.js @@ -19,274 +19,229 @@ const queryNewUserUrl = (domain, password) => `${domain ? 'MERGE (l)-[:USES]->(:DOMAIN { name: $domain })' : ''}` + 'RETURN l'; -exports.createShortUrl = params => - new Promise(async (resolve, reject) => { - const query = params.user ? queryNewUserUrl(params.user.domain, params.password) : queryNewUrl; - const session = driver.session(); - const salt = params.password && (await bcrypt.genSalt(12)); - const hash = params.password && (await bcrypt.hash(params.password, salt)); - session - .writeTransaction(tx => - tx.run(query, { - createdAt: new Date().toJSON(), - domain: params.user && params.user.domain, - email: params.user && params.user.email, - id: params.id, - password: hash || '', - target: params.target, - }) - ) - .then(({ records }) => { - session.close(); - const data = records[0].get('l').properties; - resolve({ - ...data, - password: !!data.password, - reuse: !!params.reuse, - shortUrl: generateShortUrl( - data.id, - params.user && params.user.domain, - params.user && params.user.useHttps - ), - }); - }) - .catch(err => session.close() || reject(err)); - }); +exports.createShortUrl = async params => { + const session = driver.session(); + const query = params.user ? queryNewUserUrl(params.user.domain, params.password) : queryNewUrl; + const salt = params.password && (await bcrypt.genSalt(12)); + const hash = params.password && (await bcrypt.hash(params.password, salt)); + const { records = [] } = await session.writeTransaction(tx => + tx.run(query, { + createdAt: new Date().toJSON(), + domain: params.user && params.user.domain, + email: params.user && params.user.email, + id: params.id, + password: hash || '', + target: params.target, + }) + ); + session.close(); + const data = records[0].get('l').properties; + return { + ...data, + password: !!data.password, + reuse: !!params.reuse, + shortUrl: generateShortUrl( + data.id, + params.user && params.user.domain, + params.user && params.user.useHttps + ), + }; +}; -exports.createVisit = params => - new Promise((resolve, reject) => { - const session = driver.session(); - session - .writeTransaction(tx => - tx.run( - 'MATCH (l:URL { id: $id }) ' + - `${params.domain ? 'MATCH (l)-[:USES]->({ name: $domain })' : ''} ` + - 'SET l.count = l.count + 1 ' + - 'CREATE (v:VISIT)' + - 'MERGE (b:BROWSER { browser: $browser })' + - 'MERGE (c:COUNTRY { country: $country })' + - 'MERGE (o:OS { os: $os })' + - 'MERGE (r:REFERRER { referrer: $referrer })' + - 'MERGE (d:DATE { date: $date })' + - 'MERGE (v)-[:VISITED]->(l)' + - 'MERGE (v)-[:BROWSED_BY]->(b)' + - 'MERGE (v)-[:LOCATED_IN]->(c)' + - 'MERGE (v)-[:OS]->(o)' + - 'MERGE (v)-[:REFERRED_BY]->(r)' + - 'MERGE (v)-[:VISITED_IN]->(d)' + - 'RETURN l', - { - id: params.id, - browser: params.browser, - domain: params.domain, - country: params.country, - os: params.os, - referrer: params.referrer, - date: getUTCDate().toJSON(), - } - ) - ) - .then(({ records }) => { - session.close(); - const url = records.length && records[0].get('l').properties; - resolve(url); - }) - .catch(err => session.close() || reject(err)); - }); +exports.createVisit = async params => { + const session = driver.session(); + const { records = [] } = await session.writeTransaction(tx => + tx.run( + 'MATCH (l:URL { id: $id }) ' + + `${params.domain ? 'MATCH (l)-[:USES]->({ name: $domain })' : ''} ` + + 'SET l.count = l.count + 1 ' + + 'CREATE (v:VISIT)' + + 'MERGE (b:BROWSER { browser: $browser })' + + 'MERGE (c:COUNTRY { country: $country })' + + 'MERGE (o:OS { os: $os })' + + 'MERGE (r:REFERRER { referrer: $referrer })' + + 'MERGE (d:DATE { date: $date })' + + 'MERGE (v)-[:VISITED]->(l)' + + 'MERGE (v)-[:BROWSED_BY]->(b)' + + 'MERGE (v)-[:LOCATED_IN]->(c)' + + 'MERGE (v)-[:OS]->(o)' + + 'MERGE (v)-[:REFERRED_BY]->(r)' + + 'MERGE (v)-[:VISITED_IN]->(d)' + + 'RETURN l', + { + id: params.id, + browser: params.browser, + domain: params.domain, + country: params.country, + os: params.os, + referrer: params.referrer, + date: getUTCDate().toJSON(), + } + ) + ); + session.close(); + const url = records.length && records[0].get('l').properties; + return url; +}; -exports.findUrl = ({ id, domain, target }) => - new Promise((resolve, reject) => { - const session = driver.session(); - session - .readTransaction(tx => - tx.run( - `MATCH (l:URL { ${id ? 'id: $id' : 'target: $target'} })` + - `${ - domain - ? 'MATCH (l)-[:USES]->(d:DOMAIN { name: $domain })' - : 'OPTIONAL MATCH (l)-[:USES]->(d)' - }` + - 'OPTIONAL MATCH (u)-[:CREATED]->(l)' + - 'RETURN l, d.name AS domain, u.email AS user', - { - id, - domain, - target, - } - ) - ) - .then(({ records }) => { - session.close(); - const url = - records.length && - records.map(record => ({ - ...record.get('l').properties, - domain: record.get('domain'), - user: record.get('user'), - })); - resolve(url); - }) - .catch(err => session.close() || reject(err)); - }); +exports.findUrl = async ({ id, domain, target }) => { + const session = driver.session(); + const { records = [] } = await session.readTransaction(tx => + tx.run( + `MATCH (l:URL { ${id ? 'id: $id' : 'target: $target'} })` + + `${ + domain + ? 'MATCH (l)-[:USES]->(d:DOMAIN { name: $domain })' + : 'OPTIONAL MATCH (l)-[:USES]->(d)' + }` + + 'OPTIONAL MATCH (u)-[:CREATED]->(l)' + + 'RETURN l, d.name AS domain, u.email AS user', + { + id, + domain, + target, + } + ) + ); + session.close(); + const url = + records.length && + records.map(record => ({ + ...record.get('l').properties, + domain: record.get('domain'), + user: record.get('user'), + })); + return url; +}; -exports.getCountUrls = ({ user }) => - new Promise((resolve, reject) => { - const session = driver.session(); - session - .readTransaction(tx => - tx.run('MATCH (u:USER {email: $email}) RETURN size((u)-[:CREATED]->()) as count', { - email: user.email, - }) - ) - .then(({ records }) => { - session.close(); - const countAll = records.length ? records[0].get('count').toNumber() : 0; - resolve({ countAll }); - }) - .catch(err => session.close() || reject(err)); - }); +exports.getCountUrls = async ({ user }) => { + const session = driver.session(); + const { records = [] } = await session.readTransaction(tx => + tx.run('MATCH (u:USER {email: $email}) RETURN size((u)-[:CREATED]->()) as count', { + email: user.email, + }) + ); + session.close(); + const countAll = records.length ? records[0].get('count').toNumber() : 0; + return { countAll }; +}; -exports.getUrls = ({ user, options, setCount }) => - new Promise((resolve, reject) => { - const session = driver.session(); - const { count = 5, page = 1, search = '' } = options; - const limit = parseInt(count, 10); - const skip = parseInt(page, 10); - const searchQuery = search ? 'WHERE l.id =~ $search OR l.target =~ $search' : ''; - const setVisitsCount = setCount ? 'SET l.count = size((l)<-[:VISITED]-())' : ''; - session - .readTransaction(tx => - tx.run( - `MATCH (u:USER { email: $email })-[:CREATED]->(l) ${searchQuery} ` + - 'WITH l ORDER BY l.createdAt DESC ' + - 'WITH l SKIP $skip LIMIT $limit ' + - `OPTIONAL MATCH (l)-[:USES]->(d) ${setVisitsCount} ` + - 'RETURN l, d.name AS domain, d.useHttps as useHttps', - { - email: user.email, - limit, - skip: limit * (skip - 1), - search: `(?i).*${search}.*`, - } - ) - ) - .then(({ records }) => { - session.close(); - const urls = records.map(record => { - const visitCount = record.get('l').properties.count; - const domain = record.get('domain'); - const protocol = record.get('useHttps') || !domain ? 'https://' : 'http://'; - return { - ...record.get('l').properties, - count: typeof visitCount === 'object' ? visitCount.toNumber() : visitCount, - password: !!record.get('l').properties.password, - shortUrl: `${protocol}${domain || process.env.DEFAULT_DOMAIN}/${ - record.get('l').properties.id - }`, - }; - }); - resolve({ list: urls }); - }) - .catch(err => session.close() || reject(err)); +exports.getUrls = async ({ user, options, setCount }) => { + const session = driver.session(); + const { count = 5, page = 1, search = '' } = options; + const limit = parseInt(count, 10); + const skip = parseInt(page, 10); + const searchQuery = search ? 'WHERE l.id =~ $search OR l.target =~ $search' : ''; + const setVisitsCount = setCount ? 'SET l.count = size((l)<-[:VISITED]-())' : ''; + const { records = [] } = await session.readTransaction(tx => + tx.run( + `MATCH (u:USER { email: $email })-[:CREATED]->(l) ${searchQuery} ` + + 'WITH l ORDER BY l.createdAt DESC ' + + 'WITH l SKIP $skip LIMIT $limit ' + + `OPTIONAL MATCH (l)-[:USES]->(d) ${setVisitsCount} ` + + 'RETURN l, d.name AS domain, d.useHttps as useHttps', + { + email: user.email, + limit, + skip: limit * (skip - 1), + search: `(?i).*${search}.*`, + } + ) + ); + session.close(); + const urls = records.map(record => { + const visitCount = record.get('l').properties.count; + const domain = record.get('domain'); + const protocol = record.get('useHttps') || !domain ? 'https://' : 'http://'; + return { + ...record.get('l').properties, + count: typeof visitCount === 'object' ? visitCount.toNumber() : visitCount, + password: !!record.get('l').properties.password, + shortUrl: `${protocol}${domain || process.env.DEFAULT_DOMAIN}/${ + record.get('l').properties.id + }`, + }; }); + return { list: urls }; +}; -exports.getCustomDomain = ({ customDomain }) => - new Promise((resolve, reject) => { - const session = driver.session(); - session - .readTransaction(tx => - tx.run( - 'MATCH (d:DOMAIN { name: $customDomain })<-[:OWNS]-(u) RETURN u.email as email, d.homepage as homepage', - { - customDomain, - } - ) - ) - .then(({ records }) => { - session.close(); - const data = records.length - ? { - email: records[0].get('email'), - homepage: records[0].get('homepage'), - } - : {}; - resolve(data); - }) - .catch(err => session.close() || reject(err)); - }); +exports.getCustomDomain = async ({ customDomain }) => { + const session = driver.session(); + const { records = [] } = await session.readTransaction(tx => + tx.run( + 'MATCH (d:DOMAIN { name: $customDomain })<-[:OWNS]-(u) RETURN u.email as email, d.homepage as homepage', + { + customDomain, + } + ) + ); + session.close(); + const data = records.length + ? { + email: records[0].get('email'), + homepage: records[0].get('homepage'), + } + : {}; + return data; +}; -exports.setCustomDomain = ({ user, customDomain, homepage, useHttps }) => - new Promise((resolve, reject) => { - const session = driver.session(); - session - .writeTransaction(tx => - tx.run( - 'MATCH (u:USER { email: $email }) ' + - 'OPTIONAL MATCH (u)-[r:OWNS]->() DELETE r ' + - `MERGE (d:DOMAIN { name: $customDomain, homepage: $homepage, useHttps: $useHttps }) ` + - 'MERGE (u)-[:OWNS]->(d) RETURN u, d', - { - customDomain, - homepage: homepage || '', - email: user.email, - useHttps: !!useHttps, - } - ) - ) - .then(({ records }) => { - session.close(); - const data = records.length && records[0].get('d').properties; - resolve(data); - }) - .catch(err => session.close() || reject(err)); - }); +exports.setCustomDomain = async ({ user, customDomain, homepage, useHttps }) => { + const session = driver.session(); + const { records = [] } = await session.writeTransaction(tx => + tx.run( + 'MATCH (u:USER { email: $email }) ' + + 'OPTIONAL MATCH (u)-[r:OWNS]->() DELETE r ' + + `MERGE (d:DOMAIN { name: $customDomain, homepage: $homepage, useHttps: $useHttps }) ` + + 'MERGE (u)-[:OWNS]->(d) RETURN u, d', + { + customDomain, + homepage: homepage || '', + email: user.email, + useHttps: !!useHttps, + } + ) + ); + session.close(); + const data = records.length && records[0].get('d').properties; + return data; +}; -exports.deleteCustomDomain = ({ user }) => - new Promise((resolve, reject) => { - const session = driver.session(); - session - .writeTransaction(tx => - tx.run('MATCH (u:USER { email: $email }) MATCH (u)-[r:OWNS]->() DELETE r RETURN u', { - email: user.email, - }) - ) - .then(({ records }) => { - session.close(); - const data = records.length && records[0].get('u').properties; - resolve(data); - }) - .catch(err => session.close() || reject(err)); - }); +exports.deleteCustomDomain = async ({ user }) => { + const session = driver.session(); + const { records = [] } = await session.writeTransaction(tx => + tx.run('MATCH (u:USER { email: $email }) MATCH (u)-[r:OWNS]->() DELETE r RETURN u', { + email: user.email, + }) + ); + session.close(); + const data = records.length && records[0].get('u').properties; + return data; +}; -exports.deleteUrl = ({ id, domain, user }) => - new Promise((resolve, reject) => { - const session = driver.session(); - session - .writeTransaction(tx => - tx.run( - 'MATCH (u:USER { email: $email }) ' + - 'MATCH (u)-[:CREATED]->(l { id: $id }) ' + - `${ - domain - ? 'MATCH (l)-[:USES]->(:DOMAIN { name: $domain })' - : 'MATCH (l) WHERE NOT (l)-[:USES]->()' - }` + - 'OPTIONAL MATCH (l)-[:MATCHES]->(v) ' + - 'DETACH DELETE l, v RETURN u', - { - email: user.email, - domain, - id, - } - ) - ) - .then(({ records }) => { - session.close(); - const data = records.length && records[0].get('u').properties; - resolve(data); - }) - .catch(err => session.close() || reject(err)); - }); +exports.deleteUrl = async ({ id, domain, user }) => { + const session = driver.session(); + const { records = [] } = await session.writeTransaction(tx => + tx.run( + 'MATCH (u:USER { email: $email }) ' + + 'MATCH (u)-[:CREATED]->(l { id: $id }) ' + + `${ + domain + ? 'MATCH (l)-[:USES]->(:DOMAIN { name: $domain })' + : 'MATCH (l) WHERE NOT (l)-[:USES]->()' + }` + + 'OPTIONAL MATCH (l)-[:MATCHES]->(v) ' + + 'DETACH DELETE l, v RETURN u', + { + email: user.email, + domain, + id, + } + ) + ); + session.close(); + const data = records.length && records[0].get('u').properties; + return data; +}; /* ** Collecting stats @@ -425,27 +380,22 @@ exports.getStats = ({ id, domain, user }) => }); }); -exports.urlCountFromDate = ({ date, email }) => - new Promise((resolve, reject) => { - const session = driver.session(); - session - .readTransaction(tx => - tx.run( - 'MATCH (u:USER { email: $email })-[:CREATED]->(l) WHERE l.createdAt > $date ' + - 'WITH COUNT(l) as count RETURN count', - { - date, - email, - } - ) - ) - .then(({ records }) => { - session.close(); - const count = records.length && records[0].get('count').toNumber(); - return resolve({ count }); - }) - .catch(err => reject(err)); - }); +exports.urlCountFromDate = async ({ date, email }) => { + const session = driver.session(); + const { records = [] } = await session.readTransaction(tx => + tx.run( + 'MATCH (u:USER { email: $email })-[:CREATED]->(l) WHERE l.createdAt > $date ' + + 'WITH COUNT(l) as count RETURN count', + { + date, + email, + } + ) + ); + session.close(); + const count = records.length && records[0].get('count').toNumber(); + return { count }; +}; exports.banUrl = async ({ adminEmail, id, domain, host, user }) => { const session = driver.session(); diff --git a/server/db/user.js b/server/db/user.js index 871402b..a482ed8 100644 --- a/server/db/user.js +++ b/server/db/user.js @@ -3,232 +3,183 @@ const nanoid = require('nanoid'); const subMinutes = require('date-fns/sub_minutes'); const driver = require('./neo4j'); -exports.getUser = ({ email = '', apikey = '' }) => - new Promise((resolve, reject) => { - const session = driver.session(); - session - .readTransaction(tx => - tx.run( - 'MATCH (u:USER) WHERE u.email = $email OR u.apikey = $apikey ' + - 'OPTIONAL MATCH (u)-[r:RECEIVED]->(c) WITH u, collect(c.date) as cooldowns ' + - 'OPTIONAL MATCH (u)-[:OWNS]->(d) RETURN u, d, cooldowns', - { - apikey, - email, - } - ) - ) - .then(res => { - session.close(); - const user = res.records.length && res.records[0].get('u').properties; - const cooldowns = res.records.length && res.records[0].get('cooldowns'); - const domainProps = res.records.length && res.records[0].get('d'); - const domain = domainProps ? domainProps.properties.name : ''; - const homepage = domainProps ? domainProps.properties.homepage : ''; - const useHttps = domainProps ? domainProps.properties.useHttps : ''; - return resolve(user && { ...user, cooldowns, domain, homepage, useHttps }); - }) - .catch(err => reject(err)); - }); +exports.getUser = async ({ email = '', apikey = '' }) => { + const session = driver.session(); + const { records = [] } = await session.readTransaction(tx => + tx.run( + 'MATCH (u:USER) WHERE u.email = $email OR u.apikey = $apikey ' + + 'OPTIONAL MATCH (u)-[r:RECEIVED]->(c) WITH u, collect(c.date) as cooldowns ' + + 'OPTIONAL MATCH (u)-[:OWNS]->(d) RETURN u, d, cooldowns', + { + apikey, + email, + } + ) + ); + session.close(); + const user = records.length && records[0].get('u').properties; + const cooldowns = records.length && records[0].get('cooldowns'); + const domainProps = records.length && records[0].get('d'); + const domain = domainProps ? domainProps.properties.name : ''; + const homepage = domainProps ? domainProps.properties.homepage : ''; + const useHttps = domainProps ? domainProps.properties.useHttps : ''; + return user && { ...user, cooldowns, domain, homepage, useHttps }; +}; -exports.createUser = ({ email, password }) => - new Promise(async (resolve, reject) => { - const session = driver.session(); - const salt = await bcrypt.genSalt(12); - const hash = await bcrypt.hash(password, salt); - const verificationToken = nanoid(40); - session - .writeTransaction(tx => - tx.run( - 'MERGE (u:USER { email: $email }) ' + - 'SET u.password = $hash , u.verified = $verified , ' + - 'u.verificationToken = $verificationToken , u.createdAt = $createdAt ' + - 'RETURN u', - { - email, - hash, - createdAt: new Date().toJSON(), - verified: false, - verificationToken, - } - ) - ) - .then(res => { - session.close(); - const user = res.records[0].get('u').properties; - return resolve(user); - }) - .catch(err => reject(err)); - }); +exports.createUser = async ({ email, password }) => { + const session = driver.session(); + const salt = await bcrypt.genSalt(12); + const hash = await bcrypt.hash(password, salt); + const verificationToken = nanoid(40); + const { records = [] } = await session.writeTransaction(tx => + tx.run( + 'MERGE (u:USER { email: $email }) ' + + 'SET u.password = $hash , u.verified = $verified , ' + + 'u.verificationToken = $verificationToken , u.verificationExpires = $verificationExpires, u.createdAt = $createdAt ' + + 'RETURN u', + { + email, + hash, + createdAt: new Date().toJSON(), + verified: false, + verificationToken, + verificationExpires: Date.now() + 3600000, + } + ) + ); + session.close(); + const user = records[0].get('u').properties; + return user; +}; -exports.verifyUser = ({ verificationToken }) => - new Promise((resolve, reject) => { - const session = driver.session(); - session - .writeTransaction(tx => - tx.run( - 'MATCH (u:USER { verificationToken: $verificationToken })' + - 'SET u.verified = true SET u.verificationToken = NULL RETURN u', - { - verificationToken, - } - ) - ) - .then(({ records }) => { - session.close(); - const user = records.length && records[0].get('u').properties; - return resolve(user); - }) - .catch(err => reject(err)); - }); +exports.verifyUser = async ({ verificationToken }) => { + const session = driver.session(); + const { records = [] } = await session.writeTransaction(tx => + tx.run( + 'MATCH (u:USER) ' + + 'WHERE u.verificationToken = $verificationToken AND u.verificationExpires > $currentTime ' + + 'SET u.verified = true, u.verificationToken = NULL, u.verificationExpires = NULL RETURN u', + { + verificationToken, + currentTime: Date.now(), + } + ) + ); + session.close(); + const user = records.length && records[0].get('u').properties; + return user; +}; -exports.changePassword = ({ email, password }) => - new Promise(async (resolve, reject) => { - const session = driver.session(); - const salt = await bcrypt.genSalt(12); - const hash = await bcrypt.hash(password, salt); - session - .writeTransaction(tx => - tx.run('MATCH (u:USER { email: $email }) SET u.password = $password RETURN u', { - email, - password: hash, - }) - ) - .then(res => { - session.close(); - const user = res.records.length && res.records[0].get('u').properties; - return resolve(user); - }) - .catch(err => session.close() || reject(err)); - }); +exports.changePassword = async ({ email, password }) => { + const session = driver.session(); + const salt = await bcrypt.genSalt(12); + const hash = await bcrypt.hash(password, salt); + const { records = [] } = await session.writeTransaction(tx => + tx.run('MATCH (u:USER { email: $email }) SET u.password = $password RETURN u', { + email, + password: hash, + }) + ); + session.close(); + const user = records.length && records[0].get('u').properties; + return user; +}; -exports.generateApiKey = ({ email }) => - new Promise(async (resolve, reject) => { - const session = driver.session(); - const apikey = nanoid(40); - session - .writeTransaction(tx => - tx.run('MATCH (u:USER { email: $email }) SET u.apikey = $apikey RETURN u', { - email, - apikey, - }) - ) - .then(res => { - session.close(); - const newApikey = res.records.length && res.records[0].get('u').properties.apikey; - return resolve({ apikey: newApikey }); - }) - .catch(err => session.close() || reject(err)); - }); +exports.generateApiKey = async ({ email }) => { + const session = driver.session(); + const apikey = nanoid(40); + const { records = [] } = await session.writeTransaction(tx => + tx.run('MATCH (u:USER { email: $email }) SET u.apikey = $apikey RETURN u', { + email, + apikey, + }) + ); + session.close(); + const newApikey = records.length && records[0].get('u').properties.apikey; + return { apikey: newApikey }; +}; -exports.requestPasswordReset = ({ email }) => - new Promise(async (resolve, reject) => { - const session = driver.session(); - const resetPasswordExprie = Date.now() + 3600000; - const resetPasswordToken = nanoid(40); - session - .writeTransaction(tx => - tx.run( - 'MATCH (u:USER { email: $email }) ' + - 'SET u.resetPasswordToken = $resetPasswordToken ' + - 'SET u.resetPasswordExprie = $resetPasswordExprie ' + - 'RETURN u', - { - email, - resetPasswordExprie, - resetPasswordToken, - } - ) - ) - .then(res => { - session.close(); - const user = res.records.length && res.records[0].get('u').properties; - return resolve(user); - }) - .catch(err => session.close() || reject(err)); - }); +exports.requestPasswordReset = async ({ email }) => { + const session = driver.session(); + const resetPasswordToken = nanoid(40); + const { records = [] } = await session.writeTransaction(tx => + tx.run( + 'MATCH (u:USER { email: $email }) ' + + 'SET u.resetPasswordToken = $resetPasswordToken ' + + 'SET u.resetPasswordExpires = $resetPasswordExpires ' + + 'RETURN u', + { + email, + resetPasswordExpires: Date.now() + 3600000, + resetPasswordToken, + } + ) + ); + session.close(); + const user = records.length && records[0].get('u').properties; + return user; +}; -exports.resetPassword = ({ resetPasswordToken }) => - new Promise((resolve, reject) => { - const session = driver.session(); - session - .writeTransaction(tx => - tx.run( - 'MATCH (u:USER { resetPasswordToken: $resetPasswordToken })' + - 'SET u.resetPasswordExprie = NULL SET u.resetPasswordToken = NULL RETURN u', - { - resetPasswordToken, - } - ) - ) - .then(({ records }) => { - session.close(); - const user = records.length && records[0].get('u').properties; - return resolve(user); - }) - .catch(err => reject(err)); - }); +exports.resetPassword = async ({ resetPasswordToken }) => { + const session = driver.session(); + const { records = [] } = await session.writeTransaction(tx => + tx.run( + 'MATCH (u:USER) ' + + 'WHERE u.resetPasswordToken = $resetPasswordToken AND u.resetPasswordExpires > $currentTime ' + + 'SET u.resetPasswordExpires = NULL, u.resetPasswordToken = NULL RETURN u', + { + resetPasswordToken, + currentTime: Date.now(), + } + ) + ); + session.close(); + const user = records.length && records[0].get('u').properties; + return user; +}; -exports.addCooldown = ({ email }) => - new Promise((resolve, reject) => { - const session = driver.session(); - session - .writeTransaction(tx => - tx.run( - 'MATCH (u:USER { email: $email }) ' + - 'MERGE (u)-[r:RECEIVED]->(c:COOLDOWN { date: $date }) ' + - 'RETURN COUNT(r) as count', - { - date: new Date().toJSON(), - email, - } - ) - ) - .then(({ records }) => { - session.close(); - const count = records.length && records[0].get('count').toNumber(); - return resolve({ count }); - }) - .catch(err => reject(err)); - }); +exports.addCooldown = async ({ email }) => { + const session = driver.session(); + const { records = [] } = await session.writeTransaction(tx => + tx.run( + 'MATCH (u:USER { email: $email }) ' + + 'MERGE (u)-[r:RECEIVED]->(c:COOLDOWN { date: $date }) ' + + 'RETURN COUNT(r) as count', + { + date: new Date().toJSON(), + email, + } + ) + ); + session.close(); + const count = records.length && records[0].get('count').toNumber(); + return { count }; +}; -exports.getCooldowns = ({ email }) => - new Promise((resolve, reject) => { - const session = driver.session(); - session - .writeTransaction(tx => - tx.run( - 'MATCH (u:USER { email: $email }) MATCH (u)-[r:RECEIVED]->(c) RETURN c.date as date', - { - date: new Date().toJSON(), - email, - } - ) - ) - .then(({ records = [] }) => { - session.close(); - const cooldowns = records.map(record => record.get('date')); - return resolve({ cooldowns }); - }) - .catch(err => reject(err)); - }); +exports.getCooldowns = async ({ email }) => { + const session = driver.session(); + const { records = [] } = await session.writeTransaction(tx => + tx.run('MATCH (u:USER { email: $email }) MATCH (u)-[r:RECEIVED]->(c) RETURN c.date as date', { + date: new Date().toJSON(), + email, + }) + ); + session.close(); + const cooldowns = records.map(record => record.get('date')); + return { cooldowns }; +}; -exports.banUser = ({ email }) => - new Promise((resolve, reject) => { - const session = driver.session(); - session - .writeTransaction(tx => - tx.run('MATCH (u:USER { email: $email }) SET u.banned = true RETURN u', { - email, - }) - ) - .then(({ records = [] }) => { - session.close(); - const user = records.length && records[0].get('u'); - return resolve({ user }); - }) - .catch(err => reject(err)); - }); +exports.banUser = async ({ email }) => { + const session = driver.session(); + const { records = [] } = await session.writeTransaction(tx => + tx.run('MATCH (u:USER { email: $email }) SET u.banned = true RETURN u', { + email, + }) + ); + session.close(); + const user = records.length && records[0].get('u'); + return { user }; +}; exports.addIPCooldown = async ip => { const session = driver.session(); diff --git a/server/offline/sw.js b/server/offline/sw.js deleted file mode 100644 index ed7bfec..0000000 --- a/server/offline/sw.js +++ /dev/null @@ -1,20 +0,0 @@ -// This is the "Offline copy of pages" service worker - -// eslint-disable-next-line no-restricted-globals -self.addEventListener('install', event => { - const offlinePage = new Request('/offline'); - event.waitUntil( - fetch(offlinePage).then(response => - caches.open('kutt-offline-v1').then(cache => cache.put(offlinePage, response)) - ) - ); -}); - -// eslint-disable-next-line no-restricted-globals -self.addEventListener('fetch', event => { - event.respondWith( - fetch(event.request).catch(() => - caches.open('kutt-offline-v1').then(cache => cache.match('/offline')) - ) - ); -}); diff --git a/server/server.js b/server/server.js index 9d80b11..9a112ca 100644 --- a/server/server.js +++ b/server/server.js @@ -17,6 +17,7 @@ const { } = require('./controllers/validateBodyController'); const auth = require('./controllers/authController'); const url = require('./controllers/urlController'); +const neo4j = require('./db/neo4j'); require('./cron'); require('./passport'); @@ -27,10 +28,12 @@ if (process.env.RAVEN_DSN) { const catchErrors = fn => (req, res, next) => fn(req, res, next).catch(err => { res.status(500).json({ error: 'Sorry an error ocurred. Please try again later.' }); + neo4j.close(); if (process.env.RAVEN_DSN) { Raven.captureException(err, { user: { email: req.user && req.user.email }, }); + throw new Error(err); } else { throw new Error(err); } @@ -80,12 +83,6 @@ app.prepare().then(() => { app.render(req, res, '/verify', req.user) ); - // Disabled service worker because of multiple requests - // Resulting in duplicated visist count - server.get('/sw.js', (_req, res) => { - res.sendFile(`${__dirname}/offline/sw.js`); - }); - /* User and authentication */ server.post('/api/auth/signup', validationCriterias, validateBody, catchErrors(auth.signup)); server.post('/api/auth/login', validationCriterias, validateBody, auth.authLocal, auth.login); |