Просмотр исходного кода

Merge branch 'develop' of github.com:thedevs-network/kutt into develop

Pouria Ezzati 7 лет назад
Родитель
Сommit
e57303097f

+ 14 - 11
README.md

@@ -1,8 +1,8 @@
-<a href="https://kutt.it" title="kutt.it"><img src="https://camo.githubusercontent.com/073e709d02d3cf6ee5439ee6ce0bb0895f9f3733/687474703a2f2f6f6936372e74696e797069632e636f6d2f3636797a346f2e6a7067" alt="Kutt.it"></a>
+<p align="center"><a href="https://kutt.it" title="kutt.it"><img src="https://raw.githubusercontent.com/thedevs-network/kutt/9d1c873897c3f5b9a1bd0c74dc5d23f2ed01f2ec/static/images/logo-github.png" alt="Kutt.it"></a></p>
 
 
 # Kutt.it
 # Kutt.it
 
 
-**Kutt** is a modern URL shortener which lets you set custom domains for your shortened URLs, manage your links and view the click rate statistics.
+**Kutt** is a modern URL shortener with support for custom domains. Shorten URLs, manage your links and view the click rate statistics.
 
 
 *Contributions and bug reports are welcome.*
 *Contributions and bug reports are welcome.*
 
 
@@ -22,12 +22,12 @@
 
 
 ## Key Features
 ## Key Features
 * Free and open source.
 * Free and open source.
-* Setting custom domain.
-* Using custom URLs for shortened links
+* Custom domain support.
+* Custom URLs for shortened links
 * Setting password for links.
 * Setting password for links.
 * Private statistics for shortened URLs.
 * Private statistics for shortened URLs.
 * View and manage your links.
 * View and manage your links.
-* Provided API.
+* RESTful API.
 
 
 ## Stack
 ## Stack
 * Node (Web server)
 * Node (Web server)
@@ -41,7 +41,7 @@
 * Neo4j (Graph database)
 * Neo4j (Graph database)
 
 
 ## Setup
 ## Setup
