Преглед изворни кода

Removed reCAPTCHA and added limiter

Pouria Ezzati пре 8 година
родитељ
комит
5da62905bf

+ 2 - 26
client/components/Login/Login.js

@@ -11,8 +11,6 @@ import TextInput from '../TextInput';
 import Button from '../Button';
 import Error from '../Error';
 import { loginUser, showAuthError, signupUser, showPageLoading } from '../../actions';
-import showRecaptcha from '../../helpers/recaptcha';
-import config from '../../config';
 
 const Wrapper = styled.div`
   flex: 0 0 auto;
@@ -61,11 +59,6 @@ const ForgetPassLink = styled.a`
   }
 `;
 
-const Recaptcha = styled.div`
-  display: block;
-  margin: 0 0 32px 0;
-`;
-
 class Login extends Component {
   constructor() {
     super();
@@ -75,10 +68,6 @@ class Login extends Component {
     this.goTo = this.goTo.bind(this);
   }
 
-  componentDidMount() {
-    showRecaptcha();
-  }
-
   goTo(e) {
     e.preventDefault();
     const path = e.currentTarget.getAttribute('href');
@@ -92,20 +81,14 @@ class Login extends Component {
     const form = document.getElementById('login-form');
     const { value: email } = form.elements.email;
     const { value: password } = form.elements.password;
-    const { value: reCaptchaToken } = form.elements['g-recaptcha-input'];
     if (!email) return showError('Email address must not be empty.');
     if (!emailValidator.validate(email)) return showError('Email address is not valid.');
     if (password.trim().length < 8) {
       return showError('Password must be at least 8 chars long.');
     }
-    if (!reCaptchaToken) {
-      window.grecaptcha.reset();
-      return showError('reCAPTCHA is not valid. Try again.');
-    }
-    window.grecaptcha.reset();
     return type === 'login'
-      ? this.props.login({ email, password, reCaptchaToken })
-      : this.props.signup({ email, password, reCaptchaToken });
+      ? this.props.login({ email, password })
+      : this.props.signup({ email, password });
   }
 
   loginHandler(e) {
@@ -133,13 +116,6 @@ class Login extends Component {
             <TextInput type="email" name="email" id="email" autoFocus />
             <LoginInputLabel htmlFor="password">Password (min chars: 8)</LoginInputLabel>
             <TextInput type="password" name="password" id="password" />
-            <Recaptcha
-              id="g-recaptcha"
-              className="g-recaptcha"
-              data-sitekey={config.RECAPTCHA_SITE_KEY}
-              data-callback="recaptchaCallback"
-            />
-            <input type="hidden" id="g-recaptcha-input" name="g-recaptcha-input" />
             <ForgetPassLink href="/reset-password" title="Forget password" onClick={this.goTo}>
               Forgot your password?
             </ForgetPassLink>

+ 1 - 15
client/components/Shortener/Shortener.js

@@ -6,9 +6,7 @@ import styled from 'styled-components';
 import ShortenerResult from './ShortenerResult';
 import ShortenerTitle from './ShortenerTitle';
 import ShortenerInput from './ShortenerInput';
-import ShortenerCaptcha from './ShortenerCaptcha';
 import { createShortUrl, setShortenerFormError } from '../../actions';
-import showRecaptcha from '../../helpers/recaptcha';
 import { fadeIn } from '../../helpers/animations';
 
 const Wrapper = styled.div`
@@ -50,10 +48,6 @@ class Shortener extends Component {
     this.copyHandler = this.copyHandler.bind(this);
   }
 
