| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653 |
- const { differenceInSeconds } = require("date-fns");
- const promisify = require("util").promisify;
- const bcrypt = require("bcryptjs");
- const { isbot } = require("isbot");
- const URL = require("url");
- const dns = require("dns");
- const validators = require("./validators.handler");
- const map = require("../utils/map.json");
- const transporter = require("../mail");
- const query = require("../queries");
- const queue = require("../queues");
- const utils = require("../utils");
- const env = require("../env");
- const CustomError = utils.CustomError;
- const dnsLookup = promisify(dns.lookup);
- async function get(req, res) {
- const { limit, skip } = req.context;
- const search = req.query.search;
- const userId = req.user.id;
- const match = {
- user_id: userId
- };
- const [data, total] = await Promise.all([
- query.link.get(match, { limit, search, skip }),
- query.link.total(match, { search })
- ]);
- const links = data.map(utils.sanitize.link);
- if (req.isHTML) {
- res.render("partials/links/table", {
- total,
- limit,
- skip,
- links,
- })
- return;
- }
- return res.send({
- total,
- limit,
- skip,
- data: links,
- });
- };
- async function getAdmin(req, res) {
- const { limit, skip } = req.context;
- const search = req.query.search;
- const user = req.query.user;
- let domain = req.query.domain;
- const banned = utils.parseBooleanQuery(req.query.banned);
- const anonymous = utils.parseBooleanQuery(req.query.anonymous);
- const has_domain = utils.parseBooleanQuery(req.query.has_domain);
-
- const match = {
- ...(banned !== undefined && { banned }),
- ...(anonymous !== undefined && { user_id: [anonymous ? "is" : "is not", null] }),
- ...(has_domain !== undefined && { domain_id: [has_domain ? "is not" : "is", null] }),
- };
-
- // if domain is equal to the defualt domain,
- // it means admins is looking for links with the defualt domain (no custom user domain)
- if (domain === env.DEFAULT_DOMAIN) {
- domain = undefined;
- match.domain_id = null;
- }
-
- const [data, total] = await Promise.all([
- query.link.getAdmin(match, { limit, search, user, domain, skip }),
- query.link.totalAdmin(match, { search, user, domain })
- ]);
- const links = data.map(utils.sanitize.link_admin);
- if (req.isHTML) {
- res.render("partials/admin/links/table", {
- total,
- total_formatted: total.toLocaleString("en-US"),
- limit,
- skip,
- links,
- })
- return;
- }
- return res.send({
- total,
- limit,
- skip,
- data: links,
- });
- };
- async function create(req, res) {
- const { reuse, password, customurl, description, target, fetched_domain, expire_in } = req.body;
- const domain_id = fetched_domain ? fetched_domain.id : null;
-
- const targetDomain = utils.removeWww(URL.parse(target).hostname);
-
- const tasks = await Promise.all([
- validators.cooldown(req.user),
- validators.malware(req.user, target),
- reuse &&
- query.link.find({
- target,
- user_id: req.user.id,
- domain_id
- }),
- customurl &&
- query.link.find({
- address: customurl,
- domain_id
- }),
- !customurl && utils.generateId(query, domain_id),
- validators.bannedDomain(targetDomain),
- validators.bannedHost(targetDomain)
- ]);
-
- // if "reuse" is true, try to return
- // the existent URL without creating one
- if (tasks[2]) {
- return res.json(utils.sanitize.link(tasks[2]));
- }
-
- // Check if custom link already exists
- if (tasks[3]) {
- const error = "Custom URL is already in use.";
- res.locals.errors = { customurl: error };
- throw new CustomError(error);
- }
- // Create new link
- const address = customurl || tasks[4];
- const link = await query.link.create({
- password,
- address,
- domain_id,
- description,
- target,
- expire_in,
- user_id: req.user && req.user.id
- });
- link.domain = fetched_domain?.address;
-
- if (req.isHTML) {
- res.setHeader("HX-Trigger", "reloadMainTable");
- const shortURL = utils.getShortURL(link.address, link.domain);
- return res.render("partials/shortener", {
- link: shortURL.link,
- url: shortURL.url,
- });
- }
-
- return res
- .status(201)
- .send(utils.sanitize.link({ ...link }));
- }
- async function edit(req, res) {
- const link = await query.link.find({
- uuid: req.params.id,
- ...(!req.user.admin && { user_id: req.user.id })
- });
- if (!link) {
- throw new CustomError("Link was not found.");
- }
- let isChanged = false;
- [
- [req.body.address, "address"],
- [req.body.target, "target"],
- [req.body.description, "description"],
- [req.body.expire_in, "expire_in"],
- [req.body.password, "password"]
- ].forEach(([value, name]) => {
- if (!value) {
- if (name === "password" && link.password)
- req.body.password = null;
- else {
- delete req.body[name];
- return;
- }
- }
- if (value === link[name] && name !== "password") {
- delete req.body[name];
- return;
- }
- if (name === "expire_in" && link.expire_in)
- if (Math.abs(differenceInSeconds(utils.parseDatetime(value), utils.parseDatetime(link.expire_in))) < 60)
- return;
- if (name === "password")
- if (value && value.replace(/•/ig, "").length === 0) {
- delete req.body.password;
- return;
- }
- isChanged = true;
- });
- if (!isChanged) {
- throw new CustomError("Should at least update one field.");
- }
- const { address, target, description, expire_in, password } = req.body;
-
- const targetDomain = target && utils.removeWww(URL.parse(target).hostname);
- const domain_id = link.domain_id || null;
- const tasks = await Promise.all([
- validators.cooldown(req.user),
- target && validators.malware(req.user, target),
- address &&
- query.link.find({
- address,
- domain_id
- }),
- target && validators.bannedDomain(targetDomain),
- target && validators.bannedHost(targetDomain)
- ]);
- // Check if custom link already exists
- if (tasks[2]) {
- const error = "Custom URL is already in use.";
- res.locals.errors = { address: error };
- throw new CustomError("Custom URL is already in use.");
- }
- // Update link
- const [updatedLink] = await query.link.update(
- {
- id: link.id
- },
- {
- ...(address && { address }),
- ...(description && { description }),
- ...(target && { target }),
- ...(expire_in && { expire_in }),
- ...((password || password === null) && { password })
- }
- );
- if (req.isHTML) {
- res.render("partials/links/edit", {
- swap_oob: true,
- success: "Link has been updated.",
- ...utils.sanitize.link({ ...link, ...updatedLink }),
- });
- return;
- }
- return res.status(200).send(utils.sanitize.link({ ...link, ...updatedLink }));
- };
- async function editAdmin(req, res) {
- const link = await query.link.find({
- uuid: req.params.id,
- ...(!req.user.admin && { user_id: req.user.id })
- });
- if (!link) {
- throw new CustomError("Link was not found.");
- }
- let isChanged = false;
- [
- [req.body.address, "address"],
- [req.body.target, "target"],
- [req.body.description, "description"],
- [req.body.expire_in, "expire_in"],
- [req.body.password, "password"]
- ].forEach(([value, name]) => {
- if (!value) {
- if (name === "password" && link.password)
- req.body.password = null;
- else {
- delete req.body[name];
- return;
- }
- }
- if (value === link[name] && name !== "password") {
- delete req.body[name];
- return;
- }
- if (name === "expire_in" && link.expire_in)
- if (Math.abs(differenceInSeconds(utils.parseDatetime(value), utils.parseDatetime(link.expire_in))) < 60)
- return;
- if (name === "password")
- if (value && value.replace(/•/ig, "").length === 0) {
- delete req.body.password;
- return;
- }
- isChanged = true;
- });
- if (!isChanged) {
- throw new CustomError("Should at least update one field.");
- }
- const { address, target, description, expire_in, password } = req.body;
-
- const targetDomain = target && utils.removeWww(URL.parse(target).hostname);
- const domain_id = link.domain_id || null;
- const tasks = await Promise.all([
- validators.cooldown(req.user),
- target && validators.malware(req.user, target),
- address &&
- query.link.find({
- address,
- domain_id
- }),
- target && validators.bannedDomain(targetDomain),
- target && validators.bannedHost(targetDomain)
- ]);
- // Check if custom link already exists
- if (tasks[2]) {
- const error = "Custom URL is already in use.";
- res.locals.errors = { address: error };
- throw new CustomError("Custom URL is already in use.");
- }
- // Update link
- const [updatedLink] = await query.link.update(
- {
- id: link.id
- },
- {
- ...(address && { address }),
- ...(description && { description }),
- ...(target && { target }),
- ...(expire_in && { expire_in }),
- ...((password || password === null) && { password })
- }
- );
- if (req.isHTML) {
- res.render("partials/admin/links/edit", {
- swap_oob: true,
- success: "Link has been updated.",
- ...utils.sanitize.linkAdmin({ ...link, ...updatedLink }),
- });
- return;
- }
- return res.status(200).send(utils.sanitize.link({ ...link, ...updatedLink }));
- };
- async function remove(req, res) {
- const { error, isRemoved, link } = await query.link.remove({
- uuid: req.params.id,
- ...(!req.user.admin && { user_id: req.user.id })
- });
- if (!isRemoved) {
- const messsage = error || "Could not delete the link.";
- throw new CustomError(messsage);
- }
- if (req.isHTML) {
- res.setHeader("HX-Reswap", "outerHTML");
- res.setHeader("HX-Trigger", "reloadMainTable");
- res.render("partials/links/dialog/delete_success", {
- link: utils.getShortURL(link.address, link.domain).link,
- });
- return;
- }
- return res
- .status(200)
- .send({ message: "Link has been deleted successfully." });
- };
- async function report(req, res) {
- const { link } = req.body;
- await transporter.sendReportEmail(link);
- if (req.isHTML) {
- res.render("partials/report/form", {
- message: "Report was received. We'll take actions shortly."
- });
- return;
- }
-
- return res
- .status(200)
- .send({ message: "Thanks for the report, we'll take actions shortly." });
- };
- async function ban(req, res) {
- const { id } = req.params;
- const update = {
- banned_by_id: req.user.id,
- banned: true
- };
- // 1. check if link exists
- const link = await query.link.find({ uuid: id });
- if (!link) {
- throw new CustomError("No link has been found.", 400);
- }
- if (link.banned) {
- throw new CustomError("Link has been banned already.", 400);
- }
- const tasks = [];
- // 2. ban link
- tasks.push(query.link.update({ uuid: id }, update));
- const domain = utils.removeWww(URL.parse(link.target).hostname);
- // 3. ban target's domain
- if (req.body.domain) {
- tasks.push(query.domain.add({ ...update, address: domain }));
- }
- // 4. ban target's host
- if (req.body.host) {
- const dnsRes = await dnsLookup(domain).catch(() => {
- throw new CustomError("Couldn't fetch DNS info.");
- });
- const host = dnsRes?.address;
- tasks.push(query.host.add({ ...update, address: host }));
- }
- // 5. ban link owner
- if (req.body.user && link.user_id) {
- tasks.push(query.user.update({ id: link.user_id }, update));
- }
- // 6. ban all of owner's links
- if (req.body.userLinks && link.user_id) {
- tasks.push(query.link.update({ user_id: link.user_id }, update));
- }
- // 7. wait for all tasks to finish
- await Promise.all(tasks).catch((err) => {
- throw new CustomError("Couldn't ban entries.");
- });
- // 8. send response
- if (req.isHTML) {
- res.setHeader("HX-Reswap", "outerHTML");
- res.setHeader("HX-Trigger", "reloadMainTable");
- res.render("partials/links/dialog/ban_success", {
- link: utils.getShortURL(link.address, link.domain).link,
- });
- return;
- }
- return res.status(200).send({ message: "Banned link successfully." });
- };
- async function redirect(req, res, next) {
- const isPreservedUrl = utils.preservedURLs.some(
- item => item === req.path.replace("/", "")
- );
- if (isPreservedUrl) return next();
- // 1. If custom domain, get domain info
- const host = utils.removeWww(req.headers.host);
- const domain =
- host !== env.DEFAULT_DOMAIN
- ? await query.domain.find({ address: host })
- : null;
- // 2. Get link
- const address = req.params.id.replace("+", "");
- const link = await query.link.find({
- address,
- domain_id: domain ? domain.id : null
- });
- // 3. When no link, if has domain redirect to domain's homepage
- // otherwise redirect to 404
- if (!link) {
- return res.redirect(domain?.homepage || "/404");
- }
- // 4. If link is banned, redirect to banned page.
- if (link.banned) {
- return res.redirect("/banned");
- }
- // 5. If wants to see link info, then redirect
- const isRequestingInfo = /.*\+$/gi.test(req.params.id);
- if (isRequestingInfo && !link.password) {
- if (req.isHTML) {
- res.render("url_info", {
- title: "Short link information",
- target: link.target,
- link: utils.getShortURL(link.address, link.domain).link
- });
- return;
- }
- return res.send({ target: link.target });
- }
- // 6. If link is protected, redirect to password page
- if (link.password) {
- res.render("protected", {
- title: "Protected short link",
- id: link.uuid
- });
- return;
- }
- // 7. Create link visit
- const isBot = isbot(req.headers["user-agent"]);
- if (link.user_id && !isBot) {
- queue.visit.add({
- userAgent: req.headers["user-agent"],
- ip: req.ip,
- country: req.get("cf-ipcountry"),
- referrer: req.get("Referrer"),
- link
- });
- }
- // 8. Redirect to target
- return res.redirect(link.target);
- };
- async function redirectProtected(req, res) {
- // 1. Get link
- const uuid = req.params.id;
- const link = await query.link.find({ uuid });
- // 2. Throw error if no link
- if (!link || !link.password) {
- throw new CustomError("Couldn't find the link.", 400);
- }
- // 3. Check if password matches
- const matches = await bcrypt.compare(req.body.password, link.password);
- if (!matches) {
- throw new CustomError("Password is not correct.", 401);
- }
- // 4. Create visit
- if (link.user_id) {
- queue.visit.add({
- userAgent: req.headers["user-agent"],
- ip: req.ip,
- country: req.get("cf-ipcountry"),
- referrer: req.get("Referrer"),
- link
- });
- }
- // 5. Send target
- if (req.isHTML) {
- res.setHeader("HX-Redirect", link.target);
- res.render("partials/protected/form", {
- id: link.uuid,
- message: "Redirecting...",
- });
- return;
- }
- return res.status(200).send({ target: link.target });
- };
- async function redirectCustomDomainHomepage(req, res, next) {
- const host = utils.removeWww(req.headers.host);
- if (host === env.DEFAULT_DOMAIN) {
- next();
- return;
- }
- const path = req.path;
- const pathName = path.replace("/", "").split("/")[0];
- if (
- path === "/" ||
- utils.preservedURLs.includes(pathName)
- ) {
- const domain = await query.domain.find({ address: host });
- if (domain?.homepage) {
- res.redirect(302, domain.homepage);
- return;
- }
- }
- next();
- };
- async function stats(req, res) {
- const { user } = req;
- const uuid = req.params.id;
- const link = await query.link.find({
- ...(!user.admin && { user_id: user.id }),
- uuid
- });
- if (!link) {
- if (req.isHTML) {
- res.setHeader("HX-Redirect", "/404");
- res.status(200).send("");
- return;
- }
- throw new CustomError("Link could not be found.");
- }
- const stats = await query.visit.find({ link_id: link.id }, link.visit_count);
- if (!stats) {
- throw new CustomError("Could not get the short link stats. Try again later.");
- }
- if (req.isHTML) {
- res.render("partials/stats", {
- link: utils.sanitize.link(link),
- stats,
- map,
- });
- return;
- }
- return res.status(200).send({
- ...stats,
- ...utils.sanitize.link(link)
- });
- };
- module.exports = {
- ban,
- create,
- edit,
- editAdmin,
- get,
- getAdmin,
- remove,
- report,
- stats,
- redirect,
- redirectProtected,
- redirectCustomDomainHomepage,
- }
|