-You need to have [Node.js](https://nodejs.org/) and [Neo4j](https://neo4j.com/) installed on your system.
+You need to have [Node.js](https://nodejs.org/) and [Neo4j](https://neo4j.com/) installed on your machine.
 
 
 1. Clone this repository or [download zip](https://github.com/thedevs-network/kutt/archive/develop.zip).
 1. Clone this repository or [download zip](https://github.com/thedevs-network/kutt/archive/develop.zip).
 2. Copy `config.example.js` to `config.js` in both server and client folders and fill them properly.
 2. Copy `config.example.js` to `config.js` in both server and client folders and fill them properly.
@@ -53,7 +53,7 @@ You need to have [Node.js](https://nodejs.org/) and [Neo4j](https://neo4j.com/)
 **Docker:** You can use Docker to run the app. Read [docker-examples](/docker-examples) for more info.
 **Docker:** You can use Docker to run the app. Read [docker-examples](/docker-examples) for more info.
 
 
 ## API
 ## API
-In additional to the website, you can use these APIs to create, delete and get URLs.
+In addition to the website, you can use these APIs to create, delete and get URLs.
 
 
 ### Types
 ### Types
 
 
@@ -68,11 +68,11 @@ URL {
 }
 }
 ```
 ```
 
 
-In order to use these APIs you need to generate an API key from settings. Do not ever put this key in the client side of your app or anywhere that is exposed to others.
+In order to use these APIs you need to generate an API key from settings. Never put this key in the client side of your app or anywhere that is exposed to others.
 
 
 All API requests and responses are in JSON format.
 All API requests and responses are in JSON format.
 
 
-Include the API key as `X-API-Key` in the header of all below requests. Available API URLs with body parameters:
+Include the API key as `X-API-Key` in the header of all below requests. Available API endpoints with body parameters:
 
 
 **Get shortened URLs list:**
 **Get shortened URLs list:**
 ```
 ```
@@ -93,6 +93,9 @@ POST /api/url/submit
 ```
 ```
 Body:
 Body:
   * `target`: Original long URL to be shortened.
   * `target`: Original long URL to be shortened.
+  * `customurl` (optional): Set a custom URL.
+  * `password` (optional): Set a password.
+  * `reuse` (optional): If a URL with the specified target exists returns it, otherwise will send a new shortened URL.
 
 
 Returns: URL object
 Returns: URL object
 
 
@@ -108,6 +111,6 @@ Body (or query for GET request)
 ## Contributing
 ## Contributing
 Pull requests are welcome. You'll probably find lots of improvements to be made.
 Pull requests are welcome. You'll probably find lots of improvements to be made.
 
 
-Open issues for feedback, needed features, reporting bugs or discussing ideas.
+Open issues for feedback, requesting features, reporting bugs or discussing ideas.
 
 
-Special thanks to [Thomas](https://github.com/trgwii) and [Muthu](https://github.com/MKRhere). Logo design by [Muthu](https://github.com/MKRhere)
+Special thanks to [Thomas](https://github.com/trgwii) and [Muthu](https://github.com/MKRhere). Logo design by [Muthu](https://github.com/MKRhere).

+ 2 - 2
client/pages/report.js

@@ -19,8 +19,8 @@ const ReportPage = () => (
     <Wrapper>
     <Wrapper>
       <h3>Report abuse</h3>
       <h3>Report abuse</h3>
       <p>
       <p>
-        Report abuses, malware and phishing links to the below email address. We we will take them
-        down within 12 hours.
+        Report abuses, malware and phishing links to the below email address. We will take them down
+        within 12 hours.
       </p>
       </p>
       <p>{REPORT_EMAIL}</p>
       <p>{REPORT_EMAIL}</p>
     </Wrapper>
     </Wrapper>

+ 3 - 0
server/config.example.js

@@ -9,6 +9,9 @@ module.exports = {
   DB_USERNAME: '',
   DB_USERNAME: '',
   DB_PASSWORD: '',
   DB_PASSWORD: '',
 
 
+  /* The daily limit for each user */
+  USER_LIMIT_PER_DAY: 50,
+
   /* A passphrase to encrypt JWT. Use a long and secure key. */
   /* A passphrase to encrypt JWT. Use a long and secure key. */
   JWT_SECRET: 'securekey',
   JWT_SECRET: 'securekey',
 
 

+ 3 - 0
server/controllers/authController.js

@@ -46,6 +46,9 @@ const authenticate = (type, error, isStrict = true) =>
     return passport.authenticate(type, (err, user) => {
     return passport.authenticate(type, (err, user) => {
       if (err) return res.status(400);
       if (err) return res.status(400);
       if (!user && isStrict) return res.status(401).json({ error });
       if (!user && isStrict) return res.status(401).json({ error });
+      if (user.banned) {
+        return res.status(400).json({ error: 'Your are banned from using this website.' });
+      }
       req.user = user;
       req.user = user;
       return next();
       return next();
     })(req, res, next);
     })(req, res, next);

+ 49 - 60
server/controllers/urlController.js

@@ -1,62 +1,66 @@
 const urlRegex = require('url-regex');
 const urlRegex = require('url-regex');
 const URL = require('url');
 const URL = require('url');
+const generate = require('nanoid/generate');
 const useragent = require('useragent');
 const useragent = require('useragent');
 const geoip = require('geoip-lite');
 const geoip = require('geoip-lite');
 const bcrypt = require('bcryptjs');
 const bcrypt = require('bcryptjs');
-const axios = require('axios');
+const subDay = require('date-fns/sub_days');
 const {
 const {
   createShortUrl,
   createShortUrl,
   createVisit,
   createVisit,
+  deleteCustomDomain,
+  deleteUrl,
   findUrl,
   findUrl,
+  getCustomDomain,
   getStats,
   getStats,
   getUrls,
   getUrls,
-  getCustomDomain,
   setCustomDomain,
   setCustomDomain,
-  deleteCustomDomain,
-  deleteUrl,
+  urlCountFromDate,
 } = require('../db/url');
 } = require('../db/url');
-const config = require('../config');
 
 
-const preservedUrls = [
-  'login',
-  'logout',
-  'signup',
-  'reset-password',
-  'resetpassword',
-  'url-password',
-  'settings',
-  'stats',
-  'verify',
-  'api',
-  '404',
-  'static',
-  'images',
-];
+const { addProtocol, generateShortUrl } = require('../utils');
+const config = require('../config');
 
 
-exports.preservedUrls = preservedUrls;
+const generateId = async () => {
+  const id = generate('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', 6);
+  const urls = await findUrl({ id });
+  if (!urls.length) return id;
+  return generateId();
+};
 
 
 exports.urlShortener = async ({ body, user }, res) => {
 exports.urlShortener = async ({ body, user }, res) => {
-  if (!body.target) return res.status(400).json({ error: 'No target has been provided.' });
-  if (body.target.length > 1024) {
-    return res.status(400).json({ error: 'Maximum URL length is 1024.' });
+  // Check if user has passed daily limit
+  if (user) {
+    const { count } = await urlCountFromDate({
+      email: user.email,
+      date: subDay(new Date(), 1).toJSON(),
+    });
+    if (count > config.USER_LIMIT_PER_DAY) {
+      return res.status(429).json({
+        error: `You have reached your daily limit (${config.USER_LIMIT_PER_DAY}). Please wait 24h.`,
+      });
+    }
   }
   }
-  const isValidUrl = urlRegex({ exact: true, strict: false }).test(body.target);
-  if (!isValidUrl) return res.status(400).json({ error: 'URL is not valid.' });
-  const hasProtocol = /^https?/.test(URL.parse(body.target).protocol);
-  const target = hasProtocol ? body.target : `http://${body.target}`;
-  if (body.password && body.password.length > 64) {
-    return res.status(400).json({ error: 'Maximum password length is 64.' });
+
+  // if "reuse" is true, try to return
+  // the existent URL without creating one
+  if (user && body.reuse) {
+    const urls = await findUrl({ target: addProtocol(body.target) });
+    if (urls.length) {
+      urls.sort((a, b) => a.createdAt > b.createdAt);
+      const { domain: d, user: u, ...url } = urls[urls.length - 1];
+      const data = {
+        ...url,
+        password: !!url.password,
+        reuse: true,
+        shortUrl: generateShortUrl(url.id, user.domain),
+      };
+      return res.json(data);
+    }
   }
   }
+
+  // Check if custom URL already exists
   if (user && body.customurl) {
   if (user && body.customurl) {
-    if (!/^[a-zA-Z1-9-_]+$/g.test(body.customurl.trim())) {
-      return res.status(400).json({ error: 'Custom URL is not valid.' });
-    }
-    if (preservedUrls.some(url => url === body.customurl)) {
-      return res.status(400).json({ error: "You can't use this custom URL name." });
-    }
-    if (body.customurl.length > 64) {
-      return res.status(400).json({ error: 'Maximum custom URL length is 64.' });
-    }
     const urls = await findUrl({ id: body.customurl || '' });
     const urls = await findUrl({ id: body.customurl || '' });
     if (urls.length) {
     if (urls.length) {
       const urlWithNoDomain = !user.domain && urls.some(url => !url.domain);
       const urlWithNoDomain = !user.domain && urls.some(url => !url.domain);
@@ -66,27 +70,12 @@ exports.urlShortener = async ({ body, user }, res) => {
       }
       }
     }
     }
   }
   }
-  const isMalware = await axios.post(
-    `https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${
-      config.GOOGLE_SAFE_BROWSING_KEY
-    }`,
-    {
-      client: {
-        clientId: config.DEFAULT_DOMAIN.toLowerCase().replace('.', ''),
-        clientVersion: '1.0.0',
-      },
-      threatInfo: {
-        threatTypes: ['MALWARE', 'SOCIAL_ENGINEERING'],
-        platformTypes: ['WINDOWS'],
-        threatEntryTypes: ['URL'],
-        threatEntries: [{ url: body.target }],
-      },
-    }
-  );
-  if (isMalware.data && isMalware.data.matches) {
-    return res.status(400).json({ error: 'Malware detected!' });
-  }
-  const url = await createShortUrl({ ...body, target, user });
+
+  // Create new URL
+  const id = (user && body.customurl) || (await generateId());
+  const target = addProtocol(body.target);
+  const url = await createShortUrl({ ...body, id, target, user });
+
   return res.json(url);
   return res.json(url);
 };
 };
 
 

+ 107 - 3
server/controllers/validateBodyController.js

@@ -1,15 +1,22 @@
-const { body } = require('express-validator/check');
+const axios = require('axios');
+const urlRegex = require('url-regex');
+const validator = require('express-validator/check');
+const { subHours } = require('date-fns/');
 const { validationResult } = require('express-validator/check');
 const { validationResult } = require('express-validator/check');
+const { addCooldown, banUser, getCooldowns } = require('../db/user');
+const config = require('../config');
 
 
 exports.validationCriterias = [
 exports.validationCriterias = [
-  body('email')
+  validator
+    .body('email')
     .exists()
     .exists()
     .withMessage('Email must be provided.')
     .withMessage('Email must be provided.')
     .isEmail()
     .isEmail()
     .withMessage('Email is not valid.')
     .withMessage('Email is not valid.')
     .trim()
     .trim()
     .normalizeEmail(),
     .normalizeEmail(),
-  body('password', 'Password must be at least 8 chars long.')
+  validator
+    .body('password', 'Password must be at least 8 chars long.')
     .exists()
     .exists()
     .withMessage('Password must be provided.')
     .withMessage('Password must be provided.')
     .isLength({ min: 8 }),
     .isLength({ min: 8 }),
@@ -25,3 +32,100 @@ exports.validateBody = (req, res, next) => {
   }
   }
   return next();
   return next();
 };
 };
+
+const preservedUrls = [
+  'login',
+  'logout',
+  'signup',
+  'reset-password',
+  'resetpassword',
+  'url-password',
+  'settings',
+  'stats',
+  'verify',
+  'api',
+  '404',
+  'static',
+  'images',
+];
+
+exports.preservedUrls = preservedUrls;
+
+exports.validateUrl = async ({ body, user }, res, next) => {
+  // Validate URL existence
+  if (!body.target) return res.status(400).json({ error: 'No target has been provided.' });
+
+  // validate URL length
+  if (body.target.length > 1024) {
+    return res.status(400).json({ error: 'Maximum URL length is 1024.' });
+  }
+
+  // Validate URL
+  const isValidUrl = urlRegex({ exact: true, strict: false }).test(body.target);
+  if (!isValidUrl) return res.status(400).json({ error: 'URL is not valid.' });
+
+  // Validate password length
+  if (body.password && body.password.length > 64) {
+    return res.status(400).json({ error: 'Maximum password length is 64.' });
+  }
+
+  // Custom URL validations
+  if (user && body.customurl) {
+    // Validate custom URL
+    if (!/^[a-zA-Z1-9-_]+$/g.test(body.customurl.trim())) {
+      return res.status(400).json({ error: 'Custom URL is not valid.' });
+    }
+
+    // Prevent from using preserved URLs
+    if (preservedUrls.some(url => url === body.customurl)) {
+      return res.status(400).json({ error: "You can't use this custom URL name." });
+    }
+
+    // Validate custom URL length
+    if (body.customurl.length > 64) {
+      return res.status(400).json({ error: 'Maximum custom URL length is 64.' });
+    }
+  }
+
+  return next();
+};
+
+exports.cooldownCheck = async ({ user }, res, next) => {
+  if (user) {
+    const { cooldowns } = await getCooldowns(user);
+    if (cooldowns.length > 4) {
+      await banUser(user);
+      return res.status(400).json({ error: 'Too much malware requests. You are now banned.' });
+    }
+    const hasCooldownNow = cooldowns.some(cooldown => cooldown > subHours(new Date(), 12).toJSON());
+    if (hasCooldownNow) {
+      return res.status(400).json({ error: 'Cooldown because of a malware URL. Wait 12h' });
+    }
+  }
+  return next();
+};
+
+exports.malwareCheck = async ({ body, user }, res, next) => {
+  const isMalware = await axios.post(
+    `https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${
+      config.GOOGLE_SAFE_BROWSING_KEY
+    }`,
+    {
+      client: {
+        clientId: config.DEFAULT_DOMAIN.toLowerCase().replace('.', ''),
+        clientVersion: '1.0.0',
+      },
+      threatInfo: {
+        threatTypes: ['MALWARE', 'SOCIAL_ENGINEERING'],
+        platformTypes: ['WINDOWS'],
+        threatEntryTypes: ['URL'],
+        threatEntries: [{ url: body.target }],
+      },
+    }
+  );
+  if (isMalware.data && isMalware.data.matches) {
+    await addCooldown(user);
+    return res.status(400).json({ error: 'Malware detected! Cooldown for 12h.' });
+  }
+  return next();
+};

+ 38 - 18
server/db/url.js

@@ -1,4 +1,3 @@
-const generate = require('nanoid/generate');
 const bcrypt = require('bcryptjs');
 const bcrypt = require('bcryptjs');
 const _ = require('lodash/');
 const _ = require('lodash/');
 const {
 const {
@@ -12,15 +11,13 @@ const {
 } = require('date-fns');
 } = require('date-fns');
 const driver = require('./neo4j');
 const driver = require('./neo4j');
 const config = require('../config');
 const config = require('../config');
+const { generateShortUrl } = require('../utils');
 
 
 const getUTCDate = (dateString = Date.now()) => {
 const getUTCDate = (dateString = Date.now()) => {
   const date = new Date(dateString);
   const date = new Date(dateString);
   return new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours());
   return new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours());
 };
 };
 
 
-const generateId = () =>
-  generate('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', 6);
-
 const queryNewUrl = 'CREATE (l:URL { id: $id, target: $target, createdAt: $createdAt }) RETURN l';
 const queryNewUrl = 'CREATE (l:URL { id: $id, target: $target, createdAt: $createdAt }) RETURN l';
 
 
 const queryNewUserUrl = (domain, password) =>
 const queryNewUserUrl = (domain, password) =>
@@ -43,7 +40,7 @@ exports.createShortUrl = params =>
           createdAt: new Date().toJSON(),
           createdAt: new Date().toJSON(),
           domain: params.user && params.user.domain,
           domain: params.user && params.user.domain,
           email: params.user && params.user.email,
           email: params.user && params.user.email,
-          id: (params.user && params.customurl) || generateId(),
+          id: params.id,
           password: hash || '',
           password: hash || '',
           target: params.target,
           target: params.target,
         })
         })
@@ -54,11 +51,11 @@ exports.createShortUrl = params =>
         resolve({
         resolve({
           ...data,
           ...data,
           password: !!data.password,
           password: !!data.password,
-          shortUrl: `http${!params.user.domain ? 's' : ''}://${params.user.domain ||
-            config.DEFAULT_DOMAIN}/${data.id}`,
+          reuse: !!params.reuse,
+          shortUrl: generateShortUrl(data.id, params.user.domain),
         });
         });
       })
       })
