2024-09-12 14:26:39 +03:30
|
|
|
const { differenceInDays, differenceInHours, differenceInMonths, differenceInMilliseconds, addDays, subHours, subDays, subMonths, subYears } = require("date-fns");
|
2024-08-11 18:41:03 +03:30
|
|
|
const nanoid = require("nanoid/generate");
|
|
|
|
const JWT = require("jsonwebtoken");
|
2024-09-12 14:26:39 +03:30
|
|
|
const path = require("path");
|
2024-08-11 18:41:03 +03:30
|
|
|
const hbs = require("hbs");
|
2024-09-12 14:26:39 +03:30
|
|
|
const ms = require("ms");
|
2024-08-11 18:41:03 +03:30
|
|
|
|
|
|
|
const env = require("../env");
|
|
|
|
|
|
|
|
class CustomError extends Error {
|
|
|
|
constructor(message, statusCode, data) {
|
|
|
|
super(message);
|
|
|
|
this.name = this.constructor.name;
|
|
|
|
this.statusCode = statusCode ?? 500;
|
|
|
|
this.data = data;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-09 18:43:12 +03:30
|
|
|
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;
|
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
function isAdmin(email) {
|
|
|
|
return env.ADMIN_EMAILS.split(",")
|
|
|
|
.map((e) => e.trim())
|
|
|
|
.includes(email)
|
|
|
|
}
|
|
|
|
|
|
|
|
function signToken(user) {
|
|
|
|
return JWT.sign(
|
|
|
|
{
|
|
|
|
iss: "ApiAuth",
|
|
|
|
sub: user.email,
|
|
|
|
domain: user.domain || "",
|
|
|
|
iat: parseInt((new Date().getTime() / 1000).toFixed(0)),
|
|
|
|
exp: parseInt((addDays(new Date(), 7).getTime() / 1000).toFixed(0))
|
|
|
|
},
|
|
|
|
env.JWT_SECRET
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-09-12 14:26:39 +03:30
|
|
|
function setToken(res, token) {
|
|
|
|
res.cookie("token", token, {
|
|
|
|
maxAge: 1000 * 60 * 60 * 24 * 7, // expire after seven days
|
|
|
|
httpOnly: true,
|
|
|
|
secure: env.isProd
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function deleteCurrentToken(res) {
|
|
|
|
res.clearCookie("token", { httpOnly: true, secure: env.isProd });
|
|
|
|
}
|
|
|
|
|
2024-09-08 14:10:02 +03:30
|
|
|
async function generateId(query, domain_id) {
|
2024-08-11 18:41:03 +03:30
|
|
|
const address = nanoid(
|
|
|
|
"abcdefghkmnpqrstuvwxyzABCDEFGHKLMNPQRSTUVWXYZ23456789",
|
|
|
|
env.LINK_LENGTH
|
|
|
|
);
|
|
|
|
const link = await query.link.find({ address, domain_id });
|
|
|
|
if (!link) return address;
|
|
|
|
return generateId(domain_id);
|
|
|
|
}
|
|
|
|
|
|
|
|
function addProtocol(url) {
|
2024-09-09 18:43:12 +03:30
|
|
|
const hasProtocol = /^(\w+:|\/\/)/.test(url);
|
|
|
|
return hasProtocol ? url : "http://" + url;
|
2024-08-11 18:41:03 +03:30
|
|
|
}
|
|
|
|
|
2024-08-21 21:22:59 +03:30
|
|
|
function getShortURL(address, domain) {
|
2024-09-08 14:10:02 +03:30
|
|
|
const protocol = (env.CUSTOM_DOMAIN_USE_HTTPS || !domain) && !env.isDev ? "https://" : "http://";
|
2024-08-21 21:22:59 +03:30
|
|
|
const link = `${domain || env.DEFAULT_DOMAIN}/${address}`;
|
2024-08-11 18:41:03 +03:30
|
|
|
const url = `${protocol}${link}`;
|
|
|
|
return { link, url };
|
|
|
|
}
|
|
|
|
|
|
|
|
const getRedisKey = {
|
|
|
|
// TODO: remove user id and make domain id required
|
|
|
|
link: (address, domain_id, user_id) => `${address}-${domain_id || ""}-${user_id || ""}`,
|
|
|
|
domain: (address) => `d-${address}`,
|
|
|
|
host: (address) => `h-${address}`,
|
|
|
|
user: (emailOrKey) => `u-${emailOrKey}`
|
|
|
|
};
|
|
|
|
|
|
|
|
function getStatsLimit() {
|
|
|
|
return env.DEFAULT_MAX_STATS_PER_LINK || 100000000;
|
|
|
|
};
|
|
|
|
|
|
|
|
function getStatsCacheTime(total) {
|
|
|
|
return (total > 50000 ? ms("5 minutes") : ms("1 minutes")) / 1000
|
|
|
|
};
|
|
|
|
|
|
|
|
function statsObjectToArray(obj) {
|
|
|
|
const objToArr = (key) =>
|
|
|
|
Array.from(Object.keys(obj[key]))
|
|
|
|
.map((name) => ({
|
|
|
|
name,
|
|
|
|
value: obj[key][name]
|
|
|
|
}))
|
|
|
|
.sort((a, b) => b.value - a.value);
|
|
|
|
|
|
|
|
return {
|
|
|
|
browser: objToArr("browser"),
|
|
|
|
os: objToArr("os"),
|
|
|
|
country: objToArr("country"),
|
|
|
|
referrer: objToArr("referrer")
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function getDifferenceFunction(type) {
|
|
|
|
if (type === "lastDay") return differenceInHours;
|
|
|
|
if (type === "lastWeek") return differenceInDays;
|
|
|
|
if (type === "lastMonth") return differenceInDays;
|
2024-09-08 14:10:02 +03:30
|
|
|
if (type === "lastYear") return differenceInMonths;
|
2024-08-11 18:41:03 +03:30
|
|
|
throw new Error("Unknown type.");
|
|
|
|
}
|
|
|
|
|
|
|
|
function getUTCDate(dateString) {
|
|
|
|
const date = new Date(dateString || Date.now());
|
|
|
|
return new Date(
|
|
|
|
date.getUTCFullYear(),
|
|
|
|
date.getUTCMonth(),
|
|
|
|
date.getUTCDate(),
|
|
|
|
date.getUTCHours()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-09-08 14:10:02 +03:30
|
|
|
function getStatsPeriods(now) {
|
|
|
|
return [
|
|
|
|
["lastDay", subHours(now, 24)],
|
|
|
|
["lastWeek", subDays(now, 7)],
|
|
|
|
["lastMonth", subDays(now, 30)],
|
|
|
|
["lastYear", subMonths(now, 12)],
|
|
|
|
]
|
|
|
|
}
|
2024-08-11 18:41:03 +03:30
|
|
|
|
|
|
|
const preservedURLs = [
|
|
|
|
"login",
|
|
|
|
"logout",
|
|
|
|
"signup",
|
|
|
|
"reset-password",
|
|
|
|
"resetpassword",
|
|
|
|
"url-password",
|
|
|
|
"url-info",
|
|
|
|
"settings",
|
|
|
|
"stats",
|
|
|
|
"verify",
|
|
|
|
"api",
|
|
|
|
"404",
|
|
|
|
"static",
|
|
|
|
"images",
|
|
|
|
"banned",
|
|
|
|
"terms",
|
|
|
|
"privacy",
|
|
|
|
"protected",
|
|
|
|
"report",
|
|
|
|
"pricing"
|
|
|
|
];
|
|
|
|
|
|
|
|
function getInitStats() {
|
|
|
|
return Object.create({
|
|
|
|
browser: {
|
|
|
|
chrome: 0,
|
|
|
|
edge: 0,
|
|
|
|
firefox: 0,
|
|
|
|
ie: 0,
|
|
|
|
opera: 0,
|
|
|
|
other: 0,
|
|
|
|
safari: 0
|
|
|
|
},
|
|
|
|
os: {
|
|
|
|
android: 0,
|
|
|
|
ios: 0,
|
|
|
|
linux: 0,
|
|
|
|
macos: 0,
|
|
|
|
other: 0,
|
|
|
|
windows: 0
|
|
|
|
},
|
|
|
|
country: {},
|
|
|
|
referrer: {}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-08-21 21:22:59 +03:30
|
|
|
// format date to relative date
|
|
|
|
const MINUTE = 60,
|
|
|
|
HOUR = MINUTE * 60,
|
|
|
|
DAY = HOUR * 24,
|
|
|
|
WEEK = DAY * 7,
|
|
|
|
MONTH = DAY * 30,
|
|
|
|
YEAR = DAY * 365;
|
|
|
|
function getTimeAgo(date) {
|
|
|
|
const secondsAgo = Math.round((Date.now() - Number(date)) / 1000);
|
|
|
|
|
|
|
|
if (secondsAgo < MINUTE) {
|
|
|
|
return `${secondsAgo} second${secondsAgo !== 1 ? "s" : ""} ago`;
|
|
|
|
}
|
|
|
|
|
|
|
|
let divisor;
|
|
|
|
let unit = "";
|
|
|
|
|
|
|
|
if (secondsAgo < HOUR) {
|
|
|
|
[divisor, unit] = [MINUTE, "minute"];
|
|
|
|
} else if (secondsAgo < DAY) {
|
|
|
|
[divisor, unit] = [HOUR, "hour"];
|
|
|
|
} else if (secondsAgo < WEEK) {
|
|
|
|
[divisor, unit] = [DAY, "day"];
|
|
|
|
} else if (secondsAgo < MONTH) {
|
|
|
|
[divisor, unit] = [WEEK, "week"];
|
|
|
|
} else if (secondsAgo < YEAR) {
|
|
|
|
[divisor, unit] = [MONTH, "month"];
|
|
|
|
} else {
|
|
|
|
[divisor, unit] = [YEAR, "year"];
|
|
|
|
}
|
|
|
|
|
|
|
|
const count = Math.floor(secondsAgo / divisor);
|
|
|
|
return `${count} ${unit}${count > 1 ? "s" : ""} ago`;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
const sanitize = {
|
|
|
|
domain: domain => ({
|
|
|
|
...domain,
|
|
|
|
id: domain.uuid,
|
|
|
|
uuid: undefined,
|
|
|
|
user_id: undefined,
|
|
|
|
banned_by_id: undefined
|
|
|
|
}),
|
|
|
|
link: link => ({
|
|
|
|
...link,
|
|
|
|
banned_by_id: undefined,
|
|
|
|
domain_id: undefined,
|
|
|
|
user_id: undefined,
|
|
|
|
uuid: undefined,
|
|
|
|
id: link.uuid,
|
2024-08-21 21:22:59 +03:30
|
|
|
relative_created_at: getTimeAgo(link.created_at),
|
|
|
|
relative_expire_in: link.expire_in && ms(differenceInMilliseconds(new Date(link.expire_in), new Date()), { long: true }),
|
2024-08-11 18:41:03 +03:30
|
|
|
password: !!link.password,
|
|
|
|
link: getShortURL(link.address, link.domain)
|
|
|
|
})
|
|
|
|
};
|
|
|
|
|
|
|
|
function sleep(ms) {
|
|
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
|
|
}
|
|
|
|
|
|
|
|
function removeWww(host) {
|
|
|
|
return host.replace("www.", "");
|
|
|
|
};
|
|
|
|
|
2024-08-21 21:22:59 +03:30
|
|
|
function registerHandlebarsHelpers() {
|
|
|
|
hbs.registerHelper("ifEquals", function(arg1, arg2, options) {
|
|
|
|
return (arg1 === arg2) ? options.fn(this) : options.inverse(this);
|
|
|
|
});
|
2024-09-08 14:10:02 +03:30
|
|
|
|
|
|
|
hbs.registerHelper("json", function(context) {
|
|
|
|
return JSON.stringify(context);
|
|
|
|
});
|
2024-08-21 21:22:59 +03:30
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
const blocks = {};
|
2024-08-21 21:22:59 +03:30
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
hbs.registerHelper("extend", function(name, context) {
|
|
|
|
let block = blocks[name];
|
|
|
|
if (!block) {
|
|
|
|
block = blocks[name] = [];
|
|
|
|
}
|
|
|
|
block.push(context.fn(this));
|
|
|
|
});
|
|
|
|
|
|
|
|
hbs.registerHelper("block", function(name) {
|
|
|
|
const val = (blocks[name] || []).join('\n');
|
|
|
|
blocks[name] = [];
|
|
|
|
return val;
|
|
|
|
});
|
|
|
|
hbs.registerPartials(path.join(__dirname, "../views/partials"), function (err) {});
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
addProtocol,
|
|
|
|
CustomError,
|
2024-09-12 14:26:39 +03:30
|
|
|
deleteCurrentToken,
|
2024-08-11 18:41:03 +03:30
|
|
|
generateId,
|
|
|
|
getDifferenceFunction,
|
|
|
|
getInitStats,
|
|
|
|
getRedisKey,
|
2024-08-21 21:22:59 +03:30
|
|
|
getShortURL,
|
2024-08-11 18:41:03 +03:30
|
|
|
getStatsCacheTime,
|
|
|
|
getStatsLimit,
|
2024-09-09 18:43:12 +03:30
|
|
|
getStatsPeriods,
|
2024-08-11 18:41:03 +03:30
|
|
|
getUTCDate,
|
|
|
|
isAdmin,
|
|
|
|
preservedURLs,
|
2024-08-21 21:22:59 +03:30
|
|
|
registerHandlebarsHelpers,
|
2024-08-11 18:41:03 +03:30
|
|
|
removeWww,
|
|
|
|
sanitize,
|
2024-09-12 14:26:39 +03:30
|
|
|
setToken,
|
2024-08-11 18:41:03 +03:30
|
|
|
signToken,
|
|
|
|
sleep,
|
|
|
|
statsObjectToArray,
|
2024-09-09 18:43:12 +03:30
|
|
|
urlRegex,
|
2024-08-11 18:41:03 +03:30
|
|
|
}
|