utils.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  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. uuid: undefined,
  226. user_id: undefined,
  227. banned_by_id: undefined
  228. }),
  229. link: link => {
  230. const timestamps = parseTimestamps(link);
  231. return {
  232. ...link,
  233. ...timestamps,
  234. banned_by_id: undefined,
  235. domain_id: undefined,
  236. user_id: undefined,
  237. uuid: undefined,
  238. id: link.uuid,
  239. relative_created_at: getTimeAgo(timestamps.created_at),
  240. relative_expire_in: link.expire_in && ms(differenceInMilliseconds(parseDatetime(link.expire_in), new Date()), { long: true }),
  241. password: !!link.password,
  242. visit_count: link.visit_count.toLocaleString("en-US"),
  243. link: getShortURL(link.address, link.domain)
  244. }
  245. },
  246. link_admin: link => {
  247. const timestamps = parseTimestamps(link);
  248. return {
  249. ...link,
  250. ...timestamps,
  251. domain: link.domain || env.DEFAULT_DOMAIN,
  252. id: link.uuid,
  253. relative_created_at: getTimeAgo(timestamps.created_at),
  254. relative_expire_in: link.expire_in && ms(differenceInMilliseconds(parseDatetime(link.expire_in), new Date()), { long: true }),
  255. password: !!link.password,
  256. visit_count: link.visit_count.toLocaleString("en-US"),
  257. link: getShortURL(link.address, link.domain)
  258. }
  259. },
  260. user_admin: user => {
  261. const timestamps = parseTimestamps(user);
  262. return {
  263. ...user,
  264. ...timestamps,
  265. links_count: (user.links_count ?? 0).toLocaleString("en-US"),
  266. relative_created_at: getTimeAgo(timestamps.created_at),
  267. relative_updated_at: getTimeAgo(timestamps.updated_at),
  268. }
  269. },
  270. domain_admin: domain => {
  271. const timestamps = parseTimestamps(domain);
  272. return {
  273. ...domain,
  274. ...timestamps,
  275. links_count: (domain.links_count ?? 0).toLocaleString("en-US"),
  276. relative_created_at: getTimeAgo(timestamps.created_at),
  277. relative_updated_at: getTimeAgo(timestamps.updated_at),
  278. }
  279. }
  280. };
  281. function sleep(ms) {
  282. return new Promise(resolve => setTimeout(resolve, ms));
  283. }
  284. function removeWww(host) {
  285. return host.replace("www.", "");
  286. };
  287. function registerHandlebarsHelpers() {
  288. hbs.registerHelper("ifEquals", function(arg1, arg2, options) {
  289. return (arg1 === arg2) ? options.fn(this) : options.inverse(this);
  290. });
  291. hbs.registerHelper("json", function(context) {
  292. return JSON.stringify(context);
  293. });
  294. const blocks = {};
  295. hbs.registerHelper("extend", function(name, context) {
  296. let block = blocks[name];
  297. if (!block) {
  298. block = blocks[name] = [];
  299. }
  300. block.push(context.fn(this));
  301. });
  302. hbs.registerHelper("block", function(name) {
  303. const val = (blocks[name] || []).join('\n');
  304. blocks[name] = [];
  305. return val;
  306. });
  307. hbs.registerPartials(path.join(__dirname, "../views/partials"), function (err) {});
  308. }
  309. module.exports = {
  310. addProtocol,
  311. CustomError,
  312. dateToUTC,
  313. deleteCurrentToken,
  314. generateId,
  315. getDifferenceFunction,
  316. getInitStats,
  317. getShortURL,
  318. getStatsPeriods,
  319. isAdmin,
  320. parseBooleanQuery,
  321. parseDatetime,
  322. parseTimestamps,
  323. preservedURLs,
  324. registerHandlebarsHelpers,
  325. removeWww,
  326. sanitize,
  327. setToken,
  328. signToken,
  329. sleep,
  330. statsObjectToArray,
  331. urlRegex,
  332. ...knexUtils,
  333. }