links.handler.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. const promisify = require("util").promisify;
  2. const bcrypt = require("bcryptjs");
  3. const isbot = require("isbot");
  4. const URL = require("url");
  5. const dns = require("dns");
  6. const validators = require("./validators.handler");
  7. // const transporter = require("../mail");
  8. const query = require("../queries");
  9. // const queue = require("../queues");
  10. const utils = require("../utils");
  11. const env = require("../env");
  12. const { differenceInSeconds } = require("date-fns");
  13. const CustomError = utils.CustomError;
  14. const dnsLookup = promisify(dns.lookup);
  15. /**
  16. * @type {import("express").Handler}
  17. */
  18. async function get(req, res) {
  19. const { limit, skip, all } = req.context;
  20. const search = req.query.search;
  21. const userId = req.user.id;
  22. const match = {
  23. ...(!all && { user_id: userId })
  24. };
  25. const [data, total] = await Promise.all([
  26. query.link.get(match, { limit, search, skip }),
  27. query.link.total(match, { search })
  28. ]);
  29. const links = data.map(utils.sanitize.link);
  30. await utils.sleep(1000);
  31. if (req.isHTML) {
  32. res.render("partials/links/table", {
  33. total,
  34. limit,
  35. skip,
  36. links,
  37. })
  38. return;
  39. }
  40. return res.send({
  41. total,
  42. limit,
  43. skip,
  44. data: links,
  45. });
  46. };
  47. /**
  48. * @type {import("express").Handler}
  49. */
  50. async function create(req, res) {
  51. const { reuse, password, customurl, description, target, fetched_domain, expire_in } = req.body;
  52. const domain_id = fetched_domain ? fetched_domain.id : null;
  53. const targetDomain = utils.removeWww(URL.parse(target).hostname);
  54. const queries = await Promise.all([
  55. validators.cooldown(req.user),
  56. validators.malware(req.user, target),
  57. validators.linksCount(req.user),
  58. reuse &&
  59. query.link.find({
  60. target,
  61. user_id: req.user.id,
  62. domain_id
  63. }),
  64. customurl &&
  65. query.link.find({
  66. address: customurl,
  67. domain_id
  68. }),
  69. !customurl && utils.generateId(domain_id),
  70. validators.bannedDomain(targetDomain),
  71. validators.bannedHost(targetDomain)
  72. ]);
  73. // if "reuse" is true, try to return
  74. // the existent URL without creating one
  75. if (queries[3]) {
  76. return res.json(utils.sanitize.link(queries[3]));
  77. }
  78. // Check if custom link already exists
  79. if (queries[4]) {
  80. const error = "Custom URL is already in use.";
  81. res.locals.errors = { customurl: error };
  82. throw new CustomError(error);
  83. }
  84. // Create new link
  85. const address = customurl || queries[5];
  86. const link = await query.link.create({
  87. password,
  88. address,
  89. domain_id,
  90. description,
  91. target,
  92. expire_in,
  93. user_id: req.user && req.user.id
  94. });
  95. if (!req.user && env.NON_USER_COOLDOWN) {
  96. query.ip.add(req.realIP);
  97. }
  98. link.domain = fetched_domain?.address;
  99. if (req.isHTML) {
  100. res.setHeader("HX-Trigger", "reloadLinks");
  101. const shortURL = utils.getShortURL(link.address, link.domain);
  102. return res.render("partials/shortener", {
  103. link: shortURL.link,
  104. url: shortURL.url,
  105. });
  106. }
  107. return res
  108. .status(201)
  109. .send(utils.sanitize.link({ ...link }));
  110. }
  111. async function edit(req, res) {
  112. const { address, target, description, expire_in, password } = req.body;
  113. const link = await query.link.find({
  114. uuid: req.params.id,
  115. ...(!req.user.admin && { user_id: req.user.id })
  116. });
  117. if (!link) {
  118. throw new CustomError("Link was not found.");
  119. }
  120. let isChanged = false;
  121. [
  122. [address, "address"],
  123. [target, "target"],
  124. [description, "description"],
  125. [expire_in, "expire_in"],
  126. [password, "password"]
  127. ].forEach(([value, name]) => {
  128. if (!value) {
  129. delete req.body[name];
  130. return;
  131. }
  132. if (value === link[name]) {
  133. delete req.body[name];
  134. return;
  135. }
  136. if (name === "expire_in")
  137. if (differenceInSeconds(new Date(value), new Date(link.expire_in)) <= 60)
  138. return;
  139. isChanged = true;
  140. });
  141. await utils.sleep(1000);
  142. if (!isChanged) {
  143. throw new CustomError("Should at least update one field.");
  144. }
  145. const targetDomain = utils.removeWww(URL.parse(target).hostname);
  146. const domain_id = link.domain_id || null;
  147. const queries = await Promise.all([
  148. validators.cooldown(req.user),
  149. target && validators.malware(req.user, target),
  150. address && address !== link.address &&
  151. query.link.find({
  152. address,
  153. domain_id
  154. }),
  155. validators.bannedDomain(targetDomain),
  156. validators.bannedHost(targetDomain)
  157. ]);
  158. // Check if custom link already exists
  159. if (queries[2]) {
  160. const error = "Custom URL is already in use.";
  161. res.locals.errors = { address: error };
  162. throw new CustomError("Custom URL is already in use.");
  163. }
  164. // Update link
  165. const [updatedLink] = await query.link.update(
  166. {
  167. id: link.id
  168. },
  169. {
  170. ...(address && { address }),
  171. ...(description && { description }),
  172. ...(target && { target }),
  173. ...(expire_in && { expire_in }),
  174. ...(password && { password })
  175. }
  176. );
  177. if (req.isHTML) {
  178. res.render("partials/links/edit", {
  179. swap_oob: true,
  180. success: "Link has been updated.",
  181. ...utils.sanitize.link({ ...link, ...updatedLink }),
  182. });
  183. return;
  184. }
  185. return res.status(200).send(utils.sanitize.link({ ...link, ...updatedLink }));
  186. };
  187. /**
  188. * @type {import("express").Handler}
  189. */
  190. async function remove(req, res) {
  191. const { error, isRemoved, link } = await query.link.remove({
  192. uuid: req.params.id,
  193. ...(!req.user.admin && { user_id: req.user.id })
  194. });
  195. if (!isRemoved) {
  196. const messsage = error || "Could not delete the link.";
  197. throw new CustomError(messsage);
  198. }
  199. await utils.sleep(1000);
  200. if (req.isHTML) {
  201. res.setHeader("HX-Reswap", "outerHTML");
  202. res.setHeader("HX-Trigger", "reloadLinks");
  203. res.render("partials/links/dialog/delete_success", {
  204. link: utils.getShortURL(link.address, link.domain).link,
  205. });
  206. return;
  207. }
  208. return res
  209. .status(200)
  210. .send({ message: "Link has been deleted successfully." });
  211. };
  212. // export const report: Handler = async (req, res) => {
  213. // const { link } = req.body;
  214. // const mail = await transporter.sendMail({
  215. // from: env.MAIL_FROM || env.MAIL_USER,
  216. // to: env.REPORT_EMAIL,
  217. // subject: "[REPORT]",
  218. // text: link,
  219. // html: link
  220. // });
  221. // if (!mail.accepted.length) {
  222. // throw new CustomError("Couldn't submit the report. Try again later.");
  223. // }
  224. // return res
  225. // .status(200)
  226. // .send({ message: "Thanks for the report, we'll take actions shortly." });
  227. // };
  228. async function ban(req, res) {
  229. const { id } = req.params;
  230. const update = {
  231. banned_by_id: req.user.id,
  232. banned: true
  233. };
  234. // 1. Check if link exists
  235. const link = await query.link.find({ uuid: id });
  236. if (!link) {
  237. throw new CustomError("No link has been found.", 400);
  238. }
  239. if (link.banned) {
  240. throw new CustomError("Link has been banned already.", 400);
  241. }
  242. const tasks = [];
  243. // 2. Ban link
  244. tasks.push(query.link.update({ uuid: id }, update));
  245. const domain = utils.removeWww(URL.parse(link.target).hostname);
  246. // 3. Ban target's domain
  247. if (req.body.domain) {
  248. tasks.push(query.domain.add({ ...update, address: domain }));
  249. }
  250. // 4. Ban target's host
  251. if (req.body.host) {
  252. const dnsRes = await dnsLookup(domain).catch(() => {
  253. throw new CustomError("Couldn't fetch DNS info.");
  254. });
  255. const host = dnsRes?.address;
  256. tasks.push(query.host.add({ ...update, address: host }));
  257. }
  258. // 5. Ban link owner
  259. if (req.body.user && link.user_id) {
  260. tasks.push(query.user.update({ id: link.user_id }, update));
  261. }
  262. // 6. Ban all of owner's links
  263. if (req.body.userLinks && link.user_id) {
  264. tasks.push(query.link.update({ user_id: link.user_id }, update));
  265. }
  266. // 7. Wait for all tasks to finish
  267. await Promise.all(tasks).catch((err) => {
  268. throw new CustomError("Couldn't ban entries.");
  269. });
  270. // 8. Send response
  271. await utils.sleep(1000);
  272. if (req.isHTML) {
  273. res.setHeader("HX-Reswap", "outerHTML");
  274. res.setHeader("HX-Trigger", "reloadLinks");
  275. res.render("partials/links/dialog/ban_success", {
  276. link: utils.getShortURL(link.address, link.domain).link,
  277. });
  278. return;
  279. }
  280. return res.status(200).send({ message: "Banned link successfully." });
  281. };
  282. // export const redirect = (app) => async (
  283. // req,
  284. // res,
  285. // next
  286. // ) => {
  287. // const isBot = isbot(req.headers["user-agent"]);
  288. // const isPreservedUrl = validators.preservedUrls.some(
  289. // item => item === req.path.replace("/", "")
  290. // );
  291. // if (isPreservedUrl) return next();
  292. // // 1. If custom domain, get domain info
  293. // const host = utils.removeWww(req.headers.host);
  294. // const domain =
  295. // host !== env.DEFAULT_DOMAIN
  296. // ? await query.domain.find({ address: host })
  297. // : null;
  298. // // 2. Get link
  299. // const address = req.params.id.replace("+", "");
  300. // const link = await query.link.find({
  301. // address,
  302. // domain_id: domain ? domain.id : null
  303. // });
  304. // // 3. When no link, if has domain redirect to domain's homepage
  305. // // otherwise redirect to 404
  306. // if (!link) {
  307. // return res.redirect(302, domain ? domain.homepage : "/404");
  308. // }
  309. // // 4. If link is banned, redirect to banned page.
  310. // if (link.banned) {
  311. // return res.redirect("/banned");
  312. // }
  313. // // 5. If wants to see link info, then redirect
  314. // const doesRequestInfo = /.*\+$/gi.test(req.params.id);
  315. // if (doesRequestInfo && !link.password) {
  316. // return app.render(req, res, "/url-info", { target: link.target });
  317. // }
  318. // // 6. If link is protected, redirect to password page
  319. // if (link.password) {
  320. // return res.redirect(`/protected/${link.uuid}`);
  321. // }
  322. // // 7. Create link visit
  323. // if (link.user_id && !isBot) {
  324. // queue.visit.add({
  325. // headers: req.headers,
  326. // realIP: req.realIP,
  327. // referrer: req.get("Referrer"),
  328. // link
  329. // });
  330. // }
  331. // // 8. Redirect to target
  332. // return res.redirect(link.target);
  333. // };
  334. // export const redirectProtected: Handler = async (req, res) => {
  335. // // 1. Get link
  336. // const uuid = req.params.id;
  337. // const link = await query.link.find({ uuid });
  338. // // 2. Throw error if no link
  339. // if (!link || !link.password) {
  340. // throw new CustomError("Couldn't find the link.", 400);
  341. // }
  342. // // 3. Check if password matches
  343. // const matches = await bcrypt.compare(req.body.password, link.password);
  344. // if (!matches) {
  345. // throw new CustomError("Password is not correct.", 401);
  346. // }
  347. // // 4. Create visit
  348. // if (link.user_id) {
  349. // queue.visit.add({
  350. // headers: req.headers,
  351. // realIP: req.realIP,
  352. // referrer: req.get("Referrer"),
  353. // link
  354. // });
  355. // }
  356. // // 5. Send target
  357. // return res.status(200).send({ target: link.target });
  358. // };
  359. // export const redirectCustomDomain: Handler = async (req, res, next) => {
  360. // const { path } = req;
  361. // const host = utils.removeWww(req.headers.host);
  362. // if (host === env.DEFAULT_DOMAIN) {
  363. // return next();
  364. // }
  365. // if (
  366. // path === "/" ||
  367. // validators.preservedUrls
  368. // .filter(l => l !== "url-password")
  369. // .some(item => item === path.replace("/", ""))
  370. // ) {
  371. // const domain = await query.domain.find({ address: host });
  372. // const redirectURL = domain
  373. // ? domain.homepage
  374. // : `https://${env.DEFAULT_DOMAIN + path}`;
  375. // return res.redirect(302, redirectURL);
  376. // }
  377. // return next();
  378. // };
  379. // export const stats: Handler = async (req, res) => {
  380. // const { user } = req;
  381. // const uuid = req.params.id;
  382. // const link = await query.link.find({
  383. // ...(!user.admin && { user_id: user.id }),
  384. // uuid
  385. // });
  386. // if (!link) {
  387. // throw new CustomError("Link could not be found.");
  388. // }
  389. // const stats = await query.visit.find({ link_id: link.id }, link.visit_count);
  390. // if (!stats) {
  391. // throw new CustomError("Could not get the short link stats.");
  392. // }
  393. // return res.status(200).send({
  394. // ...stats,
  395. // ...utils.sanitize.link(link)
  396. // });
  397. // };
  398. module.exports = {
  399. ban,
  400. create,
  401. edit,
  402. get,
  403. remove,
  404. }