-      .catch(reject);
+      .catch(() => session.close() && reject);
   });
   });
 
 
 exports.createVisit = params =>
 exports.createVisit = params =>
@@ -98,16 +95,16 @@ exports.createVisit = params =>
         const url = records.length && records[0].get('l').properties;
         const url = records.length && records[0].get('l').properties;
         resolve(url);
         resolve(url);
       })
       })
-      .catch(reject);
+      .catch(() => session.close() && reject);
   });
   });
 
 
-exports.findUrl = ({ id, domain }) =>
+exports.findUrl = ({ id, domain, target }) =>
   new Promise((resolve, reject) => {
   new Promise((resolve, reject) => {
     const session = driver.session();
     const session = driver.session();
     session
     session
       .readTransaction(tx =>
       .readTransaction(tx =>
         tx.run(
         tx.run(
-          'MATCH (l:URL { id: $id })' +
+          `MATCH (l:URL { ${id ? 'id: $id' : 'target: $target'} })` +
             `${
             `${
               domain
               domain
                 ? 'MATCH (l)-[:USES]->(d:DOMAIN { name: $domain })'
                 ? 'MATCH (l)-[:USES]->(d:DOMAIN { name: $domain })'
@@ -118,6 +115,7 @@ exports.findUrl = ({ id, domain }) =>
           {
           {
             id,
             id,
             domain,
             domain,
+            target,
           }
           }
         )
         )
       )
       )
@@ -132,7 +130,7 @@ exports.findUrl = ({ id, domain }) =>
           }));
           }));
         resolve(url);
         resolve(url);
       })
       })
