links.handler.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  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. res.setHeader("HX-Trigger-After-Swap", "resetForm");
  102. const shortURL = utils.getShortURL(link.address, link.domain);
  103. return res.render("partials/shortener", {
  104. link: shortURL.link,
  105. url: shortURL.url,
  106. });
  107. }
  108. return res
  109. .status(201)
  110. .send(utils.sanitize.link({ ...link }));
  111. }
  112. async function edit(req, res) {
  113. const { address, target, description, expire_in, password } = req.body;
  114. const link = await query.link.find({
  115. uuid: req.params.id,
  116. ...(!req.user.admin && { user_id: req.user.id })
  117. });
  118. if (!link) {
  119. throw new CustomError("Link was not found.");
  120. }
  121. let isChanged = false;
  122. [
  123. [address, "address"],
  124. [target, "target"],
  125. [description, "description"],
  126. [expire_in, "expire_in"],
  127. [password, "password"]
  128. ].forEach(([value, name]) => {
  129. if (!value) {
  130. delete req.body[name];
  131. return;
  132. }
  133. if (value === link[name]) {
  134. delete req.body[name];
  135. return;
  136. }
  137. if (name === "expire_in")
  138. if (differenceInSeconds(new Date(value), new Date(link.expire_in)) <= 60)
  139. return;
  140. isChanged = true;
  141. });
  142. await utils.sleep(1000);
  143. if (!isChanged) {
  144. throw new CustomError("Should at least update one field.");
  145. }
  146. const targetDomain = utils.removeWww(URL.parse(target).hostname);
  147. const domain_id = link.domain_id || null;
  148. const queries = await Promise.all([
  149. validators.cooldown(req.user),
  150. target && validators.malware(req.user, target),
  151. address && address !== link.address &&
  152. query.link.find({
  153. address,
  154. domain_id
  155. }),
  156. validators.bannedDomain(targetDomain),
  157. validators.bannedHost(targetDomain)
  158. ]);
  159. // Check if custom link already exists
  160. if (queries[2]) {
  161. const error = "Custom URL is already in use.";
  162. res.locals.errors = { address: error };
  163. throw new CustomError("Custom URL is already in use.");
  164. }
  165. // Update link
  166. const [updatedLink] = await query.link.update(
  167. {
  168. id: link.id
  169. },
  170. {
  171. ...(address && { address }),
  172. ...(description && { description }),
  173. ...(target && { target }),
  174. ...(expire_in && { expire_in }),
  175. ...(password && { password })
  176. }
  177. );
  178. if (req.isHTML) {
  179. res.render("partials/links/edit", {
  180. swap_oob: true,
  181. success: "Link has been updated.",
  182. ...utils.sanitize.link({ ...link, ...updatedLink }),
  183. });
  184. return;
  185. }
  186. return res.status(200).send(utils.sanitize.link({ ...link, ...updatedLink }));
  187. };
  188. /**
  189. * @type {import("express").Handler}
  190. */
  191. async function remove(req, res) {
  192. const { error, isRemoved, link } = await query.link.remove({
  193. uuid: req.params.id,
  194. ...(!req.user.admin && { user_id: req.user.id })
  195. });
  196. if (!isRemoved) {
  197. const messsage = error || "Could not delete the link.";
  198. throw new CustomError(messsage);
  199. }
  200. await utils.sleep(1000);
  201. if (req.isHTML) {
  202. res.setHeader("HX-Reswap", "outerHTML");
  203. res.setHeader("HX-Trigger", "reloadLinks");
  204. res.render("partials/links/dialog_delete_success", {
  205. link: utils.getShortURL(link.address, link.domain).link,
  206. });
  207. return;
  208. }
  209. return res
  210. .status(200)
  211. .send({ message: "Link has been deleted successfully." });
  212. };
  213. // export const report: Handler = async (req, res) => {
  214. // const { link } = req.body;
  215. // const mail = await transporter.sendMail({
  216. // from: env.MAIL_FROM || env.MAIL_USER,
  217. // to: env.REPORT_EMAIL,
  218. // subject: "[REPORT]",
  219. // text: link,
  220. // html: link
  221. // });
  222. // if (!mail.accepted.length) {
  223. // throw new CustomError("Couldn't submit the report. Try again later.");
  224. // }
  225. // return res
  226. // .status(200)
  227. // .send({ message: "Thanks for the report, we'll take actions shortly." });
  228. // };
  229. // export const ban: Handler = async (req, res) => {
  230. // const { id } = req.params;
  231. // const update = {
  232. // banned_by_id: req.user.id,
  233. // banned: true
  234. // };
  235. // // 1. Check if link exists
  236. // const link = await query.link.find({ uuid: id });
  237. // if (!link) {
  238. // throw new CustomError("No link has been found.", 400);
  239. // }
  240. // if (link.banned) {
  241. // return res.status(200).send({ message: "Link has been banned already." });
  242. // }
  243. // const tasks = [];
  244. // // 2. Ban link
  245. // tasks.push(query.link.update({ uuid: id }, update));
  246. // const domain = utils.removeWww(URL.parse(link.target).hostname);
  247. // // 3. Ban target's domain
  248. // if (req.body.domain) {
  249. // tasks.push(query.domain.add({ ...update, address: domain }));
  250. // }
  251. // // 4. Ban target's host
  252. // if (req.body.host) {
  253. // const dnsRes = await dnsLookup(domain).catch(() => {
  254. // throw new CustomError("Couldn't fetch DNS info.");
  255. // });
  256. // const host = dnsRes?.address;
  257. // tasks.push(query.host.add({ ...update, address: host }));
  258. // }
  259. // // 5. Ban link owner
  260. // if (req.body.user && link.user_id) {
  261. // tasks.push(query.user.update({ id: link.user_id }, update));
  262. // }
  263. // // 6. Ban all of owner's links
  264. // if (req.body.userLinks && link.user_id) {
  265. // tasks.push(query.link.update({ user_id: link.user_id }, update));
  266. // }
  267. // // 7. Wait for all tasks to finish
  268. // await Promise.all(tasks).catch(() => {
  269. // throw new CustomError("Couldn't ban entries.");
  270. // });
  271. // // 8. Send response
  272. // return res.status(200).send({ message: "Banned link successfully." });
  273. // };
  274. // export const redirect = (app) => async (
  275. // req,
  276. // res,
  277. // next
  278. // ) => {
  279. // const isBot = isbot(req.headers["user-agent"]);
  280. // const isPreservedUrl = validators.preservedUrls.some(
  281. // item => item === req.path.replace("/", "")
  282. // );
  283. // if (isPreservedUrl) return next();
  284. // // 1. If custom domain, get domain info
  285. // const host = utils.removeWww(req.headers.host);
  286. // const domain =
  287. // host !== env.DEFAULT_DOMAIN
  288. // ? await query.domain.find({ address: host })
  289. // : null;
  290. // // 2. Get link
  291. // const address = req.params.id.replace("+", "");
  292. // const link = await query.link.find({
  293. // address,
  294. // domain_id: domain ? domain.id : null
  295. // });
  296. // // 3. When no link, if has domain redirect to domain's homepage
  297. // // otherwise redirect to 404
  298. // if (!link) {
  299. // return res.redirect(302, domain ? domain.homepage : "/404");
  300. // }
  301. // // 4. If link is banned, redirect to banned page.
  302. // if (link.banned) {
  303. // return res.redirect("/banned");
  304. // }
  305. // // 5. If wants to see link info, then redirect
  306. // const doesRequestInfo = /.*\+$/gi.test(req.params.id);
  307. // if (doesRequestInfo && !link.password) {
  308. // return app.render(req, res, "/url-info", { target: link.target });
  309. // }
  310. // // 6. If link is protected, redirect to password page
  311. // if (link.password) {
  312. // return res.redirect(`/protected/${link.uuid}`);
  313. // }
  314. // // 7. Create link visit
  315. // if (link.user_id && !isBot) {
  316. // queue.visit.add({
  317. // headers: req.headers,
  318. // realIP: req.realIP,
  319. // referrer: req.get("Referrer"),
  320. // link
  321. // });
  322. // }
  323. // // 8. Redirect to target
  324. // return res.redirect(link.target);
  325. // };
  326. // export const redirectProtected: Handler = async (req, res) => {
  327. // // 1. Get link
  328. // const uuid = req.params.id;
  329. // const link = await query.link.find({ uuid });
  330. // // 2. Throw error if no link
  331. // if (!link || !link.password) {
  332. // throw new CustomError("Couldn't find the link.", 400);
  333. // }
  334. // // 3. Check if password matches
  335. // const matches = await bcrypt.compare(req.body.password, link.password);
  336. // if (!matches) {
  337. // throw new CustomError("Password is not correct.", 401);
  338. // }
  339. // // 4. Create visit
  340. // if (link.user_id) {
  341. // queue.visit.add({
  342. // headers: req.headers,
  343. // realIP: req.realIP,
  344. // referrer: req.get("Referrer"),
  345. // link
  346. // });
  347. // }
  348. // // 5. Send target
  349. // return res.status(200).send({ target: link.target });
  350. // };
  351. // export const redirectCustomDomain: Handler = async (req, res, next) => {
  352. // const { path } = req;
  353. // const host = utils.removeWww(req.headers.host);
  354. // if (host === env.DEFAULT_DOMAIN) {
  355. // return next();
  356. // }
  357. // if (
  358. // path === "/" ||
  359. // validators.preservedUrls
  360. // .filter(l => l !== "url-password")
  361. // .some(item => item === path.replace("/", ""))
  362. // ) {
  363. // const domain = await query.domain.find({ address: host });
  364. // const redirectURL = domain
  365. // ? domain.homepage
  366. // : `https://${env.DEFAULT_DOMAIN + path}`;
  367. // return res.redirect(302, redirectURL);
  368. // }
  369. // return next();
  370. // };
  371. // export const stats: Handler = async (req, res) => {
  372. // const { user } = req;
  373. // const uuid = req.params.id;
  374. // const link = await query.link.find({
  375. // ...(!user.admin && { user_id: user.id }),
  376. // uuid
  377. // });
  378. // if (!link) {
  379. // throw new CustomError("Link could not be found.");
  380. // }
  381. // const stats = await query.visit.find({ link_id: link.id }, link.visit_count);
  382. // if (!stats) {
  383. // throw new CustomError("Could not get the short link stats.");
  384. // }
  385. // return res.status(200).send({
  386. // ...stats,
  387. // ...utils.sanitize.link(link)
  388. // });
  389. // };
  390. module.exports = {
  391. create,
  392. edit,
  393. get,
  394. remove,
  395. }