kutt/server/utils/utils.js

416 lines
11 KiB
JavaScript
Raw Normal View History

2024-10-08 08:18:10 +03:30
const { differenceInDays, differenceInHours, differenceInMonths, differenceInMilliseconds, addDays, subHours, subDays, subMonths, subYears, format } = require("date-fns");
2025-01-01 14:27:04 +03:30
const { customAlphabet } = require("nanoid");
2024-08-11 18:41:03 +03:30
const JWT = require("jsonwebtoken");
2025-01-15 11:45:53 +03:30
const path = require("node:path");
const fs = require("node:fs");
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
2024-11-19 07:58:57 +03:30
const { ROLES } = require("../consts");
2024-11-23 14:43:18 +03:30
const knexUtils = require("./knex");
const knex = require("../knex");
2024-08-11 18:41:03 +03:30
const env = require("../env");
2025-01-01 14:27:04 +03:30
const nanoid = customAlphabet(
"abcdefghkmnpqrstuvwxyzABCDEFGHKLMNPQRSTUVWXYZ23456789",
env.LINK_LENGTH
);
2024-08-11 18:41:03 +03:30
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-11-19 07:58:57 +03:30
function isAdmin(user) {
return user.role === ROLES.ADMIN;
2024-08-11 18:41:03 +03:30
}
function signToken(user) {
return JWT.sign(
{
iss: "ApiAuth",
sub: user.id,
2024-08-11 18:41:03 +03:30
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) {
2025-01-01 14:27:04 +03:30
const address = nanoid();
2024-08-11 18:41:03 +03:30
const link = await query.link.find({ address, domain_id });
2025-01-01 14:27:04 +03:30
if (link) {
return generateId(query, domain_id)
};
return address;
2024-08-11 18:41:03 +03:30
}
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}`;
2024-11-19 07:58:57 +03:30
return { address, link, url };
2024-08-11 18:41:03 +03:30
}
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.");
}
2024-10-07 09:08:40 +03:30
function parseDatetime(date) {
2024-10-08 09:26:48 +03:30
// because postgres and mysql return date, sqlite returns formatted iso 8601 string in utc
2024-10-07 09:08:40 +03:30
return date instanceof Date ? date : new Date(date + "Z");
}
function parseTimestamps(item) {
return {
created_at: parseDatetime(item.created_at),
updated_at: parseDatetime(item.updated_at),
}
}
function dateToUTC(date) {
const dateUTC = date instanceof Date ? date.toISOString() : new Date(date).toISOString();
2024-10-07 14:35:47 +03:30
2024-10-08 09:26:48 +03:30
// format the utc date in 'YYYY-MM-DD hh:mm:ss' for SQLite
if (knex.isSQLite) {
2024-10-07 09:08:40 +03:30
return dateUTC.substring(0, 10) + " " + dateUTC.substring(11, 19);
2024-10-08 09:26:48 +03:30
}
// mysql doesn't save time in utc, so format the date in local timezone instead
if (knex.isMySQL) {
return format(new Date(date), "yyyy-MM-dd HH:mm:ss");
}
2024-10-07 09:08:40 +03:30
2024-10-08 09:26:48 +03:30
// return unformatted utc string for postgres
2024-10-07 09:08:40 +03:30
return dateUTC;
2024-08-11 18:41:03 +03:30
}
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",
2025-01-01 12:45:29 +03:30
"create-admin",
2024-09-12 14:35:35 +03:30
"404",
"settings",
2025-01-01 12:45:29 +03:30
"admin",
2024-09-12 14:35:35 +03:30
"stats",
2024-08-11 18:41:03 +03:30
"signup",
2024-09-12 14:35:35 +03:30
"banned",
"report",
2024-08-11 18:41:03 +03:30
"reset-password",
"resetpassword",
2024-09-12 14:35:35 +03:30
"verify-email",
"verifyemail",
"verify",
"terms",
"confirm-link-delete",
"confirm-link-ban",
2025-01-01 12:45:29 +03:30
"confirm-user-delete",
"confirm-user-ban",
"create-user",
"confirm-domain-delete-admin",
"confirm-domain-ban",
2024-09-12 14:35:35 +03:30
"add-domain-form",
"confirm-domain-delete",
"get-report-email",
2025-01-01 12:45:29 +03:30
"get-support-email",
2024-09-12 14:35:35 +03:30
"link",
2025-01-01 12:45:29 +03:30
"admin",
2024-08-11 18:41:03 +03:30
"url-password",
"url-info",
"api",
"static",
"images",
"privacy",
"protected",
2024-09-12 14:35:35 +03:30
"css",
"fonts",
"libs",
2024-08-11 18:41:03 +03:30
"pricing"
];
2024-11-19 07:58:57 +03:30
function parseBooleanQuery(query) {
if (query === "true" || query === true) return true;
if (query === "false" || query === false) return false;
return undefined;
}
2024-08-11 18:41:03 +03:30
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;
2024-10-07 09:08:40 +03:30
function getTimeAgo(dateString) {
const date = new Date(dateString);
2024-08-21 21:22:59 +03:30
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,
2024-10-07 09:08:40 +03:30
...parseTimestamps(domain),
2024-08-11 18:41:03 +03:30
id: domain.uuid,
2025-01-08 10:28:32 +03:30
banned: !!domain.banned,
homepage: domain.homepage || env.DEFAULT_DOMAIN,
2024-08-11 18:41:03 +03:30
uuid: undefined,
user_id: undefined,
banned_by_id: undefined
}),
2024-10-07 09:08:40 +03:30
link: link => {
const timestamps = parseTimestamps(link);
return {
...link,
...timestamps,
banned_by_id: undefined,
domain_id: undefined,
user_id: undefined,
uuid: undefined,
2025-01-08 10:28:32 +03:30
banned: !!link.banned,
id: link.uuid,
password: !!link.password,
}
},
link_html: link => {
const timestamps = parseTimestamps(link);
return {
...link,
...timestamps,
banned_by_id: undefined,
domain_id: undefined,
user_id: undefined,
uuid: undefined,
banned: !!link.banned,
2024-10-07 09:08:40 +03:30
id: link.uuid,
relative_created_at: getTimeAgo(timestamps.created_at),
relative_expire_in: link.expire_in && ms(differenceInMilliseconds(parseDatetime(link.expire_in), new Date()), { long: true }),
password: !!link.password,
2024-11-19 07:58:57 +03:30
visit_count: link.visit_count.toLocaleString("en-US"),
2025-01-08 10:28:32 +03:30
link: getShortURL(link.address, link.domain),
2024-11-19 07:58:57 +03:30
}
},
link_admin: link => {
const timestamps = parseTimestamps(link);
return {
...link,
...timestamps,
domain: link.domain || env.DEFAULT_DOMAIN,
id: link.uuid,
relative_created_at: getTimeAgo(timestamps.created_at),
relative_expire_in: link.expire_in && ms(differenceInMilliseconds(parseDatetime(link.expire_in), new Date()), { long: true }),
password: !!link.password,
visit_count: link.visit_count.toLocaleString("en-US"),
2024-10-07 09:08:40 +03:30
link: getShortURL(link.address, link.domain)
}
2024-11-19 07:58:57 +03:30
},
user_admin: user => {
const timestamps = parseTimestamps(user);
return {
...user,
...timestamps,
links_count: (user.links_count ?? 0).toLocaleString("en-US"),
relative_created_at: getTimeAgo(timestamps.created_at),
relative_updated_at: getTimeAgo(timestamps.updated_at),
}
},
domain_admin: domain => {
const timestamps = parseTimestamps(domain);
return {
...domain,
...timestamps,
links_count: (domain.links_count ?? 0).toLocaleString("en-US"),
relative_created_at: getTimeAgo(timestamps.created_at),
relative_updated_at: getTimeAgo(timestamps.updated_at),
}
2024-10-07 09:08:40 +03:30
}
2024-08-11 18:41:03 +03:30
};
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) {});
2025-01-15 11:45:53 +03:30
const customPartialsPath = path.join(__dirname, "../../custom/views/partials");
const customPartialsExist = fs.existsSync(customPartialsPath);
if (customPartialsExist) {
hbs.registerPartials(customPartialsPath, function (err) {});
}
}
// grab custom styles file name from the custom/css folder
const custom_css_file_names = [];
const customCSSPath = path.join(__dirname, "../../custom/css");
const customCSSExists = fs.existsSync(customCSSPath);
if (customCSSExists) {
fs.readdir(customCSSPath, function(error, files) {
if (error) {
console.warn("Could not read the custom CSS folder:", error);
} else {
files.forEach(function(file_name) {
custom_css_file_names.push(file_name);
});
}
})
}
function getCustomCSSFileNames() {
return custom_css_file_names;
2024-08-11 18:41:03 +03:30
}
module.exports = {
addProtocol,
CustomError,
2024-10-07 09:08:40 +03:30
dateToUTC,
2024-09-12 14:26:39 +03:30
deleteCurrentToken,
2024-08-11 18:41:03 +03:30
generateId,
2025-01-15 11:45:53 +03:30
getCustomCSSFileNames,
2024-08-11 18:41:03 +03:30
getDifferenceFunction,
getInitStats,
2024-08-21 21:22:59 +03:30
getShortURL,
2024-09-09 18:43:12 +03:30
getStatsPeriods,
2024-08-11 18:41:03 +03:30
isAdmin,
2024-11-19 07:58:57 +03:30
parseBooleanQuery,
2024-10-07 09:08:40 +03:30
parseDatetime,
parseTimestamps,
2024-08-11 18:41:03 +03:30
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-09-11 20:51:14 +02:00
...knexUtils,
2024-08-11 18:41:03 +03:30
}