-      .catch(reject);
+      .catch(() => session.close() && reject);
   });
   });
 
 
 exports.getUrls = ({ user, options }) =>
 exports.getUrls = ({ user, options }) =>
@@ -167,7 +165,7 @@ exports.getUrls = ({ user, options }) =>
         }));
         }));
         resolve({ list: urls, countAll });
         resolve({ list: urls, countAll });
       })
       })
-      .catch(reject);
+      .catch(() => session.close() && reject);
   });
   });
 
 
 exports.getCustomDomain = ({ customDomain }) =>
 exports.getCustomDomain = ({ customDomain }) =>
@@ -184,7 +182,7 @@ exports.getCustomDomain = ({ customDomain }) =>
         const data = records.length && records[0].get('u').properties;
         const data = records.length && records[0].get('u').properties;
         resolve(data);
         resolve(data);
       })
       })
-      .catch(reject);
+      .catch(() => session.close() && reject);
   });
   });
 
 
 exports.setCustomDomain = ({ user, customDomain }) =>
 exports.setCustomDomain = ({ user, customDomain }) =>
@@ -208,7 +206,7 @@ exports.setCustomDomain = ({ user, customDomain }) =>
         const data = records.length && records[0].get('d').properties;
         const data = records.length && records[0].get('d').properties;
         resolve(data);
         resolve(data);
       })
       })
