瀏覽代碼

chore: remove v1 endpoints and neo4j migration guide

Pouria Ezzati 3 年之前
父節點
當前提交
ea9ac99d87

+ 0 - 6
.docker.env

@@ -18,12 +18,6 @@ DB_USER=
 DB_PASSWORD=
 DB_SSL=false
 
-# ONLY NEEDED FOR MIGRATION !!1!
-# Neo4j database credential details
-NEO4J_DB_URI=bolt://localhost
-NEO4J_DB_USERNAME=neo4j
-NEO4J_DB_PASSWORD=pass
-
 # Redis host and port
 REDIS_HOST=redis
 REDIS_PORT=6379

+ 0 - 6
.example.env

@@ -18,12 +18,6 @@ DB_USER=
 DB_PASSWORD=
 DB_SSL=false
 
-# ONLY NEEDED FOR MIGRATION !!1!
-# Neo4j database credential details
-NEO4J_DB_URI=bolt://localhost
-NEO4J_DB_USERNAME=neo4j
-NEO4J_DB_PASSWORD=pass
-
 # Redis host and port
 REDIS_HOST=127.0.0.1
 REDIS_PORT=6379

+ 0 - 44
MIGRATION.md

@@ -1,44 +0,0 @@
-# Migrate database from Neo4j to Postgres
-
-As explained in issue #197, Kutt is ditching Neo4j in favor of Postgres in version 2. But what happens to old data? Well, I have created migration scripts that you can use to transfer data from your Neo4j database to your new Postgres database.
-
-### 🚧 IMPORTANT: v2 is still in beta (but somehow more stable than v1)
-
-## General recommendations
-
-- Importing Neo4j data into local Neo4j database and migrate from there would speed things up.
-- Use local Postgres database (where app lives), because using a remote database server will be way slower. If you're doing this locally, you can import data from local database to the remote one after migration has finished. I used this command to move data:
-
-## 1. Set up a Postgres database
-
-Set up a Postgres database, either on your own server or using a SaaS service.
-
-## 2. Pull and run Kutt's new version
-
-Right now version 2 is in beta. Therefore, pull from `develop` branch and create and fill the `.env` file based on `.example.env`.
-
-**NOTE**: Run the app at least once and let it create and initialize tables in the database. You just need to do `npm run dev` and wait for it to create tables. Then check your database to make sure tables have been created. (If your production database is separate, you need to initialize it too).
-
-## 3. Migrate data using scripts
-
-First, do `npm run build` to build the files. Now if you check `production-server/migration` folder you will fine 4 files. You can now run these scripts one by one.
-
-**NOTE:** that the order of running the scripts is important.
-
-**NOTE:** Step 4 is going to take a good chunk of time.
-
-**NOTE:** If step 4 fails at any stage, you should delete links and visits data from the database and try again.
-
-```
-// 1. Migrate data: Hosts
-node production-server/migration/01_hosts.js
-
-// 2. Migrate data: Users
-node production-server/migration/02_users.js
-
-// 3. Migrate data: Domains
-node production-server/migration/03_domains.js
-
-// 4. Migrate data: Links
-node production-server/migration/04_links.js
-```

+ 0 - 6
README.md

