Kaynağa Gözat

reverted removing reCAPTCHA

Pouria Ezzati 8 yıl önce
ebeveyn
işleme
2871262cd2

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

@@ -11,6 +11,8 @@ 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;
@@ -59,6 +61,11 @@ const ForgetPassLink = styled.a`
   }
 `;
 
+const Recaptcha = styled.div`
+  display: block;
+  margin: 0 0 32px 0;
+`;
+
 class Login extends Component {
   constructor() {
     super();
@@ -68,6 +75,10 @@ class Login extends Component {
     this.goTo = this.goTo.bind(this);
   }
 
+  componentDidMount() {
+    showRecaptcha();
+  }
+
   goTo(e) {
     e.preventDefault();
     const path = e.currentTarget.getAttribute('href');
@@ -81,14 +92,20 @@ 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 })
-      : this.props.signup({ email, password });
+      ? this.props.login({ email, password, reCaptchaToken })
+      : this.props.signup({ email, password, reCaptchaToken });
   }
 
   loginHandler(e) {
@@ -116,6 +133,13 @@ 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>

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

@@ -6,7 +6,9 @@ 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`
@@ -48,6 +50,10 @@ class Shortener extends Component {
     this.copyHandler = this.copyHandler.bind(this);
   }
 
+  componentDidMount() {
+    showRecaptcha();
+  }
+
   shouldComponentUpdate(nextProps, nextState) {
     const { isAuthenticated, shortenerError, shortenerLoading, url: { isShortened } } = this.props;
     return (
@@ -67,13 +73,20 @@ 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();
-    return this.props.createShortUrl({ target, ...options });
+    if (!isAuthenticated && recaptcha) window.grecaptcha.reset();
+    return this.props.createShortUrl({ target, reCaptchaToken, ...options });
   }
 
   copyHandler() {
@@ -105,6 +118,7 @@ class Shortener extends Component {
           handleSubmit={this.handleSubmit}
           setShortenerFormError={this.props.setShortenerFormError}
         />
+        {!isAuthenticated && <ShortenerCaptcha />}
       </Wrapper>
     );
   }

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

@@ -0,0 +1,19 @@
+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;

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

@@ -58,6 +58,7 @@ 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}

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

@@ -1,6 +1,6 @@
 import React, { Component } from 'react';
 import PropTypes from 'prop-types';
-import styled from 'styled-components';
+import styled, { css } from 'styled-components';
 import Checkbox from '../Checkbox';
 import TextInput from '../TextInput';
 import { fadeIn } from '../../helpers/animations';
@@ -15,9 +15,23 @@ 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`

+ 6 - 0
client/config.example.js

@@ -1,4 +1,10 @@
 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',
 };

+ 17 - 0
client/helpers/recaptcha.js

@@ -0,0 +1,17 @@
+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);
+}

+ 1 - 0
client/pages/_document.js

@@ -52,6 +52,7 @@ 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}>

+ 6 - 0
server/config.example.js

@@ -12,6 +12,12 @@ 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/

+ 23 - 0
server/controllers/authController.js

@@ -2,6 +2,7 @@ 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');
@@ -54,6 +55,28 @@ 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) {

+ 2 - 9
server/controllers/urlController.js

@@ -9,7 +9,6 @@ const {
   findUrl,
   getStats,
   getUrls,
-  // getUrlsWithIp,
   getCustomDomain,
   setCustomDomain,
   deleteCustomDomain,
@@ -35,7 +34,7 @@ const preservedUrls = [
 
 exports.preservedUrls = preservedUrls;
 
-exports.urlShortener = async ({ body, ip, 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.' });
@@ -46,12 +45,6 @@ exports.urlShortener = async ({ body, ip, 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.' });
@@ -71,7 +64,7 @@ exports.urlShortener = async ({ body, ip, user }, res) => {
       }
     }
   }
-  const url = await createShortUrl({ ...body, ip, target, user });
+  const url = await createShortUrl({ ...body, target, user });
   return res.json(url);
 };
 

+ 1 - 29
server/db/url.js

@@ -21,10 +21,7 @@ const getUTCDate = (dateString = Date.now()) => {
 const generateId = () =>
   generate('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', 6);
 
-const queryNewUrl =
-  'CREATE (l:URL { id: $id, target: $target, createdAt: $createdAt }) ' +
-  'MERGE (i:IP { ip: $ip })' +
-  'CREATE (l)-[:WITH_IP]->(i) RETURN l';
+const queryNewUrl = 'CREATE (l:URL { id: $id, target: $target, createdAt: $createdAt }) RETURN l';
 
 const queryNewUserUrl = (domain, password) =>
   'MATCH (u:USER { email: $email })' +
@@ -32,8 +29,6 @@ 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 =>
@@ -49,7 +44,6 @@ 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,
         })
@@ -176,28 +170,6 @@ 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();

+ 22 - 3
server/server.js

@@ -89,8 +89,21 @@ app.prepare().then(() => {
   );
 
   /* User and authentication */
-  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/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/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));
@@ -98,7 +111,13 @@ app.prepare().then(() => {
   server.post('/api/auth/usersettings', auth.authJwt, auth.userSettings);
 
   /* URL shortener */
-  server.post('/api/url/submit', auth.authApikey, auth.authJwtLoose, catchErrors(url.urlShortener));
+  server.post(
+    '/api/url/submit',
+    auth.authApikey,
+    auth.authJwtLoose,
+    catchErrors(auth.recaptcha),
+    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));