const { differenceInSeconds } = require("date-fns"); const promisify = require("node:util").promisify; const bcrypt = require("bcryptjs"); const { isbot } = require("isbot"); const URL = require("node:url"); const dns = require("node: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 }) ]); if (req.isHTML) { res.render("partials/links/table", { total, limit, skip, links: data.map(utils.sanitize.link_html), }) return; } return res.send({ total, limit, skip, data: data.map(utils.sanitize.link), }); }; 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([ 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[0]) { return res.json(utils.sanitize.link(tasks[0])); } // Check if custom link already exists if (tasks[1]) { const error = "Custom URL is already in use."; res.locals.errors = { customurl: error }; throw new CustomError(error); } // Create new link const address = customurl || tasks[2]; 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([ address && query.link.find({ address, domain_id }), target && validators.bannedDomain(targetDomain), target && validators.bannedHost(targetDomain) ]); // Check if custom link already exists if (tasks[0]) { 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_html({ ...updatedLink }), }); return; } return res.status(200).send(utils.sanitize.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([ address && query.link.find({ address, domain_id }), target && validators.bannedDomain(targetDomain), target && validators.bannedHost(targetDomain) ]); // Check if custom link already exists if (tasks[0]) { 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.link_admin({ ...updatedLink }), }); return; } return res.status(200).send(utils.sanitize.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) { if ("authorization" in req.headers) { const auth = req.headers.authorization; const firstSpace = auth.indexOf(" "); if (firstSpace !== -1) { const method = auth.slice(0, firstSpace); const payload = auth.slice(firstSpace + 1); if (method === "Basic") { const decoded = Buffer.from(payload, "base64").toString("utf8"); const colon = decoded.indexOf(":"); if (colon !== -1) { const password = decoded.slice(colon + 1); const matches = await bcrypt.compare(password, link.password); if (matches) return res.redirect(link.target); } } } } 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_html(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, }