Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/thedevs-network/kutt.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpoeti8 <ezzati.upt@gmail.com>2019-07-06 09:07:39 +0300
committerpoeti8 <ezzati.upt@gmail.com>2019-07-06 09:07:39 +0300
commit5f434c59f29ad676c078c8d37ec9f266d3432638 (patch)
treef17c0468be95159c96ae81e4d0e01f2fd3aff163
parentbacb93b24a14b38cc7a3c2923b37232c293ec128 (diff)
parent1de09a388a520aa1502e71201c80c7f695ffb19d (diff)
Merge branch 'develop' into feature/pricing-tablefeature/pricing-table
-rw-r--r--Dockerfile47
-rw-r--r--README.md1
-rw-r--r--client/actions/__test__/url.js2
-rw-r--r--client/store/index.js15
-rw-r--r--docker-compose.yml27
-rw-r--r--docker-examples/README.md63
-rw-r--r--docker-examples/server-config.example.js36
-rw-r--r--package.json4
-rwxr-xr-xrun.sh94
-rw-r--r--server/controllers/urlController.js2
-rw-r--r--server/db/url.js510
-rw-r--r--server/db/user.js385
-rw-r--r--server/offline/sw.js20
-rw-r--r--server/server.js9
14 files changed, 452 insertions, 763 deletions
diff --git a/Dockerfile b/Dockerfile
index 143901d..94f2f98 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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
diff --git a/README.md b/README.md
index 964e27e..e7a72e0 100644
--- a/README.md
+++ b/README.md
@@ -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",
diff --git a/run.sh b/run.sh
deleted file mode 100755
index 9e9d1f9..0000000
--- a/run.sh
+++ /dev/null
@@ -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);