-  componentDidMount() {
-    showRecaptcha();
-  }
-
   shouldComponentUpdate(nextProps, nextState) {
     const { isAuthenticated, shortenerError, shortenerLoading, url: { isShortened } } = this.props;
     return (
@@ -73,20 +67,13 @@ class Shortener extends Component {
       target: originalUrl,
       customurl: customurlInput,
       password: pwd,
-      'g-recaptcha-input': recaptcha,
     } = shortenerForm.elements;
     const target = originalUrl.value.trim();
     const customurl = customurlInput && customurlInput.value.trim();
     const password = pwd && pwd.value;
-    const reCaptchaToken = !isAuthenticated && recaptcha && recaptcha.value;
-    if (!isAuthenticated && !reCaptchaToken) {
-      window.grecaptcha.reset();
-      return this.props.setShortenerFormError('reCAPTCHA is not valid. Try again.');
-    }
     const options = isAuthenticated && { customurl, password };
     shortenerForm.reset();
-    if (!isAuthenticated && recaptcha) window.grecaptcha.reset();
-    return this.props.createShortUrl({ target, reCaptchaToken, ...options });
+    return this.props.createShortUrl({ target, ...options });
   }
 
   copyHandler() {
@@ -118,7 +105,6 @@ class Shortener extends Component {
           handleSubmit={this.handleSubmit}
           setShortenerFormError={this.props.setShortenerFormError}
         />
-        {!isAuthenticated && <ShortenerCaptcha />}
       </Wrapper>
     );
   }

+ 0 - 19
client/components/Shortener/ShortenerCaptcha.js

@@ -1,19 +0,0 @@
-import React from 'react';
-import styled from 'styled-components';
-import config from '../../config';
-
-const Recaptcha = styled.div`
-  display: block;
-  margin: 32px 0;
-`;
-
-const ShortenerCaptcha = () => (
-  <Recaptcha
-    id="g-recaptcha"
-    className="g-recaptcha"
-    data-sitekey={config.RECAPTCHA_SITE_KEY}
-    data-callback="recaptchaCallback"
-  />
-);
-
-export default ShortenerCaptcha;

+ 0 - 1
client/components/Shortener/ShortenerInput.js

@@ -58,7 +58,6 @@ const ShortenerInput = ({ isAuthenticated, handleSubmit, setShortenerFormError }
     <Submit onClick={handleSubmit}>
       <Icon src="/images/send.svg" />
     </Submit>
-    <input type="hidden" id="g-recaptcha-input" name="g-recaptcha-input" />
     <Error type="shortener" />
     <ShortenerOptions
       isAuthenticated={isAuthenticated}

+ 1 - 15
client/components/Shortener/ShortenerOptions.js

@@ -1,6 +1,6 @@
 import React, { Component } from 'react';
 import PropTypes from 'prop-types';
-import styled, { css } from 'styled-components';
+import styled from 'styled-components';
 import Checkbox from '../Checkbox';
 import TextInput from '../TextInput';
 import { fadeIn } from '../../helpers/animations';
@@ -15,23 +15,9 @@ const Wrapper = styled.div`
   justify-content: flex-start;
   z-index: 2;
 
-  ${({ isAuthenticated }) =>
-    !isAuthenticated &&
-    css`
-      top: 180px;
-    `};
-
   @media only screen and (max-width: 448px) {
     top: 56px;
   }
-
-  ${({ isAuthenticated }) =>
-    !isAuthenticated &&
-    css`
-      @media only screen and (max-width: 448px) {
-        top: 156px;
-      }
-    `};
 `;
 
 const CheckboxWrapper = styled.div`

+ 0 - 6
client/config.example.js

@@ -1,10 +1,4 @@
 module.exports = {
-  /*
-    reCaptcha site key
-    Create one in https://www.google.com/recaptcha/intro/
-  */
-  RECAPTCHA_SITE_KEY: '',
-
   // Google analytics tracking ID
   GOOGLE_ANALYTICS_ID: '6Lc4TUAUAAAAAMRHnlEEt21UkPlOXKCXHaIapdTT',
 };

+ 0 - 17
client/helpers/recaptcha.js

@@ -1,17 +0,0 @@
-export default function showRecaptcha() {
-  const captcha = document.getElementById('g-recaptcha');
-  if (!captcha) return null;
-  window.recaptchaCallback = response => {
-    const captchaInput = document.getElementById('g-recaptcha-input');
-    captchaInput.value = response;
-  };
-  if (!window.grecaptcha) {
-    return setTimeout(() => showRecaptcha(captcha), 200);
-  }
-  return setTimeout(() => {
-    if ((window, captcha, !captcha.childNodes.length)) {
-      window.grecaptcha.render(captcha);
-    }
-    return null;
-  }, 1000);
-}

+ 0 - 1
client/pages/_document.js

@@ -52,7 +52,6 @@ class AppDocument extends Document {
 
           {this.props.styleTags}
 
-          <script src="https://www.google.com/recaptcha/api.js?render=explicit" />
           <script src="/analytics.js" />
         </Head>
         <body style={style}>

+ 0 - 6
server/config.example.js

@@ -12,12 +12,6 @@ module.exports = {
   /* A passphrase to encrypt JWT. Use a long and secure key. */
   JWT_SECRET: 'securekey',
 
-  /*
-    reCaptcha secret key
-    Create one in https://www.google.com/recaptcha/intro/
-  */
-  RECAPTCHA_SECRET_KEY: '',
-
   /*
     Your email host details to use to send verification emails.
     More info on http://nodemailer.com/

+ 0 - 23
server/controllers/authController.js

@@ -2,7 +2,6 @@ const fs = require('fs');
 const path = require('path');
 const passport = require('passport');
 const JWT = require('jsonwebtoken');
-const axios = require('axios');
 const config = require('../config');
 const transporter = require('../mail/mail');
 const { resetMailText, verifyMailText } = require('../mail/text');
@@ -55,28 +54,6 @@ exports.authJwt = authenticate('jwt', 'Unauthorized.');
 exports.authJwtLoose = authenticate('jwt', 'Unauthorized.', false);
 exports.authApikey = authenticate('localapikey', 'API key is not correct.', false);
 
-/* reCaptcha controller */
-exports.recaptcha = async (req, res, next) => {
-  if (!req.user) {
-    const isReCaptchaValid = await axios({
-      method: 'post',
-      url: 'https://www.google.com/recaptcha/api/siteverify',
-      headers: {
-        'Content-type': 'application/x-www-form-urlencoded',
-      },
-      params: {
-        secret: config.RECAPTCHA_SECRET_KEY,
-        response: req.body.reCaptchaToken,
-        remoteip: req.realIp,
-      },
-    });
-    if (!isReCaptchaValid.data.success) {
-      return res.status(401).json({ error: 'reCAPTCHA is not valid. Try again.' });
-    }
-  }
-  return next();
-};
-
 exports.signup = async (req, res) => {
   const { email, password } = req.body;
   if (password.length > 64) {

+ 9 - 2
server/controllers/urlController.js

@@ -9,6 +9,7 @@ const {
   findUrl,
   getStats,
   getUrls,
+  getUrlsWithIp,
   getCustomDomain,
   setCustomDomain,
   deleteCustomDomain,
@@ -34,7 +35,7 @@ const preservedUrls = [
 
 exports.preservedUrls = preservedUrls;
 
-exports.urlShortener = async ({ body, user }, res) => {
+exports.urlShortener = async ({ body, ip, 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.' });
@@ -45,6 +46,12 @@ exports.urlShortener = async ({ body, user }, res) => {
   if (body.password && body.password.length > 64) {
     return res.status(400).json({ error: 'Maximum password length is 64.' });
   }
+  const urlsWithIp = await getUrlsWithIp({ ip });
+  if (urlsWithIp && urlsWithIp.length > 200) {
+    return res
+      .status(400)
+      .json({ error: 'Too much requests! You can request again after one day.' });
+  }
   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.' });
@@ -64,7 +71,7 @@ exports.urlShortener = async ({ body, user }, res) => {
       }
     }
   }
-  const url = await createShortUrl({ ...body, target, user });
+  const url = await createShortUrl({ ...body, ip, target, user });
   return res.json(url);
 };
 

+ 29 - 1
server/db/url.js

@@ -21,7 +21,10 @@ const getUTCDate = (dateString = Date.now()) => {
 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 }) ' +
+  'MERGE (i:IP { ip: $ip })' +
+  'CREATE (l)-[:WITH_IP]->(i) RETURN l';
 
 const queryNewUserUrl = (domain, password) =>
   'MATCH (u:USER { email: $email })' +
@@ -29,6 +32,8 @@ const queryNewUserUrl = (domain, password) =>
   `${password ? ', password: $password' : ''} })` +
   'CREATE (u)-[:CREATED]->(l)' +
   `${domain ? 'MERGE (l)-[:USES]->(:DOMAIN { name: $domain })' : ''}` +
+  'MERGE (i:IP { ip: $ip })' +
+  'CREATE (l)-[:WITH_IP]->(i)' +
   'RETURN l';
 
 exports.createShortUrl = params =>
@@ -44,6 +49,7 @@ exports.createShortUrl = params =>
           domain: params.user && params.user.domain,
           email: params.user && params.user.email,
           id: (params.user && params.customurl) || generateId(),
+          ip: params.ip,
           password: hash || '',
           target: params.target,
         })
@@ -170,6 +176,28 @@ exports.getUrls = ({ user, options }) =>
       .catch(reject);
   });
 
+exports.getUrlsWithIp = ({ ip }) =>
+  new Promise((resolve, reject) => {
+    const session = driver.session();
+    session
+      .readTransaction(tx =>
+        tx.run('MATCH (i:IP { ip: $ip })<-[:WITH_IP]-(l:URL) RETURN l', {
+          ip,
+        })
+      )
+      .then(({ records }) => {
+        session.close();
+        const now = new Date();
+        const urls =
+          records.length &&
+          records
+            .map(record => record.get('l').properties.createdAt)
+            .filter(item => isAfter(item, subDays(now, 7)));
+        resolve(urls);
+      })
+      .catch(reject);
+  });
+
 exports.getCustomDomain = ({ customDomain }) =>
   new Promise((resolve, reject) => {
     const session = driver.session();

+ 3 - 22
server/server.js

@@ -89,21 +89,8 @@ app.prepare().then(() => {
   );
 
   /* User and authentication */
-  server.post(
-    '/api/auth/signup',
-    validationCriterias,
-    validateBody,
-    catchErrors(auth.recaptcha),
-    catchErrors(auth.signup)
-  );
-  server.post(
-    '/api/auth/login',
-    validationCriterias,
-    validateBody,
-    catchErrors(auth.recaptcha),
-    auth.authLocal,
-    auth.login
-  );
+  server.post('/api/auth/signup', validationCriterias, validateBody, catchErrors(auth.signup));
+  server.post('/api/auth/login', validationCriterias, validateBody, auth.authLocal, auth.login);
   server.post('/api/auth/renew', auth.authJwt, auth.renew);
   server.post('/api/auth/changepassword', auth.authJwt, catchErrors(auth.changePassword));
   server.post('/api/auth/generateapikey', auth.authJwt, catchErrors(auth.generateApiKey));
@@ -111,13 +98,7 @@ app.prepare().then(() => {
   server.post('/api/auth/usersettings', auth.authJwt, auth.userSettings);
 
   /* URL shortener */
-  server.post(
-    '/api/url/submit',
-    auth.authApikey,
-    auth.authJwtLoose,
-    catchErrors(auth.recaptcha),
-    catchErrors(url.urlShortener)
-  );
+  server.post('/api/url/submit', auth.authApikey, auth.authJwtLoose, catchErrors(url.urlShortener));
   server.post('/api/url/deleteurl', auth.authApikey, auth.authJwt, catchErrors(url.deleteUrl));
   server.post('/api/url/geturls', auth.authApikey, auth.authJwt, catchErrors(url.getUrls));
   server.post('/api/url/customdomain', auth.authJwt, catchErrors(url.setCustomDomain));