-      .catch(reject);
+      .catch(() => session.close() && reject);
   });
   });
 
 
 exports.deleteCustomDomain = ({ user }) =>
 exports.deleteCustomDomain = ({ user }) =>
@@ -225,7 +223,7 @@ exports.deleteCustomDomain = ({ user }) =>
         const data = records.length && records[0].get('u').properties;
         const data = records.length && records[0].get('u').properties;
         resolve(data);
         resolve(data);
       })
       })
-      .catch(reject);
+      .catch(() => session.close() && reject);
   });
   });
 
 
 exports.deleteUrl = ({ id, domain, user }) =>
 exports.deleteUrl = ({ id, domain, user }) =>
@@ -255,7 +253,7 @@ exports.deleteUrl = ({ id, domain, user }) =>
         const data = records.length && records[0].get('u').properties;
         const data = records.length && records[0].get('u').properties;
         resolve(data);
         resolve(data);
       })
       })
-      .catch(reject);
+      .catch(() => session.close() && reject);
   });
   });
 
 
 /* Collecting stats */
 /* Collecting stats */
@@ -393,5 +391,27 @@ exports.getStats = ({ id, domain, user }) =>
 
 
         return resolve(response);
         return resolve(response);
       })
       })
-      .catch(reject);
+      .catch(() => session.close() && reject);
+  });
+
+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 ' +
+            'RETURN COUNT(l) as count',
+          {
+            date,
+            email,
+          }
+        )
+      )
+      .then(({ records }) => {
+        session.close();
+        const count = records.length && records[0].get('count').toNumber();
+        return resolve({ count });
+      })
+      .catch(err => reject(err));
   });
   });

