utils.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. const { differenceInDays, differenceInHours, differenceInMonths, differenceInMilliseconds, addDays, subHours, subDays, subMonths, subYears, format } = require("date-fns");
  2. const { customAlphabet } = require("nanoid");
  3. const JWT = require("jsonwebtoken");
  4. const path = require("path");
  5. const hbs = require("hbs");
  6. const ms = require("ms");
  7. const { ROLES } = require("../consts");
  8. const knexUtils = require("./knex");
  9. const knex = require("../knex");
  10. const env = require("../env");
  11. const nanoid = customAlphabet(
  12. "abcdefghkmnpqrstuvwxyzABCDEFGHKLMNPQRSTUVWXYZ23456789",
  13. env.LINK_LENGTH
  14. );
  15. class CustomError extends Error {
  16. constructor(message, statusCode, data) {
  17. super(message);
  18. this.name = this.constructor.name;
  19. this.statusCode = statusCode ?? 500;
  20. this.data = data;
  21. }
  22. }
  23. const urlRegex = /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/i;
  24. function isAdmin(user) {
  25. return user.role === ROLES.ADMIN;
  26. }
  27. function signToken(user) {
  28. return JWT.sign(
  29. {
  30. iss: "ApiAuth",
  31. sub: user.id,
  32. iat: parseInt((new Date().getTime() / 1000).toFixed(0)),
  33. exp: parseInt((addDays(new Date(), 7).getTime() / 1000).toFixed(0))
  34. },
  35. env.JWT_SECRET
  36. )
  37. }
  38. function setToken(res, token) {
  39. res.cookie("token", token, {
  40. maxAge: 1000 * 60 * 60 * 24 * 7, // expire after seven days
  41. httpOnly: true,
  42. secure: env.isProd
  43. });
  44. }
  45. function deleteCurrentToken(res) {
  46. res.clearCookie("token", { httpOnly: true, secure: env.isProd });
  47. }
  48. async function generateId(query, domain_id) {
  49. const address = nanoid();
  50. const link = await query.link.find({ address, domain_id });
  51. if (link) {
  52. return generateId(query, domain_id)
  53. };
  54. return address;
  55. }
  56. function addProtocol(url) {
  57. const hasProtocol = /^(\w+:|\/\/)/.test(url);
  58. return hasProtocol ? url : "http://" + url;
  59. }
  60. function getShortURL(address, domain) {
  61. const protocol = (env.CUSTOM_DOMAIN_USE_HTTPS || !domain) && !env.isDev ? "https://" : "http://";
  62. const link = `${domain || env.DEFAULT_DOMAIN}/${address}`;
  63. const url = `${protocol}${link}`;
  64. return { address, link, url };
  65. }
  66. function statsObjectToArray(obj) {
  67. const objToArr = (key) =>
  68. Array.from(Object.keys(obj[key]))
  69. .map((name) => ({
  70. name,
  71. value: obj[key][name]
  72. }))
  73. .sort((a, b) => b.value - a.value);
  74. return {
  75. browser: objToArr("browser"),
  76. os: objToArr("os"),
  77. country: objToArr("country"),
  78. referrer: objToArr("referrer")
  79. };
  80. }
  81. function getDifferenceFunction(type) {
  82. if (type === "lastDay") return differenceInHours;
  83. if (type === "lastWeek") return differenceInDays;
  84. if (type === "lastMonth") return differenceInDays;
  85. if (type === "lastYear") return differenceInMonths;
  86. throw new Error("Unknown type.");
  87. }
  88. function parseDatetime(date) {
  89. // because postgres and mysql return date, sqlite returns formatted iso 8601 string in utc
  90. return date instanceof Date ? date : new Date(date + "Z");
  91. }
  92. function parseTimestamps(item) {
  93. return {
  94. created_at: parseDatetime(item.created_at),
  95. updated_at: parseDatetime(item.updated_at),
  96. }
  97. }
  98. function dateToUTC(date) {
  99. const dateUTC = date instanceof Date ? date.toISOString() : new Date(date).toISOString();
  100. // format the utc date in 'YYYY-MM-DD hh:mm:ss' for SQLite
  101. if (knex.isSQLite) {
  102. return dateUTC.substring(0, 10) + " " + dateUTC.substring(11, 19);
  103. }
  104. // mysql doesn't save time in utc, so format the date in local timezone instead
  105. if (knex.isMySQL) {
  106. return format(new Date(date), "yyyy-MM-dd HH:mm:ss");
  107. }
  108. // return unformatted utc string for postgres
  109. return dateUTC;
  110. }
  111. function getStatsPeriods(now) {
  112. return [
  113. ["lastDay", subHours(now, 24)],
  114. ["lastWeek", subDays(now, 7)],
  115. ["lastMonth", subDays(now, 30)],
  116. ["lastYear", subMonths(now, 12)],
  117. ]
  118. }
  119. const preservedURLs = [
  120. "login",
  121. "logout",
  122. "create-admin",
  123. "404",
  124. "settings",
  125. "admin",
  126. "stats",
  127. "signup",
  128. "banned",
  129. "report",
  130. "reset-password",
  131. "resetpassword",
  132. "verify-email",
  133. "verifyemail",
  134. "verify",
  135. "terms",
  136. "confirm-link-delete",
  137. "confirm-link-ban",
  138. "confirm-user-delete",
  139. "confirm-user-ban",
  140. "create-user",
  141. "confirm-domain-delete-admin",
  142. "confirm-domain-ban",
  143. "add-domain-form",
  144. "confirm-domain-delete",
  145. "get-report-email",
  146. "get-support-email",
  147. "link",
  148. "admin",
  149. "url-password",
  150. "url-info",
  151. "api",
  152. "static",
  153. "images",
  154. "privacy",
  155. "protected",
  156. "css",
  157. "fonts",
  158. "libs",
  159. "pricing"
  160. ];
  161. function parseBooleanQuery(query) {
  162. if (query === "true" || query === true) return true;
  163. if (query === "false" || query === false) return false;
  164. return undefined;
  165. }
  166. function getInitStats() {
  167. return Object.create({
  168. browser: {
  169. chrome: 0,
  170. edge: 0,
  171. firefox: 0,
  172. ie: 0,
  173. opera: 0,
  174. other: 0,
  175. safari: 0
  176. },
  177. os: {
  178. android: 0,
  179. ios: 0,
  180. linux: 0,
  181. macos: 0,
  182. other: 0,
  183. windows: 0
  184. },
  185. country: {},
  186. referrer: {}
  187. });
  188. }
  189. // format date to relative date
  190. const MINUTE = 60,
  191. HOUR = MINUTE * 60,
  192. DAY = HOUR * 24,
  193. WEEK = DAY * 7,
  194. MONTH = DAY * 30,
  195. YEAR = DAY * 365;
  196. function getTimeAgo(dateString) {
  197. const date = new Date(dateString);
  198. const secondsAgo = Math.round((Date.now() - Number(date)) / 1000);
  199. if (secondsAgo < MINUTE) {
  200. return `${secondsAgo} second${secondsAgo !== 1 ? "s" : ""} ago`;
  201. }
  202. let divisor;
  203. let unit = "";
  204. if (secondsAgo < HOUR) {
  205. [divisor, unit] = [MINUTE, "minute"];
  206. } else if (secondsAgo < DAY) {
  207. [divisor, unit] = [HOUR, "hour"];
  208. } else if (secondsAgo < WEEK) {
  209. [divisor, unit] = [DAY, "day"];
  210. } else if (secondsAgo < MONTH) {
  211. [divisor, unit] = [WEEK, "week"];
  212. } else if (secondsAgo < YEAR) {
  213. [divisor, unit] = [MONTH, "month"];
  214. } else {
  215. [divisor, unit] = [YEAR, "year"];
  216. }
  217. const count = Math.floor(secondsAgo / divisor);
  218. return `${count} ${unit}${count > 1 ? "s" : ""} ago`;
  219. }
  220. const sanitize = {
  221. domain: domain => ({
  222. ...domain,
  223. ...parseTimestamps(domain),
  224. id: domain.uuid,
  225. banned: !!domain.banned,
  226. homepage: domain.homepage || env.DEFAULT_DOMAIN,
  227. uuid: undefined,
  228. user_id: undefined,
  229. banned_by_id: undefined
  230. }),
  231. link: link => {
  232. const timestamps = parseTimestamps(link);
  233. return {
  234. ...link,
  235. ...timestamps,
  236. banned_by_id: undefined,
  237. domain_id: undefined,
  238. user_id: undefined,
  239. uuid: undefined,
  240. banned: !!link.banned,
  241. id: link.uuid,
  242. password: !!link.password,
  243. }
  244. },
  245. link_html: link => {
  246. const timestamps = parseTimestamps(link);
  247. return {
  248. ...link,
  249. ...timestamps,
  250. banned_by_id: undefined,
  251. domain_id: undefined,
  252. user_id: undefined,
  253. uuid: undefined,
  254. banned: !!link.banned,
  255. id: link.uuid,
  256. relative_created_at: getTimeAgo(timestamps.created_at),
  257. relative_expire_in: link.expire_in && ms(differenceInMilliseconds(parseDatetime(link.expire_in), new Date()), { long: true }),
  258. password: !!link.password,
  259. visit_count: link.visit_count.toLocaleString("en-US"),
  260. link: getShortURL(link.address, link.domain),
  261. }
  262. },
  263. link_admin: link => {
  264. const timestamps = parseTimestamps(link);
  265. return {
  266. ...link,
  267. ...timestamps,
  268. domain: link.domain || env.DEFAULT_DOMAIN,
  269. id: link.uuid,
  270. relative_created_at: getTimeAgo(timestamps.created_at),
  271. relative_expire_in: link.expire_in && ms(differenceInMilliseconds(parseDatetime(link.expire_in), new Date()), { long: true }),
  272. password: !!link.password,
  273. visit_count: link.visit_count.toLocaleString("en-US"),
  274. link: getShortURL(link.address, link.domain)
  275. }
  276. },
  277. user_admin: user => {
  278. const timestamps = parseTimestamps(user);
  279. return {
  280. ...user,
  281. ...timestamps,
  282. links_count: (user.links_count ?? 0).toLocaleString("en-US"),
  283. relative_created_at: getTimeAgo(timestamps.created_at),
  284. relative_updated_at: getTimeAgo(timestamps.updated_at),
  285. }
  286. },
  287. domain_admin: domain => {
  288. const timestamps = parseTimestamps(domain);
  289. return {
  290. ...domain,
  291. ...timestamps,
  292. links_count: (domain.links_count ?? 0).toLocaleString("en-US"),
  293. relative_created_at: getTimeAgo(timestamps.created_at),
  294. relative_updated_at: getTimeAgo(timestamps.updated_at),
  295. }
  296. }
  297. };
  298. function sleep(ms) {
  299. return new Promise(resolve => setTimeout(resolve, ms));
  300. }
  301. function removeWww(host) {
  302. return host.replace("www.", "");
  303. };
  304. function registerHandlebarsHelpers() {
  305. hbs.registerHelper("ifEquals", function(arg1, arg2, options) {
  306. return (arg1 === arg2) ? options.fn(this) : options.inverse(this);
  307. });
  308. hbs.registerHelper("json", function(context) {
  309. return JSON.stringify(context);
  310. });
  311. const blocks = {};
  312. hbs.registerHelper("extend", function(name, context) {
  313. let block = blocks[name];
  314. if (!block) {
  315. block = blocks[name] = [];
  316. }
  317. block.push(context.fn(this));
  318. });
  319. hbs.registerHelper("block", function(name) {
  320. const val = (blocks[name] || []).join('\n');
  321. blocks[name] = [];
  322. return val;
  323. });
  324. hbs.registerPartials(path.join(__dirname, "../views/partials"), function (err) {});
  325. }
  326. module.exports = {
  327. addProtocol,
  328. CustomError,
  329. dateToUTC,
  330. deleteCurrentToken,
  331. generateId,
  332. getDifferenceFunction,
  333. getInitStats,
  334. getShortURL,
  335. getStatsPeriods,
  336. isAdmin,
  337. parseBooleanQuery,
  338. parseDatetime,
  339. parseTimestamps,
  340. preservedURLs,
  341. registerHandlebarsHelpers,
  342. removeWww,
  343. sanitize,
  344. setToken,
  345. signToken,
  346. sleep,
  347. statsObjectToArray,
  348. urlRegex,
  349. ...knexUtils,
  350. }