Kaynağa Gözat

Use mongodb for auth queries

poeti8 6 yıl önce
ebeveyn
işleme
d8991baca3

+ 14 - 14
server/controllers/authController.js

@@ -110,9 +110,9 @@ exports.signup = async (req, res) => {
   if (email.length > 64) {
     return res.status(400).json({ error: 'Maximum email length is 64.' });
   }
-  const user = await getUser({ email });
+  const user = await getUser(email);
   if (user && user.verified) return res.status(403).json({ error: 'Email is already in use.' });
-  const newUser = await createUser({ email, password });
+  const newUser = await createUser(email, password);
   const mail = await transporter.sendMail({
     from: process.env.MAIL_FROM || process.env.MAIL_USER,
     to: newUser.email,
@@ -137,8 +137,7 @@ exports.renew = ({ user }, res) => {
 };
 
 exports.verify = async (req, res, next) => {
-  const { verificationToken = '' } = req.params;
-  const user = await verifyUser({ verificationToken });
+  const user = await verifyUser(req.params.verificationToken);
   if (user) {
     const token = signToken(user);
     req.user = { token };
@@ -153,17 +152,17 @@ exports.changePassword = async ({ body: { password }, user }, res) => {
   if (password.length > 64) {
     return res.status(400).json({ error: 'Maximum password length is 64.' });
   }
-  const changedUser = await changePassword({ email: user.email, password });
+  const changedUser = await changePassword(user._id, password);
   if (changedUser) {
     return res.status(200).json({ message: 'Your password has been changed successfully.' });
   }
   return res.status(400).json({ error: "Couldn't change the password. Try again later" });
 };
 
-exports.generateApiKey = async ({ user }, res) => {
-  const { apikey } = await generateApiKey({ email: user.email });
-  if (apikey) {
-    return res.status(201).json({ apikey });
+exports.generateApiKey = async (req, res) => {
+  const user = await generateApiKey(req.user._id);
+  if (user.apikey) {
+    return res.status(201).json({ apikey: user.apikey });
   }
   return res.status(400).json({ error: 'Sorry, an error occured. Please try again later.' });
 };
@@ -176,8 +175,8 @@ exports.userSettings = ({ user }, res) =>
     useHttps: user.useHttps || false,
   });
 
-exports.requestPasswordReset = async ({ body: { email } }, res) => {
-  const user = await requestPasswordReset({ email });
+exports.requestPasswordReset = async (req, res) => {
+  const user = await requestPasswordReset(req.body.email);
   if (!user) {
     return res.status(400).json({ error: "Couldn't reset password." });
   }
@@ -193,14 +192,15 @@ exports.requestPasswordReset = async ({ body: { email } }, res) => {
       .replace(/{{domain}}/gm, process.env.DEFAULT_DOMAIN),
   });
   if (mail.accepted.length) {
-    return res.status(200).json({ email, message: 'Reset password email has been sent.' });
+    return res
+      .status(200)
+      .json({ email: user.email, message: 'Reset password email has been sent.' });
   }
   return res.status(400).json({ error: "Couldn't reset password." });
 };
 
 exports.resetPassword = async (req, res, next) => {
-  const { resetPasswordToken = '' } = req.params;
-  const user = await resetPassword({ resetPasswordToken });
+  const user = await resetPassword(req.params.resetPasswordToken);
   if (user) {
     const token = signToken(user);
     req.user = { token };

+ 2 - 2
server/controllers/urlController.js

@@ -8,7 +8,7 @@ const geoip = require('geoip-lite');
 const bcrypt = require('bcryptjs');
 const ua = require('universal-analytics');
 const isbot = require('isbot');
-const { addIPCooldown } = require('../db/user');
+const { addIp } = require('../db/user');
 const {
   addUrlCount,
   createShortUrl,
@@ -93,7 +93,7 @@ exports.urlShortener = async ({ body, realIp, user }, res) => {
     const target = addProtocol(body.target);
     const url = await createShortUrl({ ...body, id, target, user });
     if (!user && Number(process.env.NON_USER_COOLDOWN)) {
-      addIPCooldown(realIp);
+      addIp(realIp);
     }
 
     return res.json(url);

+ 8 - 8
server/controllers/validateBodyController.js

@@ -6,7 +6,7 @@ const urlRegex = require('url-regex');
 const validator = require('express-validator/check');
 const { differenceInMinutes, subHours } = require('date-fns/');
 const { validationResult } = require('express-validator/check');
-const { addCooldown, banUser, getIPCooldown: getIPCooldownCount } = require('../db/user');
+const { addCooldown, banUser, getIp } = require('../db/user');
 const { getBannedDomain, getBannedHost, urlCountFromDate } = require('../db/url');
 const subDay = require('date-fns/sub_days');
 const { addProtocol } = require('../utils');
@@ -112,7 +112,7 @@ exports.validateUrl = async ({ body, user }, res, next) => {
 exports.cooldownCheck = async user => {
   if (user && user.cooldowns) {
     if (user.cooldowns.length > 4) {
-      await banUser(user);
+      await banUser(user._id);
       throw new Error('Too much malware requests. You are now banned.');
     }
     const hasCooldownNow = user.cooldowns.some(
@@ -125,11 +125,11 @@ exports.cooldownCheck = async user => {
 };
 
 exports.ipCooldownCheck = async (req, res, next) => {
-  const cooldonwConfig = Number(process.env.NON_USER_COOLDOWN);
-  if (req.user || !cooldonwConfig) return next();
-  const cooldownDate = await getIPCooldownCount(req.realIp);
-  if (cooldownDate) {
-    const timeToWait = cooldonwConfig - differenceInMinutes(new Date(), cooldownDate);
+  const cooldownConfig = Number(process.env.NON_USER_COOLDOWN);
+  if (req.user || !cooldownConfig) return next();
+  const ip = await getIp(req.realIp);
+  if (ip) {
+    const timeToWait = cooldownConfig - differenceInMinutes(new Date(), ip.createdAt);
     return res
       .status(400)
       .json({ error: `Non-logged in users are limited. Wait ${timeToWait} minutes or log in.` });
@@ -163,7 +163,7 @@ exports.malwareCheck = async (user, target) => {
   );
   if (isMalware.data && isMalware.data.matches) {
     if (user) {
-      await addCooldown(user);
+      await addCooldown(user._id);
     }
     throw new Error(user ? 'Malware detected! Cooldown for 12h.' : 'Malware detected!');
   }

+ 2 - 2
server/cron.js

@@ -1,8 +1,8 @@
 const cron = require('node-cron');
-const { clearIPs } = require('./db/user');
+const { clearIps } = require('./db/user');
 
 if (Number(process.env.NON_USER_COOLDOWN)) {
   cron.schedule('* */24 * * *', () => {
-    clearIPs().catch();
+    clearIps().catch();
   });
 }

+ 113 - 188
server/db/user.js

@@ -1,229 +1,154 @@
 const bcrypt = require('bcryptjs');
 const nanoid = require('nanoid');
+const uuid = require('uuid/v4');
 const subMinutes = require('date-fns/sub_minutes');
-const driver = require('./neo4j');
-
-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 };
+const addMinutes = require('date-fns/add_minutes');
+const User = require('../models/user');
+const Ip = require('../models/ip');
+
+exports.getUser = async (emailOrKey = '') => {
+  const user = await User.findOne({
+    $or: [{ email: emailOrKey }, { apikey: emailOrKey }],
+  }).lean();
+  // TODO: Get domains
+
+  // 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 };
+  return user;
 };
 
-exports.createUser = async ({ email, password }) => {
-  const session = driver.session();
+exports.createUser = async (email, password) => {
   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,
-      }
-    )
+  const hashedPassword = await bcrypt.hash(password, salt);
+
+  const user = await User.findOneAndUpdate(
+    { email },
+    {
+      email,
+      password: hashedPassword,
+      verificationToken: uuid(),
+      verificationExpires: addMinutes(new Date(), 60),
+    },
+    { new: true, upsert: true, runValidators: true, setDefaultsOnInsert: true }
   );
-  session.close();
-  const user = records[0].get('u').properties;
+
   return user;
 };
 
-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(),
-      }
-    )
+exports.verifyUser = async verificationToken => {
+  const user = await User.findOneAndUpdate(
+    { verificationToken, verificationExpires: { $gt: new Date() } },
+    {
+      verified: true,
+      verificationToken: undefined,
+      verificationExpires: undefined,
+    },
+    { new: true }
   );
-  session.close();
-  const user = records.length && records[0].get('u').properties;
+
   return user;
 };
 
-exports.changePassword = async ({ email, password }) => {
-  const session = driver.session();
+exports.changePassword = async (id, newPassword) => {
   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;
+  const password = await bcrypt.hash(newPassword, salt);
+
+  const user = await User.findByIdAndUpdate(id, { password }, { new: true });
+
   return user;
 };
 
-exports.generateApiKey = async ({ email }) => {
-  const session = driver.session();
+exports.generateApiKey = async id => {
   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 = 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;
+  const user = await User.findByIdAndUpdate(id, { apikey }, { new: true });
+
   return user;
 };
 
-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(),
-      }
-    )
+exports.requestPasswordReset = async email => {
+  const resetPasswordToken = uuid();
+
+  const user = await User.findOneAndUpdate(
+    { email },
+    {
+      resetPasswordToken,
+      resetPasswordExpires: addMinutes(new Date(), 30),
+    },
+    { new: true }
   );
-  session.close();
-  const user = records.length && records[0].get('u').properties;
+
   return user;
 };
 
-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,
-      }
-    )
+exports.resetPassword = async resetPasswordToken => {
+  const user = await User.findOneAndUpdate(
+    { resetPasswordToken, resetPasswordExpires: { $gt: new Date() } },
+    { resetPasswordExpires: undefined, resetPasswordToken: undefined },
+    { new: true }
   );
-  session.close();
-  const count = records.length && records[0].get('count').toNumber();
-  return { count };
-};
 
-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 };
+  return user;
 };
 
-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,
-    })
+exports.addCooldown = async id => {
+  const user = await User.findByIdAndUpdate(
+    id,
+    { $push: { cooldowns: new Date() } },
+    { new: true }
   );
-  session.close();
-  const user = records.length && records[0].get('u');
-  return { user };
+
+  return user;
 };
 
-exports.addIPCooldown = async ip => {
-  const session = driver.session();
-  const { records = [] } = await session.writeTransaction(tx =>
-    tx.run(
-      'MERGE (i:IP { ip: $ip }) ' +
-        'MERGE (i)-[r:RECEIVED]->(c:COOLDOWN { date: $date }) ' +
-        'RETURN COUNT(r) as count',
-      {
-        date: new Date().toJSON(),
-        ip,
-      }
-    )
+exports.banUser = async id => {
+  const user = await User.findByIdAndUpdate(
+    id,
+    {
+      banned: true,
+    },
+    { new: true }
   );
-  session.close();
-  const count = records.length && records[0].get('count').toNumber();
-  return count;
+
+  return user;
 };
 
-exports.getIPCooldown = async ip => {
-  const session = driver.session();
-  const { records = [] } = await session.readTransaction(tx =>
-    tx.run(
-      'MATCH (i:IP { ip: $ip }) ' +
-        'MATCH (i)-[:RECEIVED]->(c:COOLDOWN) ' +
-        'WHERE c.date > $date ' +
-        'RETURN c.date as date',
-      {
-        date: subMinutes(new Date(), Number(process.env.NON_USER_COOLDOWN)).toJSON(),
-        ip,
-      }
-    )
+exports.addIp = async newIp => {
+  const ip = await Ip.findOneAndUpdate(
+    { ip: newIp },
+    { ip: newIp, createdAt: new Date() },
+    { new: true, upsert: true, runValidators: true }
   );
-  session.close();
-  const count = records.length && records[0].get('date');
-  return count;
+  return ip;
 };
 
-exports.clearIPs = async () => {
-  const session = driver.session();
-  await session.writeTransaction(tx =>
-    tx.run('MATCH (i:IP)-[:RECEIVED]->(c:COOLDOWN) WHERE c.date < $date DETACH DELETE i, c', {
-      date: subMinutes(new Date(), Number(process.env.NON_USER_COOLDOWN)).toJSON(),
-    })
-  );
-  session.close();
+exports.getIp = async ip => {
+  const matchedIp = await Ip.findOne({
+    ip,
+    createdAt: { $gt: subMinutes(new Date(), Number(process.env.NON_USER_COOLDOWN)) },
+  });
+  return matchedIp;
 };
+
+exports.clearIps = async () =>
+  Ip.deleteMany({
+    createdAt: { $lt: subMinutes(new Date(), Number(process.env.NON_USER_COOLDOWN)) },
+  });

+ 8 - 0
server/models/ip.js

@@ -0,0 +1,8 @@
+const mongoose = require('mongoose');
+
+const IpSchema = new mongoose.Schema({
+  ip: { type: String, required: true, trim: true },
+  createdAt: { type: Date, default: Date.now },
+});
+
+module.exports = mongoose.model('ip', IpSchema);

+ 18 - 0
server/models/user.js

@@ -0,0 +1,18 @@
+const mongoose = require('mongoose');
+
+const UserSchema = new mongoose.Schema({
+  apikey: { type: String, unique: true },
+  banned: { type: Boolean, default: false },
+  cooldowns: [Date],
+  createdAt: { type: Date, required: true, default: Date.now },
+  email: { type: String, required: true, trim: true, lowercase: true, unique: true },
+  password: { type: String, required: true },
+  resetPasswordExpires: { type: Date },
+  resetPasswordToken: { type: String },
+  verificationExpires: { type: Date },
+  verificationToken: { type: String },
+  verified: { type: Boolean, required: true, default: false },
+  // TODO: domains
+});
+
+module.exports = mongoose.model('user', UserSchema);

+ 3 - 3
server/passport.js

@@ -14,7 +14,7 @@ const jwtOptions = {
 passport.use(
   new JwtStrategy(jwtOptions, async (payload, done) => {
     try {
-      const user = await getUser({ email: payload.sub });
+      const user = await getUser(payload.sub);
       if (!user) return done(null, false);
       return done(null, user);
     } catch (err) {
@@ -30,7 +30,7 @@ const localOptions = {
 passport.use(
   new LocalStratergy(localOptions, async (email, password, done) => {
     try {
-      const user = await getUser({ email });
+      const user = await getUser(email);
       if (!user) {
         return done(null, false);
       }
@@ -53,7 +53,7 @@ const localAPIKeyOptions = {
 passport.use(
   new LocalAPIKeyStrategy(localAPIKeyOptions, async (apikey, done) => {
     try {
-      const user = await getUser({ apikey });
+      const user = await getUser(apikey);
       if (!user) {
         return done(null, false);
       }

+ 4 - 0
server/server.js

@@ -2,6 +2,7 @@ require('./configToEnv');
 require('dotenv').config();
 const nextApp = require('next');
 const express = require('express');
+const mongoose = require('mongoose');
 const helmet = require('helmet');
 const morgan = require('morgan');
 const Raven = require('raven');
@@ -22,9 +23,12 @@ const neo4j = require('./db/neo4j');
 require('./cron');
 require('./passport');
 
+mongoose.connect(process.env.MONGODB_URI, { useNewUrlParser: true });
+
 if (process.env.RAVEN_DSN) {
   Raven.config(process.env.RAVEN_DSN).install();
 }
+
 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.' });