+ 64 - 3
server/db/user.js

@@ -94,7 +94,7 @@ exports.changePassword = ({ email, password }) =>
         const user = res.records.length && res.records[0].get('u').properties;
         const user = res.records.length && res.records[0].get('u').properties;
         return resolve(user);
         return resolve(user);
       })
       })
-      .catch(reject);
+      .catch(() => session.close() && reject);
   });
   });
 
 
 exports.generateApiKey = ({ email }) =>
 exports.generateApiKey = ({ email }) =>
@@ -113,7 +113,7 @@ exports.generateApiKey = ({ email }) =>
         const newApikey = res.records.length && res.records[0].get('u').properties.apikey;
         const newApikey = res.records.length && res.records[0].get('u').properties.apikey;
         return resolve({ apikey: newApikey });
         return resolve({ apikey: newApikey });
       })
       })
-      .catch(reject);
+      .catch(() => session.close() && reject);
   });
   });
 
 
 exports.requestPasswordReset = ({ email }) =>
 exports.requestPasswordReset = ({ email }) =>
@@ -140,7 +140,7 @@ exports.requestPasswordReset = ({ email }) =>
         const user = res.records.length && res.records[0].get('u').properties;
         const user = res.records.length && res.records[0].get('u').properties;
         return resolve(user);
         return resolve(user);
       })
       })