@@ -13,12 +13,6 @@ _Contributions and bug reports are welcome._
 [![GitHub license](https://img.shields.io/github/license/thedevs-network/kutt.svg)](https://github.com/thedevs-network/kutt/blob/develop/LICENSE)
 [![Twitter](https://img.shields.io/twitter/url/https/github.com/thedevs-network/kutt/.svg?style=social)](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fthedevs-network%2Fkutt%2F)
 
-## Migration from v1
-
-The new version of Kutt is here. In version 2, we used TypeScript and we moved from Neo4j to PostgreSQL database in favor of performance and we're working on adding new features.
-
-If you're coming from v1, refer to [MIGRATION.md](MIGRATION.md) to migrate data from Neo4j to PostgreSQL.
-
 ## Table of Contents
 
 - [Key Features](#key-features)

+ 0 - 404
server/__v1/controllers/linkController.ts

@@ -1,404 +0,0 @@
-import bcrypt from "bcryptjs";
-import dns from "dns";
-import { Handler } from "express";
-import isbot from "isbot";
-import generate from "nanoid/generate";
-import ua from "universal-analytics";
-import URL from "url";
-import urlRegex from "url-regex";
-import { promisify } from "util";
-import { deleteDomain, getDomain, setDomain } from "../db/domain";
-import { addIP } from "../db/ip";
-import env from "../../env";
-import {
-  banLink,
-  createShortLink,
-  deleteLink,
-  findLink,
-  getLinks,
-  getStats,
-  getUserLinksCount
-} from "../db/link";
-import transporter from "../../mail/mail";
-import * as redis from "../../redis";
-import {
-  addProtocol,
-  generateShortLink,
-  getStatsCacheTime,
-  removeWww
-} from "../../utils";
-import {
-  checkBannedDomain,
-  checkBannedHost,
-  cooldownCheck,
-  malwareCheck,
-  preservedUrls,
-  urlCountsCheck
-} from "./validateBodyController";
-import queue from "../../queues";
-
-const dnsLookup = promisify(dns.lookup);
-
-const generateId = async () => {
-  const address = generate(
-    "abcdefghkmnpqrstuvwxyzABCDEFGHKLMNPQRSTUVWXYZ23456789",
-    env.LINK_LENGTH
-  );
-  const link = await findLink({ address });
-  if (!link) return address;
-  return generateId();
-};
-
-export const shortener: Handler = async (req, res) => {
-  try {
-    const target = addProtocol(req.body.target);
-    const targetDomain = removeWww(URL.parse(target).hostname);
-
-    const queries = await Promise.all([
-      env.GOOGLE_SAFE_BROWSING_KEY && cooldownCheck(req.user),
-      env.GOOGLE_SAFE_BROWSING_KEY && malwareCheck(req.user, req.body.target),
-      req.user && urlCountsCheck(req.user),
-      req.user &&
-        req.body.reuse &&
-        findLink({
-          target,
-          user_id: req.user.id
-        }),
-      req.user &&
-        req.body.customurl &&
-        findLink({
-          address: req.body.customurl,
-          domain_id: req.user.domain_id || null
-        }),
-      (!req.user || !req.body.customurl) && generateId(),
-      checkBannedDomain(targetDomain),
-      checkBannedHost(targetDomain)
-    ]);
-
-    // if "reuse" is true, try to return
-    // the existent URL without creating one
-    if (queries[3]) {
-      const { domain_id: d, user_id: u, ...link } = queries[3];
-      const shortLink = generateShortLink(link.address, req.user.domain);
-      const data = {
-        ...link,
-        id: link.address,
-        password: !!link.password,
-        reuse: true,
-        shortLink,
-        shortUrl: shortLink
-      };
-      return res.json(data);
-    }
-
-    // Check if custom link already exists
-    if (queries[4]) {
-      throw new Error("Custom URL is already in use.");
-    }
-
-    // Create new link
-    const address = (req.user && req.body.customurl) || queries[5];
-    const link = await createShortLink(
-      {
-        ...req.body,
-        address,
-        target
-      },
-      req.user
-    );
-    if (!req.user && env.NON_USER_COOLDOWN) {
-      addIP(req.realIP);
-    }
-
-    return res.json({ ...link, id: link.address });
-  } catch (error) {
-    return res.status(400).json({ error: error.message });
-  }
-};
-
-export const goToLink: Handler = async (req, res, next) => {
-  const host = removeWww(req.headers.host);
-  const requestedId = req.params.id || req.body.id;
-  const address = requestedId.replace("+", "");
-  const customDomain = host !== env.DEFAULT_DOMAIN && host;
-  const isBot = isbot(req.headers["user-agent"]);
-
-  let domain;
-  if (customDomain) {
-    domain = await getDomain({ address: customDomain });
-  }
-
-  const link = await findLink({ address, domain_id: domain && domain.id });
-
-  if (!link) {
-    if (host !== env.DEFAULT_DOMAIN) {
-      if (!domain || !domain.homepage) return next();
-      return res.redirect(302, domain.homepage);
-    }
-    return next();
-  }
-
-  if (link.banned) {
-    return res.redirect("/banned");
-  }
-
-  const doesRequestInfo = /.*\+$/gi.test(requestedId);
-  if (doesRequestInfo && !link.password) {
-    req.linkTarget = link.target;
-    req.pageType = "info";
-    return next();
-  }
-
-  if (link.password && !req.body.password) {
-    req.protectedLink = address;
-    req.pageType = "password";
-    return next();
-  }
-
-  if (link.password) {
-    const isMatch = await bcrypt.compare(req.body.password, link.password);
-    if (!isMatch) {
-      return res.status(401).json({ error: "Password is not correct" });
-    }
-    if (link.user_id && !isBot) {
-      queue.visit.add({
-        headers: req.headers,
-        realIP: req.realIP,
-        referrer: req.get("Referrer"),
-        link,
-        customDomain
-      });
-    }
-    return res.status(200).json({ target: link.target });
-  }
-
-  if (link.user_id && !isBot) {
-    queue.visit.add({
-      headers: req.headers,
-      realIP: req.realIP,
-      referrer: req.get("Referrer"),
-      link,
-      customDomain
-    });
-  }
-
-  if (env.GOOGLE_ANALYTICS_UNIVERSAL && !isBot) {
-    const visitor = ua(env.GOOGLE_ANALYTICS_UNIVERSAL);
-    visitor
-      .pageview({
-        dp: `/${address}`,
-        ua: req.headers["user-agent"],
-        uip: req.realIP,
-        aip: 1
-      })
-      .send();
-  }
-
-  return res.redirect(link.target);
-};
-
-export const getUserLinks: Handler = async (req, res) => {
-  const [countAll, list] = await Promise.all([
-    getUserLinksCount({ user_id: req.user.id }),
-    getLinks(req.user.id, req.query)
-  ]);
-  return res.json({ list, countAll: parseInt(countAll) });
-};
-
-export const setCustomDomain: Handler = async (req, res) => {
-  const parsed = URL.parse(req.body.customDomain);
-  const customDomain = removeWww(parsed.hostname || parsed.href);
-  if (!customDomain)
-    return res.status(400).json({ error: "Domain is not valid." });
-  if (customDomain.length > 40) {
-    return res
-      .status(400)
-      .json({ error: "Maximum custom domain length is 40." });
-  }
-  if (customDomain === env.DEFAULT_DOMAIN) {
-    return res.status(400).json({ error: "You can't use default domain." });
-  }
-  const isValidHomepage =
-    !req.body.homepage ||
-    urlRegex({ exact: true, strict: false }).test(req.body.homepage);
-  if (!isValidHomepage)
-    return res.status(400).json({ error: "Homepage is not valid." });
-  const homepage =
-    req.body.homepage &&
-    (URL.parse(req.body.homepage).protocol
-      ? req.body.homepage
-      : `http://${req.body.homepage}`);
-  const matchedDomain = await getDomain({ address: customDomain });
-  if (
-    matchedDomain &&
-    matchedDomain.user_id &&
-    matchedDomain.user_id !== req.user.id
-  ) {
-    return res.status(400).json({
-      error: "Domain is already taken. Contact us for multiple users."
-    });
-  }
-  const userCustomDomain = await setDomain(
-    {
-      address: customDomain,
-      homepage
-    },
-    req.user,
-    matchedDomain
-  );
-  if (userCustomDomain) {
-    return res.status(201).json({
-      customDomain: userCustomDomain.address,
-      homepage: userCustomDomain.homepage
-    });
-  }
-  return res.status(400).json({ error: "Couldn't set custom domain." });
-};
-
-export const deleteCustomDomain: Handler = async (req, res) => {
-  const response = await deleteDomain(req.user);
-  if (response)
-    return res.status(200).json({ message: "Domain deleted successfully" });
-  return res.status(400).json({ error: "Couldn't delete custom domain." });
-};
-
-export const customDomainRedirection: Handler = async (req, res, next) => {
-  const { path } = req;
-  const host = removeWww(req.headers.host);
-  if (
-    host !== env.DEFAULT_DOMAIN &&
-    (path === "/" ||
-      preservedUrls
-        .filter(l => l !== "url-password")
-        .some(item => item === path.replace("/", "")))
-  ) {
-    const domain = await getDomain({ address: host });
-    return res.redirect(
-      302,
-      (domain && domain.homepage) || `https://${env.DEFAULT_DOMAIN + path}`
-    );
-  }
-  return next();
-};
-
-export const deleteUserLink: Handler = async (req, res) => {
-  const { id, domain } = req.body;
-
-  if (!id) {
-    return res.status(400).json({ error: "No id has been provided." });
-  }
-
-  const response = await deleteLink({
-    address: id,
-    domain: !domain || domain === env.DEFAULT_DOMAIN ? null : domain,
-    user_id: req.user.id
-  });
-
-  if (response) {
-    return res.status(200).json({ message: "Short link deleted successfully" });
-  }
-
-  return res.status(400).json({ error: "Couldn't delete the short link." });
-};
-
-export const getLinkStats: Handler = async (req, res) => {
-  if (!req.query.id) {
-    return res.status(400).json({ error: "No id has been provided." });
-  }
-
-  const hostname = removeWww(URL.parse(req.query.domain).hostname);
-  const hasCustomDomain = req.query.domain && hostname !== env.DEFAULT_DOMAIN;
-  const customDomain = hasCustomDomain
-    ? (await getDomain({ address: req.query.domain })) || ({ id: -1 } as Domain)
-    : ({} as Domain);
-
-  const redisKey = req.query.id + (customDomain.address || "") + req.user.email;
-  const cached = await redis.get(redisKey);
-  if (cached) return res.status(200).json(JSON.parse(cached));
-
-  const link = await findLink({
-    address: req.query.id,
-    domain_id: hasCustomDomain ? customDomain.id : null,
-    user_id: req.user && req.user.id
-  });
-
-  if (!link) {
-    return res.status(400).json({ error: "Couldn't find the short link." });
-  }
-
-  const stats = await getStats(link, customDomain);
-
-  if (!stats) {
-    return res
-      .status(400)
-      .json({ error: "Could not get the short link stats." });
-  }
-
-  const cacheTime = getStatsCacheTime(0);
-  redis.set(redisKey, JSON.stringify(stats), "EX", cacheTime);
-  return res.status(200).json(stats);
-};
-
-export const reportLink: Handler = async (req, res) => {
-  if (!req.body.link) {
-    return res.status(400).json({ error: "No URL has been provided." });
-  }
-
-  const hostname = removeWww(URL.parse(req.body.link).hostname);
-  if (hostname !== env.DEFAULT_DOMAIN) {
-    return res.status(400).json({
-      error: `You can only report a ${env.DEFAULT_DOMAIN} link`
-    });
-  }
-
-  const mail = await transporter.sendMail({
-    from: env.MAIL_FROM || env.MAIL_USER,
-    to: env.REPORT_MAIL,
-    subject: "[REPORT]",
-    text: req.body.link,
-    html: req.body.link
-  });
-  if (mail.accepted.length) {
-    return res
-      .status(200)
-      .json({ message: "Thanks for the report, we'll take actions shortly." });
-  }
-  return res
-    .status(400)
-    .json({ error: "Couldn't submit the report. Try again later." });
-};
-
-export const ban: Handler = async (req, res) => {
-  if (!req.body.id)
-    return res.status(400).json({ error: "No id has been provided." });
-
-  const link = await findLink({ address: req.body.id, domain_id: null });
-
-  if (!link) return res.status(400).json({ error: "Link does not exist." });
-
-  if (link.banned) {
-    return res.status(200).json({ message: "Link was banned already." });
-  }
-
-  const domain = removeWww(URL.parse(link.target).hostname);
-
-  let host;
-  if (req.body.host) {
-    try {
-      const dnsRes = await dnsLookup(domain);
-      host = dnsRes && dnsRes.address;
-    } catch (error) {
-      host = null;
-    }
-  }
-
-  await banLink({
-    adminId: req.user.id,
-    domain,
-    host,
-    address: req.body.id,
-    banUser: !!req.body.user
-  });
-
-  return res.status(200).json({ message: "Link has been banned successfully" });
-};

+ 0 - 223
server/__v1/controllers/validateBodyController.ts

@@ -1,223 +0,0 @@
-import { differenceInMinutes, subHours, subDays, isAfter } from "date-fns";
-import { validationResult } from "express-validator";
-import { body } from "express-validator";
-import { RequestHandler } from "express";
-import { promisify } from "util";
-import urlRegex from "url-regex";
-import axios from "axios";
-import dns from "dns";
-import URL from "url";
-
-import { addProtocol, CustomError, removeWww } from "../../utils";
-import { addCooldown, banUser } from "../db/user";
-import { getUserLinksCount } from "../db/link";
-import { getDomain } from "../db/domain";
-import { getHost } from "../db/host";
-import { getIP } from "../db/ip";
-import env from "../../env";
-
-const dnsLookup = promisify(dns.lookup);
-
-export const validationCriterias = [
-  body("email")
-    .exists()
-    .withMessage("Email must be provided.")
-    .isEmail()
-    .withMessage("Email is not valid.")
-    .trim(),
-  body("password", "Password must be at least 8 chars long.")
-    .exists()
-    .withMessage("Password must be provided.")
-    .isLength({ min: 8 })
-];
-
-export const validateBody = (req, res, next) => {
-  const errors = validationResult(req);
-  if (!errors.isEmpty()) {
-    const errorsObj = errors.mapped();
-    const emailError = errorsObj.email && errorsObj.email.msg;
-    const passwordError = errorsObj.password && errorsObj.password.msg;
-    return res.status(400).json({ error: emailError || passwordError });
-  }
-  return next();
-};
-
-export const preservedUrls = [
-  "login",
-  "logout",
-  "signup",
-  "reset-password",
-  "resetpassword",
-  "url-password",
-  "url-info",
-  "settings",
-  "stats",
-  "verify",
-  "api",
-  "404",
-  "static",
-  "images",
-  "banned",
-  "terms",
-  "privacy",
-  "protected",
-  "report",
-  "pricing"
-];
-
-export const validateUrl: RequestHandler = async (req, res, next) => {
-  // Validate URL existence
-  if (!req.body.target)
-    return res.status(400).json({ error: "No target has been provided." });
-
-  // validate URL length
-  if (req.body.target.length > 2040) {
-    return res.status(400).json({ error: "Maximum URL length is 2040." });
-  }
-
-  // Validate URL
-  const isValidUrl = urlRegex({ exact: true, strict: false }).test(
-    req.body.target
-  );
-  if (!isValidUrl && !/^\w+:\/\//.test(req.body.target))
-    return res.status(400).json({ error: "URL is not valid." });
-
-  // If target is the URL shortener itself
-  const host = removeWww(URL.parse(addProtocol(req.body.target)).host);
-  if (host === env.DEFAULT_DOMAIN) {
-    return res
-      .status(400)
-      .json({ error: `${env.DEFAULT_DOMAIN} URLs are not allowed.` });
-  }
-
-  // Validate password length
-  if (req.body.password && req.body.password.length > 64) {
-    return res.status(400).json({ error: "Maximum password length is 64." });
-  }
-
-  // Custom URL validations
-  if (req.user && req.body.customurl) {
-    // Validate custom URL
-    if (!/^[a-zA-Z0-9-_]+$/g.test(req.body.customurl.trim())) {
-      return res.status(400).json({ error: "Custom URL is not valid." });
-    }
-
-    // Prevent from using preserved URLs
-    if (preservedUrls.some(url => url === req.body.customurl)) {
-      return res
-        .status(400)
-        .json({ error: "You can't use this custom URL name." });
-    }
-
-    // Validate custom URL length
-    if (req.body.customurl.length > 64) {
-      return res
-        .status(400)
-        .json({ error: "Maximum custom URL length is 64." });
-    }
-  }
-
-  return next();
-};
-
-export const cooldownCheck = async (user: User) => {
-  if (user && user.cooldowns) {
-    if (user.cooldowns.length > 4) {
-      await banUser(user.id);
-      throw new Error("Too much malware requests. You are banned.");
-    }
-    const hasCooldownNow = user.cooldowns.some(cooldown =>
-      isAfter(subHours(new Date(), 12), new Date(cooldown))
-    );
-    if (hasCooldownNow) {
-      throw new Error("Cooldown because of a malware URL. Wait 12h");
-    }
-  }
-};
-
-export const ipCooldownCheck: RequestHandler = async (req, res, next) => {
-  const cooldownConfig = env.NON_USER_COOLDOWN;
-  if (req.user || !cooldownConfig) return next();
-  const ip = await getIP(req.realIP);
-  if (ip) {
-    const timeToWait =
-      cooldownConfig - differenceInMinutes(new Date(), new Date(ip.created_at));
-    return res.status(400).json({
-      error:
-        `Non-logged in users are limited. Wait ${timeToWait} ` +
-        "minutes or log in."
-    });
-  }
-  next();
-};
-
-export const malwareCheck = async (user: User, target: string) => {
-  const isMalware = await axios.post(
-    `https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${env.GOOGLE_SAFE_BROWSING_KEY}`,
-    {
-      client: {
-        clientId: env.DEFAULT_DOMAIN.toLowerCase().replace(".", ""),
-        clientVersion: "1.0.0"
-      },
-      threatInfo: {
-        threatTypes: [
-          "THREAT_TYPE_UNSPECIFIED",
-          "MALWARE",
-          "SOCIAL_ENGINEERING",
-          "UNWANTED_SOFTWARE",
-          "POTENTIALLY_HARMFUL_APPLICATION"
-        ],
-        platformTypes: ["ANY_PLATFORM", "PLATFORM_TYPE_UNSPECIFIED"],
-        threatEntryTypes: [
-          "EXECUTABLE",
-          "URL",
-          "THREAT_ENTRY_TYPE_UNSPECIFIED"
-        ],
-        threatEntries: [{ url: target }]
-      }
-    }
-  );
-  if (isMalware.data && isMalware.data.matches) {
-    if (user) {
-      await addCooldown(user.id);
-    }
-    throw new CustomError(
-      user ? "Malware detected! Cooldown for 12h." : "Malware detected!"
-    );
-  }
-};
-
-export const urlCountsCheck = async (user: User) => {
-  const count = await getUserLinksCount({
-    user_id: user.id,
-    date: subDays(new Date(), 1)
-  });
-  if (count > env.USER_LIMIT_PER_DAY) {
-    throw new CustomError(
-      `You have reached your daily limit (${env.USER_LIMIT_PER_DAY}). Please wait 24h.`
-    );
-  }
-};
-
-export const checkBannedDomain = async (domain: string) => {
-  const bannedDomain = await getDomain({ address: domain, banned: true });
-  if (bannedDomain) {
-    throw new CustomError("URL is containing malware/scam.");
-  }
-};
-
-export const checkBannedHost = async (domain: string) => {
-  let isHostBanned;
-  try {
-    const dnsRes = await dnsLookup(domain);
-    isHostBanned = await getHost({
-      address: dnsRes && dnsRes.address,
-      banned: true
-    });
-  } catch (error) {
-    isHostBanned = null;
-  }
-  if (isHostBanned) {
-    throw new CustomError("URL is containing malware/scam.");
-  }
-};

+ 0 - 114
server/__v1/db/domain.ts

@@ -1,114 +0,0 @@
-import knex from "../../knex";
-import * as redis from "../../redis";
-import { getRedisKey } from "../../utils";
-
-export const getDomain = async (data: Partial<Domain>): Promise<Domain> => {
-  const getData = {
-    ...data,
-    ...(data.address && { address: data.address.toLowerCase() }),
-    ...(data.homepage && { homepage: data.homepage.toLowerCase() })
-  };
-
-  const redisKey = getRedisKey.domain(getData.address);
-  const cachedDomain = await redis.get(redisKey);
-
-  if (cachedDomain) return JSON.parse(cachedDomain);
-
-  const domain = await knex<Domain>("domains")
-    .where(getData)
-    .first();
-
-  if (domain) {
-    redis.set(redisKey, JSON.stringify(domain), "EX", 60 * 60 * 6);
-  }
-
-  return domain;
-};
-
-export const setDomain = async (
-  data: Partial<Domain>,
-  user: UserJoined,
-  matchedDomain: Domain
-) => {
-  // 1. If user has domain, remove it from their possession
-  await knex<Domain>("domains")
-    .where({ user_id: user.id })
-    .update({ user_id: null });
-
-  // 2. Create or update the domain with user's ID
-  let domain;
-
-  const updateDate: Partial<Domain> = {
-    address: data.address.toLowerCase(),
-    homepage: data.homepage && data.homepage.toLowerCase(),
-    user_id: user.id,
-    updated_at: new Date().toISOString()
-  };
-
-  if (matchedDomain) {
-    const [response]: Domain[] = await knex<Domain>("domains")
-      .where("id", matchedDomain.id)
-      .update(updateDate, "*");
-    domain = response;
-  } else {
-    const [response]: Domain[] = await knex<Domain>("domains").insert(
-      updateDate,
-      "*"
-    );
-    domain = response;
-  }
-
-  redis.del(getRedisKey.user(user.email));
-  redis.del(getRedisKey.user(user.apikey));
-  redis.del(getRedisKey.domain(updateDate.address));
-
-  return domain;
-};
-
-export const deleteDomain = async (user: UserJoined) => {
-  // Remove user from domain, do not actually delete the domain
-  const [domain]: Domain[] = await knex<Domain>("domains")
-    .where({ user_id: user.id })
-    .update({ user_id: null, updated_at: new Date().toISOString() }, "*");
-
-  if (domain) {
-    redis.del(getRedisKey.domain(domain.address));
-  }
-
-  redis.del(getRedisKey.user(user.email));
-  redis.del(getRedisKey.user(user.apikey));
-
-  return domain;
-};
-
-export const banDomain = async (
-  addressToban: string,
-  banned_by_id?: number
-): Promise<Domain> => {
-  const address = addressToban.toLowerCase();
-
-  const currentDomain = await getDomain({ address });
-
-  let domain;
-  if (currentDomain) {
-    const updates: Domain[] = await knex<Domain>("domains")
-      .where({ address })
-      .update(
-        { banned: true, banned_by_id, updated_at: new Date().toISOString() },
-        "*"
-      );
-    domain = updates[0];
-  } else {
-    const inserts: Domain[] = await knex<Domain>("domains").insert(
-      { address, banned: true, banned_by_id },
-      "*"
-    );
-    domain = inserts[0];
-  }
-
-  if (domain) {
-    redis.del(getRedisKey.domain(domain.address));
-  }
-
-  return domain;
-};

+ 0 - 51
server/__v1/db/host.ts

@@ -1,51 +0,0 @@
-import knex from "../../knex";
-import * as redis from "../../redis";
-import { getRedisKey } from "../../utils";
-
-export const getHost = async (data: Partial<Host>) => {
-  const getData = {
-    ...data,
-    ...(data.address && { address: data.address.toLowerCase() })
-  };
-
-  const redisKey = getRedisKey.host(getData.address);
-  const cachedHost = await redis.get(redisKey);
-
-  if (cachedHost) return JSON.parse(cachedHost);
-
-  const host = await knex<Host>("hosts")
-    .where(getData)
-    .first();
-
-  if (host) {
-    redis.set(redisKey, JSON.stringify(host), "EX", 60 * 60 * 6);
-  }
-
-  return host;
-};
-
-export const banHost = async (addressToBan: string, banned_by_id?: number) => {
-  const address = addressToBan.toLowerCase();
-
-  const currentHost = await knex<Host>("hosts")
-    .where({ address })
-    .first();
-
-  if (currentHost) {
-    await knex<Host>("hosts")
-      .where({ address })
-      .update({
-        banned: true,
-        banned_by_id,
-        updated_at: new Date().toISOString()
-      });
-  } else {
-    await knex<Host>("hosts").insert({ address, banned: true, banned_by_id });
-  }
-
-  if (currentHost) {
-    redis.del(getRedisKey.host(currentHost.address));
-  }
-
-  return currentHost;
-};

+ 0 - 47
server/__v1/db/ip.ts

@@ -1,47 +0,0 @@
-import { subMinutes } from "date-fns";
-
-import knex from "../../knex";
-import env from "../../env";
-
-export const addIP = async (ipToGet: string) => {
-  const ip = ipToGet.toLowerCase();
-
-  const currentIP = await knex<IP>("ips")
-    .where({ ip })
-    .first();
-
-  if (currentIP) {
-    const currentDate = new Date().toISOString();
-    await knex<IP>("ips")
-      .where({ ip })
-      .update({
-        created_at: currentDate,
-        updated_at: currentDate
-      });
-  } else {
-    await knex<IP>("ips").insert({ ip });
-  }
-
-  return ip;
-};
-export const getIP = async (ip: string) => {
-  const cooldownConfig = env.NON_USER_COOLDOWN;
-  const matchedIp = await knex<IP>("ips")
-    .where({ ip: ip.toLowerCase() })
-    .andWhere(
-      "created_at",
-      ">",
-      subMinutes(new Date(), cooldownConfig).toISOString()
-    )
-    .first();
-
-  return matchedIp;
-};
-export const clearIPs = async () =>
-  knex<IP>("ips")
-    .where(
-      "created_at",
-      "<",
-      subMinutes(new Date(), env.NON_USER_COOLDOWN).toISOString()
-    )
-    .delete();

+ 0 - 519
server/__v1/db/link.ts

@@ -1,519 +0,0 @@
-import bcrypt from "bcryptjs";
-import { isAfter, subDays, set } from "date-fns";
-import knex from "../../knex";
-import * as redis from "../../redis";
-import {
-  generateShortLink,
-  getRedisKey,
-  getUTCDate,
-  getDifferenceFunction,
-  statsObjectToArray
-} from "../../utils";
-import { banDomain } from "./domain";
-import { banHost } from "./host";
-import { banUser } from "./user";
-
-interface CreateLink extends Link {
-  reuse?: boolean;
-  domainName?: string;
-}
-
-export const createShortLink = async (data: CreateLink, user: UserJoined) => {
-  const { id: user_id = null, domain, domain_id = null } =
-    user || ({} as UserJoined);
-  let password;
-
-  if (data.password) {
-    const salt = await bcrypt.genSalt(12);
-    password = await bcrypt.hash(data.password, salt);
-  }
-
-  const [link]: Link[] = await knex<Link>("links").insert(
-    {
-      domain_id,
-      address: data.address,
-      password,
-      target: data.target,
-      user_id
-    },
-    "*"
-  );
-
-  return {
-    ...link,
-    password: !!data.password,
-    reuse: !!data.reuse,
-    shortLink: generateShortLink(data.address, domain),
-    shortUrl: generateShortLink(data.address, domain)
-  };
-};
-
-export const addLinkCount = async (id: number) => {
-  return knex<Link>("links")
-    .where({ id })
-    .increment("visit_count", 1);
-};
-
-interface ICreateVisit {
-  browser: string;
-  country: string;
-  domain?: string;
-  id: number;
-  os: string;
-  referrer: string;
-}
-
-export const createVisit = async (params: ICreateVisit) => {
-  const data = {
-    ...params,
-    country: params.country.toLowerCase(),
-    referrer: params.referrer.toLowerCase()
-  };
-
-  const visit = await knex<Visit>("visits")
-    .where({ link_id: params.id })
-    .andWhere(
-      knex.raw("date_trunc('hour', created_at) = date_trunc('hour', ?)", [
-        knex.fn.now()
-      ])
-    )
-    .first();
-
-  if (visit) {
-    await knex("visits")
-      .where({ id: visit.id })
-      .increment(`br_${data.browser}`, 1)
-      .increment(`os_${data.os}`, 1)
-      .increment("total", 1)
-      .update({
-        updated_at: new Date().toISOString(),
-        countries: knex.raw(
-          "jsonb_set(countries, '{??}', (COALESCE(countries->>?,'0')::int + 1)::text::jsonb)",
-          [data.country, data.country]
-        ),
-        referrers: knex.raw(
-          "jsonb_set(referrers, '{??}', (COALESCE(referrers->>?,'0')::int + 1)::text::jsonb)",
-          [data.referrer, data.referrer]
-        )
-      });
-  } else {
-    await knex<Visit>("visits").insert({
-      [`br_${data.browser}`]: 1,
-      countries: { [data.country]: 1 },
-      referrers: { [data.referrer]: 1 },
-      [`os_${data.os}`]: 1,
-      total: 1,
-      link_id: data.id
-    });
-  }
-
-  return visit;
-};
-
-interface IFindLink {
-  address?: string;
-  domain_id?: number | null;
-  user_id?: number | null;
-  target?: string;
-}
-
-export const findLink = async ({
-  address,
-  domain_id,
-  user_id,
-  target
-}: IFindLink): Promise<Link> => {
-  const redisKey = getRedisKey.link(address, domain_id, user_id);
-  const cachedLink = await redis.get(redisKey);
-
-  if (cachedLink) return JSON.parse(cachedLink);
-
-  const link = await knex<Link>("links")
-    .where({
-      ...(address && { address }),
-      ...(domain_id && { domain_id }),
-      ...(user_id && { user_id }),
-      ...(target && { target })
-    })
-    .first();
-
-  if (link) {
-    redis.set(redisKey, JSON.stringify(link), "EX", 60 * 60 * 2);
-  }
-
-  return link;
-};
-
-export const getUserLinksCount = async (params: {
-  user_id: number;
-  date?: Date;
-}) => {
-  const model = knex<Link>("links").where({ user_id: params.user_id });
-
-  // TODO: Test counts;
-  let res;
-  if (params.date) {
-    res = await model
-      .andWhere("created_at", ">", params.date.toISOString())
-      .count("id");
-  } else {
-    res = await model.count("id");
-  }
-
-  return res[0] && res[0].count;
-};
-
-interface IGetLinksOptions {
-  count?: string;
-  page?: string;
-  search?: string;
-}
-
-export const getLinks = async (
-  user_id: number,
-  options: IGetLinksOptions = {}
-) => {
-  const { count = "5", page = "1", search = "" } = options;
-  const limit = parseInt(count) < 50 ? parseInt(count) : 50;
-  const offset = (parseInt(page) - 1) * limit;
-
-  const model = knex<LinkJoinedDomain>("links")
-    .select(
-      "links.id",
-      "links.address",
-      "links.banned",
-      "links.created_at",
-      "links.domain_id",
-      "links.updated_at",
-      "links.password",
-      "links.target",
-      "links.visit_count",
-      "links.user_id",
-      "links.uuid",
-      "domains.address as domain"
-    )
-    .offset(offset)
-    .limit(limit)
-    .orderBy("created_at", "desc")
-    .where("links.user_id", user_id);
-
-  if (search) {
-    model.andWhereRaw("links.address || ' ' || target ILIKE '%' || ? || '%'", [
-      search
-    ]);
-  }
-
-  const matchedLinks = await model.leftJoin(
-    "domains",
-    "links.domain_id",
-    "domains.id"
-  );
-
-  const links = matchedLinks.map(link => ({
-    ...link,
-    id: link.address,
-    password: !!link.password,
-    shortLink: generateShortLink(link.address, link.domain),
-    shortUrl: generateShortLink(link.address, link.domain)
-  }));
-
-  return links;
-};
-
-interface IDeleteLink {
-  address: string;
-  user_id: number;
-  domain?: string;
-}
-
-export const deleteLink = async (data: IDeleteLink) => {
-  const link: LinkJoinedDomain = await knex<LinkJoinedDomain>("links")
-    .select("links.id", "domains.address as domain")
-    .where("links.address", data.address)
-    .where("links.user_id", data.user_id)
-    .where({
-      ...(!data.domain && { domain_id: null })
-    })
-    .leftJoin("domains", "links.domain_id", "domains.id")
-    .first();
-
-  if (!link) return;
-
-  if (link.domain !== data.domain) {
-    return;
-  }
-
-  await knex<Visit>("visits")
-    .where("link_id", link.id)
-    .delete();
-
-  const deletedLink = await knex<Link>("links")
-    .where("id", link.id)
-    .delete();
-
-  redis.del(getRedisKey.link(link.address, link.domain_id, link.user_id));
-
-  return !!deletedLink;
-};
-
-/*
- ** Collecting stats
- */
-
-interface StatsResult {
-  stats: {
-    browser: { name: string; value: number }[];
-    os: { name: string; value: number }[];
-    country: { name: string; value: number }[];
-    referrer: { name: string; value: number }[];
-  };
-  views: number[];
-}
-
-const getInitStats = (): Stats =>
-  Object.create({
-    browser: {
-      chrome: 0,
-      edge: 0,
-      firefox: 0,
-      ie: 0,
-      opera: 0,
-      other: 0,
-      safari: 0
-    },
-    os: {
-      android: 0,
-      ios: 0,
-      linux: 0,
-      macos: 0,
-      other: 0,
-      windows: 0
-    },
-    country: {},
-    referrer: {}
-  });
-
-const STATS_PERIODS: [number, "lastDay" | "lastWeek" | "lastMonth"][] = [
-  [1, "lastDay"],
-  [7, "lastWeek"],
-  [30, "lastMonth"]
-];
-
-interface IGetStatsResponse {
-  allTime: StatsResult;
-  id: string;
-  lastDay: StatsResult;
-  lastMonth: StatsResult;
-  lastWeek: StatsResult;
-  shortLink: string;
-  shortUrl: string;
-  target: string;
-  total: number;
-  updatedAt: string;
-}
-
-export const getStats = async (link: Link, domain: Domain) => {
-  const stats = {
-    lastDay: {
-      stats: getInitStats(),
-      views: new Array(24).fill(0)
-    },
-    lastWeek: {
-      stats: getInitStats(),
-      views: new Array(7).fill(0)
-    },
-    lastMonth: {
-      stats: getInitStats(),
-      views: new Array(30).fill(0)
-    },
-    allTime: {
-      stats: getInitStats(),
-      views: new Array(18).fill(0)
-    }
-  };
-
-  const visitsStream: any = knex<Visit>("visits")
-    .where("link_id", link.id)
-    .stream();
-  const nowUTC = getUTCDate();
-  const now = new Date();
-
-  for await (const visit of visitsStream as Visit[]) {
-    STATS_PERIODS.forEach(([days, type]) => {
-      const isIncluded = isAfter(
-        new Date(visit.created_at),
-        subDays(nowUTC, days)
-      );
-      if (isIncluded) {
-        const diffFunction = getDifferenceFunction(type);
-        const diff = diffFunction(now, visit.created_at);
-        const index = stats[type].views.length - diff - 1;
-        const view = stats[type].views[index];
-        const period = stats[type].stats;
-        stats[type].stats = {
-          browser: {
-            chrome: period.browser.chrome + visit.br_chrome,
-            edge: period.browser.edge + visit.br_edge,
-            firefox: period.browser.firefox + visit.br_firefox,
-            ie: period.browser.ie + visit.br_ie,
-            opera: period.browser.opera + visit.br_opera,
-            other: period.browser.other + visit.br_other,
-            safari: period.browser.safari + visit.br_safari
-          },
-          os: {
-            android: period.os.android + visit.os_android,
-            ios: period.os.ios + visit.os_ios,
-            linux: period.os.linux + visit.os_linux,
-            macos: period.os.macos + visit.os_macos,
-            other: period.os.other + visit.os_other,
-            windows: period.os.windows + visit.os_windows
-          },
-          country: {
-            ...period.country,
-            ...Object.entries(visit.countries).reduce(
-              (obj, [country, count]) => ({
-                ...obj,
-                [country]: (period.country[country] || 0) + count
-              }),
-              {}
-            )
-          },
-          referrer: {
-            ...period.referrer,
-            ...Object.entries(visit.referrers).reduce(
-              (obj, [referrer, count]) => ({
-                ...obj,
-                [referrer]: (period.referrer[referrer] || 0) + count
-              }),
-              {}
-            )
-          }
-        };
-        stats[type].views[index] = view + visit.total;
-      }
-    });
-
-    const allTime = stats.allTime.stats;
-    const diffFunction = getDifferenceFunction("allTime");
-    const diff = diffFunction(
-      set(new Date(), { date: 1 }),
-      set(new Date(visit.created_at), { date: 1 })
-    );
-    const index = stats.allTime.views.length - diff - 1;
-    const view = stats.allTime.views[index];
-    stats.allTime.stats = {
-      browser: {
-        chrome: allTime.browser.chrome + visit.br_chrome,
-        edge: allTime.browser.edge + visit.br_edge,
-        firefox: allTime.browser.firefox + visit.br_firefox,
-        ie: allTime.browser.ie + visit.br_ie,
-        opera: allTime.browser.opera + visit.br_opera,
-        other: allTime.browser.other + visit.br_other,
-        safari: allTime.browser.safari + visit.br_safari
-      },
-      os: {
-        android: allTime.os.android + visit.os_android,
-        ios: allTime.os.ios + visit.os_ios,
-        linux: allTime.os.linux + visit.os_linux,
-        macos: allTime.os.macos + visit.os_macos,
-        other: allTime.os.other + visit.os_other,
-        windows: allTime.os.windows + visit.os_windows
-      },
-      country: {
-        ...allTime.country,
-        ...Object.entries(visit.countries).reduce(
-          (obj, [country, count]) => ({
-            ...obj,
-            [country]: (allTime.country[country] || 0) + count
-          }),
-          {}
-        )
-      },
-      referrer: {
-        ...allTime.referrer,
-        ...Object.entries(visit.referrers).reduce(
-          (obj, [referrer, count]) => ({
-            ...obj,
-            [referrer]: (allTime.referrer[referrer] || 0) + count
-          }),
-          {}
-        )
-      }
-    };
-    stats.allTime.views[index] = view + visit.total;
-  }
-
-  const response: IGetStatsResponse = {
-    allTime: {
-      stats: statsObjectToArray(stats.allTime.stats),
-      views: stats.allTime.views
-    },
-    id: link.address,
-    lastDay: {
-      stats: statsObjectToArray(stats.lastDay.stats),
-      views: stats.lastDay.views
-    },
-    lastMonth: {
-      stats: statsObjectToArray(stats.lastMonth.stats),
-      views: stats.lastMonth.views
-    },
-    lastWeek: {
-      stats: statsObjectToArray(stats.lastWeek.stats),
-      views: stats.lastWeek.views
-    },
-    shortLink: generateShortLink(link.address, domain.address),
-    shortUrl: generateShortLink(link.address, domain.address),
-    target: link.target,
-    total: link.visit_count,
-    updatedAt: new Date().toISOString()
-  };
-  return response;
-};
-
-interface IBanLink {
-  adminId?: number;
-  banUser?: boolean;
-  domain?: string;
-  host?: string;
-  address: string;
-}
-
-export const banLink = async (data: IBanLink) => {
-  const tasks = [];
-  const banned_by_id = data.adminId;
-
-  // Ban link
-  const [link]: Link[] = await knex<Link>("links")
-    .where({ address: data.address, domain_id: null })
-    .update(
-      { banned: true, banned_by_id, updated_at: new Date().toISOString() },
-      "*"
-    );
-
-  if (!link) throw new Error("No link has been found.");
-
-  // If user, ban user and all of their links.
-  if (data.banUser && link.user_id) {
-    tasks.push(banUser(link.user_id, banned_by_id));
-    tasks.push(
-      knex<Link>("links")
-        .where({ user_id: link.user_id })
-        .update(
-          { banned: true, banned_by_id, updated_at: new Date().toISOString() },
-          "*"
-        )
-    );
-  }
-
-  // Ban host
-  if (data.host) tasks.push(banHost(data.host, banned_by_id));
-
-  // Ban domain
-  if (data.domain) tasks.push(banDomain(data.domain, banned_by_id));
-
-  redis.del(getRedisKey.link(link.address, link.domain_id, link.user_id));
-  redis.del(getRedisKey.link(link.address, link.domain_id));
-  redis.del(getRedisKey.link(link.address));
-
-  return Promise.all(tasks);
-};

+ 0 - 207
server/__v1/db/user.ts

@@ -1,207 +0,0 @@
-import bcrypt from "bcryptjs";
-import nanoid from "nanoid";
-import uuid from "uuid/v4";
-import { addMinutes } from "date-fns";
-
-import knex from "../../knex";
-import * as redis from "../../redis";
-import { getRedisKey } from "../../utils";
-
-export const getUser = async (emailOrKey = ""): Promise<User> => {
-  const redisKey = getRedisKey.user(emailOrKey);
-  const cachedUser = await redis.get(redisKey);
-
-  if (cachedUser) return JSON.parse(cachedUser);
-
-  const user = await knex<UserJoined>("users")
-    .select(
-      "users.id",
-      "users.apikey",
-      "users.banned",
-      "users.banned_by_id",
-      "users.cooldowns",
-      "users.created_at",
-      "users.email",
-      "users.password",
-      "users.updated_at",
-      "users.verified",
-      "domains.id as domain_id",
-      "domains.homepage as homepage",
-      "domains.address as domain"
-    )
-    .where("email", "ILIKE", emailOrKey)
-    .orWhere({ apikey: emailOrKey })
-    .leftJoin("domains", "users.id", "domains.user_id")
-    .first();
-
-  if (user) {
-    redis.set(redisKey, JSON.stringify(user), "EX", 60 * 60 * 1);
-  }
-
-  return user;
-};
-
-export const createUser = async (
-  emailToCreate: string,
-  password: string,
-  user?: User
-) => {
-  const email = emailToCreate.toLowerCase();
-  const salt = await bcrypt.genSalt(12);
-  const hashedPassword = await bcrypt.hash(password, salt);
-
-  const data = {
-    email,
-    password: hashedPassword,
-    verification_token: uuid(),
-    verification_expires: addMinutes(new Date(), 60).toISOString()
-  };
-
-  if (user) {
-    await knex<User>("users")
-      .where({ email })
-      .update({ ...data, updated_at: new Date().toISOString() });
-  } else {
-    await knex<User>("users").insert(data);
-  }
-
-  redis.del(getRedisKey.user(email));
-
-  return {
-    ...user,
-    ...data
-  };
-};
-
-export const verifyUser = async (verification_token: string) => {
-  const [user]: User[] = await knex<User>("users")
-    .where({ verification_token })
-    .andWhere("verification_expires", ">", new Date().toISOString())
-    .update(
-      {
-        verified: true,
-        verification_token: undefined,
-        verification_expires: undefined,
-        updated_at: new Date().toISOString()
-      },
-      "*"
-    );
-
-  if (user) {
-    redis.del(getRedisKey.user(user.email));
-  }
-
-  return user;
-};
-
-export const changePassword = async (id: number, newPassword: string) => {
-  const salt = await bcrypt.genSalt(12);
-  const password = await bcrypt.hash(newPassword, salt);
-
-  const [user]: User[] = await knex<User>("users")
-    .where({ id })
-    .update({ password, updated_at: new Date().toISOString() }, "*");
-
-  if (user) {
-    redis.del(getRedisKey.user(user.email));
-    redis.del(getRedisKey.user(user.apikey));
-  }
-
-  return user;
-};
-
-export const generateApiKey = async (id: number) => {
-  const apikey = nanoid(40);
-
-  const [user]: User[] = await knex<User>("users")
-    .where({ id })
-    .update({ apikey, updated_at: new Date().toISOString() }, "*");
-
-  if (user) {
-    redis.del(getRedisKey.user(user.email));
-    redis.del(getRedisKey.user(user.apikey));
-  }
-
-  return user && apikey;
-};
-
-export const requestPasswordReset = async (emailToMatch: string) => {
-  const email = emailToMatch.toLowerCase();
-  const reset_password_token = uuid();
-
-  const [user]: User[] = await knex<User>("users")
-    .where({ email })
-    .update(
-      {
-        reset_password_token,
-        reset_password_expires: addMinutes(new Date(), 30).toISOString(),
-        updated_at: new Date().toISOString()
-      },
-      "*"
-    );
-
-  if (user) {
-    redis.del(getRedisKey.user(user.email));
-    redis.del(getRedisKey.user(user.apikey));
-  }
-
-  return user;
-};
-
-export const resetPassword = async (reset_password_token: string) => {
-  const [user]: User[] = await knex<User>("users")
-    .where({ reset_password_token })
-    .andWhere("reset_password_expires", ">", new Date().toISOString())
-    .update(
-      {
-        reset_password_expires: null,
-        reset_password_token: null,
-        updated_at: new Date().toISOString()
-      },
-      "*"
-    );
-
-  if (user) {
-    redis.del(getRedisKey.user(user.email));
-    redis.del(getRedisKey.user(user.apikey));
-  }
-
-  return user;
-};
-
-export const addCooldown = async (id: number) => {
-  const [user]: User[] = await knex("users")
-    .where({ id })
-    .update(
-      {
-        cooldowns: knex.raw("array_append(cooldowns, ?)", [
-          new Date().toISOString()
-        ]),
-        updated_at: new Date().toISOString()
-      },
-      "*"
-    );
-
-  if (user) {
-    redis.del(getRedisKey.user(user.email));
-    redis.del(getRedisKey.user(user.apikey));
-  }
-
-  return user;
-};
-
-export const banUser = async (id: number, banned_by_id?: number) => {
-  const [user]: User[] = await knex<User>("users")
-    .where({ id })
-    .update(
-      { banned: true, banned_by_id, updated_at: new Date().toISOString() },
-      "*"
-    );
-
-  if (user) {
-    redis.del(getRedisKey.user(user.email));
-    redis.del(getRedisKey.user(user.apikey));
-  }
-
-  return user;
-};

+ 0 - 64
server/__v1/index.ts

@@ -1,64 +0,0 @@
-import asyncHandler from "express-async-handler";
-import { Router } from "express";
-import cors from "cors";
-
-import {
-  validateUrl,
-  ipCooldownCheck
-} from "./controllers/validateBodyController";
-import * as auth from "../handlers/auth";
-import * as link from "./controllers/linkController";
-import env from "../env";
-
-const router = Router();
-
-/* URL shortener */
-router.post(
-  "/url/submit",
-  cors(),
-  asyncHandler(auth.apikey),
-  asyncHandler(env.DISALLOW_ANONYMOUS_LINKS ? auth.jwt : auth.jwtLoose),
-  asyncHandler(auth.recaptcha),
-  asyncHandler(validateUrl),
-  asyncHandler(ipCooldownCheck),
-  asyncHandler(link.shortener)
-);
-router.post(
-  "/url/deleteurl",
-  asyncHandler(auth.apikey),
-  asyncHandler(auth.jwt),
-  asyncHandler(link.deleteUserLink)
-);
-router.get(
-  "/url/geturls",
-  asyncHandler(auth.apikey),
-  asyncHandler(auth.jwt),
-  asyncHandler(link.getUserLinks)
-);
-router.post(
-  "/url/customdomain",
-  asyncHandler(auth.jwt),
-  asyncHandler(link.setCustomDomain)
-);
-router.delete(
-  "/url/customdomain",
-  asyncHandler(auth.jwt),
-  asyncHandler(link.deleteCustomDomain)
-);
-router.get(
-  "/url/stats",
-  asyncHandler(auth.apikey),
-  asyncHandler(auth.jwt),
-  asyncHandler(link.getLinkStats)
-);
-router.post("/url/requesturl", asyncHandler(link.goToLink));
-router.post("/url/report", asyncHandler(link.reportLink));
-router.post(
-  "/url/admin/ban",
-  asyncHandler(auth.apikey),
-  asyncHandler(auth.jwt),
-  asyncHandler(auth.admin),
-  asyncHandler(link.ban)
-);
-
-export default router;

+ 0 - 3
server/env.ts

@@ -13,9 +13,6 @@ const env = cleanEnv(process.env, {
   DB_SSL: bool({ default: false }),
   DB_POOL_MIN: num({ default: 2 }),
   DB_POOL_MAX: num({ default: 10 }),
-  NEO4J_DB_URI: str({ default: "" }),
-  NEO4J_DB_USERNAME: str({ default: "" }),
-  NEO4J_DB_PASSWORD: str({ default: "" }),
   REDIS_HOST: str({ default: "127.0.0.1" }),
   REDIS_PORT: num({ default: 6379 }),
   REDIS_PASSWORD: str({ default: "" }),

+ 0 - 68
server/migration/01_host.ts

@@ -1,68 +0,0 @@
-import env from "../env";
-
-import { v1 as NEO4J } from "neo4j-driver";
-import knex from "knex";
-import PQueue from "p-queue";
-
-const queue = new PQueue({ concurrency: 10 });
-
-// 1. Connect to Neo4j database
-const neo4j = NEO4J.driver(
-  env.NEO4J_DB_URI,
-  NEO4J.auth.basic(env.NEO4J_DB_USERNAME, env.NEO4J_DB_PASSWORD)
-);
-// 2. Connect to Postgres database
-const postgres = knex({
-  client: "postgres",
-  connection: {
-    host: env.DB_HOST,
-    database: env.DB_NAME,
-    user: env.DB_USER,
-    password: env.DB_PASSWORD
-  }
-});
-
-(async function() {
-  const startTime = Date.now();
-
-  // 3. [NEO4J] Get all hosts
-  const session = neo4j.session();
-  session.run("MATCH (h:HOST) RETURN h").subscribe({
-    onNext(record) {
-      queue.add(async () => {
-        // 4. [Postgres] Upsert Hosts
-        const host = record.get("h").properties;
-        const address = host.name;
-        const banned = !!host.banned;
-        const exists = await postgres<Host>("hosts")
-          .where({
-            address
-          })
-          .first();
-        if (exists) {
-          await postgres<Host>("hosts")
-            .where("id", exists.id)
-            .update({ banned });
-        } else {
-          await postgres<Host>("hosts").insert({
-            address,
-            banned
-          });
-        }
-      });
-    },
-    onCompleted() {
-      session.close();
-      queue.add(() => {
-        const endTime = Date.now();
-        console.log(
-          `✅ Done! It took ${(endTime - startTime) / 1000} seconds.`
-        );
-      });
-    },
-    onError(error) {
-      session.close();
-      throw error;
-    }
-  });
-})();

+ 0 - 86
server/migration/02_users.ts

@@ -1,86 +0,0 @@
-import env from "../env";
-
-import { v1 as NEO4J } from "neo4j-driver";
-import PQueue from "p-queue";
-import knex from "knex";
-
-const queue = new PQueue({ concurrency: 10 });
-
-// 1. Connect to Neo4j database
-const neo4j = NEO4J.driver(
-  env.NEO4J_DB_URI,
-  NEO4J.auth.basic(env.NEO4J_DB_USERNAME, env.NEO4J_DB_PASSWORD)
-);
-// 2. Connect to Postgres database
-const postgres = knex({
-  client: "postgres",
-  connection: {
-    host: env.DB_HOST,
-    database: env.DB_NAME,
-    user: env.DB_USER,
-    password: env.DB_PASSWORD
-  }
-});
-
-(async function() {
-  const startTime = Date.now();
-
-  // 3. [NEO4J] Get all users
-  const session = neo4j.session();
-  session
-    .run(
-      "MATCH (u:USER) OPTIONAL MATCH (u)-[r:RECEIVED]->(c) WITH u, collect(c.date) as cooldowns RETURN u, cooldowns"
-    )
-    .subscribe({
-      onNext(record) {
-        queue.add(async () => {
-          // 4. [Postgres] Upsert users
-          const user = record.get("u").properties;
-          const cooldowns = record.get("cooldowns");
-
-          const email = user.email;
-          const password = user.password;
-          const verified = !!user.verified;
-          const banned = !!user.banned;
-          const apikey = user.apikey;
-          const created_at = user.createdAt;
-
-          const data = {
-            email,
-            password,
-            verified,
-            banned,
-            ...(apikey && { apikey }),
-            ...(created_at && { created_at }),
-            ...(cooldowns && cooldowns.length && { cooldowns })
-          };
-
-          const exists = await postgres<User>("users")
-            .where({
-              email
-            })
-            .first();
-          if (exists) {
-            await postgres<User>("users")
-              .where("id", exists.id)
-              .update(data);
-          } else {
-            await postgres<User>("users").insert(data);
-          }
-        });
-      },
-      onCompleted() {
-        session.close();
-        queue.add(() => {
-          const endTime = Date.now();
-          console.log(
-            `✅ Done! It took ${(endTime - startTime) / 1000} seconds.`
-          );
-        });
-      },
-      onError(error) {
-        session.close();
-        throw error;
-      }
-    });
-})();

+ 0 - 89
server/migration/03_domains.ts

@@ -1,89 +0,0 @@
-import env from "../env";
-
-import { v1 as NEO4J } from "neo4j-driver";
-import PQueue from "p-queue";
-import knex from "knex";
-
-const queue = new PQueue({ concurrency: 1 });
-
-// 1. Connect to Neo4j database
-const neo4j = NEO4J.driver(
-  env.NEO4J_DB_URI,
-  NEO4J.auth.basic(env.NEO4J_DB_USERNAME, env.NEO4J_DB_PASSWORD)
-);
-// 2. Connect to Postgres database
-const postgres = knex({
-  client: "postgres",
-  connection: {
-    host: env.DB_HOST,
-    database: env.DB_NAME,
-    user: env.DB_USER,
-    password: env.DB_PASSWORD
-  }
-});
-
-(async function() {
-  const startTime = Date.now();
-
-  // 3. [NEO4J] Get all domain
-  const session = neo4j.session();
-  session
-    .run(
-      "MATCH (d:DOMAIN) OPTIONAL MATCH (u)-[:OWNS]->(d) RETURN d as domain, u.email as email"
-    )
-    .subscribe({
-      onNext(record) {
-        queue.add(async () => {
-          const domain = record.get("domain").properties;
-          const email = record.get("email");
-
-          // 4. [Postgres] Get user ID
-          const user =
-            email &&
-            (await postgres<User>("users")
-              .where({ email })
-              .first());
-
-          // 5. [Postgres] Upsert domains
-          const banned = !!domain.banned;
-          const address = domain.name;
-          const homepage = domain.homepage;
-          const user_id = user ? user.id : null;
-
-          const data = {
-            banned,
-            address,
-            ...(homepage && { homepage }),
-            ...(user_id && { user_id })
-          };
-
-          const exists = await postgres<Domain>("domains")
-            .where({
-              address
-            })
-            .first();
-          if (exists) {
-            await postgres<Domain>("domains")
-              .where("id", exists.id)
-              .update(data);
-          } else {
-            await postgres<Domain>("domains").insert(data);
-          }
-        });
-      },
-      onCompleted() {
-        session.close();
-        queue.add(() => {
-          const endTime = Date.now();
-          console.log(
-            `✅ Done! It took ${(endTime - startTime) / 1000} seconds.`
-          );
-        });
-      },
-      onError(error) {
-        console.log(error);
-        session.close();
-        throw error;
-      }
-    });
-})();

+ 0 - 196
server/migration/04_links.ts

@@ -1,196 +0,0 @@
-import env from "../env";
-
-import { v1 as NEO4J } from "neo4j-driver";
-import { startOfHour } from "date-fns";
-import PQueue from "p-queue";
-import knex from "knex";
-
-let count = 0;
-const queue = new PQueue({ concurrency: 5 });
-
-queue.on("active", () => (count % 1000 === 0 ? console.log(count++) : count++));
-
-// 1. Connect to Neo4j database
-const neo4j = NEO4J.driver(
-  env.NEO4J_DB_URI,
-  NEO4J.auth.basic(env.NEO4J_DB_USERNAME, env.NEO4J_DB_PASSWORD)
-);
-
-// 2. Connect to Postgres database
-const postgres = knex({
-  client: "postgres",
-  connection: {
-    host: env.DB_HOST,
-    database: env.DB_NAME,
-    user: env.DB_USER,
-    password: env.DB_PASSWORD
-  }
-});
-
-(async function() {
-  const startTime = Date.now();
-
-  // 3. [NEO4J] Get all links
-  const session = neo4j.session();
-  const { records } = await session.run(
-    "MATCH (l:URL) WITH COUNT(l) as count RETURN count"
-  );
-  const total = records[0].get("count").toNumber();
-  const limit = 20000;
-
-  function main(index = 0) {
-    queue.add(
-      () =>
-        new Promise((resolve, reject) => {
-          session
-            .run(
-              "MATCH (l:URL) WITH l SKIP $skip LIMIT $limit " +
-                "OPTIONAL MATCH (l)-[:USES]->(d) " +
-                "OPTIONAL MATCH (l)<-[:CREATED]-(u) " +
-                "OPTIONAL MATCH (v)-[:VISITED]->(l) " +
-                "OPTIONAL MATCH (v)-[:BROWSED_BY]->(b) " +
-                "OPTIONAL MATCH (v)-[:OS]->(o) " +
-                "OPTIONAL MATCH (v)-[:LOCATED_IN]->(c) " +
-                "OPTIONAL MATCH (v)-[:REFERRED_BY]->(r) " +
-                "OPTIONAL MATCH (v)-[:VISITED_IN]->(dd) " +
-                "WITH l, u, d, COLLECT([b.browser, o.os, c.country, r.referrer, dd.date]) as stats " +
-                "RETURN l, u.email as email, d.name as domain, stats",
-              { limit: limit, skip: index * limit }
-            )
-            .subscribe({
-              onNext(record) {
-                queue.add(async () => {
-                  const link = record.get("l").properties;
-                  const email = record.get("email");
-                  const address = record.get("domain");
-                  const stats = record.get("stats");
-
-                  // 4. Merge and normalize stats based on hour
-                  const visits: Record<
-                    string,
-                    Record<string, number | Record<string, number>>
-                  > = {} as any;
-
-                  stats.forEach(([b, o, country, referrer, date]) => {
-                    if (b && o && country && referrer && date) {
-                      const dateHour = startOfHour(
-                        new Date(date)
-                      ).toISOString();
-                      const browser = b.toLowerCase();
-                      const os = o === "Mac Os X" ? "macos" : o.toLowerCase();
-                      visits[dateHour] = {
-                        ...visits[dateHour],
-                        total:
-                          (((visits[dateHour] &&
-                            visits[dateHour].total) as number) || 0) + 1,
-                        [`br_${browser}`]:
-                          (((visits[dateHour] &&
-                            visits[dateHour][`br_${browser}`]) as number) ||
-                            0) + 1,
-                        [`os_${os}`]:
-                          (((visits[dateHour] &&
-                            visits[dateHour][`os_${os}`]) as number) || 0) + 1,
-                        countries: {
-                          ...((visits[dateHour] || {}).countries as {}),
-                          [country.toLowerCase()]:
-                            ((visits[dateHour] &&
-                              visits[dateHour].countries[
-                                country.toLowerCase()
-                              ]) ||
-                              0) + 1
-                        },
-                        referrers: {
-                          ...((visits[dateHour] || {}).referrers as {}),
-                          [referrer.toLowerCase()]:
-                            ((visits[dateHour] &&
-                              visits[dateHour].referrers[
-                                referrer.toLowerCase()
-                              ]) ||
-                              0) + 1
-                        }
-                      };
-                    }
-                  });
-
-                  // 5. [Postgres] Find matching user and or domain
-                  const [user, domain] = await Promise.all([
-                    email &&
-                      postgres<User>("users")
-                        .where({ email })
-                        .first(),
-                    address &&
-                      postgres<Domain>("domains")
-                        .where({ address })
-                        .first()
-                  ]);
-
-                  // 6. [Postgres] Create link
-                  const data = {
-                    address: link.id,
-                    banned: !!link.banned,
-                    domain_id: domain ? domain.id : null,
-                    password: link.password,
-                    target: link.target,
-                    user_id: user ? user.id : null,
-                    ...(link.count && { visit_count: link.count.toNumber() }),
-                    ...(link.createdAt && { created_at: link.createdAt })
-                  };
-
-                  const res = await postgres<Link>("links").insert(data, "id");
-                  const link_id = res[0];
-
-                  // 7. [Postgres] Create visits
-                  const newVisits = Object.entries(visits).map(
-                    ([date, details]) => ({
-                      link_id,
-                      created_at: date,
-                      countries: details.countries as Record<string, number>,
-                      referrers: details.referrers as Record<string, number>,
-                      total: details.total as number,
-                      br_chrome: details.br_chrome as number,
-                      br_edge: details.br_edge as number,
-                      br_firefox: details.br_firefox as number,
-                      br_ie: details.br_ie as number,
-                      br_opera: details.br_opera as number,
-                      br_other: details.br_other as number,
-                      br_safari: details.br_safari as number,
-                      os_android: details.os_android as number,
-                      os_ios: details.os_ios as number,
-                      os_linux: details.os_linux as number,
-                      os_macos: details.os_macos as number,
-                      os_other: details.os_other as number,
-                      os_windows: details.os_windows as number
-                    })
-                  );
-
-                  await postgres<Visit>("visits").insert(newVisits);
-                });
-              },
-              onCompleted() {
-                session.close();
-                if ((index + 1) * limit < total) {
-                  queue.add(() => main(index + 1));
-                } else {
-                  queue.add(() => {
-                    const endTime = Date.now();
-                    console.log(
-                      `✅ Done! It took ${(endTime - startTime) /
-                        1000} seconds.`
-                    );
-                  });
-                }
-                resolve(null);
-              },
-              onError(error) {
-                session.close();
-                if ((index + 1) * limit < total) {
-                  queue.add(() => main(index + 1));
-                }
-                reject(error);
-              }
-            });
-        })
-    );
-  }
-  main();
-})();

+ 0 - 62
server/migration/neo4j_delete_duplicated.ts

@@ -1,62 +0,0 @@
-import env from "../env";
-
-import { v1 as NEO4J } from "neo4j-driver";
-import PQueue from "p-queue";
-
-let count = 0;
-const queue = new PQueue({ concurrency: 1 });
-queue.on("active", () => console.log(count++));
-
-// 1. Connect to Neo4j database
-const neo4j = NEO4J.driver(
-  env.NEO4J_DB_URI,
-  NEO4J.auth.basic(env.NEO4J_DB_USERNAME, env.NEO4J_DB_PASSWORD)
-);
-
-(async function() {
-  const startTime = Date.now();
-
-  const nodes = [
-    ["VISITED_IN", "DATE"]
-    // ['BROWSED_BY', 'BROWSER'],
-    // ['OS', 'OS'],
-    // ['LOCATED_IN', 'COUNTRY'],
-    // ['REFERRED_BY', 'REFERRER'],
-  ];
-
-  // 3. [NEO4J] Get all hosts
-  const session = neo4j.session();
-  const { records } = await session.run(
-    "MATCH (v:VISIT) WITH COUNT(v) as count RETURN count;"
-  );
-  const total = records[0].get("count").toNumber();
-  const limit = 100000;
-
-  function main(index = 0) {
-    nodes.forEach(([r, n]) => {
-      queue.add(() => {
-        return session.run(`
-          MATCH (a:VISIT)-[r:${r}]->(b:${n})
-          WITH a, r, b SKIP ${index * limit} LIMIT ${limit}
-          WITH a, b, TYPE(r) AS t, COLLECT(r) AS rr
-          WHERE SIZE(rr) > 1
-          WITH rr
-          FOREACH (r IN TAIL(rr) | DELETE r);
-        `);
-      });
-    });
-
-    if ((index + 1) * limit < total) {
-      main(index + 1);
-    } else {
-      queue.add(() => {
-        const endTime = Date.now();
-        console.log(
-          `✅ Done! It took ${(endTime - startTime) / 1000} seconds.`
-        );
-      });
-    }
-  }
-
-  main();
-})();

+ 0 - 2
server/server.ts

@@ -11,7 +11,6 @@ import nextApp from "next";
 import * as helpers from "./handlers/helpers";
 import * as links from "./handlers/links";
 import * as auth from "./handlers/auth";
-import __v1Routes from "./__v1";
 import routes from "./routes";
 import { stream } from "./config/winston";
 
@@ -42,7 +41,6 @@ app.prepare().then(async () => {
   server.use(asyncHandler(links.redirectCustomDomain));
 
   server.use("/api/v2", routes);
-  server.use("/api", __v1Routes);
 
   server.get(
     "/reset-password/:resetPasswordToken?",