-      .catch(reject);
+      .catch(() => session.close() && reject);
   });
   });
 
 
 exports.resetPassword = ({ resetPasswordToken }) =>
 exports.resetPassword = ({ resetPasswordToken }) =>
@@ -163,3 +163,64 @@ exports.resetPassword = ({ resetPasswordToken }) =>
       })
       })
       .catch(err => reject(err));
       .catch(err => reject(err));
   });
   });
+
+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.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.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));
+  });

+ 12 - 2
server/server.js

@@ -6,7 +6,14 @@ const Raven = require('raven');
 const cookieParser = require('cookie-parser');
 const cookieParser = require('cookie-parser');
 const bodyParser = require('body-parser');
 const bodyParser = require('body-parser');
 const passport = require('passport');
 const passport = require('passport');
-const { validateBody, validationCriterias } = require('./controllers/validateBodyController');
+const {
+  validateBody,
+  validationCriterias,
+  preservedUrls,
+  validateUrl,
+  cooldownCheck,
+  malwareCheck,
+} = require('./controllers/validateBodyController');
 const auth = require('./controllers/authController');
 const auth = require('./controllers/authController');
 const url = require('./controllers/urlController');
 const url = require('./controllers/urlController');
 const config = require('./config');
 const config = require('./config');
@@ -56,7 +63,7 @@ app.prepare().then(() => {
     const { headers, path } = req;
     const { headers, path } = req;
     if (
     if (
       headers.host !== config.DEFAULT_DOMAIN &&
       headers.host !== config.DEFAULT_DOMAIN &&
-      (path === '/' || url.preservedUrls.some(item => item === path.replace('/', '')))
+      (path === '/' || preservedUrls.some(item => item === path.replace('/', '')))
     ) {
     ) {
       return res.redirect(`http://${config.DEFAULT_DOMAIN + path}`);
       return res.redirect(`http://${config.DEFAULT_DOMAIN + path}`);
     }
     }
@@ -93,6 +100,9 @@ app.prepare().then(() => {
     auth.authApikey,
     auth.authApikey,
     auth.authJwtLoose,
     auth.authJwtLoose,
     catchErrors(auth.recaptcha),
     catchErrors(auth.recaptcha),
+    catchErrors(validateUrl),
+    catchErrors(cooldownCheck),
+    catchErrors(malwareCheck),
     catchErrors(url.urlShortener)
     catchErrors(url.urlShortener)
   );
   );
   server.post('/api/url/deleteurl', auth.authApikey, auth.authJwt, catchErrors(url.deleteUrl));
   server.post('/api/url/deleteurl', auth.authApikey, auth.authJwt, catchErrors(url.deleteUrl));

+ 10 - 0
server/utils/index.js

@@ -0,0 +1,10 @@
+const URL = require('url');
+const config = require('../config');
+
+exports.addProtocol = url => {
+  const hasProtocol = /^https?/.test(URL.parse(url).protocol);
+  return hasProtocol ? url : `http://${url}`;
+};
+
+exports.generateShortUrl = (id, domain) =>
+  `http${!domain ? 's' : ''}://${domain || config.DEFAULT_DOMAIN}/${id}`;